@fuzdev/fuz_app 0.105.0 → 0.106.0

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.
Files changed (70) hide show
  1. package/dist/actions/action_rpc.d.ts.map +1 -1
  2. package/dist/actions/action_rpc.js +2 -1
  3. package/dist/actions/perform_action.d.ts +8 -0
  4. package/dist/actions/perform_action.d.ts.map +1 -1
  5. package/dist/actions/perform_action.js +30 -12
  6. package/dist/actions/register_action_ws.d.ts.map +1 -1
  7. package/dist/actions/register_action_ws.js +27 -3
  8. package/dist/auth/account_action_specs.d.ts +28 -5
  9. package/dist/auth/account_action_specs.d.ts.map +1 -1
  10. package/dist/auth/account_action_specs.js +19 -6
  11. package/dist/auth/account_actions.d.ts.map +1 -1
  12. package/dist/auth/account_actions.js +15 -2
  13. package/dist/auth/account_schema.d.ts +13 -0
  14. package/dist/auth/account_schema.d.ts.map +1 -1
  15. package/dist/auth/account_schema.js +8 -1
  16. package/dist/auth/api_token_queries.d.ts +2 -1
  17. package/dist/auth/api_token_queries.d.ts.map +1 -1
  18. package/dist/auth/api_token_queries.js +21 -5
  19. package/dist/auth/audit_log_routes.d.ts.map +1 -1
  20. package/dist/auth/audit_log_routes.js +9 -1
  21. package/dist/auth/bearer_auth.d.ts.map +1 -1
  22. package/dist/auth/bearer_auth.js +6 -1
  23. package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
  24. package/dist/auth/daemon_token_middleware.js +5 -1
  25. package/dist/auth/migrations.d.ts.map +1 -1
  26. package/dist/auth/migrations.js +33 -0
  27. package/dist/auth/request_context.d.ts +18 -0
  28. package/dist/auth/request_context.d.ts.map +1 -1
  29. package/dist/auth/request_context.js +27 -1
  30. package/dist/auth/role_grant_offer_action_specs.d.ts +12 -0
  31. package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
  32. package/dist/auth/role_grant_offer_action_specs.js +12 -1
  33. package/dist/auth/self_service_role_action_specs.d.ts +7 -0
  34. package/dist/auth/self_service_role_action_specs.d.ts.map +1 -1
  35. package/dist/auth/self_service_role_action_specs.js +7 -1
  36. package/dist/auth/token_scope.d.ts +155 -0
  37. package/dist/auth/token_scope.d.ts.map +1 -0
  38. package/dist/auth/token_scope.js +190 -0
  39. package/dist/db/CLAUDE.md +1 -1
  40. package/dist/hono_context.d.ts +23 -0
  41. package/dist/hono_context.d.ts.map +1 -1
  42. package/dist/hono_context.js +14 -0
  43. package/dist/http/error_schemas.d.ts +12 -0
  44. package/dist/http/error_schemas.d.ts.map +1 -1
  45. package/dist/http/error_schemas.js +12 -0
  46. package/dist/server/serve_fact_route.d.ts.map +1 -1
  47. package/dist/server/serve_fact_route.js +9 -1
  48. package/dist/server/x_accel.d.ts +2 -4
  49. package/dist/server/x_accel.d.ts.map +1 -1
  50. package/dist/server/x_accel.js +2 -4
  51. package/dist/testing/admin_integration.js +2 -2
  52. package/dist/testing/app_server.d.ts.map +1 -1
  53. package/dist/testing/app_server.js +2 -1
  54. package/dist/testing/audit_completeness.js +1 -1
  55. package/dist/testing/cross_backend/conformance_case.d.ts +23 -0
  56. package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -1
  57. package/dist/testing/cross_backend/conformance_case.js +21 -0
  58. package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
  59. package/dist/testing/cross_backend/conformance_table.js +58 -5
  60. package/dist/testing/cross_backend/expected_schema.json +1 -0
  61. package/dist/testing/cross_backend/setup.d.ts.map +1 -1
  62. package/dist/testing/cross_backend/setup.js +5 -1
  63. package/dist/testing/integration.js +3 -3
  64. package/package.json +1 -1
  65. package/dist/server/fact_write.d.ts +0 -32
  66. package/dist/server/fact_write.d.ts.map +0 -1
  67. package/dist/server/fact_write.js +0 -57
  68. package/dist/server/validate_nginx.d.ts +0 -34
  69. package/dist/server/validate_nginx.d.ts.map +0 -1
  70. 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,CA0F5C,CAAC;AAEF,wDAAwD;AACxD,eAAO,MAAM,iBAAiB,EAAE,kBAG/B,CAAC"}
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"}
@@ -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":"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"}
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;AASrD,OAAO,EAIN,KAAK,YAAY,EACjB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAA+B,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACpF,OAAO,EAIN,oBAAoB,EACpB,0BAA0B,EAC1B,0BAA0B,EAC1B,sBAAsB,EACtB,MAAM,0BAA0B,CAAC;AAElC;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,cAAc;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,WAAW,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;CAC9B;AAED,0DAA0D;AAC1D,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AAErD;;;;;;;;GAQG;AACH,eAAO,MAAM,2BAA2B,4BAA4B,CAAC;AAErE;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAAI,GAAG,OAAO,KAAG,cAAc,GAAG,IAEjE,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,uBAAuB,GAAI,GAAG,OAAO,KAAG,cAQpD,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,WAAW,mBAAoB,SAAQ,cAAc;IAC1D,KAAK,EAAE,KAAK,CAAC;CACb;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,QAAQ,GACpB,KAAK,cAAc,GAAG,IAAI,EAC1B,MAAM,MAAM,EACZ,MAAK,IAAiB,KACpB,OACoF,CAAC;AAExF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,eAAe,GAC3B,KAAK,cAAc,GAAG,IAAI,EAC1B,MAAM,MAAM,EACZ,UAAU,MAAM,GAAG,IAAI,EACvB,MAAK,IAAiB,KACpB,OAKF,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,mBAAmB,GAC/B,KAAK,cAAc,GAAG,IAAI,EAC1B,OAAO,aAAa,CAAC,MAAM,CAAC,EAC5B,UAAU,MAAM,GAAG,IAAI,EACvB,MAAK,IAAiB,KACpB,OAMF,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,wBAAwB,GACjC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,WAAW,CAAA;CAAE,GAClC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,gBAAgB,CAAC;IAAC,SAAS,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE,GACvF;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,sBAAsB,CAAA;CAAE,CAAC;AAEjD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,iBAAiB,MAAM,GAAG,SAAS,KACjC,OAAO,CAAC,wBAAwB,CAclC,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,iCAAiC,GAC7C,MAAM,SAAS,EACf,KAAK,MAAM,EACX,4BAAuC,KACrC,iBAgCF,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,0BAA0B,GAAI,GAAG,OAAO,EAAE,SAAS,YAAY,KAAG,QAAQ,GAAG,IAMzF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,YAAY,EAAE,iBAK1B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,aAAa,CAAC,MAAM,CAAC,KAAG,iBAW3D,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,wBAAwB,GACpC,kBAAkB,aAAa,CAAC,MAAM,CAAC,KACrC,iBAiBF,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,mBAAmB,GAC/B,KAAK,cAAc,EACnB,MAAM,SAAS,KACb,OAAO,CAAC,cAAc,CAQxB,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,qBAAqB,GACjC,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,UAAU,MAAM,KACd,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAUpC,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,qBAAqB,GACjC,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,cAAc,GAAG,IAAI,CAI/B,CAAC;AAEF;;;;;;;;;;GAUG;AACH,MAAM,MAAM,wBAAwB,GACjC;IAAE,KAAK,EAAE,OAAO,oBAAoB,CAAC;IAAC,SAAS,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE,GACtF;IAAE,KAAK,EAAE,OAAO,0BAA0B,CAAA;CAAE,GAC5C;IAAE,KAAK,EAAE,OAAO,0BAA0B,CAAA;CAAE,GAC5C;IAAE,KAAK,EAAE,OAAO,sBAAsB,CAAA;CAAE,CAAC;AAE5C;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,mBAAmB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,eAAe,EAAE,cAAc,GAAG,IAAI,CAAA;CAAE,GACpD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,GAAG,GAAG,GAAG,CAAC;IAAC,IAAI,EAAE,wBAAwB,CAAA;CAAE,CAAC;AAEpE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,eAAO,MAAM,yBAAyB,GACrC,MAAM,SAAS,EACf,YAAY,MAAM,GAAG,IAAI,EACzB,MAAM,SAAS,EACf,cAAc,MAAM,GAAG,SAAS,KAC9B,OAAO,CAAC,mBAAmB,CAwC7B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,SAAS,KACb,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,KAAK,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAoB5D,CAAC"}
@@ -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;;;;;;GAMG;AACH,eAAO,MAAM,iCAAiC;;;;;;;;;;;;;;;;;;;;;;CAYT,CAAC;AAEtC;;;;GAIG;AACH,eAAO,MAAM,kCAAkC,EAAE,aAAa,CAAC,yBAAyB,CAEvF,CAAC"}
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`, `fact_write.ts`, `serve_fact_route.ts`).
138
+ (`file_fact_fetcher.ts`, `serve_fact_route.ts`).
139
139
 
140
140
  ### Migration namespace order
141
141