@fuzdev/fuz_app 0.68.0 → 0.70.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 (105) hide show
  1. package/dist/actions/perform_action.d.ts.map +1 -1
  2. package/dist/actions/perform_action.js +10 -3
  3. package/dist/auth/admin_action_specs.d.ts +2 -3
  4. package/dist/auth/admin_action_specs.d.ts.map +1 -1
  5. package/dist/auth/admin_action_specs.js +2 -3
  6. package/dist/auth/admin_actions.d.ts +4 -14
  7. package/dist/auth/admin_actions.d.ts.map +1 -1
  8. package/dist/auth/admin_actions.js +28 -36
  9. package/dist/auth/signup_routes.d.ts +0 -3
  10. package/dist/auth/signup_routes.d.ts.map +1 -1
  11. package/dist/auth/signup_routes.js +9 -3
  12. package/dist/auth/standard_rpc_actions.d.ts +5 -5
  13. package/dist/auth/standard_rpc_actions.js +4 -4
  14. package/dist/server/app_server.d.ts +1 -7
  15. package/dist/server/app_server.d.ts.map +1 -1
  16. package/dist/server/app_server.js +1 -5
  17. package/dist/testing/CLAUDE.md +98 -10
  18. package/dist/testing/app_server.d.ts +34 -0
  19. package/dist/testing/app_server.d.ts.map +1 -1
  20. package/dist/testing/app_server.js +31 -6
  21. package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -1
  22. package/dist/testing/cross_backend/account_lifecycle.js +69 -1
  23. package/dist/testing/cross_backend/actor_lookup.d.ts +10 -0
  24. package/dist/testing/cross_backend/actor_lookup.d.ts.map +1 -0
  25. package/dist/testing/cross_backend/actor_lookup.js +83 -0
  26. package/dist/testing/cross_backend/actor_search.d.ts +6 -0
  27. package/dist/testing/cross_backend/actor_search.d.ts.map +1 -0
  28. package/dist/testing/cross_backend/actor_search.js +92 -0
  29. package/dist/testing/cross_backend/app_settings.d.ts +6 -0
  30. package/dist/testing/cross_backend/app_settings.d.ts.map +1 -0
  31. package/dist/testing/cross_backend/app_settings.js +95 -0
  32. package/dist/testing/cross_backend/backend_config.d.ts +1 -1
  33. package/dist/testing/cross_backend/capabilities.d.ts +0 -9
  34. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  35. package/dist/testing/cross_backend/capabilities.js +0 -1
  36. package/dist/testing/cross_backend/cell_grant_role.d.ts +8 -0
  37. package/dist/testing/cross_backend/cell_grant_role.d.ts.map +1 -0
  38. package/dist/testing/cross_backend/cell_grant_role.js +102 -0
  39. package/dist/testing/cross_backend/conformance_case.d.ts +144 -0
  40. package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -0
  41. package/dist/testing/cross_backend/conformance_case.js +132 -0
  42. package/dist/testing/cross_backend/conformance_table.d.ts +46 -0
  43. package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -0
  44. package/dist/testing/cross_backend/conformance_table.js +199 -0
  45. package/dist/testing/cross_backend/create_cross_backend_global_setup.d.ts +57 -0
  46. package/dist/testing/cross_backend/create_cross_backend_global_setup.d.ts.map +1 -0
  47. package/dist/testing/cross_backend/create_cross_backend_global_setup.js +31 -0
  48. package/dist/testing/cross_backend/default_backend_configs.d.ts +13 -0
  49. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  50. package/dist/testing/cross_backend/default_backend_configs.js +4 -6
  51. package/dist/testing/cross_backend/default_spine_surface.d.ts +17 -9
  52. package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -1
  53. package/dist/testing/cross_backend/default_spine_surface.js +20 -12
  54. package/dist/testing/cross_backend/make_cross_backend_project.d.ts +72 -0
  55. package/dist/testing/cross_backend/make_cross_backend_project.d.ts.map +1 -0
  56. package/dist/testing/cross_backend/make_cross_backend_project.js +51 -0
  57. package/dist/testing/cross_backend/origin.d.ts +10 -0
  58. package/dist/testing/cross_backend/origin.d.ts.map +1 -0
  59. package/dist/testing/cross_backend/origin.js +73 -0
  60. package/dist/testing/cross_backend/setup.d.ts +22 -40
  61. package/dist/testing/cross_backend/setup.d.ts.map +1 -1
  62. package/dist/testing/cross_backend/setup.js +34 -5
  63. package/dist/testing/cross_backend/standard.d.ts +8 -0
  64. package/dist/testing/cross_backend/standard.d.ts.map +1 -1
  65. package/dist/testing/cross_backend/standard.js +1 -0
  66. package/dist/testing/cross_backend/testing_reset_actions.d.ts +102 -10
  67. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  68. package/dist/testing/cross_backend/testing_reset_actions.js +96 -5
  69. package/dist/testing/cross_backend/xfail.d.ts +15 -0
  70. package/dist/testing/cross_backend/xfail.d.ts.map +1 -0
  71. package/dist/testing/cross_backend/xfail.js +37 -0
  72. package/dist/testing/integration.d.ts +2 -3
  73. package/dist/testing/integration.d.ts.map +1 -1
  74. package/dist/testing/integration.js +40 -88
  75. package/dist/testing/rate_limiting.d.ts +1 -1
  76. package/dist/testing/rpc_helpers.d.ts +3 -3
  77. package/dist/testing/sse_round_trip.d.ts +1 -1
  78. package/dist/testing/stubs.d.ts.map +1 -1
  79. package/dist/testing/stubs.js +0 -1
  80. package/dist/ui/AdminAccounts.svelte +74 -83
  81. package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
  82. package/dist/ui/AdminSessions.svelte +21 -23
  83. package/dist/ui/AdminSessions.svelte.d.ts.map +1 -1
  84. package/dist/ui/CLAUDE.md +17 -26
  85. package/dist/ui/OpenSignupToggle.svelte +2 -5
  86. package/dist/ui/OpenSignupToggle.svelte.d.ts.map +1 -1
  87. package/dist/ui/account_sessions_state.svelte.d.ts +9 -10
  88. package/dist/ui/account_sessions_state.svelte.d.ts.map +1 -1
  89. package/dist/ui/account_sessions_state.svelte.js +7 -17
  90. package/dist/ui/admin_accounts_state.svelte.d.ts +12 -19
  91. package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
  92. package/dist/ui/admin_accounts_state.svelte.js +10 -24
  93. package/dist/ui/admin_invites_state.svelte.d.ts +8 -11
  94. package/dist/ui/admin_invites_state.svelte.d.ts.map +1 -1
  95. package/dist/ui/admin_invites_state.svelte.js +7 -16
  96. package/dist/ui/admin_sessions_state.svelte.d.ts +6 -10
  97. package/dist/ui/admin_sessions_state.svelte.d.ts.map +1 -1
  98. package/dist/ui/admin_sessions_state.svelte.js +4 -14
  99. package/dist/ui/app_settings_state.svelte.d.ts +8 -12
  100. package/dist/ui/app_settings_state.svelte.d.ts.map +1 -1
  101. package/dist/ui/app_settings_state.svelte.js +6 -16
  102. package/dist/ui/audit_log_state.svelte.d.ts +9 -8
  103. package/dist/ui/audit_log_state.svelte.d.ts.map +1 -1
  104. package/dist/ui/audit_log_state.svelte.js +8 -20
  105. package/package.json +1 -1
