@fuzdev/fuz_app 0.134.0 → 0.136.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 (118) hide show
  1. package/dist/actions/CLAUDE.md +93 -11
  2. package/dist/actions/register_action_ws.d.ts +23 -3
  3. package/dist/actions/register_action_ws.d.ts.map +1 -1
  4. package/dist/actions/register_action_ws.js +57 -37
  5. package/dist/actions/transports.d.ts +23 -0
  6. package/dist/actions/transports.d.ts.map +1 -1
  7. package/dist/actions/transports.js +23 -0
  8. package/dist/actions/transports_ws_backend.d.ts +133 -10
  9. package/dist/actions/transports_ws_backend.d.ts.map +1 -1
  10. package/dist/actions/transports_ws_backend.js +237 -34
  11. package/dist/actions/ws_endpoint_spec.d.ts +9 -0
  12. package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
  13. package/dist/actions/ws_outbound_writer.d.ts +206 -0
  14. package/dist/actions/ws_outbound_writer.d.ts.map +1 -0
  15. package/dist/actions/ws_outbound_writer.js +431 -0
  16. package/dist/auth/CLAUDE.md +10 -4
  17. package/dist/auth/account_action_specs.d.ts +7 -0
  18. package/dist/auth/account_action_specs.d.ts.map +1 -1
  19. package/dist/auth/account_action_specs.js +11 -1
  20. package/dist/auth/audit_emitter.d.ts +4 -0
  21. package/dist/auth/audit_emitter.d.ts.map +1 -1
  22. package/dist/auth/audit_emitter.js +18 -2
  23. package/dist/auth/audit_log_routes.d.ts.map +1 -1
  24. package/dist/auth/audit_log_routes.js +3 -1
  25. package/dist/auth/cell_action_specs.d.ts +10 -0
  26. package/dist/auth/cell_action_specs.d.ts.map +1 -1
  27. package/dist/auth/cell_action_specs.js +17 -3
  28. package/dist/auth/cell_actions.js +1 -1
  29. package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
  30. package/dist/auth/role_grant_offer_action_specs.js +7 -2
  31. package/dist/auth/role_schema.d.ts +14 -3
  32. package/dist/auth/role_schema.d.ts.map +1 -1
  33. package/dist/auth/role_schema.js +14 -2
  34. package/dist/auth/scope_kind_schema.d.ts +10 -0
  35. package/dist/auth/scope_kind_schema.d.ts.map +1 -1
  36. package/dist/auth/scope_kind_schema.js +11 -0
  37. package/dist/db/migrate.d.ts +1 -1
  38. package/dist/db/migrate.js +2 -2
  39. package/dist/env/load.d.ts +1 -1
  40. package/dist/env/load.js +1 -1
  41. package/dist/http/CLAUDE.md +2 -1
  42. package/dist/http/body_limit.d.ts +74 -0
  43. package/dist/http/body_limit.d.ts.map +1 -0
  44. package/dist/http/body_limit.js +100 -0
  45. package/dist/http/db_routes.d.ts.map +1 -1
  46. package/dist/http/db_routes.js +5 -3
  47. package/dist/http/error_schemas.d.ts +2 -0
  48. package/dist/http/error_schemas.d.ts.map +1 -1
  49. package/dist/http/error_schemas.js +2 -0
  50. package/dist/realtime/outbound_budget.d.ts +91 -0
  51. package/dist/realtime/outbound_budget.d.ts.map +1 -0
  52. package/dist/realtime/outbound_budget.js +108 -0
  53. package/dist/realtime/sse.d.ts +36 -3
  54. package/dist/realtime/sse.d.ts.map +1 -1
  55. package/dist/realtime/sse.js +58 -6
  56. package/dist/realtime/sse_constants.d.ts +12 -0
  57. package/dist/realtime/sse_constants.d.ts.map +1 -1
  58. package/dist/realtime/sse_constants.js +12 -0
  59. package/dist/server/app_server.d.ts.map +1 -1
  60. package/dist/server/app_server.js +3 -6
  61. package/dist/testing/CLAUDE.md +96 -19
  62. package/dist/testing/admin_integration.js +1 -1
  63. package/dist/testing/audit_completeness.d.ts +1 -1
  64. package/dist/testing/audit_completeness.js +1 -1
  65. package/dist/testing/audit_drift_guard.d.ts +19 -6
  66. package/dist/testing/audit_drift_guard.d.ts.map +1 -1
  67. package/dist/testing/audit_drift_guard.js +25 -9
  68. package/dist/testing/cross_backend/body_size.d.ts +18 -5
  69. package/dist/testing/cross_backend/body_size.d.ts.map +1 -1
  70. package/dist/testing/cross_backend/body_size.js +233 -34
  71. package/dist/testing/cross_backend/body_size_smuggling.d.ts +0 -12
  72. package/dist/testing/cross_backend/body_size_smuggling.d.ts.map +1 -1
  73. package/dist/testing/cross_backend/body_size_smuggling.js +103 -108
  74. package/dist/testing/cross_backend/capabilities.d.ts +3 -21
  75. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  76. package/dist/testing/cross_backend/capabilities.js +1 -3
  77. package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
  78. package/dist/testing/cross_backend/cell_crud.js +31 -2
  79. package/dist/testing/cross_backend/conformance_case.d.ts +3 -3
  80. package/dist/testing/cross_backend/credential_header_robustness.d.ts.map +1 -1
  81. package/dist/testing/cross_backend/credential_header_robustness.js +18 -54
  82. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  83. package/dist/testing/cross_backend/default_backend_configs.js +2 -8
  84. package/dist/testing/cross_backend/role_grant_role_grammar.d.ts +6 -0
  85. package/dist/testing/cross_backend/role_grant_role_grammar.d.ts.map +1 -0
  86. package/dist/testing/cross_backend/role_grant_role_grammar.js +117 -0
  87. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts +2 -3
  88. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
  89. package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +2 -3
  90. package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
  91. package/dist/testing/cross_backend/testing_backdoor.js +10 -4
  92. package/dist/testing/cross_backend/testing_reset_actions.d.ts +87 -2
  93. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  94. package/dist/testing/cross_backend/testing_reset_actions.js +106 -2
  95. package/dist/testing/cross_backend/token_lifetime.d.ts.map +1 -1
  96. package/dist/testing/cross_backend/token_lifetime.js +34 -1
  97. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
  98. package/dist/testing/cross_backend/ts_spine_backend_config.js +8 -13
  99. package/dist/testing/cross_backend/ws_outbound_overflow.d.ts +29 -0
  100. package/dist/testing/cross_backend/ws_outbound_overflow.d.ts.map +1 -0
  101. package/dist/testing/cross_backend/ws_outbound_overflow.js +276 -0
  102. package/dist/testing/integration.js +1 -1
  103. package/dist/testing/schema_introspect.d.ts +30 -2
  104. package/dist/testing/schema_introspect.d.ts.map +1 -1
  105. package/dist/testing/schema_introspect.js +38 -7
  106. package/dist/testing/transports/raw_http.d.ts +78 -0
  107. package/dist/testing/transports/raw_http.d.ts.map +1 -0
  108. package/dist/testing/transports/raw_http.js +141 -0
  109. package/dist/testing/transports/ws_transport.d.ts +4 -2
  110. package/dist/testing/transports/ws_transport.d.ts.map +1 -1
  111. package/dist/testing/transports/ws_transport.js +2 -1
  112. package/dist/testing/ws_round_trip.d.ts +32 -2
  113. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  114. package/dist/testing/ws_round_trip.js +26 -4
  115. package/package.json +1 -1
  116. package/dist/env/resolve.d.ts +0 -163
  117. package/dist/env/resolve.d.ts.map +0 -1
  118. package/dist/env/resolve.js +0 -267
