@fuzdev/fuz_app 0.113.0 → 0.115.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 (129) hide show
  1. package/dist/actions/CLAUDE.md +46 -20
  2. package/dist/actions/action_dispatcher.d.ts +1 -1
  3. package/dist/actions/action_dispatcher.d.ts.map +1 -1
  4. package/dist/actions/action_dispatcher.js +22 -16
  5. package/dist/actions/action_event.d.ts +18 -2
  6. package/dist/actions/action_event.d.ts.map +1 -1
  7. package/dist/actions/action_event.js +63 -19
  8. package/dist/actions/action_event_types.d.ts +22 -0
  9. package/dist/actions/action_event_types.d.ts.map +1 -1
  10. package/dist/actions/action_event_types.js +19 -0
  11. package/dist/actions/compile_action_registry.d.ts +4 -2
  12. package/dist/actions/compile_action_registry.d.ts.map +1 -1
  13. package/dist/actions/compile_action_registry.js +8 -3
  14. package/dist/actions/frontend_rpc_client.d.ts +6 -0
  15. package/dist/actions/frontend_rpc_client.d.ts.map +1 -1
  16. package/dist/actions/frontend_rpc_client.js +2 -1
  17. package/dist/actions/heartbeat.d.ts +8 -4
  18. package/dist/actions/heartbeat.d.ts.map +1 -1
  19. package/dist/actions/heartbeat.js +8 -4
  20. package/dist/actions/peer_ping.d.ts.map +1 -1
  21. package/dist/actions/peer_ping.js +3 -1
  22. package/dist/actions/perform_action.d.ts +1 -1
  23. package/dist/actions/perform_action.d.ts.map +1 -1
  24. package/dist/actions/perform_action.js +6 -4
  25. package/dist/actions/register_action_ws.d.ts +13 -0
  26. package/dist/actions/register_action_ws.d.ts.map +1 -1
  27. package/dist/actions/register_action_ws.js +23 -2
  28. package/dist/actions/rpc_client.d.ts +1 -1
  29. package/dist/actions/rpc_client.d.ts.map +1 -1
  30. package/dist/actions/rpc_client.js +2 -1
  31. package/dist/actions/socket.svelte.d.ts +53 -10
  32. package/dist/actions/socket.svelte.d.ts.map +1 -1
  33. package/dist/actions/socket.svelte.js +84 -23
  34. package/dist/actions/transports.d.ts +24 -0
  35. package/dist/actions/transports.d.ts.map +1 -1
  36. package/dist/actions/transports.js +30 -0
  37. package/dist/actions/transports_http.d.ts +11 -4
  38. package/dist/actions/transports_http.d.ts.map +1 -1
  39. package/dist/actions/transports_http.js +63 -18
  40. package/dist/actions/transports_ws.d.ts +28 -3
  41. package/dist/actions/transports_ws.d.ts.map +1 -1
  42. package/dist/actions/transports_ws.js +33 -4
  43. package/dist/actions/ws_endpoint_spec.d.ts +7 -0
  44. package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
  45. package/dist/auth/CLAUDE.md +52 -8
  46. package/dist/auth/admin_actions.d.ts.map +1 -1
  47. package/dist/auth/admin_actions.js +4 -3
  48. package/dist/auth/audit_log_schema.d.ts +3 -1
  49. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  50. package/dist/auth/audit_log_schema.js +13 -3
  51. package/dist/auth/cell_action_specs.js +1 -1
  52. package/dist/auth/cell_actions.d.ts.map +1 -1
  53. package/dist/auth/cell_actions.js +33 -4
  54. package/dist/auth/cell_authorize.d.ts +3 -0
  55. package/dist/auth/cell_authorize.d.ts.map +1 -1
  56. package/dist/auth/cell_authorize.js +7 -4
  57. package/dist/auth/request_context.d.ts +12 -3
  58. package/dist/auth/request_context.d.ts.map +1 -1
  59. package/dist/auth/request_context.js +12 -3
  60. package/dist/auth/role_grant_offer_action_specs.d.ts +30 -5
  61. package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
  62. package/dist/auth/role_grant_offer_action_specs.js +53 -17
  63. package/dist/auth/role_grant_offer_actions.d.ts +23 -12
  64. package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
  65. package/dist/auth/role_grant_offer_actions.js +132 -38
  66. package/dist/auth/role_grant_offer_queries.d.ts +36 -0
  67. package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
  68. package/dist/auth/role_grant_offer_queries.js +76 -0
  69. package/dist/auth/role_schema.d.ts +12 -0
  70. package/dist/auth/role_schema.d.ts.map +1 -1
  71. package/dist/auth/role_schema.js +12 -0
  72. package/dist/auth/self_service_role_actions.d.ts +14 -0
  73. package/dist/auth/self_service_role_actions.d.ts.map +1 -1
  74. package/dist/auth/self_service_role_actions.js +25 -1
  75. package/dist/http/CLAUDE.md +7 -3
  76. package/dist/http/jsonrpc_helpers.d.ts +0 -5
  77. package/dist/http/jsonrpc_helpers.d.ts.map +1 -1
  78. package/dist/http/jsonrpc_helpers.js +0 -13
  79. package/dist/http/schema_helpers.d.ts +56 -0
  80. package/dist/http/schema_helpers.d.ts.map +1 -1
  81. package/dist/http/schema_helpers.js +91 -0
  82. package/dist/server/app_server.d.ts.map +1 -1
  83. package/dist/server/app_server.js +1 -0
  84. package/dist/server/serve_fact_route.d.ts +3 -1
  85. package/dist/server/serve_fact_route.d.ts.map +1 -1
  86. package/dist/server/serve_fact_route.js +7 -4
  87. package/dist/testing/CLAUDE.md +29 -16
  88. package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
  89. package/dist/testing/cross_backend/cell_gated_create.js +23 -1
  90. package/dist/testing/cross_backend/peer_ping_ws.d.ts.map +1 -1
  91. package/dist/testing/cross_backend/peer_ping_ws.js +18 -0
  92. package/dist/testing/cross_backend/role_grant_participation.d.ts.map +1 -1
  93. package/dist/testing/cross_backend/role_grant_participation.js +34 -0
  94. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts +12 -1
  95. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
  96. package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +13 -1
  97. package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts.map +1 -1
  98. package/dist/testing/cross_backend/test_cell_gated_create_authorize.js +9 -7
  99. package/dist/testing/cross_backend/ws_round_trip.d.ts +11 -3
  100. package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
  101. package/dist/testing/cross_backend/ws_round_trip.js +74 -12
  102. package/dist/testing/entities.d.ts +6 -3
  103. package/dist/testing/entities.d.ts.map +1 -1
  104. package/dist/testing/entities.js +4 -1
  105. package/dist/testing/transports/ws_client.d.ts +6 -0
  106. package/dist/testing/transports/ws_client.d.ts.map +1 -1
  107. package/dist/testing/transports/ws_transport.d.ts.map +1 -1
  108. package/dist/testing/transports/ws_transport.js +6 -1
  109. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  110. package/dist/testing/ws_round_trip.js +5 -0
  111. package/dist/ui/BootstrapForm.svelte +14 -10
  112. package/dist/ui/BootstrapForm.svelte.d.ts.map +1 -1
  113. package/dist/ui/CLAUDE.md +19 -7
  114. package/dist/ui/LoginForm.svelte +8 -4
  115. package/dist/ui/LoginForm.svelte.d.ts.map +1 -1
  116. package/dist/ui/RoleGrantOfferForm.svelte +29 -14
  117. package/dist/ui/RoleGrantOfferForm.svelte.d.ts +2 -3
  118. package/dist/ui/RoleGrantOfferForm.svelte.d.ts.map +1 -1
  119. package/dist/ui/SignupForm.svelte +14 -13
  120. package/dist/ui/form_state.svelte.d.ts +24 -11
  121. package/dist/ui/form_state.svelte.d.ts.map +1 -1
  122. package/dist/ui/form_state.svelte.js +38 -10
  123. package/dist/ui/role_grant_offers_state.svelte.d.ts +25 -15
  124. package/dist/ui/role_grant_offers_state.svelte.d.ts.map +1 -1
  125. package/dist/ui/role_grant_offers_state.svelte.js +7 -1
  126. package/package.json +2 -22
  127. package/dist/actions/request_tracker.svelte.d.ts +0 -77
  128. package/dist/actions/request_tracker.svelte.d.ts.map +0 -1
  129. package/dist/actions/request_tracker.svelte.js +0 -169
