@fuzdev/fuz_app 0.88.0 → 0.90.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/auth/account_action_specs.d.ts +1 -1
- package/dist/auth/account_route_schema.d.ts +12 -6
- package/dist/auth/account_route_schema.d.ts.map +1 -1
- package/dist/auth/account_route_schema.js +11 -5
- package/dist/auth/account_routes.d.ts +18 -4
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +13 -5
- package/dist/auth/account_schema.d.ts +3 -3
- package/dist/auth/admin_action_specs.d.ts +8 -8
- package/dist/auth/audit_log_schema.d.ts +1 -1
- package/dist/auth/invite_schema.d.ts +2 -2
- package/dist/auth/signup_route_schema.d.ts +2 -2
- package/dist/auth/signup_route_schema.d.ts.map +1 -1
- package/dist/auth/signup_route_schema.js +2 -2
- package/dist/primitive_schemas.d.ts +27 -1
- package/dist/primitive_schemas.d.ts.map +1 -1
- package/dist/primitive_schemas.js +55 -2
- package/dist/server/env.d.ts +1 -1
- package/dist/server/env.d.ts.map +1 -1
- package/dist/server/env.js +15 -3
- package/dist/testing/CLAUDE.md +62 -2
- package/dist/testing/app_server.d.ts +16 -5
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +3 -1
- package/dist/testing/cross_backend/body_size_smuggling.d.ts +12 -0
- package/dist/testing/cross_backend/body_size_smuggling.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size_smuggling.js +68 -41
- package/dist/testing/cross_backend/capabilities.d.ts +29 -0
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +15 -0
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +13 -0
- package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_spine_surface.js +7 -0
- package/dist/testing/cross_backend/identity_parity.d.ts +13 -0
- package/dist/testing/cross_backend/identity_parity.d.ts.map +1 -0
- package/dist/testing/cross_backend/identity_parity.js +465 -0
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +13 -6
- package/dist/testing/cross_backend/setup.d.ts +9 -0
- package/dist/testing/cross_backend/setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/setup.js +14 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.js +16 -1
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +13 -16
- package/dist/testing/schema_generators.d.ts.map +1 -1
- package/dist/testing/schema_generators.js +7 -0
- package/package.json +3 -3
|
@@ -99,7 +99,7 @@ export declare const account_verify_action_spec: {
|
|
|
99
99
|
output: z.ZodObject<{
|
|
100
100
|
id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
101
101
|
username: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
102
|
-
email: z.ZodNullable<z.
|
|
102
|
+
email: z.ZodNullable<z.ZodString>;
|
|
103
103
|
email_verified: z.ZodBoolean;
|
|
104
104
|
created_at: z.ZodString;
|
|
105
105
|
}, z.core.$strict>;
|
|
@@ -21,7 +21,7 @@ export declare const AccountStatusOutput: z.ZodObject<{
|
|
|
21
21
|
account: z.ZodObject<{
|
|
22
22
|
id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
23
23
|
username: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
24
|
-
email: z.ZodNullable<z.
|
|
24
|
+
email: z.ZodNullable<z.ZodString>;
|
|
25
25
|
email_verified: z.ZodBoolean;
|
|
26
26
|
created_at: z.ZodString;
|
|
27
27
|
}, z.core.$strict>;
|
|
@@ -84,10 +84,16 @@ export declare const DEFAULT_MAX_SESSIONS = 5;
|
|
|
84
84
|
/** Default maximum API tokens per account. */
|
|
85
85
|
export declare const DEFAULT_MAX_TOKENS = 10;
|
|
86
86
|
/**
|
|
87
|
-
* The `GET /
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
87
|
+
* The `GET /status` route shape minus its handler — pure hono-free data.
|
|
88
|
+
* `create_account_status_route_spec` spreads this and attaches the live handler
|
|
89
|
+
* (which reads the account id off the request context); surface generation
|
|
90
|
+
* spreads it with a stub handler.
|
|
91
|
+
*
|
|
92
|
+
* The path is **relative** like the sibling account shapes (`/login`,
|
|
93
|
+
* `/verify`), so it composes under `prefix_route_specs('/api/account', …)` into
|
|
94
|
+
* `/api/account/status`. `create_account_route_specs` bundles it (so every
|
|
95
|
+
* account surface serves `/status`, matching the Rust `account_router`);
|
|
96
|
+
* mirror Rust by mounting it as part of the account family, not separately.
|
|
91
97
|
*/
|
|
92
98
|
export declare const account_status_route_shape: {
|
|
93
99
|
method: "GET";
|
|
@@ -102,7 +108,7 @@ export declare const account_status_route_shape: {
|
|
|
102
108
|
account: z.ZodObject<{
|
|
103
109
|
id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
104
110
|
username: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
105
|
-
email: z.ZodNullable<z.
|
|
111
|
+
email: z.ZodNullable<z.ZodString>;
|
|
106
112
|
email_verified: z.ZodBoolean;
|
|
107
113
|
created_at: z.ZodString;
|
|
108
114
|
}, z.core.$strict>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account_route_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_route_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAKtB,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAQrD,kFAAkF;AAClF,eAAO,MAAM,kBAAkB,WAAW,CAAC;AAC3C,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEpE,4CAA4C;AAC5C,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;kBAI9B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,4EAA4E;AAC5E,eAAO,MAAM,iCAAiC;;;iBAG5C,CAAC;AACH,MAAM,MAAM,iCAAiC,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iCAAiC,CAAC,CAAC;AAElG,oFAAoF;AACpF,eAAO,MAAM,UAAU;;;kBAGrB,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAEpD,6EAA6E;AAC7E,eAAO,MAAM,WAAW;;kBAEtB,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,2EAA2E;AAC3E,eAAO,MAAM,WAAW,WAAW,CAAC;AACpC,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,wFAAwF;AACxF,eAAO,MAAM,YAAY;;;kBAGvB,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD,sHAAsH;AACtH,eAAO,MAAM,mBAAmB;;;kBAG9B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,uGAAuG;AACvG,eAAO,MAAM,oBAAoB;;;;kBAI/B,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAExE,4CAA4C;AAC5C,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC,8CAA8C;AAC9C,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC
|
|
1
|
+
{"version":3,"file":"account_route_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_route_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAKtB,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAQrD,kFAAkF;AAClF,eAAO,MAAM,kBAAkB,WAAW,CAAC;AAC3C,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEpE,4CAA4C;AAC5C,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;kBAI9B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,4EAA4E;AAC5E,eAAO,MAAM,iCAAiC;;;iBAG5C,CAAC;AACH,MAAM,MAAM,iCAAiC,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iCAAiC,CAAC,CAAC;AAElG,oFAAoF;AACpF,eAAO,MAAM,UAAU;;;kBAGrB,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAEpD,6EAA6E;AAC7E,eAAO,MAAM,WAAW;;kBAEtB,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,2EAA2E;AAC3E,eAAO,MAAM,WAAW,WAAW,CAAC;AACpC,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,wFAAwF;AACxF,eAAO,MAAM,YAAY;;;kBAGvB,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD,sHAAsH;AACtH,eAAO,MAAM,mBAAmB;;;kBAG9B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,uGAAuG;AACvG,eAAO,MAAM,oBAAoB;;;;kBAI/B,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAExE,4CAA4C;AAC5C,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC,8CAA8C;AAC9C,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAUD,CAAC;AAEvC,8EAA8E;AAC9E,MAAM,WAAW,wBAAwB;IACxC,8FAA8F;IAC9F,0BAA0B,EAAE,OAAO,CAAC;CACpC;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,GACvC,SAAS,wBAAwB,KAC/B,CACF,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,EAC1B,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,EAC1B,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,EAC1B,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,CAoD1B,CAAC"}
|
|
@@ -61,14 +61,20 @@ export const DEFAULT_MAX_SESSIONS = 5;
|
|
|
61
61
|
/** Default maximum API tokens per account. */
|
|
62
62
|
export const DEFAULT_MAX_TOKENS = 10;
|
|
63
63
|
/**
|
|
64
|
-
* The `GET /
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
64
|
+
* The `GET /status` route shape minus its handler — pure hono-free data.
|
|
65
|
+
* `create_account_status_route_spec` spreads this and attaches the live handler
|
|
66
|
+
* (which reads the account id off the request context); surface generation
|
|
67
|
+
* spreads it with a stub handler.
|
|
68
|
+
*
|
|
69
|
+
* The path is **relative** like the sibling account shapes (`/login`,
|
|
70
|
+
* `/verify`), so it composes under `prefix_route_specs('/api/account', …)` into
|
|
71
|
+
* `/api/account/status`. `create_account_route_specs` bundles it (so every
|
|
72
|
+
* account surface serves `/status`, matching the Rust `account_router`);
|
|
73
|
+
* mirror Rust by mounting it as part of the account family, not separately.
|
|
68
74
|
*/
|
|
69
75
|
export const account_status_route_shape = {
|
|
70
76
|
method: 'GET',
|
|
71
|
-
path: '/
|
|
77
|
+
path: '/status',
|
|
72
78
|
auth: { account: 'none', actor: 'none' },
|
|
73
79
|
description: 'Current account info (unauthenticated: 401 with bootstrap status)',
|
|
74
80
|
input: AccountStatusInput,
|
|
@@ -107,16 +107,30 @@ export interface AccountRouteOptions extends AuthSessionRouteOptions {
|
|
|
107
107
|
* `audit.on_event_chain`) runs.
|
|
108
108
|
*/
|
|
109
109
|
connection_closer?: ConnectionCloser | null;
|
|
110
|
+
/**
|
|
111
|
+
* Runtime bootstrap status for the bundled `GET /status` route — when
|
|
112
|
+
* `available`, its unauthenticated 401 carries `bootstrap_available: true`
|
|
113
|
+
* so a fresh frontend can route to the bootstrap flow. Pass
|
|
114
|
+
* `ctx.bootstrap_status` (the live `BootstrapStatus` ref) so the flag tracks
|
|
115
|
+
* the one-shot bootstrap completing. Omit when no bootstrap flow is wired —
|
|
116
|
+
* `/status` is still served, just without the flag.
|
|
117
|
+
*/
|
|
118
|
+
bootstrap_status?: {
|
|
119
|
+
available: boolean;
|
|
120
|
+
};
|
|
110
121
|
}
|
|
111
122
|
/**
|
|
112
123
|
* Create account route specs for session-based auth.
|
|
113
124
|
*
|
|
114
|
-
* The returned specs cover the
|
|
115
|
-
*
|
|
116
|
-
*
|
|
125
|
+
* The returned specs cover the REST flows that stay after the RPC migration:
|
|
126
|
+
* `/status` (account info + bootstrap availability), `/verify` (nginx
|
|
127
|
+
* `auth_request` shim), `/login`, `/logout`, `/password`. `/status` is bundled
|
|
128
|
+
* here (relative path, prefixed to `/api/account/status` by the caller) so
|
|
129
|
+
* every account surface serves it, matching the Rust `account_router`.
|
|
130
|
+
* Self-service session/token management is on `auth/account_actions.ts`.
|
|
117
131
|
*
|
|
118
132
|
* @param deps - stateless capabilities (keyring, password, log)
|
|
119
|
-
* @param options - per-factory configuration (session_options, ip_rate_limiter, login_account_rate_limiter)
|
|
133
|
+
* @param options - per-factory configuration (session_options, ip_rate_limiter, login_account_rate_limiter, bootstrap_status)
|
|
120
134
|
* @returns route specs (not yet applied to Hono)
|
|
121
135
|
*/
|
|
122
136
|
export declare const create_account_route_specs: (deps: RouteFactoryDeps, options: AccountRouteOptions) => Array<RouteSpec>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,qBAAqB,CAAC;AA4BxD,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;AAChD,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,iCAAiC,CAAC;AAGtE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gCAAgC,GAAI,UAAU,oBAAoB,KAAG,SA4EhF,CAAC;AAEH,iDAAiD;AACjD,MAAM,WAAW,oBAAoB;IACpC,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8FAA8F;IAC9F,gBAAgB,CAAC,EAAE;QAAC,SAAS,EAAE,OAAO,CAAA;KAAC,CAAC;CACxC;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,MAAM,CAAC;AAE/C;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,KAAK,CAAC;AAQ/C;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACvC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,kFAAkF;IAClF,eAAe,EAAE,WAAW,GAAG,IAAI,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,mBAAoB,SAAQ,uBAAuB;IACnE,4FAA4F;IAC5F,0BAA0B,EAAE,WAAW,GAAG,IAAI,CAAC;IAC/C,2FAA2F;IAC3F,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;OAOG;IACH,iBAAiB,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"account_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,qBAAqB,CAAC;AA4BxD,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;AAChD,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,iCAAiC,CAAC;AAGtE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gCAAgC,GAAI,UAAU,oBAAoB,KAAG,SA4EhF,CAAC;AAEH,iDAAiD;AACjD,MAAM,WAAW,oBAAoB;IACpC,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8FAA8F;IAC9F,gBAAgB,CAAC,EAAE;QAAC,SAAS,EAAE,OAAO,CAAA;KAAC,CAAC;CACxC;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,MAAM,CAAC;AAE/C;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,KAAK,CAAC;AAQ/C;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACvC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,kFAAkF;IAClF,eAAe,EAAE,WAAW,GAAG,IAAI,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,mBAAoB,SAAQ,uBAAuB;IACnE,4FAA4F;IAC5F,0BAA0B,EAAE,WAAW,GAAG,IAAI,CAAC;IAC/C,2FAA2F;IAC3F,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;OAOG;IACH,iBAAiB,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC5C;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE;QAAC,SAAS,EAAE,OAAO,CAAA;KAAC,CAAC;CACxC;AAID;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,0BAA0B,GACtC,MAAM,gBAAgB,EACtB,SAAS,mBAAmB,KAC1B,KAAK,CAAC,SAAS,CAwRjB,CAAC"}
|
|
@@ -147,21 +147,29 @@ const login_fail_delay = (floor_ms, jitter_ms) => {
|
|
|
147
147
|
/**
|
|
148
148
|
* Create account route specs for session-based auth.
|
|
149
149
|
*
|
|
150
|
-
* The returned specs cover the
|
|
151
|
-
*
|
|
152
|
-
*
|
|
150
|
+
* The returned specs cover the REST flows that stay after the RPC migration:
|
|
151
|
+
* `/status` (account info + bootstrap availability), `/verify` (nginx
|
|
152
|
+
* `auth_request` shim), `/login`, `/logout`, `/password`. `/status` is bundled
|
|
153
|
+
* here (relative path, prefixed to `/api/account/status` by the caller) so
|
|
154
|
+
* every account surface serves it, matching the Rust `account_router`.
|
|
155
|
+
* Self-service session/token management is on `auth/account_actions.ts`.
|
|
153
156
|
*
|
|
154
157
|
* @param deps - stateless capabilities (keyring, password, log)
|
|
155
|
-
* @param options - per-factory configuration (session_options, ip_rate_limiter, login_account_rate_limiter)
|
|
158
|
+
* @param options - per-factory configuration (session_options, ip_rate_limiter, login_account_rate_limiter, bootstrap_status)
|
|
156
159
|
* @returns route specs (not yet applied to Hono)
|
|
157
160
|
*/
|
|
158
161
|
export const create_account_route_specs = (deps, options) => {
|
|
159
162
|
const { keyring, password } = deps;
|
|
160
|
-
const { session_options, ip_rate_limiter, login_account_rate_limiter, max_sessions = DEFAULT_MAX_SESSIONS, login_fail_floor_ms = DEFAULT_LOGIN_FAIL_FLOOR_MS, login_fail_jitter_ms = DEFAULT_LOGIN_FAIL_JITTER_MS, connection_closer = null, } = options;
|
|
163
|
+
const { session_options, ip_rate_limiter, login_account_rate_limiter, max_sessions = DEFAULT_MAX_SESSIONS, login_fail_floor_ms = DEFAULT_LOGIN_FAIL_FLOOR_MS, login_fail_jitter_ms = DEFAULT_LOGIN_FAIL_JITTER_MS, connection_closer = null, bootstrap_status, } = options;
|
|
161
164
|
const [verify_shape, login_shape, logout_shape, password_shape] = create_account_route_shapes({
|
|
162
165
|
login_account_rate_limited: login_account_rate_limiter !== null,
|
|
163
166
|
});
|
|
164
167
|
return [
|
|
168
|
+
// `/status` is bundled into the account family (relative path, prefixed
|
|
169
|
+
// to `/api/account/status` by the caller) so every account surface serves
|
|
170
|
+
// it — matching the Rust `account_router`. The standalone
|
|
171
|
+
// `create_account_status_route_spec` stays the building block.
|
|
172
|
+
create_account_status_route_spec({ bootstrap_status }),
|
|
165
173
|
{
|
|
166
174
|
...verify_shape,
|
|
167
175
|
handler: (c) => {
|
|
@@ -124,7 +124,7 @@ export interface ApiToken {
|
|
|
124
124
|
export declare const SessionAccountJson: z.ZodObject<{
|
|
125
125
|
id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
126
126
|
username: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
127
|
-
email: z.ZodNullable<z.
|
|
127
|
+
email: z.ZodNullable<z.ZodString>;
|
|
128
128
|
email_verified: z.ZodBoolean;
|
|
129
129
|
created_at: z.ZodString;
|
|
130
130
|
}, z.core.$strict>;
|
|
@@ -170,7 +170,7 @@ export type ActorSummaryJson = z.infer<typeof ActorSummaryJson>;
|
|
|
170
170
|
export declare const AdminAccountJson: z.ZodObject<{
|
|
171
171
|
id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
172
172
|
username: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
173
|
-
email: z.ZodNullable<z.
|
|
173
|
+
email: z.ZodNullable<z.ZodString>;
|
|
174
174
|
email_verified: z.ZodBoolean;
|
|
175
175
|
created_at: z.ZodString;
|
|
176
176
|
updated_at: z.ZodString;
|
|
@@ -207,7 +207,7 @@ export declare const AdminAccountEntryJson: z.ZodObject<{
|
|
|
207
207
|
account: z.ZodObject<{
|
|
208
208
|
id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
209
209
|
username: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
210
|
-
email: z.ZodNullable<z.
|
|
210
|
+
email: z.ZodNullable<z.ZodString>;
|
|
211
211
|
email_verified: z.ZodBoolean;
|
|
212
212
|
created_at: z.ZodString;
|
|
213
213
|
updated_at: z.ZodString;
|
|
@@ -37,7 +37,7 @@ export declare const AdminAccountListOutput: z.ZodObject<{
|
|
|
37
37
|
account: z.ZodObject<{
|
|
38
38
|
id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
39
39
|
username: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
40
|
-
email: z.ZodNullable<z.
|
|
40
|
+
email: z.ZodNullable<z.ZodString>;
|
|
41
41
|
email_verified: z.ZodBoolean;
|
|
42
42
|
created_at: z.ZodString;
|
|
43
43
|
updated_at: z.ZodString;
|
|
@@ -184,7 +184,7 @@ export declare const AuditLogRoleGrantHistoryOutput: z.ZodObject<{
|
|
|
184
184
|
export type AuditLogRoleGrantHistoryOutput = z.infer<typeof AuditLogRoleGrantHistoryOutput>;
|
|
185
185
|
/** Input for `invite_create`. At least one of `email` / `username` must be provided. */
|
|
186
186
|
export declare const InviteCreateInput: z.ZodObject<{
|
|
187
|
-
email: z.ZodOptional<z.ZodNullable<z.
|
|
187
|
+
email: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
188
188
|
username: z.ZodOptional<z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
|
|
189
189
|
acting: z.ZodOptional<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
|
|
190
190
|
}, z.core.$strict>;
|
|
@@ -194,7 +194,7 @@ export declare const InviteCreateOutput: z.ZodObject<{
|
|
|
194
194
|
ok: z.ZodLiteral<true>;
|
|
195
195
|
invite: z.ZodObject<{
|
|
196
196
|
id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
197
|
-
email: z.ZodNullable<z.
|
|
197
|
+
email: z.ZodNullable<z.ZodString>;
|
|
198
198
|
username: z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
199
199
|
claimed_by: z.ZodNullable<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
|
|
200
200
|
claimed_at: z.ZodNullable<z.ZodString>;
|
|
@@ -212,7 +212,7 @@ export type InviteListInput = z.infer<typeof InviteListInput>;
|
|
|
212
212
|
export declare const InviteListOutput: z.ZodObject<{
|
|
213
213
|
invites: z.ZodArray<z.ZodObject<{
|
|
214
214
|
id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
215
|
-
email: z.ZodNullable<z.
|
|
215
|
+
email: z.ZodNullable<z.ZodString>;
|
|
216
216
|
username: z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
217
217
|
claimed_by: z.ZodNullable<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
|
|
218
218
|
claimed_at: z.ZodNullable<z.ZodString>;
|
|
@@ -368,7 +368,7 @@ export declare const admin_account_list_action_spec: {
|
|
|
368
368
|
account: z.ZodObject<{
|
|
369
369
|
id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
370
370
|
username: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
371
|
-
email: z.ZodNullable<z.
|
|
371
|
+
email: z.ZodNullable<z.ZodString>;
|
|
372
372
|
email_verified: z.ZodBoolean;
|
|
373
373
|
created_at: z.ZodString;
|
|
374
374
|
updated_at: z.ZodString;
|
|
@@ -590,7 +590,7 @@ export declare const invite_create_action_spec: {
|
|
|
590
590
|
};
|
|
591
591
|
side_effects: true;
|
|
592
592
|
input: z.ZodObject<{
|
|
593
|
-
email: z.ZodOptional<z.ZodNullable<z.
|
|
593
|
+
email: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
594
594
|
username: z.ZodOptional<z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
|
|
595
595
|
acting: z.ZodOptional<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
|
|
596
596
|
}, z.core.$strict>;
|
|
@@ -598,7 +598,7 @@ export declare const invite_create_action_spec: {
|
|
|
598
598
|
ok: z.ZodLiteral<true>;
|
|
599
599
|
invite: z.ZodObject<{
|
|
600
600
|
id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
601
|
-
email: z.ZodNullable<z.
|
|
601
|
+
email: z.ZodNullable<z.ZodString>;
|
|
602
602
|
username: z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
603
603
|
claimed_by: z.ZodNullable<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
|
|
604
604
|
claimed_at: z.ZodNullable<z.ZodString>;
|
|
@@ -632,7 +632,7 @@ export declare const invite_list_action_spec: {
|
|
|
632
632
|
output: z.ZodObject<{
|
|
633
633
|
invites: z.ZodArray<z.ZodObject<{
|
|
634
634
|
id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
635
|
-
email: z.ZodNullable<z.
|
|
635
|
+
email: z.ZodNullable<z.ZodString>;
|
|
636
636
|
username: z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
637
637
|
claimed_by: z.ZodNullable<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
|
|
638
638
|
claimed_at: z.ZodNullable<z.ZodString>;
|
|
@@ -83,7 +83,7 @@ export declare const audit_metadata_schemas: Readonly<{
|
|
|
83
83
|
invite_id: z.ZodOptional<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
|
|
84
84
|
open_signup: z.ZodOptional<z.ZodBoolean>;
|
|
85
85
|
reason: z.ZodOptional<z.ZodString>;
|
|
86
|
-
email: z.ZodOptional<z.
|
|
86
|
+
email: z.ZodOptional<z.ZodString>;
|
|
87
87
|
}, z.core.$loose>;
|
|
88
88
|
password_change: z.ZodNullable<z.ZodObject<{
|
|
89
89
|
sessions_revoked: z.ZodOptional<z.ZodNumber>;
|
|
@@ -22,7 +22,7 @@ export interface Invite {
|
|
|
22
22
|
/** Zod schema for client-safe invite data. */
|
|
23
23
|
export declare const InviteJson: z.ZodObject<{
|
|
24
24
|
id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
25
|
-
email: z.ZodNullable<z.
|
|
25
|
+
email: z.ZodNullable<z.ZodString>;
|
|
26
26
|
username: z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
27
27
|
claimed_by: z.ZodNullable<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
|
|
28
28
|
claimed_at: z.ZodNullable<z.ZodString>;
|
|
@@ -33,7 +33,7 @@ export type InviteJson = z.infer<typeof InviteJson>;
|
|
|
33
33
|
/** Zod schema for invite data with resolved creator/claimer usernames. */
|
|
34
34
|
export declare const InviteWithUsernamesJson: z.ZodObject<{
|
|
35
35
|
id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
36
|
-
email: z.ZodNullable<z.
|
|
36
|
+
email: z.ZodNullable<z.ZodString>;
|
|
37
37
|
username: z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
38
38
|
claimed_by: z.ZodNullable<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
|
|
39
39
|
claimed_at: z.ZodNullable<z.ZodString>;
|
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { z } from 'zod';
|
|
15
15
|
import type { RouteSpec } from '../http/route_spec.js';
|
|
16
|
-
/** Input for `POST /signup`. `email` is optional and must match any referenced invite. */
|
|
16
|
+
/** Input for `POST /signup`. `email` is optional (absent or `null` = no email) and must match any referenced invite. */
|
|
17
17
|
export declare const SignupInput: z.ZodObject<{
|
|
18
18
|
username: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
19
19
|
password: z.ZodString;
|
|
20
|
-
email: z.ZodOptional<z.
|
|
20
|
+
email: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
21
21
|
}, z.core.$strict>;
|
|
22
22
|
export type SignupInput = z.infer<typeof SignupInput>;
|
|
23
23
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signup_route_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/signup_route_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAKtB,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAQrD,
|
|
1
|
+
{"version":3,"file":"signup_route_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/signup_route_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAKtB,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAQrD,wHAAwH;AACxH,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,4EAA4E;AAC5E,MAAM,WAAW,uBAAuB;IACvC,iFAAiF;IACjF,2BAA2B,EAAE,OAAO,CAAC;CACrC;AAED;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,GACrC,SAAS,uBAAuB,KAC9B,IAAI,CAAC,SAAS,EAAE,SAAS,CAgB1B,CAAC"}
|
|
@@ -16,11 +16,11 @@ import { Uuid } from '@fuzdev/fuz_util/id.js';
|
|
|
16
16
|
import { Username, Email } from '../primitive_schemas.js';
|
|
17
17
|
import { Password } from './password.js';
|
|
18
18
|
import { ERROR_NO_MATCHING_INVITE, ERROR_SIGNUP_CONFLICT, ERROR_INVALID_JSON_BODY, ERROR_INVALID_REQUEST_BODY, } from '../http/error_schemas.js';
|
|
19
|
-
/** Input for `POST /signup`. `email` is optional and must match any referenced invite. */
|
|
19
|
+
/** Input for `POST /signup`. `email` is optional (absent or `null` = no email) and must match any referenced invite. */
|
|
20
20
|
export const SignupInput = z.strictObject({
|
|
21
21
|
username: Username,
|
|
22
22
|
password: Password,
|
|
23
|
-
email: Email.
|
|
23
|
+
email: Email.nullish(),
|
|
24
24
|
});
|
|
25
25
|
/**
|
|
26
26
|
* Output for `POST /signup`.
|
|
@@ -40,16 +40,42 @@ export type Username = z.infer<typeof Username>;
|
|
|
40
40
|
* per-account rate-limit key and DB lookup see a uniform value
|
|
41
41
|
* regardless of casing or surrounding whitespace. Mirrors the storage
|
|
42
42
|
* canonicalization on `Username` so submission and storage agree.
|
|
43
|
+
*
|
|
44
|
+
* The trailing `.refine` rejects a whitespace-only identifier: `.min(1)`
|
|
45
|
+
* runs on the raw string (so `" "` passes it), and without the post-trim
|
|
46
|
+
* check the value would canonicalize to `""` and fall through to a
|
|
47
|
+
* lookup-miss 401 instead of a 400 — an empty identifier is malformed
|
|
48
|
+
* input, not a wrong credential. Keeps the Rust spine's
|
|
49
|
+
* `account_login` (which rejects empty-after-trim) in parity.
|
|
43
50
|
*/
|
|
44
51
|
export declare const UsernameProvided: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
45
52
|
export type UsernameProvided = z.infer<typeof UsernameProvided>;
|
|
53
|
+
/**
|
|
54
|
+
* Maximum email length in **bytes** — RFC 5321 §4.5.3.1.3 path-length limit
|
|
55
|
+
* (the limit is octets). `Email` bounds the UTF-8 byte length, matching the
|
|
56
|
+
* Rust spine's `s.len()` check; for an all-ASCII address bytes == characters.
|
|
57
|
+
*/
|
|
58
|
+
export declare const EMAIL_LENGTH_MAX = 254;
|
|
46
59
|
/**
|
|
47
60
|
* Email validation. Lives here rather than `@fuzdev/fuz_util` because every
|
|
48
61
|
* current consumer pairs it with `Username` (signup, invites, audit log) —
|
|
49
62
|
* keeping the two together avoids a cross-package import for the
|
|
50
63
|
* identity-primitive bundle. Promote to fuz_util if a non-identity consumer
|
|
51
64
|
* surfaces.
|
|
65
|
+
*
|
|
66
|
+
* Deliberately permissive — a structural shape check (`EMAIL_REGEX` plus the
|
|
67
|
+
* `EMAIL_LENGTH_MAX` byte bound), not RFC 5322 conformance or deliverability
|
|
68
|
+
* (real delivery is proven by a confirmation email). Replaces Zod's stricter
|
|
69
|
+
* `z.email()` so the rule is one explicit regex the Rust spine's
|
|
70
|
+
* `is_valid_email` mirrors; `z.email()`'s internal regex (2+ char TLD, no
|
|
71
|
+
* consecutive dots) was brittle to keep in cross-impl parity and rejected
|
|
72
|
+
* addresses like `a@b.c`. The length bound is the UTF-8 **byte** count (RFC
|
|
73
|
+
* 5321 measures octets), so a multibyte address is bounded identically to the
|
|
74
|
+
* Rust spine's `s.len()` rather than diverging on JS's UTF-16 `.length`. No
|
|
75
|
+
* transform: case is preserved and surrounding whitespace is rejected (not
|
|
76
|
+
* trimmed), so storage is verbatim and the case-insensitive lookup rides the
|
|
77
|
+
* DB-side `LOWER(email)`.
|
|
52
78
|
*/
|
|
53
|
-
export declare const Email: z.
|
|
79
|
+
export declare const Email: z.ZodString;
|
|
54
80
|
export type Email = z.infer<typeof Email>;
|
|
55
81
|
//# sourceMappingURL=primitive_schemas.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"primitive_schemas.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/primitive_schemas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAItB,2EAA2E;AAC3E,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC,wDAAwD;AACxD,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC,gKAAgK;AAChK,eAAO,MAAM,4BAA4B,MAAM,CAAC;AAEhD;;;;;;;;;GASG;AACH,eAAO,MAAM,QAAQ,wDAKc,CAAC;AACpC,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,QAAQ,CAAC,CAAC;AAEhD
|
|
1
|
+
{"version":3,"file":"primitive_schemas.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/primitive_schemas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAItB,2EAA2E;AAC3E,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC,wDAAwD;AACxD,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC,gKAAgK;AAChK,eAAO,MAAM,4BAA4B,MAAM,CAAC;AAEhD;;;;;;;;;GASG;AACH,eAAO,MAAM,QAAQ,wDAKc,CAAC;AACpC,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,QAAQ,CAAC,CAAC;AAEhD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,gBAAgB,wDAKmE,CAAC;AACjG,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAqBpC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,KAAK,aAYhB,CAAC;AACH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,KAAK,CAAC,CAAC"}
|
|
@@ -45,17 +45,70 @@ export const Username = z
|
|
|
45
45
|
* per-account rate-limit key and DB lookup see a uniform value
|
|
46
46
|
* regardless of casing or surrounding whitespace. Mirrors the storage
|
|
47
47
|
* canonicalization on `Username` so submission and storage agree.
|
|
48
|
+
*
|
|
49
|
+
* The trailing `.refine` rejects a whitespace-only identifier: `.min(1)`
|
|
50
|
+
* runs on the raw string (so `" "` passes it), and without the post-trim
|
|
51
|
+
* check the value would canonicalize to `""` and fall through to a
|
|
52
|
+
* lookup-miss 401 instead of a 400 — an empty identifier is malformed
|
|
53
|
+
* input, not a wrong credential. Keeps the Rust spine's
|
|
54
|
+
* `account_login` (which rejects empty-after-trim) in parity.
|
|
48
55
|
*/
|
|
49
56
|
export const UsernameProvided = z
|
|
50
57
|
.string()
|
|
51
58
|
.min(1)
|
|
52
59
|
.max(USERNAME_PROVIDED_LENGTH_MAX)
|
|
53
|
-
.transform((s) => s.trim().toLowerCase())
|
|
60
|
+
.transform((s) => s.trim().toLowerCase())
|
|
61
|
+
.refine((s) => s.length > 0, { message: 'Username must not be empty after trimming whitespace' });
|
|
62
|
+
/**
|
|
63
|
+
* Maximum email length in **bytes** — RFC 5321 §4.5.3.1.3 path-length limit
|
|
64
|
+
* (the limit is octets). `Email` bounds the UTF-8 byte length, matching the
|
|
65
|
+
* Rust spine's `s.len()` check; for an all-ASCII address bytes == characters.
|
|
66
|
+
*/
|
|
67
|
+
export const EMAIL_LENGTH_MAX = 254;
|
|
68
|
+
/**
|
|
69
|
+
* Loose email shape `local@domain.tld`: a non-empty local part, exactly one
|
|
70
|
+
* `@`, a domain with an interior dot (a non-empty label on each side), and
|
|
71
|
+
* no whitespace. Accepts `a@b.c` (single-char TLDs are fine) and
|
|
72
|
+
* `user+tag@a.co`; rejects `notanemail`, `@x.com`, `user@`, `user@host`, and
|
|
73
|
+
* any whitespace.
|
|
74
|
+
*
|
|
75
|
+
* The negated class excludes `\s` **plus** U+0085 (NEL): JS's `\s` omits
|
|
76
|
+
* U+0085 but Unicode `White_Space` (Rust's `char::is_whitespace`) includes
|
|
77
|
+
* it, so excluding it keeps the whitespace set identical across the twins.
|
|
78
|
+
* (The Rust side correspondingly also rejects U+FEFF, which JS `\s` treats as
|
|
79
|
+
* whitespace but Unicode `White_Space` does not — so both spines reject
|
|
80
|
+
* exactly `White_Space ∪ {U+FEFF}`.)
|
|
81
|
+
*/
|
|
82
|
+
const EMAIL_REGEX = /^[^\s\u0085@]+@[^\s\u0085@]+\.[^\s\u0085@]+$/;
|
|
83
|
+
/** UTF-8 encoder for the `EMAIL_LENGTH_MAX` byte (octet) bound. */
|
|
84
|
+
const utf8_encoder = new TextEncoder();
|
|
54
85
|
/**
|
|
55
86
|
* Email validation. Lives here rather than `@fuzdev/fuz_util` because every
|
|
56
87
|
* current consumer pairs it with `Username` (signup, invites, audit log) —
|
|
57
88
|
* keeping the two together avoids a cross-package import for the
|
|
58
89
|
* identity-primitive bundle. Promote to fuz_util if a non-identity consumer
|
|
59
90
|
* surfaces.
|
|
91
|
+
*
|
|
92
|
+
* Deliberately permissive — a structural shape check (`EMAIL_REGEX` plus the
|
|
93
|
+
* `EMAIL_LENGTH_MAX` byte bound), not RFC 5322 conformance or deliverability
|
|
94
|
+
* (real delivery is proven by a confirmation email). Replaces Zod's stricter
|
|
95
|
+
* `z.email()` so the rule is one explicit regex the Rust spine's
|
|
96
|
+
* `is_valid_email` mirrors; `z.email()`'s internal regex (2+ char TLD, no
|
|
97
|
+
* consecutive dots) was brittle to keep in cross-impl parity and rejected
|
|
98
|
+
* addresses like `a@b.c`. The length bound is the UTF-8 **byte** count (RFC
|
|
99
|
+
* 5321 measures octets), so a multibyte address is bounded identically to the
|
|
100
|
+
* Rust spine's `s.len()` rather than diverging on JS's UTF-16 `.length`. No
|
|
101
|
+
* transform: case is preserved and surrounding whitespace is rejected (not
|
|
102
|
+
* trimmed), so storage is verbatim and the case-insensitive lookup rides the
|
|
103
|
+
* DB-side `LOWER(email)`.
|
|
60
104
|
*/
|
|
61
|
-
export const Email = z
|
|
105
|
+
export const Email = z
|
|
106
|
+
.string()
|
|
107
|
+
.regex(EMAIL_REGEX)
|
|
108
|
+
// `s.length` (UTF-16 units) is a cheap upper bound that short-circuits the
|
|
109
|
+
// `encode` on absurd inputs: a valid (<= 254-byte) address is necessarily
|
|
110
|
+
// <= 254 code units, so this never rejects one — the byte count is the real
|
|
111
|
+
// RFC-5321 octet bound, matching the Rust spine's `s.len()`.
|
|
112
|
+
.refine((s) => s.length <= EMAIL_LENGTH_MAX && utf8_encoder.encode(s).length <= EMAIL_LENGTH_MAX, {
|
|
113
|
+
message: `Email must be at most ${EMAIL_LENGTH_MAX} bytes`,
|
|
114
|
+
});
|
package/dist/server/env.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export declare const BaseServerEnv: z.ZodObject<{
|
|
|
33
33
|
PUBLIC_FUZ_CONTACT_EMAIL: z.ZodOptional<z.ZodUnion<readonly [z.ZodEmail, z.ZodLiteral<"">]>>;
|
|
34
34
|
FUZ_BOOTSTRAP_TOKEN_PATH: z.ZodOptional<z.ZodString>;
|
|
35
35
|
SMTP_HOST: z.ZodOptional<z.ZodString>;
|
|
36
|
-
SMTP_USER: z.ZodOptional<z.
|
|
36
|
+
SMTP_USER: z.ZodOptional<z.ZodString>;
|
|
37
37
|
SMTP_PASSWORD: z.ZodOptional<z.ZodString>;
|
|
38
38
|
FUZ_FACTS_DIR: z.ZodDefault<z.ZodString>;
|
|
39
39
|
FUZ_FACTS_X_ACCEL_REDIRECT_PREFIX: z.ZodOptional<z.ZodString>;
|
package/dist/server/env.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,EAA2B,KAAK,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAG1E;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"env.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,EAA2B,KAAK,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAG1E;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;kBAoDxB,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAChC,EAAE,EAAE,IAAI,CAAC;IACT,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC/B,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACrC,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,wBAAwB,GAAG,qBAAqB,CAAC;IACxD,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACtB;AAED,MAAM,MAAM,sBAAsB,GAAG,gBAAgB,GAAG,qBAAqB,CAAC;AAE9E;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAAI,KAAK,aAAa,KAAG,sBA4BxD,CAAC"}
|
package/dist/server/env.js
CHANGED
|
@@ -21,7 +21,13 @@ import { parse_allowed_origins } from '../http/origin.js';
|
|
|
21
21
|
*/
|
|
22
22
|
export const BaseServerEnv = z.strictObject({
|
|
23
23
|
NODE_ENV: z.enum(['development', 'production']).meta({ description: 'Runtime environment mode' }),
|
|
24
|
-
PORT: z.coerce
|
|
24
|
+
PORT: z.coerce
|
|
25
|
+
.number()
|
|
26
|
+
.int()
|
|
27
|
+
.min(1)
|
|
28
|
+
.max(65535)
|
|
29
|
+
.default(4040)
|
|
30
|
+
.meta({ description: 'HTTP server port' }),
|
|
25
31
|
HOST: z.string().default('localhost').meta({ description: 'HTTP server bind address' }),
|
|
26
32
|
DATABASE_URL: z.string().min(1).meta({
|
|
27
33
|
description: 'Database URL (postgres://, file://, or memory://)',
|
|
@@ -45,10 +51,16 @@ export const BaseServerEnv = z.strictObject({
|
|
|
45
51
|
.optional()
|
|
46
52
|
.meta({ description: 'Path to one-shot admin bootstrap token', sensitivity: 'secret' }),
|
|
47
53
|
SMTP_HOST: z.string().optional().meta({ description: 'SMTP server hostname' }),
|
|
54
|
+
// SMTP usernames are frequently not emails (SendGrid uses "apikey",
|
|
55
|
+
// AWS SES / Postmark use token-style credentials), so validate as a plain
|
|
56
|
+
// string — `z.email()` here would reject valid provider configs. Marked
|
|
57
|
+
// `secret`: those token-style usernames are credentials (Postmark reuses
|
|
58
|
+
// the server token as both user and password), so the value is masked in
|
|
59
|
+
// the startup summary / logs like `SMTP_PASSWORD`.
|
|
48
60
|
SMTP_USER: z
|
|
49
|
-
.
|
|
61
|
+
.string()
|
|
50
62
|
.optional()
|
|
51
|
-
.meta({ description: 'SMTP authentication username' }),
|
|
63
|
+
.meta({ description: 'SMTP authentication username', sensitivity: 'secret' }),
|
|
52
64
|
SMTP_PASSWORD: z
|
|
53
65
|
.string()
|
|
54
66
|
.optional()
|
package/dist/testing/CLAUDE.md
CHANGED
|
@@ -840,7 +840,12 @@ source of truth for wire-shape conformance.
|
|
|
840
840
|
whose `RoleSpec.grant_paths` don't include `'admin'` reject loudly at
|
|
841
841
|
offer-create time (`ERROR_ROLE_GRANT_OFFER_ROLE_NOT_GRANTABLE`); those
|
|
842
842
|
roles must be seeded via `extra_accounts` at bootstrap-equivalent time
|
|
843
|
-
instead.
|
|
843
|
+
instead. `create_account({email})` threads an email onto the signup body
|
|
844
|
+
(the username-scoped invite matches regardless of email), so the account
|
|
845
|
+
lands with an email set — the hook the username-or-email login parity suite
|
|
846
|
+
uses; the in-process path sets it directly on the inserted row. Must be a
|
|
847
|
+
valid, per-test-unique `Email` (the `LOWER(email)` partial-unique index
|
|
848
|
+
rejects collisions). Caller-supplied `username`s pass through _as-is_ now that the DB
|
|
844
849
|
wipes between tests — hardcoded names like `'user_two'` work and the
|
|
845
850
|
earlier uniquification prefix is gone. Every `TestFixture` also exposes
|
|
846
851
|
`fresh_transport({origin?: string | null})` — cookie-jar-free probe;
|
|
@@ -858,7 +863,7 @@ source of truth for wire-shape conformance.
|
|
|
858
863
|
- `testing/cross_backend/capabilities.ts` — `BackendCapabilities` vocabulary
|
|
859
864
|
(`bearer_auth` / `trusted_proxy` / `login_rate_limit` / `ws` / `sse` /
|
|
860
865
|
`cell_crud` / `cell_relations` / `account_lifecycle` / `fact_serving` /
|
|
861
|
-
`ready`),
|
|
866
|
+
`ready` / `account_status` / `oversized_reject_closes_connection`),
|
|
862
867
|
`test_if(cond, name, fn)`
|
|
863
868
|
for capability-gated cases, and `in_process_capabilities` preset. `cell_crud`
|
|
864
869
|
gates the CRUD parity suite, `cell_relations` the relation / ACL / audit
|
|
@@ -875,6 +880,14 @@ source of truth for wire-shape conformance.
|
|
|
875
880
|
(anonymous `GET /ready` → `200 {ready: true}` on a clean spine bootstrap);
|
|
876
881
|
like cells/sse the `/ready` deploy gate stays off the declared surface, `true`
|
|
877
882
|
on every spine that live-mounts it over the shared `expected_schema.json`.
|
|
883
|
+
`account_status` gates the integration suite's `account status response body`
|
|
884
|
+
case — `GET /api/account/status` is bundled into `create_account_route_specs`,
|
|
885
|
+
so every spine serves it (`true`); the gate fails loud if a declaring backend
|
|
886
|
+
doesn't mount it (replacing the old runtime 404-sniff-skip).
|
|
887
|
+
`oversized_reject_closes_connection` gates the strong half of the body-size
|
|
888
|
+
smuggling probe (`true` Node/Deno/Rust — they close on an oversized reject;
|
|
889
|
+
`false` Bun — `Bun.serve` drains + keepalives but frames correctly, so the
|
|
890
|
+
suite's no-desync half still runs).
|
|
878
891
|
|
|
879
892
|
### `cross_backend/standard.ts` — `describe_standard_cross_process_tests`
|
|
880
893
|
|
|
@@ -1239,6 +1252,53 @@ in-process spec-level gate test (`src/test/testing/testing_actions_auth.test.ts`
|
|
|
1239
1252
|
asserting each spec declares `credential_types: ['daemon_token']`) + the
|
|
1240
1253
|
`assert_no_testing_methods` surface invariant.
|
|
1241
1254
|
|
|
1255
|
+
### Identity-primitive parity — `cross_backend/identity_parity.ts`
|
|
1256
|
+
|
|
1257
|
+
`describe_identity_parity_cross_tests({setup_test, login_path?, signup_path?})` —
|
|
1258
|
+
the imperative suite pinning the `primitive_schemas` twins (`Username`,
|
|
1259
|
+
`UsernameProvided`, `Email`) + login/signup input handling end-to-end over real
|
|
1260
|
+
HTTP (flat REST `/login` + `/signup`, so imperative, not a `conformance_table`
|
|
1261
|
+
row). Six facets, each a TS↔Rust reimplementation pair that the spec-derived
|
|
1262
|
+
round-trip / conformance suites never vary:
|
|
1263
|
+
|
|
1264
|
+
- **Login canonicalization** — case-insensitive + whitespace-trim lookup
|
|
1265
|
+
(`UsernameProvided` `.trim().toLowerCase()` + `LOWER(username)`), plus a
|
|
1266
|
+
Turkish-`İ` homograph **no-Unicode-fold-collision** negative.
|
|
1267
|
+
- **Username-or-email login** — an account created with an email logs in via
|
|
1268
|
+
that email, case-insensitively both directions; username login still works;
|
|
1269
|
+
non-existent email → 401. This is the corner that surfaced and closed the Rust
|
|
1270
|
+
spine's username-only divergence (it now mirrors TS's
|
|
1271
|
+
`query_account_by_username_or_email`).
|
|
1272
|
+
- **Login input validation** — malformed login input → 400 `invalid_request_body`
|
|
1273
|
+
on every spine: whitespace-only username (empty after trim), over-long username
|
|
1274
|
+
(> 255), empty password, unknown body key (strict object). TS runs the full
|
|
1275
|
+
`LoginInput` Zod schema; the Rust spine enforces the same shape in
|
|
1276
|
+
`account_login` (closed a cluster where three of these fell through to a
|
|
1277
|
+
lookup-miss 401 on Rust and a whitespace-only identifier 401'd on TS). Asserted
|
|
1278
|
+
by error reason, not just status.
|
|
1279
|
+
- **ASCII-only creation invariant** — non-ASCII username → 400 at signup.
|
|
1280
|
+
- **Length + format regex parity** — both `[USERNAME_LENGTH_MIN,
|
|
1281
|
+
USERNAME_LENGTH_MAX]` = `[3, 39]` boundary edges (just-outside → 400,
|
|
1282
|
+
just-inside → 403 no-matching-invite) and the creation regex shape (leading
|
|
1283
|
+
non-letter / trailing punctuation / embedded disallowed char → 400; mid-string
|
|
1284
|
+
`_`/`-` accepted), catching an off-by-one or position bug in the TS Zod
|
|
1285
|
+
`.min/.max/.regex` vs the Rust byte-scan `is_valid_username_for_creation`.
|
|
1286
|
+
- **Email format (creation)** — the optional signup `email` validated to the
|
|
1287
|
+
loose `local@domain.tld` shape (TS `Email` `^[^\s@]+@[^\s@]+\.[^\s@]+$` + 254-byte
|
|
1288
|
+
bound, Rust `is_valid_email`): malformed (no `@` / no domain dot / empty
|
|
1289
|
+
side / dot at a domain edge / whitespace / over-length) → 400, well-formed →
|
|
1290
|
+
403 no-invite. Accepted siblings include the single-char-TLD `a@b.c` that
|
|
1291
|
+
Zod's `z.email()` rejects — the deliberately-looser shared rule. **Closed a
|
|
1292
|
+
real divergence**: the Rust spine previously length-checked the email only,
|
|
1293
|
+
accepting any non-empty string TS rejected.
|
|
1294
|
+
|
|
1295
|
+
Ungated (login + signup are on every spine). Runs both legs via the shared
|
|
1296
|
+
`{setup_test}` protocol: in-process (`identity_parity.db.test.ts`, plain
|
|
1297
|
+
`gro test`) and cross-process (`identity_parity.cross.test.ts`). `$lib`-free.
|
|
1298
|
+
The accepted-case 403s rely on `create_spine_route_specs` zeroing
|
|
1299
|
+
`signup_fail_floor_ms` (mirroring `login_fail_floor_ms: 0`) so they don't each
|
|
1300
|
+
pay the 250ms denial floor.
|
|
1301
|
+
|
|
1242
1302
|
### Building a TS test-server binary — `testing_server_core.ts` + adapters
|
|
1243
1303
|
|
|
1244
1304
|
The reusable shape for standing up a **spawnable TS** cross-process test
|