@fuzdev/fuz_app 0.112.0 → 0.113.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 (45) hide show
  1. package/dist/auth/CLAUDE.md +6 -1
  2. package/dist/auth/audit_log_route_schema.d.ts.map +1 -1
  3. package/dist/auth/audit_log_route_schema.js +12 -1
  4. package/dist/auth/role_grant_offer_action_specs.d.ts +2 -1
  5. package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
  6. package/dist/auth/role_grant_offer_action_specs.js +2 -1
  7. package/dist/db/fact_disk_storage.js +1 -1
  8. package/dist/http/CLAUDE.md +3 -3
  9. package/dist/http/auth_shape.d.ts +19 -6
  10. package/dist/http/auth_shape.d.ts.map +1 -1
  11. package/dist/http/auth_shape.js +19 -6
  12. package/dist/http/surface_query.d.ts +29 -4
  13. package/dist/http/surface_query.d.ts.map +1 -1
  14. package/dist/http/surface_query.js +30 -5
  15. package/dist/realtime/sse_auth_guard.d.ts +9 -3
  16. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  17. package/dist/realtime/sse_auth_guard.js +9 -3
  18. package/dist/testing/CLAUDE.md +1 -1
  19. package/dist/testing/attack_surface.d.ts +5 -2
  20. package/dist/testing/attack_surface.d.ts.map +1 -1
  21. package/dist/testing/attack_surface.js +45 -17
  22. package/dist/testing/auth_apps.d.ts +25 -0
  23. package/dist/testing/auth_apps.d.ts.map +1 -1
  24. package/dist/testing/auth_apps.js +34 -5
  25. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  26. package/dist/testing/cross_backend/testing_reset_actions.js +3 -2
  27. package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
  28. package/dist/testing/cross_backend/token_scope_surface.js +36 -7
  29. package/dist/testing/surface_invariants.d.ts +7 -1
  30. package/dist/testing/surface_invariants.d.ts.map +1 -1
  31. package/dist/testing/surface_invariants.js +11 -5
  32. package/dist/ui/AdminAccounts.svelte +6 -4
  33. package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
  34. package/dist/ui/AdminInvites.svelte +3 -3
  35. package/dist/ui/AdminInvites.svelte.d.ts.map +1 -1
  36. package/dist/ui/AdminOverview.svelte +10 -10
  37. package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
  38. package/dist/ui/AdminRoleGrantHistory.svelte +7 -7
  39. package/dist/ui/AdminRoleGrantHistory.svelte.d.ts.map +1 -1
  40. package/dist/ui/AdminSettings.svelte +1 -1
  41. package/dist/ui/RoleGrantOfferInbox.svelte +4 -1
  42. package/dist/ui/RoleGrantOfferInbox.svelte.d.ts.map +1 -1
  43. package/dist/ui/SurfaceExplorer.svelte +33 -17
  44. package/dist/ui/SurfaceExplorer.svelte.d.ts.map +1 -1
  45. package/package.json +10 -10
@@ -11,9 +11,11 @@ import { Hono } from 'hono';
11
11
  import { Logger } from '@fuzdev/fuz_util/log.ts';
12
12
  import { apply_route_specs } from '../http/route_spec.js';
13
13
  import { is_public_auth } from '../http/auth_shape.js';
14
+ import { CREDENTIAL_TYPE_DAEMON_TOKEN, CREDENTIAL_TYPE_SESSION } from '../auth/credential_type_schema.js';
15
+ import { ROLE_KEEPER } from '../auth/role_schema.js';
14
16
  import { fuz_auth_guard_resolver } from '../auth/auth_guard_resolver.js';
15
17
  import { REQUEST_CONTEXT_KEY, create_fuz_authorization_handler } from '../auth/request_context.js';
16
- import { ACCOUNT_ID_KEY, CREDENTIAL_TYPE_KEY, TEST_CONTEXT_PRESET_KEY } from '../hono_context.js';
18
+ import { ACCOUNT_ID_KEY, CREDENTIAL_TYPES, CREDENTIAL_TYPE_KEY, TEST_CONTEXT_PRESET_KEY } from '../hono_context.js';
17
19
  import { create_stub_db } from './stubs.js';
18
20
  import { create_test_account, create_test_actor, create_test_role_grant } from './entities.js';