@@ -22,7 +22,7 @@
22
22
  */
23
23
  import { z } from 'zod';
24
24
  import { Uuid } from '@fuzdev/fuz_util/id.ts';
25
- import { ERROR_ROLE_GRANT_NOT_FOUND, ERROR_ROLE_NOT_WEB_GRANTABLE } from '../http/error_schemas.js';
25
+ import { ERROR_INSUFFICIENT_PERMISSIONS, ERROR_ROLE_GRANT_NOT_FOUND, ERROR_ROLE_NOT_WEB_GRANTABLE } from '../http/error_schemas.js';
26
26
  import { RoleName } from './role_schema.js';
27
27
  import { ROLE_GRANT_OFFER_MESSAGE_LENGTH_MAX, RoleGrantOfferJson } from './role_grant_offer_schema.js';
28
28
  import { ROLE_GRANT_REVOKED_REASON_LENGTH_MAX } from './account_schema.js';
@@ -43,6 +43,29 @@ export const ERROR_ROLE_GRANT_OFFER_NOT_AUTHORIZED = 'role_grant_offer_not_autho
43
43
  export const ERROR_ROLE_GRANT_OFFER_ACTOR_MISMATCH = 'role_grant_offer_actor_mismatch';
44
44
  /** Error reason — `role_grant_offer_create` was called with a `to_actor_id` that does not belong to `to_account_id`. */
45
45
  export const ERROR_ROLE_GRANT_OFFER_ACTOR_ACCOUNT_MISMATCH = 'role_grant_offer_actor_account_mismatch';
46
+ /**
47
+ * Error reason — a grant path was asked to bind a builtin role to a scope
48
+ * (builtin roles are global-only, see `is_builtin_role`). Refused by
49
+ * `role_grant_assign`, `role_grant_offer_create`, and `role_grant_offer_accept`.
50
+ */
51
+ export const ERROR_ROLE_GRANT_BUILTIN_SCOPED = 'role_grant_builtin_scoped';
52
+ /**
53
+ * Whether a grant input's `scope_kind` + `scope_id` are paired — both absent
54
+ * (a global grant) or both present (a scoped one), the
55
+ * `role_grant_scope_kind_paired` / `role_grant_offer_scope_kind_paired`
56
+ * CHECKs. The grant inputs refine on it so a half-scoped call is a `-32602`
57
+ * rather than a constraint-violation 500.
58
+ *
59
+ * @param input - the grant input's scope pair
60
+ * @returns `true` when both are set or both are absent
61
+ */
62
+ export const is_grant_scope_paired = (input) => (input.scope_kind == null) === (input.scope_id == null);
63
+ /** The message a half-scoped grant input fails with — the grant inputs' refine, and the UI's pre-RPC guard. */
64
+ export const GRANT_SCOPE_PAIRED_MESSAGE = "'scope_kind' and 'scope_id' must be given together (both for a scoped grant, neither for a global one)";
65
+ /** The `scope_kind` field both grant inputs share, paired with `scope_id`. */
66
+ const GrantScopeKind = z.string().nullish().meta({
67
+ description: 'Machine-readable kind tag for `scope_id` — paired-null with `scope_id` (both null for global, both non-null for scoped). Required iff `scope_id` is set.'
68
+ });
46
69
  // -- Input/output schemas ---------------------------------------------------
47
70
  /**
48
71
  * Input for `role_grant_offer_create`.
@@ -55,15 +78,14 @@ export const ERROR_ROLE_GRANT_OFFER_ACTOR_ACCOUNT_MISMATCH = 'role_grant_offer_a
55
78
  * events. Omit (or pass null) for the account-grain default — any actor
56
79
  * on `to_account_id` may accept.
57
80
  */
