@fuzdev/fuz_app 0.105.0 → 0.106.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +2 -1
- package/dist/actions/perform_action.d.ts +8 -0
- package/dist/actions/perform_action.d.ts.map +1 -1
- package/dist/actions/perform_action.js +30 -12
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +27 -3
- package/dist/auth/account_action_specs.d.ts +28 -5
- package/dist/auth/account_action_specs.d.ts.map +1 -1
- package/dist/auth/account_action_specs.js +19 -6
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +15 -2
- package/dist/auth/account_schema.d.ts +13 -0
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/account_schema.js +8 -1
- package/dist/auth/api_token_queries.d.ts +2 -1
- package/dist/auth/api_token_queries.d.ts.map +1 -1
- package/dist/auth/api_token_queries.js +21 -5
- package/dist/auth/audit_log_routes.d.ts.map +1 -1
- package/dist/auth/audit_log_routes.js +9 -1
- package/dist/auth/bearer_auth.d.ts.map +1 -1
- package/dist/auth/bearer_auth.js +6 -1
- package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
- package/dist/auth/daemon_token_middleware.js +5 -1
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +33 -0
- package/dist/auth/request_context.d.ts +18 -0
- package/dist/auth/request_context.d.ts.map +1 -1
- package/dist/auth/request_context.js +27 -1
- package/dist/auth/role_grant_offer_action_specs.d.ts +12 -0
- package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_action_specs.js +12 -1
- package/dist/auth/self_service_role_action_specs.d.ts +7 -0
- package/dist/auth/self_service_role_action_specs.d.ts.map +1 -1
- package/dist/auth/self_service_role_action_specs.js +7 -1
- package/dist/auth/token_scope.d.ts +155 -0
- package/dist/auth/token_scope.d.ts.map +1 -0
- package/dist/auth/token_scope.js +190 -0
- package/dist/db/CLAUDE.md +1 -1
- package/dist/hono_context.d.ts +23 -0
- package/dist/hono_context.d.ts.map +1 -1
- package/dist/hono_context.js +14 -0
- package/dist/http/error_schemas.d.ts +12 -0
- package/dist/http/error_schemas.d.ts.map +1 -1
- package/dist/http/error_schemas.js +12 -0
- package/dist/server/serve_fact_route.d.ts.map +1 -1
- package/dist/server/serve_fact_route.js +9 -1
- package/dist/server/x_accel.d.ts +2 -4
- package/dist/server/x_accel.d.ts.map +1 -1
- package/dist/server/x_accel.js +2 -4
- package/dist/testing/admin_integration.js +2 -2
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +2 -1
- package/dist/testing/audit_completeness.js +1 -1
- package/dist/testing/cross_backend/conformance_case.d.ts +23 -0
- package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_case.js +21 -0
- package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_table.js +58 -5
- package/dist/testing/cross_backend/expected_schema.json +1 -0
- package/dist/testing/cross_backend/setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/setup.js +5 -1
- package/dist/testing/integration.js +3 -3
- package/dist/ui/account_sessions_state.svelte.d.ts +3 -3
- package/dist/ui/account_sessions_state.svelte.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/server/fact_write.d.ts +0 -32
- package/dist/server/fact_write.d.ts.map +0 -1
- package/dist/server/fact_write.js +0 -57
- package/dist/server/validate_nginx.d.ts +0 -34
- package/dist/server/validate_nginx.d.ts.map +0 -1
- package/dist/server/validate_nginx.js +0 -169
|
@@ -13,7 +13,8 @@ import { DEV } from 'esm-env';
|
|
|
13
13
|
import {} from '../runtime/deps.js';
|
|
14
14
|
import { write_file_atomic } from '../runtime/fs.js';
|
|
15
15
|
import { get_app_dir } from '../cli/config.js';
|
|
16
|
-
import { ACCOUNT_ID_KEY, AUTH_API_TOKEN_ID_KEY, CREDENTIAL_TYPE_KEY } from '../hono_context.js';
|
|
16
|
+
import { ACCOUNT_ID_KEY, AUTH_API_TOKEN_ID_KEY, CREDENTIAL_TYPE_KEY, TOKEN_SCOPE_KEY } from '../hono_context.js';
|
|
17
|
+
import { token_scope_full } from './token_scope.js';
|
|
17
18
|
import { is_browser_context } from '../http/origin.js';
|
|
18
19
|
import { query_role_grant_find_account_id_for_role } from './role_grant_queries.js';
|
|
19
20
|
import { ROLE_KEEPER } from './role_schema.js';
|
|
@@ -232,6 +233,9 @@ export const create_daemon_token_middleware = (state, deps, log) => {
|
|
|
232
233
|
}
|
|
233
234
|
c.set(ACCOUNT_ID_KEY, state.keeper_account_id);
|
|
234
235
|
c.set(CREDENTIAL_TYPE_KEY, 'daemon_token');
|
|
236
|
+
// Singular, filesystem-proved, keeper-bound — no minting step at which a
|
|
237
|
+
// narrowing could have been chosen.
|
|
238
|
+
c.set(TOKEN_SCOPE_KEY, token_scope_full());
|
|
235
239
|
c.set(AUTH_API_TOKEN_ID_KEY, null);
|
|
236
240
|
await next();
|
|
237
241
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrations.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/migrations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AA+BH,OAAO,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEtE,wDAAwD;AACxD,eAAO,MAAM,wBAAwB,aAAa,CAAC;AAEnD;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,EAAE,aAAa,CAAC,MAAM,CAA8B,CAAC;AAE/F;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,SAAS,
|
|
1
|
+
{"version":3,"file":"migrations.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/migrations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AA+BH,OAAO,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEtE,wDAAwD;AACxD,eAAO,MAAM,wBAAwB,aAAa,CAAC;AAEnD;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,EAAE,aAAa,CAAC,MAAM,CAA8B,CAAC;AAE/F;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,SAAS,CA6H5C,CAAC;AAEF,wDAAwD;AACxD,eAAO,MAAM,iBAAiB,EAAE,kBAG/B,CAAC"}
|
package/dist/auth/migrations.js
CHANGED
|
@@ -153,6 +153,39 @@ export const auth_migrations = [
|
|
|
153
153
|
ON role_grant (actor_id, role, scope_id)
|
|
154
154
|
WHERE revoked_at IS NULL`);
|
|
155
155
|
}
|
|
156
|
+
},
|
|
157
|
+
// v2: per-credential token scoping — `api_token.scope JSONB NOT NULL`.
|
|
158
|
+
//
|
|
159
|
+
// Name must match the Rust twin's migration byte for byte
|
|
160
|
+
// (`fuz_auth/src/migrations.rs`) or the `_testing_migration_tracker` parity
|
|
161
|
+
// gate fails and the swap-freely invariant breaks.
|
|
162
|
+
//
|
|
163
|
+
// Three statements, and the middle one is the whole compatibility story:
|
|
164
|
+
//
|
|
165
|
+
// 1. Add the column nullable, so the ALTER is instant on a populated table.
|
|
166
|
+
// 2. Backfill every pre-existing token as full authority, flagged
|
|
167
|
+
// `grandfathered` — tokens minted before scoping existed keep working,
|
|
168
|
+
// but the debt is a *value in the row*, not an absence. That distinction
|
|
169
|
+
// is the point: it stays distinguishable from a deliberately minted full
|
|
170
|
+
// token, so the list can label it and an operator can count what needs
|
|
171
|
+
// re-minting.
|
|
172
|
+
// 3. `SET NOT NULL`, which is what makes "unscoped" unrepresentable from
|
|
173
|
+
// here on. A missing scope is exactly the permissive default that made
|
|
174
|
+
// the 2026-02 `scope` column a false promise; after this statement the
|
|
175
|
+
// database cannot hold one.
|
|
176
|
+
//
|
|
177
|
+
// The backfilled literal is pinned by the Rust twin's migration test —
|
|
178
|
+
// `parse_token_scope` is fail-closed, so a drift between this literal and
|
|
179
|
+
// the parser stops every pre-scoping token loudly rather than widening it.
|
|
180
|
+
{
|
|
181
|
+
name: 'api_token_scope',
|
|
182
|
+
up: async (db) => {
|
|
183
|
+
await db.query('ALTER TABLE api_token ADD COLUMN IF NOT EXISTS scope JSONB');
|
|
184
|
+
await db.query(`UPDATE api_token
|
|
185
|
+
SET scope = '{"version":1,"kind":"full","grandfathered":true}'::jsonb
|
|
186
|
+
WHERE scope IS NULL`);
|
|
187
|
+
await db.query('ALTER TABLE api_token ALTER COLUMN scope SET NOT NULL');
|
|
188
|
+
}
|
|
156
189
|
}
|
|
157
190
|
];
|
|
158
191
|
/** Pre-composed migration namespace for auth tables. */
|
|
@@ -38,6 +38,7 @@ import type { Context, MiddlewareHandler } from 'hono';
|
|
|
38
38
|
import type { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
39
39
|
import { type Account, type Actor, type RoleGrant } from './account_schema.js';
|
|
40
40
|
import type { QueryDeps } from '../db/query_deps.js';
|
|
41
|
+
import { type TokenSurface } from './token_scope.js';
|
|
41
42
|
import type { RouteSpec } from '../http/route_spec.js';
|
|
42
43
|
import { type RouteAuth } from '../http/auth_shape.js';
|
|
43
44
|
import { ERROR_ACTOR_REQUIRED, ERROR_ACTOR_NOT_ON_ACCOUNT, ERROR_NO_ACTORS_ON_ACCOUNT, ERROR_ACCOUNT_VANISHED } from '../http/error_schemas.js';
|
|
@@ -229,6 +230,23 @@ export declare const resolve_acting_actor: (deps: QueryDeps, account_id: string,
|
|
|
229
230
|
* @mutates Hono context - sets `ACCOUNT_ID_KEY`, `CREDENTIAL_TYPE_KEY`, `AUTH_SESSION_TOKEN_HASH_KEY`, and `AUTH_API_TOKEN_ID_KEY`
|
|
230
231
|
*/
|
|
231
232
|
export declare const create_request_context_middleware: (deps: QueryDeps, log: Logger, session_context_key?: string) => MiddlewareHandler;
|
|
233
|
+
/**
|
|
234
|
+
* Refuse a non-RPC spine surface to a narrowed token — **rule 3**, in one place.
|
|
235
|
+
*
|
|
236
|
+
* Called by every surface rule 3 covers: the bare-hash fact read, the audit SSE
|
|
237
|
+
* stream, and the WS upgrade (as pre-upgrade middleware, since
|
|
238
|
+
* `upgradeWebSocket`'s callback cannot produce a denial).
|
|
239
|
+
*
|
|
240
|
+
* An unset `TOKEN_SCOPE_KEY` is the anonymous caller, who holds no credential to
|
|
241
|
+
* narrow, so it passes. Sharing that branch is the point — hand-rolled per
|
|
242
|
+
* surface it is one `&&` from either refusing every anonymous read or admitting
|
|
243
|
+
* an authenticated caller whose middleware forgot to set the key.
|
|
244
|
+
*
|
|
245
|
+
* @param c - the request context, after the auth middleware chain
|
|
246
|
+
* @param surface - the surface being gated; names itself in the denial
|
|
247
|
+
* @returns the 403 to return, or `null` when the caller may proceed
|
|
248
|
+
*/
|
|
249
|
+
export declare const token_scope_surface_denial: (c: Context, surface: TokenSurface) => Response | null;
|
|
232
250
|
/**
|
|
233
251
|
* Middleware that requires authentication.
|
|
234
252
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"request_context.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/request_context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AACvD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,EACN,KAAK,OAAO,EACZ,KAAK,KAAK,EAEV,KAAK,SAAS,EACd,MAAM,qBAAqB,CAAC;AAY7B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"request_context.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/request_context.ts"],"names":[],"mappings":"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"}
|
|
@@ -38,7 +38,8 @@ import { is_role_grant_active } from './account_schema.js';
|
|
|
38
38
|
import { hash_session_token, session_touch_fire_and_forget, query_session_get_valid } from './session_queries.js';
|
|
39
39
|
import { query_account_by_id, query_actor_by_id, query_active_actors_by_account } from './account_queries.js';
|
|
40
40
|
import { query_role_grant_find_active_for_actor } from './role_grant_queries.js';
|
|
41
|
-
import { ACCOUNT_ID_KEY, AUTH_API_TOKEN_ID_KEY, CREDENTIAL_TYPE_KEY, TEST_CONTEXT_PRESET_KEY } from '../hono_context.js';
|
|
41
|
+
import { ACCOUNT_ID_KEY, AUTH_API_TOKEN_ID_KEY, CREDENTIAL_TYPE_KEY, TOKEN_SCOPE_KEY, TEST_CONTEXT_PRESET_KEY } from '../hono_context.js';
|
|
42
|
+
import { token_scope_admits_non_rpc, token_scope_full, token_scope_surface_denied_body } from './token_scope.js';
|
|
42
43
|
import { is_public_auth, needs_actor } from '../http/auth_shape.js';
|
|
43
44
|
import { ERROR_AUTHENTICATION_REQUIRED, ERROR_INSUFFICIENT_PERMISSIONS, ERROR_CREDENTIAL_TYPE_REQUIRED, ERROR_ACTOR_REQUIRED, ERROR_ACTOR_NOT_ON_ACCOUNT, ERROR_NO_ACTORS_ON_ACCOUNT, ERROR_ACCOUNT_VANISHED } from '../http/error_schemas.js';
|
|
44
45
|
/** Hono context variable name for the request context. */
|
|
@@ -230,12 +231,37 @@ export const create_request_context_middleware = (deps, log, session_context_key
|
|
|
230
231
|
}
|
|
231
232
|
c.set(ACCOUNT_ID_KEY, session.account_id);
|
|
232
233
|
c.set(CREDENTIAL_TYPE_KEY, 'session');
|
|
234
|
+
// A session is full account authority by construction.
|
|
235
|
+
c.set(TOKEN_SCOPE_KEY, token_scope_full());
|
|
233
236
|
c.set(AUTH_SESSION_TOKEN_HASH_KEY, token_hash);
|
|
234
237
|
// Touch session (fire-and-forget, don't block the request)
|
|
235
238
|
void session_touch_fire_and_forget(deps, token_hash, c.var.pending_effects, log);
|
|
236
239
|
await next();
|
|
237
240
|
};
|
|
238
241
|
};
|
|
242
|
+
/**
|
|
243
|
+
* Refuse a non-RPC spine surface to a narrowed token — **rule 3**, in one place.
|
|
244
|
+
*
|
|
245
|
+
* Called by every surface rule 3 covers: the bare-hash fact read, the audit SSE
|
|
246
|
+
* stream, and the WS upgrade (as pre-upgrade middleware, since
|
|
247
|
+
* `upgradeWebSocket`'s callback cannot produce a denial).
|
|
248
|
+
*
|
|
249
|
+
* An unset `TOKEN_SCOPE_KEY` is the anonymous caller, who holds no credential to
|
|
250
|
+
* narrow, so it passes. Sharing that branch is the point — hand-rolled per
|
|
251
|
+
* surface it is one `&&` from either refusing every anonymous read or admitting
|
|
252
|
+
* an authenticated caller whose middleware forgot to set the key.
|
|
253
|
+
*
|
|
254
|
+
* @param c - the request context, after the auth middleware chain
|
|
255
|
+
* @param surface - the surface being gated; names itself in the denial
|
|
256
|
+
* @returns the 403 to return, or `null` when the caller may proceed
|
|
257
|
+
*/
|
|
258
|
+
export const token_scope_surface_denial = (c, surface) => {
|
|
259
|
+
const scope = c.get(TOKEN_SCOPE_KEY);
|
|
260
|
+
if (scope && !token_scope_admits_non_rpc(scope)) {
|
|
261
|
+
return c.json(token_scope_surface_denied_body(surface), 403);
|
|
262
|
+
}
|
|
263
|
+
return null;
|
|
264
|
+
};
|
|
239
265
|
/**
|
|
240
266
|
* Middleware that requires authentication.
|
|
241
267
|
*
|
|
@@ -284,6 +284,17 @@ export declare const role_grant_offer_create_action_spec: {
|
|
|
284
284
|
error_reasons: ("role_grant_offer_self_target" | "role_grant_offer_role_not_grantable" | "role_grant_offer_not_authorized" | "role_grant_offer_actor_account_mismatch")[];
|
|
285
285
|
rate_limit: "account";
|
|
286
286
|
};
|
|
287
|
+
/**
|
|
288
|
+
* `credential_types: ['session']` — see `docs/security.md` §Credential-channel
|
|
289
|
+
* gating. The only offer verb that moves the *caller's own* authority:
|
|
290
|
+
* accepting a pending admin offer over a leaked API token is a privilege pivot
|
|
291
|
+
* in the same family the credential-lifecycle mutations are gated to prevent
|
|
292
|
+
* (the token is account-wide and non-expiring, and the resulting role_grant
|
|
293
|
+
* outlives revoking it). Consent inherently belongs to a human at a browser,
|
|
294
|
+
* and no automation accepts an offer on its own behalf. The sibling verbs stay
|
|
295
|
+
* ungated deliberately — `_create` is the offering side, and `_decline` /
|
|
296
|
+
* `_retract` destroy a pending offer and confer nothing.
|
|
297
|
+
*/
|
|
287
298
|
export declare const role_grant_offer_accept_action_spec: {
|
|
288
299
|
method: string;
|
|
289
300
|
kind: "request_response";
|
|
@@ -291,6 +302,7 @@ export declare const role_grant_offer_accept_action_spec: {
|
|
|
291
302
|
auth: {
|
|
292
303
|
account: "required";
|
|
293
304
|
actor: "required";
|
|
305
|
+
credential_types: string[];
|
|
294
306
|
};
|
|
295
307
|
side_effects: true;
|
|
296
308
|
input: z.ZodObject<{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role_grant_offer_action_specs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/role_grant_offer_action_specs.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"role_grant_offer_action_specs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/role_grant_offer_action_specs.ts"],"names":[],"mappings":"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"}
|
|
@@ -211,11 +211,22 @@ export const role_grant_offer_create_action_spec = {
|
|
|
211
211
|
],
|
|
212
212
|
rate_limit: 'account'
|
|
213
213
|
};
|
|
214
|
+
/**
|
|
215
|
+
* `credential_types: ['session']` — see `docs/security.md` §Credential-channel
|
|
216
|
+
* gating. The only offer verb that moves the *caller's own* authority:
|
|
217
|
+
* accepting a pending admin offer over a leaked API token is a privilege pivot
|
|
218
|
+
* in the same family the credential-lifecycle mutations are gated to prevent
|
|
219
|
+
* (the token is account-wide and non-expiring, and the resulting role_grant
|
|
220
|
+
* outlives revoking it). Consent inherently belongs to a human at a browser,
|
|
221
|
+
* and no automation accepts an offer on its own behalf. The sibling verbs stay
|
|
222
|
+
* ungated deliberately — `_create` is the offering side, and `_decline` /
|
|
223
|
+
* `_retract` destroy a pending offer and confer nothing.
|
|
224
|
+
*/
|
|
214
225
|
export const role_grant_offer_accept_action_spec = {
|
|
215
226
|
method: 'role_grant_offer_accept',
|
|
216
227
|
kind: 'request_response',
|
|
217
228
|
initiator: 'frontend',
|
|
218
|
-
auth: { account: 'required', actor: 'required' },
|
|
229
|
+
auth: { account: 'required', actor: 'required', credential_types: ['session'] },
|
|
219
230
|
side_effects: true,
|
|
220
231
|
input: RoleGrantOfferAcceptInput,
|
|
221
232
|
output: RoleGrantOfferAcceptOutput,
|
|
@@ -35,6 +35,12 @@ export type SelfServiceRoleSetOutput = z.infer<typeof SelfServiceRoleSetOutput>;
|
|
|
35
35
|
* `role_grant_create` or `role_grant_revoke` audit row with
|
|
36
36
|
* `self_service: true`. Without the cap, a caller could flap the role in
|
|
37
37
|
* a loop to inflate the audit log and obscure other activity.
|
|
38
|
+
*
|
|
39
|
+
* `credential_types: ['session']` — see `docs/security.md` §Credential-channel
|
|
40
|
+
* gating. Self-toggling a role moves the caller's own authority, the same
|
|
41
|
+
* privilege-pivot shape as `role_grant_offer_accept`. The verb exists to serve
|
|
42
|
+
* a UI affordance, so the bearer channel buys nothing and a leaked
|
|
43
|
+
* account-wide token self-granting an eligible role is what the gate refuses.
|
|
38
44
|
*/
|
|
39
45
|
export declare const self_service_role_set_action_spec: {
|
|
40
46
|
method: string;
|
|
@@ -43,6 +49,7 @@ export declare const self_service_role_set_action_spec: {
|
|
|
43
49
|
auth: {
|
|
44
50
|
account: "required";
|
|
45
51
|
actor: "required";
|
|
52
|
+
credential_types: string[];
|
|
46
53
|
};
|
|
47
54
|
side_effects: true;
|
|
48
55
|
input: z.ZodObject<{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"self_service_role_action_specs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/self_service_role_action_specs.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAC;AAI3E,0FAA0F;AAC1F,eAAO,MAAM,oCAAoC,EAAG,gCAAyC,CAAC;AAE9F,yCAAyC;AACzC,eAAO,MAAM,uBAAuB;;;;kBAOlC,CAAC;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAE9E;;;;GAIG;AACH,eAAO,MAAM,wBAAwB;;;;kBAInC,CAAC;AACH,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAEhF
|
|
1
|
+
{"version":3,"file":"self_service_role_action_specs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/self_service_role_action_specs.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAC;AAI3E,0FAA0F;AAC1F,eAAO,MAAM,oCAAoC,EAAG,gCAAyC,CAAC;AAE9F,yCAAyC;AACzC,eAAO,MAAM,uBAAuB;;;;kBAOlC,CAAC;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAE9E;;;;GAIG;AACH,eAAO,MAAM,wBAAwB;;;;kBAInC,CAAC;AACH,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAEhF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,iCAAiC;;;;;;;;;;;;;;;;;;;;;;;CAYT,CAAC;AAEtC;;;;GAIG;AACH,eAAO,MAAM,kCAAkC,EAAE,aAAa,CAAC,yBAAyB,CAEvF,CAAC"}
|
|
@@ -36,12 +36,18 @@ export const SelfServiceRoleSetOutput = z.strictObject({
|
|
|
36
36
|
* `role_grant_create` or `role_grant_revoke` audit row with
|
|
37
37
|
* `self_service: true`. Without the cap, a caller could flap the role in
|
|
38
38
|
* a loop to inflate the audit log and obscure other activity.
|
|
39
|
+
*
|
|
40
|
+
* `credential_types: ['session']` — see `docs/security.md` §Credential-channel
|
|
41
|
+
* gating. Self-toggling a role moves the caller's own authority, the same
|
|
42
|
+
* privilege-pivot shape as `role_grant_offer_accept`. The verb exists to serve
|
|
43
|
+
* a UI affordance, so the bearer channel buys nothing and a leaked
|
|
44
|
+
* account-wide token self-granting an eligible role is what the gate refuses.
|
|
39
45
|
*/
|
|
40
46
|
export const self_service_role_set_action_spec = {
|
|
41
47
|
method: 'self_service_role_set',
|
|
42
48
|
kind: 'request_response',
|
|
43
49
|
initiator: 'frontend',
|
|
44
|
-
auth: { account: 'required', actor: 'required' },
|
|
50
|
+
auth: { account: 'required', actor: 'required', credential_types: ['session'] },
|
|
45
51
|
side_effects: true,
|
|
46
52
|
input: SelfServiceRoleSetInput,
|
|
47
53
|
output: SelfServiceRoleSetOutput,
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `TokenScope` — the per-credential authority narrowing stored on
|
|
3
|
+
* `api_token.scope`.
|
|
4
|
+
*
|
|
5
|
+
* Twin of the Rust spine's `fuz_auth::token_scope`. The stored document is the
|
|
6
|
+
* wire contract; both spines read the same `api_token.scope` JSONB, so the
|
|
7
|
+
* `kind` strings, the field names, and the version constant must match exactly.
|
|
8
|
+
*
|
|
9
|
+
* ## The whole vocabulary
|
|
10
|
+
*
|
|
11
|
+
* Two variants, and three rules:
|
|
12
|
+
*
|
|
13
|
+
* 1. **`full` must be spelled at mint.** `account_token_create` takes a required
|
|
14
|
+
* `scope`, the column is `NOT NULL`, and session / daemon-token credentials
|
|
15
|
+
* resolve to `full` by construction. There is no absent state — which is the
|
|
16
|
+
* whole point. The 2026-02 `scope` column stored `full` / `read` / `write`
|
|
17
|
+
* and nothing enforced them; the reason that was worse than nothing is that
|
|
18
|
+
* the absent/unenforced case was indistinguishable from a decision.
|
|
19
|
+
* 2. **`methods` denies every method it does not list.** An empty list is legal
|
|
20
|
+
* and renders the token inert for RPC — unlike an empty `credential_types`
|
|
21
|
+
* allowlist, which is rejected because it renders an *action* unreachable.
|
|
22
|
+
* 3. **A narrowed token is RPC-only.** `methods` denies every non-RPC spine
|
|
23
|
+
* surface outright — the db-admin browser, the bare-hash fact read, the
|
|
24
|
+
* audit SSE stream, and the WS upgrade. See `token_scope_admits_non_rpc`.
|
|
25
|
+
*
|
|
26
|
+
* @module
|
|
27
|
+
*/
|
|
28
|
+
import { z } from 'zod';
|
|
29
|
+
/** Current `TokenScope` document version. Must match the Rust `TOKEN_SCOPE_VERSION`. */
|
|
30
|
+
export declare const TOKEN_SCOPE_VERSION = 1;
|
|
31
|
+
/**
|
|
32
|
+
* Serialized-document cap. Matches the Rust `MAX_TOKEN_SCOPE_BYTES` and
|
|
33
|
+
* fuz_forge's `MAX_POLICY_BYTES`.
|
|
34
|
+
*/
|
|
35
|
+
export declare const MAX_TOKEN_SCOPE_BYTES: number;
|
|
36
|
+
/**
|
|
37
|
+
* The stored `api_token.scope` document.
|
|
38
|
+
*
|
|
39
|
+
* `grandfathered` is a fact about the *row* — the `api_token_scope` migration
|
|
40
|
+
* wrote it; no minter chose it — that only the token list cares about. The
|
|
41
|
+
* enforcement path never branches on it, which is what stops "grandfathered"
|
|
42
|
+
* from becoming a third authority level.
|
|
43
|
+
*
|
|
44
|
+
* Strict on both arms so a v2 document carrying a grant arm a v1 reader doesn't
|
|
45
|
+
* understand is refused rather than parsed as permissively-narrower-than-stored.
|
|
46
|
+
* The discriminated union also refuses the contradiction
|
|
47
|
+
* `{kind: 'full', methods: [...]}`.
|
|
48
|
+
*/
|
|
49
|
+
export declare const TokenScope: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
50
|
+
version: z.ZodLiteral<1>;
|
|
51
|
+
kind: z.ZodLiteral<"full">;
|
|
52
|
+
grandfathered: z.ZodOptional<z.ZodBoolean>;
|
|
53
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
54
|
+
version: z.ZodLiteral<1>;
|
|
55
|
+
kind: z.ZodLiteral<"methods">;
|
|
56
|
+
methods: z.ZodArray<z.ZodString>;
|
|
57
|
+
grandfathered: z.ZodOptional<z.ZodBoolean>;
|
|
58
|
+
}, z.core.$strict>], "kind">;
|
|
59
|
+
export type TokenScope = z.infer<typeof TokenScope>;
|
|
60
|
+
/**
|
|
61
|
+
* The `scope` input accepted by `account_token_create` — the stored shape minus
|
|
62
|
+
* the row-owned fields.
|
|
63
|
+
*
|
|
64
|
+
* A minter cannot set `version` or `grandfathered`: the first is stamped by
|
|
65
|
+
* `token_scope_full` / `token_scope_methods`, and the second is the migration's
|
|
66
|
+
* mark for tokens that predate scoping. Letting a caller forge `grandfathered`
|
|
67
|
+
* would turn a tracked debt into a laundered one.
|
|
68
|
+
*/
|
|
69
|
+
export declare const TokenScopeInput: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
70
|
+
kind: z.ZodLiteral<"full">;
|
|
71
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
72
|
+
kind: z.ZodLiteral<"methods">;
|
|
73
|
+
methods: z.ZodArray<z.ZodString>;
|
|
74
|
+
}, z.core.$strict>], "kind">;
|
|
75
|
+
export type TokenScopeInput = z.infer<typeof TokenScopeInput>;
|
|
76
|
+
/** A deliberately-minted full-authority scope. */
|
|
77
|
+
export declare const token_scope_full: () => TokenScope;
|
|
78
|
+
/** A narrowed scope over `methods`. */
|
|
79
|
+
export declare const token_scope_methods: (methods: Array<string>) => TokenScope;
|
|
80
|
+
/** Build the stored scope from a mint-time input. */
|
|
81
|
+
export declare const token_scope_from_input: (input: TokenScopeInput) => TokenScope;
|
|
82
|
+
/**
|
|
83
|
+
* Does `scope` admit dispatching `method`?
|
|
84
|
+
*
|
|
85
|
+
* Consulted by `perform_action` between the credential gate and the role gate.
|
|
86
|
+
*/
|
|
87
|
+
export declare const token_scope_admits_method: (scope: TokenScope, method: string) => boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Does `scope` admit a non-RPC spine surface?
|
|
90
|
+
*
|
|
91
|
+
* **Rule 3, and it is the load-bearing one.** A method-name allowlist alone
|
|
92
|
+
* would have been a false promise: the db-admin browser serves paginated rows of
|
|
93
|
+
* any `public` table (including `account.password_hash`, `auth_session`, and
|
|
94
|
+
* `api_token`) plus row `DELETE`, gated on a global role a bearer credential
|
|
95
|
+
* satisfies. A token whose UI badge read "scoped to `cell_get`" could still
|
|
96
|
+
* delete an account row.
|
|
97
|
+
*
|
|
98
|
+
* So a narrowed token reaches no non-RPC surface at all. Strictly more
|
|
99
|
+
* restrictive than naming each surface, no vocabulary to maintain, and one
|
|
100
|
+
* sentence a user can hold: *a scoped token is RPC-only*.
|
|
101
|
+
*/
|
|
102
|
+
export declare const token_scope_admits_non_rpc: (scope: TokenScope) => boolean;
|
|
103
|
+
/** Surfaces gated by rule 3. Capability strings match the Rust `TokenSurface`. */
|
|
104
|
+
export declare const TOKEN_SURFACE_CAPABILITIES: {
|
|
105
|
+
readonly db_admin: "surface:db_admin";
|
|
106
|
+
readonly fact_bare: "surface:fact_bare";
|
|
107
|
+
readonly audit_stream: "surface:audit_stream";
|
|
108
|
+
readonly ws_upgrade: "surface:ws_upgrade";
|
|
109
|
+
};
|
|
110
|
+
export type TokenSurface = keyof typeof TOKEN_SURFACE_CAPABILITIES;
|
|
111
|
+
/**
|
|
112
|
+
* Build the flat denial body a non-RPC surface returns when a narrowed token
|
|
113
|
+
* reaches it: `{error: 'token_scope_required', required_scope: 'surface:<name>'}`
|
|
114
|
+
* at 403.
|
|
115
|
+
*
|
|
116
|
+
* One shape for every surface, matching the Rust
|
|
117
|
+
* `token_scope_surface_denied_response`. These are REST-ish routes whose
|
|
118
|
+
* sibling denials are already flat, so the JSON-RPC envelope stays with the
|
|
119
|
+
* dispatcher's per-method gate and stops there.
|
|
120
|
+
*/
|
|
121
|
+
export declare const token_scope_surface_denied_body: (surface: TokenSurface) => {
|
|
122
|
+
error: "token_scope_required";
|
|
123
|
+
required_scope: string;
|
|
124
|
+
};
|
|
125
|
+
/** Stable label for the token list / UI. */
|
|
126
|
+
export declare const token_scope_label: (scope: TokenScope) => string;
|
|
127
|
+
/**
|
|
128
|
+
* Parse a stored document, enforcing the size cap and the schema.
|
|
129
|
+
*
|
|
130
|
+
* **Fail-closed**: returns `null` on an unreadable document, and every caller on
|
|
131
|
+
* the enforcement path treats `null` as "refuse the credential" rather than
|
|
132
|
+
* "allow everything". fuz_forge's hydrate path deliberately widens on a DB blip
|
|
133
|
+
* — safe there only because a role gate still applies. Here the scope *is* the
|
|
134
|
+
* gate, so widening on a parse failure would hand a narrowed token full
|
|
135
|
+
* authority exactly when something is already wrong.
|
|
136
|
+
*/
|
|
137
|
+
export declare const parse_token_scope: (raw: string) => TokenScope | null;
|
|
138
|
+
/**
|
|
139
|
+
* Serialize for storage, enforcing the size cap.
|
|
140
|
+
*
|
|
141
|
+
* Throws on an over-cap document — the mint path maps that to a 400, matching
|
|
142
|
+
* the Rust side's `TokenScopeError::TooLarge`.
|
|
143
|
+
*/
|
|
144
|
+
export declare const serialize_token_scope: (scope: TokenScope) => string;
|
|
145
|
+
/**
|
|
146
|
+
* Read a stored `api_token.scope` value back into a `TokenScope`, or `null`
|
|
147
|
+
* when the document is unreadable.
|
|
148
|
+
*
|
|
149
|
+
* The DB driver may hand `scope` back already-parsed (JSONB → object) or as
|
|
150
|
+
* text depending on backend, so both are normalized here rather than at each
|
|
151
|
+
* call site. Lives here rather than beside the queries so the pure scope module
|
|
152
|
+
* owns every parse — and so mocking the query module doesn't drag this along.
|
|
153
|
+
*/
|
|
154
|
+
export declare const parse_stored_token_scope: (value: unknown) => TokenScope | null;
|
|
155
|
+
//# sourceMappingURL=token_scope.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token_scope.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/token_scope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,wFAAwF;AACxF,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC;;;GAGG;AACH,eAAO,MAAM,qBAAqB,QAAW,CAAC;AAE9C;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,UAAU;;;;;;;;;4BAYrB,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAEpD;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe;;;;;4BAG1B,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAE9D,kDAAkD;AAClD,eAAO,MAAM,gBAAgB,QAAO,UAGlC,CAAC;AAEH,uCAAuC;AACvC,eAAO,MAAM,mBAAmB,GAAI,SAAS,KAAK,CAAC,MAAM,CAAC,KAAG,UAI3D,CAAC;AAEH,qDAAqD;AACrD,eAAO,MAAM,sBAAsB,GAAI,OAAO,eAAe,KAAG,UACgB,CAAC;AAEjF;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,GAAI,OAAO,UAAU,EAAE,QAAQ,MAAM,KAAG,OACtB,CAAC;AAEzD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,0BAA0B,GAAI,OAAO,UAAU,KAAG,OAAgC,CAAC;AAEhG,kFAAkF;AAClF,eAAO,MAAM,0BAA0B;;;;;CAK7B,CAAC;AACX,MAAM,MAAM,YAAY,GAAG,MAAM,OAAO,0BAA0B,CAAC;AAEnE;;;;;;;;;GASG;AACH,eAAO,MAAM,+BAA+B,GAC3C,SAAS,YAAY,KACnB;IAAE,KAAK,EAAE,sBAAsB,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAGxD,CAAC;AAEH,4CAA4C;AAC5C,eAAO,MAAM,iBAAiB,GAAI,OAAO,UAAU,KAAG,MAGrD,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,GAAI,KAAK,MAAM,KAAG,UAAU,GAAG,IAU5D,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GAAI,OAAO,UAAU,KAAG,MAQzD,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,GAAI,OAAO,OAAO,KAAG,UAAU,GAAG,IAQtE,CAAC"}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `TokenScope` — the per-credential authority narrowing stored on
|
|
3
|
+
* `api_token.scope`.
|
|
4
|
+
*
|
|
5
|
+
* Twin of the Rust spine's `fuz_auth::token_scope`. The stored document is the
|
|
6
|
+
* wire contract; both spines read the same `api_token.scope` JSONB, so the
|
|
7
|
+
* `kind` strings, the field names, and the version constant must match exactly.
|
|
8
|
+
*
|
|
9
|
+
* ## The whole vocabulary
|
|
10
|
+
*
|
|
11
|
+
* Two variants, and three rules:
|
|
12
|
+
*
|
|
13
|
+
* 1. **`full` must be spelled at mint.** `account_token_create` takes a required
|
|
14
|
+
* `scope`, the column is `NOT NULL`, and session / daemon-token credentials
|
|
15
|
+
* resolve to `full` by construction. There is no absent state — which is the
|
|
16
|
+
* whole point. The 2026-02 `scope` column stored `full` / `read` / `write`
|
|
17
|
+
* and nothing enforced them; the reason that was worse than nothing is that
|
|
18
|
+
* the absent/unenforced case was indistinguishable from a decision.
|
|
19
|
+
* 2. **`methods` denies every method it does not list.** An empty list is legal
|
|
20
|
+
* and renders the token inert for RPC — unlike an empty `credential_types`
|
|
21
|
+
* allowlist, which is rejected because it renders an *action* unreachable.
|
|
22
|
+
* 3. **A narrowed token is RPC-only.** `methods` denies every non-RPC spine
|
|
23
|
+
* surface outright — the db-admin browser, the bare-hash fact read, the
|
|
24
|
+
* audit SSE stream, and the WS upgrade. See `token_scope_admits_non_rpc`.
|
|
25
|
+
*
|
|
26
|
+
* @module
|
|
27
|
+
*/
|
|
28
|
+
import { z } from 'zod';
|
|
29
|
+
/** Current `TokenScope` document version. Must match the Rust `TOKEN_SCOPE_VERSION`. */
|
|
30
|
+
export const TOKEN_SCOPE_VERSION = 1;
|
|
31
|
+
/**
|
|
32
|
+
* Serialized-document cap. Matches the Rust `MAX_TOKEN_SCOPE_BYTES` and
|
|
33
|
+
* fuz_forge's `MAX_POLICY_BYTES`.
|
|
34
|
+
*/
|
|
35
|
+
export const MAX_TOKEN_SCOPE_BYTES = 4 * 1024;
|
|
36
|
+
/**
|
|
37
|
+
* The stored `api_token.scope` document.
|
|
38
|
+
*
|
|
39
|
+
* `grandfathered` is a fact about the *row* — the `api_token_scope` migration
|
|
40
|
+
* wrote it; no minter chose it — that only the token list cares about. The
|
|
41
|
+
* enforcement path never branches on it, which is what stops "grandfathered"
|
|
42
|
+
* from becoming a third authority level.
|
|
43
|
+
*
|
|
44
|
+
* Strict on both arms so a v2 document carrying a grant arm a v1 reader doesn't
|
|
45
|
+
* understand is refused rather than parsed as permissively-narrower-than-stored.
|
|
46
|
+
* The discriminated union also refuses the contradiction
|
|
47
|
+
* `{kind: 'full', methods: [...]}`.
|
|
48
|
+
*/
|
|
49
|
+
export const TokenScope = z.discriminatedUnion('kind', [
|
|
50
|
+
z.strictObject({
|
|
51
|
+
version: z.literal(TOKEN_SCOPE_VERSION),
|
|
52
|
+
kind: z.literal('full'),
|
|
53
|
+
grandfathered: z.boolean().optional()
|
|
54
|
+
}),
|
|
55
|
+
z.strictObject({
|
|
56
|
+
version: z.literal(TOKEN_SCOPE_VERSION),
|
|
57
|
+
kind: z.literal('methods'),
|
|
58
|
+
methods: z.array(z.string()),
|
|
59
|
+
grandfathered: z.boolean().optional()
|
|
60
|
+
})
|
|
61
|
+
]);
|
|
62
|
+
/**
|
|
63
|
+
* The `scope` input accepted by `account_token_create` — the stored shape minus
|
|
64
|
+
* the row-owned fields.
|
|
65
|
+
*
|
|
66
|
+
* A minter cannot set `version` or `grandfathered`: the first is stamped by
|
|
67
|
+
* `token_scope_full` / `token_scope_methods`, and the second is the migration's
|
|
68
|
+
* mark for tokens that predate scoping. Letting a caller forge `grandfathered`
|
|
69
|
+
* would turn a tracked debt into a laundered one.
|
|
70
|
+
*/
|
|
71
|
+
export const TokenScopeInput = z.discriminatedUnion('kind', [
|
|
72
|
+
z.strictObject({ kind: z.literal('full') }),
|
|
73
|
+
z.strictObject({ kind: z.literal('methods'), methods: z.array(z.string()) })
|
|
74
|
+
]);
|
|
75
|
+
/** A deliberately-minted full-authority scope. */
|
|
76
|
+
export const token_scope_full = () => ({
|
|
77
|
+
version: TOKEN_SCOPE_VERSION,
|
|
78
|
+
kind: 'full'
|
|
79
|
+
});
|
|
80
|
+
/** A narrowed scope over `methods`. */
|
|
81
|
+
export const token_scope_methods = (methods) => ({
|
|
82
|
+
version: TOKEN_SCOPE_VERSION,
|
|
83
|
+
kind: 'methods',
|
|
84
|
+
methods
|
|
85
|
+
});
|
|
86
|
+
/** Build the stored scope from a mint-time input. */
|
|
87
|
+
export const token_scope_from_input = (input) => input.kind === 'full' ? token_scope_full() : token_scope_methods(input.methods);
|
|
88
|
+
/**
|
|
89
|
+
* Does `scope` admit dispatching `method`?
|
|
90
|
+
*
|
|
91
|
+
* Consulted by `perform_action` between the credential gate and the role gate.
|
|
92
|
+
*/
|
|
93
|
+
export const token_scope_admits_method = (scope, method) => scope.kind === 'full' || scope.methods.includes(method);
|
|
94
|
+
/**
|
|
95
|
+
* Does `scope` admit a non-RPC spine surface?
|
|
96
|
+
*
|
|
97
|
+
* **Rule 3, and it is the load-bearing one.** A method-name allowlist alone
|
|
98
|
+
* would have been a false promise: the db-admin browser serves paginated rows of
|
|
99
|
+
* any `public` table (including `account.password_hash`, `auth_session`, and
|
|
100
|
+
* `api_token`) plus row `DELETE`, gated on a global role a bearer credential
|
|
101
|
+
* satisfies. A token whose UI badge read "scoped to `cell_get`" could still
|
|
102
|
+
* delete an account row.
|
|
103
|
+
*
|
|
104
|
+
* So a narrowed token reaches no non-RPC surface at all. Strictly more
|
|
105
|
+
* restrictive than naming each surface, no vocabulary to maintain, and one
|
|
106
|
+
* sentence a user can hold: *a scoped token is RPC-only*.
|
|
107
|
+
*/
|
|
108
|
+
export const token_scope_admits_non_rpc = (scope) => scope.kind === 'full';
|
|
109
|
+
/** Surfaces gated by rule 3. Capability strings match the Rust `TokenSurface`. */
|
|
110
|
+
export const TOKEN_SURFACE_CAPABILITIES = {
|
|
111
|
+
db_admin: 'surface:db_admin',
|
|
112
|
+
fact_bare: 'surface:fact_bare',
|
|
113
|
+
audit_stream: 'surface:audit_stream',
|
|
114
|
+
ws_upgrade: 'surface:ws_upgrade'
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Build the flat denial body a non-RPC surface returns when a narrowed token
|
|
118
|
+
* reaches it: `{error: 'token_scope_required', required_scope: 'surface:<name>'}`
|
|
119
|
+
* at 403.
|
|
120
|
+
*
|
|
121
|
+
* One shape for every surface, matching the Rust
|
|
122
|
+
* `token_scope_surface_denied_response`. These are REST-ish routes whose
|
|
123
|
+
* sibling denials are already flat, so the JSON-RPC envelope stays with the
|
|
124
|
+
* dispatcher's per-method gate and stops there.
|
|
125
|
+
*/
|
|
126
|
+
export const token_scope_surface_denied_body = (surface) => ({
|
|
127
|
+
error: 'token_scope_required',
|
|
128
|
+
required_scope: TOKEN_SURFACE_CAPABILITIES[surface]
|
|
129
|
+
});
|
|
130
|
+
/** Stable label for the token list / UI. */
|
|
131
|
+
export const token_scope_label = (scope) => {
|
|
132
|
+
if (scope.kind === 'full')
|
|
133
|
+
return scope.grandfathered ? 'full (grandfathered)' : 'full';
|
|
134
|
+
return `${scope.methods.length} methods`;
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Parse a stored document, enforcing the size cap and the schema.
|
|
138
|
+
*
|
|
139
|
+
* **Fail-closed**: returns `null` on an unreadable document, and every caller on
|
|
140
|
+
* the enforcement path treats `null` as "refuse the credential" rather than
|
|
141
|
+
* "allow everything". fuz_forge's hydrate path deliberately widens on a DB blip
|
|
142
|
+
* — safe there only because a role gate still applies. Here the scope *is* the
|
|
143
|
+
* gate, so widening on a parse failure would hand a narrowed token full
|
|
144
|
+
* authority exactly when something is already wrong.
|
|
145
|
+
*/
|
|
146
|
+
export const parse_token_scope = (raw) => {
|
|
147
|
+
if (raw.length > MAX_TOKEN_SCOPE_BYTES)
|
|
148
|
+
return null;
|
|
149
|
+
let value;
|
|
150
|
+
try {
|
|
151
|
+
value = JSON.parse(raw);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const parsed = TokenScope.safeParse(value);
|
|
157
|
+
return parsed.success ? parsed.data : null;
|
|
158
|
+
};
|
|
159
|
+
/**
|
|
160
|
+
* Serialize for storage, enforcing the size cap.
|
|
161
|
+
*
|
|
162
|
+
* Throws on an over-cap document — the mint path maps that to a 400, matching
|
|
163
|
+
* the Rust side's `TokenScopeError::TooLarge`.
|
|
164
|
+
*/
|
|
165
|
+
export const serialize_token_scope = (scope) => {
|
|
166
|
+
const json = JSON.stringify(scope);
|
|
167
|
+
if (json.length > MAX_TOKEN_SCOPE_BYTES) {
|
|
168
|
+
throw new Error(`token scope document is ${json.length} bytes, exceeding the ${MAX_TOKEN_SCOPE_BYTES} byte cap`);
|
|
169
|
+
}
|
|
170
|
+
return json;
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* Read a stored `api_token.scope` value back into a `TokenScope`, or `null`
|
|
174
|
+
* when the document is unreadable.
|
|
175
|
+
*
|
|
176
|
+
* The DB driver may hand `scope` back already-parsed (JSONB → object) or as
|
|
177
|
+
* text depending on backend, so both are normalized here rather than at each
|
|
178
|
+
* call site. Lives here rather than beside the queries so the pure scope module
|
|
179
|
+
* owns every parse — and so mocking the query module doesn't drag this along.
|
|
180
|
+
*/
|
|
181
|
+
export const parse_stored_token_scope = (value) => {
|
|
182
|
+
if (typeof value === 'string')
|
|
183
|
+
return parse_token_scope(value);
|
|
184
|
+
// `JSON.stringify(undefined)` returns `undefined`, not a string — so a row
|
|
185
|
+
// missing the column entirely must be caught here rather than reaching
|
|
186
|
+
// `parse_token_scope` and throwing on `.length`. Absent is *unreadable*,
|
|
187
|
+
// which is fail-closed: the caller refuses the credential.
|
|
188
|
+
const json = JSON.stringify(value);
|
|
189
|
+
return json === undefined ? null : parse_token_scope(json);
|
|
190
|
+
};
|
package/dist/db/CLAUDE.md
CHANGED
|
@@ -135,7 +135,7 @@ DO NOTHING`), `_put_fact_refs`, `_get_fact` / `_get_fact_meta` / `_has_fact`
|
|
|
135
135
|
- **`fact_store_errors.ts`** — `PayloadTooLargeError` / `StorageFullError` (+
|
|
136
136
|
`is_enospc_error`) thrown by `put_stream`, for a consumer route's 413 / 507.
|
|
137
137
|
- The read-side fetcher + write/serve plumbing also live under `server/`
|
|
138
|
-
(`file_fact_fetcher.ts`, `
|
|
138
|
+
(`file_fact_fetcher.ts`, `serve_fact_route.ts`).
|
|
139
139
|
|
|
140
140
|
### Migration namespace order
|
|
141
141
|
|