@openlup/platform 0.15.0 → 0.16.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 (239) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +350 -3
  3. package/dist/identity/adminAuthBinding.d.ts +28 -0
  4. package/dist/identity/adminAuthBinding.js +66 -0
  5. package/dist/identity/adminAuthorization.d.ts +34 -0
  6. package/dist/identity/adminAuthorization.js +72 -0
  7. package/dist/identity/adminContracts.d.ts +70 -0
  8. package/dist/identity/adminContracts.js +43 -0
  9. package/dist/identity/adminMagicLinkHandler.d.ts +39 -0
  10. package/dist/identity/adminMagicLinkHandler.js +94 -0
  11. package/dist/identity/adminMeHandler.d.ts +15 -0
  12. package/dist/identity/adminMeHandler.js +49 -0
  13. package/dist/identity/adminPreflight.d.ts +16 -0
  14. package/dist/identity/adminPreflight.js +38 -0
  15. package/dist/identity/adminUserActionPorts.d.ts +31 -0
  16. package/dist/identity/adminUserActionPorts.js +51 -0
  17. package/dist/identity/adminUserHandlers.d.ts +38 -0
  18. package/dist/identity/adminUserHandlers.js +140 -0
  19. package/dist/identity/authVerifyUrl.d.ts +6 -0
  20. package/dist/identity/authVerifyUrl.js +54 -0
  21. package/dist/identity/contracts/contracts.d.ts +8 -0
  22. package/dist/identity/contracts/contracts.js +2 -0
  23. package/dist/identity/contracts/emailTrust.d.ts +3 -0
  24. package/dist/identity/contracts/emailTrust.js +4 -0
  25. package/dist/identity/contracts/index.d.ts +8 -0
  26. package/dist/identity/contracts/index.js +8 -0
  27. package/dist/identity/contracts/oidc.d.ts +3 -0
  28. package/dist/identity/contracts/oidc.js +9 -0
  29. package/dist/identity/contracts/operatorAuth.d.ts +35 -0
  30. package/dist/identity/contracts/operatorAuth.js +53 -0
  31. package/dist/identity/contracts/operatorInvitations.d.ts +37 -0
  32. package/dist/identity/contracts/operatorInvitations.js +1 -0
  33. package/dist/identity/contracts/ports.d.ts +22 -0
  34. package/dist/identity/contracts/ports.js +1 -0
  35. package/dist/identity/contracts/privacy.d.ts +62 -0
  36. package/dist/identity/contracts/privacy.js +99 -0
  37. package/dist/identity/contracts/providerList.d.ts +3 -0
  38. package/dist/identity/contracts/providerList.js +11 -0
  39. package/dist/identity/contracts/types.d.ts +16 -0
  40. package/dist/identity/contracts/types.js +4 -0
  41. package/dist/identity/contribution.d.ts +8 -0
  42. package/dist/identity/contribution.js +22 -0
  43. package/dist/identity/customerAuth.d.ts +8 -0
  44. package/dist/identity/customerAuth.js +1 -0
  45. package/dist/identity/customerAuthorization.d.ts +5 -0
  46. package/dist/identity/customerAuthorization.js +22 -0
  47. package/dist/identity/customerContracts.d.ts +42 -0
  48. package/dist/identity/customerContracts.js +43 -0
  49. package/dist/identity/customerIdentityBinding.d.ts +44 -0
  50. package/dist/identity/customerIdentityBinding.js +60 -0
  51. package/dist/identity/customerIdentityVerifier.d.ts +8 -0
  52. package/dist/identity/customerIdentityVerifier.js +101 -0
  53. package/dist/identity/customerMagicLinkHandler.d.ts +38 -0
  54. package/dist/identity/customerMagicLinkHandler.js +115 -0
  55. package/dist/identity/customerMeHandler.d.ts +11 -0
  56. package/dist/identity/customerMeHandler.js +46 -0
  57. package/dist/identity/customerPorts.d.ts +4 -0
  58. package/dist/identity/customerPorts.js +1 -0
  59. package/dist/identity/customerSessionBinding.d.ts +38 -0
  60. package/dist/identity/customerSessionBinding.js +80 -0
  61. package/dist/identity/customerSessionIssuer.d.ts +10 -0
  62. package/dist/identity/customerSessionIssuer.js +57 -0
  63. package/dist/identity/customerVerifyOtpHandler.d.ts +8 -0
  64. package/dist/identity/customerVerifyOtpHandler.js +41 -0
  65. package/dist/identity/directOperatorInvitationStore.d.ts +19 -0
  66. package/dist/identity/directOperatorInvitationStore.js +193 -0
  67. package/dist/identity/emailTrustPolicy.d.ts +14 -0
  68. package/dist/identity/emailTrustPolicy.js +16 -0
  69. package/dist/identity/identityPersonalDataContributor.d.ts +7 -0
  70. package/dist/identity/identityPersonalDataContributor.js +46 -0
  71. package/dist/identity/identityReadiness.d.ts +10 -0
  72. package/dist/identity/identityReadiness.js +25 -0
  73. package/dist/identity/index.d.ts +67 -0
  74. package/dist/identity/index.js +66 -0
  75. package/dist/identity/inviteAdminUserUseCase.d.ts +89 -0
  76. package/dist/identity/inviteAdminUserUseCase.js +75 -0
  77. package/dist/identity/jwtLibrary.d.ts +54 -0
  78. package/dist/identity/jwtLibrary.js +1 -0
  79. package/dist/identity/machineActorWriteFence.d.ts +4 -0
  80. package/dist/identity/machineActorWriteFence.js +13 -0
  81. package/dist/identity/magicLinkPorts.d.ts +28 -0
  82. package/dist/identity/magicLinkPorts.js +1 -0
  83. package/dist/identity/managedAccountLinkStore.d.ts +2 -0
  84. package/dist/identity/managedAccountLinkStore.js +93 -0
  85. package/dist/identity/managedAdminAuthVerifier.d.ts +7 -0
  86. package/dist/identity/managedAdminAuthVerifier.js +27 -0
  87. package/dist/identity/managedAdminUserGateways.d.ts +123 -0
  88. package/dist/identity/managedAdminUserGateways.js +127 -0
  89. package/dist/identity/managedClient.d.ts +90 -0
  90. package/dist/identity/managedClient.js +10 -0
  91. package/dist/identity/managedClientAuthLink.d.ts +3 -0
  92. package/dist/identity/managedClientAuthLink.js +115 -0
  93. package/dist/identity/managedCustomerMe.d.ts +13 -0
  94. package/dist/identity/managedCustomerMe.js +30 -0
  95. package/dist/identity/managedEmailTrustPolicy.d.ts +11 -0
  96. package/dist/identity/managedEmailTrustPolicy.js +105 -0
  97. package/dist/identity/managedIdentityVerifier.d.ts +2 -0
  98. package/dist/identity/managedIdentityVerifier.js +16 -0
  99. package/dist/identity/managedPrivacy.d.ts +4 -0
  100. package/dist/identity/managedPrivacy.js +39 -0
  101. package/dist/identity/messages.d.ts +29 -0
  102. package/dist/identity/messages.js +39 -0
  103. package/dist/identity/oidcIdentityVerifier.d.ts +45 -0
  104. package/dist/identity/oidcIdentityVerifier.js +309 -0
  105. package/dist/identity/oidcPrincipal.d.ts +2 -0
  106. package/dist/identity/oidcPrincipal.js +10 -0
  107. package/dist/identity/operatorAdminAuth.d.ts +6 -0
  108. package/dist/identity/operatorAdminAuth.js +13 -0
  109. package/dist/identity/operatorAuthHandlers.d.ts +18 -0
  110. package/dist/identity/operatorAuthHandlers.js +71 -0
  111. package/dist/identity/operatorIdentityVerifier.d.ts +8 -0
  112. package/dist/identity/operatorIdentityVerifier.js +19 -0
  113. package/dist/identity/operatorInvitationLifecycle.d.ts +7 -0
  114. package/dist/identity/operatorInvitationLifecycle.js +15 -0
  115. package/dist/identity/operatorInvitationPorts.d.ts +31 -0
  116. package/dist/identity/operatorInvitationPorts.js +1 -0
  117. package/dist/identity/operatorPorts.d.ts +37 -0
  118. package/dist/identity/operatorPorts.js +1 -0
  119. package/dist/identity/operatorSession.d.ts +13 -0
  120. package/dist/identity/operatorSession.js +31 -0
  121. package/dist/identity/operatorTokenAdminAuth.d.ts +15 -0
  122. package/dist/identity/operatorTokenAdminAuth.js +50 -0
  123. package/dist/identity/personalDataRegistry.d.ts +4 -0
  124. package/dist/identity/personalDataRegistry.js +11 -0
  125. package/dist/identity/platformAdminAuth.d.ts +15 -0
  126. package/dist/identity/platformAdminAuth.js +1 -0
  127. package/dist/identity/ports.d.ts +127 -0
  128. package/dist/identity/ports.js +38 -0
  129. package/dist/identity/postgresAccountLinkStore.d.ts +7 -0
  130. package/dist/identity/postgresAccountLinkStore.js +52 -0
  131. package/dist/identity/postgresCustomerMe.d.ts +3 -0
  132. package/dist/identity/postgresCustomerMe.js +21 -0
  133. package/dist/identity/postgresCustomerRecovery.d.ts +11 -0
  134. package/dist/identity/postgresCustomerRecovery.js +57 -0
  135. package/dist/identity/postgresOperatorIdentity.d.ts +5 -0
  136. package/dist/identity/postgresOperatorIdentity.js +137 -0
  137. package/dist/identity/postgresPrivacy.d.ts +7 -0
  138. package/dist/identity/postgresPrivacy.js +126 -0
  139. package/dist/identity/privacyHandlers.d.ts +10 -0
  140. package/dist/identity/privacyHandlers.js +35 -0
  141. package/dist/identity/privacyPorts.d.ts +72 -0
  142. package/dist/identity/privacyPorts.js +8 -0
  143. package/dist/identity/privacyUseCases.d.ts +5 -0
  144. package/dist/identity/privacyUseCases.js +55 -0
  145. package/dist/identity/reconcileAccountForPrincipal.d.ts +20 -0
  146. package/dist/identity/reconcileAccountForPrincipal.js +66 -0
  147. package/dist/identity/reconcileAccountHandler.d.ts +11 -0
  148. package/dist/identity/reconcileAccountHandler.js +30 -0
  149. package/dist/identity/removeAdminUserUseCase.d.ts +31 -0
  150. package/dist/identity/removeAdminUserUseCase.js +60 -0
  151. package/dist/identity/response.d.ts +3 -0
  152. package/dist/identity/response.js +2 -0
  153. package/dist/identity/sql.d.ts +44 -0
  154. package/dist/identity/sql.js +10 -0
  155. package/package.json +12 -2
  156. package/release-gates.json +22 -0
  157. package/sql/20260810180001_self_host_actor_auth.sql +40 -0
  158. package/sql/20260810204057_customer_identity_read.sql +31 -0
  159. package/sql/20260814090000_customer_selfservice_recovery.sql +477 -0
  160. package/sql/20260901143000_admin_membership_authority_foundation.sql +234 -0
  161. package/sql/20261018120000_identity_operator_invitations.sql +42 -0
  162. package/sql/20261018180000_identity_operator_challenges.sql +56 -0
  163. package/sql/20261018190000_identity_privacy_erasures.sql +15 -0
  164. package/src/identity/adminAuthBinding.ts +76 -0
  165. package/src/identity/adminAuthorization.ts +106 -0
  166. package/src/identity/adminContracts.ts +52 -0
  167. package/src/identity/adminMagicLinkHandler.ts +132 -0
  168. package/src/identity/adminMeHandler.ts +64 -0
  169. package/src/identity/adminPreflight.ts +46 -0
  170. package/src/identity/adminUserActionPorts.ts +78 -0
  171. package/src/identity/adminUserHandlers.ts +174 -0
  172. package/src/identity/authVerifyUrl.ts +56 -0
  173. package/src/identity/contracts/contracts.ts +15 -0
  174. package/src/identity/contracts/emailTrust.ts +5 -0
  175. package/src/identity/contracts/index.ts +8 -0
  176. package/src/identity/contracts/oidc.ts +10 -0
  177. package/src/identity/contracts/operatorAuth.ts +61 -0
  178. package/src/identity/contracts/operatorInvitations.ts +32 -0
  179. package/src/identity/contracts/ports.ts +25 -0
  180. package/src/identity/contracts/privacy.ts +86 -0
  181. package/src/identity/contracts/providerList.ts +13 -0
  182. package/src/identity/contracts/types.ts +25 -0
  183. package/src/identity/contribution.ts +29 -0
  184. package/src/identity/customerAuth.ts +3 -0
  185. package/src/identity/customerAuthorization.ts +38 -0
  186. package/src/identity/customerContracts.ts +52 -0
  187. package/src/identity/customerIdentityBinding.ts +125 -0
  188. package/src/identity/customerIdentityVerifier.ts +141 -0
  189. package/src/identity/customerMagicLinkHandler.ts +171 -0
  190. package/src/identity/customerMeHandler.ts +64 -0
  191. package/src/identity/customerPorts.ts +2 -0
  192. package/src/identity/customerSessionBinding.ts +114 -0
  193. package/src/identity/customerSessionIssuer.ts +71 -0
  194. package/src/identity/customerVerifyOtpHandler.ts +44 -0
  195. package/src/identity/directOperatorInvitationStore.ts +178 -0
  196. package/src/identity/emailTrustPolicy.ts +16 -0
  197. package/src/identity/identityPersonalDataContributor.ts +48 -0
  198. package/src/identity/identityReadiness.ts +26 -0
  199. package/src/identity/index.ts +69 -0
  200. package/src/identity/inviteAdminUserUseCase.ts +134 -0
  201. package/src/identity/jwtLibrary.ts +24 -0
  202. package/src/identity/machineActorWriteFence.ts +19 -0
  203. package/src/identity/magicLinkPorts.ts +7 -0
  204. package/src/identity/managedAccountLinkStore.ts +105 -0
  205. package/src/identity/managedAdminAuthVerifier.ts +40 -0
  206. package/src/identity/managedAdminUserGateways.ts +164 -0
  207. package/src/identity/managedClient.ts +58 -0
  208. package/src/identity/managedClientAuthLink.ts +120 -0
  209. package/src/identity/managedCustomerMe.ts +52 -0
  210. package/src/identity/managedEmailTrustPolicy.ts +90 -0
  211. package/src/identity/managedIdentityVerifier.ts +20 -0
  212. package/src/identity/managedPrivacy.ts +37 -0
  213. package/src/identity/messages.ts +41 -0
  214. package/src/identity/oidcIdentityVerifier.ts +229 -0
  215. package/src/identity/oidcPrincipal.ts +10 -0
  216. package/src/identity/operatorAdminAuth.ts +14 -0
  217. package/src/identity/operatorAuthHandlers.ts +52 -0
  218. package/src/identity/operatorIdentityVerifier.ts +19 -0
  219. package/src/identity/operatorInvitationLifecycle.ts +14 -0
  220. package/src/identity/operatorInvitationPorts.ts +11 -0
  221. package/src/identity/operatorPorts.ts +11 -0
  222. package/src/identity/operatorSession.ts +30 -0
  223. package/src/identity/operatorTokenAdminAuth.ts +79 -0
  224. package/src/identity/personalDataRegistry.ts +8 -0
  225. package/src/identity/platformAdminAuth.ts +7 -0
  226. package/src/identity/ports.ts +158 -0
  227. package/src/identity/postgresAccountLinkStore.ts +49 -0
  228. package/src/identity/postgresCustomerMe.ts +33 -0
  229. package/src/identity/postgresCustomerRecovery.ts +72 -0
  230. package/src/identity/postgresOperatorIdentity.ts +123 -0
  231. package/src/identity/postgresPrivacy.ts +117 -0
  232. package/src/identity/privacyHandlers.ts +33 -0
  233. package/src/identity/privacyPorts.ts +52 -0
  234. package/src/identity/privacyUseCases.ts +48 -0
  235. package/src/identity/reconcileAccountForPrincipal.ts +99 -0
  236. package/src/identity/reconcileAccountHandler.ts +36 -0
  237. package/src/identity/removeAdminUserUseCase.ts +85 -0
  238. package/src/identity/response.ts +3 -0
  239. package/src/identity/sql.ts +10 -0
