@fuzdev/fuz_app 0.149.0 → 0.150.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 (141) hide show
  1. package/dist/actions/CLAUDE.md +9 -7
  2. package/dist/actions/action_bridge.d.ts +7 -0
  3. package/dist/actions/action_bridge.d.ts.map +1 -1
  4. package/dist/actions/action_bridge.js +10 -0
  5. package/dist/actions/action_event_types.d.ts +1 -1
  6. package/dist/actions/action_event_types.d.ts.map +1 -1
  7. package/dist/actions/perform_action.d.ts.map +1 -1
  8. package/dist/actions/perform_action.js +8 -5
  9. package/dist/actions/register_action_ws.d.ts +8 -2
  10. package/dist/actions/register_action_ws.d.ts.map +1 -1
  11. package/dist/actions/register_action_ws.js +24 -4
  12. package/dist/actions/transports_ws_auth_guard.d.ts +6 -4
  13. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  14. package/dist/actions/transports_ws_auth_guard.js +9 -6
  15. package/dist/actions/ws_endpoint_spec.d.ts +1 -1
  16. package/dist/auth/CLAUDE.md +69 -31
  17. package/dist/auth/actor_lookup_queries.d.ts +3 -2
  18. package/dist/auth/actor_lookup_queries.d.ts.map +1 -1
  19. package/dist/auth/actor_lookup_queries.js +6 -3
  20. package/dist/auth/actor_search_queries.d.ts +5 -2
  21. package/dist/auth/actor_search_queries.d.ts.map +1 -1
  22. package/dist/auth/actor_search_queries.js +11 -4
  23. package/dist/auth/admin_action_specs.d.ts +4 -4
  24. package/dist/auth/admin_action_specs.js +4 -4
  25. package/dist/auth/admin_actions.d.ts.map +1 -1
  26. package/dist/auth/admin_actions.js +35 -6
  27. package/dist/auth/all_cell_actions.d.ts +11 -2
  28. package/dist/auth/all_cell_actions.d.ts.map +1 -1
  29. package/dist/auth/all_cell_actions.js +2 -1
  30. package/dist/auth/audit_emitter.d.ts +9 -3
  31. package/dist/auth/audit_emitter.d.ts.map +1 -1
  32. package/dist/auth/audit_emitter.js +9 -3
  33. package/dist/auth/audit_log_schema.d.ts +1 -0
  34. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  35. package/dist/auth/audit_log_schema.js +9 -5
  36. package/dist/auth/bootstrap_account.d.ts +25 -1
  37. package/dist/auth/bootstrap_account.d.ts.map +1 -1
  38. package/dist/auth/bootstrap_account.js +37 -4
  39. package/dist/auth/bootstrap_routes.d.ts +3 -2
  40. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  41. package/dist/auth/bootstrap_routes.js +5 -4
  42. package/dist/auth/cell_action_specs.d.ts +215 -15
  43. package/dist/auth/cell_action_specs.d.ts.map +1 -1
  44. package/dist/auth/cell_action_specs.js +103 -16
  45. package/dist/auth/cell_actions.d.ts +40 -18
  46. package/dist/auth/cell_actions.d.ts.map +1 -1
  47. package/dist/auth/cell_actions.js +126 -19
  48. package/dist/auth/cell_audit_events.d.ts +1 -1
  49. package/dist/auth/cell_audit_events.d.ts.map +1 -1
  50. package/dist/auth/cell_audit_events.js +3 -2
  51. package/dist/auth/cell_audit_metadata.d.ts +11 -0
  52. package/dist/auth/cell_audit_metadata.d.ts.map +1 -1
  53. package/dist/auth/cell_audit_metadata.js +10 -0
  54. package/dist/auth/cell_grant_action_specs.d.ts +7 -0
  55. package/dist/auth/cell_grant_action_specs.d.ts.map +1 -1
  56. package/dist/auth/cell_grant_action_specs.js +7 -0
  57. package/dist/auth/cell_grant_actions.d.ts +36 -4
  58. package/dist/auth/cell_grant_actions.d.ts.map +1 -1
  59. package/dist/auth/cell_grant_actions.js +20 -3
  60. package/dist/auth/credential_type_schema.d.ts +3 -2
  61. package/dist/auth/credential_type_schema.d.ts.map +1 -1
  62. package/dist/auth/credential_type_schema.js +3 -2
  63. package/dist/auth/role_grant_offer_action_specs.d.ts +18 -2
  64. package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
  65. package/dist/auth/role_grant_offer_action_specs.js +25 -3
  66. package/dist/auth/role_grant_offer_actions.d.ts +9 -4
  67. package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
  68. package/dist/auth/role_grant_offer_actions.js +58 -7
  69. package/dist/auth/role_grant_offer_notifications.d.ts +3 -2
  70. package/dist/auth/role_grant_offer_notifications.d.ts.map +1 -1
  71. package/dist/auth/role_grant_offer_notifications.js +3 -2
  72. package/dist/auth/role_grant_offer_queries.d.ts +20 -0
  73. package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
  74. package/dist/auth/role_grant_offer_queries.js +35 -0
  75. package/dist/auth/role_grant_queries.d.ts +27 -9
  76. package/dist/auth/role_grant_queries.d.ts.map +1 -1
  77. package/dist/auth/role_grant_queries.js +40 -13
  78. package/dist/auth/role_schema.d.ts +14 -8
  79. package/dist/auth/role_schema.d.ts.map +1 -1
  80. package/dist/auth/role_schema.js +6 -3
  81. package/dist/auth/self_service_role_actions.d.ts.map +1 -1
  82. package/dist/auth/self_service_role_actions.js +20 -0
  83. package/dist/db/CLAUDE.md +9 -2
  84. package/dist/db/cell_ddl.d.ts +9 -1
  85. package/dist/db/cell_ddl.d.ts.map +1 -1
  86. package/dist/db/cell_ddl.js +11 -1
  87. package/dist/db/cell_queries.d.ts +53 -7
  88. package/dist/db/cell_queries.d.ts.map +1 -1
  89. package/dist/db/cell_queries.js +61 -7
  90. package/dist/http/body_limit.d.ts +3 -1
  91. package/dist/http/body_limit.d.ts.map +1 -1
  92. package/dist/http/body_limit.js +6 -1
  93. package/dist/http/jsonrpc.d.ts +9 -0
  94. package/dist/http/jsonrpc.d.ts.map +1 -1
  95. package/dist/http/jsonrpc.js +17 -0
  96. package/dist/http/jsonrpc_errors.d.ts.map +1 -1
  97. package/dist/http/jsonrpc_errors.js +29 -20
  98. package/dist/http/proxy.d.ts +4 -2
  99. package/dist/http/proxy.d.ts.map +1 -1
  100. package/dist/http/proxy.js +7 -2
  101. package/dist/http/surface.d.ts +3 -1
  102. package/dist/http/surface.d.ts.map +1 -1
  103. package/dist/http/surface.js +7 -4
  104. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  105. package/dist/realtime/sse_auth_guard.js +3 -2
  106. package/dist/schema_meta.d.ts +32 -1
  107. package/dist/schema_meta.d.ts.map +1 -1
  108. package/dist/schema_meta.js +58 -5
  109. package/dist/server/app_server.d.ts +13 -5
  110. package/dist/server/app_server.d.ts.map +1 -1
  111. package/dist/server/app_server.js +32 -8
  112. package/dist/testing/CLAUDE.md +34 -21
  113. package/dist/testing/app_server.d.ts +3 -1
  114. package/dist/testing/app_server.d.ts.map +1 -1
  115. package/dist/testing/cross_backend/actor_lookup.d.ts.map +1 -1
  116. package/dist/testing/cross_backend/actor_lookup.js +27 -0
  117. package/dist/testing/cross_backend/capabilities.d.ts +2 -1
  118. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  119. package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
  120. package/dist/testing/cross_backend/cell_gated_create.js +109 -13
  121. package/dist/testing/cross_backend/cell_moderate.d.ts +7 -1
  122. package/dist/testing/cross_backend/cell_moderate.d.ts.map +1 -1
  123. package/dist/testing/cross_backend/cell_moderate.js +68 -4
  124. package/dist/testing/cross_backend/cell_ref_admission.d.ts.map +1 -1
  125. package/dist/testing/cross_backend/cell_ref_admission.js +18 -9
  126. package/dist/testing/cross_backend/conformance_table.d.ts +5 -0
  127. package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
  128. package/dist/testing/cross_backend/conformance_table.js +12 -10
  129. package/dist/testing/cross_backend/differential.d.ts +7 -0
  130. package/dist/testing/cross_backend/differential.d.ts.map +1 -1
  131. package/dist/testing/cross_backend/differential.js +17 -0
  132. package/dist/testing/cross_backend/expected_schema.json +1 -0
  133. package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
  134. package/dist/testing/cross_backend/full_spine_mount.js +5 -4
  135. package/dist/testing/cross_backend/role_grant_offer_lifecycle.d.ts.map +1 -1
  136. package/dist/testing/cross_backend/role_grant_offer_lifecycle.js +74 -2
  137. package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts +18 -19
  138. package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts.map +1 -1
  139. package/dist/testing/cross_backend/test_cell_gated_create_authorize.js +66 -42
  140. package/dist/testing/cross_backend/transport_composition.js +3 -3
  141. package/package.json +1 -1