58
- export const RoleGrantOfferCreateInput = z.strictObject({
81
+ export const RoleGrantOfferCreateInput = z
82
+ .strictObject({
59
83
  to_account_id: Uuid.meta({ description: 'Account id of the recipient.' }),
60
84
  to_actor_id: Uuid.nullish().meta({
61
85
  description: 'Optional actor-grain target on the recipient account. When set, only this actor may accept and the audit envelope carries it on offer-shape events. Must belong to `to_account_id`.'
62
86
  }),
63
87
  role: RoleName.meta({ description: 'Role being offered.' }),
64
- scope_kind: z.string().nullish().meta({
65
- description: 'Machine-readable kind tag for `scope_id` — paired-null with `scope_id` (both null for global, both non-null for scoped). Required iff `scope_id` is set.'
66
- }),
88
+ scope_kind: GrantScopeKind,
67
89
  scope_id: Uuid.nullish().meta({
68
90
  description: 'Scope id for resource-scoped grants (e.g. classroom id). `null` for global.'
69
91
  }),
@@ -73,7 +95,8 @@ export const RoleGrantOfferCreateInput = z.strictObject({
73
95
  .nullish()
74
96
  .meta({ description: 'Optional free-form note from the grantor.' }),
75
97
  acting: ActingActor
76
- });
98
+ })
99
+ .refine(is_grant_scope_paired, { message: GRANT_SCOPE_PAIRED_MESSAGE });
77
100
  /** Input for `role_grant_offer_accept`. */
78
101
  export const RoleGrantOfferAcceptInput = z.strictObject({
79
102
  offer_id: Uuid.meta({ description: 'The offer to accept.' }),
@@ -162,10 +185,12 @@ export const RoleGrantRevokeOutput = z.strictObject({
162
185
  * Input for `role_grant_assign` — the immediate admin-only conferral path, the
163
186
  * consent-free sibling of `role_grant_offer_create`. An admin assigns a
164
187
  * role_grant straight onto the target actor; there is no offer for a grantee to
165
- * accept. No `message` (no offer to carry it on) and no `scope_kind` (`scope_id`
166
- * alone is the v1 scope discriminator — the grant row's `scope_kind` stays null).
188
+ * accept. No `message` (no offer to carry it on). `scope_kind` pairs with
189
+ * `scope_id` exactly as on `role_grant_offer_create` — both or neither — and
190
+ * the grant row carries the pair.
167
191
  */
168
- export const RoleGrantAssignInput = z.strictObject({
192
+ export const RoleGrantAssignInput = z
193
+ .strictObject({
169
194
  to_account_id: Uuid.meta({ description: 'Account id of the grantee.' }),
170
195
  to_actor_id: Uuid.nullish().meta({
171
196
  description: "Optional actor-grain target on the grantee account. When set, must belong to `to_account_id`. Omit to resolve the account's sole active actor — a multi-actor account must name one."
@@ -173,11 +198,13 @@ export const RoleGrantAssignInput = z.strictObject({
173
198
  role: RoleName.meta({
174
199
  description: "Role to assign. Must be admin-grantable (its `grant_paths` includes `'admin'`)."
175
200
  }),
201
+ scope_kind: GrantScopeKind,
176
202
  scope_id: Uuid.nullish().meta({
177
- description: 'Scope id for resource-scoped grants. `null` for a global role_grant.'
203
+ description: 'Scope id for resource-scoped grants. `null` for a global role_grant. Builtin roles are global-only.'
178
204
  }),
179
205
  acting: ActingActor
180
- });
206
+ })
207
+ .refine(is_grant_scope_paired, { message: GRANT_SCOPE_PAIRED_MESSAGE });
181
208
  /** Output for `role_grant_assign`. */
182
209
  export const RoleGrantAssignOutput = z.strictObject({
183
210
  ok: z.literal(true),
@@ -203,12 +230,13 @@ export const role_grant_offer_create_action_spec = {
203
230
  input: RoleGrantOfferCreateInput,
204
231
  output: RoleGrantOfferCreateOutput,
205
232
  async: true,
206
- description: "Offer a role_grant to another account. Grantor must hold the offered role (or pass a consumer authorize callback); role's `grant_paths` must include `'admin'`.",
233
+ description: "Offer a role_grant to another account. Admin-only by default (a consumer authorize callback may widen it for app roles); role's `grant_paths` must include `'admin'`. A builtin role is global-only and offered only by a global admin, whatever the callback says.",
207
234
  error_reasons: [
208
235
  ERROR_ROLE_GRANT_OFFER_SELF_TARGET,
209
236
  ERROR_ROLE_GRANT_OFFER_ROLE_NOT_GRANTABLE,
210
237
  ERROR_ROLE_GRANT_OFFER_NOT_AUTHORIZED,
211
- ERROR_ROLE_GRANT_OFFER_ACTOR_ACCOUNT_MISMATCH
238
+ ERROR_ROLE_GRANT_OFFER_ACTOR_ACCOUNT_MISMATCH,
239
+ ERROR_ROLE_GRANT_BUILTIN_SCOPED
212
240
  ],
213
241
  rate_limit: 'account'
214
242
  };
@@ -232,12 +260,15 @@ export const role_grant_offer_accept_action_spec = {
232
260
  input: RoleGrantOfferAcceptInput,
233
261
  output: RoleGrantOfferAcceptOutput,
234
262
  async: true,
235
- description: 'Accept an offer. Atomically marks the offer accepted, inserts the role_grant, and supersedes sibling pending offers for the same (account, role, scope).',
263
+ description: 'Accept an offer. Atomically marks the offer accepted, inserts the role_grant, and supersedes sibling pending offers for the same (account, role, scope). A builtin-role offer is refused (and stays pending) when it is `keeper`, scoped, or its grantor no longer holds a global admin.',
236
264
  error_reasons: [
237
265
  ERROR_ROLE_GRANT_OFFER_NOT_FOUND,
238
266
  ERROR_ROLE_GRANT_OFFER_TERMINAL,
239
267
  ERROR_ROLE_GRANT_OFFER_EXPIRED,
240
- ERROR_ROLE_GRANT_OFFER_ACTOR_MISMATCH
268
+ ERROR_ROLE_GRANT_OFFER_ACTOR_MISMATCH,
269
+ ERROR_ROLE_GRANT_OFFER_ROLE_NOT_GRANTABLE,
270
+ ERROR_ROLE_GRANT_BUILTIN_SCOPED,
271
+ ERROR_ROLE_GRANT_OFFER_NOT_AUTHORIZED
241
272
  ]
242
273
  };
243
274
  export const role_grant_offer_decline_action_spec = {
@@ -319,8 +350,13 @@ export const role_grant_assign_action_spec = {
319
350
  input: RoleGrantAssignInput,
320
351
  output: RoleGrantAssignOutput,
321
352
  async: true,
322
- description: "Immediately assign a role_grant to a target account — admin-only, no consent step (the capability-unlock UX). The role's `grant_paths` must include `'admin'`. Idempotent: re-assigning an active grant returns it. Emits a role_grant_create audit event.",
323
- error_reasons: [ERROR_ROLE_NOT_WEB_GRANTABLE, ERROR_ROLE_GRANT_OFFER_ACTOR_ACCOUNT_MISMATCH],
353
+ description: "Immediately assign a role_grant to a target account — global-admin-only, no consent step (the capability-unlock UX). The role's `grant_paths` must include `'admin'`; `scope_kind` + `scope_id` are paired, and a builtin role is global-only. Idempotent: re-assigning an active grant returns it. Emits a role_grant_create audit event.",
354
+ error_reasons: [
355
+ ERROR_INSUFFICIENT_PERMISSIONS,
356
+ ERROR_ROLE_NOT_WEB_GRANTABLE,
357
+ ERROR_ROLE_GRANT_BUILTIN_SCOPED,
358
+ ERROR_ROLE_GRANT_OFFER_ACTOR_ACCOUNT_MISMATCH
359
+ ],
324
360
  rate_limit: 'account'
325
361
  };
326
362
  /**
@@ -11,12 +11,18 @@
11
11
  * `side_effects: false` so they are addressable via GET.
12
12
  *
13
13
  * Authorization:
14
- * - `role_grant_offer_create` — the grantor must hold an active role_grant for the
15
- * role being offered, and that role's `grant_paths` must include `'admin'`.
16
- * Consumers needing a richer policy (e.g., "teacher may offer student in
17
- * *their* classroom") pass an `authorize` callback that overrides the default.
14
+ * - `role_grant_offer_create` — the role's `grant_paths` must include `'admin'`,
15
+ * and by default the grantor must hold a **global** `admin`. Consumers needing
16
+ * a richer policy (e.g., "teacher may offer student in *their* classroom")
17
+ * pass an `authorize` callback that overrides the default — for app roles
18
+ * only: a builtin role (`admin` / `keeper`) is global-only (a scoped one is
19
+ * refused with `role_grant_builtin_scoped`) and offered only by a global
20
+ * admin, both decided before the callback runs.
18
21
  * - `role_grant_offer_accept` / `role_grant_offer_decline` — keyed to the caller's
19
- * account; `query_*` helpers enforce the IDOR guard.
22
+ * account; `query_*` helpers enforce the IDOR guard. Accept also refuses a
23
+ * builtin-role offer that must not mint (`keeper`, scoped, or a grantor who
24
+ * no longer holds a global admin), auditing the refusal and leaving the
25
+ * offer pending.
20
26
  * - `role_grant_offer_retract` — keyed to the caller's actor.
21
27
  * - `role_grant_offer_list` / `role_grant_offer_history` — self by default;
22
28
  * `{account_id}` is admin-only.
@@ -24,10 +30,12 @@
24
30
  * dispatcher rejects non-admin callers before the handler runs.
25
31
  * The admin-grant-path gate prevents revoking keeper / daemon-scoped
26
32
  * roles via this surface. Keys on `actor_id` to survive multi-actor accounts.
27
- * - `role_grant_assign` — spec-level `auth: {role: 'admin'}`; the immediate
28
- * consent-free conferral path. The admin-grant-path gate prevents assigning
29
- * keeper / daemon-scoped roles via this surface. No holder-propagation — only
30
- * admins confer (see participation-gates.md Decision 7).
33
+ * - `role_grant_assign` — spec-level `auth: {role: 'admin'}` (a **global**
34
+ * grant, re-checked in the handler); the immediate consent-free conferral
35
+ * path. The admin-grant-path gate prevents assigning keeper / daemon-scoped
36
+ * roles via this surface, and a scoped `admin` is refused. No
37
+ * holder-propagation — only a global admin confers; holding a role grants
38
+ * no power to assign it.
31
39
  *
32
40
  * Audit events are emitted in-transaction by the query layer (atomic with
33
41
  * the role_grant write on accept/revoke) or by the handler via the bound
@@ -77,9 +85,12 @@ export interface RoleGrantOfferActionOptions {
77
85
  default_ttl_ms?: number;
78
86
  /**
79
87
  * Custom authorization for `role_grant_offer_create`. The default requires the
80
- * caller to hold an active role_grant for the offered role *and* the role's
81
- * `RoleSpec.grant_paths` to include `'admin'`. Consumers with richer
82
- * policies (scope-aware, chained roles) override this.
88
+ * caller to hold a **global** `admin` grant; the role's
89
+ * `RoleSpec.grant_paths` must include `'admin'` either way. Consumers with
90
+ * richer policies (scope-aware, chained roles) override this. A builtin
91
+ * role never reaches the callback unless the caller holds a global `admin`
92
+ * and the offer is unscoped — the spine decides builtin-role offers itself,
93
+ * so a callback needs no builtin handling of its own.
83
94
  */
84
95
  authorize?: RoleGrantOfferCreateAuthorize;
85
96
  }
@@ -1 +1 @@
1
- {"version":3,"file":"role_grant_offer_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/role_grant_offer_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,EAGN,KAAK,aAAa,EAClB,KAAK,SAAS,EACd,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EAIN,KAAK,gBAAgB,EACrB,MAAM,kBAAkB,CAAC;AA2B1B,OAAO,EAGN,KAAK,cAAc,EACnB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAEnD,OAAO,EAON,KAAK,kBAAkB,EACvB,MAAM,qCAAqC,CAAC;AAoC7C;;;;;;;;GAQG;AACH,MAAM,MAAM,6BAA6B,GAAG,CAC3C,IAAI,EAAE,cAAc,EACpB,KAAK,EAAE;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EACvE,IAAI,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,EACrB,GAAG,EAAE,aAAa,KACd,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEhC,qDAAqD;AACrD,MAAM,WAAW,2BAA2B;IAC3C;;;;;OAKG;IACH,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,0FAA0F;IAC1F,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,6BAA6B,CAAC;CAC1C;AAgCD;;;;;;;GAOG;AACH,eAAO,MAAM,+BAA+B,GAC3C,MAAM,iBAAiB,GAAG;IACzB,mBAAmB,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;CAChD,EACD,UAAS,2BAAgC,KACvC,KAAK,CAAC,SAAS,CAkiBjB,CAAC"}
1
+ {"version":3,"file":"role_grant_offer_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/role_grant_offer_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,EAGN,KAAK,aAAa,EAClB,KAAK,SAAS,EACd,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EAKN,KAAK,gBAAgB,EACrB,MAAM,kBAAkB,CAAC;AA4B1B,OAAO,EAGN,KAAK,cAAc,EACnB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAEnD,OAAO,EAON,KAAK,kBAAkB,EACvB,MAAM,qCAAqC,CAAC;AAyC7C;;;;;;;;GAQG;AACH,MAAM,MAAM,6BAA6B,GAAG,CAC3C,IAAI,EAAE,cAAc,EACpB,KAAK,EAAE;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EACvE,IAAI,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,EACrB,GAAG,EAAE,aAAa,KACd,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEhC,qDAAqD;AACrD,MAAM,WAAW,2BAA2B;IAC3C;;;;;OAKG;IACH,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,0FAA0F;IAC1F,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,6BAA6B,CAAC;CAC1C;AA6CD;;;;;;;GAOG;AACH,eAAO,MAAM,+BAA+B,GAC3C,MAAM,iBAAiB,GAAG;IACzB,mBAAmB,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;CAChD,EACD,UAAS,2BAAgC,KACvC,KAAK,CAAC,SAAS,CAsnBjB,CAAC"}
@@ -11,12 +11,18 @@
11
11
  * `side_effects: false` so they are addressable via GET.
12
12
  *
13
13
  * Authorization:
14
- * - `role_grant_offer_create` — the grantor must hold an active role_grant for the
15
- * role being offered, and that role's `grant_paths` must include `'admin'`.
16
- * Consumers needing a richer policy (e.g., "teacher may offer student in
17
- * *their* classroom") pass an `authorize` callback that overrides the default.
14
+ * - `role_grant_offer_create` — the role's `grant_paths` must include `'admin'`,
15
+ * and by default the grantor must hold a **global** `admin`. Consumers needing
16
+ * a richer policy (e.g., "teacher may offer student in *their* classroom")
17
+ * pass an `authorize` callback that overrides the default — for app roles
18
+ * only: a builtin role (`admin` / `keeper`) is global-only (a scoped one is
19
+ * refused with `role_grant_builtin_scoped`) and offered only by a global
20
+ * admin, both decided before the callback runs.
18
21
  * - `role_grant_offer_accept` / `role_grant_offer_decline` — keyed to the caller's
19
- * account; `query_*` helpers enforce the IDOR guard.
22
+ * account; `query_*` helpers enforce the IDOR guard. Accept also refuses a
23
+ * builtin-role offer that must not mint (`keeper`, scoped, or a grantor who
24
+ * no longer holds a global admin), auditing the refusal and leaving the
25
+ * offer pending.
20
26
  * - `role_grant_offer_retract` — keyed to the caller's actor.
21
27
  * - `role_grant_offer_list` / `role_grant_offer_history` — self by default;
22
28
  * `{account_id}` is admin-only.
@@ -24,10 +30,12 @@
24
30
  * dispatcher rejects non-admin callers before the handler runs.
25
31
  * The admin-grant-path gate prevents revoking keeper / daemon-scoped
26
32
  * roles via this surface. Keys on `actor_id` to survive multi-actor accounts.
27
- * - `role_grant_assign` — spec-level `auth: {role: 'admin'}`; the immediate
28
- * consent-free conferral path. The admin-grant-path gate prevents assigning
29
- * keeper / daemon-scoped roles via this surface. No holder-propagation — only
30
- * admins confer (see participation-gates.md Decision 7).
33
+ * - `role_grant_assign` — spec-level `auth: {role: 'admin'}` (a **global**
34
+ * grant, re-checked in the handler); the immediate consent-free conferral
35
+ * path. The admin-grant-path gate prevents assigning keeper / daemon-scoped
36
+ * roles via this surface, and a scoped `admin` is refused. No
37
+ * holder-propagation — only a global admin confers; holding a role grants
38
+ * no power to assign it.
31
39
  *
32
40
  * Audit events are emitted in-transaction by the query layer (atomic with
33
41
  * the role_grant write on accept/revoke) or by the handler via the bound
@@ -45,16 +53,16 @@
45
53
  import { rpc_action } from '../actions/action_rpc.js';
46
54
  import { jsonrpc_errors } from '../http/jsonrpc_errors.js';
47
55
  import { emit_after_commit } from '../http/pending_effects.js';
48
- import { builtin_role_specs_by_name, ROLE_ADMIN, role_has_grant_path } from './role_schema.js';
56
+ import { builtin_role_specs_by_name, is_builtin_role, ROLE_ADMIN, role_has_grant_path } from './role_schema.js';
49
57
  import { GRANT_PATH_ADMIN } from './grant_path_schema.js';
50
58
  import { ROLE_GRANT_OFFER_DEFAULT_TTL_MS, to_role_grant_offer_json } from './role_grant_offer_schema.js';
51
- import { query_role_grant_offer_create, query_role_grant_offer_decline, query_role_grant_offer_retract, query_role_grant_offer_list, query_role_grant_offer_history_for_account, query_accept_offer, RoleGrantOfferActorAccountMismatchError, RoleGrantOfferActorMismatchError, RoleGrantOfferAlreadyTerminalError, RoleGrantOfferExpiredError, RoleGrantOfferNotFoundError, RoleGrantOfferSelfTargetError } from './role_grant_offer_queries.js';
59
+ import { query_role_grant_offer_create, query_role_grant_offer_decline, query_role_grant_offer_retract, query_role_grant_offer_list, query_role_grant_offer_history_for_account, query_accept_offer, RoleGrantOfferActorAccountMismatchError, RoleGrantOfferActorMismatchError, RoleGrantOfferAlreadyTerminalError, RoleGrantOfferBuiltinRefusedError, RoleGrantOfferExpiredError, RoleGrantOfferNotFoundError, RoleGrantOfferSelfTargetError } from './role_grant_offer_queries.js';
52
60
  import { query_create_role_grant, query_role_grant_find_active_role_for_actor, query_revoke_role_grant } from './role_grant_queries.js';
53
61
  import { query_actor_by_id, query_active_actors_by_account } from './account_queries.js';
54
62
  import { has_scoped_role } from './request_context.js';
55
63
  import { build_role_grant_offer_accepted_notification, build_role_grant_offer_declined_notification, build_role_grant_offer_received_notification, build_role_grant_offer_retracted_notification, build_role_grant_offer_supersede_notification, build_role_grant_revoke_notification } from './role_grant_offer_notifications.js';
56
- import { ERROR_ROLE_GRANT_NOT_FOUND, ERROR_ROLE_NOT_WEB_GRANTABLE } from '../http/error_schemas.js';
57
- import { ERROR_ROLE_GRANT_OFFER_ACTOR_ACCOUNT_MISMATCH, ERROR_ROLE_GRANT_OFFER_ACTOR_MISMATCH, ERROR_ROLE_GRANT_OFFER_EXPIRED, ERROR_ROLE_GRANT_OFFER_NOT_AUTHORIZED, ERROR_ROLE_GRANT_OFFER_NOT_FOUND, ERROR_ROLE_GRANT_OFFER_ROLE_NOT_GRANTABLE, ERROR_ROLE_GRANT_OFFER_SELF_TARGET, ERROR_ROLE_GRANT_OFFER_TERMINAL, role_grant_offer_create_action_spec, role_grant_offer_accept_action_spec, role_grant_offer_decline_action_spec, role_grant_offer_retract_action_spec, role_grant_offer_list_action_spec, role_grant_offer_history_action_spec, role_grant_revoke_action_spec, role_grant_assign_action_spec } from './role_grant_offer_action_specs.js';
64
+ import { ERROR_INSUFFICIENT_PERMISSIONS, ERROR_ROLE_GRANT_NOT_FOUND, ERROR_ROLE_NOT_WEB_GRANTABLE } from '../http/error_schemas.js';
65
+ import { ERROR_ROLE_GRANT_BUILTIN_SCOPED, ERROR_ROLE_GRANT_OFFER_ACTOR_ACCOUNT_MISMATCH, ERROR_ROLE_GRANT_OFFER_ACTOR_MISMATCH, ERROR_ROLE_GRANT_OFFER_EXPIRED, ERROR_ROLE_GRANT_OFFER_NOT_AUTHORIZED, ERROR_ROLE_GRANT_OFFER_NOT_FOUND, ERROR_ROLE_GRANT_OFFER_ROLE_NOT_GRANTABLE, ERROR_ROLE_GRANT_OFFER_SELF_TARGET, ERROR_ROLE_GRANT_OFFER_TERMINAL, role_grant_offer_create_action_spec, role_grant_offer_accept_action_spec, role_grant_offer_decline_action_spec, role_grant_offer_retract_action_spec, role_grant_offer_list_action_spec, role_grant_offer_history_action_spec, role_grant_revoke_action_spec, role_grant_assign_action_spec } from './role_grant_offer_action_specs.js';
58
66
  // -- Helpers ----------------------------------------------------------------
59
67
  /**
60
68
  * Fan out a batch of pre-written audit rows to the bound emitter's
@@ -71,13 +79,22 @@ const fan_out_audit_events = (events, audit) => {
71
79
  audit.notify(event);
72
80
  }
73
81
  };
82
+ /**
83
+ * Whether a grant input binds a builtin role to a scope — refused on every
84
+ * grant path (see `ERROR_ROLE_GRANT_BUILTIN_SCOPED`).
85
+ */
86
+ const is_scoped_builtin = (role, scope_id) => scope_id != null && is_builtin_role(role);
87
+ /** The `invalid_params` a scoped builtin-role grant input answers. */
88
+ const builtin_scoped_error = () => jsonrpc_errors.invalid_params('builtin roles are global-only and cannot be scoped', {
89
+ reason: ERROR_ROLE_GRANT_BUILTIN_SCOPED
90
+ });
74
91
  // eslint-disable-next-line @typescript-eslint/require-await
75
92
  const default_authorize = async (auth, _input, _deps, _ctx) => {
76
93
  // Admin-only conferral: the caller must hold a *global* `admin` grant. Holding
77
- // the offered role itself confers no power to offer it (no holder-propagation —
78
- // see participation-gates.md Decision 6). The global check (scope_id null) stops
79
- // a scoped `admin` escalating into global authority over the offer surface.
80
- // Reads the in-memory `auth.role_grants` snapshot; no DB roundtrip.
94
+ // the offered role itself confers no power to offer it (no holder-propagation).
95
+ // The global check (scope_id null) stops a scoped `admin` escalating into
96
+ // global authority over the offer surface. Reads the in-memory
97
+ // `auth.role_grants` snapshot; no DB roundtrip.
81
98
  return has_scoped_role(auth, ROLE_ADMIN, null);
82
99
  };
83
100
  // -- Action factory ---------------------------------------------------------
@@ -94,11 +111,11 @@ export const create_role_grant_offer_actions = (deps, options = {}) => {
94
111
  const role_specs = options.roles?.role_specs ?? builtin_role_specs_by_name;
95
112
  const default_ttl_ms = options.default_ttl_ms ?? ROLE_GRANT_OFFER_DEFAULT_TTL_MS;
96
113
  const authorize = options.authorize ?? default_authorize;
97
- // Four denial paths (admin-grant-path gate, authorize, self-target,
98
- // actor-account mismatch) all emit the same failure-outcome audit
99
- // event. `target_actor_id` is populated when the caller supplied a
100
- // `to_actor_id` so failure rows match the success-shape envelope of
101
- // actor-targeted offers.
114
+ // Every denial path (admin-grant-path gate, scoped builtin, builtin without a
115
+ // global admin, authorize, self-target, actor-account mismatch) emits the
116
+ // same failure-outcome audit event. `target_actor_id` is populated when the
117
+ // caller supplied a `to_actor_id` so failure rows match the success-shape
118
+ // envelope of actor-targeted offers.
102
119
  const emit_create_failure_audit = (ctx, auth, input) => {
103
120
  audit.emit_role_grant_target(ctx, auth, {
104
121
  event_type: 'role_grant_offer_create',
@@ -112,6 +129,46 @@ export const create_role_grant_offer_actions = (deps, options = {}) => {
112
129
  }
113
130
  });
114
131
  };
132
+ // Answer (and audit) an accept the query refused as a builtin-role offer that
133
+ // must not mint. The refusal is a forensic event — someone holds an offer of a
134
+ // builtin role the spine won't confer — so it is audited as a failed
135
+ // `role_grant_offer_accept` (pool-routed, surviving the rollback), like
136
+ // offer-create refusals. Metadata is the success row's shape minus
137
+ // `role_grant_id` (no grant was minted), plus the wire `reason`. The offer
138
+ // stays pending. Returns the error the verb answers.
139
+ const refuse_builtin_accept = (ctx, auth, err) => {
140
+ let error;
141
+ switch (err.refusal) {
142
+ case 'bootstrap_only':
143
+ // `keeper`'s grant path is bootstrap-only — the offer-create answer
144
+ // for a role no offer may confer
145
+ error = jsonrpc_errors.forbidden('role not grantable', {
146
+ reason: ERROR_ROLE_GRANT_OFFER_ROLE_NOT_GRANTABLE
147
+ });
148
+ break;
149
+ case 'scoped':
150
+ error = jsonrpc_errors.invalid_request({ reason: ERROR_ROLE_GRANT_BUILTIN_SCOPED });
151
+ break;
152
+ case 'grantor_not_authorized':
153
+ error = jsonrpc_errors.forbidden("the offer's grantor can no longer confer this role", {
154
+ reason: ERROR_ROLE_GRANT_OFFER_NOT_AUTHORIZED
155
+ });
156
+ break;
157
+ }
158
+ audit.emit_role_grant_target(ctx, auth, {
159
+ event_type: 'role_grant_offer_accept',
160
+ outcome: 'failure',
161
+ target_account_id: auth.account.id,
162
+ target_actor_id: auth.actor.id,
163
+ metadata: {
164
+ offer_id: err.offer.id,
165
+ role: err.offer.role,
166
+ scope_id: err.offer.scope_id,
167
+ reason: error.data.reason
168
+ }
169
+ });
170
+ return error;
171
+ };
115
172
  // Returns {offer} only — no auto-accept. Recipient must call
116
173
  // role_grant_offer_accept; admin tests drive the full consent flow over
117
174
  // RPC (see testing/admin_integration.ts `offer_and_accept`), or seed
@@ -126,6 +183,23 @@ export const create_role_grant_offer_actions = (deps, options = {}) => {
126
183
  reason: ERROR_ROLE_GRANT_OFFER_ROLE_NOT_GRANTABLE
127
184
  });
128
185
  }
186
+ // Builtin roles are global-only — refused before the injected callback,
187
+ // so no consumer authorizer can admit a scoped `admin` / `keeper` offer.
188
+ if (is_scoped_builtin(input.role, input.scope_id)) {
189
+ emit_create_failure_audit(ctx, auth, input);
190
+ throw builtin_scoped_error();
191
+ }
192
+ // Offering a builtin role is global-admin authority, decided here rather
193
+ // than by the injected callback: a consumer callback answering by holding
194
+ // the role (a scope-blind `has_role`) would otherwise let a scoped `admin`
195
+ // row offer a global `admin` to a second account it controls. Same denial
196
+ // the default callback gives.
197
+ if (is_builtin_role(input.role) && !has_scoped_role(auth, ROLE_ADMIN, null)) {
198
+ emit_create_failure_audit(ctx, auth, input);
199
+ throw jsonrpc_errors.forbidden('not authorized to offer this role', {
200
+ reason: ERROR_ROLE_GRANT_OFFER_NOT_AUTHORIZED
201
+ });
202
+ }
129
203
  const authorized = await authorize(auth, {
130
204
  to_account_id: input.to_account_id,
131
205
  role: input.role,
@@ -214,6 +288,9 @@ export const create_role_grant_offer_actions = (deps, options = {}) => {
214
288
  reason: ERROR_ROLE_GRANT_OFFER_ACTOR_MISMATCH
215
289
  });
216
290
  }
291
+ if (err instanceof RoleGrantOfferBuiltinRefusedError) {
292
+ throw refuse_builtin_accept(ctx, auth, err);
293
+ }
217
294
  throw err;
218
295
  }
219
296
  // Look up the grantor's account_id inside the transaction so the
@@ -448,25 +525,43 @@ export const create_role_grant_offer_actions = (deps, options = {}) => {
448
525
  return { ok: true, revoked: true };
449
526
  };
450
527
  // The immediate admin-only conferral path — the consent-free sibling of
451
- // `role_grant_offer_create`. Admin-gated at the dispatcher (`roles:
452
- // ['admin']`), it re-checks admin-grantability (so keeper / CLI-only roles
453
- // can't be web-assigned), resolves the target actor, writes the idempotent
454
- // grant, and emits a `role_grant_create` audit row — the same event the
455
- // offer-accept and self-service paths emit. No WS notification in v1 (the
456
- // grantee picks the capability up on its next authenticated request).
528
+ // `role_grant_offer_create`. Gated at the dispatcher on a **global** admin
529
+ // (`roles: ['admin']`) and re-checked here, it re-checks admin-grantability
530
+ // (so keeper / CLI-only roles can't be web-assigned), refuses a scoped
531
+ // builtin role, resolves the target actor, writes the idempotent grant with
532
+ // the input's `(scope_kind, scope_id)` pair, and emits a `role_grant_create`
533
+ // audit row — the same event the offer-accept and self-service paths emit.
534
+ // No WS notification in v1 (the grantee picks the capability up on its next
535
+ // authenticated request).
457
536
  const assign_handler = async (input, ctx) => {
458
537
  const auth = ctx.auth;
538
+ // Defense in depth behind the dispatcher's role gate: conferring roles is
539
+ // global-admin authority. A scoped `admin` must never reach the write, or
540
+ // it could assign itself a global `admin`.
541
+ if (!has_scoped_role(auth, ROLE_ADMIN, null)) {
542
+ throw jsonrpc_errors.forbidden('forbidden', { reason: ERROR_INSUFFICIENT_PERMISSIONS });
543
+ }
459
544
  // Admin-grant-path gate — the same registry check the offer/revoke gates
460
- // use; keeper / daemon-scoped roles stay CLI-only. Denial is
461
- // forensic-audited (`role_grant_id` omitted — no grant was created).
462
- if (!role_has_grant_path(role_specs, input.role, GRANT_PATH_ADMIN)) {
545
+ // use; keeper / daemon-scoped roles stay CLI-only. A builtin role asked
546
+ // for with a scope is refused the same way, with its own reason: builtin
547
+ // roles are global-only. Denial is forensic-audited (`role_grant_id`
548
+ // omitted — no grant was created).
549
+ const admin_grantable = role_has_grant_path(role_specs, input.role, GRANT_PATH_ADMIN);
550
+ const builtin_scoped = is_scoped_builtin(input.role, input.scope_id);
551
+ if (builtin_scoped || !admin_grantable) {
463
552
  audit.emit_role_grant_target(ctx, auth, {
464
553
  event_type: 'role_grant_create',
465
554
  outcome: 'failure',
466
555
  target_account_id: input.to_account_id,
467
556
  target_actor_id: input.to_actor_id ?? null,
468
- metadata: { role: input.role, scope_id: input.scope_id ?? null }
557
+ metadata: {
558
+ role: input.role,
559
+ scope_kind: input.scope_kind ?? null,
560
+ scope_id: input.scope_id ?? null
561
+ }
469
562
  });
563
+ if (builtin_scoped && admin_grantable)
564
+ throw builtin_scoped_error();
470
565
  throw jsonrpc_errors.forbidden('role not web-grantable', {
471
566
  reason: ERROR_ROLE_NOT_WEB_GRANTABLE
472
567
  });
@@ -474,9 +569,7 @@ export const create_role_grant_offer_actions = (deps, options = {}) => {
474
569
  // Resolve the grant's target actor. A supplied `to_actor_id` must be an
475
570
  // active actor of `to_account_id`; otherwise the account's sole active
476
571
  // actor is used (a multi-actor account must name one). A missing /
477
- // actorless account 404-masks. Inlined (not a helper) so the
478
- // actor-account-mismatch throw stays in the handler body the
479
- // error_reasons drift scan reads.
572
+ // actorless account 404-masks.
480
573
  const actors = await query_active_actors_by_account(ctx, input.to_account_id);
481
574
  let target_actor_id;
482
575
  if (input.to_actor_id != null) {
@@ -497,12 +590,12 @@ export const create_role_grant_offer_actions = (deps, options = {}) => {
497
590
  throw jsonrpc_errors.invalid_params('to_actor_id is required for a multi-actor account');
498
591
  }
499
592
  // Idempotent write — re-assigning an already-held grant returns the
500
- // existing row. `scope_kind` stays null: `scope_id` alone is the v1
501
- // discriminator. `granted_by` is the assigning admin.
593
+ // existing row. `scope_kind` + `scope_id` land as the pair the input
594
+ // carried. `granted_by` is the assigning admin.
502
595
  const grant = await query_create_role_grant(ctx, {
503
596
  actor_id: target_actor_id,
504
597
  role: input.role,
505
- scope_kind: null,
598
+ scope_kind: input.scope_kind ?? null,
506
599
  scope_id: input.scope_id ?? null,
507
600
  granted_by: auth.actor.id
508
601
  });
@@ -513,6 +606,7 @@ export const create_role_grant_offer_actions = (deps, options = {}) => {
513
606
  metadata: {
514
607
  role: grant.role,
515
608
  role_grant_id: grant.id,
609
+ scope_kind: input.scope_kind ?? null,
516
610
  scope_id: grant.scope_id
517
611
  }
518
612
  });
@@ -66,6 +66,35 @@ export declare class RoleGrantOfferSelfTargetError extends Error {
66
66
  export declare class RoleGrantOfferActorMismatchError extends Error {
67
67
  constructor(offer_id: string);
68
68
  }
69
+ /**
70
+ * Why `query_accept_offer` refused to mint a builtin-role grant.
71
+ *
72
+ * Each is a row no current grant path creates but a database can still hold
73
+ * (a direct `query_role_grant_offer_create` call, or an offer made under a
74
+ * since-closed path), so accept — the chokepoint where an offer becomes a
75
+ * `role_grant` — checks them itself.
76
+ *
77
+ * - `bootstrap_only` — the offer is of `keeper`, which is bootstrap-only:
78
+ * never conferred through an offer, whoever made it.
79
+ * - `scoped` — the offer binds a builtin role to a scope; builtin roles are
80
+ * global-only.
81
+ * - `grantor_not_authorized` — the grantor no longer holds an active
82
+ * **global** `admin` on an active actor of an active account. Conferring a
83
+ * builtin role is global-admin authority, so it is re-checked when the
84
+ * grant is minted, not only when the offer was made.
85
+ */
86
+ export type BuiltinOfferRefusal = 'bootstrap_only' | 'scoped' | 'grantor_not_authorized';
87
+ /**
88
+ * Error thrown by `query_accept_offer` when the offer confers a builtin role
89
+ * (`admin` / `keeper`) that this accept must not mint — `refusal` names the
90
+ * rule. Carries the locked (still pending, untouched) offer so the caller can
91
+ * audit the refusal with the offer's role and scope.
92
+ */
93
+ export declare class RoleGrantOfferBuiltinRefusedError extends Error {
94
+ readonly offer: RoleGrantOffer;
95
+ readonly refusal: BuiltinOfferRefusal;
96
+ constructor(offer: RoleGrantOffer, refusal: BuiltinOfferRefusal);
97
+ }
69
98
  /**
70
99
  * Error thrown when `query_role_grant_offer_create` is called with a
71
100
  * `to_actor_id` that does not exist or does not belong to `to_account_id`.
@@ -224,6 +253,11 @@ export interface AcceptOfferResult {
224
253
  * - `RoleGrantOfferAlreadyTerminalError` — offer is declined, retracted, or
225
254
  * superseded.
226
255
  * - `RoleGrantOfferExpiredError` — offer is pending but past `expires_at`.
256
+ * - `RoleGrantOfferActorMismatchError` — actor-targeted offer, different actor.
257
+ * - `RoleGrantOfferBuiltinRefusedError` — a builtin-role offer that must not
258
+ * mint: `keeper` at all, a builtin bound to a scope, or one whose grantor no
259
+ * longer holds an active global `admin` (read under `FOR SHARE`). The offer
260
+ * stays pending.
227
261
  *
228
262
  * Sibling supersede is what closes the "accept a pre-revoke sibling offer
229
263
  * to bypass a revoke" path: once A is accepted, B/C/... can no longer be
@@ -236,6 +270,8 @@ export interface AcceptOfferResult {
236
270
  * @throws RoleGrantOfferNotFoundError if the offer is missing or belongs to another recipient
237
271
  * @throws RoleGrantOfferAlreadyTerminalError if the offer is declined, retracted, or superseded
238
272
  * @throws RoleGrantOfferExpiredError if the offer is pending but past `expires_at`
273
+ * @throws RoleGrantOfferActorMismatchError if the offer is actor-targeted to a different actor
274
+ * @throws RoleGrantOfferBuiltinRefusedError if a builtin-role offer must not mint (see `BuiltinOfferRefusal`)
239
275
  * @throws Error if the accepting `actor_id` does not belong to `to_account_id`, or invariant assertions fail
240
276
  */
241
277
  export declare const query_accept_offer: (deps: QueryDeps, input: AcceptOfferInput) => Promise<AcceptOfferResult>;
@@ -1 +1 @@
1
- {"version":3,"file":"role_grant_offer_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/role_grant_offer_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAEnD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AASrD,OAAO,KAAK,EACX,yBAAyB,EACzB,cAAc,EACd,eAAe,EACf,MAAM,8BAA8B,CAAC;AAEtC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D;;;;;GAKG;AACH,qBAAa,kCAAmC,SAAQ,KAAK;gBAChD,QAAQ,EAAE,MAAM;CAI5B;AAED;;;;;GAKG;AACH,qBAAa,0BAA2B,SAAQ,KAAK;gBACxC,QAAQ,EAAE,MAAM;CAI5B;AAED;;;;;GAKG;AACH,qBAAa,2BAA4B,SAAQ,KAAK;gBACzC,QAAQ,EAAE,MAAM;CAI5B;AAED;;;;;;;;GAQG;AACH,qBAAa,6BAA8B,SAAQ,KAAK;;CAKvD;AAED;;;;;;;GAOG;AACH,qBAAa,gCAAiC,SAAQ,KAAK;gBAC9C,QAAQ,EAAE,MAAM;CAI5B;AAED;;;;;GAKG;AACH,qBAAa,uCAAwC,SAAQ,KAAK;;CAKjE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,SAAS,EACf,OAAO,yBAAyB,KAC9B,OAAO,CAAC,cAAc,CAuDxB,CAAC;AAEF,uGAAuG;AACvG,MAAM,WAAW,aAAc,SAAQ,cAAc;IACpD;;;;;OAKG;IACH,eAAe,EAAE,IAAI,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,8BAA8B,GAC1C,MAAM,SAAS,EACf,UAAU,MAAM,EAChB,eAAe,MAAM,EACrB,QAAQ,MAAM,GAAG,IAAI,KACnB,OAAO,CAAC,aAAa,GAAG,IAAI,CAmB9B,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,8BAA8B,GAC1C,MAAM,SAAS,EACf,UAAU,MAAM,EAChB,eAAe,MAAM,KACnB,OAAO,CAAC,cAAc,GAAG,IAAI,CAe/B,CAAC;AA8BF;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,GACvC,MAAM,SAAS,EACf,eAAe,MAAM,KACnB,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,CAY/B,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,0CAA0C,GACtD,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,cAAW,EACX,eAAU,KACR,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,CAS/B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,mCAAmC,GAC/C,MAAM,SAAS,EACf,UAAU,MAAM,KACd,OAAO,CAAC,cAAc,GAAG,IAAI,CAY/B,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,oCAAoC,GAChD,MAAM,SAAS,KACb,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,CAU/B,CAAC;AAEF,sCAAsC;AACtC,MAAM,WAAW,gBAAgB;IAChC,QAAQ,EAAE,IAAI,CAAC;IACf,mGAAmG;IACnG,aAAa,EAAE,IAAI,CAAC;IACpB;;;;;;;;;;OAUG;IACH,QAAQ,EAAE,IAAI,CAAC;IACf,gDAAgD;IAChD,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACnB;AAED,6HAA6H;AAC7H,MAAM,WAAW,iBAAiB;IACjC,UAAU,EAAE,SAAS,CAAC;IACtB,KAAK,EAAE,cAAc,CAAC;IACtB,oJAAoJ;IACpJ,OAAO,EAAE,OAAO,CAAC;IACjB;;;;;OAKG;IACH,iBAAiB,EAAE,KAAK,CAAC,eAAe,CAAC,CAAC;IAC1C,yMAAyM;IACzM,YAAY,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC;CACnC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,eAAO,MAAM,kBAAkB,GAC9B,MAAM,SAAS,EACf,OAAO,gBAAgB,KACrB,OAAO,CAAC,iBAAiB,CA0N3B,CAAC"}
1
+ {"version":3,"file":"role_grant_offer_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/role_grant_offer_queries.ts"],"names":[],"mappings":"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"}