@@ -0,0 +1,95 @@
1
+ import '../assert_dev_env.js';
2
+ /**
3
+ * Cross-backend effect suite for the `open_signup` app setting.
4
+ *
5
+ * The declarative conformance table pins the admin gate on
6
+ * `app_settings_get` / `app_settings_update` (401 / 403 / 200). This suite
7
+ * pins the **behavioral effect** of the toggle end to end: an admin flips
8
+ * `open_signup` via `app_settings_update`, and a subsequent anonymous
9
+ * `POST /signup` observes the new value.
10
+ *
11
+ * - **toggle on → anonymous signup without an invite succeeds (200)** — with
12
+ * `open_signup: true`, the invite gate is skipped.
13
+ * - **toggle off → anonymous signup is refused (403 `no_matching_invite`)** —
14
+ * flipping it back restores the invite requirement, proving the gate keys
15
+ * on the live value rather than a one-time read.
16
+ *
17
+ * The signup handler reads the toggle fresh from the database on every
18
+ * request, so the admin's write is visible to the next signup. This suite
19
+ * runs in a single process, so it validates the read-through *mechanism* —
20
+ * not multi-process consistency (which the fresh-read shape provides by
21
+ * construction but no single-binary test can observe).
22
+ *
23
+ * Cites `security.md` §Signup. Runs both legs via the shared `{setup_test}`
24
+ * protocol: in-process (`auth/app_settings_parity.db.test.ts`) +
25
+ * cross-process (`cross_backend/app_settings.cross.test.ts`, TS spine
26
+ * binaries + Rust `testing_spine_stub`). Mounted on every spine, so the
27
+ * suite is ungated.
28
+ *
29
+ * `$lib`-free by contract (relative specifiers only).
30
+ *
31
+ * @module
32
+ */
33
+ import { describe, test, assert } from 'vitest';
34
+ import { app_settings_update_action_spec } from '../../auth/admin_action_specs.js';
35
+ import { ERROR_NO_MATCHING_INVITE } from '../../http/error_schemas.js';
36
+ import { SPINE_RPC_PATH } from './default_spine_surface.js';
37
+ /** REST signup path on the spine surface (`/api/account` prefix + `/signup`). */
38
+ const SIGNUP_PATH = '/api/account/signup';
39
+ /** A password that satisfies the creation-strength schema (min 12). */
40
+ const SIGNUP_PASSWORD = 'securepassword123';
41
+ /** Build the JSON-RPC envelope for an `app_settings_update` call. */
42
+ const update_envelope = (open_signup, id) => JSON.stringify({
43
+ jsonrpc: '2.0',
44
+ method: app_settings_update_action_spec.method,
45
+ params: { open_signup },
46
+ id,
47
+ });
48
+ export const describe_app_settings_cross_tests = (options) => {
49
+ const { setup_test } = options;
50
+ const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
51
+ describe('app_settings open_signup effect', () => {
52
+ test('admin enables open_signup → anonymous signup without invite succeeds', async () => {
53
+ const fixture = await setup_test();
54
+ const enable = await fixture.transport(rpc_path, {
55
+ method: 'POST',
56
+ headers: { ...fixture.create_session_headers(), 'content-type': 'application/json' },
57
+ body: update_envelope(true, 'enable-open-signup'),
58
+ });
59
+ assert.strictEqual(enable.status, 200, 'admin app_settings_update must succeed');
60
+ const res = await fixture.fresh_transport()(SIGNUP_PATH, {
61
+ method: 'POST',
62
+ headers: { 'content-type': 'application/json' },
63
+ body: JSON.stringify({ username: 'open_signup_user', password: SIGNUP_PASSWORD }),
64
+ });
65
+ assert.strictEqual(res.status, 200, 'open signup must admit an anonymous account');
66
+ const body = (await res.json());
67
+ assert.strictEqual(body.ok, true, 'signup response reports success');
68
+ });
69
+ test('admin disables open_signup → anonymous signup is refused (no_matching_invite)', async () => {
70
+ const fixture = await setup_test();
71
+ // Enable then disable so the assertion proves the *flip back* takes
72
+ // effect, not merely the closed default.
73
+ const enable = await fixture.transport(rpc_path, {
74
+ method: 'POST',
75
+ headers: { ...fixture.create_session_headers(), 'content-type': 'application/json' },
76
+ body: update_envelope(true, 'enable-before-disable'),
77
+ });
78
+ assert.strictEqual(enable.status, 200, 'admin app_settings_update (enable) must succeed');
79
+ const disable = await fixture.transport(rpc_path, {
80
+ method: 'POST',
81
+ headers: { ...fixture.create_session_headers(), 'content-type': 'application/json' },
82
+ body: update_envelope(false, 'disable-open-signup'),
83
+ });
84
+ assert.strictEqual(disable.status, 200, 'admin app_settings_update (disable) must succeed');
85
+ const res = await fixture.fresh_transport()(SIGNUP_PATH, {
86
+ method: 'POST',
87
+ headers: { 'content-type': 'application/json' },
88
+ body: JSON.stringify({ username: 'closed_signup_user', password: SIGNUP_PASSWORD }),
89
+ });
90
+ assert.strictEqual(res.status, 403, 'closed signup must refuse a no-invite anonymous account');
91
+ const body = (await res.json());
92
+ assert.strictEqual(body.error, ERROR_NO_MATCHING_INVITE, 'rejection carries the no-matching-invite reason');
93
+ });
94
+ });
95
+ };
@@ -106,7 +106,7 @@ export interface BackendConfig {
106
106
  /**
107
107
  * Capabilities this backend supports — drives `test_if(capabilities.X, ...)`
108
108
  * gating in suite bodies. See `capabilities.ts` for the vocabulary and
109
- * existing flags. Cross-process backends set `in_process_only: false`.
109
+ * existing flags.
110
110
  */
111
111
  readonly capabilities: BackendCapabilities;
112
112
  }
