@fuzdev/fuz_app 0.64.0 → 0.66.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.
- package/dist/actions/CLAUDE.md +510 -946
- package/dist/actions/action_codegen.d.ts +1 -1
- package/dist/actions/action_codegen.js +1 -1
- package/dist/actions/action_event_data.d.ts +1 -1
- package/dist/actions/broadcast_api.d.ts +1 -1
- package/dist/actions/broadcast_api.js +1 -1
- package/dist/actions/cancel.d.ts +2 -2
- package/dist/actions/cancel.js +3 -3
- package/dist/actions/connection_closer.d.ts +1 -4
- package/dist/actions/connection_closer.d.ts.map +1 -1
- package/dist/actions/connection_closer.js +1 -4
- package/dist/actions/register_action_ws.d.ts +2 -2
- package/dist/actions/register_ws_endpoint.d.ts +1 -1
- package/dist/actions/transports_ws_auth_guard.d.ts +1 -2
- package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
- package/dist/actions/transports_ws_auth_guard.js +1 -2
- package/dist/auth/CLAUDE.md +570 -1871
- package/dist/auth/account_schema.d.ts +1 -1
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/api_token_queries.js +1 -1
- package/dist/auth/audit_log_ddl.d.ts +1 -1
- package/dist/auth/audit_log_ddl.d.ts.map +1 -1
- package/dist/auth/audit_log_ddl.js +1 -1
- package/dist/auth/audit_log_schema.js +2 -2
- package/dist/auth/bootstrap_account.d.ts.map +1 -1
- package/dist/auth/bootstrap_account.js +1 -5
- package/dist/auth/bootstrap_routes.d.ts +7 -1
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +15 -11
- package/dist/auth/daemon_token_middleware.d.ts +15 -5
- package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
- package/dist/auth/daemon_token_middleware.js +24 -15
- package/dist/auth/invite_queries.d.ts +17 -7
- package/dist/auth/invite_queries.d.ts.map +1 -1
- package/dist/auth/invite_queries.js +19 -8
- package/dist/auth/keyring.d.ts +6 -6
- package/dist/auth/keyring.js +8 -8
- package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_actions.js +4 -2
- package/dist/auth/signup_routes.d.ts +47 -1
- package/dist/auth/signup_routes.d.ts.map +1 -1
- package/dist/auth/signup_routes.js +103 -52
- package/dist/db/create_db.d.ts.map +1 -1
- package/dist/db/create_db.js +13 -0
- package/dist/dev/setup.d.ts +2 -2
- package/dist/dev/setup.js +3 -3
- package/dist/env/resolve.d.ts +44 -7
- package/dist/env/resolve.d.ts.map +1 -1
- package/dist/env/resolve.js +94 -27
- package/dist/http/CLAUDE.md +243 -522
- package/dist/http/error_schemas.d.ts +0 -4
- package/dist/http/error_schemas.d.ts.map +1 -1
- package/dist/http/error_schemas.js +0 -4
- package/dist/http/ip_canonical.d.ts +5 -4
- package/dist/http/ip_canonical.d.ts.map +1 -1
- package/dist/http/ip_canonical.js +8 -4
- package/dist/http/jsonrpc.d.ts +23 -7
- package/dist/http/jsonrpc.d.ts.map +1 -1
- package/dist/http/jsonrpc.js +19 -3
- package/dist/http/origin.d.ts +1 -1
- package/dist/http/origin.js +1 -1
- package/dist/http/surface.d.ts +9 -2
- package/dist/http/surface.d.ts.map +1 -1
- package/dist/runtime/mock.d.ts +1 -1
- package/dist/runtime/mock.js +2 -2
- package/dist/server/app_server.d.ts +41 -10
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +10 -4
- package/dist/server/env.d.ts +7 -7
- package/dist/server/env.d.ts.map +1 -1
- package/dist/server/env.js +14 -14
- package/dist/server/static.d.ts +4 -4
- package/dist/server/static.js +7 -7
- package/dist/testing/CLAUDE.md +740 -418
- package/dist/testing/admin_integration.d.ts +18 -23
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +230 -216
- package/dist/testing/app_server.d.ts +141 -39
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +157 -44
- package/dist/testing/audit_completeness.d.ts +25 -22
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +198 -159
- package/dist/testing/bootstrap_success.d.ts +28 -0
- package/dist/testing/bootstrap_success.d.ts.map +1 -0
- package/dist/testing/bootstrap_success.js +144 -0
- package/dist/testing/cross_backend/backend_config.d.ts +113 -0
- package/dist/testing/cross_backend/backend_config.d.ts.map +1 -0
- package/dist/testing/cross_backend/backend_config.js +1 -0
- package/dist/testing/cross_backend/bench/bench_report.d.ts +46 -0
- package/dist/testing/cross_backend/bench/bench_report.d.ts.map +1 -0
- package/dist/testing/cross_backend/bench/bench_report.js +83 -0
- package/dist/testing/cross_backend/bench/run_cross_impl_bench.d.ts +44 -0
- package/dist/testing/cross_backend/bench/run_cross_impl_bench.d.ts.map +1 -0
- package/dist/testing/cross_backend/bench/run_cross_impl_bench.js +38 -0
- package/dist/testing/cross_backend/bench/scenario.d.ts +57 -0
- package/dist/testing/cross_backend/bench/scenario.d.ts.map +1 -0
- package/dist/testing/cross_backend/bench/scenario.js +28 -0
- package/dist/testing/cross_backend/bootstrap_backend.d.ts +41 -0
- package/dist/testing/cross_backend/bootstrap_backend.d.ts.map +1 -0
- package/dist/testing/cross_backend/bootstrap_backend.js +34 -0
- package/dist/testing/cross_backend/build_test_backend_paths.d.ts +24 -0
- package/dist/testing/cross_backend/build_test_backend_paths.d.ts.map +1 -0
- package/dist/testing/cross_backend/build_test_backend_paths.js +33 -0
- package/dist/testing/cross_backend/capabilities.d.ts +65 -0
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -0
- package/dist/testing/cross_backend/capabilities.js +47 -0
- package/dist/testing/cross_backend/default_backend_configs.d.ts +122 -0
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -0
- package/dist/testing/cross_backend/default_backend_configs.js +111 -0
- package/dist/testing/cross_backend/default_secrets.d.ts +40 -0
- package/dist/testing/cross_backend/default_secrets.d.ts.map +1 -0
- package/dist/testing/cross_backend/default_secrets.js +39 -0
- package/dist/testing/cross_backend/default_spine_surface.d.ts +64 -0
- package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -0
- package/dist/testing/cross_backend/default_spine_surface.js +121 -0
- package/dist/testing/cross_backend/setup.d.ts +451 -0
- package/dist/testing/cross_backend/setup.d.ts.map +1 -0
- package/dist/testing/cross_backend/setup.js +581 -0
- package/dist/testing/cross_backend/spawn_backend.d.ts +58 -0
- package/dist/testing/cross_backend/spawn_backend.d.ts.map +1 -0
- package/dist/testing/cross_backend/spawn_backend.js +229 -0
- package/dist/testing/cross_backend/spine_stub_backend_config.d.ts +66 -0
- package/dist/testing/cross_backend/spine_stub_backend_config.d.ts.map +1 -0
- package/dist/testing/cross_backend/spine_stub_backend_config.js +49 -0
- package/dist/testing/cross_backend/sse_round_trip.d.ts +37 -0
- package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -0
- package/dist/testing/cross_backend/sse_round_trip.js +137 -0
- package/dist/testing/cross_backend/standard.d.ts +96 -0
- package/dist/testing/cross_backend/standard.d.ts.map +1 -0
- package/dist/testing/cross_backend/standard.js +49 -0
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +171 -0
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -0
- package/dist/testing/cross_backend/testing_reset_actions.js +213 -0
- package/dist/testing/cross_backend/testing_server_bun.d.ts +5 -0
- package/dist/testing/cross_backend/testing_server_bun.d.ts.map +1 -0
- package/dist/testing/cross_backend/testing_server_bun.js +59 -0
- package/dist/testing/cross_backend/testing_server_core.d.ts +140 -0
- package/dist/testing/cross_backend/testing_server_core.d.ts.map +1 -0
- package/dist/testing/cross_backend/testing_server_core.js +68 -0
- package/dist/testing/cross_backend/testing_server_deno.d.ts +5 -0
- package/dist/testing/cross_backend/testing_server_deno.d.ts.map +1 -0
- package/dist/testing/cross_backend/testing_server_deno.js +37 -0
- package/dist/testing/cross_backend/testing_server_node.d.ts +5 -0
- package/dist/testing/cross_backend/testing_server_node.d.ts.map +1 -0
- package/dist/testing/cross_backend/testing_server_node.js +50 -0
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts +72 -0
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -0
- package/dist/testing/cross_backend/ts_spine_backend_config.js +112 -0
- package/dist/testing/cross_backend/ws_round_trip.d.ts +35 -0
- package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -0
- package/dist/testing/cross_backend/ws_round_trip.js +113 -0
- package/dist/testing/data_exposure.d.ts +11 -14
- package/dist/testing/data_exposure.d.ts.map +1 -1
- package/dist/testing/data_exposure.js +123 -146
- package/dist/testing/db_entities.d.ts +22 -1
- package/dist/testing/db_entities.d.ts.map +1 -1
- package/dist/testing/db_entities.js +24 -1
- package/dist/testing/integration.d.ts +56 -21
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +294 -319
- package/dist/testing/integration_helpers.d.ts +16 -6
- package/dist/testing/integration_helpers.d.ts.map +1 -1
- package/dist/testing/integration_helpers.js +7 -7
- package/dist/testing/mock_fs.d.ts.map +1 -1
- package/dist/testing/mock_fs.js +0 -2
- package/dist/testing/rate_limiting.d.ts.map +1 -1
- package/dist/testing/rate_limiting.js +9 -0
- package/dist/testing/role_grant_helpers.d.ts +31 -0
- package/dist/testing/role_grant_helpers.d.ts.map +1 -0
- package/dist/testing/role_grant_helpers.js +46 -0
- package/dist/testing/round_trip.d.ts +20 -16
- package/dist/testing/round_trip.d.ts.map +1 -1
- package/dist/testing/round_trip.js +61 -86
- package/dist/testing/rpc_helpers.d.ts +10 -4
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_helpers.js +1 -1
- package/dist/testing/rpc_round_trip.d.ts +24 -21
- package/dist/testing/rpc_round_trip.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.js +87 -104
- package/dist/testing/schema_introspect.d.ts +106 -0
- package/dist/testing/schema_introspect.d.ts.map +1 -0
- package/dist/testing/schema_introspect.js +123 -0
- package/dist/testing/schema_parity.d.ts +144 -0
- package/dist/testing/schema_parity.d.ts.map +1 -0
- package/dist/testing/schema_parity.js +233 -0
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/sse_round_trip.js +1 -68
- package/dist/testing/standard.d.ts +56 -25
- package/dist/testing/standard.d.ts.map +1 -1
- package/dist/testing/standard.js +62 -5
- package/dist/testing/stubs.d.ts +21 -6
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +33 -23
- package/dist/testing/testing_rate_limiter.d.ts +59 -0
- package/dist/testing/testing_rate_limiter.d.ts.map +1 -0
- package/dist/testing/testing_rate_limiter.js +74 -0
- package/dist/testing/transports/bootstrap.d.ts +52 -0
- package/dist/testing/transports/bootstrap.d.ts.map +1 -0
- package/dist/testing/transports/bootstrap.js +70 -0
- package/dist/testing/transports/fetch_transport.d.ts +81 -0
- package/dist/testing/transports/fetch_transport.d.ts.map +1 -0
- package/dist/testing/transports/fetch_transport.js +74 -0
- package/dist/testing/transports/sse_frame_reader.d.ts +41 -0
- package/dist/testing/transports/sse_frame_reader.d.ts.map +1 -0
- package/dist/testing/transports/sse_frame_reader.js +84 -0
- package/dist/testing/transports/sse_transport.d.ts +54 -0
- package/dist/testing/transports/sse_transport.d.ts.map +1 -0
- package/dist/testing/transports/sse_transport.js +51 -0
- package/dist/testing/transports/ws_client.d.ts +108 -0
- package/dist/testing/transports/ws_client.d.ts.map +1 -0
- package/dist/testing/transports/ws_client.js +56 -0
- package/dist/testing/transports/ws_transport.d.ts +43 -0
- package/dist/testing/transports/ws_transport.d.ts.map +1 -0
- package/dist/testing/transports/ws_transport.js +169 -0
- package/dist/testing/ws_round_trip.d.ts +21 -103
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +42 -40
- package/dist/ui/CLAUDE.md +5 -3
- package/dist/ui/MenuLink.svelte +16 -16
- package/dist/ui/MenuLink.svelte.d.ts +13 -4
- package/dist/ui/MenuLink.svelte.d.ts.map +1 -1
- package/package.json +10 -4
|
@@ -13,6 +13,27 @@ import { type RateLimiter } from '../rate_limiter.js';
|
|
|
13
13
|
import type { RouteFactoryDeps } from './deps.js';
|
|
14
14
|
import type { AppSettings } from './app_settings_schema.js';
|
|
15
15
|
import type { AuthSessionRouteOptions } from './account_routes.js';
|
|
16
|
+
/**
|
|
17
|
+
* Default minimum wall-clock time (ms) for a signup denial (403 / 409) response.
|
|
18
|
+
*
|
|
19
|
+
* Parallel to login's `DEFAULT_LOGIN_FAIL_FLOOR_MS`. Without a floor, an
|
|
20
|
+
* attacker can distinguish `ERROR_NO_MATCHING_INVITE` (cheap — bails before
|
|
21
|
+
* Argon2 + tx) from `ERROR_SIGNUP_CONFLICT` (Argon2 + tx + rollback) via
|
|
22
|
+
* response time and use the gap as a username-enumeration oracle. Picked
|
|
23
|
+
* to exceed the p99 of every denial code path (Argon2id dominates at
|
|
24
|
+
* ~100ms, plus DB + overhead). 429 stays fast by design (same precedent
|
|
25
|
+
* as login) so rate-limit DoS handling stays cheap.
|
|
26
|
+
*/
|
|
27
|
+
export declare const DEFAULT_SIGNUP_FAIL_FLOOR_MS = 250;
|
|
28
|
+
/**
|
|
29
|
+
* Default uniform jitter window (±ms) layered on the floor.
|
|
30
|
+
*
|
|
31
|
+
* Random jitter prevents a stable clamp point from leaking whenever a
|
|
32
|
+
* path occasionally exceeds the floor. `Math.random` is sufficient —
|
|
33
|
+
* we only need unpredictability of the exact delay, not cryptographic
|
|
34
|
+
* guarantees.
|
|
35
|
+
*/
|
|
36
|
+
export declare const DEFAULT_SIGNUP_FAIL_JITTER_MS = 25;
|
|
16
37
|
/**
|
|
17
38
|
* Per-factory configuration for signup route specs.
|
|
18
39
|
*/
|
|
@@ -21,6 +42,18 @@ export interface SignupRouteOptions extends AuthSessionRouteOptions {
|
|
|
21
42
|
signup_account_rate_limiter: RateLimiter | null;
|
|
22
43
|
/** Mutable ref to app settings — when `open_signup` is true, invite check is skipped. */
|
|
23
44
|
app_settings: AppSettings;
|
|
45
|
+
/**
|
|
46
|
+
* Minimum wall-clock time (ms) for signup denial responses (403 / 409).
|
|
47
|
+
* Set to `0` or a negative number to disable (e.g., in tests). Default
|
|
48
|
+
* `DEFAULT_SIGNUP_FAIL_FLOOR_MS`. 429 responses are not floored.
|
|
49
|
+
*/
|
|
50
|
+
signup_fail_floor_ms?: number;
|
|
51
|
+
/**
|
|
52
|
+
* Uniform jitter window (±ms) layered on the floor. Set to `0` to
|
|
53
|
+
* disable jitter while keeping the floor. Default
|
|
54
|
+
* `DEFAULT_SIGNUP_FAIL_JITTER_MS`.
|
|
55
|
+
*/
|
|
56
|
+
signup_fail_jitter_ms?: number;
|
|
24
57
|
}
|
|
25
58
|
/** Input for `POST /signup`. `email` is optional and must match any referenced invite. */
|
|
26
59
|
export declare const SignupInput: z.ZodObject<{
|
|
@@ -29,9 +62,22 @@ export declare const SignupInput: z.ZodObject<{
|
|
|
29
62
|
email: z.ZodOptional<z.ZodEmail>;
|
|
30
63
|
}, z.core.$strict>;
|
|
31
64
|
export type SignupInput = z.infer<typeof SignupInput>;
|
|
32
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* Output for `POST /signup`.
|
|
67
|
+
*
|
|
68
|
+
* Session cookie is the operative side effect. The returned `account` and
|
|
69
|
+
* `actor` mirror `BootstrapOutput` so cross-process per-test setup can read
|
|
70
|
+
* the per-test identity straight off the signup response.
|
|
71
|
+
*/
|
|
33
72
|
export declare const SignupOutput: z.ZodObject<{
|
|
34
73
|
ok: z.ZodLiteral<true>;
|
|
74
|
+
account: z.ZodObject<{
|
|
75
|
+
id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
76
|
+
username: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
77
|
+
}, z.core.$strict>;
|
|
78
|
+
actor: z.ZodObject<{
|
|
79
|
+
id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
80
|
+
}, z.core.$strict>;
|
|
35
81
|
}, z.core.$strict>;
|
|
36
82
|
export type SignupOutput = z.infer<typeof SignupOutput>;
|
|
37
83
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signup_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/signup_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"signup_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/signup_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAatB,OAAO,EAAkB,KAAK,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAEtE,OAAO,EAA+B,KAAK,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAClF,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,WAAW,CAAC;AAOhD,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,0BAA0B,CAAC;AAE1D,OAAO,KAAK,EAAC,uBAAuB,EAAC,MAAM,qBAAqB,CAAC;AAEjE;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,MAAM,CAAC;AAEhD;;;;;;;GAOG;AACH,eAAO,MAAM,6BAA6B,KAAK,CAAC;AAQhD;;GAEG;AACH,MAAM,WAAW,kBAAmB,SAAQ,uBAAuB;IAClE,6FAA6F;IAC7F,2BAA2B,EAAE,WAAW,GAAG,IAAI,CAAC;IAChD,yFAAyF;IACzF,YAAY,EAAE,WAAW,CAAC;IAC1B;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAID,0FAA0F;AAC1F,eAAO,MAAM,WAAW;;;;kBAItB,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD;;;;;;GAMG;AACH,eAAO,MAAM,YAAY;;;;;;;;;kBAIvB,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,GACrC,MAAM,gBAAgB,EACtB,SAAS,kBAAkB,KACzB,KAAK,CAAC,SAAS,CAmLjB,CAAC"}
|
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
10
|
import { z } from 'zod';
|
|
11
|
+
import { Uuid } from '@fuzdev/fuz_util/id.js';
|
|
11
12
|
import { create_session_and_set_cookie } from './session_middleware.js';
|
|
12
13
|
import { query_create_account_with_actor } from './account_queries.js';
|
|
13
|
-
import {
|
|
14
|
+
import { query_invite_find_unclaimed_match_for_update, query_invite_claim_unscoped, } from './invite_queries.js';
|
|
14
15
|
import { Username, Email } from '../primitive_schemas.js';
|
|
15
16
|
import { Password } from './password.js';
|
|
16
17
|
import { get_route_input } from '../http/route_spec.js';
|
|
@@ -18,6 +19,33 @@ import { get_client_ip } from '../http/proxy.js';
|
|
|
18
19
|
import { rate_limit_exceeded_response } from '../rate_limiter.js';
|
|
19
20
|
import { ERROR_NO_MATCHING_INVITE, ERROR_SIGNUP_CONFLICT, ERROR_INVALID_JSON_BODY, ERROR_INVALID_REQUEST_BODY, } from '../http/error_schemas.js';
|
|
20
21
|
import { is_pg_unique_violation } from '../db/pg_error.js';
|
|
22
|
+
/**
|
|
23
|
+
* Default minimum wall-clock time (ms) for a signup denial (403 / 409) response.
|
|
24
|
+
*
|
|
25
|
+
* Parallel to login's `DEFAULT_LOGIN_FAIL_FLOOR_MS`. Without a floor, an
|
|
26
|
+
* attacker can distinguish `ERROR_NO_MATCHING_INVITE` (cheap — bails before
|
|
27
|
+
* Argon2 + tx) from `ERROR_SIGNUP_CONFLICT` (Argon2 + tx + rollback) via
|
|
28
|
+
* response time and use the gap as a username-enumeration oracle. Picked
|
|
29
|
+
* to exceed the p99 of every denial code path (Argon2id dominates at
|
|
30
|
+
* ~100ms, plus DB + overhead). 429 stays fast by design (same precedent
|
|
31
|
+
* as login) so rate-limit DoS handling stays cheap.
|
|
32
|
+
*/
|
|
33
|
+
export const DEFAULT_SIGNUP_FAIL_FLOOR_MS = 250;
|
|
34
|
+
/**
|
|
35
|
+
* Default uniform jitter window (±ms) layered on the floor.
|
|
36
|
+
*
|
|
37
|
+
* Random jitter prevents a stable clamp point from leaking whenever a
|
|
38
|
+
* path occasionally exceeds the floor. `Math.random` is sufficient —
|
|
39
|
+
* we only need unpredictability of the exact delay, not cryptographic
|
|
40
|
+
* guarantees.
|
|
41
|
+
*/
|
|
42
|
+
export const DEFAULT_SIGNUP_FAIL_JITTER_MS = 25;
|
|
43
|
+
const signup_fail_delay = (floor_ms, jitter_ms) => {
|
|
44
|
+
if (floor_ms <= 0)
|
|
45
|
+
return Promise.resolve();
|
|
46
|
+
const jitter = jitter_ms > 0 ? Math.floor(Math.random() * (jitter_ms * 2 + 1)) - jitter_ms : 0;
|
|
47
|
+
return new Promise((resolve) => setTimeout(resolve, floor_ms + jitter));
|
|
48
|
+
};
|
|
21
49
|
// -- Input/output schemas ---------------------------------------------------
|
|
22
50
|
/** Input for `POST /signup`. `email` is optional and must match any referenced invite. */
|
|
23
51
|
export const SignupInput = z.strictObject({
|
|
@@ -25,9 +53,17 @@ export const SignupInput = z.strictObject({
|
|
|
25
53
|
password: Password,
|
|
26
54
|
email: Email.optional(),
|
|
27
55
|
});
|
|
28
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Output for `POST /signup`.
|
|
58
|
+
*
|
|
59
|
+
* Session cookie is the operative side effect. The returned `account` and
|
|
60
|
+
* `actor` mirror `BootstrapOutput` so cross-process per-test setup can read
|
|
61
|
+
* the per-test identity straight off the signup response.
|
|
62
|
+
*/
|
|
29
63
|
export const SignupOutput = z.strictObject({
|
|
30
64
|
ok: z.literal(true),
|
|
65
|
+
account: z.strictObject({ id: Uuid, username: Username }),
|
|
66
|
+
actor: z.strictObject({ id: Uuid }),
|
|
31
67
|
});
|
|
32
68
|
/**
|
|
33
69
|
* Create signup route specs for account creation.
|
|
@@ -38,7 +74,7 @@ export const SignupOutput = z.strictObject({
|
|
|
38
74
|
*/
|
|
39
75
|
export const create_signup_route_specs = (deps, options) => {
|
|
40
76
|
const { keyring, password } = deps;
|
|
41
|
-
const { session_options, ip_rate_limiter, signup_account_rate_limiter, app_settings } = options;
|
|
77
|
+
const { session_options, ip_rate_limiter, signup_account_rate_limiter, app_settings, signup_fail_floor_ms = DEFAULT_SIGNUP_FAIL_FLOOR_MS, signup_fail_jitter_ms = DEFAULT_SIGNUP_FAIL_JITTER_MS, } = options;
|
|
42
78
|
return [
|
|
43
79
|
{
|
|
44
80
|
method: 'POST',
|
|
@@ -57,7 +93,7 @@ export const create_signup_route_specs = (deps, options) => {
|
|
|
57
93
|
409: z.looseObject({ error: z.literal(ERROR_SIGNUP_CONFLICT) }),
|
|
58
94
|
},
|
|
59
95
|
handler: async (c, route) => {
|
|
60
|
-
// Per-IP rate limit check (before any work)
|
|
96
|
+
// Per-IP rate limit check (before any work). 429 stays fast.
|
|
61
97
|
const ip = ip_rate_limiter ? get_client_ip(c) : null;
|
|
62
98
|
if (ip_rate_limiter && ip) {
|
|
63
99
|
const check = ip_rate_limiter.check(ip);
|
|
@@ -66,7 +102,8 @@ export const create_signup_route_specs = (deps, options) => {
|
|
|
66
102
|
}
|
|
67
103
|
}
|
|
68
104
|
const { username, password: pw, email } = get_route_input(c);
|
|
69
|
-
// Per-account rate limit check (after input parsing, before DB work)
|
|
105
|
+
// Per-account rate limit check (after input parsing, before DB work).
|
|
106
|
+
// 429 stays fast — same precedent as login.
|
|
70
107
|
const account_key = username.toLowerCase();
|
|
71
108
|
if (signup_account_rate_limiter) {
|
|
72
109
|
const check = signup_account_rate_limiter.check(account_key);
|
|
@@ -74,13 +111,23 @@ export const create_signup_route_specs = (deps, options) => {
|
|
|
74
111
|
return rate_limit_exceeded_response(c, check.retry_after);
|
|
75
112
|
}
|
|
76
113
|
}
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
114
|
+
// Start the denial-time floor concurrently with failure work.
|
|
115
|
+
// Observed response time for 403 / 409 is `max(work, delay)`
|
|
116
|
+
// so the cheap `no_match` path (no Argon2, find returns
|
|
117
|
+
// nothing) and the expensive `signup_conflict` path (Argon2
|
|
118
|
+
// + tx + rollback) converge — closes the username-enumeration
|
|
119
|
+
// timing oracle. Mirrors login's `login_fail_delay`. Started
|
|
120
|
+
// after rate-limit checks so 429 stays fast.
|
|
121
|
+
const delay = signup_fail_delay(signup_fail_floor_ms, signup_fail_jitter_ms);
|
|
122
|
+
// Hash before the transaction so the connection isn't held
|
|
123
|
+
// across the ~100ms Argon2id. Paid unconditionally — bounded
|
|
124
|
+
// by the per-IP + per-account rate limiters above.
|
|
125
|
+
const password_hash = await password.hash_password(pw);
|
|
126
|
+
// `invite` is assigned inside the tx by the FOR UPDATE find;
|
|
127
|
+
// captured at the outer scope so the failure-audit catch
|
|
128
|
+
// branch can still reference `invite.id` after the tx rolls
|
|
129
|
+
// back on PG unique violation.
|
|
80
130
|
let invite;
|
|
81
|
-
if (!app_settings.open_signup) {
|
|
82
|
-
invite = await query_invite_find_unclaimed_match({ db: route.db }, email ?? null, username);
|
|
83
|
-
}
|
|
84
131
|
const emit_failure_audit = (reason) => {
|
|
85
132
|
deps.audit.emit(route, {
|
|
86
133
|
event_type: 'signup',
|
|
@@ -95,43 +142,34 @@ export const create_signup_route_specs = (deps, options) => {
|
|
|
95
142
|
},
|
|
96
143
|
});
|
|
97
144
|
};
|
|
98
|
-
if (!app_settings.open_signup && !invite) {
|
|
99
|
-
if (ip_rate_limiter && ip)
|
|
100
|
-
ip_rate_limiter.record(ip);
|
|
101
|
-
if (signup_account_rate_limiter)
|
|
102
|
-
signup_account_rate_limiter.record(account_key);
|
|
103
|
-
emit_failure_audit('no_match');
|
|
104
|
-
return c.json({ error: ERROR_NO_MATCHING_INVITE }, 403);
|
|
105
|
-
}
|
|
106
|
-
// Create account, optionally claim invite, and create session atomically.
|
|
107
|
-
// Username/email uniqueness enforced by DB unique constraints.
|
|
108
|
-
const password_hash = await password.hash_password(pw);
|
|
109
145
|
let result;
|
|
110
146
|
try {
|
|
111
147
|
result = await route.db.transaction(async (tx) => {
|
|
112
148
|
const tx_deps = { db: tx };
|
|
113
|
-
|
|
149
|
+
// Find + claim run inside the same transaction so the
|
|
150
|
+
// row lock makes them atomic. Concurrent signups for
|
|
151
|
+
// the same (username, email) tuple block on the lock
|
|
152
|
+
// and observe the post-commit state on retry — the
|
|
153
|
+
// loser's `find_for_update` returns no row (winner
|
|
154
|
+
// flipped `claimed_at`) and falls through to
|
|
155
|
+
// `ERROR_NO_MATCHING_INVITE`. No race window.
|
|
156
|
+
if (!app_settings.open_signup) {
|
|
157
|
+
invite = await query_invite_find_unclaimed_match_for_update(tx_deps, email ?? null, username);
|
|
158
|
+
if (!invite) {
|
|
159
|
+
throw new NoMatchingInviteError();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const { account, actor } = await query_create_account_with_actor(tx_deps, {
|
|
114
163
|
username,
|
|
115
164
|
password_hash,
|
|
116
165
|
email,
|
|
117
166
|
});
|
|
118
167
|
if (invite) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
// unique constraints. Because `query_invite_find_unclaimed_match`
|
|
125
|
-
// returns at most one invite for the (username, email) tuple, two
|
|
126
|
-
// concurrent signups satisfying the same find share the same
|
|
127
|
-
// username and/or email — and the case-insensitive partial uniques
|
|
128
|
-
// on `account.username` / `account.email` (`ACCOUNT_USERNAME_CI_INDEX`
|
|
129
|
-
// / `ACCOUNT_EMAIL_INDEX`) fire on the second `query_create_account_with_actor`
|
|
130
|
-
// before the claim runs. The audit emit is kept for defense-in-depth
|
|
131
|
-
// in case those constraints are loosened or the find query starts
|
|
132
|
-
// returning multiple invites for a single signup tuple.
|
|
133
|
-
throw new SignupConflictError(ERROR_NO_MATCHING_INVITE);
|
|
134
|
-
}
|
|
168
|
+
// Guaranteed to succeed: FOR UPDATE held the row
|
|
169
|
+
// for the duration of the tx, so no concurrent
|
|
170
|
+
// claim could flip `claimed_at` between the find
|
|
171
|
+
// and this UPDATE.
|
|
172
|
+
await query_invite_claim_unscoped(tx_deps, invite.id, account.id);
|
|
135
173
|
}
|
|
136
174
|
await create_session_and_set_cookie({
|
|
137
175
|
keyring,
|
|
@@ -140,17 +178,18 @@ export const create_signup_route_specs = (deps, options) => {
|
|
|
140
178
|
account_id: account.id,
|
|
141
179
|
session_options,
|
|
142
180
|
});
|
|
143
|
-
return account;
|
|
181
|
+
return { account, actor };
|
|
144
182
|
});
|
|
145
183
|
}
|
|
146
184
|
catch (e) {
|
|
147
|
-
if (e instanceof
|
|
185
|
+
if (e instanceof NoMatchingInviteError) {
|
|
148
186
|
if (ip_rate_limiter && ip)
|
|
149
187
|
ip_rate_limiter.record(ip);
|
|
150
188
|
if (signup_account_rate_limiter)
|
|
151
189
|
signup_account_rate_limiter.record(account_key);
|
|
152
|
-
emit_failure_audit('
|
|
153
|
-
|
|
190
|
+
emit_failure_audit('no_match');
|
|
191
|
+
await delay;
|
|
192
|
+
return c.json({ error: ERROR_NO_MATCHING_INVITE }, 403);
|
|
154
193
|
}
|
|
155
194
|
// Unique constraint violation: username or email already exists.
|
|
156
195
|
if (is_pg_unique_violation(e)) {
|
|
@@ -159,8 +198,17 @@ export const create_signup_route_specs = (deps, options) => {
|
|
|
159
198
|
if (signup_account_rate_limiter)
|
|
160
199
|
signup_account_rate_limiter.record(account_key);
|
|
161
200
|
emit_failure_audit('signup_conflict');
|
|
201
|
+
await delay;
|
|
162
202
|
return c.json({ error: ERROR_SIGNUP_CONFLICT }, 409);
|
|
163
203
|
}
|
|
204
|
+
// Unclassified failure (e.g. session create error, Argon2
|
|
205
|
+
// fault on hash, DB outage mid-tx). Tx is rolled back so
|
|
206
|
+
// no account persists, but the *attempt* should leave a
|
|
207
|
+
// forensic trail — emit `outcome: 'failure'` with reason
|
|
208
|
+
// `internal_error` before rethrowing. 5xx responses are
|
|
209
|
+
// not floored: they aren't response-time-controlled
|
|
210
|
+
// enumeration oracles.
|
|
211
|
+
emit_failure_audit('internal_error');
|
|
164
212
|
throw e;
|
|
165
213
|
}
|
|
166
214
|
// Reset rate limiters on success
|
|
@@ -170,20 +218,23 @@ export const create_signup_route_specs = (deps, options) => {
|
|
|
170
218
|
signup_account_rate_limiter.reset(account_key);
|
|
171
219
|
deps.audit.emit(route, {
|
|
172
220
|
event_type: 'signup',
|
|
173
|
-
account_id: result.id,
|
|
221
|
+
account_id: result.account.id,
|
|
174
222
|
ip: get_client_ip(c),
|
|
175
223
|
metadata: invite ? { invite_id: invite.id, username } : { open_signup: true, username },
|
|
176
224
|
});
|
|
177
|
-
return c.json({
|
|
225
|
+
return c.json({
|
|
226
|
+
ok: true,
|
|
227
|
+
account: { id: result.account.id, username: result.account.username },
|
|
228
|
+
actor: { id: result.actor.id },
|
|
229
|
+
});
|
|
178
230
|
},
|
|
179
231
|
},
|
|
180
232
|
];
|
|
181
233
|
};
|
|
182
|
-
/**
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
234
|
+
/**
|
|
235
|
+
* Thrown inside the signup transaction to signal `ERROR_NO_MATCHING_INVITE`
|
|
236
|
+
* when the FOR UPDATE find returns no row (and `open_signup` is off).
|
|
237
|
+
* Caught by the handler to roll back the tx and emit the failure audit.
|
|
238
|
+
*/
|
|
239
|
+
class NoMatchingInviteError extends Error {
|
|
189
240
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create_db.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/create_db.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAC,EAAE,EAAE,MAAM,EAAC,MAAM,SAAS,CAAC;AAIxC,yCAAyC;AACzC,MAAM,WAAW,cAAc;IAC9B,EAAE,EAAE,EAAE,CAAC;IACP,iFAAiF;IACjF,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,SAAS,GAAU,cAAc,MAAM,KAAG,OAAO,CAAC,cAAc,
|
|
1
|
+
{"version":3,"file":"create_db.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/create_db.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAC,EAAE,EAAE,MAAM,EAAC,MAAM,SAAS,CAAC;AAIxC,yCAAyC;AACzC,MAAM,WAAW,cAAc;IAC9B,EAAE,EAAE,EAAE,CAAC;IACP,iFAAiF;IACjF,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,SAAS,GAAU,cAAc,MAAM,KAAG,OAAO,CAAC,cAAc,CA6C5E,CAAC"}
|
package/dist/db/create_db.js
CHANGED
|
@@ -32,6 +32,19 @@ import { create_pglite_db } from './db_pglite.js';
|
|
|
32
32
|
export const create_db = async (database_url) => {
|
|
33
33
|
if (database_url.startsWith('postgres://') || database_url.startsWith('postgresql://')) {
|
|
34
34
|
const { default: pg } = await import('pg');
|
|
35
|
+
// Parse int8 (BIGINT) as a JS number. pg defaults to returning int8
|
|
36
|
+
// as a string to avoid 2^53 precision loss; our only int8 column
|
|
37
|
+
// today (`audit_log.seq`) stays well under that bound, and reading
|
|
38
|
+
// as number keeps the wire shape uniform across the SERIAL→BIGSERIAL
|
|
39
|
+
// widening.
|
|
40
|
+
//
|
|
41
|
+
// CAVEAT: pg.types.setTypeParser mutates pg.types globally — every
|
|
42
|
+
// pg.Pool in the process inherits the coercion, including pools the
|
|
43
|
+
// consumer constructs against unrelated databases. Any future int8
|
|
44
|
+
// column that could legitimately exceed 2^53 (file sizes, byte
|
|
45
|
+
// offsets) will silently round; if one lands, localize via a
|
|
46
|
+
// per-pool `types` override instead of widening this global parser.
|
|
47
|
+
pg.types.setTypeParser(20, (val) => Number(val));
|
|
35
48
|
const pool = new pg.Pool({ connectionString: database_url });
|
|
36
49
|
const { db, close } = create_pg_db(pool);
|
|
37
50
|
return {
|
package/dist/dev/setup.d.ts
CHANGED
|
@@ -51,7 +51,7 @@ export interface ResetDbResult {
|
|
|
51
51
|
/** Options for `setup_env_file`. */
|
|
52
52
|
export interface SetupEnvOptions {
|
|
53
53
|
/**
|
|
54
|
-
* Extra env var replacements beyond the default `
|
|
54
|
+
* Extra env var replacements beyond the default `SECRET_FUZ_COOKIE_KEYS`.
|
|
55
55
|
*
|
|
56
56
|
* Keys are env var names, values are async generators.
|
|
57
57
|
* Replaces `^KEY=$` (empty value) patterns in the env file.
|
|
@@ -103,7 +103,7 @@ export declare const generate_random_key: (deps: CommandDeps) => Promise<string>
|
|
|
103
103
|
*/
|
|
104
104
|
export declare const read_env_var: (deps: Pick<FsReadDeps, "stat" | "read_text_file">, env_path: string, name: string) => Promise<string | undefined>;
|
|
105
105
|
/**
|
|
106
|
-
* Create an env file from its example template, auto-generating `
|
|
106
|
+
* Create an env file from its example template, auto-generating `SECRET_FUZ_COOKIE_KEYS`.
|
|
107
107
|
*
|
|
108
108
|
* If the file already exists, backfills any empty values that have generators.
|
|
109
109
|
* Idempotent — safe to re-run.
|
package/dist/dev/setup.js
CHANGED
|
@@ -69,7 +69,7 @@ export const read_env_var = async (deps, env_path, name) => {
|
|
|
69
69
|
};
|
|
70
70
|
// === Setup helpers ===
|
|
71
71
|
/**
|
|
72
|
-
* Create an env file from its example template, auto-generating `
|
|
72
|
+
* Create an env file from its example template, auto-generating `SECRET_FUZ_COOKIE_KEYS`.
|
|
73
73
|
*
|
|
74
74
|
* If the file already exists, backfills any empty values that have generators.
|
|
75
75
|
* Idempotent — safe to re-run.
|
|
@@ -84,9 +84,9 @@ export const read_env_var = async (deps, env_path, name) => {
|
|
|
84
84
|
export const setup_env_file = async (deps, env_path, example_path, options) => {
|
|
85
85
|
const log = options?.log ?? default_setup_logger;
|
|
86
86
|
const set_permissions = options?.set_permissions;
|
|
87
|
-
// build the full replacement map (
|
|
87
|
+
// build the full replacement map (SECRET_FUZ_COOKIE_KEYS + extras)
|
|
88
88
|
const replacements = {
|
|
89
|
-
|
|
89
|
+
SECRET_FUZ_COOKIE_KEYS: () => generate_random_key(deps),
|
|
90
90
|
...options?.replacements,
|
|
91
91
|
};
|
|
92
92
|
const stat = await deps.stat(env_path);
|
package/dist/env/resolve.d.ts
CHANGED
|
@@ -10,15 +10,32 @@
|
|
|
10
10
|
* - Easy to grep: `grep '\$\$'`
|
|
11
11
|
* - Fails loud if accidentally shell-processed (`$$`=PID in shell)
|
|
12
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
|
+
*
|
|
13
27
|
* @module
|
|
14
28
|
*/
|
|
15
29
|
import type { EnvDeps } from '../runtime/deps.js';
|
|
16
30
|
/**
|
|
17
31
|
* Resolve environment variable references in a string.
|
|
18
32
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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).
|
|
22
39
|
*
|
|
23
40
|
* @param runtime - runtime with `env_get` capability
|
|
24
41
|
* @param value - string that may contain `$$VAR$$` references
|
|
@@ -28,13 +45,20 @@ export declare const resolve_env_vars: (runtime: Pick<EnvDeps, "env_get">, value
|
|
|
28
45
|
/**
|
|
29
46
|
* Check if a string contains unresolved env var references.
|
|
30
47
|
*
|
|
48
|
+
* Escaped references (`\$$VAR$$`) do not count — they're literal text once
|
|
49
|
+
* resolved.
|
|
50
|
+
*
|
|
31
51
|
* @param value - string to check
|
|
32
|
-
* @returns `true` if string contains `$$VAR$$` patterns
|
|
52
|
+
* @returns `true` if string contains unescaped `$$VAR$$` patterns
|
|
33
53
|
*/
|
|
34
54
|
export declare const has_env_vars: (value: string) => boolean;
|
|
35
55
|
/**
|
|
36
56
|
* Get list of env var names referenced in a string.
|
|
37
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
|
+
*
|
|
38
62
|
* @param value - string to scan
|
|
39
63
|
* @returns array of variable names (without `$$` delimiters)
|
|
40
64
|
*/
|
|
@@ -50,7 +74,9 @@ export declare const resolve_env_vars_in_object: <T extends Record<string, unkno
|
|
|
50
74
|
/**
|
|
51
75
|
* Resolve env vars and throw if any are missing/empty.
|
|
52
76
|
*
|
|
53
|
-
* Use this for values that must be present.
|
|
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.
|
|
54
80
|
*
|
|
55
81
|
* @param runtime - runtime with `env_get` capability
|
|
56
82
|
* @param value - string with `$$VAR$$` references
|
|
@@ -67,15 +93,24 @@ export interface EnvVarRef {
|
|
|
67
93
|
name: string;
|
|
68
94
|
/** Path where the reference was found (e.g., `"target.host"`, `"resources[3].path"`). */
|
|
69
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;
|
|
70
102
|
}
|
|
71
103
|
/**
|
|
72
104
|
* Recursively scan an object for `$$VAR$$` env var references.
|
|
73
105
|
*
|
|
74
106
|
* Walks all string values in the object tree and extracts env var names
|
|
75
|
-
* with their path context for error reporting.
|
|
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.
|
|
76
111
|
*
|
|
77
112
|
* @param obj - object to scan (typically a config)
|
|
78
|
-
* @returns array of env var references with paths
|
|
113
|
+
* @returns array of env var references with paths and optional flags
|
|
79
114
|
*/
|
|
80
115
|
export declare const scan_env_vars: (obj: unknown) => Array<EnvVarRef>;
|
|
81
116
|
/**
|
|
@@ -97,6 +132,8 @@ export type EnvValidationResult = {
|
|
|
97
132
|
*
|
|
98
133
|
* Returns all missing refs (including duplicates by name). Grouping
|
|
99
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.
|
|
100
137
|
*
|
|
101
138
|
* @param runtime - runtime with `env_get` capability
|
|
102
139
|
* @param refs - env var references from `scan_env_vars`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/env/resolve.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"resolve.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/env/resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAiBhD;;;;;;;;;;;;;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,GAC5B;IAAC,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,IAAI,CAAA;CAAC,GACzB;IAAC,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,CAAA;CAAC,CAAC;AAE1C;;;;;;;;;;;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"}
|