@fuzdev/fuz_app 0.131.0 → 0.132.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 (65) hide show
  1. package/dist/actions/action_dispatcher.js +0 -1
  2. package/dist/actions/action_event_data.d.ts +2 -2
  3. package/dist/actions/action_event_types.d.ts +2 -2
  4. package/dist/actions/action_rpc.js +0 -1
  5. package/dist/actions/action_spec.d.ts +19 -19
  6. package/dist/actions/perform_action.js +0 -1
  7. package/dist/actions/register_action_ws.js +0 -1
  8. package/dist/actions/transports_http.d.ts +7 -0
  9. package/dist/actions/transports_http.d.ts.map +1 -1
  10. package/dist/actions/transports_http.js +18 -1
  11. package/dist/auth/CLAUDE.md +1 -1
  12. package/dist/auth/account_routes.js +1 -1
  13. package/dist/auth/audit_log_schema.d.ts +5 -5
  14. package/dist/auth/cell_action_specs.d.ts +3 -3
  15. package/dist/auth/credential_type_schema.d.ts +1 -1
  16. package/dist/auth/middleware.d.ts +13 -10
  17. package/dist/auth/middleware.d.ts.map +1 -1
  18. package/dist/auth/middleware.js +14 -10
  19. package/dist/cli/daemon.js +0 -1
  20. package/dist/cli/logger.js +0 -1
  21. package/dist/env/load.js +0 -1
  22. package/dist/hono_context.d.ts +1 -1
  23. package/dist/http/CLAUDE.md +2 -1
  24. package/dist/http/auth_shape.d.ts +6 -6
  25. package/dist/http/error_schemas.d.ts +12 -2
  26. package/dist/http/error_schemas.d.ts.map +1 -1
  27. package/dist/http/error_schemas.js +10 -1
  28. package/dist/http/origin.d.ts +10 -6
  29. package/dist/http/origin.d.ts.map +1 -1
  30. package/dist/http/origin.js +10 -6
  31. package/dist/http/route_spec.d.ts +19 -12
  32. package/dist/http/route_spec.d.ts.map +1 -1
  33. package/dist/http/route_spec.js +33 -6
  34. package/dist/http/schema_helpers.d.ts +3 -1
  35. package/dist/http/schema_helpers.d.ts.map +1 -1
  36. package/dist/http/schema_helpers.js +3 -2
  37. package/dist/http/simple_request.d.ts +63 -0
  38. package/dist/http/simple_request.d.ts.map +1 -0
  39. package/dist/http/simple_request.js +84 -0
  40. package/dist/http/surface.js +0 -1
  41. package/dist/realtime/sse.js +0 -1
  42. package/dist/server/app_server.d.ts +2 -2
  43. package/dist/server/app_server.d.ts.map +1 -1
  44. package/dist/server/app_server.js +5 -2
  45. package/dist/testing/CLAUDE.md +16 -0
  46. package/dist/testing/admin_integration.d.ts.map +1 -1
  47. package/dist/testing/admin_integration.js +2 -1
  48. package/dist/testing/attack_surface.d.ts.map +1 -1
  49. package/dist/testing/attack_surface.js +14 -15
  50. package/dist/testing/audit_completeness.js +1 -1
  51. package/dist/testing/cross_backend/action_manifest.d.ts +10 -10
  52. package/dist/testing/cross_backend/simple_request.d.ts +11 -0
  53. package/dist/testing/cross_backend/simple_request.d.ts.map +1 -0
  54. package/dist/testing/cross_backend/simple_request.js +142 -0
  55. package/dist/testing/cross_backend/testing_reset_actions.d.ts +5 -5
  56. package/dist/testing/integration.d.ts.map +1 -1
  57. package/dist/testing/integration.js +40 -11
  58. package/dist/testing/rpc_helpers.d.ts.map +1 -1
  59. package/dist/testing/rpc_helpers.js +19 -7
  60. package/dist/testing/stubs.d.ts.map +1 -1
  61. package/dist/testing/stubs.js +3 -0
  62. package/dist/testing/transports/ws_client.js +0 -1
  63. package/dist/ui/auth_state.svelte.d.ts.map +1 -1
  64. package/dist/ui/auth_state.svelte.js +5 -1
  65. package/package.json +2 -2
