New:Socket for Asana Is Now Available.Learn more
Get Started

@ultimat3/admin

Package Overview
Dependencies
Maintainers
1
Versions
26
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ultimat3/admin - npm Package Compare versions

Comparing version
10.0.0
to
11.0.0
+2
-2
CLAUDE.md

@@ -10,4 +10,4 @@ # @ultimat3/admin — boundary

- **One authz.** `AdminAuthz` (`authz.ts`) is the only decision path. `action-gate.ts`, `crud.ts` and `page-guard.tsx` call `decideAll`; views render what the gate returned. Never add a second check in a view or an MCP handler.
- **The subject carries the tenant and the loaded row.** `AdminActor.orgId` reaches `policyActor` and `AdminSubject.row` reaches `evaluate`, so an org-scoped or ownership rule can fire at all. It could not: every admin decision was evaluated with `actor.orgId === undefined` and `row === null`, so a role-only rule allowed and the coarse `admin:read` + `<entity>:read` pair was the only gate on a row — while `adminList`/`adminSearch` add no tenant predicate of their own. `adminDetail`, `adminUpdate` and `adminDestroy` load the row BEFORE the guard, the shape `packages/action/src/invoke.ts` uses; `undefined` means "not loaded" and is left off the subject, `null` means "looked and found none" and fails a row rule closed. Pinned in `policy-subject.test.ts`, which asserts the SUBJECT a policy receives — `staticAuthz` is a grant-list stub and no suite driving it can see any of this.
- **The MCP caller is the ambient actor for the whole call.** `mcp.ts` wraps `callAdminTool` in a child context (or a fresh root one over stdio, where there is no surrounding request), the same rule `@ultimat3/mcp`'s `app-tool.ts` states. `AdminApp.ctx()` builds a plain `CrudCtx` and touches nothing async-local, so everything deriving from `tryUseContext()` — entity's tenant guard, the query cache authority, the jit-preload store — read the TRANSPORT's actor: an agent token authorized as agent X while the repo reads ran as the cookie user's tenant, and over stdio `actorTenant` was `undefined` and `assertRowTenant` a no-op. Pinned in `mcp-context.test.ts`.
- **The subject carries the tenant and the loaded row.** `AdminActor.orgId` reaches `policyActor` and `AdminSubject.row` reaches `evaluate`, so an org-scoped or ownership rule can fire at all. It could not: every admin decision was evaluated with `actor.orgId === undefined` and `row === null`, so a role-only rule allowed and the coarse `admin:read` + `<entity>:read` pair was the only gate on a row — while `adminList`/`adminSearch` add no tenant predicate of their own. `adminDetail`, `adminUpdate` and `adminDestroy` load the row BEFORE the guard, the shape `packages/action/src/invoke.ts` uses; `undefined` means "not loaded" and is left off the subject, `null` means "looked and found none" and fails a row rule closed. Pinned in `policy-bridge.test.ts`, which asserts the SUBJECT a policy receives — `staticAuthz` is a grant-list stub and no suite driving it can see any of this.
- **The MCP caller is the ambient actor for the whole call.** `mcp.ts` wraps `callAdminTool` in a child context (or a fresh root one over stdio, where there is no surrounding request), the same rule `@ultimat3/mcp`'s `app-tool.ts` states. `AdminApp.ctx()` builds a plain `CrudCtx` and touches nothing async-local, so everything deriving from `tryUseContext()` — entity's tenant guard, the query cache authority, the jit-preload store — read the TRANSPORT's actor: an agent token authorized as agent X while the repo reads ran as the cookie user's tenant, and over stdio `actorTenant` was `undefined` and `assertRowTenant` a no-op. Pinned in `mcp-context.test.ts`. **And the caller's TENANT rides both hops** — `resolveToken` mints the `Actor`, `adminActorOf` rebuilds the `AdminActor` from it, and each dropped `orgId`, so every `AdminAuthz` decision over `POST /mcp` was made with `actor.orgId === undefined` while the same app's UI path saw the real org. Pinned in `mcp-tenant.test.ts`, which asserts the org an authz decision RECEIVES, over `route.handle` — the catalog filter and the call both.
- **A custom page's guard is composed, never written.** `pages.ts` turns a `pages:` entry into an `AdminRoute` carrying `[admin:read, …declared]`; `routes.ts` is the only thing that may hand a page component to a router and it hands the `guardedPage()` wrapper, with `permissions[0]` already in `defineRoute({ policy })`. `AdminPageProps.ctx` is required by the type so the wrapper cannot be bypassed by calling the component directly. An empty permission list is `X_ADMIN_PAGE_UNGUARDED` at `defineAdmin` time — the one place an unauthenticated admin screen could have been born.