@@ -66,15 +66,6 @@ export interface BackendCapabilities {
66
66
  * this flag opts a backend into the lifecycle parity coverage.
67
67
  */
68
68
  readonly account_lifecycle: boolean;
69
- /**
70
- * Test has direct access to backend-internal state (keyring for
71
- * signing cookies, DB pool for FK-structural raw queries). Always
72
- * `true` for in-process Hono via `default_in_process_setup`; always
73
- * `false` cross-process. Gates the 3 keyring reads in
74
- * `describe_standard_integration_tests` (expired-cookie generation)
75
- * and the FK-structural raw query in `describe_audit_completeness_tests`.
76
- */
77
- readonly in_process_only: boolean;
78
69
  }
79
70
  /**
80
71
  * Capability declarations for the in-process Hono transport. Every flag
@@ -1 +1 @@
1
- {"version":3,"file":"capabilities.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAmB9B;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IACnC;;;;OAIG;IACH,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC;;;OAGG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;;;;;OAQG;IACH,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;IACpC;;;;;;;OAOG;IACH,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;CAClC;AAED;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBAUpC,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,OAAO,GAAI,MAAM,OAAO,EAAE,MAAM,MAAM,EAAE,IAAI,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAG,IAMrF,CAAC"}
1
+ {"version":3,"file":"capabilities.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAmB9B;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IACnC;;;;OAIG;IACH,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC;;;OAGG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;;;;;OAQG;IACH,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;CACpC;AAED;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBASpC,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,OAAO,GAAI,MAAM,OAAO,EAAE,MAAM,MAAM,EAAE,IAAI,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAG,IAMrF,CAAC"}
@@ -29,7 +29,6 @@ export const in_process_capabilities = Object.freeze({
29
29
  cell_crud: true,
30
30
  cell_relations: true,
31
31
  account_lifecycle: true,
32
- in_process_only: true,
33
32
  });
34
33
  /**
35
34
  * Conditional `test()` wrapper — registers a vitest case only when
@@ -0,0 +1,8 @@
1
+ import '../assert_dev_env.js';
2
+ import { type CellCrossTestOptions } from './cell_cross_helpers.js';
3
+ /** App role the holder is seeded with; matches the spine's registered role. */
4
+ export declare const CELL_EDITOR_ROLE = "cell_editor";
5
+ /** Username the fixture seeds (via `extra_accounts`) holding `CELL_EDITOR_ROLE`. */
6
+ export declare const CELL_ROLE_HOLDER_USERNAME = "cell_role_holder";
7
+ export declare const describe_cell_grant_role_cross_tests: (options: CellCrossTestOptions) => void;
8
+ //# sourceMappingURL=cell_grant_role.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cell_grant_role.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_grant_role.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA6C9B,OAAO,EACN,KAAK,oBAAoB,EAIzB,MAAM,yBAAyB,CAAC;AAGjC,+EAA+E;AAC/E,eAAO,MAAM,gBAAgB,gBAAyB,CAAC;AAEvD,oFAAoF;AACpF,eAAO,MAAM,yBAAyB,qBAAqB,CAAC;AAK5D,eAAO,MAAM,oCAAoC,GAAI,SAAS,oBAAoB,KAAG,IAuIpF,CAAC"}
@@ -0,0 +1,102 @@
1
+ import '../assert_dev_env.js';
2
+ /**
3
+ * Cross-backend parity suite for **role-shaped** `cell_grant`s.
4
+ *
5
+ * The cell CRUD / relations suites exercise only actor-shaped grant
6
+ * principals. This suite covers the role-shaped path and its closed-registry
7
+ * gate — the security-correctness property that the Rust spine previously
8
+ * lacked (it created inert grant rows for any role string). Both spines now
9
+ * validate the role against a closed registry at create.
10
+ *
11
+ * - **role grant admits a holder; excludes a non-holder** — an owner grants
12
+ * `{role}` on a private cell; an account holding that role can `cell_get`
13
+ * it (200), an account without it gets the IDOR-mask 404.
14
+ * - **unknown role rejected at create (security-correctness)** — granting a
15
+ * role outside the registry is `invalid_params` / `cell_grant_unknown_role`,
16
+ * not a silent inert row.
17
+ * - **editor-level role grant admits edit** — a holder of an `editor`-level
18
+ * role grant can `cell_update` the cell's content.
19
+ *
20
+ * The holder is seeded via `extra_accounts` under `CELL_ROLE_HOLDER_USERNAME`
21
+ * holding `CELL_EDITOR_ROLE` (the role has no grant path, so it can't be
22
+ * offered — the bootstrap-cradle seed is the only path). Both legs configure
23
+ * that seed and register `CELL_EDITOR_ROLE` in their role registry; the Rust
24
+ * `testing_spine_stub` mirrors the same membership in its `known_roles`.
25
+ *
26
+ * Cites `security.md` §Authorization (role-shaped cell-grant validation).
27
+ * Runs both legs via the shared `{setup_test}` protocol: in-process
28
+ * (`auth/cell_grant_role_parity.db.test.ts`) + cross-process
29
+ * (`cross_backend/cell_grant_role.cross.test.ts`). Gated on
30
+ * `capabilities.cell_relations` (true on every spine, so it never skips).
31
+ *
32
+ * `$lib`-free by contract (relative specifiers only).
33
+ *
34
+ * @module
35
+ */
36
+ import { describe, assert } from 'vitest';
37
+ import { CellCreateOutput, CellGetOutput, CellUpdateOutput } from '../../auth/cell_action_specs.js';
38
+ import { CellGrantCreateOutput, ERROR_CELL_GRANT_UNKNOWN_ROLE, } from '../../auth/cell_grant_action_specs.js';
39
+ import { test_if } from './capabilities.js';
40
+ import { cross_rpc_call, error_reason, expect_output, } from './cell_cross_helpers.js';
41
+ import { SPINE_CELL_EDITOR_ROLE, SPINE_RPC_PATH } from './default_spine_surface.js';
42
+ /** App role the holder is seeded with; matches the spine's registered role. */
43
+ export const CELL_EDITOR_ROLE = SPINE_CELL_EDITOR_ROLE;
44
+ /** Username the fixture seeds (via `extra_accounts`) holding `CELL_EDITOR_ROLE`. */
45
+ export const CELL_ROLE_HOLDER_USERNAME = 'cell_role_holder';
46
+ /** A role string deliberately absent from the registry. */
47
+ const UNREGISTERED_ROLE = 'not_a_registered_role';
48
+ export const describe_cell_grant_role_cross_tests = (options) => {
49
+ const { setup_test, capabilities } = options;
50
+ const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
51
+ describe('cell_grant role-shaped parity', () => {
52
+ test_if(capabilities.cell_relations, 'role grant admits a holder of the role and excludes a non-holder', async () => {
53
+ const fixture = await setup_test();
54
+ const t = fixture.transport;
55
+ const owner = await fixture.create_account({ username: 'cell_role_owner' });
56
+ const owner_h = owner.create_session_headers();
57
+ const holder = fixture.extra_accounts[CELL_ROLE_HOLDER_USERNAME];
58
+ assert.ok(holder, `fixture must seed the ${CELL_ROLE_HOLDER_USERNAME} extra account`);
59
+ const stranger = await fixture.create_account({ username: 'cell_role_stranger' });
60
+ // Owner creates a private cell (default visibility) and grants
61
+ // view access to anyone holding CELL_EDITOR_ROLE.
62
+ const created = expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { data: { kind: 'note' } }, owner_h), CellCreateOutput);
63
+ const cell_id = created.cell.id;
64
+ expect_output(await cross_rpc_call(t, rpc_path, 'cell_grant_create', { cell_id, level: 'viewer', principal: { kind: 'role', role: CELL_EDITOR_ROLE } }, owner_h), CellGrantCreateOutput);
65
+ // Holder of the role is admitted through the role-shaped grant.
66
+ const holder_view = expect_output(await cross_rpc_call(t, rpc_path, 'cell_get', { id: cell_id }, holder.create_session_headers()), CellGetOutput);
67
+ assert.strictEqual(holder_view.cell.id, cell_id, 'holder sees the granted cell');
68
+ // A non-holder sees the IDOR-mask 404 — the grant keys on the role,
69
+ // not mere authentication.
70
+ const stranger_view = await cross_rpc_call(t, rpc_path, 'cell_get', { id: cell_id }, stranger.create_session_headers());
71
+ assert.ok(!stranger_view.ok, 'non-holder must not see the cell');
72
+ assert.strictEqual(error_reason(stranger_view), 'cell_not_found');
73
+ });
74
+ test_if(capabilities.cell_relations, 'role-shaped grant for an unregistered role is rejected at create', async () => {
75
+ const fixture = await setup_test();
76
+ const t = fixture.transport;
77
+ const owner = await fixture.create_account({ username: 'cell_unknown_role_owner' });
78
+ const owner_h = owner.create_session_headers();
79
+ const created = expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { data: { kind: 'note' } }, owner_h), CellCreateOutput);
80
+ const denied = await cross_rpc_call(t, rpc_path, 'cell_grant_create', {
81
+ cell_id: created.cell.id,
82
+ level: 'viewer',
83
+ principal: { kind: 'role', role: UNREGISTERED_ROLE },
84
+ }, owner_h);
85
+ assert.ok(!denied.ok, 'granting an unregistered role must fail');
86
+ assert.strictEqual(error_reason(denied), ERROR_CELL_GRANT_UNKNOWN_ROLE);
87
+ });
88
+ test_if(capabilities.cell_relations, 'editor-level role grant admits content edit', async () => {
89
+ const fixture = await setup_test();
90
+ const t = fixture.transport;
91
+ const owner = await fixture.create_account({ username: 'cell_role_edit_owner' });
92
+ const owner_h = owner.create_session_headers();
93
+ const holder = fixture.extra_accounts[CELL_ROLE_HOLDER_USERNAME];
94
+ assert.ok(holder, `fixture must seed the ${CELL_ROLE_HOLDER_USERNAME} extra account`);
95
+ const created = expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { data: { kind: 'note' } }, owner_h), CellCreateOutput);
96
+ const cell_id = created.cell.id;
97
+ expect_output(await cross_rpc_call(t, rpc_path, 'cell_grant_create', { cell_id, level: 'editor', principal: { kind: 'role', role: CELL_EDITOR_ROLE } }, owner_h), CellGrantCreateOutput);
98
+ const edited = expect_output(await cross_rpc_call(t, rpc_path, 'cell_update', { cell_id, data: { kind: 'note', label: 'by role editor' } }, holder.create_session_headers()), CellUpdateOutput);
99
+ assert.strictEqual(edited.cell.updated_by, holder.actor.id, 'edit attributed to the holder');
100
+ });
101
+ });
102
+ };
@@ -0,0 +1,144 @@
1
+ import '../assert_dev_env.js';
2
+ /**
3
+ * Declarative conformance-case schema for the cross-backend behavioral +
4
+ * security suite.
5
+ *
6
+ * A conformance case is a single request → expected-response assertion,
7
+ * carried as **data**. The case references a `method` (an RPC method name
8
+ * or a REST auth-route suffix); the runner
9
+ * (`describe_conformance_table_tests`) resolves the `input` / `output`
10
+ * Zod schemas from the live action-spec registry / `RouteSpec` — the case
11
+ * never carries a schema. This is the opinionated behavioral/security
12
+ * layer on top of the spec-derived auto-enumeration
13
+ * (`describe_rpc_round_trip_tests` / `describe_rpc_attack_surface_tests`):
14
+ * the same case definition runs in-process (fast, every `gro test`) and
15
+ * cross-process (the conformance gate) against each impl's real auth
16
+ * resolution.
17
+ *
18
+ * The table is for single-request matrices (credential-type ceiling,
19
+ * privilege gates, IDOR masks, enumeration-equivalence, validation).
20
+ * Multi-step flows stay imperative in their own `describe_*` suites,
21
+ * sharing assertion primitives — there is deliberately no declarative
22
+ * setup DSL.
23
+ *
24
+ * @module
25
+ */
26
+ import { z } from 'zod';
27
+ /**
28
+ * Closed enum of fixture-provisioned principals a case runs `as`. Each
29
+ * value maps to a `TestFixture` accessor (or a seeded `extra_accounts`
30
+ * entry) in the runner's `resolve_principal` — there is **no** inline
31
+ * credential minting in a case (that would be the setup-DSL trap).
32
+ *
33
+ * - `keeper` — the per-test bootstrapped keeper (holds `ROLE_KEEPER` +
34
+ * `ROLE_ADMIN`), session credential.
35
+ * - `daemon` — the keeper authenticated via the daemon-token header.
36
+ * - `token` — the keeper authenticated via a bearer api-token (non-browser
37
+ * context; the runner suppresses `Origin` so the token isn't discarded).
38
+ * - `anonymous` — no credential, fresh cookie jar.
39
+ * - `fresh_non_admin` — a freshly minted account with no roles, session
40
+ * credential (via the production invite → signup → login flow).
41
+ * - `role_holder` — a seeded `extra_accounts` principal holding a specific
42
+ * role; the runner reads it by the username named in
43
+ * `ConformanceTableOptions.principals.role_holder`.
44
+ * - `wrong_role` — a seeded `extra_accounts` principal holding a role
45
+ * other than the one a route requires; named via
46
+ * `ConformanceTableOptions.principals.wrong_role`.
47
+ * - `expired_session` — the keeper account presented via an *expired
48
+ * server-side session* cookie (minted by `fixture.mint_expired_session()`:
49
+ * a backdated `auth_session` row behind a still-valid signed cookie
50
+ * payload, so the authoritative DB-row expiry gate is what refuses it).
51
+ */
52
+ export declare const ConformancePrincipal: z.ZodEnum<{
53
+ keeper: "keeper";
54
+ token: "token";
55
+ daemon: "daemon";
56
+ anonymous: "anonymous";
57
+ fresh_non_admin: "fresh_non_admin";
58
+ role_holder: "role_holder";
59
+ wrong_role: "wrong_role";
60
+ expired_session: "expired_session";
61
+ }>;
62
+ export type ConformancePrincipal = z.infer<typeof ConformancePrincipal>;
63
+ /** The request a conformance case issues. */
64
+ export declare const ConformanceCaseRequest: z.ZodObject<{
65
+ method: z.ZodString;
66
+ params: z.ZodOptional<z.ZodUnknown>;
67
+ as: z.ZodEnum<{
68
+ keeper: "keeper";
69
+ token: "token";
70
+ daemon: "daemon";
71
+ anonymous: "anonymous";
72
+ fresh_non_admin: "fresh_non_admin";
73
+ role_holder: "role_holder";
74
+ wrong_role: "wrong_role";
75
+ expired_session: "expired_session";
76
+ }>;
77
+ verb: z.ZodOptional<z.ZodEnum<{
78
+ GET: "GET";
79
+ POST: "POST";
80
+ }>>;
81
+ }, z.core.$strict>;
82
+ export type ConformanceCaseRequest = z.infer<typeof ConformanceCaseRequest>;
83
+ /** The expected response shape a conformance case asserts. */
84
+ export declare const ConformanceCaseExpectation: z.ZodObject<{
85
+ status: z.ZodNumber;
86
+ error_reason: z.ZodOptional<z.ZodString>;
87
+ fields: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
88
+ }, z.core.$strict>;
89
+ export type ConformanceCaseExpectation = z.infer<typeof ConformanceCaseExpectation>;
90
+ /**
91
+ * Marks a case as a deferred-by-design gap. The runner routes it through
92
+ * `xfail_until` instead of a normal `test` — visible (distinct from pass)
93
+ * and self-cleaning (flips red when the impl starts passing, forcing the
94
+ * marker's removal). Use for declared gaps (e.g. facts), never for
95
+ * in-scope gaps (those fail loud as a red `test`).
96
+ */
97
+ export declare const ConformanceCaseXfail: z.ZodObject<{
98
+ tracking_id: z.ZodString;
99
+ reason: z.ZodString;
100
+ }, z.core.$strict>;
101
+ export type ConformanceCaseXfail = z.infer<typeof ConformanceCaseXfail>;
102
+ /**
103
+ * A single conformance case. `name` is the assertion; the optional
104
+ * free-text `note` is printed in the test label / failure output. A
105
+ * security case's `note` should reference a **public** fuz_app doc
106
+ * property (`security.md` / `architecture.md` / module TSDoc), since the
107
+ * table ships in a public package — not an internal planning doc. The note
108
+ * is documentation, not a gate: it stays free-text by design because a
109
+ * non-empty-string check never catches a *wrong* citation — the citation
110
+ * is verified in review.
111
+ */
112
+ export declare const ConformanceCase: z.ZodObject<{
113
+ name: z.ZodString;
114
+ request: z.ZodObject<{
115
+ method: z.ZodString;
116
+ params: z.ZodOptional<z.ZodUnknown>;
117
+ as: z.ZodEnum<{
118
+ keeper: "keeper";
119
+ token: "token";
120
+ daemon: "daemon";
121
+ anonymous: "anonymous";
122
+ fresh_non_admin: "fresh_non_admin";
123
+ role_holder: "role_holder";
124
+ wrong_role: "wrong_role";
125
+ expired_session: "expired_session";
126
+ }>;
127
+ verb: z.ZodOptional<z.ZodEnum<{
128
+ GET: "GET";
129
+ POST: "POST";
130
+ }>>;
131
+ }, z.core.$strict>;
132
+ expect: z.ZodObject<{
133
+ status: z.ZodNumber;
134
+ error_reason: z.ZodOptional<z.ZodString>;
135
+ fields: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
136
+ }, z.core.$strict>;
137
+ note: z.ZodOptional<z.ZodString>;
138
+ xfail: z.ZodOptional<z.ZodObject<{
139
+ tracking_id: z.ZodString;
140
+ reason: z.ZodString;
141
+ }, z.core.$strict>>;
142
+ }, z.core.$strict>;
143
+ export type ConformanceCase = z.infer<typeof ConformanceCase>;
144
+ //# sourceMappingURL=conformance_case.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conformance_case.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/conformance_case.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;EAS/B,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAExE,6CAA6C;AAC7C,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;kBAgBjC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAE5E,8DAA8D;AAC9D,eAAO,MAAM,0BAA0B;;;;kBAqBrC,CAAC;AACH,MAAM,MAAM,0BAA0B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAEpF;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB;;;kBAK/B,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAExE;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAS1B,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC"}
@@ -0,0 +1,132 @@
1
+ import '../assert_dev_env.js';
2
+ /**
3
+ * Declarative conformance-case schema for the cross-backend behavioral +
4
+ * security suite.
5
+ *
6
+ * A conformance case is a single request → expected-response assertion,
7
+ * carried as **data**. The case references a `method` (an RPC method name
8
+ * or a REST auth-route suffix); the runner
9
+ * (`describe_conformance_table_tests`) resolves the `input` / `output`
10
+ * Zod schemas from the live action-spec registry / `RouteSpec` — the case
11
+ * never carries a schema. This is the opinionated behavioral/security
12
+ * layer on top of the spec-derived auto-enumeration
13
+ * (`describe_rpc_round_trip_tests` / `describe_rpc_attack_surface_tests`):
14
+ * the same case definition runs in-process (fast, every `gro test`) and
15
+ * cross-process (the conformance gate) against each impl's real auth
16
+ * resolution.
17
+ *
18
+ * The table is for single-request matrices (credential-type ceiling,
19
+ * privilege gates, IDOR masks, enumeration-equivalence, validation).
20
+ * Multi-step flows stay imperative in their own `describe_*` suites,
21
+ * sharing assertion primitives — there is deliberately no declarative
22
+ * setup DSL.
23
+ *
24
+ * @module
25
+ */
26
+ import { z } from 'zod';
27
+ /**
28
+ * Closed enum of fixture-provisioned principals a case runs `as`. Each
29
+ * value maps to a `TestFixture` accessor (or a seeded `extra_accounts`
30
+ * entry) in the runner's `resolve_principal` — there is **no** inline
31
+ * credential minting in a case (that would be the setup-DSL trap).
32
+ *
33
+ * - `keeper` — the per-test bootstrapped keeper (holds `ROLE_KEEPER` +
34
+ * `ROLE_ADMIN`), session credential.
35
+ * - `daemon` — the keeper authenticated via the daemon-token header.
36
+ * - `token` — the keeper authenticated via a bearer api-token (non-browser
37
+ * context; the runner suppresses `Origin` so the token isn't discarded).
38
+ * - `anonymous` — no credential, fresh cookie jar.
39
+ * - `fresh_non_admin` — a freshly minted account with no roles, session
40
+ * credential (via the production invite → signup → login flow).
41
+ * - `role_holder` — a seeded `extra_accounts` principal holding a specific
42
+ * role; the runner reads it by the username named in
43
+ * `ConformanceTableOptions.principals.role_holder`.
44
+ * - `wrong_role` — a seeded `extra_accounts` principal holding a role
45
+ * other than the one a route requires; named via
46
+ * `ConformanceTableOptions.principals.wrong_role`.
47
+ * - `expired_session` — the keeper account presented via an *expired
48
+ * server-side session* cookie (minted by `fixture.mint_expired_session()`:
49
+ * a backdated `auth_session` row behind a still-valid signed cookie
50
+ * payload, so the authoritative DB-row expiry gate is what refuses it).
51
+ */
52
+ export const ConformancePrincipal = z.enum([
53
+ 'keeper',
54
+ 'daemon',
55
+ 'token',
56
+ 'anonymous',
57
+ 'fresh_non_admin',
58
+ 'role_holder',
59
+ 'wrong_role',
60
+ 'expired_session',
61
+ ]);
62
+ /** The request a conformance case issues. */
63
+ export const ConformanceCaseRequest = z.strictObject({
64
+ method: z.string().meta({
65
+ description: 'RPC method name (e.g. `admin_account_list`) or a REST auth-route suffix ' +
66
+ '(e.g. `/login`). A leading `/` selects the REST branch; otherwise the ' +
67
+ 'runner resolves the RPC action from the spec registry.',
68
+ }),
69
+ params: z
70
+ .unknown()
71
+ .optional()
72
+ .meta({ description: 'Request params / body. Omit for nullary methods.' }),
73
+ as: ConformancePrincipal,
74
+ verb: z
75
+ .enum(['POST', 'GET'])
76
+ .optional()
77
+ .meta({ description: 'HTTP verb. Defaults to POST; use GET for `side_effects: false` reads.' }),
78
+ });
79
+ /** The expected response shape a conformance case asserts. */
80
+ export const ConformanceCaseExpectation = z.strictObject({
81
+ status: z.number().int().meta({ description: 'Expected HTTP status code.' }),
82
+ error_reason: z
83
+ .string()
84
+ .optional()
85
+ .meta({
86
+ description: 'Expected error reason — pass the IMPORTED `ERROR_*` constant from ' +
87
+ '`http/error_schemas.ts`, never a string literal. Asserted against the RPC ' +
88
+ '`error.data.reason` (when the denial carries one) or the REST flat-body ' +
89
+ '`error` field. The pre-validation 401 carries `data.reason` too; a denial ' +
90
+ 'that genuinely omits it falls back to the `status` assertion to pin the class.',
91
+ }),
92
+ fields: z
93
+ .record(z.string(), z.unknown())
94
+ .optional()
95
+ .meta({
96
+ description: 'Specific field-value assertions on the success `result` (2xx) or the error ' +
97
+ '`error.data` (non-2xx). Each key must deep-equal the corresponding response field.',
98
+ }),
99
+ });
100
+ /**
101
+ * Marks a case as a deferred-by-design gap. The runner routes it through
102
+ * `xfail_until` instead of a normal `test` — visible (distinct from pass)
103
+ * and self-cleaning (flips red when the impl starts passing, forcing the
104
+ * marker's removal). Use for declared gaps (e.g. facts), never for
105
+ * in-scope gaps (those fail loud as a red `test`).
106
+ */
107
+ export const ConformanceCaseXfail = z.strictObject({
108
+ tracking_id: z
109
+ .string()
110
+ .meta({ description: 'Tracking id for the deferred gap (issue id or tracking slug).' }),
111
+ reason: z.string().meta({ description: 'Why this case is deferred-by-design.' }),
112
+ });
113
+ /**
114
+ * A single conformance case. `name` is the assertion; the optional
115
+ * free-text `note` is printed in the test label / failure output. A
116
+ * security case's `note` should reference a **public** fuz_app doc
117
+ * property (`security.md` / `architecture.md` / module TSDoc), since the
118
+ * table ships in a public package — not an internal planning doc. The note
119
+ * is documentation, not a gate: it stays free-text by design because a
120
+ * non-empty-string check never catches a *wrong* citation — the citation
121
+ * is verified in review.
122
+ */
123
+ export const ConformanceCase = z.strictObject({
124
+ name: z.string().meta({ description: 'The assertion, used as the test label.' }),
125
+ request: ConformanceCaseRequest,
126
+ expect: ConformanceCaseExpectation,
127
+ note: z
128
+ .string()
129
+ .optional()
130
+ .meta({ description: 'Free-text note printed in the label / failure output.' }),
131
+ xfail: ConformanceCaseXfail.optional(),
132
+ });
@@ -0,0 +1,46 @@
1
+ import '../assert_dev_env.js';
2
+ import type { AppSurfaceSpec } from '../../http/surface.js';
3
+ import type { SessionOptions } from '../../auth/session_cookie.js';
4
+ import { type RpcEndpointsSuiteOption } from '../rpc_helpers.js';
5
+ import { type ConformanceCase } from './conformance_case.js';
6
+ import type { BackendCapabilities } from './capabilities.js';
7
+ import type { SetupTest } from './setup.js';
8
+ /**
9
+ * Names a seeded `extra_accounts` username for the `role_holder` /
10
+ * `wrong_role` principals — the only two that aren't backed by an
11
+ * always-available fixture accessor. Suites exercising those principals
12
+ * declare the matching `extra_accounts` at setup and name them here.
13
+ */
14
+ export interface ConformancePrincipalConfig {
15
+ /** `extra_accounts` username for the `role_holder` principal. */
16
+ readonly role_holder?: string;
17
+ /** `extra_accounts` username for the `wrong_role` principal. */
18
+ readonly wrong_role?: string;
19
+ }
20
+ /** Options for `describe_conformance_table_tests`. */
21
+ export interface ConformanceTableOptions {
22
+ /** The conformance cases to run, in order. */
23
+ readonly cases: ReadonlyArray<ConformanceCase>;
24
+ /** Per-test fixture producer (in-process or cross-process). */
25
+ readonly setup_test: SetupTest;
26
+ /** Surface spec — supplies the `RouteSpec`s for the REST branch. */
27
+ readonly surface_source: AppSurfaceSpec;
28
+ /** Declared backend capabilities (reserved for capability-gated rows). */
29
+ readonly capabilities: BackendCapabilities;
30
+ /** RPC endpoints — resolved to find each method's action spec. */
31
+ readonly rpc_endpoints: RpcEndpointsSuiteOption;
32
+ /** Session options — needed to resolve the `rpc_endpoints` factory form. */
33
+ readonly session_options: SessionOptions<string>;
34
+ /** Maps the `role_holder` / `wrong_role` principals to seeded usernames. */
35
+ readonly principals?: ConformancePrincipalConfig;
36
+ /** `describe` block label. Defaults to `'conformance table'`. */
37
+ readonly suite_name?: string;
38
+ }
39
+ /**
40
+ * Register a `describe` block running every `ConformanceCase` as a
41
+ * vitest `test` (or `xfail_until` for deferred-by-design rows). Drives
42
+ * either transport via the shared `{setup_test, surface_source,
43
+ * capabilities}` protocol.
44
+ */
45
+ export declare const describe_conformance_table_tests: (options: ConformanceTableOptions) => void;
46
+ //# sourceMappingURL=conformance_table.d.ts.map
@@ -0,0 +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;AAwB9B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,uBAAuB,CAAC;AAE1D,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,8BAA8B,CAAC;AAMjE,OAAO,EAIN,KAAK,uBAAuB,EAC5B,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAC,KAAK,eAAe,EAA4B,MAAM,uBAAuB,CAAC;AACtF,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,mBAAmB,CAAC;AAC3D,OAAO,KAAK,EAAC,SAAS,EAAc,MAAM,YAAY,CAAC;AAGvD;;;;;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,4EAA4E;IAC5E,QAAQ,CAAC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACjD,4EAA4E;IAC5E,QAAQ,CAAC,UAAU,CAAC,EAAE,0BAA0B,CAAC;IACjD,iEAAiE;IACjE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC7B;AAgOD;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,GAAI,SAAS,uBAAuB,KAAG,IAgBnF,CAAC"}