@@ -17,9 +17,11 @@ export interface WsTransportOptions {
17
17
  * Origin header for the upgrade. Backends running with
18
18
  * `ALLOWED_ORIGINS=http://localhost:*` accept `http://localhost:<port>`.
19
19
  * Defaults to `base_url` — acceptable because cross-process tests
20
- * always run against `localhost`.
20
+ * always run against `localhost`. `null` sends no `Origin` at all, which a
21
+ * daemon-token upgrade needs: both spines discard a daemon token (and a
22
+ * bearer token) presented in a browser context.
21
23
  */
22
- readonly origin?: string;
24
+ readonly origin?: string | null;
23
25
  /**
24
26
  * Optional per-call default for `wait_for` timeouts. Falls back to
25
27
  * `WS_CLIENT_DEFAULT_TIMEOUT_MS` if omitted.
@@ -1 +1 @@
1
- {"version":3,"file":"ws_transport.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/transports/ws_transport.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAuB9B,OAAO,EAMN,KAAK,QAAQ,EACb,KAAK,kBAAkB,EAEvB,MAAM,gBAAgB,CAAC;AAIxB,sDAAsD;AACtD,MAAM,WAAW,kBAAkB;IAClC,gHAAgH;IAChH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,8DAA8D;IAC9D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC;;;;;;;OAOG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,kBAAkB,CAAC;IACzC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACpD;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,mBAAmB,GAAU,SAAS,kBAAkB,KAAG,OAAO,CAAC,QAAQ,CAwJvF,CAAC;AAEF,mFAAmF;AACnF,eAAO,MAAM,qBAAqB,aAAa,CAAC;AAEhD;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,4BAA4B,GACxC,SAAS,kBAAkB,KACzB,OAAO,CAAC,QAAQ,CASlB,CAAC"}
1
+ {"version":3,"file":"ws_transport.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/transports/ws_transport.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAuB9B,OAAO,EAMN,KAAK,QAAQ,EACb,KAAK,kBAAkB,EAEvB,MAAM,gBAAgB,CAAC;AAIxB,sDAAsD;AACtD,MAAM,WAAW,kBAAkB;IAClC,gHAAgH;IAChH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,8DAA8D;IAC9D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACxC;;;;;;;OAOG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC;;;OAGG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC;;;;;;;OAOG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,kBAAkB,CAAC;IACzC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACpD;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,mBAAmB,GAAU,SAAS,kBAAkB,KAAG,OAAO,CAAC,QAAQ,CAyJvF,CAAC;AAEF,mFAAmF;AACnF,eAAO,MAAM,qBAAqB,aAAa,CAAC;AAEhD;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,4BAA4B,GACxC,SAAS,kBAAkB,KACzB,OAAO,CAAC,QAAQ,CASlB,CAAC"}
@@ -50,7 +50,8 @@ export const create_ws_transport = async (options) => {
50
50
  headers.Cookie = cookies.join('; ');
51
51
  const socket = new WebSocket(ws_url, {
52
52
  headers,
53
- origin: origin ?? base_url
53
+ // `ws` sends no `Origin` header when the option is undefined
54
+ origin: origin === null ? undefined : (origin ?? base_url)
54
55
  });
55
56
  const received = [];
56
57
  const waiters = [];
@@ -48,9 +48,29 @@ import { BackendWebsocketTransport } from '../actions/transports_ws_backend.js';
48
48
  import { type RequestContext } from '../auth/request_context.js';
49
49
  import { type CredentialType } from '../hono_context.js';
50
50
  import { type WsClient, type WsRequestResponder } from './transports/ws_client.js';
51
+ /**
52
+ * The runtime socket behind a `FakeWs` — what `WSContext.raw` exposes, shaped
53
+ * like Node's `ws`: a `bufferedAmount` the outbound writer reads for
54
+ * backpressure, and a `terminate` it cuts a stalled socket with.
55
+ */
56
+ export interface FakeWsSocket {
57
+ /**
58
+ * Bytes "handed to the socket and not yet written". Grows by each sent
59
+ * frame's UTF-8 length while `stalled`; set it (e.g. to `0`) to model the
60
+ * client reading.
61
+ */
62
+ bufferedAmount: number;
63
+ /** While `true`, sent frames stay in `bufferedAmount` — a client that is not reading. */
64
+ stalled: boolean;
65
+ /** Whether `terminate` was called — the socket was cut with no close frame. */
66
+ terminated: boolean;
67
+ /** Cut the socket. Records `terminated` and empties `bufferedAmount`. */
68
+ terminate: () => void;
69
+ }
51
70
  /**
52
71
  * A `WSContext` paired with capture arrays. Use `sends` to assert on
53
- * outgoing frames; use `closes` to assert on revocation / close.
72
+ * outgoing frames; use `closes` to assert on revocation / close; use
73
+ * `socket` to model a client that stopped reading and to assert on a cut.
54
74
  */
55
75
  export interface FakeWs {
56
76
  ws: WSContext;
@@ -59,13 +79,23 @@ export interface FakeWs {
59
79
  code?: number;
60
80
  reason?: string;
61
81
  }>;
82
+ /** The fake runtime socket (`ws.raw`). */
83
+ socket: FakeWsSocket;
84
+ }
85
+ /** Options for `create_fake_ws`. */
86
+ export interface CreateFakeWsOptions {
87
+ /** Start with the client not reading (`FakeWsSocket.stalled`). */
88
+ stalled?: boolean;
62
89
  }
63
90
  /**
64
91
  * Build a real `WSContext` backed by in-memory `send`/`close` capture.
65
92
  * Parsing of outgoing frames is left to the caller — `sends` holds the
66
93
  * raw strings as the dispatcher wrote them.
94
+ *
95
+ * `readyState` reads `1` (open) until the socket is closed (`2`) or
96
+ * terminated (`3`).
67
97
  */
68
- export declare const create_fake_ws: () => FakeWs;
98
+ export declare const create_fake_ws: (options?: CreateFakeWsOptions) => FakeWs;
69
99
  /** Options for `create_fake_hono_context`. */
70
100
  export interface FakeHonoContextOptions {
71
101
  credential_type: CredentialType;
@@ -1 +1 @@
1
- {"version":3,"file":"ws_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/ws_round_trip.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ws_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/ws_round_trip.ts"],"names":[],"mappings":"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"}
@@ -11,24 +11,46 @@ import { ACCOUNT_ID_KEY, AUTH_API_TOKEN_ID_KEY, CREDENTIAL_TYPE_KEY, TEST_CONTEX
11
11
  import { create_jsonrpc_request } from '../http/jsonrpc_helpers.js';
12
12
  import { create_test_account, create_test_actor, create_test_role_grant } from './entities.js';
13
13
  import { WS_CLIENT_DEFAULT_TIMEOUT_MS, deliver_inbound, is_response_for } from './transports/ws_client.js';
14
+ const fake_ws_text_encoder = new TextEncoder();
14
15
  /**
15
16
  * Build a real `WSContext` backed by in-memory `send`/`close` capture.
16
17
  * Parsing of outgoing frames is left to the caller — `sends` holds the
17
18
  * raw strings as the dispatcher wrote them.
19
+ *
20
+ * `readyState` reads `1` (open) until the socket is closed (`2`) or
21
+ * terminated (`3`).
18
22
  */
19
- export const create_fake_ws = () => {
23
+ export const create_fake_ws = (options) => {
20
24
  const sends = [];
21
25
  const closes = [];
26
+ const socket = {
27
+ bufferedAmount: 0,
28
+ stalled: options?.stalled ?? false,
29
+ terminated: false,
30
+ terminate: () => {
31
+ socket.terminated = true;
32
+ socket.bufferedAmount = 0;
33
+ }
34
+ };
22
35
  const init = {
23
36
  send: (data) => {
24
- sends.push(typeof data === 'string' ? data : '<binary>');
37
+ const text = typeof data === 'string' ? data : '<binary>';
38
+ sends.push(text);
39
+ if (socket.stalled)
40
+ socket.bufferedAmount += fake_ws_text_encoder.encode(text).byteLength;
25
41
  },
26
42
  close: (code, reason) => {
27
43
  closes.push({ code, reason });
44
+ // a close frame is a few bytes behind whatever is still unwritten
45
+ if (socket.stalled)
46
+ socket.bufferedAmount += 2;
28
47
  },
29
- readyState: 1
48
+ raw: socket,
49
+ get readyState() {
50
+ return socket.terminated ? 3 : closes.length > 0 ? 2 : 1;
51
+ }
30
52
  };
31
- return { ws: new WSContext(init), sends, closes };
53
+ return { ws: new WSContext(init), sends, closes, socket };
32
54
  };
33
55
  /**
34
56
  * Build a fake Hono `Context` exposing the auth keys the dispatcher
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_app",
3
- "version": "0.134.0",
3
+ "version": "0.136.0",
4
4
  "description": "fullstack app library",
5
5
  "glyph": "🗝",
6
6
  "logo": "logo.svg",
@@ -1,163 +0,0 @@
1
- /**
2
- * Environment variable `$$VAR$$` resolution suite.
3
- *
4
- * Resolves `$$VAR$$` references in strings and object trees,
5
- * scans configs for references, and validates/formats missing vars.
6
- *
7
- * The double-dollar bookending syntax is:
8
- * - Visually distinct from shell `$VAR` syntax
9
- * - Unambiguous about variable boundaries
10
- * - Easy to grep: `grep '\$\$'`
11
- * - Fails loud if accidentally shell-processed (`$$`=PID in shell)
12
- *
13
- * # Syntax
14
- *
15
- * - `$$VAR$$` — required reference. Missing or empty fails validation
16
- * (see `validate_env_vars`).
17
- * - `$$?VAR$$` — optional reference. Missing or empty resolves to the
18
- * empty string and passes validation. Use for vars that exist in the
19
- * contract but may be intentionally blank (e.g. `SMTP_PASSWORD=` on
20
- * a deployment that hasn't configured SMTP yet).
21
- * - `\$$VAR$$` — escape. The leading backslash is dropped at resolution
22
- * time and the rest is emitted literally. Use this when documenting
23
- * the syntax inside a string literal (comments in env-file templates,
24
- * for example) where a regex scan would otherwise treat the mention
25
- * as a real reference. Combines with `?` as `\$$?VAR$$`.
26
- *
27
- * @module
28
- */
29
- import type { EnvDeps } from '../runtime/deps.js';
30
- /**
31
- * Resolve environment variable references in a string.
32
- *
33
- * - `$$VAR$$` resolves from the runtime env; missing values are left as-is
34
- * for the validation phase to report.
35
- * - `$$?VAR$$` is the optional form — missing or empty resolves to the empty
36
- * string. Required validation skips refs marked optional.
37
- * - `\$$VAR$$` / `\$$?VAR$$` are escapes — the leading backslash is dropped
38
- * and the body is emitted literally (no resolution attempted).
39
- *
40
- * @param runtime - runtime with `env_get` capability
41
- * @param value - string that may contain `$$VAR$$` references
42
- * @returns string with env vars resolved
43
- */
44
- export declare const resolve_env_vars: (runtime: Pick<EnvDeps, "env_get">, value: string) => string;
45
- /**
46
- * Check if a string contains unresolved env var references.
47
- *
48
- * Escaped references (`\$$VAR$$`) do not count — they're literal text once
49
- * resolved.
50
- *
51
- * @param value - string to check
52
- * @returns `true` if string contains unescaped `$$VAR$$` patterns
53
- */
54
- export declare const has_env_vars: (value: string) => boolean;
55
- /**
56
- * Get list of env var names referenced in a string.
57
- *
58
- * Escaped references are skipped; optional and required references are both
59
- * included (callers that care about the distinction should use `scan_env_vars`
60
- * which preserves the `optional` flag per ref).
61
- *
62
- * @param value - string to scan
63
- * @returns array of variable names (without `$$` delimiters)
64
- */
65
- export declare const get_env_var_names: (value: string) => Array<string>;
66
- /**
67
- * Resolve env vars in an object's string values (shallow).
68
- *
69
- * @param runtime - runtime with `env_get` capability
70
- * @param obj - object with string values
71
- * @returns new object with env vars resolved
72
- */
73
- export declare const resolve_env_vars_in_object: <T extends Record<string, unknown>>(runtime: Pick<EnvDeps, "env_get">, obj: T) => T;
74
- /**
75
- * Resolve env vars and throw if any are missing/empty.
76
- *
77
- * Use this for values that must be present. `$$?VAR$$` (optional) refs
78
- * resolve to the empty string on miss without contributing to the error.
79
- * Escaped references (`\$$VAR$$`) emit literally and never check the env.
80
- *
81
- * @param runtime - runtime with `env_get` capability
82
- * @param value - string with `$$VAR$$` references
83
- * @param context - description for error message (e.g., `"target.host"`)
84
- * @returns resolved string
85
- * @throws Error if any referenced env var is missing or empty
86
- */
87
- export declare const resolve_env_vars_required: (runtime: Pick<EnvDeps, "env_get">, value: string, context: string) => string;
88
- /**
89
- * An env var reference found in a config.
90
- */
91
- export interface EnvVarRef {
92
- /** Variable name (without `$$` delimiters). */
93
- name: string;
94
- /** Path where the reference was found (e.g., `"target.host"`, `"resources[3].path"`). */
95
- path: string;
96
- /**
97
- * Whether the reference is optional (`$$?VAR$$`). Optional refs resolve
98
- * to the empty string when unset and are skipped by `validate_env_vars` —
99
- * a var that's intentionally blank doesn't count as missing.
100
- */
101
- optional: boolean;
102
- }
103
- /**
104
- * Recursively scan an object for `$$VAR$$` env var references.
105
- *
106
- * Walks all string values in the object tree and extracts env var names
107
- * with their path context for error reporting. Escaped references
108
- * (`\$$VAR$$`) are skipped — they're literal text, not references.
109
- * The `optional` flag on each ref distinguishes `$$VAR$$` (required) from
110
- * `$$?VAR$$` (optional) for downstream validation.
111
- *
112
- * @param obj - object to scan (typically a config)
113
- * @returns array of env var references with paths and optional flags
114
- */
115
- export declare const scan_env_vars: (obj: unknown) => Array<EnvVarRef>;
116
- /**
117
- * Result of env var validation.
118
- *
119
- * Uses discriminated union for better type narrowing:
120
- * - `ok: true, missing: null` — all vars present
121
- * - `ok: false, missing: EnvVarRef[]` — some vars missing
122
- */
123
- export type EnvValidationResult = {
124
- ok: true;
125
- missing: null;
126
- } | {
127
- ok: false;
128
- missing: Array<EnvVarRef>;
129
- };
130
- /**
131
- * Validate that all referenced env vars exist in the environment.
132
- *
133
- * Returns all missing refs (including duplicates by name). Grouping
134
- * and deduplication is handled by `format_missing_env_vars` at display time.
135
- * Refs marked `optional: true` (from `$$?VAR$$` syntax) are skipped — a
136
- * deliberately-blank var is contract, not a missing dependency.
137
- *
138
- * @param runtime - runtime with `env_get` capability
139
- * @param refs - env var references from `scan_env_vars`
140
- * @returns validation result with any missing vars
141
- */
142
- export declare const validate_env_vars: (runtime: Pick<EnvDeps, "env_get">, refs: Array<EnvVarRef>) => EnvValidationResult;
143
- /**
144
- * Options for `format_missing_env_vars`.
145
- */
146
- export interface FormatMissingEnvVarsOptions {
147
- /** Path to env file if one was loaded. */
148
- env_file?: string;
149
- /** Hint text for how to set up the environment. */
150
- setup_hint?: string;
151
- }
152
- /**
153
- * Format missing env vars error message.
154
- *
155
- * Groups refs by variable name so each missing var is shown once
156
- * with all paths where it's referenced.
157
- *
158
- * @param missing - missing env var references (may contain duplicate names)
159
- * @param options - formatting options
160
- * @returns formatted error message for display
161
- */
162
- export declare const format_missing_env_vars: (missing: Array<EnvVarRef>, options?: FormatMissingEnvVarsOptions) => string;
163
- //# sourceMappingURL=resolve.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"resolve.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/env/resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAiBlD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,gBAAgB,GAAI,SAAS,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,EAAE,OAAO,MAAM,KAAG,MAYnF,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,MAAM,KAAG,OAQ5C,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,GAAI,OAAO,MAAM,KAAG,KAAK,CAAC,MAAM,CAS7D,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,GAAI,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3E,SAAS,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,EACjC,KAAK,CAAC,KACJ,CAQF,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,yBAAyB,GACrC,SAAS,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,EACjC,OAAO,MAAM,EACb,SAAS,MAAM,KACb,MAyBF,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,SAAS;IACzB,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,yFAAyF;IACzF,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,QAAQ,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,aAAa,GAAI,KAAK,OAAO,KAAG,KAAK,CAAC,SAAS,CAI3D,CAAC;AA2BF;;;;;;GAMG;AACH,MAAM,MAAM,mBAAmB,GAC9B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,CAAA;CAAE,CAAC;AAExE;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iBAAiB,GAC7B,SAAS,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,EACjC,MAAM,KAAK,CAAC,SAAS,CAAC,KACpB,mBAYF,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC3C,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,uBAAuB,GACnC,SAAS,KAAK,CAAC,SAAS,CAAC,EACzB,UAAU,2BAA2B,KACnC,MA+BF,CAAC"}
@@ -1,267 +0,0 @@
1
- /**
2
- * Environment variable `$$VAR$$` resolution suite.
3
- *
4
- * Resolves `$$VAR$$` references in strings and object trees,
5
- * scans configs for references, and validates/formats missing vars.
6
- *
7
- * The double-dollar bookending syntax is:
8
- * - Visually distinct from shell `$VAR` syntax
9
- * - Unambiguous about variable boundaries
10
- * - Easy to grep: `grep '\$\$'`
11
- * - Fails loud if accidentally shell-processed (`$$`=PID in shell)
12
- *
13
- * # Syntax
14
- *
15
- * - `$$VAR$$` — required reference. Missing or empty fails validation
16
- * (see `validate_env_vars`).
17
- * - `$$?VAR$$` — optional reference. Missing or empty resolves to the
18
- * empty string and passes validation. Use for vars that exist in the
19
- * contract but may be intentionally blank (e.g. `SMTP_PASSWORD=` on
20
- * a deployment that hasn't configured SMTP yet).
21
- * - `\$$VAR$$` — escape. The leading backslash is dropped at resolution
22
- * time and the rest is emitted literally. Use this when documenting
23
- * the syntax inside a string literal (comments in env-file templates,
24
- * for example) where a regex scan would otherwise treat the mention
25
- * as a real reference. Combines with `?` as `\$$?VAR$$`.
26
- *
27
- * @module
28
- */
29
- /**
30
- * Pattern matching `$$VAR$$`, `$$?VAR$$`, and their escaped forms
31
- * `\$$VAR$$` / `\$$?VAR$$`. Capture groups:
32
- *
33
- * - 1: the `$$...$$` body (without any leading escape char) — what to
34
- * emit when the match is escaped.
35
- * - 2: `?` if the reference is optional, empty otherwise.
36
- * - 3: the variable name (without `$$` delimiters).
37
- *
38
- * The leading `\\?` (optional backslash) is the escape signal — its
39
- * presence in `match[0]` (vs. `match[1]`) drives the literal-emission
40
- * branch in the resolver.
41
- */
42
- const ENV_VAR_PATTERN = /\\?(\$\$(\??)([A-Za-z_][A-Za-z0-9_]*)\$\$)/g;
43
- /**
44
- * Resolve environment variable references in a string.
45
- *
46
- * - `$$VAR$$` resolves from the runtime env; missing values are left as-is
47
- * for the validation phase to report.
48
- * - `$$?VAR$$` is the optional form — missing or empty resolves to the empty
49
- * string. Required validation skips refs marked optional.
50
- * - `\$$VAR$$` / `\$$?VAR$$` are escapes — the leading backslash is dropped
51
- * and the body is emitted literally (no resolution attempted).
52
- *
53
- * @param runtime - runtime with `env_get` capability
54
- * @param value - string that may contain `$$VAR$$` references
55
- * @returns string with env vars resolved
56
- */
57
- export const resolve_env_vars = (runtime, value) => {
58
- return value.replace(ENV_VAR_PATTERN, (match, body, modifier, name) => {
59
- if (match.startsWith('\\')) {
60
- // Escaped: drop the leading backslash, emit the body literally.
61
- return body;
62
- }
63
- const resolved = runtime.env_get(name);
64
- if (resolved !== undefined && resolved !== '')
65
- return resolved;
66
- // Optional: empty-on-miss. Required: leave unresolved for validation.
67
- if (modifier === '?')
68
- return '';
69
- return match;
70
- });
71
- };
72
- /**
73
- * Check if a string contains unresolved env var references.
74
- *
75
- * Escaped references (`\$$VAR$$`) do not count — they're literal text once
76
- * resolved.
77
- *
78
- * @param value - string to check
79
- * @returns `true` if string contains unescaped `$$VAR$$` patterns
80
- */
81
- export const has_env_vars = (value) => {
82
- // Iterate the shared pattern (fresh regex; lastIndex is fine on construct).
83
- const pattern = new RegExp(ENV_VAR_PATTERN.source, 'g');
84
- let match;
85
- while ((match = pattern.exec(value)) !== null) {
86
- if (!match[0].startsWith('\\'))
87
- return true;
88
- }
89
- return false;
90
- };
91
- /**
92
- * Get list of env var names referenced in a string.
93
- *
94
- * Escaped references are skipped; optional and required references are both
95
- * included (callers that care about the distinction should use `scan_env_vars`
96
- * which preserves the `optional` flag per ref).
97
- *
98
- * @param value - string to scan
99
- * @returns array of variable names (without `$$` delimiters)
100
- */
101
- export const get_env_var_names = (value) => {
102
- const names = [];
103
- const pattern = new RegExp(ENV_VAR_PATTERN.source, 'g');
104
- let match;
105
- while ((match = pattern.exec(value)) !== null) {
106
- if (match[0].startsWith('\\'))
107
- continue;
108
- names.push(match[3]);
109
- }
110
- return names;
111
- };
112
- /**
113
- * Resolve env vars in an object's string values (shallow).
114
- *
115
- * @param runtime - runtime with `env_get` capability
116
- * @param obj - object with string values
117
- * @returns new object with env vars resolved
118
- */
119
- export const resolve_env_vars_in_object = (runtime, obj) => {
120
- const result = { ...obj };
121
- for (const [key, value] of Object.entries(result)) {
122
- if (typeof value === 'string') {
123
- result[key] = resolve_env_vars(runtime, value);
124
- }
125
- }
126
- return result;
127
- };
128
- /**
129
- * Resolve env vars and throw if any are missing/empty.
130
- *
131
- * Use this for values that must be present. `$$?VAR$$` (optional) refs
132
- * resolve to the empty string on miss without contributing to the error.
133
- * Escaped references (`\$$VAR$$`) emit literally and never check the env.
134
- *
135
- * @param runtime - runtime with `env_get` capability
136
- * @param value - string with `$$VAR$$` references
137
- * @param context - description for error message (e.g., `"target.host"`)
138
- * @returns resolved string
139
- * @throws Error if any referenced env var is missing or empty
140
- */
141
- export const resolve_env_vars_required = (runtime, value, context) => {
142
- const missing = [];
143
- const result = value.replace(ENV_VAR_PATTERN, (match, body, modifier, name) => {
144
- if (match.startsWith('\\')) {
145
- // Escaped — emit body, no env lookup.
146
- return body;
147
- }
148
- const resolved = runtime.env_get(name);
149
- if (resolved !== undefined && resolved !== '')
150
- return resolved;
151
- if (modifier === '?')
152
- return '';
153
- missing.push(name);
154
- return match; // keep original for error message
155
- });
156
- if (missing.length > 0) {
157
- throw new Error(`Missing required environment variable(s) for ${context}: ${missing.join(', ')}`);
158
- }
159
- return result;
160
- };
161
- /**
162
- * Recursively scan an object for `$$VAR$$` env var references.
163
- *
164
- * Walks all string values in the object tree and extracts env var names
165
- * with their path context for error reporting. Escaped references
166
- * (`\$$VAR$$`) are skipped — they're literal text, not references.
167
- * The `optional` flag on each ref distinguishes `$$VAR$$` (required) from
168
- * `$$?VAR$$` (optional) for downstream validation.
169
- *
170
- * @param obj - object to scan (typically a config)
171
- * @returns array of env var references with paths and optional flags
172
- */
173
- export const scan_env_vars = (obj) => {
174
- const refs = [];
175
- scan_recursive(obj, '', refs);
176
- return refs;
177
- };
178
- /**
179
- * Recursive helper for `scan_env_vars`. Uses the full pattern (not
180
- * `get_env_var_names`) to preserve the `optional` modifier on each ref.
181
- */
182
- const scan_recursive = (value, path, refs) => {
183
- if (typeof value === 'string') {
184
- const pattern = new RegExp(ENV_VAR_PATTERN.source, 'g');
185
- let match;
186
- while ((match = pattern.exec(value)) !== null) {
187
- if (match[0].startsWith('\\'))
188
- continue; // escaped — literal text
189
- refs.push({ name: match[3], path, optional: match[2] === '?' });
190
- }
191
- }
192
- else if (Array.isArray(value)) {
193
- for (let i = 0; i < value.length; i++) {
194
- scan_recursive(value[i], `${path}[${i}]`, refs);
195
- }
196
- }
197
- else if (value !== null && typeof value === 'object') {
198
- for (const [key, v] of Object.entries(value)) {
199
- const new_path = path ? `${path}.${key}` : key;
200
- scan_recursive(v, new_path, refs);
201
- }
202
- }
203
- // primitives (number, boolean, null, undefined) have no env vars
204
- };
205
- /**
206
- * Validate that all referenced env vars exist in the environment.
207
- *
208
- * Returns all missing refs (including duplicates by name). Grouping
209
- * and deduplication is handled by `format_missing_env_vars` at display time.
210
- * Refs marked `optional: true` (from `$$?VAR$$` syntax) are skipped — a
211
- * deliberately-blank var is contract, not a missing dependency.
212
- *
213
- * @param runtime - runtime with `env_get` capability
214
- * @param refs - env var references from `scan_env_vars`
215
- * @returns validation result with any missing vars
216
- */
217
- export const validate_env_vars = (runtime, refs) => {
218
- let missing = null;
219
- for (const ref of refs) {
220
- if (ref.optional)
221
- continue;
222
- const value = runtime.env_get(ref.name);
223
- if (value === undefined || value === '') {
224
- (missing ??= []).push(ref);
225
- }
226
- }
227
- return missing === null ? { ok: true, missing: null } : { ok: false, missing };
228
- };
229
- /**
230
- * Format missing env vars error message.
231
- *
232
- * Groups refs by variable name so each missing var is shown once
233
- * with all paths where it's referenced.
234
- *
235
- * @param missing - missing env var references (may contain duplicate names)
236
- * @param options - formatting options
237
- * @returns formatted error message for display
238
- */
239
- export const format_missing_env_vars = (missing, options) => {
240
- const lines = ['Missing required environment variables:', ''];
241
- // group by variable name, preserving insertion order
242
- const grouped = new Map();
243
- for (const ref of missing) {
244
- let paths = grouped.get(ref.name);
245
- if (!paths) {
246
- paths = [];
247
- grouped.set(ref.name, paths);
248
- }
249
- paths.push(ref.path);
250
- }
251
- for (const [name, paths] of grouped) {
252
- lines.push(` ${name} - used in ${paths.join(', ')}`);
253
- }
254
- lines.push('');
255
- const env_file = options?.env_file;
256
- if (env_file) {
257
- lines.push(`Loaded from: ${env_file}`);
258
- lines.push(`Add these to your ${env_file} file.`);
259
- if (options.setup_hint) {
260
- lines.push(options.setup_hint);
261
- }
262
- }
263
- else {
264
- lines.push('Use --env_file to load environment variables from a file.');
265
- }
266
- return lines.join('\n');
267
- };