@@ -14,0 +14,0 @@ - **One `AdminAction.name`, one handler.** The name is the MCP tool name (`admin.action.<name>`), the default label key, AND the key `callAdminTool` resolves a handler by — three addresses, one string. Two actions sharing it is `X_ADMIN_ACTION_DUPLICATE` at `defineAdmin` time (`admin.ts`, `mcp.test.ts`), not at the first agent call: `.find()` on a name dispatches to whichever resource came first, which is a call that SUCCEEDS against the wrong action and reports nothing. Refused at declaration rather than in `adminMcp()` because an app that renders the dashboard and never wires MCP has the same two broken label keys and the same ambiguous dispatch. The framework's own examples already qualify the name with the entity (`post.publish`) — the `fix:` line is that convention, made into the instruction. The same object attached through both `actions:` and `resources[e].actions` is one action, not two: identity, not name, is what "already seen" means.

{
"name": "@ultimat3/admin",
"version": "10.0.0",
"version": "11.0.0",
"description": "Two dashboards: the /_x framework dev panels and the generated, AI-first app admin",

@@ -35,18 +35,18 @@ "license": "MIT",

"dependencies": {
"@ultimat3/action": "10.0.0",
"@ultimat3/ai": "10.0.0",
"@ultimat3/cache": "10.0.0",
"@ultimat3/core": "10.0.0",
"@ultimat3/db": "10.0.0",
"@ultimat3/entity": "10.0.0",
"@ultimat3/i18n": "10.0.0",
"@ultimat3/jobs": "10.0.0",
"@ultimat3/mcp": "10.0.0",
"@ultimat3/money": "10.0.0",
"@ultimat3/policy": "10.0.0",
"@ultimat3/query": "10.0.0",
"@ultimat3/render": "10.0.0",
"@ultimat3/schema": "10.0.0",
"@ultimat3/ui": "10.0.0"
"@ultimat3/action": "11.0.0",
"@ultimat3/ai": "11.0.0",
"@ultimat3/cache": "11.0.0",
"@ultimat3/core": "11.0.0",
"@ultimat3/db": "11.0.0",
"@ultimat3/entity": "11.0.0",
"@ultimat3/i18n": "11.0.0",
"@ultimat3/jobs": "11.0.0",
"@ultimat3/mcp": "11.0.0",
"@ultimat3/money": "11.0.0",
"@ultimat3/policy": "11.0.0",
"@ultimat3/query": "11.0.0",
"@ultimat3/render": "11.0.0",
"@ultimat3/schema": "11.0.0",
"@ultimat3/ui": "11.0.0"
}
}

@@ -139,2 +139,8 @@ // Mounts the /_x panels — and refuses to mount in production, because these panels print

});
// A rejected import (a transient resolution failure) must not be memoised, or `/_x` is
// unstyled for the life of the process — the `jwks.ts` inflight pattern.
stylePromise = stylePromise.catch((error: unknown) => {
stylePromise = undefined;
throw error;
});
return stylePromise;

@@ -141,0 +147,0 @@ }

@@ -213,3 +213,10 @@ // The AI-first surface: the admin's resources and actions as MCP tools, wired through

* `caller.actor` is whatever `resolveToken` returned, so the identity the tool runs as is the
* one the session authenticated as — id and roles are all authz reads.
* one the session authenticated as — id, roles and the TENANT are the authz reads.
*
* `orgId` rides the whole way or an org-scoped rule cannot fire: this hop rebuilt an `AdminActor`
* from id and roles alone, so every `AdminAuthz` decision over `POST /mcp` was evaluated with
* `actor.orgId === undefined` while the same app's UI path saw the real org — and
* `adminList`/`adminSearch` add no tenant predicate of their own. Absent stays ABSENT rather than
* becoming an explicit `undefined` key: `policy-bridge.ts` hands it to `userActor`, where
* "single-tenant app" and "we dropped it" must not be the same value by accident.
*/

@@ -219,2 +226,3 @@ const adminActorOf = (caller: McpCaller): AdminActor => ({

roles: caller.actor.roles,
...(caller.actor.orgId === undefined ? {} : { orgId: caller.actor.orgId }),
});

@@ -348,7 +356,15 @@

// that distinguishes agents from people keeps working on this surface.
//
// `orgId` is carried because this is the FIRST of the two hops between the app's resolver
// and an authz decision (`adminActorOf` is the second): dropped here, the tenant is gone
// before any tool, catalog filter or child context can see it, and nothing downstream can
// put it back.
return actor === null
? null
: { actor: agentActor({ id: actor.id, roles: actor.roles ?? [] }), scopes: new Set() };
: {
actor: agentActor({ id: actor.id, roles: actor.roles ?? [], orgId: actor.orgId }),
scopes: new Set(),
};
},
});
}