@openlup/platform 0.27.0 → 0.29.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 (232) hide show
  1. package/CHANGELOG.md +386 -127
  2. package/README.md +251 -9
  3. package/dist/account-support/accountReadFacade.d.ts +8 -0
  4. package/dist/account-support/accountReadFacade.js +10 -0
  5. package/dist/account-support/accountReadModels.d.ts +69 -0
  6. package/dist/account-support/accountReadModels.js +272 -0
  7. package/dist/account-support/bindings.d.ts +23 -0
  8. package/dist/account-support/bindings.js +16 -0
  9. package/dist/account-support/contracts/account.d.ts +281 -0
  10. package/dist/account-support/contracts/account.js +1 -0
  11. package/dist/account-support/contracts/capabilities.d.ts +44 -0
  12. package/dist/account-support/contracts/capabilities.js +1 -0
  13. package/dist/account-support/contracts/commands.d.ts +307 -0
  14. package/dist/account-support/contracts/commands.js +1 -0
  15. package/dist/account-support/contracts/customer360.d.ts +154 -0
  16. package/dist/account-support/contracts/customer360.js +1 -0
  17. package/dist/account-support/contracts/index.d.ts +10 -0
  18. package/dist/account-support/contracts/index.js +1 -0
  19. package/dist/account-support/contracts/invoice.d.ts +79 -0
  20. package/dist/account-support/contracts/invoice.js +1 -0
  21. package/dist/account-support/contracts/operator.d.ts +289 -0
  22. package/dist/account-support/contracts/operator.js +1 -0
  23. package/dist/account-support/contribution.d.ts +24 -0
  24. package/dist/account-support/contribution.js +22 -0
  25. package/dist/account-support/customer360.d.ts +106 -0
  26. package/dist/account-support/customer360.js +163 -0
  27. package/dist/account-support/customerAccountReadDiagnostics.d.ts +11 -0
  28. package/dist/account-support/customerAccountReadDiagnostics.js +71 -0
  29. package/dist/account-support/index.d.ts +26 -0
  30. package/dist/account-support/index.js +15 -0
  31. package/dist/account-support/invoiceAccess.d.ts +57 -0
  32. package/dist/account-support/invoiceAccess.js +40 -0
  33. package/dist/account-support/migrations.d.ts +14 -0
  34. package/dist/account-support/migrations.js +7 -0
  35. package/dist/account-support/operatorAuthority.d.ts +67 -0
  36. package/dist/account-support/operatorAuthority.js +379 -0
  37. package/dist/account-support/operatorRecovery.d.ts +66 -0
  38. package/dist/account-support/operatorRecovery.js +129 -0
  39. package/dist/account-support/orderHistory.d.ts +50 -0
  40. package/dist/account-support/orderHistory.js +188 -0
  41. package/dist/account-support/paymentMethodComposition.d.ts +5 -0
  42. package/dist/account-support/paymentMethodComposition.js +4 -0
  43. package/dist/account-support/postgresOperatorAuthority.d.ts +16 -0
  44. package/dist/account-support/postgresOperatorAuthority.js +93 -0
  45. package/dist/account-support/selfService.d.ts +6 -0
  46. package/dist/account-support/selfService.js +81 -0
  47. package/dist/account-support/subscriptionCommandCodec.d.ts +10 -0
  48. package/dist/account-support/subscriptionCommandCodec.js +38 -0
  49. package/dist/catalog/draftSchemas.d.ts +1 -1
  50. package/dist/catalog/foundationSchemas.d.ts +3 -3
  51. package/dist/catalog/resolvedSkuProjection.d.ts +2 -2
  52. package/dist/catalog/sellableSchemas.d.ts +2 -2
  53. package/dist/commerce/checkoutCommandSchemas.d.ts +2 -2
  54. package/dist/commerce/checkoutResumeSchemas.d.ts +6 -6
  55. package/dist/commerce/checkoutSchemas.d.ts +12 -12
  56. package/dist/commerce/commerceReturnsPort.d.ts +2 -2
  57. package/dist/commerce/configuratorIntentSchemas.d.ts +2 -2
  58. package/dist/commerce/contractPrimitives.d.ts +8 -8
  59. package/dist/commerce/customerDefaultsSnapshotSchemas.d.ts +4 -4
  60. package/dist/commerce/model/omsReadModelHelpers.d.ts +4 -4
  61. package/dist/commerce/offerAvailabilitySchemas.d.ts +1 -1
  62. package/dist/commerce/oms/omsContractBase.d.ts +15 -15
  63. package/dist/commerce/oms/omsContractReadAuxSchemas.d.ts +11 -11
  64. package/dist/commerce/oms/omsContractReadSchemas.d.ts +121 -121
  65. package/dist/commerce/oms/omsContractRequests.d.ts +13 -13
  66. package/dist/commerce/oms/omsControlPlane.d.ts +22 -22
  67. package/dist/commerce/oms/omsFulfillmentContractSchemas.d.ts +12 -12
  68. package/dist/commerce/oms/omsFulfillmentDebugSchemas.d.ts +12 -12
  69. package/dist/commerce/orderReviewContracts.d.ts +33 -33
  70. package/dist/commerce/outboxEventSchemas.d.ts +4 -4
  71. package/dist/commerce/schemas.d.ts +21 -21
  72. package/dist/commerce/server/orderReviewHandlers.d.ts +10 -10
  73. package/dist/communications/consentPurposeRegistry.d.ts +1 -1
  74. package/dist/communications/newsletterIntegrationContracts.d.ts +8 -8
  75. package/dist/composition/accountSupportContribution.d.ts +18 -0
  76. package/dist/composition/accountSupportContribution.js +21 -0
  77. package/dist/composition/index.d.ts +2 -0
  78. package/dist/composition/index.js +2 -0
  79. package/dist/composition/marketingContribution.d.ts +21 -0
  80. package/dist/composition/marketingContribution.js +29 -0
  81. package/dist/fulfillment/handlerSchemas.d.ts +40 -40
  82. package/dist/identity/customerContracts.d.ts +2 -2
  83. package/dist/invoicing/snapshot.d.ts +1 -1
  84. package/dist/marketing/acquisitionCaseSchemas.d.ts +103 -0
  85. package/dist/marketing/acquisitionCaseSchemas.js +53 -0
  86. package/dist/marketing/acquisitionEvidenceSchemas.d.ts +105 -0
  87. package/dist/marketing/acquisitionEvidenceSchemas.js +55 -0
  88. package/dist/marketing/analytics/dataLayer.d.ts +18 -0
  89. package/dist/marketing/analytics/dataLayer.js +111 -0
  90. package/dist/marketing/analytics/noop.d.ts +9 -0
  91. package/dist/marketing/analytics/noop.js +8 -0
  92. package/dist/marketing/analytics/purchaseFacts.d.ts +4 -0
  93. package/dist/marketing/analytics/purchaseFacts.js +69 -0
  94. package/dist/marketing/analytics/routePolicy.d.ts +10 -0
  95. package/dist/marketing/analytics/routePolicy.js +59 -0
  96. package/dist/marketing/consentEvidence.d.ts +8 -0
  97. package/dist/marketing/consentEvidence.js +32 -0
  98. package/dist/marketing/contactLegal.d.ts +14 -0
  99. package/dist/marketing/contactLegal.js +19 -0
  100. package/dist/marketing/contracts/acquisition.d.ts +88 -0
  101. package/dist/marketing/contracts/acquisition.js +1 -0
  102. package/dist/marketing/contracts/acquisitionPorts.d.ts +49 -0
  103. package/dist/marketing/contracts/acquisitionPorts.js +1 -0
  104. package/dist/marketing/contracts/analytics.d.ts +144 -0
  105. package/dist/marketing/contracts/analytics.js +1 -0
  106. package/dist/marketing/contracts/contactLegal.d.ts +53 -0
  107. package/dist/marketing/contracts/contactLegal.js +1 -0
  108. package/dist/marketing/contracts/index.d.ts +6 -0
  109. package/dist/marketing/contracts/index.js +1 -0
  110. package/dist/marketing/contracts/prelaunch.d.ts +39 -0
  111. package/dist/marketing/contracts/prelaunch.js +1 -0
  112. package/dist/marketing/index.d.ts +16 -0
  113. package/dist/marketing/index.js +14 -0
  114. package/dist/marketing/prelaunchAcquisitionSchemas.d.ts +116 -0
  115. package/dist/marketing/prelaunchAcquisitionSchemas.js +51 -0
  116. package/dist/marketing/seo/jsonLd.d.ts +157 -0
  117. package/dist/marketing/seo/jsonLd.js +127 -0
  118. package/dist/marketing/seo/metadata.d.ts +41 -0
  119. package/dist/marketing/seo/metadata.js +36 -0
  120. package/dist/marketing/seo/serializeJsonLd.d.ts +1 -0
  121. package/dist/marketing/seo/serializeJsonLd.js +6 -0
  122. package/dist/marketing/seo/urls.d.ts +14 -0
  123. package/dist/marketing/seo/urls.js +23 -0
  124. package/dist/marketing/server/acquisitionCaseDirect.d.ts +4 -0
  125. package/dist/marketing/server/acquisitionCaseDirect.js +88 -0
  126. package/dist/marketing/server/acquisitionCaseHandlers.d.ts +28 -0
  127. package/dist/marketing/server/acquisitionCaseHandlers.js +40 -0
  128. package/dist/marketing/server/acquisitionCasePorts.d.ts +42 -0
  129. package/dist/marketing/server/acquisitionCasePorts.js +1 -0
  130. package/dist/marketing/server/acquisitionEvidenceDirect.d.ts +5 -0
  131. package/dist/marketing/server/acquisitionEvidenceDirect.js +100 -0
  132. package/dist/marketing/server/analyticsContext.d.ts +3 -0
  133. package/dist/marketing/server/analyticsContext.js +65 -0
  134. package/dist/marketing/server/analyticsContextPorts.d.ts +31 -0
  135. package/dist/marketing/server/analyticsContextPorts.js +1 -0
  136. package/dist/marketing/server/bindings.d.ts +14 -0
  137. package/dist/marketing/server/bindings.js +18 -0
  138. package/dist/marketing/server/directPorts.d.ts +26 -0
  139. package/dist/marketing/server/directPorts.js +1 -0
  140. package/dist/marketing/server/index.d.ts +14 -0
  141. package/dist/marketing/server/index.js +11 -0
  142. package/dist/marketing/server/postgres/acquisitionCaseRoutines.d.ts +10 -0
  143. package/dist/marketing/server/postgres/acquisitionCaseRoutines.js +204 -0
  144. package/dist/marketing/server/postgres/acquisitionEvidenceRoutines.d.ts +10 -0
  145. package/dist/marketing/server/postgres/acquisitionEvidenceRoutines.js +107 -0
  146. package/dist/marketing/server/postgres/index.d.ts +3 -0
  147. package/dist/marketing/server/postgres/index.js +3 -0
  148. package/dist/marketing/server/prelaunchAcquisitionDirect.d.ts +4 -0
  149. package/dist/marketing/server/prelaunchAcquisitionDirect.js +122 -0
  150. package/dist/marketing/server/privacyContributor.d.ts +66 -0
  151. package/dist/marketing/server/privacyContributor.js +87 -0
  152. package/dist/marketing/server/research/adminSurveyResponsesHandler.d.ts +9 -0
  153. package/dist/marketing/server/research/adminSurveyResponsesHandler.js +53 -0
  154. package/dist/marketing/server/research/managedSurveyResponseSubmit.d.ts +39 -0
  155. package/dist/marketing/server/research/managedSurveyResponseSubmit.js +102 -0
  156. package/dist/marketing/server/research/surveyResponseSubmitHandler.d.ts +8 -0
  157. package/dist/marketing/server/research/surveyResponseSubmitHandler.js +41 -0
  158. package/dist/marketing/validation.d.ts +2 -0
  159. package/dist/marketing/validation.js +3 -0
  160. package/dist/pricing/acquisitionQuoteBinding.d.ts +1 -1
  161. package/dist/pricing/catalogFactsSchemas.d.ts +1 -1
  162. package/dist/pricing/quotePromotionEvidence.d.ts +1 -1
  163. package/dist/pricing/quoteSchemas.d.ts +56 -56
  164. package/dist/promotions/adminPromotionCodesContracts.d.ts +30 -30
  165. package/dist/promotions/adminPromotionsContracts.d.ts +14 -14
  166. package/package.json +32 -2
  167. package/release-gates.json +99 -4
  168. package/sql/20261026120000_account_support_operator_authority.sql +693 -0
  169. package/src/account-support/accountReadFacade.ts +15 -0
  170. package/src/account-support/accountReadModels.ts +371 -0
  171. package/src/account-support/bindings.ts +28 -0
  172. package/src/account-support/contracts/account.ts +306 -0
  173. package/src/account-support/contracts/capabilities.ts +30 -0
  174. package/src/account-support/contracts/commands.ts +243 -0
  175. package/src/account-support/contracts/customer360.ts +150 -0
  176. package/src/account-support/contracts/index.ts +141 -0
  177. package/src/account-support/contracts/invoice.ts +44 -0
  178. package/src/account-support/contracts/operator.ts +123 -0
  179. package/src/account-support/contribution.ts +35 -0
  180. package/src/account-support/customer360.ts +268 -0
  181. package/src/account-support/customerAccountReadDiagnostics.ts +83 -0
  182. package/src/account-support/index.ts +72 -0
  183. package/src/account-support/invoiceAccess.ts +63 -0
  184. package/src/account-support/migrations.ts +7 -0
  185. package/src/account-support/operatorAuthority.ts +393 -0
  186. package/src/account-support/operatorRecovery.ts +136 -0
  187. package/src/account-support/orderHistory.ts +309 -0
  188. package/src/account-support/paymentMethodComposition.ts +8 -0
  189. package/src/account-support/postgresOperatorAuthority.ts +92 -0
  190. package/src/account-support/selfService.ts +83 -0
  191. package/src/account-support/subscriptionCommandCodec.ts +48 -0
  192. package/src/composition/accountSupportContribution.ts +28 -0
  193. package/src/composition/index.ts +2 -0
  194. package/src/composition/marketingContribution.ts +37 -0
  195. package/src/marketing/acquisitionCaseSchemas.ts +75 -0
  196. package/src/marketing/acquisitionEvidenceSchemas.ts +72 -0
  197. package/src/marketing/analytics/dataLayer.ts +100 -0
  198. package/src/marketing/analytics/noop.ts +14 -0
  199. package/src/marketing/analytics/purchaseFacts.ts +63 -0
  200. package/src/marketing/analytics/routePolicy.ts +55 -0
  201. package/src/marketing/consentEvidence.ts +36 -0
  202. package/src/marketing/contactLegal.ts +22 -0
  203. package/src/marketing/contracts/acquisition.ts +50 -0
  204. package/src/marketing/contracts/acquisitionPorts.ts +48 -0
  205. package/src/marketing/contracts/analytics.ts +88 -0
  206. package/src/marketing/contracts/contactLegal.ts +41 -0
  207. package/src/marketing/contracts/index.ts +17 -0
  208. package/src/marketing/contracts/prelaunch.ts +15 -0
  209. package/src/marketing/index.ts +16 -0
  210. package/src/marketing/prelaunchAcquisitionSchemas.ts +64 -0
  211. package/src/marketing/seo/jsonLd.ts +153 -0
  212. package/src/marketing/seo/metadata.ts +62 -0
  213. package/src/marketing/seo/serializeJsonLd.ts +6 -0
  214. package/src/marketing/seo/urls.ts +34 -0
  215. package/src/marketing/server/acquisitionCaseDirect.ts +67 -0
  216. package/src/marketing/server/acquisitionCaseHandlers.ts +57 -0
  217. package/src/marketing/server/acquisitionCasePorts.ts +42 -0
  218. package/src/marketing/server/acquisitionEvidenceDirect.ts +76 -0
  219. package/src/marketing/server/analyticsContext.ts +55 -0
  220. package/src/marketing/server/analyticsContextPorts.ts +28 -0
  221. package/src/marketing/server/bindings.ts +27 -0
  222. package/src/marketing/server/directPorts.ts +27 -0
  223. package/src/marketing/server/index.ts +14 -0
  224. package/src/marketing/server/postgres/acquisitionCaseRoutines.ts +218 -0
  225. package/src/marketing/server/postgres/acquisitionEvidenceRoutines.ts +103 -0
  226. package/src/marketing/server/postgres/index.ts +3 -0
  227. package/src/marketing/server/prelaunchAcquisitionDirect.ts +121 -0
  228. package/src/marketing/server/privacyContributor.ts +123 -0
  229. package/src/marketing/server/research/adminSurveyResponsesHandler.ts +78 -0
  230. package/src/marketing/server/research/managedSurveyResponseSubmit.ts +158 -0
  231. package/src/marketing/server/research/surveyResponseSubmitHandler.ts +57 -0
  232. package/src/marketing/validation.ts +3 -0
