@fuzdev/fuz_app 0.89.0 → 0.91.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 (57) hide show
  1. package/dist/auth/CLAUDE.md +7 -0
  2. package/dist/auth/account_action_specs.d.ts +1 -1
  3. package/dist/auth/account_route_schema.d.ts +2 -2
  4. package/dist/auth/account_schema.d.ts +3 -3
  5. package/dist/auth/admin_action_specs.d.ts +8 -8
  6. package/dist/auth/all_cell_actions.d.ts +35 -0
  7. package/dist/auth/all_cell_actions.d.ts.map +1 -0
  8. package/dist/auth/all_cell_actions.js +32 -0
  9. package/dist/auth/audit_log_schema.d.ts +1 -1
  10. package/dist/auth/invite_schema.d.ts +2 -2
  11. package/dist/auth/signup_route_schema.d.ts +2 -2
  12. package/dist/auth/signup_route_schema.d.ts.map +1 -1
  13. package/dist/auth/signup_route_schema.js +2 -2
  14. package/dist/primitive_schemas.d.ts +27 -1
  15. package/dist/primitive_schemas.d.ts.map +1 -1
  16. package/dist/primitive_schemas.js +55 -2
  17. package/dist/server/env.d.ts +1 -1
  18. package/dist/server/env.d.ts.map +1 -1
  19. package/dist/server/env.js +15 -3
  20. package/dist/testing/CLAUDE.md +135 -10
  21. package/dist/testing/app_server.d.ts +16 -5
  22. package/dist/testing/app_server.d.ts.map +1 -1
  23. package/dist/testing/app_server.js +3 -1
  24. package/dist/testing/cross_backend/action_manifest.d.ts +113 -0
  25. package/dist/testing/cross_backend/action_manifest.d.ts.map +1 -0
  26. package/dist/testing/cross_backend/action_manifest.js +89 -0
  27. package/dist/testing/cross_backend/action_manifest_parity.d.ts +67 -0
  28. package/dist/testing/cross_backend/action_manifest_parity.d.ts.map +1 -0
  29. package/dist/testing/cross_backend/action_manifest_parity.js +88 -0
  30. package/dist/testing/cross_backend/capabilities.d.ts +47 -30
  31. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  32. package/dist/testing/cross_backend/capabilities.js +19 -4
  33. package/dist/testing/cross_backend/default_backend_configs.d.ts +31 -15
  34. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  35. package/dist/testing/cross_backend/default_backend_configs.js +30 -14
  36. package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -1
  37. package/dist/testing/cross_backend/default_spine_surface.js +6 -0
  38. package/dist/testing/cross_backend/full_spine_mount.d.ts +81 -0
  39. package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -0
  40. package/dist/testing/cross_backend/full_spine_mount.js +49 -0
  41. package/dist/testing/cross_backend/identity_parity.d.ts +13 -0
  42. package/dist/testing/cross_backend/identity_parity.d.ts.map +1 -0
  43. package/dist/testing/cross_backend/identity_parity.js +465 -0
  44. package/dist/testing/cross_backend/method_coverage.d.ts +71 -0
  45. package/dist/testing/cross_backend/method_coverage.d.ts.map +1 -0
  46. package/dist/testing/cross_backend/method_coverage.js +89 -0
  47. package/dist/testing/cross_backend/setup.d.ts +21 -0
  48. package/dist/testing/cross_backend/setup.d.ts.map +1 -1
  49. package/dist/testing/cross_backend/setup.js +33 -1
  50. package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
  51. package/dist/testing/cross_backend/testing_backdoor.js +9 -5
  52. package/dist/testing/cross_backend/testing_reset_actions.d.ts +76 -2
  53. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  54. package/dist/testing/cross_backend/testing_reset_actions.js +63 -2
  55. package/dist/testing/schema_generators.d.ts.map +1 -1
  56. package/dist/testing/schema_generators.js +7 -0
  57. package/package.json +2 -2