19
21
  /**
@@ -40,7 +42,7 @@ export const create_test_app_from_specs = (route_specs, auth_ctx, credential_typ
40
42
  if (auth_ctx) {
41
43
  c.set(ACCOUNT_ID_KEY, auth_ctx.account.id);
42
44
  c.set(REQUEST_CONTEXT_KEY, auth_ctx);
43
- c.set(CREDENTIAL_TYPE_KEY, credential_type ?? 'session');
45
+ c.set(CREDENTIAL_TYPE_KEY, credential_type ?? CREDENTIAL_TYPE_SESSION);
44
46
  c.set(TEST_CONTEXT_PRESET_KEY, true);
45
47
  }
46
48
  await next();
@@ -59,25 +61,52 @@ export const create_auth_test_apps = (route_specs, roles) => {
59
61
  for (const role of roles) {
60
62
  by_role.set(role, create_test_app_from_specs(route_specs, create_test_request_context(role)));
61
63
  }
64
+ const by_credential_type = new Map(CREDENTIAL_TYPES.map((credential_type) => [
65
+ credential_type,
66
+ create_test_app_from_specs(route_specs, create_test_request_context(ROLE_KEEPER), credential_type)
67
+ ]));
62
68
  return {
63
69
  public: create_test_app_from_specs(route_specs),
64
70
  authed: create_test_app_from_specs(route_specs, create_test_request_context()),
65
- keeper: create_test_app_from_specs(route_specs, create_test_request_context('keeper'), 'daemon_token'),
71
+ keeper: by_credential_type.get(CREDENTIAL_TYPE_DAEMON_TOKEN),
72
+ by_credential_type,
66
73
  by_role
67
74
  };
68
75
  };
69
76
  /**
70
77
  * Select the Hono test app with correct auth for a route.
71
78
  *
79
+ * The credential channel is decided first, because the channel gate is
80
+ * pre-authorization: an app whose credential the route refuses never reaches
81
+ * the role gate, so a role-correct app on the wrong channel answers
82
+ * `credential_type_required`, and a "correct auth passes guard" case reads it
83
+ * as a failure of the role gate that never ran.
84
+ *
85
+ * A gate admitting `'session'` falls through to the `by_role` /
86
+ * `authed` apps, which carry that channel. Any other gate leaves the
87
+ * `by_credential_type` app as the only candidate — those hold the keeper
88
+ * role, which is why a route pairing a non-session channel with some *other*
89
+ * role has no app and says so.
90
+ *
72
91
  * @throws Error if `auth.roles` names a role not present in `apps.by_role` —
73
92
  * surfaces a missing entry in the `roles` array passed to
74
93
  * `create_auth_test_apps`.
94
+ * @throws Error if the route pairs a non-session channel with a role the
95
+ * channel apps don't hold. Silently handing back a session app would turn
96
+ * every case on the route into a misattributed 403.
75
97
  */
