@fuzdev/fuz_app 0.7.0 → 0.8.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 (33) hide show
  1. package/dist/actions/action_rpc.d.ts +3 -0
  2. package/dist/actions/action_rpc.d.ts.map +1 -1
  3. package/dist/actions/action_rpc.js +22 -10
  4. package/dist/auth/bearer_auth.d.ts +10 -0
  5. package/dist/auth/bearer_auth.d.ts.map +1 -1
  6. package/dist/auth/bearer_auth.js +29 -7
  7. package/dist/auth/middleware.d.ts.map +1 -1
  8. package/dist/auth/middleware.js +5 -1
  9. package/dist/http/schema_helpers.d.ts.map +1 -1
  10. package/dist/http/schema_helpers.js +21 -7
  11. package/dist/testing/adversarial_headers.d.ts.map +1 -1
  12. package/dist/testing/adversarial_headers.js +7 -10
  13. package/dist/testing/app_server.d.ts +2 -0
  14. package/dist/testing/app_server.d.ts.map +1 -1
  15. package/dist/testing/app_server.js +17 -0
  16. package/dist/testing/data_exposure.js +3 -4
  17. package/dist/testing/integration.d.ts +1 -1
  18. package/dist/testing/integration.d.ts.map +1 -1
  19. package/dist/testing/integration.js +11 -17
  20. package/dist/testing/round_trip.d.ts.map +1 -1
  21. package/dist/testing/round_trip.js +3 -4
  22. package/dist/testing/rpc_attack_surface.d.ts.map +1 -1
  23. package/dist/testing/rpc_attack_surface.js +25 -5
  24. package/dist/testing/rpc_round_trip.d.ts.map +1 -1
  25. package/dist/testing/rpc_round_trip.js +3 -4
  26. package/dist/testing/schema_generators.d.ts.map +1 -1
  27. package/dist/testing/schema_generators.js +30 -2
  28. package/dist/ui/BootstrapForm.svelte +13 -1
  29. package/dist/ui/BootstrapForm.svelte.d.ts +4 -1
  30. package/dist/ui/BootstrapForm.svelte.d.ts.map +1 -1
  31. package/dist/ui/LoginForm.svelte +1 -1
  32. package/dist/ui/SignupForm.svelte +1 -1
  33. package/package.json +1 -1
@@ -16,6 +16,7 @@ import type { RequestResponseActionSpec } from './action_spec.js';
16
16
  import { type RouteSpec } from '../http/route_spec.js';
17
17
  import { type RequestContext } from '../auth/request_context.js';
18
18
  import type { Db } from '../db/db.js';
19
+ import { type JsonrpcRequestId } from '../http/jsonrpc.js';
19
20
  /**
20
21
  * Per-request context provided to RPC action handlers.
21
22
  *
@@ -26,6 +27,8 @@ import type { Db } from '../db/db.js';
26
27
  export interface ActionContext {
27
28
  /** The authenticated identity, or `null` for public routes. */
28
29
  auth: RequestContext | null;
30
+ /** The JSON-RPC request ID from the envelope. */
31
+ request_id: JsonrpcRequestId;
29
32
  /** Transaction-scoped for mutations, pool-level for reads. */
30
33
  db: Db;
31
34
  /** Always pool-level — for fire-and-forget effects that outlive the transaction. */
@@ -1 +1 @@
1
- {"version":3,"file":"action_rpc.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_rpc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAoB,KAAK,SAAS,EAAC,MAAM,uBAAuB,CAAC;AACxE,OAAO,EAAgC,KAAK,cAAc,EAAC,MAAM,4BAA4B,CAAC;AAC9F,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,aAAa,CAAC;AAWpC;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC7B,+DAA+D;IAC/D,IAAI,EAAE,cAAc,GAAG,IAAI,CAAC;IAC5B,8DAA8D;IAC9D,EAAE,EAAE,EAAE,CAAC;IACP,oFAAoF;IACpF,aAAa,EAAE,EAAE,CAAC;IAClB,2EAA2E;IAC3E,eAAe,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IACtC,uBAAuB;IACvB,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;;;;GAKG;AACH,MAAM,MAAM,aAAa,CAAC,MAAM,GAAG,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,CACxD,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,aAAa,KACd,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEhC;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACzB,IAAI,EAAE,yBAAyB,CAAC;IAChC,OAAO,EAAE,aAAa,CAAC;CACvB;AAED,yCAAyC;AACzC,MAAM,WAAW,wBAAwB;IACxC,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,4BAA4B;IAC5B,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAC1B,2CAA2C;IAC3C,GAAG,EAAE,MAAM,CAAC;CACZ;AA4CD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,mBAAmB,GAAI,SAAS,wBAAwB,KAAG,KAAK,CAAC,SAAS,CA6NtF,CAAC"}
1
+ {"version":3,"file":"action_rpc.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_rpc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAoB,KAAK,SAAS,EAAC,MAAM,uBAAuB,CAAC;AACxE,OAAO,EAAgC,KAAK,cAAc,EAAC,MAAM,4BAA4B,CAAC;AAE9F,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,aAAa,CAAC;AAEpC,OAAO,EAAkC,KAAK,gBAAgB,EAAC,MAAM,oBAAoB,CAAC;AAS1F;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC7B,+DAA+D;IAC/D,IAAI,EAAE,cAAc,GAAG,IAAI,CAAC;IAC5B,iDAAiD;IACjD,UAAU,EAAE,gBAAgB,CAAC;IAC7B,8DAA8D;IAC9D,EAAE,EAAE,EAAE,CAAC;IACP,oFAAoF;IACpF,aAAa,EAAE,EAAE,CAAC;IAClB,2EAA2E;IAC3E,eAAe,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IACtC,uBAAuB;IACvB,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;;;;GAKG;AACH,MAAM,MAAM,aAAa,CAAC,MAAM,GAAG,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,CACxD,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,aAAa,KACd,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEhC;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACzB,IAAI,EAAE,yBAAyB,CAAC;IAChC,OAAO,EAAE,aAAa,CAAC;CACvB;AAED,yCAAyC;AACzC,MAAM,WAAW,wBAAwB;IACxC,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,4BAA4B;IAC5B,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAC1B,2CAA2C;IAC3C,GAAG,EAAE,MAAM,CAAC;CACZ;AAkDD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,mBAAmB,GAAI,SAAS,wBAAwB,KAAG,KAAK,CAAC,SAAS,CAoOtF,CAAC"}
@@ -15,9 +15,10 @@ import { z } from 'zod';
15
15
  import { DEV } from 'esm-env';
16
16
  import {} from '../http/route_spec.js';
17
17
  import { get_request_context, has_role } from '../auth/request_context.js';
18
+ import { CREDENTIAL_TYPE_KEY } from '../hono_context.js';
18
19
  import { is_null_schema } from '../http/schema_helpers.js';
19
20
  import { JSONRPC_VERSION, JsonrpcRequest } from '../http/jsonrpc.js';