@@ -31,7 +31,9 @@ import '../assert_dev_env.js';
31
31
  *
32
32
  * Facts are put through `_testing_put_fact`, with `holder` naming the actor a
33
33
  * real upload would record. Gated on `capabilities.fact_serving` (the
34
- * seeder + the serve routes); runs under every `cross_backend_*` project.
34
+ * seeder + the serve routes), and the cases driving a grant, item, or clone
35
+ * verb on `capabilities.cell_relations` as well; runs under every
36
+ * `cross_backend_*` project.
35
37
  *
36
38
  * @module
37
39
  */
@@ -101,7 +103,7 @@ export const describe_cell_ref_admission_cross_tests = (options) => {
101
103
  }), uploaded);
102
104
  expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { data: { cover: uploaded } }, bob_headers), CellCreateOutput);
103
105
  });
104
- test_if(capabilities.fact_serving, 'update keeps existing refs and refuses new unheld ones', async () => {
106
+ test_if(capabilities.fact_serving && capabilities.cell_relations, 'update keeps existing refs and refuses new unheld ones', async () => {
105
107
  const fixture = await setup_test();
106
108
  const alice = await fixture.create_account({ username: 'ref_upd_alice' });
107
109
  const bob = await fixture.create_account({ username: 'ref_upd_bob' });
@@ -135,7 +137,7 @@ export const describe_cell_ref_admission_cross_tests = (options) => {
135
137
  expect_output(await cross_rpc_call(t, rpc_path, 'cell_update', { cell_id, data: { label: 'dropped' } }, bob_headers), CellUpdateOutput);
136
138
  assert_not_held(await cross_rpc_call(t, rpc_path, 'cell_update', { cell_id, data: { cover: alices } }, bob_headers), 'update restoring a dropped ref');
137
139
  });
138
- test_if(capabilities.fact_serving, 'clone carries source refs per row without holding them', async () => {
140
+ test_if(capabilities.fact_serving && capabilities.cell_relations, 'clone carries source refs per row without holding them', async () => {
139
141
  const fixture = await setup_test();
140
142
  const alice = await fixture.create_account({ username: 'ref_clone_alice' });
141
143
  const bob = await fixture.create_account({ username: 'ref_clone_bob' });
@@ -184,7 +186,7 @@ export const describe_cell_ref_admission_cross_tests = (options) => {
184
186
  assert.deepStrictEqual(created.cell.refs, [named]);
185
187
  }
186
188
  });
187
- test_if(capabilities.fact_serving, 'a revoked grantee cannot re-reference a fact it read', async () => {
189
+ test_if(capabilities.fact_serving && capabilities.cell_relations, 'a revoked grantee cannot re-reference a fact it read', async () => {
188
190
  const fixture = await setup_test();
189
191
  const alice = await fixture.create_account({ username: 'ref_rev_alice' });
190
192
  const bob = await fixture.create_account({ username: 'ref_rev_bob' });
@@ -242,17 +244,21 @@ export const describe_cell_ref_admission_cross_tests = (options) => {
242
244
  const prose = expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { data: not_refs }, bob.create_session_headers()), CellCreateOutput);
243
245
  assert.strictEqual(prose.cell.refs, null);
244
246
  });
245
- test_if(capabilities.fact_serving, 'ref admission answers after the 404 masks', async () => {
247
+ /** Alice's private cell over her own fact, which Bob cannot see. */
248
+ const setup_hidden_cell = async (prefix) => {
246
249
  const fixture = await setup_test();
247
- const alice = await fixture.create_account({ username: 'ref_mask_alice' });
248
- const bob = await fixture.create_account({ username: 'ref_mask_bob' });
250
+ const alice = await fixture.create_account({ username: `${prefix}_alice` });
251
+ const bob = await fixture.create_account({ username: `${prefix}_bob` });
249
252
  const t = fixture.fresh_transport();
250
- const bob_headers = bob.create_session_headers();
251
253
  const alices = await cross_put_fact(fixture, rpc_path, {
252
- content: 'alice hidden bytes',
254
+ content: `${prefix} hidden bytes`,
253
255
  holder: alice.actor.id
254
256
  });
255
257
  const hidden = expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { data: { file: alices }, visibility: 'private' }, alice.create_session_headers()), CellCreateOutput).cell.id;
258
+ return { t, bob_headers: bob.create_session_headers(), alices, hidden };
259
+ };
260
+ test_if(capabilities.fact_serving, 'ref admission answers after the 404 masks', async () => {
261
+ const { t, bob_headers, alices, hidden } = await setup_hidden_cell('ref_mask');
256
262
  // An update of a hidden cell answers its mask whatever `data` names —
257
263
  // an unheld new ref or the hidden cell's own ref alike.
258
264
  for (const named of [UNSTORED_HASH, alices]) {
@@ -261,6 +267,9 @@ export const describe_cell_ref_admission_cross_tests = (options) => {
261
267
  }
262
268
  const under = await cross_rpc_call(t, rpc_path, 'cell_create', { data: { file: UNSTORED_HASH }, parent_id: hidden }, bob_headers);
263
269
  assert.strictEqual(error_reason(under), 'cell_not_found', 'create under a hidden parent');
270
+ });
271
+ test_if(capabilities.fact_serving && capabilities.cell_relations, 'clone ref admission answers after the 404 mask', async () => {
272
+ const { t, bob_headers, hidden } = await setup_hidden_cell('ref_clone_mask');
264
273
  const clone = await cross_rpc_call(t, rpc_path, 'cell_clone', { source_id: hidden, with_data_patch: { extra: UNSTORED_HASH } }, bob_headers);
265
274
  assert.strictEqual(error_reason(clone), 'cell_not_found', 'clone of a hidden source');
266
275
  });
@@ -154,6 +154,11 @@ export interface SendConformanceCaseOptions {
154
154
  readonly resolved_rpc_endpoints: ReturnType<typeof resolve_rpc_endpoints_for_setup>;
155
155
  /** Send the request as a frame on a socket at this endpoint instead of over HTTP. */
156
156
  readonly ws?: WsEndpointTarget;
157
+ /**
158
+ * Headers added to the HTTP request beneath the principal's, which win on
159
+ * a name clash. Not sent on the WS leg (a frame carries no headers).
160
+ */
161
+ readonly request_headers?: Readonly<Record<string, string>>;
157
162
  }
158
163
  /** The spec a sent case resolved to — the RPC action, or the REST route. */
159
164
  export type ConformanceCaseTarget = {
@@ -1 +1 @@
1
- {"version":3,"file":"conformance_table.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/conformance_table.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA8C9B,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7E,OAAO,KAAK,EAAe,SAAS,EAAE,MAAM,0BAA0B,CAAC;AACvE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAOnE,OAAO,EAKN,+BAA+B,EAC/B,KAAK,uBAAuB,EAC5B,MAAM,mBAAmB,CAAC;AAI3B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAGvE,OAAO,EAGN,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAGzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEzD;;;;;GAKG;AACH,MAAM,WAAW,0BAA0B;IAC1C,iEAAiE;IACjE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,gEAAgE;IAChE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,sDAAsD;AACtD,MAAM,WAAW,uBAAuB;IACvC,8CAA8C;IAC9C,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IAC/C,+DAA+D;IAC/D,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,oEAAoE;IACpE,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;IACxC,0EAA0E;IAC1E,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,kEAAkE;IAClE,QAAQ,CAAC,aAAa,EAAE,uBAAuB,CAAC;IAChD;;;;;;OAMG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,uBAAuB,CAAC;IACtD,4EAA4E;IAC5E,QAAQ,CAAC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACjD,4EAA4E;IAC5E,QAAQ,CAAC,UAAU,CAAC,EAAE,0BAA0B,CAAC;IACjD;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,CAAC,EAAE,gBAAgB,CAAC;IAC/B,iEAAiE;IACjE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,sDAAsD;AACtD,MAAM,WAAW,8BAA8B;IAC9C,kCAAkC;IAClC,QAAQ,CAAC,aAAa,EAAE,uBAAuB,CAAC;IAChD,4EAA4E;IAC5E,QAAQ,CAAC,kBAAkB,CAAC,EAAE,uBAAuB,CAAC;IACtD,6DAA6D;IAC7D,QAAQ,CAAC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;CACjD;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,iCAAiC,GAC7C,SAAS,8BAA8B,KACrC,KAAK,CAAC,eAAe,CAKvB,CAAC;AAkBF;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,GAAI,GAAG,eAAe,KAAG,OAIf,CAAC;AAE9C,0EAA0E;AAC1E,MAAM,WAAW,4BAA4B;IAC5C,QAAQ,CAAC,SAAS,EAAE,cAAc,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC;;;;OAIG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAC3C;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B;AA+CD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,6BAA6B,GACzC,SAAS,WAAW,EACpB,IAAI,oBAAoB,EACxB,aAAa,0BAA0B,EACvC,yBAAwB,UAAU,CAAC,OAAO,+BAA+B,CAAM,KAC7E,OAAO,CAAC,4BAA4B,CAmJtC,CAAC;AA4CF;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,EAAE,aAAa,CAAC,MAAM,CAIrD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,6BAA6B,GACzC,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,OAAO,MAAM,KACX,IAQF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GACnC,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,UAAU,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,EACvC,OAAO,MAAM,KACX,IAYF,CAAC;AA0BF,2DAA2D;AAC3D,MAAM,WAAW,0BAA0B;IAC1C,wDAAwD;IACxD,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAC9B,oEAAoE;IACpE,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;IACxC,iEAAiE;IACjE,QAAQ,CAAC,UAAU,CAAC,EAAE,0BAA0B,CAAC;IACjD;;;OAGG;IACH,QAAQ,CAAC,sBAAsB,EAAE,UAAU,CAAC,OAAO,+BAA+B,CAAC,CAAC;IACpF,qFAAqF;IACrF,QAAQ,CAAC,EAAE,CAAC,EAAE,gBAAgB,CAAC;CAC/B;AAED,4EAA4E;AAC5E,MAAM,MAAM,qBAAqB,GAC9B;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAA;CAAE,GAC3E;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAA;CAAE,CAAC;AAExD;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,uBAAuB;IACvC,QAAQ,CAAC,MAAM,EAAE,qBAAqB,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACvB;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qBAAqB,GACjC,GAAG,eAAe,EAClB,SAAS,0BAA0B,KACjC,OAAO,CAAC,uBAAuB,CAuEjC,CAAC;AAsNF;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,GAAI,SAAS,uBAAuB,KAAG,IAuEnF,CAAC"}
1
+ {"version":3,"file":"conformance_table.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/conformance_table.ts"],"names":[],"mappings":"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"}
@@ -391,13 +391,16 @@ const is_success_status = (status) => status >= 200 && status < 300;
391
391
  * auth routes
392
392
  */
393
393
  export const send_conformance_case = async (c, options) => {
394
- const { fixture, surface_source, principals, resolved_rpc_endpoints, ws } = options;
394
+ const { fixture, surface_source, principals, resolved_rpc_endpoints, ws, request_headers } = options;
395
395
  if (ws && !is_ws_conformance_case(c)) {
396
396
  throw new Error(`conformance: case '${c.name}' cannot run over a WebSocket (is_ws_conformance_case) — ` +
397
397
  `filter the cases before sending them on the WS leg.`);
398
398
  }
399
399
  const principal = await resolve_conformance_principal(fixture, c.request.as, principals, resolved_rpc_endpoints);
400
- const { transport, headers, suppress_default_origin } = principal;
400
+ const { transport, suppress_default_origin } = principal;
401
+ const headers = request_headers
402
+ ? { ...request_headers, ...principal.headers }
403
+ : principal.headers;
401
404
  const params = c.request.acting
402
405
  ? bind_acting(c, resolve_acting_binding(c, c.request.acting, fixture, principal))
403
406
  : c.request.params;
@@ -461,15 +464,14 @@ const CONFORMANCE_REQUEST_ID = 'test';
461
464
  * The upgrade carries the principal's headers (its cookie, bearer or daemon
462
465
  * token) and the `Origin` the HTTP leg would send: none for a principal that
463
466
  * suppresses it, the target's otherwise. It also names the principal's own
464
- * actor as `?acting=`. Only the TS spine resolves an actor at the upgrade,
465
- * refusing a multi-actor account's upgrade without one (400
466
- * `actor_required`); the Rust spine's upgrade resolves the account and
467
- * ignores `?acting=`. Per-frame dispatch resolves the actor from each frame's
467
+ * actor as `?acting=`, which both spines resolve at the upgrade (a
468
+ * multi-actor account's upgrade without one is refused 400
469
+ * `actor_required`). Per-frame dispatch resolves the actor from each frame's
468
470
  * own `acting` on both. Because the leg always sends `?acting=`, it does not
469
- * cover that upgrade-level difference — the front-door table's multi-actor
470
- * row does. The socket closes afterwards; a
471
- * close error is not the case's subject (a request that revokes its own
472
- * credential closes the socket after its reply).
471
+ * cover the upgrade's own refusal — the front-door table's multi-actor row
472
+ * does. The socket closes afterwards; a close error is not the case's subject
473
+ * (a request that revokes its own credential closes the socket after its
474
+ * reply).
473
475
  */
474
476
  const send_ws_frame = async (method, params, principal, ws) => {
475
477
  const client = await create_admitted_ws_transport({
@@ -6,6 +6,13 @@ import type { ConformanceCase } from './conformance_case.js';
6
6
  import { type ConformancePrincipalConfig } from './conformance_table.js';
7
7
  import type { SetupTest } from './setup.js';
8
8
  import type { WsEndpointTarget } from './ws_endpoint.js';
9
+ /**
10
+ * Headers every HTTP request the differential sends carries, beneath the
11
+ * principal's: `accept-encoding: identity`, so each backend answers
12
+ * uncompressed and a runtime's own compression (`Deno.serve` compresses for a
13
+ * request that accepts gzip) never reads as a header difference.
14
+ */
15
+ export declare const DIFFERENTIAL_REQUEST_HEADERS: Readonly<Record<string, string>>;
9
16
  /** One side of the differential. */
10
17
  export interface DifferentialBackend {
11
18
  /** Short label used in failure output (`ts`, `rust`). */
@@ -1 +1 @@
1
- {"version":3,"file":"differential.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/differential.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"differential.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/differential.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAoD9B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAIN,KAAK,0BAA0B,EAC/B,MAAM,wBAAwB,CAAC;AAShC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEzD;;;;;GAKG;AACH,eAAO,MAAM,4BAA4B,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAEzE,CAAC;AAEF,oCAAoC;AACpC,MAAM,WAAW,mBAAmB;IACnC,yDAAyD;IACzD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,kDAAkD;IAClD,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,CAAC,EAAE,gBAAgB,CAAC;CAC/B;AAED;;;GAGG;AACH,MAAM,MAAM,oBAAoB,GAC7B;IAAE,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;IAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,KAAK,CAAA;CAAE,GAC/E;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;IAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,KAAK,CAAA;CAAE,CAAC;AAEnF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,eAAe,GAAG,oBAAoB,GAAG;IACpD,sEAAsE;IACtE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,8CAA8C;IAC9C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,2EAA2E;IAC3E,QAAQ,CAAC,YAAY,EAAE,aAAa,CAAC,MAAM,GAAG,mBAAmB,CAAC,CAAC;CACnE,CAAC;AAEF,6DAA6D;AAC7D,MAAM,WAAW,8BAA8B;IAC9C,oDAAoD;IACpD,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IAC/C,kEAAkE;IAClE,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC,mBAAmB,EAAE,mBAAmB,CAAC,CAAC;IACvE,oEAAoE;IACpE,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;IACxC,kEAAkE;IAClE,QAAQ,CAAC,aAAa,EAAE,uBAAuB,CAAC;IAChD;;;;OAIG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,uBAAuB,CAAC;IACtD,4EAA4E;IAC5E,QAAQ,CAAC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACjD,4EAA4E;IAC5E,QAAQ,CAAC,UAAU,CAAC,EAAE,0BAA0B,CAAC;IACjD,oDAAoD;IACpD,QAAQ,CAAC,iBAAiB,CAAC,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IAC5D,wEAAwE;IACxE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC7B;AAgBD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,6BAA6B,GACzC,OAAO,aAAa,CAAC,eAAe,CAAC,EACrC,QAAQ,aAAa,CAAC,eAAe,CAAC,KACpC,KAAK,CAAC,MAAM,CA0Bd,CAAC;AA6BF;;;;;;GAMG;AACH,eAAO,MAAM,uCAAuC,GACnD,SAAS,8BAA8B,KACrC,IA2GF,CAAC"}
@@ -19,6 +19,13 @@ import '../assert_dev_env.js';
19
19
  * each backend's conformance leg already holds them; the differential holds
20
20
  * the two answers to each other.
21
21
  *
22
+ * Every HTTP request asks for an uncompressed response
23
+ * (`DIFFERENTIAL_REQUEST_HEADERS`): content negotiation is the transport's,
24
+ * not the app's — Deno's `Deno.serve` compresses a response whose request
25
+ * accepts gzip, as Node's `fetch` sends by default, while other servers send
26
+ * it as is — so letting a server choose would read as a `content-encoding` and
27
+ * `vary` difference on every case.
28
+ *
22
29
  * A difference that is tracked and accepted goes in the **known-divergence
23
30
  * ledger** (`KnownDivergence`), matched by case names or by methods, naming
24
31
  * the paths that differ (`what_differs`), each optionally with the value each
@@ -42,6 +49,15 @@ import '../assert_dev_env.js';
42
49
  import { assert, describe, test } from 'vitest';
43
50
  import { is_ws_conformance_case, resolve_conformance_rpc_endpoints, send_conformance_case } from './conformance_table.js';
44
51
  import { diff_differential_responses, differential_path_covers, format_differential_diffs, normalize_differential_response } from './differential_normalize.js';
52
+ /**
53
+ * Headers every HTTP request the differential sends carries, beneath the
54
+ * principal's: `accept-encoding: identity`, so each backend answers
55
+ * uncompressed and a runtime's own compression (`Deno.serve` compresses for a
56
+ * request that accepts gzip) never reads as a header difference.
57
+ */
58
+ export const DIFFERENTIAL_REQUEST_HEADERS = {
59
+ 'accept-encoding': 'identity'
60
+ };
45
61
  const as_list = (value) => typeof value === 'string' ? [value] : value;
46
62
  /** The ledger entries matching a case. */
47
63
  const find_known_divergences = (c, ledger) => ledger.filter((d) => d.case_name !== undefined
@@ -126,6 +142,7 @@ export const describe_differential_conformance_tests = (options) => {
126
142
  surface_source,
127
143
  ...(principals && { principals }),
128
144
  resolved_rpc_endpoints,
145
+ request_headers: DIFFERENTIAL_REQUEST_HEADERS,
129
146
  ...(backend.ws && { ws: backend.ws })
130
147
  }));
131
148
  describe(options.suite_name ?? 'differential conformance', () => {
@@ -72,6 +72,7 @@
72
72
  "moderation",
73
73
  "parent_id",
74
74
  "path",
75
+ "policy",
75
76
  "refs",
76
77
  "root_id",
77
78
  "updated_at",
@@ -1 +1 @@
1
- {"version":3,"file":"full_spine_mount.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/full_spine_mount.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,+BAA+B,CAAC;AAG5D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8CAA8C,CAAC;AAMvF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAkC7D,6EAA6E;AAC7E,MAAM,WAAW,qBAAqB;IACrC;;;;;;OAMG;IACH,QAAQ,CAAC,kBAAkB,EAAE,gBAAgB,CAAC;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;CACzD;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,OAAO,EACb,SAAS,qBAAqB,KAC5B,KAAK,CAAC,SAAS,CA2CjB,CAAC;AAEF,6EAA6E;AAC7E,MAAM,WAAW,cAAc;IAC9B,gFAAgF;IAChF,QAAQ,CAAC,WAAW,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IACvC;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACnC;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,OAAO,EACb,SAAS,qBAAqB,KAC5B,cAUF,CAAC;AAaF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,+BAA+B,QAAO,KAAK,CAAC,eAAe,CAOvE,CAAC"}
1
+ {"version":3,"file":"full_spine_mount.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/full_spine_mount.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,+BAA+B,CAAC;AAG5D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8CAA8C,CAAC;AAMvF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAkC7D,6EAA6E;AAC7E,MAAM,WAAW,qBAAqB;IACrC;;;;;;OAMG;IACH,QAAQ,CAAC,kBAAkB,EAAE,gBAAgB,CAAC;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;CACzD;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,OAAO,EACb,SAAS,qBAAqB,KAC5B,KAAK,CAAC,SAAS,CA4CjB,CAAC;AAEF,6EAA6E;AAC7E,MAAM,WAAW,cAAc;IAC9B,gFAAgF;IAChF,QAAQ,CAAC,WAAW,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IACvC;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACnC;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,OAAO,EACb,SAAS,qBAAqB,KAC5B,cAUF,CAAC;AAaF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,+BAA+B,QAAO,KAAK,CAAC,eAAe,CAOvE,CAAC"}
@@ -56,10 +56,11 @@ export const build_full_spine_rpc_actions = (deps, options) => {
56
56
  // Mount the directory-model `cell_gated_create` test policy (twin of the
57
57
  // Rust stub's `TestCellGatedCreateAuthorize`) so the cross-backend
58
58
  // authorizer-parity suite has a known gate: `kind: 'space'` roots are
59
- // admin-only, contributions are gated by the root's `data.policy`, and
60
- // plain parentless creates stay open (the other cell suites are
61
- // unaffected). The handler hands the governing root's `data` to the
62
- // (pure) authorizer.
59
+ // admin-only, contributions are gated by the root's `policy`, and plain
60
+ // parentless creates stay open (the other cell suites are unaffected).
61
+ // The handler hands the governing root's `policy` to the (pure)
62
+ // authorizer, whose `validate_policy` accepts or refuses each policy
63
+ // write.
63
64
  ...create_all_cell_actions({ ...deps, authorize_create: test_cell_gated_create_authorize }, { roles: spine_roles }),
64
65
  ...create_actor_lookup_actions(deps),
65
66
  ...create_actor_search_actions(deps),
@@ -1 +1 @@
1
- {"version":3,"file":"role_grant_offer_lifecycle.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/role_grant_offer_lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAmD9B,OAAO,KAAK,EAAE,wBAAwB,EAAe,MAAM,YAAY,CAAC;AAMxE,qEAAqE;AACrE,MAAM,WAAW,uCAAwC,SAAQ,wBAAwB;IACxF;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,eAAO,MAAM,+CAA+C,GAC3D,SAAS,uCAAuC,KAC9C,IAyRF,CAAC"}
1
+ {"version":3,"file":"role_grant_offer_lifecycle.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/role_grant_offer_lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAiE9B,OAAO,KAAK,EAAE,wBAAwB,EAAe,MAAM,YAAY,CAAC;AAMxE,qEAAqE;AACrE,MAAM,WAAW,uCAAwC,SAAQ,wBAAwB;IACxF;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,eAAO,MAAM,+CAA+C,GAC3D,SAAS,uCAAuC,KAC9C,IAyYF,CAAC"}
@@ -17,6 +17,13 @@ import '../assert_dev_env.js';
17
17
  * - **already decided → 400** `invalid_request` with reason
18
18
  * `role_grant_offer_terminal` — a decline or retract after an accept, a
19
19
  * second decline, a second retract, and either one after the other.
20
+ * - **a grantor's lost authority ends its pending offers.** Revoking a
21
+ * grantor's grant, or deleting its account, supersedes every pending offer
22
+ * it made — gone from the recipient's inbox, an accept answers
23
+ * `role_grant_offer_terminal`, and a `role_grant_offer_supersede` row records
24
+ * it (`role_grant_revoked` with the grant as cause, `account_deleted` with
25
+ * the account). The keeper's own `admin` grant refuses a revoke, 403
26
+ * `cannot_revoke_keeper_admin`.
20
27
  * - **one audit row per success, none per refusal.** A decline writes
21
28
  * `role_grant_offer_decline` attributed to the recipient and targeting the
22
29
  * grantor, a retract `role_grant_offer_retract` attributed to the grantor
@@ -37,11 +44,14 @@ import '../assert_dev_env.js';
37
44
  */
38
45
  import { assert, describe, test } from 'vitest';
39
46
  import { rpc_call } from '../rpc_helpers.js';
40
- import { ERROR_ROLE_GRANT_OFFER_NOT_FOUND, ERROR_ROLE_GRANT_OFFER_TERMINAL } from '../../auth/role_grant_offer_action_specs.js';
47
+ import { ERROR_CANNOT_REVOKE_KEEPER_ADMIN, ERROR_ROLE_GRANT_OFFER_NOT_FOUND, ERROR_ROLE_GRANT_OFFER_TERMINAL } from '../../auth/role_grant_offer_action_specs.js';
48
+ import { AdminAccountListOutput } from '../../auth/admin_action_specs.js';
49
+ import { ROLE_ADMIN } from '../../auth/role_schema.js';
50
+ import { expect_output } from './cross_rpc.js';
41
51
  import { JSONRPC_INVALID_REQUEST } from '../../http/jsonrpc.js';
42
52
  import { JSONRPC_ERROR_CODES } from '../../http/jsonrpc_errors.js';
43
53
  import { assert_audit_metadata_fields, list_audit_events } from './audit_cross_helpers.js';
44
- import { create_pending_role_grant_offer } from './role_grant_cross_helpers.js';
54
+ import { create_active_role_grant, create_pending_role_grant_offer } from './role_grant_cross_helpers.js';
45
55
  import { SPINE_PARTICIPANT_ROLE, SPINE_RPC_PATH } from './spine_surface_constants.js';
46
56
  /** A well-formed UUID that names no offer. */
47
57
  const NIL_UUID = '00000000-0000-0000-0000-000000000000';
@@ -200,4 +210,66 @@ export const describe_role_grant_offer_lifecycle_cross_tests = (options) => {
200
210
  assert.strictEqual(after.retracts.length, before.retracts.length, 'no retract row added');
201
211
  });
202
212
  });
213
+ describe("a grantor's lost authority ends its pending offers", () => {
214
+ /** A grantor holding a global `admin` grant (offer authority) and its id. */
215
+ const setup_grantor = async (fixture, username) => {
216
+ const grantor = await fixture.create_account({ username });
217
+ const grant_id = await create_active_role_grant(fixture, grantor, {
218
+ role: ROLE_ADMIN,
219
+ rpc_path
220
+ });
221
+ return { grantor, grant_id };
222
+ };
223
+ /** The pending `role` offer `grantor` makes to `recipient`. */
224
+ const offer_from = (fixture, grantor, recipient) => create_pending_role_grant_offer(fixture, recipient.account.id, grantor.create_session_headers(), { role, rpc_path });
225
+ const assert_superseded = async (fixture, recipient, offer_id, cause) => {
226
+ assert.ok(!(await is_pending(fixture, recipient, offer_id)), 'gone from the recipient inbox');
227
+ assert_terminal(await accept(fixture, offer_id, recipient.create_session_headers()), 'an accept of the superseded offer');
228
+ const rows = (await list_audit_events(fixture, { event_type: 'role_grant_offer_supersede' }, rpc_path)).filter((e) => e.metadata?.offer_id === offer_id);
229
+ assert.strictEqual(rows.length, 1, 'one role_grant_offer_supersede row');
230
+ assert_audit_metadata_fields(fixture, rows[0].metadata, {
231
+ offer_id,
232
+ role,
233
+ scope_id: null,
234
+ ...cause
235
+ });
236
+ };
237
+ test("revoking the grantor's grant supersedes its pending offers", async () => {
238
+ const fixture = await setup_test();
239
+ const { grantor, grant_id } = await setup_grantor(fixture, 'lost_authority_revoked');
240
+ const recipient = await fixture.create_account({ username: 'lost_authority_to' });
241
+ const offer = await offer_from(fixture, grantor, recipient);
242
+ // another grantor's offer to the same recipient is the control
243
+ const control = await create_offer(fixture, recipient);
244
+ assert_ok(await rpc(fixture, 'role_grant_revoke', { actor_id: grantor.actor.id, role_grant_id: grant_id }, fixture.create_session_headers()), 'revoke');
245
+ await assert_superseded(fixture, recipient, offer.id, {
246
+ reason: 'role_grant_revoked',
247
+ cause_id: grant_id
248
+ });
249
+ assert.ok(await is_pending(fixture, recipient, control.id), "the keeper's offer stays");
250
+ });
251
+ test("deleting the grantor's account supersedes its pending offers", async () => {
252
+ const fixture = await setup_test();
253
+ const { grantor } = await setup_grantor(fixture, 'lost_authority_deleted');
254
+ const recipient = await fixture.create_account({ username: 'lost_authority_del_to' });
255
+ const offer = await offer_from(fixture, grantor, recipient);
256
+ assert_ok(await rpc(fixture, 'account_delete', { account_id: grantor.account.id }, fixture.create_session_headers()), 'account_delete');
257
+ await assert_superseded(fixture, recipient, offer.id, {
258
+ reason: 'account_deleted',
259
+ cause_id: grantor.account.id
260
+ });
261
+ });
262
+ test("the keeper's admin grant refuses a revoke → 403 cannot_revoke_keeper_admin", async () => {
263
+ const fixture = await setup_test();
264
+ const listed = expect_output(await rpc(fixture, 'admin_account_list', {}, fixture.create_session_headers()), AdminAccountListOutput);
265
+ const keeper = listed.accounts.find((a) => a.account.id === fixture.account.id);
266
+ const keeper_admin = keeper?.role_grants.find((g) => g.role === ROLE_ADMIN && g.scope_id === null);
267
+ assert.ok(keeper_admin, "the keeper's global admin grant");
268
+ const refused = await rpc(fixture, 'role_grant_revoke', { actor_id: fixture.actor.id, role_grant_id: keeper_admin.id }, fixture.create_session_headers());
269
+ assert.ok(!refused.ok, 'the keeper keeps its admin grant');
270
+ assert.strictEqual(refused.status, 403);
271
+ assert.strictEqual(refused.error.code, JSONRPC_ERROR_CODES.forbidden);
272
+ assert.deepStrictEqual(refused.error.data, { reason: ERROR_CANNOT_REVOKE_KEEPER_ADMIN });
273
+ });
274
+ });
203
275
  };
@@ -9,38 +9,37 @@ export declare const SPACE_CELL_KIND = "space";
9
9
  */
10
10
  export declare const PARTICIPATION_ROLE = "participant";
11
11
  /**
12
- * A space `data.policy` mirroring the test ladder: gated `post` (moderated) +
12
+ * A space `policy` mirroring the test ladder: gated `post` (moderated) +
13
13
  * `react` (live) + `comment` (live), each on `PARTICIPATION_ROLE`. `comment`
14
14
  * exists so a suite can create a comment under a post (`root_id !==
15
15
  * parent_id`) and prove the policy resolves from the governing root (the
16
16
  * space), not the immediate parent. Shared by the gated-create and moderate
17
- * cross suites.
17
+ * cross suites, which pass it as a root's `cell_create` `policy`.
18
18
  */
19
19
  export declare const TEST_SPACE_POLICY: {
20
- readonly policy: {
21
- readonly post: {
22
- readonly min_role: "participant";
23
- readonly moderation_required: true;
24
- };
25
- readonly react: {
26
- readonly min_role: "participant";
27
- readonly moderation_required: false;
28
- };
29
- readonly comment: {
30
- readonly min_role: "participant";
31
- readonly moderation_required: false;
32
- };
20
+ readonly post: {
21
+ readonly min_role: "participant";
22
+ readonly moderation_required: true;
23
+ };
24
+ readonly react: {
25
+ readonly min_role: "participant";
26
+ readonly moderation_required: false;
27
+ };
28
+ readonly comment: {
29
+ readonly min_role: "participant";
30
+ readonly moderation_required: false;
33
31
  };
34
32
  };
35
- /** A space's per-kind contribution rule (the shape stamped into `space.data.policy`). */
33
+ /** A space's per-kind contribution rule (the shape of each `policy` entry). */
36
34
  export interface ContributionRule {
37
35
  min_role?: string;
38
36
  moderation_required?: boolean;
39
37
  }
40
38
  /**
41
- * The directory-model test authorizer — a **pure** function of the input (it
42
- * reads the governing root's policy off `input.root_data`, supplied by the
43
- * handler). Byte-equivalent with the Rust `TestCellGatedCreateAuthorize`.
39
+ * The directory-model test authorizer — `authorize` is a **pure** function of
40
+ * the input (it reads the governing root's policy off `input.root_policy`,
41
+ * supplied by the handler). Byte-equivalent with the Rust
42
+ * `TestCellGatedCreateAuthorize`.
44
43
  */
45
44
  export declare const test_cell_gated_create_authorize: CellCreateAuthorize;
46
45
  //# sourceMappingURL=test_cell_gated_create_authorize.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"test_cell_gated_create_authorize.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/test_cell_gated_create_authorize.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA4B9B,OAAO,KAAK,EACX,mBAAmB,EAGnB,MAAM,4BAA4B,CAAC;AAGpC,8EAA8E;AAC9E,eAAO,MAAM,eAAe,UAAU,CAAC;AAEvC;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,gBAAyB,CAAC;AAEzD;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;CAMpB,CAAC;AAEX,yFAAyF;AACzF,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAYD;;;;GAIG;AACH,eAAO,MAAM,gCAAgC,EAAE,mBAsB9C,CAAC"}
1
+ {"version":3,"file":"test_cell_gated_create_authorize.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/test_cell_gated_create_authorize.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAiC9B,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAKtE,8EAA8E;AAC9E,eAAO,MAAM,eAAe,UAAU,CAAC;AAEvC;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,gBAAyB,CAAC;AAEzD;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;CAIpB,CAAC;AAEX,+EAA+E;AAC/E,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAwBD;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,EAAE,mBA8B9C,CAAC"}
@@ -13,17 +13,24 @@ import '../assert_dev_env.js';
13
13
  * every other parentless kind stays open, so the plain-create `cell_crud` /
14
14
  * `cell_relations` suites are unaffected.
15
15
  * - **Contribution** (`root_id` set): the governing root's
16
- * `data.policy[kind] = {min_role?, moderation_required?}` decides — a missing
17
- * entry denies, a present `min_role` the actor lacks globally denies, and otherwise it
18
- * admits with the entry's `moderation_required` folded into the verdict. The
19
- * root's `data` arrives in `input.root_data` (the handler read it in-tx), so
20
- * the predicate is **pure** — no DB read of its own (which also dodges the
21
- * single-connection in-memory deadlock a separate handle would hit — pglet, PGlite).
16
+ * `policy[kind] = {min_role?, moderation_required?}` decides — a missing
17
+ * entry (or no policy) denies, a present `min_role` the actor lacks globally
18
+ * denies, and otherwise it admits with the entry's `moderation_required`
19
+ * folded into the verdict. The root's policy arrives in `input.root_policy`
20
+ * (the handler read it in-tx), so the predicate is **pure** — no DB read of
21
+ * its own (which also dodges the single-connection in-memory deadlock a
22
+ * separate handle would hit — pglet, PGlite).
23
+ *
24
+ * Its `validate_policy` accepts a policy whose every entry is an object with
25
+ * an optional string `min_role` and an optional boolean `moderation_required`,
26
+ * and refuses any other with `ERROR_CELL_POLICY_INVALID`.
22
27
  *
23
28
  * @module
24
29
  */
25
30
  import { has_scoped_role } from '../../auth/request_context.js';
26
31
  import { ROLE_ADMIN } from '../../auth/role_schema.js';
32
+ import { ERROR_CELL_POLICY_INVALID } from '../../auth/cell_action_specs.js';
33
+ import { jsonrpc_errors } from '../../http/jsonrpc_errors.js';
27
34
  import { SPINE_PARTICIPANT_ROLE } from './spine_surface_constants.js';
28
35
  /** The admin-only root kind the test policy gates (talk's `space` analog). */
29
36
  export const SPACE_CELL_KIND = 'space';
@@ -34,26 +41,32 @@ export const SPACE_CELL_KIND = 'space';
34
41
  */
35
42
  export const PARTICIPATION_ROLE = SPINE_PARTICIPANT_ROLE;
36
43
  /**
37
- * A space `data.policy` mirroring the test ladder: gated `post` (moderated) +
44
+ * A space `policy` mirroring the test ladder: gated `post` (moderated) +
38
45
  * `react` (live) + `comment` (live), each on `PARTICIPATION_ROLE`. `comment`
39
46
  * exists so a suite can create a comment under a post (`root_id !==
40
47
  * parent_id`) and prove the policy resolves from the governing root (the
41
48
  * space), not the immediate parent. Shared by the gated-create and moderate
42
- * cross suites.
49
+ * cross suites, which pass it as a root's `cell_create` `policy`.
43
50
  */
44
51
  export const TEST_SPACE_POLICY = {
45
- policy: {
46
- post: { min_role: PARTICIPATION_ROLE, moderation_required: true },
47
- react: { min_role: PARTICIPATION_ROLE, moderation_required: false },
48
- comment: { min_role: PARTICIPATION_ROLE, moderation_required: false }
49
- }
52
+ post: { min_role: PARTICIPATION_ROLE, moderation_required: true },
53
+ react: { min_role: PARTICIPATION_ROLE, moderation_required: false },
54
+ comment: { min_role: PARTICIPATION_ROLE, moderation_required: false }
50
55
  };
51
- /** Pull the governing root's `data.policy[kind]` rule, defensively (loose `data`). */
52
- const get_contribution_rule = (data, kind) => {
53
- if (data === null || typeof data !== 'object')
54
- return undefined;
55
- const policy = data.policy;
56
- if (policy === null || typeof policy !== 'object')
56
+ /** Whether `value` is a `ContributionRule` — an object of only those two keys, each its type. */
57
+ const is_contribution_rule = (value) => value !== null &&
58
+ typeof value === 'object' &&
59
+ !Array.isArray(value) &&
60
+ Object.entries(value).every(([key, v]) => {
61
+ if (key === 'min_role')
62
+ return typeof v === 'string';
63
+ if (key === 'moderation_required')
64
+ return typeof v === 'boolean';
65
+ return false;
66
+ });
67
+ /** Pull the governing root's `policy[kind]` rule, defensively. */
68
+ const get_contribution_rule = (policy, kind) => {
69
+ if (policy === null)
57
70
  return undefined;
58
71
  const rule = policy[kind];
59
72
  if (rule === null || typeof rule !== 'object')
@@ -61,29 +74,40 @@ const get_contribution_rule = (data, kind) => {
61
74
  return rule;
62
75
  };
63
76
  /**
64
- * The directory-model test authorizer — a **pure** function of the input (it
65
- * reads the governing root's policy off `input.root_data`, supplied by the
66
- * handler). Byte-equivalent with the Rust `TestCellGatedCreateAuthorize`.
77
+ * The directory-model test authorizer — `authorize` is a **pure** function of
78
+ * the input (it reads the governing root's policy off `input.root_policy`,
79
+ * supplied by the handler). Byte-equivalent with the Rust
80
+ * `TestCellGatedCreateAuthorize`.
67
81
  */
68
- export const test_cell_gated_create_authorize = (auth, input) => {
69
- // Admin bypass — admins create roots + any kind, live immediately. The
70
- // global grant only: a scoped `admin` row bypasses nothing.
71
- if (has_scoped_role(auth, ROLE_ADMIN, null))
72
- return { allow: true, moderation_required: false };
73
- // Root creation (no governing root): `space` is admin-only; other parentless
74
- // kinds stay open (the plain-create suites).
75
- if (input.root_id === null) {
76
- return input.kind === SPACE_CELL_KIND
77
- ? { allow: false }
78
- : { allow: true, moderation_required: false };
82
+ export const test_cell_gated_create_authorize = {
83
+ authorize: (auth, input) => {
84
+ // Admin bypass — admins create roots + any kind, live immediately. The
85
+ // global grant only: a scoped `admin` row bypasses nothing.
86
+ if (has_scoped_role(auth, ROLE_ADMIN, null)) {
87
+ return { allow: true, moderation_required: false };
88
+ }
89
+ // Root creation (no governing root): `space` is admin-only; other
90
+ // parentless kinds stay open (the plain-create suites).
91
+ if (input.root_id === null) {
92
+ return input.kind === SPACE_CELL_KIND
93
+ ? { allow: false }
94
+ : { allow: true, moderation_required: false };
95
+ }
96
+ // Contribution: resolve the governing root's per-kind policy.
97
+ const rule = get_contribution_rule(input.root_policy, input.kind ?? '');
98
+ if (!rule)
99
+ return { allow: false };
100
+ // `min_role` names no scope, so only the global grant satisfies it.
101
+ const admitted = rule.min_role === undefined || has_scoped_role(auth, rule.min_role, null);
102
+ return admitted
103
+ ? { allow: true, moderation_required: rule.moderation_required ?? false }
104
+ : { allow: false };
105
+ },
106
+ validate_policy: (policy) => {
107
+ if (!Object.values(policy).every(is_contribution_rule)) {
108
+ throw jsonrpc_errors.invalid_params('invalid space policy', {
109
+ reason: ERROR_CELL_POLICY_INVALID
110
+ });
111
+ }
79
112
  }
80
- // Contribution: resolve the governing root's per-kind policy.
81
- const rule = get_contribution_rule(input.root_data, input.kind ?? '');
82
- if (!rule)
83
- return { allow: false };
84
- // `min_role` names no scope, so only the global grant satisfies it.
85
- const admitted = rule.min_role === undefined || has_scoped_role(auth, rule.min_role, null);
86
- return admitted
87
- ? { allow: true, moderation_required: rule.moderation_required ?? false }
88
- : { allow: false };
89
113
  };
@@ -60,9 +60,9 @@ import '../assert_dev_env.js';
60
60
  * **Multi-actor upgrade.** One more WS row needs a keeper seeded with a
61
61
  * second actor (`extra_actors`), so it runs only given `multi_actor`: a
62
62
  * multi-actor keeper's session upgrading with no `?acting=` → 400
63
- * `actor_required`, the flat body the TS spine's upgrade-time authorization
64
- * phase answers. The upgrade resolves an actor there, and a multi-actor
65
- * account must name one. A backend whose upgrade resolves no actor admits
63
+ * `actor_required`, the flat body both spines' upgrade-time actor resolution
64
+ * answers. The upgrade resolves an actor there, and a multi-actor account
65
+ * must name one. A backend whose upgrade resolves no actor admits
66
66
  * the socket instead; `multi_actor.xfail` runs the row as `xfail_until` for
67
67
  * it.
68
68
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_app",
3
- "version": "0.149.0",
3
+ "version": "0.150.0",
4
4
  "description": "fullstack app library",
5
5
  "glyph": "🗝",
6
6
  "logo": "logo.svg",