@@ -0,0 +1,393 @@
1
+ import type {
2
+ EmailCorrectionRefusal as OperatorEmailCorrectionRefusalCode, PhoneCorrectionRefusal as OperatorPhoneCorrectionRefusalCode,
3
+ OperatorSubscriptionRefusal as OperatorSubscriptionRefusalCode, OperatorSubscriptionResult as OperatorSubscriptionCommandResult,
4
+ OperatorEmailCorrectionResult, OperatorPhoneCorrectionResult, OperatorEmailCorrectionAdmission,
5
+ OperatorEmailCorrectionExecution, OperatorEmailCorrectionSettlement, NativeOperatorSubscriptionResult,
6
+ OperatorOwnerReceipt, ManagedOperatorAuthorityPort, OperatorAuthorityCapabilities, OperatorAccountLinkResult,
7
+ OperatorAccountLinkRefusal, OperatorLeadAbsorptionResult, LeadAbsorptionCarried,
8
+ LeadRefusal as OperatorLeadAbsorptionRefusalCode, CustomerRecoveryAuthorityResult, RecoveryRefusal,
9
+ } from "./contracts/index.js";
10
+ type OperatorSubscriptionAppliedBy = "operator_reschedule_band" | "customer_self_service_delegate";
11
+
12
+ export class CustomerSupportCommandInvalidError extends Error { constructor() { super("customer_support_operator_command_invalid"); } }
13
+ export class CustomerSupportCommandIdempotencyConflictError extends Error { constructor() { super("customer_support_idempotency_conflict"); } }
14
+ export class OperatorSubscriptionAuthorityUnavailableError extends Error { constructor() { super("operator_subscription_authority_unavailable"); } }
15
+ export class CustomerSupportCommandRequiresHumanError extends Error { constructor() { super("customer_support_command_requires_human"); } }
16
+ export class CustomerSupportOperatorInactiveError extends Error { constructor() { super("managed_customer_support_operator_inactive"); } }
17
+ export class CustomerSupportOperatorNotProvisionedError extends Error { constructor() { super("operator_identity_not_provisioned"); } }
18
+
19
+ type Row = Record<string, unknown>;
20
+
21
+ const SETTLED_OUTCOMES = new Set(["applied", "noop", "replayed"]);
22
+ const EMAIL_REFUSALS = new Set<OperatorEmailCorrectionRefusalCode>(["subject_not_found", "subject_account_linked", "email_expectation_conflict", "email_already_in_use"]);
23
+ const PHONE_REFUSALS = new Set<OperatorPhoneCorrectionRefusalCode>(["subject_not_found", "phone_expectation_conflict"]);
24
+ // Must stay equal to operatorSubscriptionRefusalCodeSchema, whose docstring says why:
25
+ // a code missing here becomes an invalid response, i.e. a 503 hiding a stated reason.
26
+ // Equality is now pinned by customerSupportRefusalParity.test.ts rather than by these
27
+ // two sentences, which is what the last drift proved was necessary.
28
+ export const SUBSCRIPTION_REFUSALS = new Set<OperatorSubscriptionRefusalCode>(["subscription_not_found", "subject_not_found", "subject_account_unlinked", "version_conflict", "slide_target_invalid", "slide_not_available", "slide_out_of_window", "payment_blocked", "invalid_transition", "charge_timing_not_confirmed", "payment_method_not_chargeable", "invalid_address"]);
29
+ const APPLIED_BY = new Set<OperatorSubscriptionAppliedBy>(["operator_reschedule_band", "customer_self_service_delegate"]);
30
+
31
+ function object(value: unknown, failure: string): Row {
32
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(failure);
33
+ return value as Row;
34
+ }
35
+ function text(value: unknown): string {
36
+ return typeof value === "string" ? value : "";
37
+ }
38
+
39
+ export function emailCorrection(value: unknown): OperatorEmailCorrectionResult {
40
+ const row = object(value, "customer_support_email_correction_response_invalid");
41
+ const outcome = text(row.outcome);
42
+ const refusalCode = text(row.refusalCode ?? row.refusal_code) as OperatorEmailCorrectionRefusalCode;
43
+ if ((outcome === "refused" || outcome === "conflict") && EMAIL_REFUSALS.has(refusalCode)) {
44
+ return { outcome, refusalCode };
45
+ }
46
+ if (SETTLED_OUTCOMES.has(outcome ?? "")) {
47
+ // The completed receipt reports the linkage observed at admission.
48
+ const authUserLinked = row.authUserLinked ?? row.auth_user_linked;
49
+ if (typeof authUserLinked === "boolean") {
50
+ return { outcome: outcome as "applied" | "noop" | "replayed", authUserLinked };
51
+ }
52
+ }
53
+ throw new Error("customer_support_email_correction_response_invalid");
54
+ }
55
+
56
+ /** Admission authorizes a command; only a service claim authorizes its execution. */
57
+ export function emailCorrectionAdmission(value: unknown): OperatorEmailCorrectionAdmission {
58
+ const row = object(value, "customer_support_email_correction_response_invalid");
59
+ if (row.outcome === "failed") throw new Error("customer_support_email_correction_failed");
60
+ if ((row.outcome === "admitted" || row.outcome === "pending")
61
+ && (row.authUserId === null || typeof row.authUserId === "string")
62
+ && typeof row.originalEmail === "string" && typeof row.newEmail === "string") {
63
+ return { outcome: "admitted", authUserId: typeof row.authUserId === "string" ? row.authUserId : null, originalEmail: row.originalEmail, newEmail: row.newEmail };
64
+ }
65
+ return emailCorrection(value);
66
+ }
67
+ export function emailCorrectionExecution(value: unknown): OperatorEmailCorrectionExecution {
68
+ const row = object(value, "customer_support_email_correction_response_invalid");
69
+ if (row.outcome === "claimed" && typeof row.idempotencyKey === "string"
70
+ && typeof row.claimToken === "string" && typeof row.sequence === "string") {
71
+ const admission = emailCorrectionAdmission({ ...row, outcome: "pending" });
72
+ if (admission.outcome === "admitted") return { ...admission, outcome: "claimed",
73
+ idempotencyKey: row.idempotencyKey, claimToken: row.claimToken, sequence: row.sequence };
74
+ }
75
+ return emailCorrectionSettlement(value);
76
+ }
77
+ export function emailCorrectionSettlement(value: unknown): OperatorEmailCorrectionSettlement {
78
+ const row = object(value, "customer_support_email_correction_response_invalid");
79
+ if (row.outcome === "pending" || row.outcome === "failed") return { outcome: row.outcome };
80
+ return emailCorrection(value);
81
+ }
82
+
83
+ export function phoneCorrection(value: unknown): OperatorPhoneCorrectionResult {
84
+ const row = object(value, "customer_support_phone_correction_response_invalid");
85
+ const outcome = text(row.outcome);
86
+ const refusalCode = text(row.refusalCode ?? row.refusal_code) as OperatorPhoneCorrectionRefusalCode;
87
+ if ((outcome === "refused" || outcome === "conflict") && PHONE_REFUSALS.has(refusalCode)) {
88
+ return { outcome, refusalCode };
89
+ }
90
+ if (SETTLED_OUTCOMES.has(outcome ?? "")) return { outcome: outcome as "applied" | "noop" | "replayed" };
91
+ throw new Error("customer_support_phone_correction_response_invalid");
92
+ }
93
+
94
+ /**
95
+ * A `timestamptz` reaches us as PostgreSQL renders it (`+00:00`), so it is
96
+ * re-rendered as an ISO instant before it can reach a contract that pins the
97
+ * offset form. An unparseable value becomes null rather than a 500.
98
+ */
99
+ function instant(value: unknown): string | null {
100
+ const raw = text(value).trim();
101
+ if (!raw) return null;
102
+ const parsed = Date.parse(raw);
103
+ return Number.isNaN(parsed) ? null : new Date(parsed).toISOString();
104
+ }
105
+
106
+ export function subscriptionCommand(value: unknown): OperatorSubscriptionCommandResult {
107
+ const row = object(value, "customer_support_subscription_command_response_invalid");
108
+ const outcome = text(row.outcome);
109
+ const refusalCode = text(row.refusalCode ?? row.refusal_code).trim() as OperatorSubscriptionRefusalCode;
110
+ if ((outcome === "refused" || outcome === "conflict") && SUBSCRIPTION_REFUSALS.has(refusalCode)) {
111
+ return { outcome, refusalCode };
112
+ }
113
+ const templateVersion = row.templateVersion ?? row.template_version;
114
+ if (SETTLED_OUTCOMES.has(outcome) && typeof templateVersion === "number" && Number.isInteger(templateVersion)) {
115
+ const appliedBy = text(row.appliedBy ?? row.applied_by).trim() as OperatorSubscriptionAppliedBy;
116
+ return {
117
+ outcome: outcome as "applied" | "noop" | "replayed",
118
+ subscriptionStatus: text(row.subscriptionStatus ?? row.subscription_status).trim() || "unknown",
119
+ nextCycleAt: instant(row.nextCycleAt ?? row.next_cycle_at),
120
+ templateVersion,
121
+ eventId: text(row.eventId ?? row.event_id).trim() || null,
122
+ appliedBy: APPLIED_BY.has(appliedBy) ? appliedBy : null,
123
+ };
124
+ }
125
+ throw new Error("customer_support_subscription_command_response_invalid");
126
+ }
127
+
128
+ /**
129
+ * Translate the routine's raised SQLSTATEs into failures the route can price as
130
+ * HTTP without reading a driver error. Anything unrecognized stays an opaque
131
+ * upstream failure rather than being guessed into a client fault.
132
+ */
133
+ export function operatorCommandFailure(error: unknown, fallback: string): Error {
134
+ const e = (error ?? {}) as { code?: unknown; message?: unknown };
135
+ const code = typeof e.code === "string" ? e.code : "";
136
+ const message = typeof e.message === "string" ? e.message : "";
137
+ // The machine fence raises the same SQLSTATE the inactive-operator gate uses, so
138
+ // its message must be read first: a refused machine actor told "operator inactive"
139
+ // would send a human chasing provisioning that is not the problem.
140
+ if (message.includes("customer_support_command_requires_human")) return new CustomerSupportCommandRequiresHumanError();
141
+ if (code === "42501" || message.includes("communications_operator_inactive")) return new CustomerSupportOperatorInactiveError();
142
+ if (code === "22023" || /customer_support_(subscription_command|email_correction|phone_correction|absorb_lead|account_link)_invalid/.test(message)) return new CustomerSupportCommandInvalidError();
143
+ if (code === "23505" || message.includes("customer_support_idempotency_conflict")) return new CustomerSupportCommandIdempotencyConflictError();
144
+ // The ledger `operator_id` foreign key. Reachable because the eligibility gate is a
145
+ // union of the admin roster and `platform_communication_operators`, so a principal
146
+ // it admits on the roster arm alone has no row for the receipt to reference. The
147
+ // scope refuses this before anything mutates when the caller declares a command
148
+ // intent; this arm is the backstop for one that does not, so the failure is still
149
+ // named rather than reaching an operator as an anonymous outage.
150
+ if (code === "23503" || /platform_communication_operators/.test(message)) return new CustomerSupportOperatorNotProvisionedError();
151
+ // The routine exists in the database but the API layer cannot see it: that layer
152
+ // answers PGRST202 from a schema cache it has not reloaded since the migration,
153
+ // and direct SQL answers 42883. Neither is a fault in the command, so neither may
154
+ // arrive as an anonymous failure - that is exactly the shape that cost an hour of
155
+ // production diagnosis, because it looked identical to the whole journey being
156
+ // down. `AuthorityUnavailable` is the honest name: the authority is deployed but
157
+ // unreachable, which is what an operator and a log both need to be told.
158
+ if (code === "PGRST202" || code === "42883" || /Could not find the function/i.test(message)) {
159
+ return new OperatorSubscriptionAuthorityUnavailableError();
160
+ }
161
+ return new Error(`${fallback}: ${code || "unknown"}`);
162
+ }
163
+
164
+ const LEAD_REFUSALS = new Set<OperatorLeadAbsorptionRefusalCode>([
165
+ "lead_not_found", "customer_not_found", "lead_email_expectation_conflict",
166
+ "lead_has_identity", "lead_has_commercial_footprint", "unclassified_referencing_table",
167
+ ]);
168
+ const SETTLED = new Set(["applied", "noop", "replayed"]);
169
+ const CARRIED_KINDS = ["personalization", "consents", "sourceLinks", "deliveries"] as const;
170
+
171
+
172
+ const INVALID = "customer_support_lead_absorption_response_invalid";
173
+
174
+ function leadText(value: unknown): string {
175
+ return typeof value === "string" ? value.trim() : "";
176
+ }
177
+
178
+ function count(value: unknown): number | null {
179
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : null;
180
+ }
181
+
182
+ /**
183
+ * Bounded, de-duplicated and order-preserving. The list is rendered verbatim to an
184
+ * operator so they can hand it to whoever classifies the table, so a blank or a
185
+ * repeat in it is noise in the one place the refusal has to be actionable.
186
+ */
187
+ function blockingTables(value: unknown): string[] {
188
+ if (!Array.isArray(value)) return [];
189
+ return [...new Set(value.map(leadText).filter(Boolean))].slice(0, 200);
190
+ }
191
+
192
+ /**
193
+ * What was carried, or null when the authority did not report it as four
194
+ * non-negative integers. Null is a refusal to guess: reporting "carried nothing"
195
+ * for an answer that simply did not say would tell an operator the campaign
196
+ * attribution was lost when it may well have moved.
197
+ */
198
+ function carried(value: unknown): LeadAbsorptionCarried | null {
199
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
200
+ const row = value as Row;
201
+ const counts = CARRIED_KINDS.map((kind) => count(row[kind] ?? row[snake(kind)]));
202
+ if (counts.some((entry) => entry === null)) return null;
203
+ const [personalization, consents, sourceLinks, deliveries] = counts as number[];
204
+ return { personalization: personalization!, consents: consents!, sourceLinks: sourceLinks!, deliveries: deliveries! };
205
+ }
206
+
207
+ function snake(value: string): string {
208
+ return value.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
209
+ }
210
+
211
+ /** Translate the routine's JSON into the one result shape both lanes report. */
212
+ export function leadAbsorption(value: unknown): OperatorLeadAbsorptionResult {
213
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(INVALID);
214
+ const row = value as Row;
215
+ const outcome = leadText(row.outcome);
216
+ const leadId = leadText(row.leadId ?? row.lead_id);
217
+ if (!leadId) throw new Error(INVALID);
218
+
219
+ if (outcome === "refused" || outcome === "conflict") {
220
+ const refusalCode = leadText(row.refusalCode ?? row.refusal_code) as OperatorLeadAbsorptionRefusalCode;
221
+ if (!LEAD_REFUSALS.has(refusalCode)) throw new Error(INVALID);
222
+ return { outcome, leadId, refusalCode, blockingTables: blockingTables(row.blockingTables ?? row.blocking_tables) };
223
+ }
224
+
225
+ const moved = carried(row.carried);
226
+ const customerId = leadText(row.customerId ?? row.customer_id);
227
+ if (!SETTLED.has(outcome) || !customerId || !moved) throw new Error(INVALID);
228
+ return { outcome: outcome as "applied" | "noop" | "replayed", leadId, customerId, carried: moved };
229
+ }
230
+
231
+
232
+ /** Translate the routine's JSON into the one result shape the route reports. */
233
+ export function accountLink(value: unknown): OperatorAccountLinkResult {
234
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("customer_support_account_link_response_invalid");
235
+ const row = value as Record<string, unknown>;
236
+ const outcome = typeof row.outcome === "string" ? row.outcome : "";
237
+ if (outcome === "refused" || outcome === "conflict") {
238
+ const code = row.refusalCode ?? row.refusal_code;
239
+ if (typeof code === "string" && LINK_REFUSALS.has(code as OperatorAccountLinkRefusal)) {
240
+ return { outcome, refusalCode: code as OperatorAccountLinkRefusal };
241
+ }
242
+ throw new Error("customer_support_account_link_response_invalid");
243
+ }
244
+ if (SETTLED_OUTCOMES.has(outcome)) return { outcome: outcome as "applied" | "noop" | "replayed" };
245
+ throw new Error("customer_support_account_link_response_invalid");
246
+ }
247
+
248
+
249
+ const LINK_REFUSALS = new Set<OperatorAccountLinkRefusal>(["subject_not_found", "subject_not_customer", "subject_email_missing",
250
+ "email_expectation_conflict", "subject_account_linked", "identity_not_linkable", "identity_linked_elsewhere"]);
251
+ const RECOVERY_REFUSALS = new Set<RecoveryRefusal>(["subject_not_found", "case_not_found", "lifecycle_not_eligible",
252
+ "dunning_authority_unavailable", "case_not_open", "recoverable_order_unavailable"]);
253
+ export function recoveryResult(value: unknown): CustomerRecoveryAuthorityResult {
254
+ const row = object(value, "customer_support_recovery_response_invalid");
255
+ const outcome = text(row.outcome);
256
+ if (outcome === "issued" || outcome === "replayed") {
257
+ const deliveryStatus = text(row.deliveryStatus ?? row.delivery_status);
258
+ const auditEventId = text(row.auditEventId ?? row.audit_event_id);
259
+ if (deliveryStatus && auditEventId) return { outcome, deliveryStatus, auditEventId };
260
+ }
261
+ const refusalCode = text(row.refusalCode ?? row.refusal_code) as RecoveryRefusal;
262
+ if (outcome === "refused" && RECOVERY_REFUSALS.has(refusalCode)) return { outcome, refusalCode };
263
+ if (outcome === "conflict") return { outcome, conflictCode: "idempotency_conflict" };
264
+ throw new Error("customer_support_recovery_response_invalid");
265
+ }
266
+
267
+ /** Native-only additions never enter the managed subscription decoder. */
268
+ export function nativeSubscriptionCommand(value: unknown): NativeOperatorSubscriptionResult {
269
+ const row = object(value, "customer_support_subscription_command_response_invalid");
270
+ let ownerReceipt: OperatorOwnerReceipt | undefined;
271
+ if (row.ownerReceipt !== undefined) {
272
+ const receipt = object(row.ownerReceipt, "customer_support_owner_receipt_invalid");
273
+ if (![receipt.id, receipt.actorId, receipt.idempotencyKey, receipt.commandFingerprint].every(v => typeof v === "string" && v.length > 0)) {
274
+ throw new Error("customer_support_owner_receipt_invalid");
275
+ }
276
+ ownerReceipt = { id: receipt.id as string, actorId: receipt.actorId as string,
277
+ idempotencyKey: receipt.idempotencyKey as string, commandFingerprint: receipt.commandFingerprint as string,
278
+ result: object(receipt.result, "customer_support_owner_receipt_invalid") };
279
+ }
280
+ const result = row.outcome === "refused" && row.refusalCode === "operator_subscription_authority_unavailable"
281
+ ? { outcome: "refused", refusalCode: "operator_subscription_authority_unavailable" } as const
282
+ : subscriptionCommand(row);
283
+ return ownerReceipt ? { ...result, ownerReceipt } : result;
284
+ }
285
+
286
+ export function operatorAuthorityCapabilities(graph: "managed" | "native"): OperatorAuthorityCapabilities {
287
+ const available = { supported: true } as const;
288
+ const conditional = graph === "managed" ? available : { supported: false, refusalCode: "operator_subscription_authority_unavailable" } as const;
289
+ return { graph, email: conditional, phone: conditional, link: conditional, address: conditional,
290
+ shortBand: conditional, unpaidResume: conditional, preparedAttemptPause: conditional,
291
+ pause: available, resume: available, slide: available };
292
+ }
293
+
294
+ export interface OperatorRpcExecutor {
295
+ rpc(name: string, args: Record<string, unknown>): PromiseLike<{ data: unknown; error: unknown }>;
296
+ }
297
+ /** Injected recovery material is private to the adapter; paths belong to the host. */
298
+ export interface OperatorRecoveryMaterial {
299
+ tokenHash: string; payloadFingerprint: string; expiresAt: string; recoveryPath: string;
300
+ }
301
+ export interface OperatorAuthorityOptions {
302
+ operatorId: string; now: () => number;
303
+ recoveryPath: (rawToken: string) => string;
304
+ /** Defaults to 32 random bytes from Web Crypto, encoded as base64url. */
305
+ createOpaqueToken?: () => string;
306
+ }
307
+ export async function operatorRecoveryMaterial(input: { subjectId: string; caseId: string }, options: OperatorAuthorityOptions): Promise<OperatorRecoveryMaterial> {
308
+ const token = options.createOpaqueToken ? options.createOpaqueToken() : btoa(String.fromCharCode(...globalThis.crypto.getRandomValues(new Uint8Array(32))))
309
+ .replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
310
+ const hash = async (value: string) => Array.from(new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(value))),
311
+ byte => byte.toString(16).padStart(2, "0")).join("");
312
+ return { tokenHash: await hash(token), payloadFingerprint: await hash(JSON.stringify({ action: "issue_recovery", subjectId: input.subjectId, caseId: input.caseId })),
313
+ expiresAt: new Date(options.now() + 86_400_000).toISOString(), recoveryPath: options.recoveryPath(token) };
314
+ }
315
+
316
+ /** Current managed calls over an injected executor; no provider, credentials or defaults. */
317
+ export function createManagedOperatorAuthority(executor: OperatorRpcExecutor, options: OperatorAuthorityOptions): ManagedOperatorAuthorityPort {
318
+ const operatorId = options.operatorId.trim();
319
+ if (!operatorId) throw new Error("customer_support_operator_required");
320
+ const check = (input: { operatorId: string }) => {
321
+ if (input.operatorId !== operatorId) throw new Error("customer_support_operator_scope_mismatch");
322
+ };
323
+ const call = async (name: string, args: Record<string, unknown>, failure: string) => {
324
+ const { data, error } = await executor.rpc(name, args);
325
+ if (error) throw operatorCommandFailure(error, failure);
326
+ return data;
327
+ };
328
+ const now = () => new Date(options.now()).toISOString();
329
+ return {
330
+ async applySubscriptionAction(input) {
331
+ check(input);
332
+ return subscriptionCommand(await call("customer_support_apply_subscription_action_v1", {
333
+ p_operator_id: operatorId, p_subscription_id: input.subscriptionId, p_action: input.subscriptionAction,
334
+ p_payload: input.payload ?? {}, p_expected_version: input.expectedVersion,
335
+ p_idempotency_key: input.idempotencyKey, p_now: now(),
336
+ }, "customer_support_subscription_command_failed"));
337
+ },
338
+ async correctSubjectEmail(input) {
339
+ check(input);
340
+ return emailCorrectionAdmission(await call("customer_support_admit_subject_email_v1", {
341
+ p_operator_id: operatorId, p_subject_id: input.subjectId, p_expected_email: input.expectedEmail,
342
+ p_new_email: input.newEmail, p_idempotency_key: input.idempotencyKey, p_now: now(),
343
+ }, "customer_support_email_correction_failed"));
344
+ },
345
+ async claimSubjectEmail(input) {
346
+ return emailCorrectionExecution(await call("customer_support_claim_subject_email_v1", {
347
+ p_idempotency_key: input.idempotencyKey,
348
+ }, "customer_support_email_correction_claim_failed"));
349
+ },
350
+ async settleSubjectEmail(input) {
351
+ return emailCorrectionSettlement(await call("customer_support_settle_subject_email_v1", {
352
+ p_idempotency_key: input.idempotencyKey, p_claim_token: input.claimToken, p_phase: input.phase,
353
+ }, "customer_support_email_correction_settlement_failed"));
354
+ },
355
+ async correctSubjectPhone(input) {
356
+ check(input);
357
+ return phoneCorrection(await call("customer_support_correct_subject_phone_v1", {
358
+ p_operator_id: operatorId, p_subject_id: input.subjectId, p_expected_phone: input.expectedPhone,
359
+ p_new_phone: input.newPhone, p_idempotency_key: input.idempotencyKey, p_now: now(),
360
+ }, "customer_support_phone_correction_failed"));
361
+ },
362
+ async linkSubjectAccount(input) {
363
+ check(input);
364
+ return accountLink(await call("customer_support_link_subject_account_v1", {
365
+ p_operator_id: operatorId, p_subject_id: input.subjectId, p_expected_email: input.expectedEmail,
366
+ p_auth_user_id: input.authUserId, p_idempotency_key: input.idempotencyKey, p_now: now(),
367
+ }, "customer_support_account_link_failed"));
368
+ },
369
+ async absorbLead(input) {
370
+ check(input);
371
+ return leadAbsorption(await call("customer_support_absorb_lead_v1", {
372
+ p_operator_id: operatorId, p_customer_id: input.customerId, p_lead_id: input.leadId,
373
+ p_expected_lead_email: input.expectedLeadEmail, p_idempotency_key: input.idempotencyKey, p_now: now(),
374
+ }, "customer_support_lead_absorption_failed"));
375
+ },
376
+ async issueRecovery(input) {
377
+ check(input);
378
+ const material = await operatorRecoveryMaterial(input, options);
379
+ const { data, error } = await executor.rpc("customer_support_issue_recovery", {
380
+ p_operator_id: operatorId, p_subject_id: input.subjectId, p_case_id: input.caseId, p_idempotency_key: input.idempotencyKey,
381
+ p_payload_fingerprint: material.payloadFingerprint, p_token_hash: material.tokenHash,
382
+ p_expires_at: material.expiresAt, p_recovery_path: material.recoveryPath,
383
+ });
384
+ if (error) {
385
+ const e = error as { code?: string; message?: string };
386
+ if (e.message?.includes("customer_support_command_requires_human")) throw new CustomerSupportCommandRequiresHumanError();
387
+ if (e.code === "23505" || e.message?.includes("idempotency_conflict")) return { outcome: "conflict", conflictCode: "idempotency_conflict" };
388
+ throw new Error("customer_support_recovery_failed");
389
+ }
390
+ return recoveryResult(data);
391
+ },
392
+ };
393
+ }
@@ -0,0 +1,136 @@
1
+ import type {
2
+ OperatorEmailCorrectionResult, OperatorEmailCorrectionAdmission, OperatorEmailCorrectionExecution,
3
+ OperatorEmailCorrectionPhase, OperatorEmailCorrectionClaim, OperatorEmailCorrectionSettlement, OperatorIdentityMovePort,
4
+ OperatorAccountLinkIdentityPort, OperatorAccountLinkRequest, OperatorAccountLinkResult,
5
+ ManagedOperatorAuthorityPort, NativeOperatorAuthorityPort, OperatorRecoveryCommandEnvelope,
6
+ OperatorRecoveryCommand, OperatorSubscriptionInput, LinkThenSubscriptionResult,
7
+ } from "./contracts/index.js";
8
+
9
+ export class OperatorIdentityMoveRefusedError extends Error {
10
+ constructor() { super("customer_support_email_correction_identity_refused"); }
11
+ }
12
+ const normalize = (value: string) => value.trim().toLowerCase();
13
+ function terminal(result: OperatorEmailCorrectionSettlement): result is OperatorEmailCorrectionResult {
14
+ return result.outcome !== "pending" && result.outcome !== "failed";
15
+ }
16
+
17
+ /** Recovery is service authority over the recorded command, independent of admission's actor. */
18
+ export async function moveIdentityForEmailCorrection(input: {
19
+ admission: OperatorEmailCorrectionAdmission;
20
+ claim: () => Promise<OperatorEmailCorrectionExecution>;
21
+ identity: () => OperatorIdentityMovePort;
22
+ settle: (phase: OperatorEmailCorrectionPhase, claim: OperatorEmailCorrectionClaim) => Promise<OperatorEmailCorrectionSettlement>;
23
+ }): Promise<OperatorEmailCorrectionResult> {
24
+ if (input.admission.outcome !== "admitted") return input.admission;
25
+ const execution = await input.claim();
26
+ if (execution.outcome === "failed") throw new Error("customer_support_email_correction_failed");
27
+ if (execution.outcome === "pending") throw new Error("customer_support_email_correction_in_progress");
28
+ if (execution.outcome !== "claimed") return execution;
29
+ try {
30
+ if (execution.authUserId) {
31
+ const identity = input.identity();
32
+ const current = normalize(await identity.readIdentityEmail(execution.authUserId));
33
+ if (current !== normalize(execution.newEmail)) {
34
+ if (current !== normalize(execution.originalEmail)) throw new Error("identity_changed");
35
+ await identity.moveIdentityEmail(execution.authUserId, execution.newEmail, {
36
+ idempotencyKey: execution.idempotencyKey, claimToken: execution.claimToken, sequence: execution.sequence,
37
+ });
38
+ if (normalize(await identity.readIdentityEmail(execution.authUserId)) !== normalize(execution.newEmail)) {
39
+ throw new Error("identity_unconfirmed");
40
+ }
41
+ }
42
+ }
43
+ const result = await input.settle("complete", execution);
44
+ if (terminal(result)) return result;
45
+ if (result.outcome === "failed") throw new Error("customer_support_email_correction_failed");
46
+ throw new Error("completion_unsettled");
47
+ } catch (error) {
48
+ if (error instanceof Error && error.message === "customer_support_email_correction_failed") throw error;
49
+ try {
50
+ // SQL locks Auth before the receipt and checks the actual identity. Failure
51
+ // cannot release an applied effect; release fences any delayed provider PUT.
52
+ const result = await input.settle(error instanceof OperatorIdentityMoveRefusedError ? "fail" : "release", execution);
53
+ if (terminal(result)) return result;
54
+ if (result.outcome === "failed") throw new Error("customer_support_email_correction_failed");
55
+ } catch (settlementError) {
56
+ if (settlementError instanceof Error && settlementError.message === "customer_support_email_correction_failed") throw settlementError;
57
+ // The durable claim expires if storage is unavailable; its old writes are fenced.
58
+ }
59
+ throw new Error("customer_support_email_correction_repair_required");
60
+ }
61
+ }
62
+
63
+ /** Donor link resolution uses only the subject's confirmed stored email. */
64
+ export async function executeOperatorAccountLink(input: {
65
+ command: OperatorAccountLinkRequest; operatorId: string;
66
+ identity: OperatorAccountLinkIdentityPort; authority: Pick<ManagedOperatorAuthorityPort, "linkSubjectAccount">;
67
+ }): Promise<OperatorAccountLinkResult> {
68
+ const { command } = input;
69
+ const subject = await input.identity.findSubject(command.subjectId);
70
+ const expected = normalize(command.expectedEmail);
71
+ const linkable = subject !== null && subject.identityKind === "customer" && !subject.authUserId
72
+ && expected !== "" && normalize(subject.email ?? "") === expected;
73
+ const authUserId = linkable ? await input.identity.resolveIdentityForEmail(expected) : null;
74
+ return input.authority.linkSubjectAccount({ subjectId: command.subjectId, expectedEmail: expected,
75
+ authUserId, idempotencyKey: command.idempotencyKey, operatorId: input.operatorId });
76
+ }
77
+
78
+ /** Two commands, two commits. Callback invocation keeps the donor's non-await order. */
79
+ export async function linkThenApply(input: {
80
+ link: () => Promise<OperatorAccountLinkResult>;
81
+ apply: () => ReturnType<NativeOperatorAuthorityPort["applySubscriptionAction"]>;
82
+ onLinked: () => unknown;
83
+ }): Promise<LinkThenSubscriptionResult> {
84
+ const { link, apply, onLinked } = input;
85
+ const linked = await link();
86
+ if (linked.outcome === "refused" || linked.outcome === "conflict") return { outcome: "link_refused", link: linked };
87
+ void onLinked();
88
+ try {
89
+ const subscription = await apply();
90
+ return { outcome: "refusalCode" in subscription ? "partial" : "completed", link: linked, subscription };
91
+ } catch {
92
+ return { outcome: "partial", link: linked, subscription: null };
93
+ }
94
+ }
95
+
96
+ export type OperatorRecoverySelection =
97
+ | { graph: "managed"; authority: ManagedOperatorAuthorityPort; identity: () => OperatorIdentityMovePort; accountLink: OperatorAccountLinkIdentityPort }
98
+ | { graph: "native"; authority: NativeOperatorAuthorityPort };
99
+
100
+ /** Host verifies the operator seat before construction; every dispatch keeps its actor binding. */
101
+ export function createOperatorRecovery(selection: OperatorRecoverySelection) {
102
+ return {
103
+ async execute(envelope: OperatorRecoveryCommandEnvelope<OperatorRecoveryCommand | OperatorAccountLinkRequest>) {
104
+ const { actor, command } = envelope;
105
+ if (actor.role !== "admin" || actor.isMachineActor !== false) throw new Error("customer_support_command_requires_human");
106
+ if (actor.graph !== selection.graph || !actor.operatorId.trim()) throw new Error("customer_support_operator_scope_mismatch");
107
+ if (envelope.idempotencyKey !== command.idempotencyKey) throw new Error("customer_support_idempotency_conflict");
108
+ if ("subjectId" in command && envelope.subject.subjectId !== command.subjectId) throw new Error("customer_support_operator_command_invalid");
109
+ const operatorId = actor.operatorId;
110
+ switch (command.action) {
111
+ case "apply_subscription_action": {
112
+ if (envelope.subject.subscriptionId !== command.subscriptionId || envelope.expectedVersion !== command.expectedVersion) {
113
+ throw new Error("customer_support_operator_command_invalid");
114
+ }
115
+ const input: OperatorSubscriptionInput = { subscriptionId: command.subscriptionId, subscriptionAction: command.subscriptionAction,
116
+ expectedVersion: command.expectedVersion, idempotencyKey: command.idempotencyKey, operatorId, payload: command.payload ?? {} };
117
+ return selection.authority.applySubscriptionAction(input);
118
+ }
119
+ case "correct_email": {
120
+ const input = { subjectId: command.subjectId, expectedEmail: command.expectedEmail, newEmail: command.newEmail, idempotencyKey: command.idempotencyKey, operatorId };
121
+ if (selection.graph === "native") return selection.authority.correctSubjectEmail(input);
122
+ const admission = await selection.authority.correctSubjectEmail(input);
123
+ return moveIdentityForEmailCorrection({ admission, identity: selection.identity,
124
+ claim: () => selection.authority.claimSubjectEmail({ idempotencyKey: command.idempotencyKey }),
125
+ settle: (phase, claim) => selection.authority.settleSubjectEmail({ idempotencyKey: claim.idempotencyKey, claimToken: claim.claimToken, phase }) });
126
+ }
127
+ case "correct_phone": return selection.authority.correctSubjectPhone({ subjectId: command.subjectId, expectedPhone: command.expectedPhone, newPhone: command.newPhone, idempotencyKey: command.idempotencyKey, operatorId });
128
+ case "issue_recovery": return selection.authority.issueRecovery({ subjectId: command.subjectId, caseId: command.caseId, idempotencyKey: command.idempotencyKey, operatorId });
129
+ case "absorb_lead": return selection.authority.absorbLead({ customerId: command.subjectId, leadId: command.leadId, expectedLeadEmail: command.expectedLeadEmail, idempotencyKey: command.idempotencyKey, operatorId });
130
+ case "link_account":
131
+ if (selection.graph === "native") return selection.authority.linkSubjectAccount({ subjectId: command.subjectId, expectedEmail: command.expectedEmail, idempotencyKey: command.idempotencyKey, authUserId: null, operatorId });
132
+ return executeOperatorAccountLink({ command, operatorId, identity: selection.accountLink, authority: selection.authority });
133
+ }
134
+ },
135
+ };
136
+ }