76
98
  export const select_auth_app = (apps, auth) => {
77
99
  if (is_public_auth(auth))
78
100
  return apps.public;
79
- if (auth.credential_types?.includes('daemon_token'))
80
- return apps.keeper;
101
+ const credential_types = auth.credential_types;
102
+ if (credential_types?.length && !credential_types.includes(CREDENTIAL_TYPE_SESSION)) {
103
+ const channel = CREDENTIAL_TYPES.find((t) => credential_types.includes(t));
104
+ const app = channel === undefined ? undefined : apps.by_credential_type.get(channel);
105
+ const roles = auth.roles;
106
+ if (app && (!roles?.length || roles.includes(ROLE_KEEPER)))
107
+ return app;
108
+ throw new Error(`No test app for credential types '${credential_types.join("', '")}'${roles?.length ? ` with roles '${roles.join("', '")}'` : ''} — the channel apps hold the '${ROLE_KEEPER}' role, and only the session channel has per-role apps`);
109
+ }
81
110
  if (auth.roles?.length) {
82
111
  // Multi-role disjunction: any of the named roles admits the caller.
83
112
  // Tests pick the first role's app; consumers wanting per-role coverage
@@ -1 +1 @@
1
- {"version":3,"file":"testing_reset_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/testing_reset_actions.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoEG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,OAAO,EAAc,KAAK,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAKzE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,gBAAgB,CAAC;AAqCzC;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;QAiBpC;;;;;;;;WAQG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAWyC,CAAC;AAE/C;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,iCAAiC;;;;;;;;;;;;;;;;CAWA,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,gCAAgC;;;;;;;;;;;;;;;;;;;CAwBC,CAAC;AAE/C;;;;;;GAMG;AACH,eAAO,MAAM,mCAAmC,QAAO,SACmB,CAAC;AAE3E;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;;;;;;;CAeK,CAAC;AAE/C;;;;;;;;;GASG;AACH,eAAO,MAAM,mCAAmC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAWF,CAAC;AAE/C;;;;GAIG;AACH,eAAO,MAAM,qCAAqC,QAAO,SAGvD,CAAC;AAEH;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qCAAqC;;;;;;;;;;;;;;;;;;;;CAYJ,CAAC;AAE/C;;;;GAIG;AACH,eAAO,MAAM,uCAAuC,QAAO,SAGzD,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,mCAAmC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAYF,CAAC;AAyB/C,eAAO,MAAM,qCAAqC,GACjD,SAAS,aAAa,CAAC,SAAS,CAAC,KAC/B,SAMF,CAAC;AAEF,4CAA4C;AAC5C,MAAM,WAAW,2BAA2B;IAC3C;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACjD;;;;;OAKG;IACH,QAAQ,CAAC,kBAAkB,EAAE,gBAAgB,CAAC;IAC9C;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACxD;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,OAAO,EACb,SAAS,2BAA2B,KAClC,KAAK,CAAC,SAAS,CAsIjB,CAAC;AAEF,0FAA0F;AAC1F,eAAO,MAAM,0BAA0B,UAAmC,CAAC"}
1
+ {"version":3,"file":"testing_reset_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/testing_reset_actions.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoEG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,OAAO,EAAc,KAAK,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAKzE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,gBAAgB,CAAC;AAqCzC;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;QAiBpC;;;;;;;;WAQG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAWyC,CAAC;AAE/C;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,iCAAiC;;;;;;;;;;;;;;;;CAWA,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,gCAAgC;;;;;;;;;;;;;;;;;;;CAwBC,CAAC;AAE/C;;;;;;GAMG;AACH,eAAO,MAAM,mCAAmC,QAAO,SACmB,CAAC;AAE3E;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;;;;;;;CAeK,CAAC;AAE/C;;;;;;;;;GASG;AACH,eAAO,MAAM,mCAAmC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAWF,CAAC;AAE/C;;;;GAIG;AACH,eAAO,MAAM,qCAAqC,QAAO,SAGvD,CAAC;AAEH;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qCAAqC;;;;;;;;;;;;;;;;;;;;CAYJ,CAAC;AAE/C;;;;GAIG;AACH,eAAO,MAAM,uCAAuC,QAAO,SAGzD,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,mCAAmC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAaF,CAAC;AAyB/C,eAAO,MAAM,qCAAqC,GACjD,SAAS,aAAa,CAAC,SAAS,CAAC,KAC/B,SAMF,CAAC;AAEF,4CAA4C;AAC5C,MAAM,WAAW,2BAA2B;IAC3C;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACjD;;;;;OAKG;IACH,QAAQ,CAAC,kBAAkB,EAAE,gBAAgB,CAAC;IAC9C;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACxD;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,OAAO,EACb,SAAS,2BAA2B,KAClC,KAAK,CAAC,SAAS,CAsIjB,CAAC;AAEF,0FAA0F;AAC1F,eAAO,MAAM,0BAA0B,UAAmC,CAAC"}
@@ -368,8 +368,9 @@ export const testing_action_manifest_action_spec = {
368
368
  input: z.strictObject({}),
369
369
  output: ActionManifest,
370
370
  async: true,
371
- description: 'Test-binary only — dump the live RPC registry as a normalized {method, auth, side_effects} ' +
372
- 'manifest for cross-impl method-set + auth-shape parity diffing.'
371
+ description: 'Test-binary only — dump the live RPC registry as a normalized ' +
372
+ '{method, auth, side_effects, rate_limit} manifest for cross-impl method-set + ' +
373
+ 'auth-shape parity diffing.'
373
374
  };
374
375
  /**
375
376
  * Build the `_testing_action_manifest` action over a backend's full live RPC
@@ -1 +1 @@
1
- {"version":3,"file":"token_scope_surface.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/token_scope_surface.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAiE9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AAkB1E,wDAAwD;AACxD,MAAM,WAAW,iCAAkC,SAAQ,uCAAuC;IACjG,6DAA6D;IAC7D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B;AA2DD,eAAO,MAAM,wCAAwC,GACpD,SAAS,iCAAiC,KACxC,IAwHF,CAAC"}
1
+ {"version":3,"file":"token_scope_surface.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/token_scope_surface.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA8E9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AAkB1E,wDAAwD;AACxD,MAAM,WAAW,iCAAkC,SAAQ,uCAAuC;IACjG,6DAA6D;IAC7D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B;AAwFD,eAAO,MAAM,wCAAwC,GACpD,SAAS,iCAAiC,KACxC,IAwHF,CAAC"}
@@ -26,6 +26,15 @@ import '../assert_dev_env.js';
26
26
  * `required_scope` capability string, per surface. A spine that renamed a
27
27
  * surface, dropped the `surface:` prefix, or emitted the JSON-RPC envelope
28
28
  * from a REST route fails here.
29
+ * - **The audit stream's channel denial** — that surface is gated to
30
+ * `credential_types: ['session']` on both spines, so a bearer never reaches
31
+ * its scope gate and the observable is `credential_type_required` instead.
32
+ * The case is kept here rather than moved: it is still the bearer-vs-surface
33
+ * probe, and what it now pins is the coarser-gate-first ordering both spines
34
+ * run (a spine that checked scope first would answer `token_scope_required`).
35
+ * `surface:audit_stream`'s own capability string is consequently unreachable
36
+ * on the default mount and has no cross-impl pin — each spine's census holds
37
+ * its declaration, the way the WS upgrade's is held.
29
38
  * - **Scope outranks role** — a narrowed token whose account *also* lacks the
30
39
  * gating role must hear `token_scope_required`, not
31
40
  * `insufficient_permissions`. This is the observable the TS↔Rust ordering
@@ -37,16 +46,17 @@ import '../assert_dev_env.js';
37
46
  * Two non-vacuity controls hold the negatives honest: the narrowed token must
38
47
  * still reach the RPC method it *does* name (otherwise a spine that rejected
39
48
  * the credential outright would pass every denial), and a **full** bearer must
40
- * reach the same surface (otherwise a route broken for all bearers would pass
41
- * too).
49
+ * reach `fact_bare` (otherwise a route broken for all bearers would pass too).
50
+ * The second has no audit-stream twin by construction — refusing every bearer
51
+ * is exactly what that route's channel gate is for.
42
52
  *
43
53
  * ## Why the WS upgrade isn't here
44
54
  *
45
55
  * Rule 3's third probeable surface is the WebSocket upgrade, and it is
46
56
  * deliberately omitted: `create_ws_transport` threads cookies, not bearer
47
57
  * headers, and a refused upgrade surfaces as a thrown connection error whose
48
- * body — the thing this suite exists to compare — isn't readable. The two HTTP
49
- * surfaces are where the denial body is observable, which is what the parity
58
+ * body — the thing this suite exists to compare — isn't readable. The HTTP
59
+ * surfaces are where a denial body is observable, which is what the parity
50
60
  * gate needs. The WS gate keeps its per-spine coverage (fuz_app's surface
51
61
  * census pins the call site; the Rust census pins its own).
52
62
  *
@@ -57,7 +67,7 @@ import '../assert_dev_env.js';
57
67
  */
58
68
  import { describe, assert } from 'vitest';
59
69
  import { account_token_create_action_spec } from '../../auth/account_action_specs.js';
60
- import { ERROR_TOKEN_SCOPE_REQUIRED } from '../../http/error_schemas.js';
70
+ import { ERROR_CREDENTIAL_TYPE_REQUIRED, ERROR_TOKEN_SCOPE_REQUIRED } from '../../http/error_schemas.js';
61
71
  import { test_if } from './capabilities.js';
62
72
  import { cross_rpc_call } from './cell_cross_helpers.js';
63
73
  import { SPINE_RPC_PATH } from './spine_surface_constants.js';
@@ -107,6 +117,25 @@ const assert_surface_denied = async (fresh_transport, path, token, surface) => {
107
117
  const body = await res.json();
108
118
  assert.deepStrictEqual(body, surface_denial_body(surface), `${path} must return the canonical flat surface denial`);
109
119
  };
120
+ /**
121
+ * Assert a bearer GET is refused by the *channel* gate rather than the scope
122
+ * gate — the shape a surface takes once it declares
123
+ * `credential_types: ['session']`.
124
+ *
125
+ * Distinct from `assert_surface_denied` on purpose: a channel-gated surface
126
+ * refuses **every** bearer, narrow or full, so the denial cannot name a scope.
127
+ * A narrowed token is still the right probe — it proves the channel gate runs
128
+ * *first*, since a scope-gated route would have answered
129
+ * `token_scope_required` instead.
130
+ */
131
+ const assert_channel_denied = async (fresh_transport, path, token, credential_types) => {
132
+ const res = await fresh_transport({ origin: null })(path, {
133
+ headers: { authorization: `Bearer ${token}` }
134
+ });
135
+ assert.strictEqual(res.status, 403, `${path} must refuse a bearer with 403`);
136
+ const body = await res.json();
137
+ assert.deepStrictEqual(body, { error: ERROR_CREDENTIAL_TYPE_REQUIRED, required_credential_types: credential_types }, `${path} must return the canonical flat credential-type denial`);
138
+ };
110
139
  export const describe_token_scope_surface_cross_tests = (options) => {
111
140
  const { setup_test, capabilities } = options;
112
141
  const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
@@ -123,10 +152,10 @@ export const describe_token_scope_surface_cross_tests = (options) => {
123
152
  const res = await cross_rpc_call(fixture.fresh_transport({ origin: null }), rpc_path, ADMITTED_METHOD, undefined, { authorization: `Bearer ${token}` });
124
153
  assert.ok(res.ok, `admitted method must succeed: ${JSON.stringify(res.error)}`);
125
154
  });
126
- test_if(capabilities.sse, 'a narrowed token is refused the audit stream (surface:audit_stream)', async () => {
155
+ test_if(capabilities.sse, 'the audit stream refuses a bearer at the channel gate (credential_types)', async () => {
127
156
  const fixture = await setup_test();
128
157
  const token = await mint_narrowed_token(fixture.transport, rpc_path, fixture.create_session_headers());
129
- await assert_surface_denied(fixture.fresh_transport, sse_path, token, 'audit_stream');
158
+ await assert_channel_denied(fixture.fresh_transport, sse_path, token, ['session']);
130
159
  });
131
160
  test_if(capabilities.fact_serving, 'a narrowed token is refused the bare-hash fact read (surface:fact_bare)', async () => {
132
161
  const fixture = await setup_test();
@@ -5,7 +5,13 @@ import type { AppSurface } from '../http/surface.js';
5
5
  */
6
6
  export declare const assert_protected_routes_declare_401: (surface: AppSurface) => void;
7
7
  /**
8
- * Every role/keeper route has 403 in `error_schemas`.
8
+ * Every route with an authority gate that can answer 403 declares one — a
9
+ * role gate, or a credential-channel gate of any kind (keeper included).
10
+ *
11
+ * The credential half is not just the keeper routes: the session-only gates
12
+ * point the same axis the other way and refuse with the same status, so a
13
+ * route carrying one and no 403 schema would have its denial body go
14
+ * unvalidated by the DEV-only error-schema check.
9
15
  */
10
16
  export declare const assert_role_routes_declare_403: (surface: AppSurface) => void;
11
17
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"surface_invariants.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/surface_invariants.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"surface_invariants.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/surface_invariants.ts"],"names":[],"mappings":"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"}
@@ -18,7 +18,7 @@ import './assert_dev_env.js';
18
18
  */
19
19
  import { assert } from 'vitest';
20
20
  import { middleware_applies } from '../http/schema_helpers.js';
21
- import { filter_protected_routes, filter_role_routes, filter_keeper_routes, filter_routes_with_input, filter_routes_with_params, filter_routes_with_query, filter_public_routes, format_route_key } from '../http/surface_query.js';
21
+ import { filter_protected_routes, filter_role_routes, filter_keeper_routes, filter_credential_gated_routes, filter_routes_with_input, filter_routes_with_params, filter_routes_with_query, filter_public_routes, format_route_key } from '../http/surface_query.js';
22
22
  import { PROTOCOL_ACTION_METHODS } from '../actions/action_codegen.js';
23
23
  // --- Structural invariants ---
24
24
  /**
@@ -31,13 +31,19 @@ export const assert_protected_routes_declare_401 = (surface) => {
31
31
  }
32
32
  };
33
33
  /**
34
- * Every role/keeper route has 403 in `error_schemas`.
34
+ * Every route with an authority gate that can answer 403 declares one — a
35
+ * role gate, or a credential-channel gate of any kind (keeper included).
36
+ *
37
+ * The credential half is not just the keeper routes: the session-only gates
38
+ * point the same axis the other way and refuse with the same status, so a
39
+ * route carrying one and no 403 schema would have its denial body go
40
+ * unvalidated by the DEV-only error-schema check.
35
41
  */
36
42
  export const assert_role_routes_declare_403 = (surface) => {
37
43
  const role_routes = filter_role_routes(surface);
38
- const keeper_routes = filter_keeper_routes(surface);
39
- for (const route of [...role_routes, ...keeper_routes]) {
40
- assert.ok(route.error_schemas && '403' in route.error_schemas, `${format_route_key(route)} requires role/keeper but missing 403 error schema`);
44
+ const credential_gated_routes = filter_credential_gated_routes(surface);
45
+ for (const route of [...role_routes, ...credential_gated_routes]) {
46
+ assert.ok(route.error_schemas && '403' in route.error_schemas, `${format_route_key(route)} has a role or credential gate but missing 403 error schema`);
41
47
  }
42
48
  };
43
49
  /**
@@ -44,9 +44,9 @@
44
44
  <h1>accounts</h1>
45
45
  {#if admin_accounts.account_count > 0}
46
46
  <p>
47
- <span class="chip color_a"
48
- >{admin_accounts.account_count} account{admin_accounts.account_count === 1 ? '' : 's'}</span
49
- >
47
+ <span class="chip color_a">
48
+ {admin_accounts.account_count} account{admin_accounts.account_count === 1 ? '' : 's'}
49
+ </span>
50
50
  </p>
51
51
  {/if}
52
52
 
@@ -158,7 +158,9 @@
158
158
  {#each admin_accounts.grantable_roles as role (role)}
159
159
  {@const key = grant_key(row.account.id, role)}
160
160
  {@const grant_error = admin_accounts.grant.error(key)}
161
- {#if !row.role_grants.some((p) => p.role === role) && !row.pending_offers.some((o) => o.role === role)}
161
+ {#if !row.role_grants.some((p) => p.role === role) &&
162
+ !row.pending_offers.some((o) => o.role === role)
163
+ }
162
164
  <ConfirmButton
163
165
  onconfirm={() => admin_accounts.submit_grant(row.account.id, role)}
164
166
  title="offer {role}"
@@ -1 +1 @@
1
- {"version":3,"file":"AdminAccounts.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/AdminAccounts.svelte"],"names":[],"mappings":"AAkMA,QAAA,MAAM,aAAa,2DAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"AdminAccounts.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/AdminAccounts.svelte"],"names":[],"mappings":"AAqMA,QAAA,MAAM,aAAa,2DAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
@@ -59,9 +59,9 @@
59
59
  </section>
60
60
  {#if admin_invites.invite_count > 0}
61
61
  <p>
62
- <span class="chip color_a"
63
- >{admin_invites.unclaimed_count} unclaimed / {admin_invites.invite_count} total</span
64
- >
62
+ <span class="chip color_a">
63
+ {admin_invites.unclaimed_count} unclaimed / {admin_invites.invite_count} total
64
+ </span>
65
65
  </p>
66
66
  {/if}
67
67
 
@@ -1 +1 @@
1
- {"version":3,"file":"AdminInvites.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/AdminInvites.svelte"],"names":[],"mappings":"AAqJA,QAAA,MAAM,YAAY,2DAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"AdminInvites.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/AdminInvites.svelte"],"names":[],"mappings":"AAuJA,QAAA,MAAM,YAAY,2DAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -146,9 +146,9 @@
146
146
  <div class="baseline-row gap_xs font_size_sm mt_sm">
147
147
  <span class="text_50">newest session:</span>
148
148
  <strong>{most_recent.username}</strong>
149
- <span class="text_50" title={format_datetime_local(most_recent.created_at)}
150
- >{format_relative_time(most_recent.created_at)}</span
151
- >
149
+ <span class="text_50" title={format_datetime_local(most_recent.created_at)}>
150
+ {format_relative_time(most_recent.created_at)}
151
+ </span>
152
152
  </div>
153
153
  {/if}
154
154
  {/if}
@@ -214,9 +214,9 @@
214
214
  <ul class="compact-list">
215
215
  {#each recent_events as event (event.id)}
216
216
  <li>
217
- <span class="text_50 font_size_sm" title={format_datetime_local(event.created_at)}
218
- >{format_relative_time(event.created_at)}</span
219
- >
217
+ <span class="text_50 font_size_sm" title={format_datetime_local(event.created_at)}>
218
+ {format_relative_time(event.created_at)}
219
+ </span>
220
220
  <code class="font_size_sm">{event.event_type}</code>
221
221
  {#if event.outcome === 'failure'}
222
222
  <span class="chip font_size_sm color_c">fail</span>
@@ -251,9 +251,9 @@
251
251
  <ul class="compact-list">
252
252
  {#each role_grant_changes.slice(0, 4) as event (event.id)}
253
253
  <li class="font_size_sm">
254
- <span class="text_50" title={format_datetime_local(event.created_at)}
255
- >{format_relative_time(event.created_at)}</span
256
- >
254
+ <span class="text_50" title={format_datetime_local(event.created_at)}>
255
+ {format_relative_time(event.created_at)}
256
+ </span>
257
257
  <code>{event.event_type === 'role_grant_create' ? 'grant' : 'revoke'}</code>
258
258
  {#if event.metadata?.role}
259
259
  <span class="chip font_size_sm">{event.metadata.role}</span>
@@ -309,7 +309,7 @@
309
309
  label="log out"
310
310
  >
311
311
  {#snippet popover_button_content()}
312
- <span class="p_md"> log out </span>
312
+ <span class="p_md">log out</span>
313
313
  {/snippet}
314
314
  </ConfirmButton>
315
315
  </div>
@@ -1 +1 @@
1
- {"version":3,"file":"AdminOverview.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/AdminOverview.svelte"],"names":[],"mappings":"AAyUA,QAAA,MAAM,aAAa,2DAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"AdminOverview.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/AdminOverview.svelte"],"names":[],"mappings":"AA+UA,QAAA,MAAM,aAAa,2DAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
@@ -74,14 +74,14 @@
74
74
  {:else if column.key === 'username'}
75
75
  <!-- Prefer actor-grain id in the truncated fallback; account is
76
76
  the second fallback for events with no actor binding. -->
77
- <span class="text_50"
78
- >{row.username ?? truncate_uuid(row.actor_id ?? row.account_id ?? '?')}</span
79
- >
77
+ <span class="text_50">
78
+ {row.username ?? truncate_uuid(row.actor_id ?? row.account_id ?? '?')}
79
+ </span>
80
80
  {:else if column.key === 'target_username'}
81
- <span class="text_50"
82
- >{row.target_username ??
83
- truncate_uuid(row.target_actor_id ?? row.target_account_id ?? '?')}</span
84
- >
81
+ <span class="text_50">
82
+ {row.target_username ??
83
+ truncate_uuid(row.target_actor_id ?? row.target_account_id ?? '?')}
84
+ </span>
85
85
  {:else if column.key === 'created_at'}
86
86
  <span title={format_datetime_local(row.created_at)}>
87
87
  {format_relative_time(row.created_at)}
@@ -1 +1 @@
1
- {"version":3,"file":"AdminRoleGrantHistory.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/AdminRoleGrantHistory.svelte"],"names":[],"mappings":"AAmGA,QAAA,MAAM,qBAAqB,2DAAwC,CAAC;AACpE,KAAK,qBAAqB,GAAG,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAC;AACtE,eAAe,qBAAqB,CAAC"}
1
+ {"version":3,"file":"AdminRoleGrantHistory.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/AdminRoleGrantHistory.svelte"],"names":[],"mappings":"AAuGA,QAAA,MAAM,qBAAqB,2DAAwC,CAAC;AACpE,KAAK,qBAAqB,GAAG,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAC;AACtE,eAAe,qBAAqB,CAAC"}
@@ -33,7 +33,7 @@
33
33
  label="log out"
34
34
  >
35
35
  {#snippet popover_button_content()}
36
- <span class="p_md"> log out </span>
36
+ <span class="p_md">log out</span>
37
37
  {/snippet}
38
38
  </ConfirmButton>
39
39
  </section>
@@ -48,7 +48,10 @@
48
48
  <section class="role-grant-offer-inbox">
49
49
  <h2>pending offers</h2>
50
50
 
51
- {#if role_grant_offers.list.error || role_grant_offers.accept.error || role_grant_offers.decline.error}
51
+ {#if role_grant_offers.list.error ||
52
+ role_grant_offers.accept.error ||
53
+ role_grant_offers.decline.error
54
+ }
52
55
  <p class="color_c_50">
53
56
  {role_grant_offers.list.error ??
54
57
  role_grant_offers.accept.error ??
@@ -1 +1 @@
1
- {"version":3,"file":"RoleGrantOfferInbox.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/RoleGrantOfferInbox.svelte"],"names":[],"mappings":"AAmBA,OAAO,EAA6C,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAE/F,KAAK,gBAAgB,GAAI;IACxB,uEAAuE;IACvE,YAAY,CAAC,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,MAAM,CAAC;IACjD;;;;OAIG;IACH,YAAY,CAAC,EAAE,WAAW,CAAC;IAC3B,+DAA+D;IAC/D,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACvC,CAAC;AA+FH,QAAA,MAAM,mBAAmB,sDAAwC,CAAC;AAClE,KAAK,mBAAmB,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAClE,eAAe,mBAAmB,CAAC"}
1
+ {"version":3,"file":"RoleGrantOfferInbox.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/RoleGrantOfferInbox.svelte"],"names":[],"mappings":"AAmBA,OAAO,EAA6C,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAE/F,KAAK,gBAAgB,GAAI;IACxB,uEAAuE;IACvE,YAAY,CAAC,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,MAAM,CAAC;IACjD;;;;OAIG;IACH,YAAY,CAAC,EAAE,WAAW,CAAC;IAC3B,+DAA+D;IAC/D,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACvC,CAAC;AAiGH,QAAA,MAAM,mBAAmB,sDAAwC,CAAC;AAClE,KAAK,mBAAmB,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAClE,eAAe,mBAAmB,CAAC"}
@@ -13,6 +13,7 @@
13
13
  import type { AppSurface, AppSurfaceRoute, AppSurfaceDiagnostic } from '../http/surface.js';
14
14
  import { surface_auth_summary, format_route_key } from '../http/surface_query.js';
15
15
  import {
16
+ is_credential_gated_auth,
16
17
  is_keeper_auth,
17
18
  is_plain_authenticated_auth,
18
19
  is_public_auth,
@@ -22,7 +23,7 @@
22
23
 
23
24
  const { surface }: { surface: AppSurface } = $props();
24
25
 
25
- const auth_types = ['all', 'none', 'authenticated', 'role', 'keeper'] as const;
26
+ const auth_types = ['all', 'none', 'authenticated', 'role', 'credential', 'keeper'] as const;
26
27
 
27
28
  let auth_filter: (typeof auth_types)[number] = $state.raw('all');
28
29
  let expanded_route: string | null = $state.raw(null);
@@ -49,6 +50,8 @@
49
50
  return is_plain_authenticated_auth(auth);
50
51
  case 'role':
51
52
  return is_role_auth(auth);
53
+ case 'credential':
54
+ return is_credential_gated_auth(auth) && !is_keeper_auth(auth);
52
55
  case 'keeper':
53
56
  return is_keeper_auth(auth);
54
57
  }
@@ -90,32 +93,44 @@
90
93
  if (is_public_auth(auth)) return 'chip color_b';
91
94
  if (is_keeper_auth(auth)) return 'chip color_c';
92
95
  if (is_role_auth(auth)) return 'chip color_d';
96
+ if (is_credential_gated_auth(auth)) return 'chip color_e';
93
97
  if (is_plain_authenticated_auth(auth)) return 'chip color_a';
94
98
  return 'chip';
95
99
  };
96
100
 
97
101
  const role_count = $derived(Array.from(summary.role.values()).reduce((sum, n) => sum + n, 0));
102
+ const credential_count = $derived(
103
+ Array.from(summary.credential.values()).reduce((sum, n) => sum + n, 0)
104
+ );
98
105
  </script>
99
106
 
100
107
  <section>
101
108
  <div class="row" style:gap="var(--space_md)" style:flex-wrap="wrap" style:align-items="center">
102
109
  <span class="chip">{surface.routes.length} routes</span>
103
110
  {#if summary.none > 0}<span class="chip color_b">{summary.none} public</span>{/if}
104
- {#if summary.authenticated > 0}<span class="chip color_a"
105
- >{summary.authenticated} authenticated</span
106
- >{/if}
111
+ {#if summary.authenticated > 0}
112
+ <span class="chip color_a">{summary.authenticated} authenticated</span>
113
+ {/if}
107
114
  {#if role_count > 0}<span class="chip color_d">{role_count} role</span>{/if}
115
+ {#if credential_count > 0}
116
+ <span class="chip color_e">{credential_count} credential-gated</span>
117
+ {/if}
108
118
  {#if summary.keeper > 0}<span class="chip color_c">{summary.keeper} keeper</span>{/if}
109
119
  <span class="chip">{surface.middleware.length} middleware</span>
110
120
  {#if rpc_method_count > 0}<span class="chip">{rpc_method_count} rpc methods</span>{/if}
111
121
  {#if ws_method_count > 0}<span class="chip">{ws_method_count} ws methods</span>{/if}
112
122
  {#if surface.env.length}<span class="chip">{surface.env.length} env</span>{/if}
113
123
  {#if surface.events.length}<span class="chip">{surface.events.length} events</span>{/if}
114
- {#if surface.diagnostics.length}{@const warnings = surface.diagnostics.filter(
124
+ {#if surface.diagnostics.length}
125
+ {@const warnings = surface.diagnostics.filter(
115
126
  (d: AppSurfaceDiagnostic) => d.level === 'warning'
116
- )}{#if warnings.length}<span class="chip color_e"
117
- >{warnings.length} warning{warnings.length === 1 ? '' : 's'}</span
118
- >{/if}{/if}
127
+ )}
128
+ {#if warnings.length}
129
+ <span class="chip color_e">
130
+ {warnings.length} warning{warnings.length === 1 ? '' : 's'}
131
+ </span>
132
+ {/if}
133
+ {/if}
119
134
  </div>
120
135
 
121
136
  <h3>routes</h3>
@@ -221,9 +236,9 @@
221
236
  <tr>
222
237
  <td><code>{mw.name}</code></td>
223
238
  <td><code>{mw.path}</code></td>
224
- <td class="text_50"
225
- >{mw.error_schemas ? Object.keys(mw.error_schemas).join(', ') : '-'}</td
226
- >
239
+ <td class="text_50">
240
+ {mw.error_schemas ? Object.keys(mw.error_schemas).join(', ') : '-'}
241
+ </td>
227
242
  </tr>
228
243
  {/each}
229
244
  </tbody>
@@ -280,11 +295,11 @@
280
295
  <td>
281
296
  <!-- TODO fix the `as any` cast -->
282
297
  {#if event.params_schema}
283
- <code
284
- >{Object.keys(
298
+ <code>
299
+ {Object.keys(
285
300
  (event.params_schema as any).properties ?? event.params_schema
286
- ).join(', ')}</code
287
- >
301
+ ).join(', ')}
302
+ </code>
288
303
  {:else}
289
304
  <span class="text_50">none</span>
290
305
  {/if}
@@ -457,8 +472,9 @@
457
472
  <tbody>
458
473
  {#each surface.diagnostics as d, i (i)}
459
474
  <tr>
460
- <td><span class={d.level === 'warning' ? 'chip color_e' : 'chip'}>{d.level}</span></td
461
- >
475
+ <td>
476
+ <span class={d.level === 'warning' ? 'chip color_e' : 'chip'}>{d.level}</span>
477
+ </td>
462
478
  <td><code>{d.category}</code></td>
463
479
  <td>{d.message}</td>
464
480
  <td class="text_50">{d.source ?? '-'}</td>
@@ -1 +1 @@
1
- {"version":3,"file":"SurfaceExplorer.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/SurfaceExplorer.svelte"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,UAAU,EAAyC,MAAM,oBAAoB,CAAC;AAU3F,KAAK,gBAAgB,GAAI;IAAE,OAAO,EAAE,UAAU,CAAA;CAAE,CAAC;AAgclD,QAAA,MAAM,eAAe,sDAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
1
+ {"version":3,"file":"SurfaceExplorer.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/SurfaceExplorer.svelte"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,UAAU,EAAyC,MAAM,oBAAoB,CAAC;AAW3F,KAAK,gBAAgB,GAAI;IAAE,OAAO,EAAE,UAAU,CAAA;CAAE,CAAC;AAwdlD,QAAA,MAAM,eAAe,sDAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_app",
3
- "version": "0.112.0",
3
+ "version": "0.113.0",
4
4
  "description": "fullstack app library",
5
5
  "glyph": "🗝",
6
6
  "logo": "logo.svg",
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "peerDependencies": {
34
34
  "@electric-sql/pglite": ">=0.4",
35
- "@fuzdev/blake3_wasm": ">=0.1.0",
35
+ "@fuzdev/blake3-wasm": ">=0.2.0",
36
36
  "@fuzdev/fuz_util": ">=0.68.0",
37
37
  "@hono/node-server": ">=1",
38
38
  "@hono/node-ws": ">=1",
@@ -73,12 +73,12 @@
73
73
  },
74
74
  "devDependencies": {
75
75
  "@electric-sql/pglite": "^0.4.5",
76
- "@fuzdev/blake3_wasm": "^0.1.1",
77
- "@fuzdev/fuz_code": "^0.47.0",
78
- "@fuzdev/fuz_css": "^0.63.2",
79
- "@fuzdev/fuz_ui": "^0.206.6",
80
- "@fuzdev/fuz_util": "^0.68.0",
81
- "@fuzdev/gro": "0.205.1",
76
+ "@fuzdev/blake3-wasm": "^0.2.0",
77
+ "@fuzdev/fuz_code": "^0.48.0",
78
+ "@fuzdev/fuz_css": "^0.63.3",
79
+ "@fuzdev/fuz_ui": "^0.208.0",
80
+ "@fuzdev/fuz_util": "^0.69.0",
81
+ "@fuzdev/gro": "^0.212.0",
82
82
  "@fuzdev/mdz": "^0.3.0",
83
83
  "@hono/node-server": "^1.19.14",
84
84
  "@hono/node-ws": "^1.3.1",
@@ -104,13 +104,13 @@
104
104
  "prettier-plugin-svelte": "^4.1.1",
105
105
  "svelte": "^5.56.4",
106
106
  "svelte-check": "^4.7.1",
107
- "svelte-docinfo": "^0.5.4",
107
+ "svelte-docinfo": "^0.7.0",
108
108
  "svelte2tsx": "^0.7.52",
109
109
  "tslib": "^2.8.1",
110
110
  "typescript": "^5.9.3",
111
111
  "typescript-eslint": "^8.57.0",
112
112
  "vite": "^7.3.1",
113
- "vitest": "^4.0.18",
113
+ "vitest": "^5.0.0",
114
114
  "ws": "^8.20.1",
115
115
  "zimmerframe": "^1.1.4",
116
116
  "zod": "^4.3.6"