@@ -0,0 +1,132 @@
1
+ import { createMagicLinkOriginResolver } from "./authVerifyUrl.js";
2
+ import { createIdentityResponder, type IdentityResponseOptions } from "./response.js";
3
+ import type { z } from "zod";
4
+ import type { HttpRequest, HttpResponse } from "../http/index.js";
5
+ import type { MagicLinkAuthClient, MagicLinkRateLimitClient, MagicLinkDecision } from "./magicLinkPorts.js";
6
+ export interface AdminMagicLinkHandlerOptions extends IdentityResponseOptions {
7
+ requestSchema: z.ZodType<{ email: string }>;
8
+ responseSchema: z.ZodType<{ accepted: true }>;
9
+ readEnvironment: () => { url: string; anonKey: string; serviceRoleKey: string } | null;
10
+ createRuntime: (env: { url: string; anonKey: string; serviceRoleKey: string }) => {
11
+ authClient: MagicLinkAuthClient;
12
+ rateLimitClient: MagicLinkRateLimitClient;
13
+ platformPort: { checkAdminMagicLinkEligibility(email: string): Promise<string> };
14
+ };
15
+ checkAttempt: (input: { client: MagicLinkRateLimitClient; ip: string; email: string }) => Promise<MagicLinkDecision>;
16
+ extractClientIp: (headers: HttpRequest["headers"]) => string;
17
+ padResponseTime: (startedAt: number, floor: number) => Promise<void>;
18
+ redirectOrigins?: readonly string[];
19
+ resolveOrigin?: (req: HttpRequest) => string | null;
20
+ callbackPath: string;
21
+ }
22
+ const ADMIN_MAGIC_LINK_RESPONSE_FLOOR_MS = 400;
23
+ export function createAdminMagicLinkHandler(options: AdminMagicLinkHandlerOptions) {
24
+ const { sendBffError, sendBffSuccess, sendMethodNotAllowed } = createIdentityResponder(options);
25
+ const readRequestOrigin = options.resolveOrigin ?? createMagicLinkOriginResolver(options.redirectOrigins ?? []);
26
+ const { requestSchema: adminMagicLinkRequestSchema, responseSchema: adminMagicLinkResponseSchema, readEnvironment: readSupabaseAdminServiceEnv, createRuntime: createAdminMagicLinkRuntime, checkAttempt: checkAndRecordAdminMagicLinkAttempt, extractClientIp, padResponseTime } = options;
27
+ return async (req: HttpRequest, res: HttpResponse): Promise<void> => {
28
+ if (req.method !== "POST") {
29
+ sendMethodNotAllowed(res, ["POST"]);
30
+ return;
31
+ }
32
+
33
+ const request = adminMagicLinkRequestSchema.safeParse(req.body);
34
+ if (!request.success) {
35
+ sendBffError(res, "BAD_REQUEST", "Invalid admin magic-link request", {
36
+ details: request.error.flatten(),
37
+ });
38
+ return;
39
+ }
40
+
41
+ const startedAt = Date.now();
42
+ const origin = readRequestOrigin(req);
43
+ if (!origin) {
44
+ await padResponseTime(startedAt, ADMIN_MAGIC_LINK_RESPONSE_FLOOR_MS);
45
+ sendBffError(res, "UPSTREAM_UNAVAILABLE", "Magic link origin is not configured", {
46
+ details: { reason: "magic_link_origin_not_configured" },
47
+ });
48
+ return;
49
+ }
50
+
51
+ const env = readSupabaseAdminServiceEnv();
52
+ if (!env) {
53
+ sendBffError(res, "UPSTREAM_UNAVAILABLE", "Admin auth is not configured", {
54
+ details: { feature: "admin_auth", requiredEnv: "SUPABASE_SERVICE_ROLE_KEY" },
55
+ });
56
+ return;
57
+ }
58
+
59
+ const runtime = createAdminMagicLinkRuntime(env);
60
+
61
+ const decision = await checkAndRecordAdminMagicLinkAttempt({
62
+ client: runtime.rateLimitClient,
63
+ ip: extractClientIp(req.headers),
64
+ email: request.data.email,
65
+ });
66
+
67
+ if (!decision.allowed && decision.reason === "rpc_error") {
68
+ console.warn("admin_magic_link_rate_limit_unavailable", JSON.stringify({ reason: decision.reason }));
69
+ await padResponseTime(startedAt, ADMIN_MAGIC_LINK_RESPONSE_FLOOR_MS);
70
+ sendBffError(res, "UPSTREAM_UNAVAILABLE", "Magic link service temporarily unavailable");
71
+ return;
72
+ }
73
+
74
+ if (decision.allowed) {
75
+ let eligibility: Awaited<ReturnType<typeof runtime.platformPort.checkAdminMagicLinkEligibility>>;
76
+ try {
77
+ eligibility = await runtime.platformPort.checkAdminMagicLinkEligibility(request.data.email);
78
+ } catch {
79
+ console.warn("admin_magic_link_lookup_failed", JSON.stringify({ outcome: "lookup_error" }));
80
+ await padResponseTime(startedAt, ADMIN_MAGIC_LINK_RESPONSE_FLOOR_MS);
81
+ sendBffError(res, "UPSTREAM_UNAVAILABLE", "Magic link service temporarily unavailable");
82
+ return;
83
+ }
84
+
85
+ if (eligibility === "eligible") {
86
+ const outcome = await sendMagicLink(runtime.authClient, request.data.email, origin);
87
+ console.info(
88
+ "admin_magic_link_outcome",
89
+ JSON.stringify({ outcome: outcome.status, code: outcome.code ?? null }),
90
+ );
91
+ } else {
92
+ console.warn("admin_magic_link_outcome", JSON.stringify({ outcome: eligibility }));
93
+ }
94
+ } else {
95
+ console.warn(
96
+ "admin_magic_link_outcome",
97
+ JSON.stringify({ outcome: "rate_limited", reason: decision.reason ?? "unknown" }),
98
+ );
99
+ }
100
+
101
+ await padResponseTime(startedAt, ADMIN_MAGIC_LINK_RESPONSE_FLOOR_MS);
102
+ sendBffSuccess(res, adminMagicLinkResponseSchema.parse({ accepted: true }));
103
+ };
104
+
105
+ async function sendMagicLink(
106
+ client: MagicLinkAuthClient,
107
+ email: string,
108
+ origin: string,
109
+ ): Promise<{ status: "sent" | "auth_error" | "threw"; code?: string }> {
110
+ try {
111
+ const { error } = await client.auth.signInWithOtp({
112
+ email,
113
+ options: {
114
+ shouldCreateUser: false,
115
+ emailRedirectTo: `${origin}${options.callbackPath}`,
116
+ },
117
+ });
118
+
119
+ if (error) {
120
+ console.warn("admin_magic_link_send_failed", JSON.stringify({ code: error.name ?? "AuthError" }));
121
+ return { status: "auth_error", code: error.name ?? "AuthError" };
122
+ }
123
+ return { status: "sent" };
124
+ } catch (error) {
125
+ const code = error instanceof Error ? error.name : "unknown";
126
+ console.warn("admin_magic_link_send_threw", JSON.stringify({ error: code }));
127
+ return { status: "threw", code };
128
+ }
129
+ }
130
+
131
+
132
+ }
@@ -0,0 +1,64 @@
1
+ import type { OperatorInvitationStore } from "./operatorInvitationPorts.js";
2
+ import { createIdentityResponder, type IdentityResponseOptions } from "./response.js";
3
+ import type { HttpRequest, HttpResponse } from "../http/index.js";
4
+ import { adminPlatformMeRequestSchema, adminPlatformMeResponseSchema } from "./adminContracts.js";
5
+ import type { PlatformUserAuthenticationResult } from "./platformAdminAuth.js";
6
+ export interface AdminPlatformMeDeps extends IdentityResponseOptions {
7
+ invitationStore?: Pick<OperatorInvitationStore, "acceptOperatorInvitation">;
8
+ platformPort: { getAdminMe(userId: string): Promise<{ isAdmin: boolean; role: "admin" | "distributor" | null }> };
9
+ authenticateUser: (req: HttpRequest) => Promise<PlatformUserAuthenticationResult>;
10
+ }
11
+
12
+ export function createAdminPlatformMeHandler({
13
+ platformPort, invitationStore,
14
+ authenticateUser, responseRequestId,
15
+ }: AdminPlatformMeDeps) {
16
+ const { sendBffError, sendBffSuccess, sendMethodNotAllowed } = createIdentityResponder({ responseRequestId });
17
+ return async function handler(req: HttpRequest, res: HttpResponse): Promise<void> {
18
+ if (req.method !== "GET") {
19
+ sendMethodNotAllowed(res, ["GET"]);
20
+ return;
21
+ }
22
+
23
+ let authentication: PlatformUserAuthenticationResult;
24
+ try {
25
+ authentication = await authenticateUser(req);
26
+ } catch {
27
+ sendBffError(res, "UPSTREAM_UNAVAILABLE", "Platform user authentication failed");
28
+ return;
29
+ }
30
+
31
+ if (authentication.ok === false) {
32
+ sendBffError(res, authentication.code, authentication.message);
33
+ return;
34
+ }
35
+
36
+ const request = adminPlatformMeRequestSchema.safeParse(req.query);
37
+ if (!request.success) {
38
+ sendBffError(res, "BAD_REQUEST", "Invalid platform me request", {
39
+ details: request.error.flatten(),
40
+ });
41
+ return;
42
+ }
43
+
44
+ try {
45
+ if (invitationStore) {
46
+ const outcome = await invitationStore.acceptOperatorInvitation(authentication.userId);
47
+ if (outcome.code === "INVITATION_EXPIRED" || outcome.code === "INVITATION_REVOKED") {
48
+ sendBffError(res, "FORBIDDEN", outcome.code, { details: { identityCode: outcome.code } });
49
+ return;
50
+ }
51
+ }
52
+ const result = await platformPort.getAdminMe(authentication.userId);
53
+ const response = adminPlatformMeResponseSchema.safeParse(result);
54
+ if (!response.success) {
55
+ sendBffError(res, "INVALID_RESPONSE", "Platform me returned invalid response");
56
+ return;
57
+ }
58
+
59
+ sendBffSuccess(res, response.data);
60
+ } catch {
61
+ sendBffError(res, "UPSTREAM_UNAVAILABLE", "Platform me read failed");
62
+ }
63
+ };
64
+ }
@@ -0,0 +1,46 @@
1
+ import { createIdentityResponder, type IdentityResponseOptions } from "./response.js";
2
+ import type { HttpRequest, HttpResponse } from "../http/index.js";
3
+ import type { AdminAuthPort, AdminPrincipalRole } from "./ports.js";
4
+ import { readBearerToken } from "./adminAuthorization.js";
5
+ import { refusesMachineActorRequest, sendMachineActorWriteRefused } from "./machineActorWriteFence.js";
6
+
7
+ export interface AdminAuthBinding {
8
+ readonly identity: string;
9
+ run<T>(accessToken: string | null, work: (auth: AdminAuthPort) => Promise<T>): Promise<T>;
10
+ }
11
+ export function createAdminBffPreflight(options: IdentityResponseOptions & {
12
+ allowedRoles: (route: string) => readonly AdminPrincipalRole[] | null;
13
+ resolveBinding: () => { binding?: AdminAuthBinding; error?: string };
14
+ machineWriteRoutes?: readonly string[];
15
+ managedEnvironmentMessage?: string;
16
+ }) {
17
+ const { sendBffError, sendBffSuccess, sendMethodNotAllowed } = createIdentityResponder(options);
18
+ return async (req: HttpRequest, res: HttpResponse, route: string): Promise<boolean> => {
19
+ const allowedRoles = options.allowedRoles(route);
20
+ if (!allowedRoles) return true;
21
+ const resolved = options.resolveBinding();
22
+ if (!resolved.binding) {
23
+ if (resolved.error === "managed_admin_auth_env_required") {
24
+ sendBffError(res, "INTERNAL", options.managedEnvironmentMessage ?? "Identity environment is not configured");
25
+ } else sendBffError(res, "UPSTREAM_UNAVAILABLE", "Admin authorization failed");
26
+ return false;
27
+ }
28
+ const accessToken = readBearerToken(req);
29
+ try {
30
+ const authz = await resolved.binding.run(accessToken, (auth) => auth.authorize(accessToken, { allowedRoles }));
31
+ if (authz.ok === false) {
32
+ sendBffError(res, authz.code, authz.message);
33
+ return false;
34
+ }
35
+ if (resolved.binding.identity !== "node-postgres"
36
+ && refusesMachineActorRequest(route, req.method, authz.isMachineActor, options.machineWriteRoutes)) {
37
+ sendMachineActorWriteRefused(res, options);
38
+ return false;
39
+ }
40
+ return true;
41
+ } catch {
42
+ sendBffError(res, "UPSTREAM_UNAVAILABLE", "Admin authorization failed");
43
+ return false;
44
+ }
45
+ };
46
+ }
@@ -0,0 +1,78 @@
1
+ import { adminUserInviteResponseSchema } from "./adminContracts.js";
2
+ import type { IdentityAdminActionsPort } from "./adminUserHandlers.js";
3
+ import type { runInviteAdminUserUseCase } from "./inviteAdminUserUseCase.js";
4
+ import type { runRemoveAdminUserUseCase } from "./removeAdminUserUseCase.js";
5
+ export interface AdminUserInviteErrorDetails {
6
+ identityCode?: string;
7
+ invitationId?: string;
8
+ principalId?: string;
9
+ membershipState?: "active";
10
+ role?: "admin" | "distributor";
11
+ }
12
+ type AdminUserActionRuntimeEnv = Record<string, string | undefined>;
13
+ type AdminUserInviteRunner = (request: { accessToken: string | null; email: string; role: "admin" | "distributor" }, env?: AdminUserActionRuntimeEnv) => ReturnType<typeof runInviteAdminUserUseCase>;
14
+ type AdminUserRemoveRunner = (request: { accessToken: string | null; userId: string }, env?: AdminUserActionRuntimeEnv) => ReturnType<typeof runRemoveAdminUserUseCase>;
15
+ export function createAdminUserInvitePort(options: {
16
+ accessToken: string | null;
17
+ env?: AdminUserActionRuntimeEnv;
18
+ runInviteAdminUser: AdminUserInviteRunner;
19
+ }): Pick<IdentityAdminActionsPort, "inviteAdminUser"> {
20
+ const runInviteAdminUser = options.runInviteAdminUser;
21
+ return {
22
+ async inviteAdminUser(request) {
23
+ const result = await runInviteAdminUser({
24
+ accessToken: options.accessToken,
25
+ email: request.email,
26
+ role: request.role,
27
+ }, options.env);
28
+ if (result.status < 200 || result.status >= 300) {
29
+ const details: AdminUserInviteErrorDetails = {
30
+ identityCode: result.body.code,
31
+ ...(typeof result.body.invitationId === "string" ? { invitationId: result.body.invitationId } : {}),
32
+ ...(result.body.membershipState === "active" && typeof result.body.principalId === "string"
33
+ && (result.body.role === "admin" || result.body.role === "distributor")
34
+ ? { principalId: result.body.principalId, membershipState: "active", role: result.body.role } : {}),
35
+ };
36
+ throw Object.assign(new Error(String(result.body.error ?? `invite_admin_user_http_${result.status}`)), { bffCode: ({ 400: "BAD_REQUEST", 401: "UNAUTHORIZED", 403: "FORBIDDEN", 409: "CONFLICT" } as Record<number, string>)[result.status] ?? "UPSTREAM_UNAVAILABLE", inviteDetails: details });
37
+ }
38
+ if (result.body.error) throw new Error(result.body.error);
39
+
40
+ return adminUserInviteResponseSchema.parse(result.body);
41
+ },
42
+ };
43
+ }
44
+
45
+ export function createAdminUserRemovePort(options: {
46
+ accessToken: string | null;
47
+ env?: AdminUserActionRuntimeEnv;
48
+ runRemoveAdminUser: AdminUserRemoveRunner;
49
+ }): Pick<IdentityAdminActionsPort, "removeAdminUser"> {
50
+ const runRemoveAdminUser = options.runRemoveAdminUser;
51
+ return {
52
+ async removeAdminUser(request) {
53
+ const result = await runRemoveAdminUser({
54
+ accessToken: options.accessToken,
55
+ userId: request.userId,
56
+ }, options.env);
57
+ if (result.status < 200 || result.status >= 300) {
58
+ throw removePortError(
59
+ result.status,
60
+ String(result.body.error ?? `remove_admin_user_http_${result.status}`),
61
+ );
62
+ }
63
+ if (result.body.error) throw new Error(result.body.error);
64
+
65
+ if (result.body.revoked !== true) {
66
+ throw new Error("admin_revoke_admin_user returned an invalid response");
67
+ }
68
+ return { revoked: true };
69
+ },
70
+ };
71
+ }
72
+
73
+ function removePortError(status: number, message: string): Error {
74
+ const bffCode = ({ 400: "BAD_REQUEST", 403: "FORBIDDEN", 409: "CONFLICT" } as const)[status]
75
+ ?? "UPSTREAM_UNAVAILABLE";
76
+ return Object.assign(new Error(message), { bffCode });
77
+ }
78
+
@@ -0,0 +1,174 @@
1
+ import type { AdminUserInviteErrorDetails } from "./adminUserActionPorts.js";
2
+ import { createIdentityResponder, type IdentityResponseOptions } from "./response.js";
3
+ import type { HttpRequest, HttpResponse } from "../http/index.js";
4
+ import { adminUserRoleUpdateRequestSchema, adminUserRoleUpdateResponseSchema, adminUserInviteRequestSchema, adminUserInviteResponseSchema, adminUserRemoveRequestSchema, adminUserRemoveResponseSchema } from "./adminContracts.js";
5
+ import type { PlatformAdminAuthorizationResult } from "./platformAdminAuth.js";
6
+ import type { OperatorInviteResult } from "./contracts/operatorInvitations.js";
7
+ export interface IdentityAdminActionsPort {
8
+ inviteAdminUser(request: { email: string; role: "admin" | "distributor" }): Promise<OperatorInviteResult>;
9
+ removeAdminUser(request: { userId: string }): Promise<{ revoked: true }>;
10
+ updateAdminUserRole(request: { userId: string; role: "admin" | "distributor" }): Promise<{ userId: string; role: "admin" | "distributor"; saved: true }>;
11
+ }
12
+ export interface AdminUserRoleUpdateDeps extends IdentityResponseOptions { settingsPort: Pick<IdentityAdminActionsPort, "updateAdminUserRole">; authorizeAdmin: (req: HttpRequest) => Promise<PlatformAdminAuthorizationResult> }
13
+ export interface AdminUserInviteDeps extends IdentityResponseOptions { settingsPort: Pick<IdentityAdminActionsPort, "inviteAdminUser">; authorizeAdmin: (req: HttpRequest) => Promise<PlatformAdminAuthorizationResult> }
14
+ export interface AdminUserRemoveDeps extends IdentityResponseOptions { settingsPort: Pick<IdentityAdminActionsPort, "removeAdminUser">; authorizeAdmin: (req: HttpRequest) => Promise<PlatformAdminAuthorizationResult> }
15
+ export function createAdminUserRoleUpdateHandler({
16
+ settingsPort,
17
+ authorizeAdmin, responseRequestId,
18
+ }: AdminUserRoleUpdateDeps) {
19
+ const { sendBffError, sendBffSuccess, sendMethodNotAllowed } = createIdentityResponder({ responseRequestId });
20
+ return async function handler(req: HttpRequest, res: HttpResponse): Promise<void> {
21
+ if (req.method !== "POST") {
22
+ sendMethodNotAllowed(res, ["POST"]);
23
+ return;
24
+ }
25
+
26
+ let authorization: PlatformAdminAuthorizationResult;
27
+ try {
28
+ authorization = await authorizeAdmin(req);
29
+ } catch {
30
+ sendBffError(res, "UPSTREAM_UNAVAILABLE", "Admin authorization failed");
31
+ return;
32
+ }
33
+
34
+ if (authorization.ok === false) {
35
+ sendBffError(res, authorization.code, authorization.message);
36
+ return;
37
+ }
38
+
39
+ const request = adminUserRoleUpdateRequestSchema.safeParse(req.body);
40
+ if (!request.success) {
41
+ sendBffError(res, "BAD_REQUEST", "Invalid admin user role update request", {
42
+ details: request.error.flatten(),
43
+ });
44
+ return;
45
+ }
46
+
47
+ try {
48
+ const result = await settingsPort.updateAdminUserRole(request.data);
49
+ const response = adminUserRoleUpdateResponseSchema.safeParse(result);
50
+ if (!response.success) {
51
+ sendBffError(res, "INVALID_RESPONSE", "Admin user role update returned invalid response");
52
+ return;
53
+ }
54
+
55
+ sendBffSuccess(res, response.data);
56
+ } catch (error) {
57
+ const bffCode = (error as { bffCode?: string })?.bffCode;
58
+ if (bffCode) {
59
+ sendBffError(res, bffCode as Parameters<typeof sendBffError>[1], (error as Error).message);
60
+ return;
61
+ }
62
+ sendBffError(res, "UPSTREAM_UNAVAILABLE", "Admin user role update failed");
63
+ }
64
+ };
65
+ }
66
+
67
+ export function createAdminUserInviteHandler({
68
+ settingsPort,
69
+ authorizeAdmin, responseRequestId,
70
+ }: AdminUserInviteDeps) {
71
+ const { sendBffError, sendBffSuccess, sendMethodNotAllowed } = createIdentityResponder({ responseRequestId });
72
+ return async function handler(req: HttpRequest, res: HttpResponse): Promise<void> {
73
+ if (req.method !== "POST") {
74
+ sendMethodNotAllowed(res, ["POST"]);
75
+ return;
76
+ }
77
+
78
+ let authorization: PlatformAdminAuthorizationResult;
79
+ try {
80
+ authorization = await authorizeAdmin(req);
81
+ } catch {
82
+ sendBffError(res, "UPSTREAM_UNAVAILABLE", "Admin authorization failed");
83
+ return;
84
+ }
85
+
86
+ if (authorization.ok === false) {
87
+ sendBffError(res, authorization.code, authorization.message);
88
+ return;
89
+ }
90
+
91
+ const request = adminUserInviteRequestSchema.safeParse(req.body);
92
+ if (!request.success) {
93
+ sendBffError(res, "BAD_REQUEST", "Invalid admin user invite request", {
94
+ details: request.error.flatten(),
95
+ });
96
+ return;
97
+ }
98
+
99
+ try {
100
+ const result = await settingsPort.inviteAdminUser(request.data);
101
+ const response = adminUserInviteResponseSchema.safeParse(result);
102
+ if (!response.success) {
103
+ sendBffError(res, "INVALID_RESPONSE", "Admin user invite returned invalid response");
104
+ return;
105
+ }
106
+
107
+ sendBffSuccess(res, response.data);
108
+ } catch (error) {
109
+ const failure = error as { bffCode?: string; inviteDetails?: AdminUserInviteErrorDetails };
110
+ sendBffError(res, (failure.bffCode ?? "UPSTREAM_UNAVAILABLE") as Parameters<typeof sendBffError>[1], errorMessage(error, "Admin user invite failed"), failure.inviteDetails ? { details: failure.inviteDetails } : {});
111
+ }
112
+ };
113
+ }
114
+
115
+ export function createAdminUserRemoveHandler({
116
+ settingsPort,
117
+ authorizeAdmin, responseRequestId,
118
+ }: AdminUserRemoveDeps) {
119
+ const { sendBffError, sendBffSuccess, sendMethodNotAllowed } = createIdentityResponder({ responseRequestId });
120
+ return async function handler(req: HttpRequest, res: HttpResponse): Promise<void> {
121
+ if (req.method !== "POST") {
122
+ sendMethodNotAllowed(res, ["POST"]);
123
+ return;
124
+ }
125
+
126
+ let authorization: PlatformAdminAuthorizationResult;
127
+ try {
128
+ authorization = await authorizeAdmin(req);
129
+ } catch {
130
+ sendBffError(res, "UPSTREAM_UNAVAILABLE", "Admin authorization failed");
131
+ return;
132
+ }
133
+
134
+ if (authorization.ok === false) {
135
+ sendBffError(res, authorization.code, authorization.message);
136
+ return;
137
+ }
138
+
139
+ const request = adminUserRemoveRequestSchema.safeParse(req.body);
140
+ if (!request.success) {
141
+ sendBffError(res, "BAD_REQUEST", "Invalid admin user remove request", {
142
+ details: request.error.flatten(),
143
+ });
144
+ return;
145
+ }
146
+
147
+ try {
148
+ const result = await settingsPort.removeAdminUser(request.data);
149
+ const response = adminUserRemoveResponseSchema.safeParse(result);
150
+ if (!response.success) {
151
+ sendBffError(res, "INVALID_RESPONSE", "Admin user remove returned invalid response");
152
+ return;
153
+ }
154
+
155
+ sendBffSuccess(res, response.data);
156
+ } catch (error) {
157
+ sendMutationError(res, error, "Admin user revoke failed", responseRequestId);
158
+ }
159
+ };
160
+ }
161
+
162
+ function errorMessage(error: unknown, fallback: string): string {
163
+ return error instanceof Error && error.message ? error.message : fallback;
164
+ }
165
+
166
+ function sendMutationError(res: HttpResponse, error: unknown, fallback: string, responseRequestId: IdentityResponseOptions["responseRequestId"]): void {
167
+ const { sendBffError } = createIdentityResponder({ responseRequestId });
168
+ const bffCode = (error as { bffCode?: string })?.bffCode;
169
+ if (bffCode) {
170
+ sendBffError(res, bffCode as Parameters<typeof sendBffError>[1], errorMessage(error, fallback));
171
+ return;
172
+ }
173
+ sendBffError(res, "UPSTREAM_UNAVAILABLE", errorMessage(error, fallback));
174
+ }
@@ -0,0 +1,56 @@
1
+ import type { HttpRequest } from "../http/index.js";
2
+
3
+ function authVerifyType(actionType: string): string {
4
+ return actionType === "email_change_current" || actionType === "email_change_new"
5
+ ? "email_change"
6
+ : actionType;
7
+ }
8
+
9
+
10
+ function qpSafeTokenValue(token: string): string {
11
+ if (token.length === 0) return token;
12
+ return `%${token.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}${encodeURIComponent(token.slice(1))}`;
13
+ }
14
+
15
+ export function buildAuthVerifyUrl(authServiceUrl: string, actionType: string, tokenHash: string, redirectTo: string): string {
16
+ const params = new URLSearchParams({ type: authVerifyType(actionType), redirect_to: redirectTo });
17
+ return `${authServiceUrl.replace(/\/+$/, "")}/auth/v1/verify?token=${qpSafeTokenValue(tokenHash)}&${params.toString()}`;
18
+ }
19
+
20
+ /** Only configured HTTP(S) origins authorize the default magic-link destination. */
21
+ export function createMagicLinkOriginResolver(
22
+ configuredOrigins: readonly string[],
23
+ ): (req: HttpRequest) => string | null {
24
+ const origins = configuredOrigins.map(normalizeOrigin);
25
+ const origin = origins.length > 0 && origins.every((value) => value !== null)
26
+ ? (origins[0] ?? null)
27
+ : null;
28
+ return () => origin;
29
+ }
30
+
31
+ function normalizeOrigin(value: string): string | null {
32
+ const candidate = value.trim();
33
+ // URL parsing alone accepts paths, credentials and repaired backslashes.
34
+ if (!/^https?:\/\/[^/?#\\\s]+\/?$/i.test(candidate)) return null;
35
+ try {
36
+ const url = new URL(candidate);
37
+ if (url.username || url.password || url.pathname !== "/" || url.search || url.hash) return null;
38
+ return url.origin;
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ /** Invalid return targets use the callback's default landing page. */
45
+ export function normalizeMagicLinkReturnPath(value: string | undefined, origin: string): string | undefined {
46
+ if (!value || !value.startsWith("/") || value.startsWith("//")) return undefined;
47
+ // Reject encoded separators, controls and nested escapes before any later decoding.
48
+ if (value.includes("\\") || Array.from(value).some((character) => character.charCodeAt(0) <= 0x20 || character.charCodeAt(0) === 0x7f) || /%(?:2f|5c|0[0-9a-f]|1[0-9a-f]|7f|25)/i.test(value)) return undefined;
49
+ try {
50
+ const url = new URL(value, origin);
51
+ if (url.origin !== new URL(origin).origin || url.pathname.startsWith("//")) return undefined;
52
+ return `${url.pathname}${url.search}${url.hash}`;
53
+ } catch {
54
+ return undefined;
55
+ }
56
+ }
@@ -0,0 +1,15 @@
1
+ import type { EmailTrustFailureCode } from "./emailTrust.js";
2
+ export const AUTH_RECONCILE_CONTRACT_VERSION = "auth.reconcile.v1" as const;
3
+ export interface AuthReconcileResponse {
4
+ accountId: string;
5
+ created: boolean;
6
+ linked: boolean;
7
+ }
8
+ export type AuthReconcileErrorCode =
9
+ | EmailTrustFailureCode
10
+ | "NO_VERIFIED_EMAIL"
11
+ | "RESERVED_PRINCIPAL_REFUSED"
12
+ | "ACCOUNT_LINK_CONFLICT"
13
+ | "EMAIL_OWNED_BY_OTHER_PRINCIPAL"
14
+ | "PRINCIPAL_EMAIL_MISMATCH";
15
+ // Runtime schemas remain in reference composition until the validator boundary is resolved.
@@ -0,0 +1,5 @@
1
+ /** Browser-safe email-association refusal codes. */
2
+ export const EMAIL_TRUST_FAILURE_CODES = [
3
+ "EMAIL_TRUST_ENVELOPE_REFUSED", "EMAIL_TRUST_SETTINGS_UNAVAILABLE",
4
+ ] as const;
5
+ export type EmailTrustFailureCode = typeof EMAIL_TRUST_FAILURE_CODES[number];
@@ -0,0 +1,8 @@
1
+ export * from "./types.js";
2
+ export * from "./ports.js";
3
+ export * from "./providerList.js";
4
+ export * from "./contracts.js";
5
+ export * from "./emailTrust.js";
6
+ export * from "./operatorInvitations.js";
7
+ export * from "./operatorAuth.js";
8
+ export * from "./privacy.js";
@@ -0,0 +1,10 @@
1
+ /** Credential and dependency refusals; no token or transport details are exposed. */
2
+ export const OIDC_FAILURE_CODES = [
3
+ "OIDC_CONFIGURATION_INVALID", "OIDC_DISCOVERY_UNAVAILABLE", "OIDC_DISCOVERY_INVALID",
4
+ "OIDC_JWKS_UNAVAILABLE", "OIDC_JWKS_INVALID", "OIDC_JWKS_REFRESH_RATE_LIMITED",
5
+ "OIDC_JWT_LIBRARY_UNAVAILABLE", "OIDC_TOKEN_INVALID", "OIDC_KID_UNKNOWN",
6
+ "OIDC_ISSUER_MISMATCH", "OIDC_AUDIENCE_MISMATCH", "OIDC_TOKEN_EXPIRED",
7
+ "OIDC_TOKEN_NOT_ACTIVE", "OIDC_TOKEN_LIFETIME_INVALID", "OIDC_SUBJECT_INVALID",
8
+ "OIDC_EMAIL_INVALID", "OIDC_EMAIL_UNVERIFIED", "OIDC_NATIVE_SESSION_REQUIRED",
9
+ ] as const;
10
+ export type OidcFailureCode = typeof OIDC_FAILURE_CODES[number];
@@ -0,0 +1,61 @@
1
+ export interface OperatorSession {
2
+ accessToken: string;
3
+ expiresAt: string;
4
+ user: { id: string; email: string };
5
+ }
6
+ export interface OperatorChallengeRequest { email: string }
7
+ export interface OperatorChallengeVerification { token: string }
8
+ export interface OperatorBrowserAuthPort {
9
+ getSession(): Promise<OperatorSession | null>;
10
+ requestChallenge(email: string): Promise<void>;
11
+ verifyChallenge(token: string): Promise<OperatorSession>;
12
+ signOut(): Promise<void>;
13
+ onAuthStateChange(listener: (session: OperatorSession | null) => void): () => void;
14
+ }
15
+ /** Strict parsers keep the contracts entry independent of the server validator. */
16
+ export interface OperatorSchema<T> {
17
+ parse(value: unknown): T;
18
+ safeParse(value: unknown): { success: true; data: T } | { success: false; error: Error };
19
+ }
20
+ function schema<T>(parse: (value: unknown) => T): OperatorSchema<T> {
21
+ return { parse, safeParse(value) {
22
+ try { return { success: true, data: parse(value) }; }
23
+ catch { return { success: false, error: new Error("operator_contract_invalid") }; }
24
+ } };
25
+ }
26
+ function fields(value: unknown, keys: string[]): Record<string, unknown> {
27
+ if (!value || typeof value !== "object" || Array.isArray(value)
28
+ || Object.keys(value).length !== keys.length || !keys.every(key => Object.hasOwn(value, key))) throw new Error("operator_contract_invalid");
29
+ return value as Record<string, unknown>;
30
+ }
31
+ function email(value: unknown): string {
32
+ if (typeof value !== "string" || value.length > 320 || !/^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+$/i.test(value)) throw new Error("operator_email_invalid");
33
+ return value;
34
+ }
35
+ export const operatorChallengeRequestSchema: OperatorSchema<OperatorChallengeRequest> = schema(value => {
36
+ const row = fields(value, ["email"]);
37
+ if (typeof row.email !== "string") throw new Error("operator_email_invalid");
38
+ return { email: email(row.email.trim().toLowerCase()) };
39
+ });
40
+ export const operatorChallengeVerificationSchema: OperatorSchema<OperatorChallengeVerification> = schema(value => {
41
+ const row = fields(value, ["token"]);
42
+ if (typeof row.token !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(row.token)) throw new Error("operator_token_invalid");
43
+ return { token: row.token };
44
+ });
45
+ function timestamp(value: unknown): value is string {
46
+ if (typeof value !== "string") return false;
47
+ const parts = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)(?:\.\d+)?(?:Z|[+-]\d\d:\d\d)$/.exec(value);
48
+ if (!parts || !Number.isFinite(Date.parse(value))) return false;
49
+ const [, year, month, day, hour, minute, second] = parts.map(Number);
50
+ return year! >= 1970 && month! >= 1 && month! <= 12 && day! >= 1
51
+ && day! <= new Date(Date.UTC(year!, month!, 0)).getUTCDate()
52
+ && hour! <= 23 && minute! <= 59 && second! <= 59;
53
+ }
54
+ export const operatorSessionSchema: OperatorSchema<OperatorSession> = schema(value => {
55
+ const row = fields(value, ["accessToken", "expiresAt", "user"]);
56
+ const user = fields(row.user, ["id", "email"]);
57
+ if (typeof row.accessToken !== "string" || !row.accessToken || row.accessToken.length > 8192
58
+ || !timestamp(row.expiresAt)
59
+ || typeof user.id !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(user.id)) throw new Error("operator_session_invalid");
60
+ return { accessToken: row.accessToken, expiresAt: row.expiresAt, user: { id: user.id, email: email(user.email) } };
61
+ });