@@ -211,6 +211,13 @@ auth, ids)`: batched strict relation-read filter. Every relation read —
211
211
  `cell_item_actions.ts` / `cell_audit_actions.ts` — the relation + ACL +
212
212
  audit handlers, exporting `to_grant_json` / `to_field_json` /
213
213
  `to_item_json` reused by the `cell_get` bundle.
214
+ - `auth/all_cell_actions.ts` — `create_all_cell_actions(deps, {roles})`, the
215
+ handler-side twin of `all_cell_action_specs` and sibling of
216
+ `create_standard_rpc_actions`: bundles all five cell factories (CRUD + grant
217
+ - field + item + audit) into one list. Reach for this whenever a backend
218
+ mounts the full cell layer — a single source so an HTTP-RPC mount and a WS
219
+ mount (or two backends) can't diverge on which cell verbs they expose.
220
+ Distinct from `create_cell_actions` (the CRUD-only factory it bundles).
214
221
  - `auth/cell_audit_metadata.ts` + `cell_grant_audit_metadata.ts` +
215
222
  `cell_field_audit_metadata.ts` + `cell_item_audit_metadata.ts` — audit
216
223
  metadata envelopes (IDs only), registered via `extra_events:` on the
@@ -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.ZodEmail>;
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.ZodEmail>;
24
+ email: z.ZodNullable<z.ZodString>;
25
25
  email_verified: z.ZodBoolean;
26
26
  created_at: z.ZodString;
27
27
  }, z.core.$strict>;
@@ -108,7 +108,7 @@ export declare const account_status_route_shape: {
108
108
  account: z.ZodObject<{
109
109
  id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
110
110
  username: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
111
- email: z.ZodNullable<z.ZodEmail>;
111
+ email: z.ZodNullable<z.ZodString>;
112
112
  email_verified: z.ZodBoolean;
113
113
  created_at: z.ZodString;
114
114
  }, z.core.$strict>;
@@ -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.ZodEmail>;
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.ZodEmail>;
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.ZodEmail>;
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.ZodEmail>;
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.ZodEmail>>;
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.ZodEmail>;
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.ZodEmail>;
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.ZodEmail>;
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.ZodEmail>>;
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.ZodEmail>;
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.ZodEmail>;
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>;
@@ -0,0 +1,35 @@
1
+ import type { RpcAction } from '../actions/action_rpc.js';
2
+ import { type CellActionDeps } from './cell_actions.js';
3
+ import type { RoleSchemaResult } from './role_schema.js';
4
+ /**
5
+ * Options for `create_all_cell_actions`.
6
+ *
7
+ * `roles` flows to the `cell_grant_*` sub-factory — actor-shaped grants are
8
+ * role-validated against this registry, so callers thread their app's role
9
+ * schema (the same one `create_standard_rpc_actions` takes).
10
+ */
11
+ export interface AllCellActionsOptions {
12
+ readonly roles: RoleSchemaResult;
13
+ }
14
+ /**
15
+ * Build the full cell RPC action set — CRUD (`create_cell_actions`,
16
+ * which also carries `cell_clone`) + grant ACL + field + item relations +
17
+ * per-cell audit — as a single handler-bound bundle.
18
+ *
19
+ * The handler-side twin of the `all_cell_action_specs` spec bundle and the
20
+ * sibling of `create_standard_rpc_actions`. Assembling the five cell factories
21
+ * here means an HTTP-RPC mount and a WS mount (or two different backends)
22
+ * can't silently diverge on which cell verbs they expose — the
23
+ * `spine_method_coverage` reconciliation gate enforces that the spine's live
24
+ * mount matches its coverage manifest, and this aggregator is the single list
25
+ * every mount draws from.
26
+ *
27
+ * Distinct from `create_cell_actions` (the CRUD-only factory this bundles) —
28
+ * reach for this whenever a backend mounts the complete cell layer.
29
+ *
30
+ * @param deps - `CellActionDeps` (`log`, `audit`, optional `validate_data`)
31
+ * @param options - the role schema for grant validation
32
+ * @returns every cell `RpcAction`, in mount order
33
+ */
34
+ export declare const create_all_cell_actions: (deps: CellActionDeps, options: AllCellActionsOptions) => Array<RpcAction>;
35
+ //# sourceMappingURL=all_cell_actions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"all_cell_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/all_cell_actions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAsB,KAAK,cAAc,EAAC,MAAM,mBAAmB,CAAC;AAK3E,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,kBAAkB,CAAC;AAEvD;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACrC,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAAC;CACjC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,cAAc,EACpB,SAAS,qBAAqB,KAC5B,KAAK,CAAC,SAAS,CAMjB,CAAC"}
@@ -0,0 +1,32 @@
1
+ import { create_cell_actions } from './cell_actions.js';
2
+ import { create_cell_grant_actions } from './cell_grant_actions.js';
3
+ import { create_cell_field_actions } from './cell_field_actions.js';
4
+ import { create_cell_item_actions } from './cell_item_actions.js';
5
+ import { create_cell_audit_actions } from './cell_audit_actions.js';
6
+ /**
7
+ * Build the full cell RPC action set — CRUD (`create_cell_actions`,
8
+ * which also carries `cell_clone`) + grant ACL + field + item relations +
9
+ * per-cell audit — as a single handler-bound bundle.
10
+ *
11
+ * The handler-side twin of the `all_cell_action_specs` spec bundle and the
12
+ * sibling of `create_standard_rpc_actions`. Assembling the five cell factories
13
+ * here means an HTTP-RPC mount and a WS mount (or two different backends)
14
+ * can't silently diverge on which cell verbs they expose — the
15
+ * `spine_method_coverage` reconciliation gate enforces that the spine's live
16
+ * mount matches its coverage manifest, and this aggregator is the single list
17
+ * every mount draws from.
18
+ *
19
+ * Distinct from `create_cell_actions` (the CRUD-only factory this bundles) —
20
+ * reach for this whenever a backend mounts the complete cell layer.
21
+ *
22
+ * @param deps - `CellActionDeps` (`log`, `audit`, optional `validate_data`)
23
+ * @param options - the role schema for grant validation
24
+ * @returns every cell `RpcAction`, in mount order
25
+ */
26
+ export const create_all_cell_actions = (deps, options) => [
27
+ ...create_cell_actions(deps),
28
+ ...create_cell_grant_actions({ ...deps, roles: options.roles }),
29
+ ...create_cell_field_actions(deps),
30
+ ...create_cell_item_actions(deps),
31
+ ...create_cell_audit_actions(),
32
+ ];
@@ -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.ZodEmail>;
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.ZodEmail>;
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.ZodEmail>;
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.ZodEmail>;
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,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,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"}
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.optional(),
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.ZodEmail;
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;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,wDAIa,CAAC;AAC3C,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE;;;;;;GAMG;AACH,eAAO,MAAM,KAAK,YAAY,CAAC;AAC/B,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,KAAK,CAAC,CAAC"}
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.email();
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
+ });
@@ -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.ZodUnion<readonly [z.ZodEmail, z.ZodLiteral<"">]>>;
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>;
@@ -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;;;;;;;;;;;;;;;;;;;kBAwCxB,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"}
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"}
@@ -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.number().default(4040).meta({ description: 'HTTP server port' }),
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
- .union([z.email(), z.literal('')])
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()