@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,32 @@
1
+ /** Pending invitations confer no authority; only accepted native memberships do. */
2
+ export type OperatorRole = "admin" | "distributor";
3
+ export type OperatorMembershipState = "invited" | "active" | "expired" | "revoked";
4
+ export type OperatorInvitationOutcome = "INVITATION_CREATED" | "ALREADY_INVITED" | "INVITATION_REISSUED";
5
+ export type OperatorInvitationConflict = "MEMBERSHIP_ALREADY_ACTIVE" | "INVITATION_ROLE_CONFLICT" | "IDENTITY_CONFLICT" | "MANAGED_REINVITE_UNSUPPORTED";
6
+ export interface OperatorInvitation {
7
+ principalId: string;
8
+ invitationId: string;
9
+ membershipState: "invited";
10
+ role: OperatorRole;
11
+ invitedAt: string;
12
+ expiresAt: string;
13
+ }
14
+ export interface OperatorInvitationResult extends OperatorInvitation { code: OperatorInvitationOutcome; message?: string }
15
+ export interface OperatorMembershipSnapshot {
16
+ principalId: string;
17
+ membershipState: OperatorMembershipState;
18
+ role: OperatorRole;
19
+ }
20
+ export type OperatorInvitationAcceptance =
21
+ | { code: "ACTIVATED" | "ALREADY_ACTIVE"; role: OperatorRole }
22
+ | { code: "INVITATION_EXPIRED" | "INVITATION_REVOKED" | "NO_INVITATION" };
23
+
24
+ /** Managed invitations activate membership immediately; no pending attempt is stored. */
25
+ export interface ManagedOperatorMembershipResult {
26
+ principalId: string;
27
+ membershipState: "active";
28
+ role: OperatorRole;
29
+ code: "MEMBERSHIP_CREATED" | "ALREADY_ACTIVE";
30
+ message?: string;
31
+ }
32
+ export type OperatorInviteResult = OperatorInvitationResult | ManagedOperatorMembershipResult;
@@ -0,0 +1,25 @@
1
+ import type {
2
+ AuthActionResult,
3
+ AuthSession,
4
+ AuthStateListener,
5
+ OAuthProviderKind,
6
+ Unsubscribe,
7
+ } from "./types.js";
8
+
9
+
10
+ export interface CustomerAuthPort {
11
+ getSession(): Promise<AuthSession | null>;
12
+ onAuthStateChange(listener: AuthStateListener): Unsubscribe;
13
+ signInWithOtp(input: { email: string; locale: string }): Promise<AuthActionResult>;
14
+
15
+ verifyOtpCode(input: { email: string; token: string }): Promise<AuthActionResult>;
16
+ signInWithOAuth(input: {
17
+ provider: OAuthProviderKind;
18
+ redirectTo: string;
19
+ }): Promise<AuthActionResult>;
20
+ linkIdentity(input: {
21
+ provider: OAuthProviderKind;
22
+ redirectTo: string;
23
+ }): Promise<AuthActionResult>;
24
+ signOut(): Promise<void>;
25
+ }
@@ -0,0 +1,86 @@
1
+ import type { OperatorSchema } from "./operatorAuth.js";
2
+
3
+ export type PrivacySubjectRef = { kind: "contact"; contactId: string } | { kind: "principal"; principalId: string };
4
+ export type PrivacyJson = null | boolean | number | string | PrivacyJson[] | { [key: string]: PrivacyJson };
5
+ export interface PersonalDataObligation { owner: string; resource: string; disposition: "erase" | "retain" | "unresolved"; reasonCode: string; remote: boolean }
6
+ export interface PersonalDataDocument {
7
+ contributorId: string; version: string; data: PrivacyJson;
8
+ coverage: { complete: boolean; included: string[]; unavailable: string[] };
9
+ obligations: PersonalDataObligation[];
10
+ }
11
+ export interface ErasureReceipt { version: "identity.erasure.v1"; erasureId: string; completedAt: string; outcome: "ERASED" | "ALREADY_ERASED"; obligations: PersonalDataObligation[] }
12
+ export interface PrivacyEraseRequest { subject: PrivacySubjectRef; idempotencyKey: string }
13
+ export interface PrivacyExport { format: "personal-data.v1"; generatedAt: string; subject: PrivacySubjectRef; documents: PersonalDataDocument[]; missingContributors: string[]; erasure: ErasureReceipt | null }
14
+ function invalid(): never { throw new Error("privacy_contract_invalid"); }
15
+ function schema<T>(parse: (value: unknown) => T): OperatorSchema<T> {
16
+ return { parse, safeParse(value) { try { return { success: true, data: parse(value) }; } catch { return { success: false, error: new Error("privacy_contract_invalid") }; } } };
17
+ }
18
+ function fields(value: unknown, keys: string[]): Record<string, unknown> {
19
+ if (!value || typeof value !== "object" || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype
20
+ || Object.keys(value).length !== keys.length || !keys.every(key => Object.hasOwn(value, key))) invalid();
21
+ return value as Record<string, unknown>;
22
+ }
23
+ function text(value: unknown, max = 200): string {
24
+ if (typeof value !== "string" || !value.length || value.length > max) invalid();
25
+ return value;
26
+ }
27
+ function uuid(value: unknown): string {
28
+ const result = text(value, 36);
29
+ if (!/^[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(result)) invalid();
30
+ return result.toLowerCase();
31
+ }
32
+ function timestamp(value: unknown): string {
33
+ const result = text(value, 30);
34
+ if (!/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d{3})?Z$/.test(result) || !Number.isFinite(Date.parse(result))
35
+ || new Date(result).toISOString() !== result.replace(/Z$/, result.length === 20 ? ".000Z" : "Z")) invalid();
36
+ return result;
37
+ }
38
+ function list<T>(value: unknown, parse: (element: unknown) => T): T[] {
39
+ if (!Array.isArray(value) || value.length > 10000) invalid();
40
+ return value.map(element => parse(element));
41
+ }
42
+ function boolean(value: unknown): boolean { if (typeof value !== "boolean") invalid(); return value; }
43
+ function choice<T extends string>(value: unknown, allowed: readonly T[]): T { if (!allowed.includes(value as T)) invalid(); return value as T; }
44
+ function json(value: unknown, depth = 0): PrivacyJson {
45
+ if (depth > 32) invalid();
46
+ if (value === null || typeof value === "boolean" || typeof value === "string") { if (typeof value === "string" && value.length > 1000000) invalid(); return value; }
47
+ if (typeof value === "number" && Number.isFinite(value)) return value;
48
+ if (Array.isArray(value)) return list(value, element => json(element, depth + 1));
49
+ if (!value || typeof value !== "object" || Object.getPrototypeOf(value) !== Object.prototype) invalid();
50
+ const forbidden = new Set(["__proto__", "constructor", "prototype", "password", "password_hash", "token_hash", "access_token", "refresh_token", "accessToken", "refreshToken", "secret", "credentials"]);
51
+ const entries = Object.entries(value);
52
+ if (entries.length > 10000 || entries.some(([key]) => forbidden.has(key))) invalid();
53
+ return Object.fromEntries(entries.map(([key, child]) => [key, json(child, depth + 1)]));
54
+ }
55
+ export const privacySubjectSchema: OperatorSchema<PrivacySubjectRef> = schema(value => {
56
+ if (!value || typeof value !== "object" || !("kind" in value)) invalid();
57
+ if (value.kind === "contact") { const row = fields(value, ["kind", "contactId"]); return { kind: "contact", contactId: uuid(row.contactId) }; }
58
+ const row = fields(value, ["kind", "principalId"]);
59
+ return { kind: choice(row.kind, ["principal"]), principalId: uuid(row.principalId) };
60
+ });
61
+ export const personalDataObligationSchema: OperatorSchema<PersonalDataObligation> = schema(value => {
62
+ const row = fields(value, ["owner", "resource", "disposition", "reasonCode", "remote"]);
63
+ return { owner: text(row.owner, 100), resource: text(row.resource), disposition: choice(row.disposition, ["erase", "retain", "unresolved"]), reasonCode: text(row.reasonCode, 100), remote: boolean(row.remote) };
64
+ });
65
+ export const personalDataDocumentSchema: OperatorSchema<PersonalDataDocument> = schema(value => {
66
+ const row = fields(value, ["contributorId", "version", "data", "coverage", "obligations"]), coverage = fields(row.coverage, ["complete", "included", "unavailable"]);
67
+ const contributorId = text(row.contributorId, 100), version = text(row.version);
68
+ if (!/^[a-z][a-z0-9.-]{0,99}$/.test(contributorId) || !/^[a-z][a-z0-9.-]*\.v[1-9][0-9]*$/.test(version)) invalid();
69
+ return { contributorId, version, data: json(row.data), coverage: { complete: boolean(coverage.complete), included: list(coverage.included, text), unavailable: list(coverage.unavailable, text) }, obligations: list(row.obligations, personalDataObligationSchema.parse) };
70
+ });
71
+ export const erasureReceiptSchema: OperatorSchema<ErasureReceipt> = schema(value => {
72
+ const row = fields(value, ["version", "erasureId", "completedAt", "outcome", "obligations"]);
73
+ return { version: choice(row.version, ["identity.erasure.v1"]), erasureId: uuid(row.erasureId), completedAt: timestamp(row.completedAt), outcome: choice(row.outcome, ["ERASED", "ALREADY_ERASED"]), obligations: list(row.obligations, personalDataObligationSchema.parse) };
74
+ });
75
+ export const privacyExportRequestSchema: OperatorSchema<{ subject: PrivacySubjectRef }> = schema(value => ({ subject: privacySubjectSchema.parse(fields(value, ["subject"]).subject) }));
76
+ export const privacyEraseRequestSchema: OperatorSchema<PrivacyEraseRequest> = schema(value => {
77
+ const row = fields(value, ["subject", "idempotencyKey"]); return { subject: privacySubjectSchema.parse(row.subject), idempotencyKey: uuid(row.idempotencyKey) };
78
+ });
79
+ export const privacyExportResponseSchema: OperatorSchema<PrivacyExport> = schema(value => {
80
+ const row = fields(value, ["format", "generatedAt", "subject", "documents", "missingContributors", "erasure"]);
81
+ return { format: choice(row.format, ["personal-data.v1"]), generatedAt: timestamp(row.generatedAt), subject: privacySubjectSchema.parse(row.subject), documents: list(row.documents, personalDataDocumentSchema.parse), missingContributors: list(row.missingContributors, text), erasure: row.erasure === null ? null : erasureReceiptSchema.parse(row.erasure) };
82
+ });
83
+ export const privacyEraseResponseSchema = erasureReceiptSchema;
84
+ const refusalCodes = ["PRIVACY_SUBJECT_NOT_FOUND", "PRIVACY_CONTRIBUTOR_DUPLICATE", "PRIVACY_CONTRIBUTOR_MISSING", "PRIVACY_ASSESSMENT_UNAVAILABLE", "PRIVACY_OWNERSHIP_UNDECLARED", "PRIVACY_ACTIVE_SUBSCRIPTION", "PRIVACY_RESUMABLE_SUBSCRIPTION", "PRIVACY_PROTECTED_OPERATOR", "PRIVACY_IDEMPOTENCY_CONFLICT", "PRIVACY_SUBJECT_ERASED", "PRIVACY_STORE_PRIVILEGES_UNSAFE", "MANAGED_PRIVACY_ERASURE_UNAVAILABLE"] as const;
85
+ export type PrivacyRefusalCode = typeof refusalCodes[number];
86
+ export const privacyRefusalCodeSchema: OperatorSchema<PrivacyRefusalCode> = schema(value => choice(value, refusalCodes));
@@ -0,0 +1,13 @@
1
+ import { isOAuthProviderKind, type OAuthProviderKind } from "./types.js";
2
+
3
+ export const DEFAULT_SOCIAL_PROVIDERS: readonly OAuthProviderKind[] = ["google"];
4
+
5
+
6
+ export function parseSocialProviders(raw: string | undefined): OAuthProviderKind[] {
7
+ if (raw === undefined) return [...DEFAULT_SOCIAL_PROVIDERS];
8
+ const kinds = raw
9
+ .split(",")
10
+ .map((token) => token.trim().toLowerCase())
11
+ .filter((token) => isOAuthProviderKind(token));
12
+ return [...new Set(kinds)];
13
+ }
@@ -0,0 +1,25 @@
1
+ export const OAUTH_PROVIDER_KINDS = ["google", "apple", "facebook"] as const;
2
+ export type OAuthProviderKind = (typeof OAUTH_PROVIDER_KINDS)[number];
3
+
4
+ export function isOAuthProviderKind(value: string): value is OAuthProviderKind {
5
+ return (OAUTH_PROVIDER_KINDS as readonly string[]).includes(value);
6
+ }
7
+
8
+
9
+ export interface AuthUser {
10
+ id: string;
11
+ email: string | null;
12
+ }
13
+
14
+ export interface AuthSession {
15
+ accessToken: string;
16
+ user: AuthUser;
17
+ }
18
+
19
+ export type AuthStateListener = (session: AuthSession | null) => void;
20
+ export type Unsubscribe = () => void;
21
+
22
+
23
+ export interface AuthActionResult {
24
+ error: string | null;
25
+ }
@@ -0,0 +1,29 @@
1
+ import type { Contribution, RequiredSchema } from "@openlup/core/readiness";
2
+
3
+ /** Host-selected schema inventory: managed and direct installations have different objects. */
4
+ export function createIdentityContribution(options: {
5
+ version: string;
6
+ requiredSchema: RequiredSchema;
7
+ routes?: Contribution["routes"];
8
+ requiredPorts?: readonly string[];
9
+ }): Contribution {
10
+ const routes = options.routes ?? [];
11
+ return {
12
+ manifest: {
13
+ name: "@openlup/platform", version: options.version, kind: "rail",
14
+ module: {
15
+ id: "identity", kind: "capability", provides: ["./identity", "./identity/contracts"],
16
+ dependsOn: [
17
+ { module: "bff", entries: ["./bff"] },
18
+ { module: "data", entries: ["./data"] },
19
+ { module: "kernel", entries: ["./kernel"] },
20
+ { module: "http", entries: ["./http"] },
21
+ ],
22
+ },
23
+ requiredSchema: options.requiredSchema,
24
+ emits: [], handles: [], schedules: [], routes: routes.map((route) => route.id),
25
+ requiredPorts: [...new Set(["emailTrustPolicy", ...(options.requiredPorts ?? [])])], env: [],
26
+ },
27
+ handlers: [], schedules: [], routes,
28
+ };
29
+ }
@@ -0,0 +1,3 @@
1
+ export type CustomerUserAuthenticationResult =
2
+ | { ok: true; userId: string }
3
+ | { ok: false; code: "UNAUTHORIZED"; message: string };
@@ -0,0 +1,38 @@
1
+ import type { CustomerUserAuthenticationResult } from "./customerAuth.js";
2
+ import type { IdentityVerifierPort } from "./ports.js";
3
+ import type { ManagedIdentityClient } from "./managedClient.js";
4
+ import { createSupabaseIdentityVerifier } from "./managedIdentityVerifier.js";
5
+ export async function authenticateCustomerUserWith(
6
+ verifier: IdentityVerifierPort,
7
+ accessToken: string | null,
8
+ ): Promise<CustomerUserAuthenticationResult> {
9
+ if (!accessToken) {
10
+ return { ok: false, code: "UNAUTHORIZED", message: "Customer session required" };
11
+ }
12
+
13
+ const principal = await verifier.verifyAccessToken(accessToken);
14
+ if (!principal) {
15
+ return { ok: false, code: "UNAUTHORIZED", message: "Customer session required" };
16
+ }
17
+
18
+ return { ok: true, userId: principal.principalId };
19
+ }
20
+
21
+
22
+ export async function optionalCustomerSubject(
23
+ rawClient: unknown,
24
+ accessToken: string | null,
25
+ ): Promise<string | null> {
26
+ const client = rawClient as ManagedIdentityClient;
27
+ if (!accessToken) return null;
28
+ const result = await authenticateCustomerUser(client, accessToken);
29
+ return result.ok ? result.userId : null;
30
+ }
31
+
32
+ export async function authenticateCustomerUser(
33
+ rawClient: unknown,
34
+ accessToken: string | null,
35
+ ): Promise<CustomerUserAuthenticationResult> {
36
+ const client = rawClient as ManagedIdentityClient;
37
+ return authenticateCustomerUserWith(createSupabaseIdentityVerifier(client), accessToken);
38
+ }
@@ -0,0 +1,52 @@
1
+ import { z } from "zod";
2
+ z.config({ jitless: true });
3
+ const platformCurrencySchema = z.string().regex(/^[A-Z]{3}$/);
4
+ export const customerLifecycleStageSchema = z.enum([
5
+ "lead",
6
+ "tester",
7
+ "customer",
8
+ "inactive",
9
+ ]);
10
+
11
+ export const customerMeRequestSchema = z.object({}).strict();
12
+
13
+ export const customerMeResponseSchema = z.object({
14
+ clientId: z.string().min(1),
15
+ email: z.string().min(1),
16
+ firstName: z.string().nullable(),
17
+ lastName: z.string().nullable(),
18
+ lifecycleStage: customerLifecycleStageSchema,
19
+ subscriptions: z
20
+ .array(
21
+ z
22
+ .object({
23
+ subscriptionId: z.guid(),
24
+ status: z.string().min(1),
25
+ cadenceDays: z.number().int().positive(),
26
+ nextCycleAt: z.string().datetime({ offset: true }).nullable(),
27
+ paymentMethodKind: z.string().nullable(),
28
+ })
29
+ .strict(),
30
+ )
31
+ .optional(),
32
+ recentOrders: z
33
+ .array(
34
+ z
35
+ .object({
36
+ orderId: z.guid(),
37
+ orderRef: z.string().regex(/^order_[a-z0-9-]+$/),
38
+ status: z.string().min(1),
39
+ mode: z.enum(["one_time", "subscription_cycle"]),
40
+ total: z.object({
41
+ amountMinor: z.number().int().nonnegative(),
42
+ currency: platformCurrencySchema,
43
+ }),
44
+ createdAt: z.string().datetime({ offset: true }),
45
+ })
46
+ .strict(),
47
+ )
48
+ .optional(),
49
+ });
50
+
51
+
52
+ export type CustomerMeResponse = z.infer<typeof customerMeResponseSchema>;
@@ -0,0 +1,125 @@
1
+ import type { IdentityJwtLibraryLoader } from "./jwtLibrary.js";
2
+ import type { ManagedIdentityClient } from "./managedClient.js";
3
+ import {
4
+ authenticateCustomerUser,
5
+ authenticateCustomerUserWith,
6
+ } from "./customerAuthorization.js";
7
+ import {
8
+ readBearerToken,
9
+ } from "./adminAuthorization.js";
10
+ import { createPostgresCustomerMePort } from "./postgresCustomerMe.js";
11
+ import type { PgGatewayClient } from "../data/index.js";
12
+ import { createSupabaseCustomerMePort as createManagedCustomerMePort } from "./managedCustomerMe.js";
13
+ import { createCustomerIdentityVerifier } from "./customerIdentityVerifier.js";
14
+ import type { CustomerMeDeps } from "./customerMeHandler.js";
15
+ import type { CustomerMePort } from "./customerPorts.js";
16
+ import { resolveBundleId } from "../kernel/index.js";
17
+
18
+ type Env = Record<string, string | undefined>;
19
+ type CustomerRequest = Parameters<typeof readBearerToken>[0];
20
+ type ManagedReadEnv = { url: string; anonKey: string };
21
+ type CreateCustomerClient = (env: ManagedReadEnv, token: string | null) => unknown;
22
+ type ActorGateway = {
23
+ asActor<T>(
24
+ claims: { role: "authenticated"; sub: string },
25
+ work: (gateway: unknown) => Promise<T>,
26
+ ): Promise<T>;
27
+ };
28
+
29
+ export type CustomerIdentityBindingOptions = {
30
+ authenticateManaged?: typeof authenticateCustomerUser;
31
+ createManagedClient?: CreateCustomerClient;
32
+ createManagedPort?: typeof createManagedCustomerMePort;
33
+ readManagedEnv?: () => ManagedReadEnv | null;
34
+ readToken?: typeof readBearerToken;
35
+ createPostgresGateway?: (env: Env) => ActorGateway;
36
+ createPostgresPort?: typeof createPostgresCustomerMePort;
37
+ createVerifier?: (config: Parameters<typeof createCustomerIdentityVerifier>[0]) => ReturnType<typeof createCustomerIdentityVerifier>;
38
+ resolveBundle?: typeof resolveBundleId;
39
+ };
40
+
41
+ /**
42
+ * Select the customer identity/read composition for one request. The direct
43
+ * path constructs its profile port only inside one actor-scoped transaction;
44
+ * it never uses a generic service lane or caller-provided token role.
45
+ */
46
+ export function createCustomerIdentityBindingFactory(defaults: {
47
+ loadJwt: IdentityJwtLibraryLoader;
48
+ createManagedClient: CreateCustomerClient;
49
+ readManagedEnv: (env: Env) => ManagedReadEnv | null;
50
+ bindRequestActorDataPort: (token: string, env: Env) => ActorGateway | null;
51
+ }) {
52
+ return function createCustomerIdentityBinding(
53
+ req: CustomerRequest,
54
+ env: Env,
55
+ options: CustomerIdentityBindingOptions = {},
56
+ ): Omit<CustomerMeDeps, "responseSchema"> | null {
57
+ const accessToken = (options.readToken ?? readBearerToken)(req);
58
+ const bundleId = (options.resolveBundle ?? resolveBundleId)(env);
59
+
60
+ if (bundleId !== "node-postgres") {
61
+ const readEnv = options.readManagedEnv ?? (() => defaults.readManagedEnv(env));
62
+ const managedEnv = readEnv();
63
+ if (!managedEnv) return null;
64
+ const client = (options.createManagedClient ?? defaults.createManagedClient)(
65
+ managedEnv,
66
+ accessToken,
67
+ );
68
+ return {
69
+ authenticateUser: () =>
70
+ (options.authenticateManaged ?? authenticateCustomerUser)(
71
+ client,
72
+ accessToken,
73
+ ),
74
+ mePort: (options.createManagedPort ?? createManagedCustomerMePort)(
75
+ client,
76
+ ),
77
+ };
78
+ }
79
+
80
+ const gateway = (options.createPostgresGateway ?? createActorGateway)(env);
81
+ const createPort = options.createPostgresPort ?? createPostgresCustomerMePort;
82
+ const createVerifier =
83
+ options.createVerifier ?? ((config) => createCustomerIdentityVerifier(config, defaults.loadJwt));
84
+ const verifierConfig = {
85
+ audience: env.CUSTOMER_IDENTITY_AUDIENCE ?? "",
86
+ issuer: env.CUSTOMER_IDENTITY_ISSUER ?? "",
87
+ jwks: env.CUSTOMER_IDENTITY_JWKS ?? "",
88
+ };
89
+ let verifiedPrincipalId: string | null = null;
90
+ const mePort: CustomerMePort = {
91
+ getCustomerMe: (principalId) => {
92
+ if (principalId !== verifiedPrincipalId) {
93
+ return Promise.reject(new Error("customer_principal_mismatch"));
94
+ }
95
+ return gateway.asActor(
96
+ { role: "authenticated", sub: verifiedPrincipalId },
97
+ (actorGateway) =>
98
+ createPort(actorGateway as PgGatewayClient).getCustomerMe(
99
+ principalId,
100
+ ),
101
+ );
102
+ },
103
+ };
104
+
105
+ return {
106
+ async authenticateUser() {
107
+ const authentication = await authenticateCustomerUserWith(
108
+ createVerifier(verifierConfig),
109
+ accessToken,
110
+ );
111
+ if (authentication.ok) verifiedPrincipalId = authentication.userId;
112
+ return authentication;
113
+ },
114
+ mePort,
115
+ };
116
+ }
117
+
118
+ function createActorGateway(env: Env): ActorGateway {
119
+ const gateway = defaults.bindRequestActorDataPort("", env);
120
+ if (!gateway)
121
+ throw new Error("customer identity data gateway is unavailable");
122
+ return gateway as ActorGateway;
123
+ }
124
+
125
+ }
@@ -0,0 +1,141 @@
1
+ import type { IdentityJwtLibrary, IdentityJwtLibraryLoader, IdentityJwk } from "./jwtLibrary.js";
2
+ import type { IdentityVerifierPort, VerifiedPrincipal } from "./ports.js";
3
+
4
+ const MAX_JWKS_BYTES = 12_288;
5
+ const MAX_TOKEN_BYTES = 8_192;
6
+ const MAX_CLAIM_BYTES = 256;
7
+ const KID = /^[A-Za-z0-9._-]{1,64}$/;
8
+ const BASE64URL_COORDINATE = /^[A-Za-z0-9_-]{43}$/;
9
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
10
+ const JWK_FIELDS = new Set(["alg", "crv", "kid", "kty", "use", "x", "y"]);
11
+ type VerificationKey = Awaited<ReturnType<IdentityJwtLibrary["importJWK"]>>;
12
+
13
+ export interface CustomerIdentityVerifierConfig {
14
+ audience: string;
15
+ issuer: string;
16
+ jwks: string;
17
+ }
18
+
19
+ interface ExactVerificationJwk extends IdentityJwk {
20
+ alg: "ES256";
21
+ crv: "P-256";
22
+ kid: string;
23
+ kty: "EC";
24
+ use: "sig";
25
+ x: string;
26
+ y: string;
27
+ }
28
+
29
+
30
+ export function createCustomerIdentityVerifier(
31
+ config: CustomerIdentityVerifierConfig,
32
+ loadJwt: IdentityJwtLibraryLoader,
33
+ ): IdentityVerifierPort {
34
+ const issuer = readClaim(config.issuer, "issuer");
35
+ const audience = readClaim(config.audience, "audience");
36
+ const jwks = readJwks(config.jwks);
37
+ let keys: Promise<Map<string, VerificationKey>> | null = null;
38
+ let library: Promise<IdentityJwtLibrary> | undefined;
39
+ const getLibrary = () => library ??= loadJwt();
40
+ const importedKeys = () => keys ??= getLibrary().then(({ importJWK }) => Promise.all(
41
+ jwks.map(async (jwk) => [jwk.kid, await importJWK(jwk, "ES256")] as const),
42
+ )).then((entries) => new Map<string, VerificationKey>(entries));
43
+
44
+ return {
45
+ async verifyAccessToken(accessToken: string): Promise<VerifiedPrincipal | null> {
46
+ if (typeof accessToken !== "string" || accessToken.length === 0 || accessToken.length > MAX_TOKEN_BYTES) {
47
+ return null;
48
+ }
49
+ const resolvedKeys = await importedKeys();
50
+ const { jwtVerify } = await getLibrary();
51
+
52
+ try {
53
+ const verified = await jwtVerify(
54
+ accessToken,
55
+ async (header) => {
56
+ if (header.alg !== "ES256" || typeof header.kid !== "string" || !KID.test(header.kid)) {
57
+ throw new Error("customer identity token header rejected");
58
+ }
59
+ const key = resolvedKeys.get(header.kid);
60
+ if (!key) throw new Error("customer identity token key rejected");
61
+ return key;
62
+ },
63
+ { algorithms: ["ES256"], audience, issuer },
64
+ );
65
+ if (typeof verified.payload.sub !== "string" || !UUID.test(verified.payload.sub)) return null;
66
+ return { email: null, emailVerified: false, principalId: verified.payload.sub };
67
+ } catch {
68
+ return null;
69
+ }
70
+ },
71
+ };
72
+ }
73
+
74
+ function readClaim(value: string, label: string): string {
75
+ if (
76
+ typeof value !== "string"
77
+ || value.length === 0
78
+ || value.length > MAX_CLAIM_BYTES
79
+ || value.trim() !== value
80
+ || [...value].some((character) => {
81
+ const code = character.charCodeAt(0);
82
+ return code <= 0x1f || code === 0x7f;
83
+ })
84
+ ) {
85
+ throw new Error(`customer identity ${label} configuration is invalid`);
86
+ }
87
+ return value;
88
+ }
89
+
90
+ function readJwks(value: string): ExactVerificationJwk[] {
91
+ if (typeof value !== "string" || value.length === 0 || value.length > MAX_JWKS_BYTES) {
92
+ throw new Error("customer identity JWKS configuration is invalid");
93
+ }
94
+
95
+ let parsed: unknown;
96
+ try {
97
+ parsed = JSON.parse(value);
98
+ } catch {
99
+ throw new Error("customer identity JWKS configuration is invalid");
100
+ }
101
+ if (!isRecord(parsed) || !hasOnlyFields(parsed, new Set(["keys"])) || !Array.isArray(parsed.keys)) {
102
+ throw new Error("customer identity JWKS configuration is invalid");
103
+ }
104
+ if (parsed.keys.length < 1 || parsed.keys.length > 2) {
105
+ throw new Error("customer identity JWKS configuration is invalid");
106
+ }
107
+
108
+ const keys = parsed.keys.map(readJwk);
109
+ if (new Set(keys.map((key) => key.kid)).size !== keys.length) {
110
+ throw new Error("customer identity JWKS configuration is invalid");
111
+ }
112
+ return keys;
113
+ }
114
+
115
+ function readJwk(value: unknown): ExactVerificationJwk {
116
+ if (
117
+ !isRecord(value)
118
+ || !hasOnlyFields(value, JWK_FIELDS)
119
+ || value.kty !== "EC"
120
+ || value.crv !== "P-256"
121
+ || value.alg !== "ES256"
122
+ || value.use !== "sig"
123
+ || typeof value.kid !== "string"
124
+ || !KID.test(value.kid)
125
+ || typeof value.x !== "string"
126
+ || !BASE64URL_COORDINATE.test(value.x)
127
+ || typeof value.y !== "string"
128
+ || !BASE64URL_COORDINATE.test(value.y)
129
+ ) {
130
+ throw new Error("customer identity JWKS configuration is invalid");
131
+ }
132
+ return value as unknown as ExactVerificationJwk;
133
+ }
134
+
135
+ function isRecord(value: unknown): value is Record<string, unknown> {
136
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
137
+ }
138
+
139
+ function hasOnlyFields(value: Record<string, unknown>, fields: Set<string>): boolean {
140
+ return Object.keys(value).every((field) => fields.has(field));
141
+ }