20
- import { jsonrpc_error_messages, jsonrpc_error_code_to_http_status, JSONRPC_ERROR_CODES, ThrownJsonrpcError, } from '../http/jsonrpc_errors.js';
21
+ import { jsonrpc_error_messages, jsonrpc_error_code_to_http_status, JSONRPC_ERROR_CODES, } from '../http/jsonrpc_errors.js';
21
22
  /**
22
23
  * Format a JSON-RPC error response.
23
24
  *
@@ -35,9 +36,10 @@ const jsonrpc_error_response = (id, error) => ({
35
36
  *
36
37
  * @param auth - the action's auth requirement
37
38
  * @param request_context - the resolved identity (null if unauthenticated)
39
+ * @param credential_type - how the request was authenticated (session, api_token, daemon_token)
38
40
  * @returns an error json if auth fails, or null if authorized
39
41
  */
40
- const check_action_auth = (auth, request_context) => {
42
+ const check_action_auth = (auth, request_context, credential_type) => {
41
43
  if (auth === 'public')
42
44
  return null;
43
45
  if (!request_context)
@@ -45,9 +47,12 @@ const check_action_auth = (auth, request_context) => {
45
47
  if (auth === 'authenticated')
46
48
  return null;
47
49
  if (auth === 'keeper') {
48
- // keeper requires the keeper role
49
- if (!has_role(request_context, 'keeper'))
50
+ // keeper requires daemon_token credential type AND the keeper role.
51
+ // API tokens and session cookies cannot access keeper actions even
52
+ // if the account has the keeper permit.
53
+ if (credential_type !== 'daemon_token' || !has_role(request_context, 'keeper')) {
50
54
  return jsonrpc_error_messages.forbidden();
55
+ }
51
56
  return null;
52
57
  }
53
58
  // role check
@@ -117,7 +122,8 @@ export const create_rpc_endpoint = (options) => {
117
122
  }
118
123
  // step 3: auth check
119
124
  const request_context = get_request_context(c);
120
- const auth_error = check_action_auth(action.spec.auth, request_context);
125
+ const credential_type = c.get(CREDENTIAL_TYPE_KEY) ?? null;
126
+ const auth_error = check_action_auth(action.spec.auth, request_context, credential_type);
121
127
  if (auth_error) {
122
128
  const error = jsonrpc_error_response(id, auth_error);
123
129
  return c.json(error, jsonrpc_error_code_to_http_status(auth_error.code));
@@ -136,6 +142,7 @@ export const create_rpc_endpoint = (options) => {
136
142
  const execute = async (db) => {
137
143
  const action_context = {
138
144
  auth: request_context,
145
+ request_id: id,
139
146
  db,
140
147
  background_db: route.background_db,
141
148
  pending_effects: route.pending_effects,
@@ -160,11 +167,16 @@ export const create_rpc_endpoint = (options) => {
160
167
  return await execute(route.db);
161
168
  }
162
169
  catch (err) {
163
- if (err instanceof ThrownJsonrpcError) {
164
- const status = jsonrpc_error_code_to_http_status(err.code);
165
- const error_json = { code: err.code, message: err.message };
166
- if (err.data !== undefined)
167
- error_json.data = err.data;
170
+ // Duck-type check: Error with numeric `code` signals a JSON-RPC error.
171
+ // Avoids instanceof which fails when consumers throw their own ThrownJsonrpcError
172
+ // (structurally identical but different class identity, e.g. zzz's copy).
173
+ if (err instanceof Error && typeof err.code === 'number') {
174
+ const code = err.code;
175
+ const data = err.data;
176
+ const status = jsonrpc_error_code_to_http_status(code);
177
+ const error_json = { code, message: err.message };
178
+ if (data !== undefined)
179
+ error_json.data = data;
168
180
  return c.json(jsonrpc_error_response(id, error_json), status);
169
181
  }
170
182
  // generic error
@@ -17,6 +17,13 @@ import { type RateLimiter } from '../rate_limiter.js';
17
17
  /**
18
18
  * Create middleware that authenticates via bearer token.
19
19
  *
20
+ * Soft-fails for invalid, expired, or empty tokens — calls `next()` without
21
+ * setting a request context, letting downstream auth enforcement (per-action
22
+ * `check_action_auth` or `require_auth`) return a consistent JSON-RPC or
23
+ * route-level error. This avoids leaking token-specific diagnostics
24
+ * (`invalid_token`, `account_not_found`) that could aid enumeration attacks,
25
+ * and ensures public actions are not blocked by bad credentials.
26
+ *
20
27
  * Rejects bearer tokens when an `Origin` or `Referer` header is present —
21
28
  * browsers must use cookie auth to reduce attack surface.
22
29
  * Auth scheme matching is case-insensitive per RFC 7235.
@@ -24,6 +31,9 @@ import { type RateLimiter } from '../rate_limiter.js';
24
31
  * and sets it on the Hono context. Skips if a request context is already set
25
32
  * (e.g. by session middleware).
26
33
  *
34
+ * Rate limiting (429) is the only hard-fail — it's a throttling concern
35
+ * independent of auth identity.
36
+ *
27
37
  * @param deps - query dependencies (pool-level db for middleware)
28
38
  * @param ip_rate_limiter - per-IP rate limiter for bearer token attempts (null to disable)
29
39
  * @param log - the logger instance
@@ -1 +1 @@
1
- {"version":3,"file":"bearer_auth.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/bearer_auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,MAAM,CAAC;AAC5C,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAKpD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AAEnD,OAAO,EAA+B,KAAK,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAOlF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,SAAS,EACf,iBAAiB,WAAW,GAAG,IAAI,EACnC,KAAK,MAAM,KACT,iBAwEF,CAAC"}
1
+ {"version":3,"file":"bearer_auth.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/bearer_auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,MAAM,CAAC;AAC5C,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAKpD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AAEnD,OAAO,EAA+B,KAAK,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAElF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,SAAS,EACf,iBAAiB,WAAW,GAAG,IAAI,EACnC,KAAK,MAAM,KACT,iBAqFF,CAAC"}
@@ -15,10 +15,16 @@ import { CREDENTIAL_TYPE_KEY } from '../hono_context.js';
15
15
  import { query_validate_api_token } from './api_token_queries.js';
16
16
  import { get_client_ip } from '../http/proxy.js';
17
17
  import { rate_limit_exceeded_response } from '../rate_limiter.js';
18
- import { ERROR_BEARER_REJECTED_BROWSER, ERROR_INVALID_TOKEN, ERROR_ACCOUNT_NOT_FOUND, } from '../http/error_schemas.js';
19
18
  /**
20
19
  * Create middleware that authenticates via bearer token.
21
20
  *
21
+ * Soft-fails for invalid, expired, or empty tokens — calls `next()` without
22
+ * setting a request context, letting downstream auth enforcement (per-action
23
+ * `check_action_auth` or `require_auth`) return a consistent JSON-RPC or
24
+ * route-level error. This avoids leaking token-specific diagnostics
25
+ * (`invalid_token`, `account_not_found`) that could aid enumeration attacks,
26
+ * and ensures public actions are not blocked by bad credentials.
27
+ *
22
28
  * Rejects bearer tokens when an `Origin` or `Referer` header is present —
23
29
  * browsers must use cookie auth to reduce attack surface.
24
30
  * Auth scheme matching is case-insensitive per RFC 7235.
@@ -26,6 +32,9 @@ import { ERROR_BEARER_REJECTED_BROWSER, ERROR_INVALID_TOKEN, ERROR_ACCOUNT_NOT_F
26
32
  * and sets it on the Hono context. Skips if a request context is already set
27
33
  * (e.g. by session middleware).
28
34
  *
35
+ * Rate limiting (429) is the only hard-fail — it's a throttling concern
36
+ * independent of auth identity.
37
+ *
29
38
  * @param deps - query dependencies (pool-level db for middleware)
30
39
  * @param ip_rate_limiter - per-IP rate limiter for bearer token attempts (null to disable)
31
40
  * @param log - the logger instance
@@ -46,19 +55,24 @@ export const create_bearer_auth_middleware = (deps, ip_rate_limiter, log) => {
46
55
  await next();
47
56
  return;
48
57
  }
49
- // Reject bearer tokens in browser context — defense-in-depth:
58
+ // Silently discard bearer tokens in browser context — defense-in-depth:
50
59
  // checks both Origin and Referer (not just Origin) because some browser
51
60
  // requests send only Referer. Uses `!== undefined` so that empty-string
52
61
  // headers (e.g. `Origin: ''`) are still treated as browser context.
62
+ // Discards rather than returning 403 so that the RPC dispatcher can still
63
+ // handle public actions or fall through to cookie auth.
53
64
  if (c.req.header('Origin') !== undefined || c.req.header('Referer') !== undefined) {
54
- return c.json({ error: ERROR_BEARER_REJECTED_BROWSER }, 403);
65
+ log.debug('bearer auth rejected: browser context (Origin/Referer present)');
66
+ await next();
67
+ return;
55
68
  }
56
69
  const raw_token = auth_header.slice(7);
57
- // Reject empty token body before any hashing or DB work.
70
+ // Empty token body soft-fail (treat as "no credential").
58
71
  // (The Fetch API trims 'Bearer ' to 'Bearer' which skips this middleware entirely,
59
72
  // but raw HTTP clients may send 'Bearer ' with an empty token.)
60
73
  if (!raw_token) {
61
- return c.json({ error: ERROR_INVALID_TOKEN }, 401);
74
+ await next();
75
+ return;
62
76
  }
63
77
  const ip = get_client_ip(c);
64
78
  // Per-IP rate limit: record before async DB work to close the TOCTOU
@@ -73,7 +87,11 @@ export const create_bearer_auth_middleware = (deps, ip_rate_limiter, log) => {
73
87
  }
74
88
  const api_token = await query_validate_api_token({ ...deps, log }, raw_token, ip, c.var.pending_effects);
75
89
  if (!api_token) {
76
- return c.json({ error: ERROR_INVALID_TOKEN }, 401);
90
+ // Invalid or expired token — soft-fail. Rate limit counter stays
91
+ // incremented (recorded above), correctly penalizing bad attempts.
92
+ log.debug('bearer auth soft-fail: token not found or expired');
93
+ await next();
94
+ return;
77
95
  }
78
96
  // Valid token — reset rate limit counter
79
97
  if (ip_rate_limiter)
@@ -81,7 +99,11 @@ export const create_bearer_auth_middleware = (deps, ip_rate_limiter, log) => {
81
99
  // Build request context from the token's account
82
100
  const ctx = await build_request_context(deps, api_token.account_id);
83
101
  if (!ctx) {
84
- return c.json({ error: ERROR_ACCOUNT_NOT_FOUND }, 401);
102
+ // Token exists but account/actor missing — soft-fail to avoid
103
+ // leaking account lifecycle information.
104
+ log.debug('bearer auth soft-fail: account or actor not found for token');
105
+ await next();
106
+ return;
85
107
  }
86
108
  c.set(REQUEST_CONTEXT_KEY, ctx);
87
109
  c.set(CREDENTIAL_TYPE_KEY, 'api_token');
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,WAAW,CAAC;AACvC,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,mBAAmB,CAAC;AACxD,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,oBAAoB,CAAC;AACpD,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,4BAA4B,CAAC;AAG/D;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACrC,eAAe,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC/B,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mFAAmF;IACnF,kBAAkB,CAAC,EAAE,gBAAgB,CAAC;IACtC,oFAAoF;IACpF,sBAAsB,EAAE,WAAW,GAAG,IAAI,CAAC;CAC3C;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,OAAO,EACb,SAAS,qBAAqB,KAC5B,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,CA+D/B,CAAC"}
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,WAAW,CAAC;AACvC,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,mBAAmB,CAAC;AACxD,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,oBAAoB,CAAC;AACpD,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,4BAA4B,CAAC;AAG/D;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACrC,eAAe,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC/B,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mFAAmF;IACnF,kBAAkB,CAAC,EAAE,gBAAgB,CAAC;IACtC,oFAAoF;IACpF,sBAAsB,EAAE,WAAW,GAAG,IAAI,CAAC;CAC3C;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,OAAO,EACb,SAAS,qBAAqB,KAC5B,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,CAmE/B,CAAC"}
@@ -47,7 +47,11 @@ export const create_auth_middleware_specs = async (deps, options) => {
47
47
  name: 'bearer_auth',
48
48
  path,
49
49
  handler: bearer_auth_middleware,
50
- errors: { 401: ApiError, 403: ApiError, 429: RateLimitError },
50
+ // Bearer middleware soft-fails for invalid/expired tokens (calls next()
51
+ // without setting context). Only 429 is a hard-fail from this layer.
52
+ // Auth enforcement (401/403) happens downstream via check_action_auth
53
+ // or require_auth, producing consistent JSON-RPC or route-level errors.
54
+ errors: { 429: RateLimitError },
51
55
  },
52
56
  ];
53
57
  if (daemon_token_state) {
@@ -1 +1 @@
1
- {"version":3,"file":"schema_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/schema_helpers.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAuB,KAAK,YAAY,EAAE,KAAK,iBAAiB,EAAC,MAAM,oBAAoB,CAAC;AAEnG;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,OAAsC,CAAC;AAE1F;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,OACe,CAAC;AAE5E;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,OAcrD,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,MAAM,EAAE,YAAY,MAAM,KAAG,OAQxE,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM;IACL,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC;IACpB,UAAU,CAAC,EAAE,YAAY,CAAC;IAC1B,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC3B,EACD,oBAAoB,iBAAiB,GAAG,IAAI,KAC1C,iBAAiB,GAAG,IAUtB,CAAC"}
1
+ {"version":3,"file":"schema_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/schema_helpers.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAuB,KAAK,YAAY,EAAE,KAAK,iBAAiB,EAAC,MAAM,oBAAoB,CAAC;AAEnG;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,OAAsC,CAAC;AAE1F;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,OACe,CAAC;AAE5E;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,OAQrD,CAAC;AAoBF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,MAAM,EAAE,YAAY,MAAM,KAAG,OAQxE,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM;IACL,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC;IACpB,UAAU,CAAC,EAAE,YAAY,CAAC;IAC1B,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC3B,EACD,oBAAoB,iBAAiB,GAAG,IAAI,KAC1C,iBAAiB,GAAG,IAUtB,CAAC"}
@@ -34,18 +34,32 @@ export const schema_to_surface = (schema) => {
34
34
  return null;
35
35
  try {
36
36
  const json_schema = z.toJSONSchema(schema);
37
- // Strip $schema for cleaner snapshots
38
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
39
- if (typeof json_schema === 'object' && json_schema !== null && '$schema' in json_schema) {
40
- const { $schema: _, ...rest } = json_schema;
41
- return rest;
42
- }
43
- return json_schema;
37
+ return strip_json_schema_noise(json_schema);
44
38
  }
45
39
  catch {
46
40
  return null;
47
41
  }
48
42
  };
43
+ /**
44
+ * Recursively strip `$schema` and `default` from a JSON Schema value.
45
+ *
46
+ * `$schema` is noise for snapshots. `default` can be non-deterministic
47
+ * when schemas use function defaults (e.g. `z.string().default(() => new Date().toISOString())`),
48
+ * and defaults are runtime behavior, not attack surface structure.
49
+ */
50
+ const strip_json_schema_noise = (value) => {
51
+ if (typeof value !== 'object' || value === null)
52
+ return value;
53
+ if (Array.isArray(value))
54
+ return value.map(strip_json_schema_noise);
55
+ const result = {};
56
+ for (const [k, v] of Object.entries(value)) {
57
+ if (k === '$schema' || k === 'default')
58
+ continue;
59
+ result[k] = strip_json_schema_noise(v);
60
+ }
61
+ return result;
62
+ };
49
63
  /**
50
64
  * Check if a middleware path pattern applies to a route path.
51
65
  *
@@ -1 +1 @@
1
- {"version":3,"file":"adversarial_headers.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/adversarial_headers.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAY7B,OAAO,KAAK,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAS3B,OAAO,EAGN,KAAK,0BAA0B,EAC/B,MAAM,iBAAiB,CAAC;AAIzB,+DAA+D;AAC/D,MAAM,WAAW,qBAAqB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,+GAA+G;IAC/G,qBAAqB,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC;IAClC,qGAAqG;IACrG,oBAAoB,EAAE,QAAQ,GAAG,YAAY,CAAC;CAC9C;AAID;;;;;GAKG;AACH,eAAO,MAAM,iCAAiC,GAC7C,gBAAgB,MAAM,KACpB,KAAK,CAAC,qBAAqB,CAkE7B,CAAC;AAIF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qCAAqC,GACjD,YAAY,MAAM,EAClB,SAAS,0BAA0B,EACnC,gBAAgB,MAAM,EACtB,cAAc,KAAK,CAAC,qBAAqB,CAAC,KACxC,IAkCF,CAAC"}
1
+ {"version":3,"file":"adversarial_headers.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/adversarial_headers.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAY7B,OAAO,KAAK,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAG3B,OAAO,EAGN,KAAK,0BAA0B,EAC/B,MAAM,iBAAiB,CAAC;AAIzB,+DAA+D;AAC/D,MAAM,WAAW,qBAAqB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,+GAA+G;IAC/G,qBAAqB,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC;IAClC,qGAAqG;IACrG,oBAAoB,EAAE,QAAQ,GAAG,YAAY,CAAC;CAC9C;AAID;;;;;GAKG;AACH,eAAO,MAAM,iCAAiC,GAC7C,gBAAgB,MAAM,KACpB,KAAK,CAAC,qBAAqB,CA+D7B,CAAC;AAIF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qCAAqC,GACjD,YAAY,MAAM,EAClB,SAAS,0BAA0B,EACnC,gBAAgB,MAAM,EACtB,cAAc,KAAK,CAAC,qBAAqB,CAAC,KACxC,IAkCF,CAAC"}
@@ -8,7 +8,7 @@ import './assert_dev_env.js';
8
8
  * @module
9
9
  */
10
10
  import { test, assert, describe } from 'vitest';
11
- import { ApiError, ERROR_FORBIDDEN_ORIGIN, ERROR_FORBIDDEN_REFERER, ERROR_BEARER_REJECTED_BROWSER, ERROR_INVALID_TOKEN, } from '../http/error_schemas.js';
11
+ import { ApiError, ERROR_FORBIDDEN_ORIGIN, ERROR_FORBIDDEN_REFERER } from '../http/error_schemas.js';
12
12
  import { create_test_middleware_stack_app, TEST_MIDDLEWARE_PATH, } from './middleware.js';
13
13
  // --- Standard adversarial header cases ---
14
14
  /**
@@ -29,13 +29,12 @@ export const create_standard_adversarial_cases = (allowed_origin) => [
29
29
  validate_expectation: 'not_called',
30
30
  },
31
31
  {
32
- name: 'bearer token with allowed Origin is rejected as browser context',
32
+ name: 'bearer token with allowed Origin bearer silently discarded (browser context)',
33
33
  headers: {
34
34
  Authorization: 'Bearer secret_fuz_token_test',
35
35
  Origin: allowed_origin,
36
36
  },
37
- expected_status: 403,
38
- expected_error: ERROR_BEARER_REJECTED_BROWSER,
37
+ expected_status: 200,
39
38
  validate_expectation: 'not_called',
40
39
  },
41
40
  {
@@ -55,12 +54,11 @@ export const create_standard_adversarial_cases = (allowed_origin) => [
55
54
  validate_expectation: 'not_called',
56
55
  },
57
56
  {
58
- name: 'lowercase bearer scheme is recognized (case-insensitive per RFC 7235)',
57
+ name: 'lowercase bearer scheme is recognized (case-insensitive per RFC 7235), soft-fails',
59
58
  headers: {
60
59
  Authorization: 'bearer secret_fuz_token_test',
61
60
  },
62
- expected_status: 401,
63
- expected_error: ERROR_INVALID_TOKEN,
61
+ expected_status: 200,
64
62
  validate_expectation: 'called',
65
63
  },
66
64
  {
@@ -74,13 +72,12 @@ export const create_standard_adversarial_cases = (allowed_origin) => [
74
72
  validate_expectation: 'not_called',
75
73
  },
76
74
  {
77
- name: 'bearer token with Referer from allowed origin is rejected as browser context (defense-in-depth)',
75
+ name: 'bearer token with Referer from allowed origin bearer silently discarded (browser context)',
78
76
  headers: {
79
77
  Authorization: 'Bearer secret_fuz_token_test',
80
78
  Referer: `${allowed_origin}/page`,
81
79
  },
82
- expected_status: 403,
83
- expected_error: ERROR_BEARER_REJECTED_BROWSER,
80
+ expected_status: 200,
84
81
  validate_expectation: 'not_called',
85
82
  },
86
83
  ];
@@ -144,6 +144,8 @@ export interface TestApp {
144
144
  create_session_headers: (extra?: Record<string, string>) => Record<string, string>;
145
145
  /** Build request headers with the bootstrapped Bearer token. */
146
146
  create_bearer_headers: (extra?: Record<string, string>) => Record<string, string>;
147
+ /** Build request headers with the daemon token (keeper auth). */
148
+ create_daemon_token_headers: (extra?: Record<string, string>) => Record<string, string>;
147
149
  /** Create an additional account with credentials. */
148
150
  create_account: (options?: {
149
151
  username?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"app_server.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/app_server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"app_server.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/app_server.ts"],"names":[],"mappings":"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"}
@@ -12,6 +12,7 @@ import { create_session_cookie_value } from '../auth/session_cookie.js';
12
12
  import { run_migrations } from '../db/migrate.js';
13
13
  import { AUTH_MIGRATION_NS } from '../auth/migrations.js';
14
14
  import { create_app_server, } from '../server/app_server.js';
15
+ import { generate_daemon_token, DAEMON_TOKEN_HEADER, } from '../auth/daemon_token.js';
15
16
  import { create_pglite_factory } from './db.js';
16
17
  /* eslint-disable @typescript-eslint/require-await */
17
18
  /**
@@ -172,6 +173,15 @@ export const create_test_app_server = async (options) => {
172
173
  */
173
174
  export const create_test_app = async (options) => {
174
175
  const test_server = await create_test_app_server(options);
176
+ // Daemon token state for keeper auth in tests.
177
+ // Uses a static token (no rotation) — sufficient for request-level testing.
178
+ const test_daemon_token = generate_daemon_token();
179
+ const daemon_token_state = {
180
+ current_token: test_daemon_token,
181
+ previous_token: null,
182
+ rotated_at: new Date(),
183
+ keeper_account_id: test_server.account.id,
184
+ };
175
185
  const result = await create_app_server({
176
186
  backend: test_server,
177
187
  session_options: options.session_options,
@@ -183,6 +193,7 @@ export const create_test_app = async (options) => {
183
193
  signup_account_rate_limiter: null,
184
194
  bearer_ip_rate_limiter: null,
185
195
  await_pending_effects: true,
196
+ daemon_token_state,
186
197
  ...options.app_options,
187
198
  create_route_specs: options.create_route_specs,
188
199
  });
@@ -200,6 +211,11 @@ export const create_test_app = async (options) => {
200
211
  authorization: `Bearer ${test_server.api_token}`,
201
212
  ...extra,
202
213
  });
214
+ const create_daemon_token_headers = (extra) => ({
215
+ host: 'localhost',
216
+ [DAEMON_TOKEN_HEADER]: test_daemon_token,
217
+ ...extra,
218
+ });
203
219
  let account_counter = 0;
204
220
  const create_account = async (account_options) => {
205
221
  account_counter++;
@@ -235,6 +251,7 @@ export const create_test_app = async (options) => {
235
251
  route_specs: surface_spec.route_specs,
236
252
  create_session_headers,
237
253
  create_bearer_headers,
254
+ create_daemon_token_headers,
238
255
  create_account,
239
256
  cleanup: () => test_server.cleanup(),
240
257
  };
@@ -135,9 +135,8 @@ const describe_data_exposure_runtime_tests = (options) => {
135
135
  };
136
136
  const factories = options.db_factories ?? [create_pglite_factory(init_schema)];
137
137
  for (const factory of factories) {
138
- describe(`data exposure runtime (${factory.name})`, () => {
139
- if (factory.skip)
140
- return;
138
+ const describe_fn = factory.skip ? describe.skip : describe;
139
+ describe_fn(`data exposure — runtime (${factory.name})`, () => {
141
140
  let test_app;
142
141
  let authed_account;
143
142
  let admin_account;
@@ -292,6 +291,6 @@ const pick_auth_headers = (spec, test_app, authed_account, admin_account) => {
292
291
  // keeper role uses the bootstrapped account
293
292
  return test_app.create_session_headers();
294
293
  case 'keeper':
295
- return test_app.create_bearer_headers();
294
+ return test_app.create_daemon_token_headers();
296
295
  }
297
296
  };
@@ -24,7 +24,7 @@ export interface StandardIntegrationTestOptions {
24
24
  *
25
25
  * Exercises login/logout, cookie attributes, session security, session
26
26
  * revocation, password change (incl. API token revocation), origin
27
- * verification, bearer auth (incl. browser context rejection on mutations),
27
+ * verification, bearer auth (incl. browser context discard on mutations),
28
28
  * token revocation, cross-account isolation, expired credential rejection,
29
29
  * signup invite edge cases, and response body validation.
30
30
  *
@@ -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,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAC9D,OAAO,KAAK,EAAC,gBAAgB,EAAE,gBAAgB,EAAC,MAAM,yBAAyB,CAAC;AAChF,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAGrD,OAAO,EAIN,KAAK,SAAS,EACd,MAAM,SAAS,CAAC;AAgBjB;;GAEG;AACH,MAAM,WAAW,8BAA8B;IAC9C,4CAA4C;IAC5C,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,wDAAwD;IACxD,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE,iDAAiD;IACjD,WAAW,CAAC,EAAE,OAAO,CACpB,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,iBAAiB,GAAG,oBAAoB,CAAC,CAC5E,CAAC;IACF;;;OAGG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;CAChC;AAeD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,mCAAmC,GAC/C,SAAS,8BAA8B,KACrC,IAk+CF,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,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAC9D,OAAO,KAAK,EAAC,gBAAgB,EAAE,gBAAgB,EAAC,MAAM,yBAAyB,CAAC;AAChF,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAGrD,OAAO,EAIN,KAAK,SAAS,EACd,MAAM,SAAS,CAAC;AAgBjB;;GAEG;AACH,MAAM,WAAW,8BAA8B;IAC9C,4CAA4C;IAC5C,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,wDAAwD;IACxD,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE,iDAAiD;IACjD,WAAW,CAAC,EAAE,OAAO,CACpB,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,iBAAiB,GAAG,oBAAoB,CAAC,CAC5E,CAAC;IACF;;;OAGG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;CAChC;AAeD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,mCAAmC,GAC/C,SAAS,8BAA8B,KACrC,IAo+CF,CAAC"}
@@ -38,7 +38,7 @@ const build_test_app_options = (options, db) => ({
38
38
  *
39
39
  * Exercises login/logout, cookie attributes, session security, session
40
40
  * revocation, password change (incl. API token revocation), origin
41
- * verification, bearer auth (incl. browser context rejection on mutations),
41
+ * verification, bearer auth (incl. browser context discard on mutations),
42
42
  * token revocation, cross-account isolation, expired credential rejection,
43
43
  * signup invite edge cases, and response body validation.
44
44
  *
@@ -525,17 +525,15 @@ export const describe_standard_integration_tests = (options) => {
525
525
  headers: bearer_headers,
526
526
  });
527
527
  assert.strictEqual(ok_res.status, 200);
528
- // With Origin — rejected (browser context)
528
+ // With Origin — bearer silently discarded (browser context), falls through to no auth
529
529
  const res = await test_app.app.request(verify_route.path, {
530
530
  headers: {
531
531
  ...bearer_headers,
532
532
  origin: 'http://localhost:5173',
533
533
  },
534
534
  });
535
- assert.strictEqual(res.status, 403);
536
- error_collector.record(test_app.route_specs, 'GET', verify_route.path, 403);
537
- const body = await res.json();
538
- assert.strictEqual(body.error, 'bearer_token_rejected_in_browser_context');
535
+ assert.strictEqual(res.status, 401);
536
+ error_collector.record(test_app.route_specs, 'GET', verify_route.path, 401);
539
537
  });
540
538
  });
541
539
  // --- 7. Token revocation ---
@@ -912,8 +910,8 @@ export const describe_standard_integration_tests = (options) => {
912
910
  });
913
911
  });
914
912
  // --- 12. Bearer token browser context on mutation routes ---
915
- describe('bearer token browser context rejection on mutations', () => {
916
- test('bearer token with Origin header rejected on POST logout', async () => {
913
+ describe('bearer token browser context silently discarded on mutations', () => {
914
+ test('bearer token with Origin header discarded on POST logout', async () => {
917
915
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
918
916
  const logout_route = find_auth_route(test_app.route_specs, '/logout', 'POST');
919
917
  assert.ok(logout_route, 'Expected POST /logout route — ensure create_route_specs includes account routes');
@@ -924,12 +922,10 @@ export const describe_standard_integration_tests = (options) => {
924
922
  method: 'POST',
925
923
  headers: { ...bearer_headers, origin: 'http://localhost:5173' },
926
924
  });
927
- assert.strictEqual(res.status, 403, 'Bearer with Origin should be rejected on mutation');
928
- const body = await res.json();
929
- assert.strictEqual(body.error, 'bearer_token_rejected_in_browser_context');
930
- error_collector.record(test_app.route_specs, 'POST', logout_route.path, 403);
925
+ assert.strictEqual(res.status, 401, 'Bearer with Origin should be discarded unauthenticated');
926
+ error_collector.record(test_app.route_specs, 'POST', logout_route.path, 401);
931
927
  });
932
- test('bearer token with Referer header rejected on POST password', async () => {
928
+ test('bearer token with Referer header discarded on POST password', async () => {
933
929
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
934
930
  const password_route = find_auth_route(test_app.route_specs, '/password', 'POST');
935
931
  assert.ok(password_route, 'Expected POST /password route — ensure create_route_specs includes account routes');
@@ -940,10 +936,8 @@ export const describe_standard_integration_tests = (options) => {
940
936
  method: 'POST',
941
937
  headers: { ...bearer_headers, referer: 'http://localhost:5173/admin' },
942
938
  });
943
- assert.strictEqual(res.status, 403, 'Bearer with Referer should be rejected on mutation');
944
- const body = await res.json();
945
- assert.strictEqual(body.error, 'bearer_token_rejected_in_browser_context');
946
- error_collector.record(test_app.route_specs, 'POST', password_route.path, 403);
939
+ assert.strictEqual(res.status, 401, 'Bearer with Referer should be discarded unauthenticated');
940
+ error_collector.record(test_app.route_specs, 'POST', password_route.path, 401);
947
941
  });
948
942
  });
949
943
  // --- 13. Password change revokes API tokens ---
@@ -1 +1 @@
1
- {"version":3,"file":"round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAc7B,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AACrD,OAAO,KAAK,EAAC,gBAAgB,EAAE,gBAAgB,EAAC,MAAM,yBAAyB,CAAC;AAChF,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAG9D,OAAO,EAAwB,KAAK,SAAS,EAAC,MAAM,SAAS,CAAC;AAO9D,oDAAoD;AACpD,MAAM,WAAW,oBAAoB;IACpC,4CAA4C;IAC5C,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,wDAAwD;IACxD,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE,iDAAiD;IACjD,WAAW,CAAC,EAAE,OAAO,CACpB,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,iBAAiB,GAAG,oBAAoB,CAAC,CAC5E,CAAC;IACF,qEAAqE;IACrE,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAChC,kDAAkD;IAClD,WAAW,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC5B,+EAA+E;IAC/E,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACvD;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,8BAA8B,GAAI,SAAS,oBAAoB,KAAG,IAsF9E,CAAC"}
1
+ {"version":3,"file":"round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAc7B,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AACrD,OAAO,KAAK,EAAC,gBAAgB,EAAE,gBAAgB,EAAC,MAAM,yBAAyB,CAAC;AAChF,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAG9D,OAAO,EAAwB,KAAK,SAAS,EAAC,MAAM,SAAS,CAAC;AAO9D,oDAAoD;AACpD,MAAM,WAAW,oBAAoB;IACpC,4CAA4C;IAC5C,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,wDAAwD;IACxD,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE,iDAAiD;IACjD,WAAW,CAAC,EAAE,OAAO,CACpB,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,iBAAiB,GAAG,oBAAoB,CAAC,CAC5E,CAAC;IACF,qEAAqE;IACrE,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAChC,kDAAkD;IAClD,WAAW,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC5B,+EAA+E;IAC/E,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACvD;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,8BAA8B,GAAI,SAAS,oBAAoB,KAAG,IAqF9E,CAAC"}
@@ -38,9 +38,8 @@ export const describe_round_trip_validation = (options) => {
38
38
  };
39
39
  const factories = options.db_factories ?? [create_pglite_factory(init_schema)];
40
40
  for (const factory of factories) {
41
- describe(`round-trip validation (${factory.name})`, () => {
42
- if (factory.skip)
43
- return;
41
+ const describe_fn = factory.skip ? describe.skip : describe;
42
+ describe_fn(`round-trip validation (${factory.name})`, () => {
44
43
  let test_app;
45
44
  let authed_account;
46
45
  let admin_account;
@@ -123,6 +122,6 @@ const pick_auth_headers = (spec, test_app, authed_account, admin_account) => {
123
122
  // Keeper role uses the bootstrapped account (which has keeper role)
124
123
  return test_app.create_session_headers();
125
124
  case 'keeper':
126
- return test_app.create_bearer_headers();
125
+ return test_app.create_daemon_token_headers();
127
126
  }
128
127
  };
@@ -1 +1 @@
1
- {"version":3,"file":"rpc_attack_surface.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/rpc_attack_surface.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAkB7B,OAAO,KAAK,EAA6C,cAAc,EAAC,MAAM,oBAAoB,CAAC;AAenG,uDAAuD;AACvD,MAAM,WAAW,uBAAuB;IACvC,+FAA+F;IAC/F,KAAK,EAAE,MAAM,cAAc,CAAC;IAC5B,yDAAyD;IACzD,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACrB;AAkaD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iCAAiC,GAAI,SAAS,uBAAuB,KAAG,IAOpF,CAAC"}
1
+ {"version":3,"file":"rpc_attack_surface.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/rpc_attack_surface.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAkB7B,OAAO,KAAK,EAA6C,cAAc,EAAC,MAAM,oBAAoB,CAAC;AAoBnG,uDAAuD;AACvD,MAAM,WAAW,uBAAuB;IACvC,+FAA+F;IAC/F,KAAK,EAAE,MAAM,cAAc,CAAC;IAC5B,yDAAyD;IACzD,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACrB;AAodD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iCAAiC,GAAI,SAAS,uBAAuB,KAAG,IAOpF,CAAC"}
@@ -14,7 +14,7 @@ import './assert_dev_env.js';
14
14
  */
15
15
  import { test, assert, describe } from 'vitest';
16
16
  import { JSONRPC_ERROR_CODES } from '../http/jsonrpc_errors.js';
17
- import { create_auth_test_apps, select_auth_app } from './auth_apps.js';
17
+ import { create_auth_test_apps, create_test_app_from_specs, create_test_request_context, select_auth_app, } from './auth_apps.js';
18
18
  import { generate_input_test_cases } from './adversarial_input.js';
19
19
  import { ERROR_INVALID_JSON_BODY } from '../http/error_schemas.js';
20
20
  import { create_rpc_post_init, create_rpc_get_url, assert_jsonrpc_error_response, } from './rpc_helpers.js';
@@ -23,6 +23,8 @@ import { create_rpc_post_init, create_rpc_get_url, assert_jsonrpc_error_response
23
23
  const filter_protected_rpc_methods = (endpoint) => endpoint.methods.filter((m) => m.auth.type !== 'none');
24
24
  /** Filter RPC methods that require a specific role. */
25
25
  const filter_role_rpc_methods = (endpoint) => endpoint.methods.filter((m) => m.auth.type === 'role');
26
+ /** Filter RPC methods that require keeper auth (daemon_token + keeper role). */
27
+ const filter_keeper_rpc_methods = (endpoint) => endpoint.methods.filter((m) => m.auth.type === 'keeper');
26
28
  /** Find the `RpcAction` source spec for a surface method. */
27
29
  const find_rpc_action = (rpc_endpoint_specs, endpoint_path, method_name) => {
28
30
  const ep = rpc_endpoint_specs.find((e) => e.path === endpoint_path);
@@ -40,7 +42,7 @@ const find_rpc_action = (rpc_endpoint_specs, endpoint_path, method_name) => {
40
42
  * - unauthenticated → error code -32001 — every protected method
41
43
  * - wrong role → error code -32002 — every role method with non-matching roles
42
44
  * - authenticated without role → -32002 — every role method, no-role context
43
- * - keeper routes reject session credential → -32002
45
+ * - keeper rejects non-daemon credentials → -32002 — session and api_token rejected
44
46
  * - correct auth passes — every protected method, assert not 401/403
45
47
  */
46
48
  const describe_rpc_auth = (options) => {
@@ -94,9 +96,27 @@ const describe_rpc_auth = (options) => {
94
96
  }
95
97
  });
96
98
  }
97
- // NOTE: no "keeper rejects session credential" test for RPC — the RPC
98
- // dispatcher's check_action_auth only checks role, not credential type.
99
- // Credential type enforcement is a REST middleware concern (require_keeper).
99
+ const keeper_methods = filter_keeper_rpc_methods(endpoint);
100
+ if (keeper_methods.length > 0) {
101
+ describe('keeper rejects non-daemon credentials', () => {
102
+ const session_app = create_test_app_from_specs(route_specs, create_test_request_context('keeper'), 'session');
103
+ const api_token_app = create_test_app_from_specs(route_specs, create_test_request_context('keeper'), 'api_token');
104
+ for (const method of keeper_methods) {
105
+ test(`${method.name} rejects session credential`, async () => {
106
+ const res = await session_app.request(endpoint.path, create_rpc_post_init(method.name));
107
+ assert.strictEqual(res.status, 403, `${method.name} should reject session credential`);
108
+ const body = await res.json();
109
+ assert_jsonrpc_error_response(body, JSONRPC_ERROR_CODES.forbidden);
110
+ });
111
+ test(`${method.name} rejects api_token credential`, async () => {
112
+ const res = await api_token_app.request(endpoint.path, create_rpc_post_init(method.name));
113
+ assert.strictEqual(res.status, 403, `${method.name} should reject api_token credential`);
114
+ const body = await res.json();
115
+ assert_jsonrpc_error_response(body, JSONRPC_ERROR_CODES.forbidden);
116
+ });
117
+ }
118
+ });
119
+ }
100
120
  describe('correct auth passes', () => {
101
121
  for (const method of protected_methods) {
102
122
  test(method.name, async () => {
@@ -1 +1 @@
1
- {"version":3,"file":"rpc_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/rpc_round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAe7B,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AACrD,OAAO,KAAK,EAAC,gBAAgB,EAAE,gBAAgB,EAAC,MAAM,yBAAyB,CAAC;AAChF,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAG9D,OAAO,EAAwB,KAAK,SAAS,EAAC,MAAM,SAAS,CAAC;AAK9D,OAAO,KAAK,EAAC,eAAe,EAAsB,MAAM,oBAAoB,CAAC;AAQ7E,mDAAmD;AACnD,MAAM,WAAW,uBAAuB;IACvC,4CAA4C;IAC5C,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,wDAAwD;IACxD,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE,gFAAgF;IAChF,aAAa,EAAE,KAAK,CAAC,eAAe,CAAC,CAAC;IACtC,iDAAiD;IACjD,WAAW,CAAC,EAAE,OAAO,CACpB,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,iBAAiB,GAAG,oBAAoB,CAAC,CAC5E,CAAC;IACF,qEAAqE;IACrE,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAChC,oDAAoD;IACpD,YAAY,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC7B,6EAA6E;IAC7E,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACvD;AA2BD;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,6BAA6B,GAAI,SAAS,uBAAuB,KAAG,IAsIhF,CAAC"}
1
+ {"version":3,"file":"rpc_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/rpc_round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAe7B,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AACrD,OAAO,KAAK,EAAC,gBAAgB,EAAE,gBAAgB,EAAC,MAAM,yBAAyB,CAAC;AAChF,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAG9D,OAAO,EAAwB,KAAK,SAAS,EAAC,MAAM,SAAS,CAAC;AAK9D,OAAO,KAAK,EAAC,eAAe,EAAsB,MAAM,oBAAoB,CAAC;AAQ7E,mDAAmD;AACnD,MAAM,WAAW,uBAAuB;IACvC,4CAA4C;IAC5C,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,wDAAwD;IACxD,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE,gFAAgF;IAChF,aAAa,EAAE,KAAK,CAAC,eAAe,CAAC,CAAC;IACtC,iDAAiD;IACjD,WAAW,CAAC,EAAE,OAAO,CACpB,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,iBAAiB,GAAG,oBAAoB,CAAC,CAC5E,CAAC;IACF,qEAAqE;IACrE,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAChC,oDAAoD;IACpD,YAAY,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC7B,6EAA6E;IAC7E,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACvD;AA2BD;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,6BAA6B,GAAI,SAAS,uBAAuB,KAAG,IAqIhF,CAAC"}
@@ -33,7 +33,7 @@ const pick_rpc_auth_headers = (method, test_app, authed_account, admin_account)
33
33
  // keeper role uses the bootstrapped account
34
34
  return test_app.create_session_headers();
35
35
  case 'keeper':
36
- return test_app.create_bearer_headers();
36
+ return test_app.create_daemon_token_headers();
37
37
  }
38
38
  };
39
39
  /**
@@ -59,9 +59,8 @@ export const describe_rpc_round_trip_tests = (options) => {
59
59
  };
60
60
  const factories = options.db_factories ?? [create_pglite_factory(init_schema)];
61
61
  for (const factory of factories) {
62
- describe(`RPC round-trip validation (${factory.name})`, () => {
63
- if (factory.skip)
64
- return;
62
+ const describe_fn = factory.skip ? describe.skip : describe;
63
+ describe_fn(`RPC round-trip validation (${factory.name})`, () => {
65
64
  let test_app;
66
65
  let authed_account;
67
66
  let admin_account;
@@ -1 +1 @@
1
- {"version":3,"file":"schema_generators.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/schema_generators.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;GAQG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AACtB,OAAO,EAKN,KAAK,YAAY,EACjB,MAAM,yBAAyB,CAAC;AAIjC;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAI,cAAc,CAAC,CAAC,OAAO,KAAG,MAAM,GAAG,IAShE,CAAC;AAiBF,qEAAqE;AACrE,eAAO,MAAM,oBAAoB,GAAI,OAAO,YAAY,EAAE,cAAc,CAAC,CAAC,OAAO,KAAG,OAqCnF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,EAAE,gBAAgB,CAAC,CAAC,SAAS,KAAG,MAa9E,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,GAC/B,cAAc,CAAC,CAAC,OAAO,KACrB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAkB5B,CAAC"}
1
+ {"version":3,"file":"schema_generators.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/schema_generators.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;GAQG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AACtB,OAAO,EAKN,KAAK,YAAY,EACjB,MAAM,yBAAyB,CAAC;AAIjC;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAI,cAAc,CAAC,CAAC,OAAO,KAAG,MAAM,GAAG,IAShE,CAAC;AA+BF,qEAAqE;AACrE,eAAO,MAAM,oBAAoB,GAAI,OAAO,YAAY,EAAE,cAAc,CAAC,CAAC,OAAO,KAAG,OAkDnF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,EAAE,gBAAgB,CAAC,CAAC,SAAS,KAAG,MAa9E,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,GAC/B,cAAc,CAAC,CAAC,OAAO,KACrB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAkB5B,CAAC"}
@@ -43,7 +43,20 @@ const generate_valid_string = (field_schema) => {
43
43
  // no constraints
44
44
  }
45
45
  const target = Math.max(min_length, Math.min(10, max_length));
46
- return 'x'.repeat(target) || 'test_value';
46
+ const base = 'x'.repeat(target) || 'test_value';
47
+ // Validate against the full schema (including refinements/brands).
48
+ // If the base string fails, try common patterns before giving up.
49
+ if (field_schema.safeParse(base).success)
50
+ return base;
51
+ // Absolute path refinement (e.g. DiskfilePath)
52
+ const with_slash = '/' + base;
53
+ if (field_schema.safeParse(with_slash).success)
54
+ return with_slash;
55
+ // URL refinement
56
+ const as_url = 'https://example.com/' + base;
57
+ if (field_schema.safeParse(as_url).success)
58
+ return as_url;
59
+ return base; // fall through — generate_valid_body will report the failure
47
60
  };
48
61
  /** Generate a valid-ish value for a field based on its base type. */
49
62
  export const generate_valid_value = (field, field_schema) => {
@@ -58,6 +71,8 @@ export const generate_valid_value = (field, field_schema) => {
58
71
  return '00000000-0000-0000-0000-000000000000';
59
72
  if (format === 'email')
60
73
  return 'test@example.com';
74
+ if (format === 'date-time')
75
+ return '2020-01-01T00:00:00.000Z';
61
76
  return generate_valid_string(field_schema);
62
77
  case 'number':
63
78
  case 'int':
@@ -66,8 +81,21 @@ export const generate_valid_value = (field, field_schema) => {
66
81
  return true;
67
82
  case 'array':
68
83
  return [];
69
- case 'object':
84
+ case 'object': {
85
+ // Recursively generate valid nested objects
86
+ const nested_schema = zod_unwrap_to_object(field_schema);
87
+ if (nested_schema) {
88
+ const nested_fields = zod_extract_fields(nested_schema);
89
+ const nested = {};
90
+ for (const nf of nested_fields) {
91
+ if (!nf.required && !nf.has_default)
92
+ continue;
93
+ nested[nf.name] = generate_valid_value(nf, nested_schema.shape[nf.name]);
94
+ }
95
+ return nested;
96
+ }
70
97
  return {};
98
+ }
71
99
  case 'null':
72
100
  return null;
73
101
  case 'enum': {
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import {goto} from '$app/navigation';
3
+ import {resolve} from '$app/paths';
2
4
  import PendingButton from '@fuzdev/fuz_ui/PendingButton.svelte';
3
5
  import {autofocus} from '@fuzdev/fuz_ui/autofocus.svelte.js';
4
6
 
@@ -7,6 +9,12 @@
7
9
  import {auth_state_context} from './auth_state.svelte.js';
8
10
  import {FormState} from './form_state.svelte.js';
9
11
 
12
+ const {
13
+ redirect_on_bootstrap = resolve('/'),
14
+ }: {
15
+ redirect_on_bootstrap?: string;
16
+ } = $props();
17
+
10
18
  const auth_state = auth_state_context.get();
11
19
  const form_state = new FormState();
12
20
 
@@ -35,7 +43,11 @@
35
43
  else if (!passwords_match) form_state.focus('password_confirm');
36
44
  return;
37
45
  }
38
- await auth_state.bootstrap(token.trim(), username.trim(), password);
46
+ const success = await auth_state.bootstrap(token.trim(), username.trim(), password);
47
+ if (success) {
48
+ form_state.reset();
49
+ await goto(redirect_on_bootstrap);
50
+ }
39
51
  };
40
52
  </script>
41
53
 
@@ -1,4 +1,7 @@
1
- declare const BootstrapForm: import("svelte").Component<Record<string, never>, {}, "">;
1
+ type $$ComponentProps = {
2
+ redirect_on_bootstrap?: string;
3
+ };
4
+ declare const BootstrapForm: import("svelte").Component<$$ComponentProps, {}, "">;
2
5
  type BootstrapForm = ReturnType<typeof BootstrapForm>;
3
6
  export default BootstrapForm;
4
7
  //# sourceMappingURL=BootstrapForm.svelte.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"BootstrapForm.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/BootstrapForm.svelte"],"names":[],"mappings":"AAqGA,QAAA,MAAM,aAAa,2DAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"BootstrapForm.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/BootstrapForm.svelte"],"names":[],"mappings":"AAaC,KAAK,gBAAgB,GAAI;IACxB,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAC/B,CAAC;AAqGH,QAAA,MAAM,aAAa,sDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
@@ -9,7 +9,7 @@
9
9
 
10
10
  const {
11
11
  username_label = 'username or email',
12
- redirect_on_login = resolve('/account' as any),
12
+ redirect_on_login = resolve('/'),
13
13
  }: {
14
14
  username_label?: string;
15
15
  redirect_on_login?: string;
@@ -10,7 +10,7 @@
10
10
  import {FormState} from './form_state.svelte.js';
11
11
 
12
12
  const {
13
- redirect_on_signup = resolve('/account' as any),
13
+ redirect_on_signup = resolve('/'),
14
14
  }: {
15
15
  redirect_on_signup?: string;
16
16
  } = $props();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_app",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "fullstack app library",
5
5
  "glyph": "🗝",
6
6
  "logo": "logo.svg",