@@ -91,13 +91,17 @@ export const describe_adversarial_auth = (options) => {
91
91
  const spec_lookup = build_route_spec_lookup(route_specs);
92
92
  const test_path = (route) => resolve_valid_path(route.path, spec_lookup.get(`${route.method} ${route.path}`)?.params);
93
93
  const apps = create_auth_test_apps(route_specs, roles);
94
+ // every route's request carries the JSON content type a real client sends, so
95
+ // the per-route content-type check (415) never answers ahead of the auth gate
96
+ const probe_init = (route) => ({
97
+ method: route.method,
98
+ headers: { 'content-type': 'application/json' }
99
+ });
94
100
  describe('adversarial HTTP auth enforcement', () => {
95
101
  describe('unauthenticated → 401', () => {
96
102
  for (const route of protected_routes) {
97
103
  test(`${route.method} ${route.path}`, async () => {
98
- const res = await apps.public.request(test_path(route), {
99
- method: route.method
100
- });
104
+ const res = await apps.public.request(test_path(route), probe_init(route));
101
105
  assert.strictEqual(res.status, 401, `${route.method} ${route.path}`);
102
106
  const body = await res.json();
103
107
  assert.strictEqual(body.error, ERROR_AUTHENTICATION_REQUIRED);
@@ -126,9 +130,7 @@ export const describe_adversarial_auth = (options) => {
126
130
  const app = apps.by_role.get(wrong_role);
127
131
  if (!app)
128
132
  throw new Error(`No test app for role '${wrong_role}'`);
129
- const res = await app.request(test_path(route), {
130
- method: route.method
131
- });
133
+ const res = await app.request(test_path(route), probe_init(route));
132
134
  assert.strictEqual(res.status, 403, `${route.method} ${route.path}`);
133
135
  const body = await res.json();
134
136
  assert.strictEqual(body.error, ERROR_INSUFFICIENT_PERMISSIONS);
@@ -141,9 +143,7 @@ export const describe_adversarial_auth = (options) => {
141
143
  describe('authenticated without role → 403', () => {
142
144
  for (const route of role_only_routes) {
143
145
  test(`${route.method} ${route.path}`, async () => {
144
- const res = await apps.authed.request(test_path(route), {
145
- method: route.method
146
- });
146
+ const res = await apps.authed.request(test_path(route), probe_init(route));
147
147
  assert.strictEqual(res.status, 403, `${route.method} ${route.path}`);
148
148
  const body = await res.json();
149
149
  assert.strictEqual(body.error, ERROR_INSUFFICIENT_PERMISSIONS);
@@ -175,9 +175,9 @@ export const describe_adversarial_auth = (options) => {
175
175
  continue;
176
176
  }
177
177
  test(`${route.method} ${route.path} (${refused})`, async () => {
178
- const res = await apps.by_credential_type.get(refused).request(test_path(route), {
179
- method: route.method
180
- });
178
+ const res = await apps.by_credential_type
179
+ .get(refused)
180
+ .request(test_path(route), probe_init(route));
181
181
  assert.strictEqual(res.status, 403, `${route.method} ${route.path}`);
182
182
  const body = await res.json();
183
183
  assert.strictEqual(body.error, ERROR_CREDENTIAL_TYPE_REQUIRED);
@@ -190,12 +190,11 @@ export const describe_adversarial_auth = (options) => {
190
190
  describe('correct auth passes guard', () => {
191
191
  for (const route of protected_routes) {
192
192
  test(`${route.method} ${route.path}`, async () => {
193
- const res = await select_auth_app(apps, route.auth).request(test_path(route), {
194
- method: route.method
195
- });
193
+ const res = await select_auth_app(apps, route.auth).request(test_path(route), probe_init(route));
196
194
  // handler may error (500) or return 404 (stub deps) — that's fine, we only verify auth passed
197
195
  assert.notStrictEqual(res.status, 401, 'should not be 401 (auth rejected)');
198
196
  assert.notStrictEqual(res.status, 403, 'should not be 403 (role rejected)');
197
+ assert.notStrictEqual(res.status, 415, 'should not be 415 (content type refused)');
199
198
  // handler-level 404 (resource not found with stub deps) is fine —
200
199
  // only reject router-level 404 (route not registered)
201
200
  if (res.status === 404) {
@@ -155,7 +155,7 @@ export const describe_audit_completeness_tests = (options) => {
155
155
  assert.ok(logout_route, 'Expected POST /logout route');
156
156
  const res = await fixture.transport(logout_route.path, {
157
157
  method: 'POST',
158
- headers: fixture.create_session_headers()
158
+ headers: fixture.create_session_headers({ 'content-type': 'application/json' })
159
159
  });
160
160
  assert.strictEqual(res.status, 200);
161
161
  const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
@@ -49,15 +49,15 @@ export declare const ActionManifestEntry: z.ZodObject<{
49
49
  side_effects: z.ZodBoolean;
50
50
  /** The `account` auth axis. */
51
51
  account: z.ZodEnum<{
52
- optional: "optional";
53
- none: "none";
54
52
  required: "required";
53
+ none: "none";
54
+ optional: "optional";
55
55
  }>;
56
56
  /** The `actor` auth axis. */
57
57
  actor: z.ZodEnum<{
58
- optional: "optional";
59
- none: "none";
60
58
  required: "required";
59
+ none: "none";
60
+ optional: "optional";
61
61
  }>;
62
62
  /** Required roles (any-of), sorted; `[]` when the action declares no role gate. */
63
63
  roles: z.ZodArray<z.ZodString>;
@@ -65,9 +65,9 @@ export declare const ActionManifestEntry: z.ZodObject<{
65
65
  credential_types: z.ZodArray<z.ZodString>;
66
66
  /** Per-action rate-limit class, or `null` when the action declares none. */
67
67
  rate_limit: z.ZodNullable<z.ZodEnum<{
68
+ both: "both";
68
69
  account: "account";
69
70
  ip: "ip";
70
- both: "both";
71
71
  }>>;
72
72
  }, z.core.$strict>;
73
73
  export type ActionManifestEntry = z.infer<typeof ActionManifestEntry>;
@@ -80,15 +80,15 @@ export declare const ActionManifest: z.ZodObject<{
80
80
  side_effects: z.ZodBoolean;
81
81
  /** The `account` auth axis. */
82
82
  account: z.ZodEnum<{
83
- optional: "optional";
84
- none: "none";
85
83
  required: "required";
84
+ none: "none";
85
+ optional: "optional";
86
86
  }>;
87
87
  /** The `actor` auth axis. */
88
88
  actor: z.ZodEnum<{
89
- optional: "optional";
90
- none: "none";
91
89
  required: "required";
90
+ none: "none";
91
+ optional: "optional";
92
92
  }>;
93
93
  /** Required roles (any-of), sorted; `[]` when the action declares no role gate. */
94
94
  roles: z.ZodArray<z.ZodString>;
@@ -96,9 +96,9 @@ export declare const ActionManifest: z.ZodObject<{
96
96
  credential_types: z.ZodArray<z.ZodString>;
97
97
  /** Per-action rate-limit class, or `null` when the action declares none. */
98
98
  rate_limit: z.ZodNullable<z.ZodEnum<{
99
+ both: "both";
99
100
  account: "account";
100
101
  ip: "ip";
101
- both: "both";
102
102
  }>>;
103
103
  }, z.core.$strict>>;
104
104
  }, z.core.$strict>;
@@ -0,0 +1,11 @@
1
+ import '../assert_dev_env.js';
2
+ import type { RpcPathCrossSuiteOptions } from './setup.js';
3
+ /** Options for the simple-request parity suite. */
4
+ export interface SimpleRequestCrossTestOptions extends RpcPathCrossSuiteOptions {
5
+ /** REST login route path. Default `/api/account/login` (the spine convention). */
6
+ readonly login_path?: string;
7
+ /** REST logout route path. Default `/api/account/logout` (the spine convention). */
8
+ readonly logout_path?: string;
9
+ }
10
+ export declare const describe_simple_request_cross_tests: (options: SimpleRequestCrossTestOptions) => void;
11
+ //# sourceMappingURL=simple_request.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"simple_request.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/simple_request.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA6B9B,OAAO,KAAK,EAAE,wBAAwB,EAAe,MAAM,YAAY,CAAC;AAGxE,mDAAmD;AACnD,MAAM,WAAW,6BAA8B,SAAQ,wBAAwB;IAC9E,kFAAkF;IAClF,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,oFAAoF;IACpF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAaD,eAAO,MAAM,mCAAmC,GAC/C,SAAS,6BAA6B,KACpC,IAuHF,CAAC"}
@@ -0,0 +1,142 @@
1
+ import '../assert_dev_env.js';
2
+ /**
3
+ * Cross-backend parity suite for refusing CORS simple requests.
4
+ *
5
+ * A `POST` with no `Content-Type`, or a `text/plain` / form / multipart one,
6
+ * is what a browser sends cross-origin without a preflight — carrying the
7
+ * session cookie when the page is same-site, which any other `localhost`
8
+ * port is. Every spine's JSON routes require `application/json` and refuse
9
+ * anything else with a flat `415` `{error: 'unsupported_content_type'}`
10
+ * before reading a credential or the body: the TS spine per route
11
+ * (`apply_route_specs`, every `POST` that isn't `raw_body`), the Rust spine
12
+ * in each JSON handler. The cases carry an allowed `Origin` and, where it
13
+ * matters, the keeper's session — the exact shape of a same-site page's
14
+ * forged request, which the origin allowlist alone admits when it's a
15
+ * wildcard — and a refused request must leave that session untouched.
16
+ *
17
+ * Runs both legs via the shared `setup_test` protocol: in-process
18
+ * (`auth/simple_request_parity.db.test.ts`) and cross-process
19
+ * (`cross_backend/simple_request.cross.test.ts`). Every spine has the gate,
20
+ * so the suite is ungated.
21
+ *
22
+ * @module
23
+ */
24
+ import { describe, test, assert } from 'vitest';
25
+ import { account_verify_action_spec } from '../../auth/account_action_specs.js';
26
+ import { ERROR_UNSUPPORTED_CONTENT_TYPE } from '../../http/error_schemas.js';
27
+ import { SPINE_RPC_PATH } from './spine_surface_constants.js';
28
+ /** Build the JSON-RPC envelope body for a nullary `account_verify` call. */
29
+ const verify_envelope = (id) => JSON.stringify({ jsonrpc: '2.0', method: account_verify_action_spec.method, id });
30
+ /** Assert a flat `415` content-type refusal. */
31
+ const assert_refused = async (res, label) => {
32
+ assert.strictEqual(res.status, 415, `${label} must be refused with 415`);
33
+ const body = (await res.json().catch(() => undefined));
34
+ assert.strictEqual(body?.error, ERROR_UNSUPPORTED_CONTENT_TYPE, label);
35
+ };
36
+ export const describe_simple_request_cross_tests = (options) => {
37
+ const { setup_test } = options;
38
+ const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
39
+ const login_path = options.login_path ?? '/api/account/login';
40
+ const logout_path = options.logout_path ?? '/api/account/logout';
41
+ /** POST a JSON `account_verify` with the fixture's session — 200 while it's valid. */
42
+ const verify_session = async (fixture, id) => {
43
+ const res = await fixture.transport(rpc_path, {
44
+ method: 'POST',
45
+ headers: { ...fixture.create_session_headers(), 'content-type': 'application/json' },
46
+ body: verify_envelope(id)
47
+ });
48
+ return res.status;
49
+ };
50
+ describe('CORS simple-request refusal parity', () => {
51
+ test('a CORS-safelisted RPC POST with a session → 415, in any spelling', async () => {
52
+ const fixture = await setup_test();
53
+ for (const content_type of [
54
+ 'text/plain;charset=UTF-8',
55
+ 'Text/Plain',
56
+ 'application/x-www-form-urlencoded',
57
+ 'multipart/form-data; boundary=x'
58
+ ]) {
59
+ const res = await fixture.transport(rpc_path, {
60
+ method: 'POST',
61
+ headers: { ...fixture.create_session_headers(), 'content-type': content_type },
62
+ body: verify_envelope('simple-safelisted')
63
+ });
64
+ await assert_refused(res, `a ${content_type} RPC POST`);
65
+ }
66
+ });
67
+ test('RPC POST with no Content-Type → 415', async () => {
68
+ const fixture = await setup_test();
69
+ // a byte body, so `fetch` adds no `Content-Type` of its own (a string
70
+ // body would get `text/plain;charset=UTF-8`)
71
+ const res = await fixture.transport(rpc_path, {
72
+ method: 'POST',
73
+ headers: fixture.create_session_headers(),
74
+ body: new TextEncoder().encode(verify_envelope('simple-untyped'))
75
+ });
76
+ await assert_refused(res, 'an untyped RPC POST');
77
+ });
78
+ test('a JSON route refuses any type but application/json', async () => {
79
+ const fixture = await setup_test();
80
+ for (const content_type of ['application/octet-stream', 'application/vnd.api+json', '']) {
81
+ const res = await fixture.transport(rpc_path, {
82
+ method: 'POST',
83
+ headers: { ...fixture.create_session_headers(), 'content-type': content_type },
84
+ body: verify_envelope('simple-not-json')
85
+ });
86
+ await assert_refused(res, `an RPC POST typed '${content_type}'`);
87
+ const login = await fixture.fresh_transport()(login_path, {
88
+ method: 'POST',
89
+ headers: { 'content-type': content_type },
90
+ body: JSON.stringify({ username: 'keeper', password: 'whatever' })
91
+ });
92
+ await assert_refused(login, `a login typed '${content_type}'`);
93
+ }
94
+ });
95
+ test('form-encoded login → 415', async () => {
96
+ const fixture = await setup_test();
97
+ const res = await fixture.fresh_transport()(login_path, {
98
+ method: 'POST',
99
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
100
+ body: 'username=keeper&password=whatever'
101
+ });
102
+ await assert_refused(res, 'a form-encoded login');
103
+ });
104
+ test('a forged text/plain logout with a session → 415, and the session survives', async () => {
105
+ const fixture = await setup_test();
106
+ const res = await fixture.transport(logout_path, {
107
+ method: 'POST',
108
+ headers: { ...fixture.create_session_headers(), 'content-type': 'text/plain' }
109
+ });
110
+ await assert_refused(res, 'a text/plain logout');
111
+ assert.strictEqual(await verify_session(fixture, 'after-forged-logout'), 200, 'the refused logout must not end the session');
112
+ });
113
+ test('an untyped logout with no session → 415, before the auth check', async () => {
114
+ const fixture = await setup_test();
115
+ const res = await fixture.fresh_transport()(logout_path, {
116
+ method: 'POST',
117
+ headers: { 'content-type': 'application/octet-stream' }
118
+ });
119
+ await assert_refused(res, 'an octet-stream logout');
120
+ });
121
+ test('JSON logout with no body and no session → 401, not 415', async () => {
122
+ const fixture = await setup_test();
123
+ const res = await fixture.fresh_transport()(logout_path, {
124
+ method: 'POST',
125
+ headers: { 'content-type': 'application/json' }
126
+ });
127
+ assert.strictEqual(res.status, 401, 'a bodyless JSON logout must reach the auth check');
128
+ });
129
+ test('application/json with a charset → dispatched', async () => {
130
+ const fixture = await setup_test();
131
+ const res = await fixture.transport(rpc_path, {
132
+ method: 'POST',
133
+ headers: {
134
+ ...fixture.create_session_headers(),
135
+ 'content-type': 'application/json; charset=utf-8'
136
+ },
137
+ body: verify_envelope('json-charset')
138
+ });
139
+ assert.strictEqual(res.status, 200, 'a JSON RPC POST must be dispatched');
140
+ });
141
+ });
142
+ };
@@ -427,21 +427,21 @@ export declare const testing_action_manifest_action_spec: {
427
427
  method: z.ZodString;
428
428
  side_effects: z.ZodBoolean;
429
429
  account: z.ZodEnum<{
430
- optional: "optional";
431
- none: "none";
432
430
  required: "required";
431
+ none: "none";
432
+ optional: "optional";
433
433
  }>;
434
434
  actor: z.ZodEnum<{
435
- optional: "optional";
436
- none: "none";
437
435
  required: "required";
436
+ none: "none";
437
+ optional: "optional";
438
438
  }>;
439
439
  roles: z.ZodArray<z.ZodString>;
440
440
  credential_types: z.ZodArray<z.ZodString>;
441
441
  rate_limit: z.ZodNullable<z.ZodEnum<{
442
+ both: "both";
442
443
  account: "account";
443
444
  ip: "ip";
444
- both: "both";
445
445
  }>>;
446
446
  }, z.core.$strict>>;
447
447
  }, z.core.$strict>;
@@ -1 +1 @@
1
- {"version":3,"file":"integration.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/integration.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAsB7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAMhE,OAAO,EAKN,KAAK,uBAAuB,EAC5B,MAAM,kBAAkB,CAAC;AAkB1B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAW,KAAK,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AAKpF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAG1D;;GAEG;AACH,MAAM,WAAW,8BAA8B;IAC9C;;;;OAIG;IACH,UAAU,EAAE,SAAS,CAAC;IACtB;;;;;;;;;OASG;IACH,cAAc,EAAE,cAAc,CAAC;IAC/B,kEAAkE;IAClE,YAAY,EAAE,mBAAmB,CAAC;IAClC;;;;OAIG;IACH,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC;;;;;;;;;;;;OAYG;IACH,aAAa,EAAE,uBAAuB,CAAC;IACvC;;;;;;;OAOG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,mCAAmC,GAC/C,SAAS,8BAA8B,KACrC,IAs8CF,CAAC"}
1
+ {"version":3,"file":"integration.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/integration.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAsB7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAMhE,OAAO,EAKN,KAAK,uBAAuB,EAC5B,MAAM,kBAAkB,CAAC;AAkB1B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,OAAO,EAAW,KAAK,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AAKpF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAG1D;;GAEG;AACH,MAAM,WAAW,8BAA8B;IAC9C;;;;OAIG;IACH,UAAU,EAAE,SAAS,CAAC;IACtB;;;;;;;;;OASG;IACH,cAAc,EAAE,cAAc,CAAC;IAC/B,kEAAkE;IAClE,YAAY,EAAE,mBAAmB,CAAC;IAClC;;;;OAIG;IACH,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC;;;;;;;;;;;;OAYG;IACH,aAAa,EAAE,uBAAuB,CAAC;IACvC;;;;;;;OAOG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,mCAAmC,GAC/C,SAAS,8BAA8B,KACrC,IAw+CF,CAAC"}
@@ -72,6 +72,17 @@ export const describe_standard_integration_tests = (options) => {
72
72
  const { cookie_name } = options.session_options;
73
73
  // Error coverage tracking across test groups
74
74
  const error_collector = new ErrorCoverageCollector();
75
+ // the auth routes this suite drives: login / logout / password, signup
76
+ // (when wired), and the shared RPC endpoint — admin-role routes belong to
77
+ // the admin suite
78
+ const is_suite_auth_route = (s) => {
79
+ if (s.auth.roles?.includes('admin') ?? false)
80
+ return false;
81
+ const rest_suffixes = ['/login', '/logout', '/password', '/signup'];
82
+ if (rest_suffixes.some((suffix) => s.path.endsWith(suffix)))
83
+ return true;
84
+ return s.path === rpc_path;
85
+ };
75
86
  afterAll(() => {
76
87
  // Scope coverage to auth routes this suite actually exercises:
77
88
  // login / logout / password drivers + signup invite edge cases
@@ -83,14 +94,7 @@ export const describe_standard_integration_tests = (options) => {
83
94
  // `create_test_app_for_bootstrap`. Consumer-specific
84
95
  // routes would dilute the ratio; admin-role routes are scoped to
85
96
  // the admin suite instead.
86
- const auth_routes = route_specs.filter((s) => {
87
- if (s.auth.roles?.includes('admin') ?? false)
88
- return false;
89
- const rest_suffixes = ['/login', '/logout', '/password', '/signup'];
90
- if (rest_suffixes.some((suffix) => s.path.endsWith(suffix)))
91
- return true;
92
- return s.path === rpc_path;
93
- });
97
+ const auth_routes = route_specs.filter(is_suite_auth_route);
94
98
  assert_error_coverage(error_collector, auth_routes.length > 0 ? auth_routes : route_specs, {
95
99
  min_coverage: options.error_coverage_min ?? DEFAULT_INTEGRATION_ERROR_COVERAGE,
96
100
  // Authorization denials (403) on these scoped auth routes — the
@@ -233,7 +237,7 @@ export const describe_standard_integration_tests = (options) => {
233
237
  // Logout
234
238
  const logout_res = await fixture.transport(logout_route.path, {
235
239
  method: 'POST',
236
- headers: create_headers()
240
+ headers: { ...create_headers(), 'content-type': 'application/json' }
237
241
  });
238
242
  assert.strictEqual(logout_res.status, 200);
239
243
  const logout_body = await logout_res.json();
@@ -633,7 +637,11 @@ export const describe_standard_integration_tests = (options) => {
633
637
  path: rpc_path,
634
638
  spec: account_verify_action_spec,
635
639
  params: undefined,
636
- headers: { ...bearer_headers, origin: 'http://localhost:5173' }
640
+ headers: {
641
+ ...bearer_headers,
642
+ origin: 'http://localhost:5173',
643
+ 'content-type': 'application/json'
644
+ }
637
645
  });
638
646
  assert.strictEqual(res.status, 401);
639
647
  });
@@ -1029,7 +1037,11 @@ export const describe_standard_integration_tests = (options) => {
1029
1037
  });
1030
1038
  const res = await fixture.fresh_transport()(logout_route.path, {
1031
1039
  method: 'POST',
1032
- headers: { ...bearer_headers, origin: 'http://localhost:5173' }
1040
+ headers: {
1041
+ ...bearer_headers,
1042
+ origin: 'http://localhost:5173',
1043
+ 'content-type': 'application/json'
1044
+ }
1033
1045
  });
1034
1046
  assert.strictEqual(res.status, 401, 'Bearer with Origin should be discarded → unauthenticated');
1035
1047
  error_collector.record(route_specs, 'POST', logout_route.path, 401);
@@ -1049,6 +1061,23 @@ export const describe_standard_integration_tests = (options) => {
1049
1061
  error_collector.record(route_specs, 'POST', password_route.path, 401);
1050
1062
  });
1051
1063
  });
1064
+ // --- 12b. Content type: every JSON POST route refuses an untyped body ---
1065
+ describe('JSON routes refuse an untyped POST', () => {
1066
+ test('each scoped JSON POST route answers an untyped POST with 415', async () => {
1067
+ const fixture = await options.setup_test();
1068
+ const json_post_routes = route_specs.filter((s) => s.method === 'POST' && !s.raw_body && is_suite_auth_route(s));
1069
+ assert.ok(json_post_routes.length > 0, 'Expected JSON POST routes');
1070
+ for (const route of json_post_routes) {
1071
+ // a byte body, so `fetch` adds no `Content-Type` of its own
1072
+ const res = await fixture.fresh_transport()(route.path, {
1073
+ method: 'POST',
1074
+ body: new TextEncoder().encode('{}')
1075
+ });
1076
+ assert.strictEqual(res.status, 415, `untyped POST ${route.path}`);
1077
+ await error_collector.assert_and_record(route_specs, 'POST', route.path, res);
1078
+ }
1079
+ });
1080
+ });
1052
1081
  // --- 13. Password change revokes API tokens ---
1053
1082
  describe('password change revokes API tokens', () => {
1054
1083
  test('API tokens are invalidated after password change', async () => {
@@ -1 +1 @@
1
- {"version":3,"file":"rpc_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/rpc_helpers.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAa7B,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAIN,KAAK,gBAAgB,EACrB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAC;AAC3E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,EACX,qBAAqB,EACrB,mBAAmB,EACnB,eAAe,EACf,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAGhE;;;;;;;;;GASG;AACH,MAAM,MAAM,uBAAuB,GAClC,KAAK,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,eAAe,CAAC,CAAC,CAAC;AAE9E;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,+BAA+B,GAC3C,eAAe,uBAAuB,EACtC,iBAAiB,cAAc,CAAC,MAAM,CAAC,KACrC,KAAK,CAAC,eAAe,CAuBvB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB,GAChC,QAAQ,MAAM,EACd,SAAS,OAAO,EAChB,KAAI,MAAM,GAAG,MAAe,KAC1B,WAQF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,GAC9B,eAAe,MAAM,EACrB,QAAQ,MAAM,EACd,SAAS,OAAO,EAChB,KAAI,MAAM,GAAG,MAAe,KAC1B,MAMF,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,OAAO,EACb,gBAAgB,gBAAgB,KAC9B,IAUF,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,+BAA+B,GAAI,MAAM,OAAO,EAAE,gBAAgB,CAAC,CAAC,OAAO,KAAG,IAa1F,CAAC;AAIF;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAErF,2DAA2D;AAC3D,eAAO,MAAM,cAAc,GACzB,KAAK;IACL,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAAC;CAC5E,KAAG,gBAEmB,CAAC;AAEzB;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,GAAI,SAAS,OAAO,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAMzE,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,aAAa,GACtB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GAC9E;IACA,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CACxD,CAAC;AAEL;;;;GAIG;AACH,MAAM,MAAM,UAAU,GACnB;IAAE,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAAA;CAAE,GAC/E,gBAAgB,CAAC;AAEpB,gCAAgC;AAChC,MAAM,WAAW,WAAW;IAC3B,yDAAyD;IACzD,GAAG,EAAE,UAAU,CAAC;IAChB,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,4BAA4B;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,wCAAwC;IACxC,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACrB,mFAAmF;IACnF,IAAI,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IACtB;;;;;OAKG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;CAClC;AAcD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,QAAQ,GAAU,MAAM,WAAW,KAAG,OAAO,CAAC,aAAa,CA8DvE,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,IAAI,CAAC,WAAW,EAAE,yBAAyB,CAAC,KAChD,OAAO,CAAC,aAAa,CAAyD,CAAC;AAElF;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,CAAC,KAAK,SAAS,yBAAyB,IACrE;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAA;CAAE,GAC/F;IACA,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CACxD,CAAC;AAEL,mFAAmF;AACnF,MAAM,MAAM,kBAAkB,CAAC,KAAK,SAAS,yBAAyB,IAAI,IAAI,CAC7E,WAAW,EACX,QAAQ,GAAG,QAAQ,CACnB,GAAG;IACH,2GAA2G;IAC3G,IAAI,EAAE,KAAK,CAAC;IACZ,0CAA0C;IAC1C,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;CAChC,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,iBAAiB,GAAU,KAAK,SAAS,yBAAyB,EAC9E,MAAM,kBAAkB,CAAC,KAAK,CAAC,KAC7B,OAAO,CAAC,oBAAoB,CAAC,KAAK,CAAC,CAoBrC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,GAAU,CAAC,EACrC,MAAM,WAAW,EACjB,eAAe,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KACzB,OAAO,CAAC,CAAC,CAkBX,CAAC;AAIF;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAC3B,eAAe,aAAa,CAAC,eAAe,CAAC,EAC7C,QAAQ,MAAM,KACZ;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,SAAS,CAAA;CAAE,GAAG,SAOxC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAC3B,eAAe,aAAa,CAAC,qBAAqB,CAAC,EACnD,QAAQ,MAAM,KACZ;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,mBAAmB,CAAA;CAAE,GAAG,SAOvD,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,yBAAyB,GACrC,eAAe,aAAa,CAAC,eAAe,CAAC,KAC3C,MAcF,CAAC"}
1
+ {"version":3,"file":"rpc_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/rpc_helpers.ts"],"names":[],"mappings":"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"}
@@ -9,7 +9,6 @@ import './assert_dev_env.js';
9
9
  * @module
10
10
  */
11
11
  import { assert } from 'vitest';
12
- import { z } from 'zod';
13
12
  import { JSONRPC_VERSION, JsonrpcErrorResponse, JsonrpcResponse } from '../http/jsonrpc.js';
14
13
  import { create_stub_app_server_context } from './stubs.js';
15
14
  /**
@@ -146,6 +145,23 @@ const RPC_CALL_DEFAULT_HEADERS_BASE = {
146
145
  host: 'localhost',
147
146
  'Content-Type': 'application/json'
148
147
  };
148
+ /**
149
+ * Merge header records case-insensitively, later sources replacing earlier
150
+ * ones — a plain spread would keep `Content-Type` and `content-type` as two
151
+ * keys, which `fetch` joins into one comma-separated value. Names come out
152
+ * lowercased; values pass through unvalidated, so a test can still send a
153
+ * malformed header to the server.
154
+ */
155
+ const merge_header_records = (...sources) => {
156
+ const merged = {};
157
+ for (const source of sources) {
158
+ if (!source)
159
+ continue;
160
+ for (const [name, value] of Object.entries(source))
161
+ merged[name.toLowerCase()] = value;
162
+ }
163
+ return merged;
164
+ };
149
165
  /** Default headers merged into every `rpc_call` request. Includes `origin`. */
150
166
  const RPC_CALL_DEFAULT_HEADERS = {
151
167
  ...RPC_CALL_DEFAULT_HEADERS_BASE,
@@ -172,18 +188,14 @@ export const rpc_call = async (args) => {
172
188
  let init;
173
189
  if (verb === 'GET') {
174
190
  url = create_rpc_get_url(path, method, params, id);
175
- init = { method: 'GET', headers: { ...defaults, ...headers } };
191
+ init = { method: 'GET', headers: merge_header_records(defaults, headers) };
176
192
  }
177
193
  else {
178
194
  url = path;
179
195
  const post = create_rpc_post_init(method, params, id);
180
196
  init = {
181
197
  method: 'POST',
182
- headers: {
183
- ...defaults,
184
- ...post.headers,
185
- ...headers
186
- },
198
+ headers: merge_header_records(defaults, post.headers, headers),
187
199
  body: post.body
188
200
  };
189
201
  }
@@ -1 +1 @@
1
- {"version":3,"file":"stubs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/stubs.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"stubs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/stubs.ts"],"names":[],"mappings":"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"}
@@ -313,6 +313,9 @@ export const create_test_app_surface_spec = (options) => {
313
313
  { ...bootstrap_route_shape, handler: stub_handler }
314
314
  ])
315
315
  : [];
316
+ for (const route of bootstrap_route_specs) {
317
+ assert_endpoint_in_auth_scope('bootstrap.route_prefix', route.path);
318
+ }
316
319
  // the surface route mirrors `create_app_server`'s opt-in mount — its handler
317
320
  // serves the surface generated below, backfilled into the same ref
318
321
  const surface_ref = {
@@ -19,7 +19,6 @@ import '../assert_dev_env.js';
19
19
  * @module
20
20
  */
21
21
  import { create_jsonrpc_error_response, create_jsonrpc_response, is_jsonrpc_error_response, is_jsonrpc_notification, is_jsonrpc_request, is_jsonrpc_response } from '../../http/jsonrpc_helpers.js';
22
- import { JSONRPC_VERSION } from '../../http/jsonrpc.js';
23
22
  // ---------------------------------------------------------------------
24
23
  // Predicates — compose with `WsClient.wait_for` and
25
24
  // `messages.filter(...)`. Both in-process and cross-process tests use
@@ -1 +1 @@
1
- {"version":3,"file":"auth_state.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/auth_state.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAMH,OAAO,EACN,KAAK,gBAAgB,EAErB,KAAK,oBAAoB,EACzB,KAAK,cAAc,EACnB,MAAM,2BAA2B,CAAC;AAGnC;;;GAGG;AACH,eAAO,MAAM,kBAAkB;;;;CAA8B,CAAC;AAE9D,qBAAa,SAAS;IACrB,SAAS,UAAqB;IAC9B,QAAQ,UAAqB;IAC7B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAoB;IAC/C,OAAO,EAAE,cAAc,GAAG,IAAI,CAAoB;IAClD,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAoB;IAClD,WAAW,EAAE,KAAK,CAAC,oBAAoB,CAAC,CAAkB;IAC1D;;;;OAIG;IACH,QAAQ,CAAC,kBAAkB,EAAE,KAAK,CAAC,oBAAoB,CAAC,CAEtD;IACF;;;;;;;;OAQG;IACH,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAExC;IACF,wFAAwF;IACxF,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAA+C;IAEzE,gEAAgE;IAChE,eAAe,UAAqB;IAEpC;;;;;;OAMG;IACH,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAItC;;;;;;;;OAQG;IACG,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IA6BpC;;;;;;;OAOG;IACG,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA0CjE;;;;;OAKG;IACG,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAiCpF;;;;;;;OAOG;IACG,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA8ClF;;;;;OAKG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;CAW7B"}
1
+ {"version":3,"file":"auth_state.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/auth_state.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAMH,OAAO,EACN,KAAK,gBAAgB,EAErB,KAAK,oBAAoB,EACzB,KAAK,cAAc,EACnB,MAAM,2BAA2B,CAAC;AAGnC;;;GAGG;AACH,eAAO,MAAM,kBAAkB;;;;CAA8B,CAAC;AAE9D,qBAAa,SAAS;IACrB,SAAS,UAAqB;IAC9B,QAAQ,UAAqB;IAC7B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAoB;IAC/C,OAAO,EAAE,cAAc,GAAG,IAAI,CAAoB;IAClD,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAoB;IAClD,WAAW,EAAE,KAAK,CAAC,oBAAoB,CAAC,CAAkB;IAC1D;;;;OAIG;IACH,QAAQ,CAAC,kBAAkB,EAAE,KAAK,CAAC,oBAAoB,CAAC,CAEtD;IACF;;;;;;;;OAQG;IACH,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAExC;IACF,wFAAwF;IACxF,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAA+C;IAEzE,gEAAgE;IAChE,eAAe,UAAqB;IAEpC;;;;;;OAMG;IACH,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAItC;;;;;;;;OAQG;IACG,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IA6BpC;;;;;;;OAOG;IACG,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA0CjE;;;;;OAKG;IACG,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAiCpF;;;;;;;OAOG;IACG,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA8ClF;;;;;OAKG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;CAe7B"}
@@ -268,7 +268,11 @@ export class AuthState {
268
268
  */
269
269
  async logout() {
270
270
  try {
271
- await ui_fetch('/api/account/logout', { method: 'POST' });
271
+ // the content type is required of every API POST, body or not
272
+ await ui_fetch('/api/account/logout', {
273
+ method: 'POST',
274
+ headers: { 'Content-Type': 'application/json' }
275
+ });
272
276
  }
273
277
  catch {
274
278
  // Best-effort — clear local state regardless
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_app",
3
- "version": "0.131.0",
3
+ "version": "0.132.0",
4
4
  "description": "fullstack app library",
5
5
  "glyph": "🗝",
6
6
  "logo": "logo.svg",
@@ -97,7 +97,7 @@
97
97
  "@hono/node-server": "^1.19.14",
98
98
  "@hono/node-ws": "^1.3.1",
99
99
  "@node-rs/argon2": "^2.0.2",
100
- "@ryanatkn/eslint-config": "^0.12.3",
100
+ "@ryanatkn/eslint-config": "^0.13.0",
101
101
  "@sveltejs/acorn-typescript": "^1.0.9",
102
102
  "@sveltejs/adapter-static": "^4.0.0",
103
103
  "@sveltejs/kit": "^3.0.1",