@openlup/platform 0.28.0 → 0.30.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 (241) hide show
  1. package/CHANGELOG.md +369 -156
  2. package/README.md +220 -4
  3. package/dist/composition/index.d.ts +1 -0
  4. package/dist/composition/index.js +1 -0
  5. package/dist/composition/marketingContribution.d.ts +21 -0
  6. package/dist/composition/marketingContribution.js +29 -0
  7. package/dist/composition/nodeHost.d.ts +1 -1
  8. package/dist/composition/nodeHost.js +3 -2
  9. package/dist/lifecycle/appUpgrade.d.ts +52 -0
  10. package/dist/lifecycle/appUpgrade.js +180 -0
  11. package/dist/lifecycle/index.d.ts +2 -0
  12. package/dist/lifecycle/index.js +2 -0
  13. package/dist/lifecycle/postgresInstallQualification.d.ts +51 -0
  14. package/dist/lifecycle/postgresInstallQualification.js +74 -0
  15. package/dist/marketing/acquisitionCaseSchemas.d.ts +103 -0
  16. package/dist/marketing/acquisitionCaseSchemas.js +53 -0
  17. package/dist/marketing/acquisitionEvidenceSchemas.d.ts +105 -0
  18. package/dist/marketing/acquisitionEvidenceSchemas.js +55 -0
  19. package/dist/marketing/analytics/dataLayer.d.ts +18 -0
  20. package/dist/marketing/analytics/dataLayer.js +111 -0
  21. package/dist/marketing/analytics/noop.d.ts +9 -0
  22. package/dist/marketing/analytics/noop.js +8 -0
  23. package/dist/marketing/analytics/purchaseFacts.d.ts +4 -0
  24. package/dist/marketing/analytics/purchaseFacts.js +69 -0
  25. package/dist/marketing/analytics/routePolicy.d.ts +10 -0
  26. package/dist/marketing/analytics/routePolicy.js +59 -0
  27. package/dist/marketing/consentEvidence.d.ts +8 -0
  28. package/dist/marketing/consentEvidence.js +32 -0
  29. package/dist/marketing/contactLegal.d.ts +14 -0
  30. package/dist/marketing/contactLegal.js +19 -0
  31. package/dist/marketing/contracts/acquisition.d.ts +88 -0
  32. package/dist/marketing/contracts/acquisition.js +1 -0
  33. package/dist/marketing/contracts/acquisitionPorts.d.ts +49 -0
  34. package/dist/marketing/contracts/acquisitionPorts.js +1 -0
  35. package/dist/marketing/contracts/analytics.d.ts +144 -0
  36. package/dist/marketing/contracts/analytics.js +1 -0
  37. package/dist/marketing/contracts/contactLegal.d.ts +53 -0
  38. package/dist/marketing/contracts/contactLegal.js +1 -0
  39. package/dist/marketing/contracts/index.d.ts +6 -0
  40. package/dist/marketing/contracts/index.js +1 -0
  41. package/dist/marketing/contracts/prelaunch.d.ts +39 -0
  42. package/dist/marketing/contracts/prelaunch.js +1 -0
  43. package/dist/marketing/index.d.ts +16 -0
  44. package/dist/marketing/index.js +14 -0
  45. package/dist/marketing/prelaunchAcquisitionSchemas.d.ts +116 -0
  46. package/dist/marketing/prelaunchAcquisitionSchemas.js +51 -0
  47. package/dist/marketing/seo/jsonLd.d.ts +157 -0
  48. package/dist/marketing/seo/jsonLd.js +127 -0
  49. package/dist/marketing/seo/metadata.d.ts +41 -0
  50. package/dist/marketing/seo/metadata.js +36 -0
  51. package/dist/marketing/seo/serializeJsonLd.d.ts +1 -0
  52. package/dist/marketing/seo/serializeJsonLd.js +6 -0
  53. package/dist/marketing/seo/urls.d.ts +14 -0
  54. package/dist/marketing/seo/urls.js +23 -0
  55. package/dist/marketing/server/acquisitionCaseDirect.d.ts +4 -0
  56. package/dist/marketing/server/acquisitionCaseDirect.js +88 -0
  57. package/dist/marketing/server/acquisitionCaseHandlers.d.ts +28 -0
  58. package/dist/marketing/server/acquisitionCaseHandlers.js +40 -0
  59. package/dist/marketing/server/acquisitionCasePorts.d.ts +42 -0
  60. package/dist/marketing/server/acquisitionCasePorts.js +1 -0
  61. package/dist/marketing/server/acquisitionEvidenceDirect.d.ts +5 -0
  62. package/dist/marketing/server/acquisitionEvidenceDirect.js +100 -0
  63. package/dist/marketing/server/analyticsContext.d.ts +3 -0
  64. package/dist/marketing/server/analyticsContext.js +65 -0
  65. package/dist/marketing/server/analyticsContextPorts.d.ts +31 -0
  66. package/dist/marketing/server/analyticsContextPorts.js +1 -0
  67. package/dist/marketing/server/bindings.d.ts +14 -0
  68. package/dist/marketing/server/bindings.js +18 -0
  69. package/dist/marketing/server/directPorts.d.ts +26 -0
  70. package/dist/marketing/server/directPorts.js +1 -0
  71. package/dist/marketing/server/index.d.ts +14 -0
  72. package/dist/marketing/server/index.js +11 -0
  73. package/dist/marketing/server/postgres/acquisitionCaseRoutines.d.ts +10 -0
  74. package/dist/marketing/server/postgres/acquisitionCaseRoutines.js +204 -0
  75. package/dist/marketing/server/postgres/acquisitionEvidenceRoutines.d.ts +10 -0
  76. package/dist/marketing/server/postgres/acquisitionEvidenceRoutines.js +107 -0
  77. package/dist/marketing/server/postgres/index.d.ts +3 -0
  78. package/dist/marketing/server/postgres/index.js +3 -0
  79. package/dist/marketing/server/prelaunchAcquisitionDirect.d.ts +4 -0
  80. package/dist/marketing/server/prelaunchAcquisitionDirect.js +122 -0
  81. package/dist/marketing/server/privacyContributor.d.ts +66 -0
  82. package/dist/marketing/server/privacyContributor.js +87 -0
  83. package/dist/marketing/server/research/adminSurveyResponsesHandler.d.ts +9 -0
  84. package/dist/marketing/server/research/adminSurveyResponsesHandler.js +53 -0
  85. package/dist/marketing/server/research/managedSurveyResponseSubmit.d.ts +39 -0
  86. package/dist/marketing/server/research/managedSurveyResponseSubmit.js +102 -0
  87. package/dist/marketing/server/research/surveyResponseSubmitHandler.d.ts +8 -0
  88. package/dist/marketing/server/research/surveyResponseSubmitHandler.js +41 -0
  89. package/dist/marketing/validation.d.ts +2 -0
  90. package/dist/marketing/validation.js +3 -0
  91. package/dist/migrations/postgresRunner.d.ts +9 -0
  92. package/dist/migrations/postgresRunner.js +17 -10
  93. package/dist/operations/adminAlertsSchemas.d.ts +153 -0
  94. package/dist/operations/adminAlertsSchemas.js +69 -0
  95. package/dist/operations/adminAlertsView.d.ts +73 -0
  96. package/dist/operations/adminAlertsView.js +184 -0
  97. package/dist/operations/alertPagingPolicy.d.ts +15 -0
  98. package/dist/operations/alertPagingPolicy.js +43 -0
  99. package/dist/operations/alertThrottlePolicy.d.ts +3 -0
  100. package/dist/operations/alertThrottlePolicy.js +18 -0
  101. package/dist/operations/clients.d.ts +17 -0
  102. package/dist/operations/clients.js +32 -0
  103. package/dist/operations/contracts/admin.d.ts +93 -0
  104. package/dist/operations/contracts/admin.js +1 -0
  105. package/dist/operations/contracts/adminAlertsContracts.d.ts +40 -0
  106. package/dist/operations/contracts/adminAlertsContracts.js +1 -0
  107. package/dist/operations/contracts/alertHumanContext.d.ts +7 -0
  108. package/dist/operations/contracts/alertHumanContext.js +1 -0
  109. package/dist/operations/contracts/index.d.ts +6 -0
  110. package/dist/operations/contracts/index.js +6 -0
  111. package/dist/operations/contracts/observabilityContracts.d.ts +41 -0
  112. package/dist/operations/contracts/observabilityContracts.js +1 -0
  113. package/dist/operations/contracts/observabilityPorts.d.ts +18 -0
  114. package/dist/operations/contracts/observabilityPorts.js +1 -0
  115. package/dist/operations/contracts/ports.d.ts +19 -0
  116. package/dist/operations/contracts/ports.js +1 -0
  117. package/dist/operations/errors.d.ts +6 -0
  118. package/dist/operations/errors.js +6 -0
  119. package/dist/operations/index.d.ts +9 -0
  120. package/dist/operations/index.js +9 -0
  121. package/dist/operations/schemas.d.ts +138 -0
  122. package/dist/operations/schemas.js +104 -0
  123. package/dist/operations/server/adminAlertsHandlers.d.ts +16 -0
  124. package/dist/operations/server/adminAlertsHandlers.js +35 -0
  125. package/dist/operations/server/adminPipelineHandlers.d.ts +12 -0
  126. package/dist/operations/server/adminPipelineHandlers.js +48 -0
  127. package/dist/operations/server/adminSettingsHandlers.d.ts +14 -0
  128. package/dist/operations/server/adminSettingsHandlers.js +82 -0
  129. package/dist/operations/server/controlPlaneHandler.d.ts +7 -0
  130. package/dist/operations/server/controlPlaneHandler.js +54 -0
  131. package/dist/operations/server/controlPlaneStore.d.ts +12 -0
  132. package/dist/operations/server/controlPlaneStore.js +138 -0
  133. package/dist/operations/server/controlPlaneWatchdog.d.ts +7 -0
  134. package/dist/operations/server/controlPlaneWatchdog.js +20 -0
  135. package/dist/operations/server/index.d.ts +12 -0
  136. package/dist/operations/server/index.js +12 -0
  137. package/dist/operations/server/migrations.d.ts +14 -0
  138. package/dist/operations/server/migrations.js +7 -0
  139. package/dist/operations/server/platformWatchdogDecision.d.ts +34 -0
  140. package/dist/operations/server/platformWatchdogDecision.js +71 -0
  141. package/dist/operations/server/platformWatchdogDeliveryPolicy.d.ts +10 -0
  142. package/dist/operations/server/platformWatchdogDeliveryPolicy.js +53 -0
  143. package/dist/operations/server/platformWatchdogQueue.d.ts +11 -0
  144. package/dist/operations/server/platformWatchdogQueue.js +94 -0
  145. package/dist/operations/server/platformWatchdogService.d.ts +47 -0
  146. package/dist/operations/server/platformWatchdogService.js +105 -0
  147. package/dist/operations/validation.d.ts +2 -0
  148. package/dist/operations/validation.js +3 -0
  149. package/dist/portability/filesystemObjects.d.ts +12 -0
  150. package/dist/portability/filesystemObjects.js +191 -0
  151. package/dist/portability/index.d.ts +5 -0
  152. package/dist/portability/index.js +4 -0
  153. package/dist/portability/objects.d.ts +63 -0
  154. package/dist/portability/objects.js +138 -0
  155. package/dist/portability/postgresRows.d.ts +64 -0
  156. package/dist/portability/postgresRows.js +182 -0
  157. package/dist/scheduler/advisoryLock.js +20 -2
  158. package/dist/scheduler/nodeScheduler.d.ts +2 -2
  159. package/dist/scheduler/nodeScheduler.js +35 -6
  160. package/package.json +47 -2
  161. package/release-gates.json +143 -9
  162. package/sql/20260814010000_platform_control_plane.sql +286 -0
  163. package/src/composition/index.ts +1 -0
  164. package/src/composition/marketingContribution.ts +37 -0
  165. package/src/composition/nodeHost.ts +4 -3
  166. package/src/lifecycle/appUpgrade.ts +186 -0
  167. package/src/lifecycle/index.ts +19 -0
  168. package/src/lifecycle/postgresInstallQualification.ts +125 -0
  169. package/src/marketing/acquisitionCaseSchemas.ts +75 -0
  170. package/src/marketing/acquisitionEvidenceSchemas.ts +72 -0
  171. package/src/marketing/analytics/dataLayer.ts +100 -0
  172. package/src/marketing/analytics/noop.ts +14 -0
  173. package/src/marketing/analytics/purchaseFacts.ts +63 -0
  174. package/src/marketing/analytics/routePolicy.ts +55 -0
  175. package/src/marketing/consentEvidence.ts +36 -0
  176. package/src/marketing/contactLegal.ts +22 -0
  177. package/src/marketing/contracts/acquisition.ts +50 -0
  178. package/src/marketing/contracts/acquisitionPorts.ts +48 -0
  179. package/src/marketing/contracts/analytics.ts +88 -0
  180. package/src/marketing/contracts/contactLegal.ts +41 -0
  181. package/src/marketing/contracts/index.ts +17 -0
  182. package/src/marketing/contracts/prelaunch.ts +15 -0
  183. package/src/marketing/index.ts +16 -0
  184. package/src/marketing/prelaunchAcquisitionSchemas.ts +64 -0
  185. package/src/marketing/seo/jsonLd.ts +153 -0
  186. package/src/marketing/seo/metadata.ts +62 -0
  187. package/src/marketing/seo/serializeJsonLd.ts +6 -0
  188. package/src/marketing/seo/urls.ts +34 -0
  189. package/src/marketing/server/acquisitionCaseDirect.ts +67 -0
  190. package/src/marketing/server/acquisitionCaseHandlers.ts +57 -0
  191. package/src/marketing/server/acquisitionCasePorts.ts +42 -0
  192. package/src/marketing/server/acquisitionEvidenceDirect.ts +76 -0
  193. package/src/marketing/server/analyticsContext.ts +55 -0
  194. package/src/marketing/server/analyticsContextPorts.ts +28 -0
  195. package/src/marketing/server/bindings.ts +27 -0
  196. package/src/marketing/server/directPorts.ts +27 -0
  197. package/src/marketing/server/index.ts +14 -0
  198. package/src/marketing/server/postgres/acquisitionCaseRoutines.ts +218 -0
  199. package/src/marketing/server/postgres/acquisitionEvidenceRoutines.ts +103 -0
  200. package/src/marketing/server/postgres/index.ts +3 -0
  201. package/src/marketing/server/prelaunchAcquisitionDirect.ts +121 -0
  202. package/src/marketing/server/privacyContributor.ts +123 -0
  203. package/src/marketing/server/research/adminSurveyResponsesHandler.ts +78 -0
  204. package/src/marketing/server/research/managedSurveyResponseSubmit.ts +158 -0
  205. package/src/marketing/server/research/surveyResponseSubmitHandler.ts +57 -0
  206. package/src/marketing/validation.ts +3 -0
  207. package/src/migrations/postgresRunner.ts +24 -9
  208. package/src/operations/adminAlertsSchemas.ts +78 -0
  209. package/src/operations/adminAlertsView.ts +238 -0
  210. package/src/operations/alertPagingPolicy.ts +50 -0
  211. package/src/operations/alertThrottlePolicy.ts +22 -0
  212. package/src/operations/clients.ts +44 -0
  213. package/src/operations/contracts/admin.ts +24 -0
  214. package/src/operations/contracts/adminAlertsContracts.ts +13 -0
  215. package/src/operations/contracts/alertHumanContext.ts +17 -0
  216. package/src/operations/contracts/index.ts +6 -0
  217. package/src/operations/contracts/observabilityContracts.ts +40 -0
  218. package/src/operations/contracts/observabilityPorts.ts +30 -0
  219. package/src/operations/contracts/ports.ts +22 -0
  220. package/src/operations/errors.ts +6 -0
  221. package/src/operations/index.ts +9 -0
  222. package/src/operations/schemas.ts +122 -0
  223. package/src/operations/server/adminAlertsHandlers.ts +61 -0
  224. package/src/operations/server/adminPipelineHandlers.ts +73 -0
  225. package/src/operations/server/adminSettingsHandlers.ts +114 -0
  226. package/src/operations/server/controlPlaneHandler.ts +56 -0
  227. package/src/operations/server/controlPlaneStore.ts +150 -0
  228. package/src/operations/server/controlPlaneWatchdog.ts +28 -0
  229. package/src/operations/server/index.ts +12 -0
  230. package/src/operations/server/migrations.ts +7 -0
  231. package/src/operations/server/platformWatchdogDecision.ts +93 -0
  232. package/src/operations/server/platformWatchdogDeliveryPolicy.ts +92 -0
  233. package/src/operations/server/platformWatchdogQueue.ts +96 -0
  234. package/src/operations/server/platformWatchdogService.ts +209 -0
  235. package/src/operations/validation.ts +3 -0
  236. package/src/portability/filesystemObjects.ts +138 -0
  237. package/src/portability/index.ts +12 -0
  238. package/src/portability/objects.ts +160 -0
  239. package/src/portability/postgresRows.ts +169 -0
  240. package/src/scheduler/advisoryLock.ts +20 -5
  241. package/src/scheduler/nodeScheduler.ts +28 -8
package/README.md CHANGED
@@ -10,8 +10,12 @@ Development preview. This package is part of the publishable set with core and o
10
10
 
11
11
  It holds the runnable server substrate an OpenLup application composes: the Node HTTP runtime, BFF dispatch, scheduling and job leases, blob storage, data gateways, the node-postgres migration runner and bundle composition. Ports and their contracts stay in `@openlup/core/platform-runtime`.
12
12
 
13
- For the prepared `0.28.0` set, read the [adjacent-release migration and known
14
- limitations](CHANGELOG.md#0280) before selecting optional services. Experimental
13
+ For the prepared `0.30.0` set, read the [adjacent-release migration and known
14
+ limitations](CHANGELOG.md#0300) before selecting optional services. Experimental
15
+ M13 marketing uses browser-safe `./marketing`, type-only browser-safe
16
+ `./marketing/contracts`, and Node-only `./marketing/server` and
17
+ `./marketing/server/postgres`. Marketing contributions and bindings remain
18
+ inert and default-off. Experimental
15
19
  account/support APIs use `./account-support` and the type-only
16
20
  `./account-support/contracts`; reference bindings remain inert and default-off.
17
21
  Apply the M12 support-authority forwards after complete same-rail predecessors
@@ -50,7 +54,7 @@ Adopters must not activate unselected or unready invoicing; installation and
50
54
  inert contributions register no runner, worker, route or schedule.
51
55
  M06 managed-host proof classes remain pending in the
52
56
  [M06 parity ledger](../../docs/platform/plans/oss-m06-stock-reservations-delivery.md#parity-pending-rows).
53
- Size comparison uses the published `0.27.0` tarball. Preparation and offline
57
+ Size comparison uses the published `0.29.0` tarball. Preparation and offline
54
58
  adapter evidence grant no publication or provider-account certification.
55
59
 
56
60
  Longer module recipes live in the [platform package reference](../../docs/platform/PLATFORM_PACKAGE_REFERENCE.md).
@@ -64,12 +68,52 @@ alongside seconds/fractions, compact offsets and local timestamps. Calendar
64
68
  validation still rejects impossible dates; validation never rewrites the raw
65
69
  timestamp used in the webhook's idempotency key.
66
70
 
71
+ The experimental migration source may supply `assertSession` to observe host
72
+ facts once under the existing advisory lock, after exact ledger-prefix admission
73
+ and before status, dry-run or pending SQL. Sources without it retain current
74
+ execution. The [migration owner](../../docs/platform/DATA_AND_MIGRATIONS.md#module-catalogues-and-installed-histories)
75
+ and [runner witness](test/migrations/postgresRunner.test.ts) define that boundary;
76
+ this callback does not provision or qualify a hosted installation.
77
+
67
78
  ## Package surface
68
79
 
80
+ The experimental Node-only `./lifecycle` entry plans and applies explicit
81
+ application-file snapshots. It preserves unmanaged files and local edits in
82
+ upstream-unchanged files, refuses divergent changed files before writes, and
83
+ resumes exact completed replacements after interruption. The caller holds an
84
+ exclusive maintenance window and supplies schema-range evidence; this entry
85
+ runs no database migration, npm install or deployment. Read the
86
+ [file-ownership boundary](../../docs/platform/ARCHITECTURE_AND_EXTENSIONS.md#application-file-upgrades)
87
+ and [disposable app recipe](../../docs/platform/PLATFORM_PACKAGE_REFERENCE.md#application-file-upgrade).
88
+
89
+ The same entry exports `inspectLegacyPostgresInstallRoles`, a read-only query
90
+ through a caller-owned PostgreSQL handle. Its
91
+ `legacy-fresh-bootstrap-role-prerequisites` report observes finite immutable
92
+ legacy bootstrap role conditions; `rolePrerequisitesMet` does not establish
93
+ install readiness, schema/ACL correctness or provider support. See the
94
+ [legacy role boundary](../../docs/platform/DATA_AND_MIGRATIONS.md#legacy-fresh-bootstrap-role-observation).
95
+
96
+ The experimental Node-only `./portability` entry copies a bounded, explicitly
97
+ complete immutable/quiesced object inventory and captures/restores selected
98
+ compatible PostgreSQL rows through injected transaction handles. Private
99
+ filesystem objects publish bytes and metadata together, bind an explicit access
100
+ label to the filesystem owner's private policy and resume by verified readback.
101
+ Row replay preserves exact JSON/numeric/time values, refuses conflicts and every
102
+ enabled user INSERT trigger, and copies no schema, migration ledger or grants.
103
+ The host qualifies the canonical catalogue, credentials and source completeness.
104
+ These helpers activate no executor and do not supply managed identity mapping,
105
+ live changed-row catch-up or hosted old-worker fencing. Read the
106
+ [transfer boundary](../../docs/platform/ARCHITECTURE_AND_EXTENSIONS.md#existing-store-transfer-and-recovery)
107
+ and the retained [M14 obligations](../../docs/platform/plans/oss-m14-operations-portability.md).
108
+ The provider-owned HTTP counterexample exercises an existing adapter's cached
109
+ bearer; its local fixture establishes no hosted revocation guarantee.
110
+
69
111
  `release-gates.json` declares kind `rail`: this foundation provides runtime infrastructure over core ports, matching the architecture's rail responsibility. Its experimental entries each bind a declaration snapshot and package-owned smoke tests. The API checker reads this same inventory.
70
112
 
71
113
  | Export | Contents |
72
114
  | --- | --- |
115
+ | `./lifecycle` | Experimental Node file-snapshot planning/apply and read-only legacy PostgreSQL role prerequisites; no install-readiness claim, optional SDK or activation |
116
+ | `./portability` | Experimental server object copy/resume, private filesystem transfer store and compatible PostgreSQL row capture/restore; injected handles, explicit refusal and no activation |
73
117
  | `./http` | Generic capped raw-body reader, hex HMAC and constant-time token comparison; Node request adaptation, deadlines, security headers and static documents, and `createNodeHttpRuntime` with its fixed request order |
74
118
  | `./bff` | `createBffDispatcher`, `lazyBffRoute`, the hosted router URL rewrite, the response envelope (`createBffResponder`), the observed request context and the health route |
75
119
  | `./scheduler` | Transport-neutral scheduled jobs (`ScheduledJob`, the job registry, the lease-aware runner and the authenticated HTTP cron entry), path-declared jobs from configuration, the node-cron scheduler with per-job advisory locks, hosted cron verification, the cron entrypoint guard (it compares the presented bearer token with `CRON_SECRET` in constant time) and direct job bindings |
@@ -93,6 +137,12 @@ timestamp used in the webhook's idempotency key.
93
137
  | `./inventory` | Experimental schemas/ports, handlers, native stores, projection, injected forecast and SQL descriptor; `./inventory/contracts` DTOs. |
94
138
  | `./catalog` | Experimental neutral catalog schemas, drafts, reads, media source packs and publication ports |
95
139
  | `./catalog/contracts` | Dependency-free catalog DTOs, media resolver, authority and canonicalization helpers |
140
+ | `./marketing/server` | Node-only acquisition handlers, direct read/evidence factories and research orchestration without `pg`; host responder, authorization, binding and notification copy |
141
+ | `./marketing/server/postgres` | Acquisition case and research evidence PostgreSQL port factories; requires the existing optional `pg` peer |
142
+ | `./marketing/server` (analytics context) | Injected analytics context handler with host persistence, capability and identity bindings |
143
+ | `./marketing`, `./marketing/contracts` | Browser-safe acquisition history, portable prelaunch, survey evidence and newsletter consent schemas; type-only DTOs and acquisition/research ports |
144
+ | `./marketing`, `./marketing/contracts` (analytics) | Browser-safe acquisition and research schemas, injected analytics, paid purchase and consent-time facts; type-only DTOs and ports |
145
+ | `./marketing`, `./marketing/contracts` (SEO and content slots) | Browser-safe JSON-LD serialization and graph builders, URL/hreflang helpers, ordered head data and contact/legal projections; type-only organization, contact, legal-document and privacy-processor content slots |
96
146
  | `./commerce`, `./commerce/contracts` | Browser-safe neutral commerce schema cores, generic ports and pure contract helpers |
97
147
  | `./commerce/server` | Node-only OMS, returns, native reviews/media and hosted-feedback handlers/adapters and order-detail composition, email consumers, dispatch registry, paid-fulfillment binding and checkout compensation, checkout handlers, runtime and shared paid-order saga |
98
148
  | `./pricing` | Experimental price authority, quote computation/handlers, offer projection/cache, readiness and policy tokens |
@@ -103,6 +153,135 @@ timestamp used in the webhook's idempotency key.
103
153
  | `./shipping` | Experimental delivery ports, injected-schema handlers, summary/evidence; native flat-rate port + Core schema readiness |
104
154
  | `./shipping/contracts` | Type-only delivery, pickup-point, summary and evidence DTOs |
105
155
 
156
+ ## Marketing contracts
157
+
158
+ `./marketing` provides acquisition address references, historical case projections,
159
+ list/count reads, portable prelaunch list/detail schemas, idempotency keys, survey submission/evidence/list schemas and
160
+ newsletter consent events/responses. Reference compatibility shims forward the
161
+ same schema objects and load the reference validation wrapper before package
162
+ schemas. The marketing runtime entry configures the shared Zod instance in
163
+ jitless mode before constructing schemas, preserving CSP-safe initialization
164
+ for direct imports as well as reference facades, like the pricing, catalog and
165
+ communications entries. The type-only `./marketing/contracts` entry has no
166
+ runtime initialization. The rich prelaunch DTO and case submit/transition schemas remain application-owned
167
+ and are excluded from the browser entries. The shared
168
+ `acquisitionOpaqueReferenceSchema` lets those retained contracts reuse the exact
169
+ reference syntax. No entry registers a route automatically.
170
+
171
+ `./marketing/contracts` exports only types. Explicit acquisition DTO shapes are
172
+ checked against the runtime schema inference by an executed TypeScript equality
173
+ witness. Reachable case ports expose list, get, active count and close; the app
174
+ uses the server-only compatibility port for its retired methods. Evidence ports retain survey submission,
175
+ survey listing and consent application. Port types are also forwarded through
176
+ the runtime entry, following the commerce boundary. Both entries forward research schemas or types from the compatibility
177
+ owner, `@openlup/core/marketing/research`; runtime schemas retain object identity.
178
+ Case locale remains `en|pl`, matching the persisted constraint. Widening it requires
179
+ a separate migration. Historical wire values remain unchanged.
180
+
181
+ The source and built entries have browser graph proofs. The root test runner
182
+ selects `test/marketing/`; the installed consumer checks both public entries,
183
+ compiles typed usage and refuses deep imports. Kiosk and research reference
184
+ contracts/ports continue to consume the core export. The app owns its admin
185
+ response client and host composition.
186
+
187
+ `./marketing/server` owns acquisition case handlers, list/count and prelaunch
188
+ list/detail factories, survey evidence submit/list and consent application,
189
+ research submit/admin handlers and managed survey orchestration without loading
190
+ `pg`. Select `./marketing/server/postgres` for the two PostgreSQL port factories
191
+ after installing the existing optional `pg` peer. Hosts inject the BFF responder,
192
+ authorization and binding resolvers. Direct case reads receive the host bundle
193
+ predicate at their original guard; prelaunch and evidence dispatch stays in the
194
+ host. Notification HTML and subject are injected separately, preserving
195
+ HTML rendering before the subject clock read. The reference owns retired
196
+ route refusals, raw signed consent ingress and notification copy. PostgreSQL
197
+ SQL, role selection and transaction/pool handling remain unchanged. The
198
+ server-only compatibility command types do not expose retired admission schemas.
199
+
200
+ Analytics facts share these browser-safe entries. `createNoopAnalytics` implements
201
+ Core's existing analytics port; deployment and advertising defaults remain inert.
202
+ `AnalyticsEventMap<StepName>` retains the host's literal step vocabulary, with
203
+ `string` as the neutral default. Private transport ownership fields and envelopes
204
+ remain host extensions. Types are forwarded type-only by `./marketing/contracts`.
205
+
206
+ `createAnalyticsDataLayer` owns loader readiness per instance and accepts the host's
207
+ browser/storage access, consent key and change event, `consentSignalNames`, event
208
+ prefix, container reader and deployment callback. `consentSignalNames` supplies
209
+ one `analytics` wire name and the ordered `advertising` wire names. Without it,
210
+ no consent command is pushed and optional event admission returns false, even
211
+ with browser access, a container and stored consent; deployment attempts remain
212
+ independent. Consent commands seed default before update and
213
+ use array-like arguments; advertising grants read marketing consent at push time.
214
+ Revocation denies every signal. Sinks are attempted independently with failure
215
+ isolation and no replay of observations made before consent. Admission booleans
216
+ mean synchronous local acceptance only, never delivery or business acknowledgement.
217
+
218
+ `createAnalyticsRoutePolicy` requires public paths, redirect sources and all
219
+ sensitive-path sets. Its optional-route predicate preserves broad eligibility;
220
+ deployment URL sanitization admits exact public paths and safe campaign values.
221
+ `projectPaidPurchase` consumes only structural paid-recap facts, preserving
222
+ allocation/grouping, the item cap and ordered suppression reasons without an
223
+ ambient currency policy. `selectMarketingHistoricalConsentTime` selects exact
224
+ native chronology only after host authorization, without changing the local
225
+ grant. The reference retains its narrower checkout-kind admission and all wire
226
+ names. [Purchase equivalence](../../src/checkout/machine/purchasePayloadEquivalence.test.ts)
227
+ and the package analytics witnesses pin these boundaries.
228
+
229
+ `createAnalyticsContextHandler` in `./marketing/server` accepts a required BFF
230
+ responder, enable reader, persistence resolver, access-token reader, trusted-subject
231
+ resolver, capability mint/hash functions, historical cookie name and `clientIdField`.
232
+ The host supplies the client-id wire field name through `clientIdField`; the strict
233
+ request also keeps `granted` and `explicitGrant`. Grants require a client
234
+ id and existing revoked contexts require an explicit new grant for replacement.
235
+ Only token hashes reach persistence. Enrollment uses only the server-verified
236
+ subject, and invalid bearer grants refuse before writes. Cookie attributes and
237
+ method/flag/origin/schema refusal order are preserved. The host owns flag and
238
+ RPC names, gateway authority, route mounting and observability. Persistence
239
+ operations must reject on failure; null persistence refuses as unavailable.
240
+ There is no default persistence, portable SQL or reference route for this handler.
241
+ An `active` result is a local context fact, never delivery, and telemetry context
242
+ failure never gates commerce. The [context witness](test/marketing/analyticsContext.test.ts)
243
+ exercises the factory through synthetic host bindings.
244
+
245
+ `./marketing` also provides browser-safe JSON-LD serialization and graph builders,
246
+ route URL/hreflang helpers, an ordered head descriptor and plain contact/legal
247
+ projections. Applications supply identity, origin, routes, locales, images and
248
+ content; the descriptor is plain data for a host head manager. The type-only
249
+ `./marketing/contracts` entry exports shared organization facts and contact,
250
+ legal-document and privacy-processor shapes, also forwarded as types by the
251
+ runtime entry. The package supplies no document wording, processor inventory or
252
+ content policy. The [marketing recipe](../../docs/platform/PLATFORM_PACKAGE_REFERENCE.md#marketing-seo-and-content-slots)
253
+ owns the mechanics and installed-artifact proof.
254
+
255
+ ## Marketing composition
256
+
257
+ `./composition` exports `createMarketingContribution`, an inert descriptor for
258
+ the `marketing` capability's `./marketing`, `./marketing/contracts` and
259
+ `./marketing/server` entries, with a runtime dependency on `identity/contracts`
260
+ for obligation validation.
261
+ Supply already-bound operations and host schema requirements. Ports, routes and
262
+ triggers share the supplied functions; null ports remain required for Core
263
+ readiness, and handlers retain only registration descriptors. Duplicate handler,
264
+ schedule or route identities refuse. Construction invokes no callable and
265
+ installs no worker, route or schedule. See the
266
+ [composition contract](../../docs/platform/ARCHITECTURE_AND_EXTENSIONS.md#contributions-seams-and-readiness)
267
+ for the host admission boundary.
268
+
269
+ `./marketing/server` also exports `createMarketingPrivacyContributor` and
270
+ `createMarketingPrivacyBindings`. Explicit enablement and both complete owner
271
+ ports select one contributor; its `exportPersonalData`, `assessErasure` and
272
+ `eraseOrRetain` callables can be passed directly to `createMarketingContribution`.
273
+ Missing ownership makes coverage unavailable and erasure unresolved. Identity
274
+ owns assessment ordering, subscription vetoes, receipt replay and transaction
275
+ rollback; the marketing fence runs only during `eraseOrRetain`. Provider request
276
+ acceptance never proves deletion. The reference leaves marketing unselected,
277
+ so its marketing privacy coverage remains an open gap. See the
278
+ [privacy composition recipe](../../docs/platform/PLATFORM_PACKAGE_REFERENCE.md#marketing-server-composition).
279
+
280
+ The contributor and binding declarations use identity contract types and local
281
+ structural interfaces compatible with identity's `PersonalDataContributor`.
282
+ They do not reference identity's main entry; `./marketing/server` compiles without
283
+ `pg` or `@types/pg`. Select `./marketing/server/postgres` explicitly for those ports.
284
+
106
285
  ## Commerce contracts
107
286
 
108
287
  `./composition` exports the inert `createCommerceContribution` for the installed
@@ -256,7 +435,7 @@ SQL (class (b) retention): native uses 14 days and the contributed managed profi
256
435
  uses 21 days. There is no TypeScript delay parameter or SQL change.
257
436
  The reference browser contract facades initialize their shared Zod instance in
258
437
  jitless mode before loading package schemas, preserving CSP-safe construction.
259
- The package contracts add no global Zod configuration side effect. Browser
438
+ The commerce package contracts add no global Zod configuration side effect. Browser
260
439
  conformance bundles the complete commerce entry and both reference facades
261
440
  without externals; the raw-upload source is a failing browser control, while
262
441
  the complete server entry bundles for Node.
@@ -584,6 +763,25 @@ both exported helpers and their signatures: `calculateIncludedVat` delegates to
584
763
 
585
764
  A job is `{ id, schedule, lease?, run(context) }` with a UTC cron expression. Domains and packages contribute jobs to `createScheduledJobRegistry`, which refuses duplicate ids and ships empty. The Node host runs registry jobs on its own timers and calls `run` directly, with no HTTP request and no secret. `createScheduledJobHttpEntry` mounts the same job behind the cron guard for a hosted cron. A job that declares `lease` runs only after the injected job-lease port grants it; a lease held elsewhere skips the run, and a finish the port refuses fails the run. `scheduledJobsFromContribution` turns a contribution's cadence schedules into jobs and refuses a cadence that no cron expression matches exactly; a contribution run that reports a failed outcome fails the job.
586
765
 
766
+ `NodeSchedulerAdapter.stopAll()` returns `Promise<void>`. It fences and stops the
767
+ current registrations synchronously, then waits for callbacks already admitted,
768
+ including pending lock acquisition and unlock. Await it before disposing their
769
+ resources; custom scheduler implementations must return that drain promise.
770
+ Later registrations remain supported. The Node host caches its `stop()` promise,
771
+ closes its listener and retains the dedicated PostgreSQL lock session until the
772
+ drain settles, including failure cleanup.
773
+
774
+ The PostgreSQL lock adapter also reserves each numeric advisory key locally
775
+ before acquisition. This prevents reentry on the same session, including pending
776
+ acquisitions and colliding job IDs. Failed acquisition releases the reservation;
777
+ failed unlock retains it until session disposal. The
778
+ [disposable PostgreSQL witness](../../tests/postgres/nodeHostRuntimeDrain.test.ts)
779
+ checks same-session exclusion and replacement-host admission after drain.
780
+ This graceful-stop contract does not fence an old host at external provider I/O
781
+ or settle an effect accepted before an acknowledgement; the
782
+ [transfer boundary](../../docs/platform/ARCHITECTURE_AND_EXTENSIONS.md#existing-store-transfer-and-recovery)
783
+ retains those separate duties.
784
+
587
785
  ## Bundle readiness
588
786
 
589
787
  `PLATFORM_BUNDLE` selects `vercel-supabase` (the default when absent), `node-supabase` or `node-postgres`. A bundle's readiness refuses, before any traffic or timer: an unknown `PLATFORM_BUNDLE` (resolution itself still falls back to the default); a self-host bundle without `APP_BASE_URL`; and, reported together under `OPENLUP_E_ENV_MISSING`, every selected capability without its settings: the scheduler without `CRON_SECRET`, Node scheduling without `DATABASE_URL` for its cluster lock (never a silent single-instance lock), and the selected blob capability without its settings (see Blob storage). An unknown `PLATFORM_BLOB` is refused too. The Node host checks readiness at boot.
@@ -1144,3 +1342,21 @@ their owner meaning. Contribution descriptors install no routes, handlers,
1144
1342
  schedules or workers. See the
1145
1343
  [reference recipe](../../docs/platform/RUNTIME_AND_SELF_HOSTING.md#opt-in-disposable-subscription-reference)
1146
1344
  for the host's schema and transport obligations.
1345
+
1346
+ ## Operations and diagnostics
1347
+
1348
+ The experimental `./operations` entry contains browser-safe operational schemas,
1349
+ alert projections and `createOperationsClients`, which binds an existing BFF
1350
+ requester. `./operations/contracts` contains DTOs and narrow ports only.
1351
+ `./operations/server` contains settings read/update, pipeline read, alert overview,
1352
+ control-plane handlers and the existing watchdog delivery mechanism. Authorization,
1353
+ SQL execution, evidence evaluation and alert transport are supplied by the host;
1354
+ importing these entries registers no route, schedule or provider.
1355
+
1356
+ `runControlPlaneWatchdogTick` evaluates persisted neutral job failures and records
1357
+ an idempotent heartbeat unless `checkOnly` is true. The richer watchdog accepts a
1358
+ typed evaluator; the public reference binds its maintained domain evaluator.
1359
+ Settings preserve scalar values, and pipeline reads preserve the existing tester
1360
+ and email projection. Identity membership and tracking effects keep their owners.
1361
+ See the [operations architecture](../../docs/platform/ARCHITECTURE_AND_EXTENSIONS.md#operations-and-diagnostics)
1362
+ for refusal behavior, SQL prerequisites and the bounded preview scope.
@@ -8,3 +8,4 @@ export { createFulfillmentContribution } from "./fulfillmentContribution.js";
8
8
  export { createSubscriptionContribution } from "./subscriptionContribution.js";
9
9
  export { createCommerceContribution } from "./commerceContribution.js";
10
10
  export { createAccountSupportContribution } from "./accountSupportContribution.js";
11
+ export { createMarketingContribution } from "./marketingContribution.js";
@@ -9,3 +9,4 @@ export { createFulfillmentContribution } from "./fulfillmentContribution.js";
9
9
  export { createSubscriptionContribution } from "./subscriptionContribution.js";
10
10
  export { createCommerceContribution } from "./commerceContribution.js";
11
11
  export { createAccountSupportContribution } from "./accountSupportContribution.js";
12
+ export { createMarketingContribution } from "./marketingContribution.js";
@@ -0,0 +1,21 @@
1
+ import type { Contribution, RequiredSchema } from "@openlup/core/readiness";
2
+ /** Describe existing selected marketing callables; never install another executor. */
3
+ export declare function createMarketingContribution(input: {
4
+ version: string;
5
+ requiredSchema: RequiredSchema;
6
+ marketing: Readonly<Record<string, ((...args: never[]) => unknown) | null>>;
7
+ handlers: Contribution["handlers"];
8
+ schedules: Contribution["schedules"];
9
+ routes: Contribution["routes"];
10
+ }): {
11
+ contributions: Contribution[];
12
+ ports: {
13
+ [k: string]: ((...args: never[]) => unknown) | null;
14
+ };
15
+ routes: {
16
+ [k: string]: (...args: never[]) => unknown;
17
+ };
18
+ triggers: {
19
+ [k: string]: (...args: never[]) => unknown;
20
+ };
21
+ };
@@ -0,0 +1,29 @@
1
+ /** Describe existing selected marketing callables; never install another executor. */
2
+ export function createMarketingContribution(input) {
3
+ for (const [kind, names] of [
4
+ ["handler", input.handlers.map(handler => handler.eventType)],
5
+ ["schedule", input.schedules.map(schedule => schedule.id)],
6
+ ["route", input.routes.map(route => route.id)],
7
+ ]) {
8
+ if (new Set(names).size !== names.length)
9
+ throw new Error(`marketing_duplicate_${kind}`);
10
+ }
11
+ const contribution = {
12
+ manifest: {
13
+ name: "@openlup/platform", version: input.version, kind: "rail",
14
+ module: { id: "marketing", kind: "capability", provides: ["./marketing", "./marketing/contracts", "./marketing/server"],
15
+ // The server entry validates obligations with identity's runtime contract schema.
16
+ dependsOn: [{ module: "identity", entries: ["./identity/contracts"] }] },
17
+ emits: [], handles: input.handlers.map(handler => handler.eventType),
18
+ schedules: input.schedules.map(({ id, cadenceSeconds }) => ({ id, cadenceSeconds })), env: [],
19
+ requiredPorts: Object.keys(input.marketing), routes: input.routes.map(route => route.id), requiredSchema: input.requiredSchema,
20
+ },
21
+ handlers: input.handlers.map(({ eventType, timeoutMs }) => Object.freeze({ eventType, timeoutMs })), schedules: input.schedules, routes: input.routes,
22
+ };
23
+ return {
24
+ contributions: [contribution],
25
+ ports: Object.fromEntries(Object.entries(input.marketing).filter(([, fn]) => typeof fn === "function").map(([name, fn]) => [`@openlup/platform/marketing:${name}`, fn])),
26
+ routes: Object.fromEntries(input.routes.map(route => [`@openlup/platform/marketing:${route.id}`, route.handle])),
27
+ triggers: Object.fromEntries(input.schedules.map(schedule => [`@openlup/platform/marketing:${schedule.id}`, schedule.run])),
28
+ };
29
+ }
@@ -88,7 +88,7 @@ export interface ServeNodeResult {
88
88
  bundleId: string;
89
89
  httpRuntime: HttpRuntimePort;
90
90
  registeredJobs: string[];
91
- /** Stop timers and the local listener after a host signal; safe to call repeatedly. */
91
+ /** Fence timers, drain admitted jobs, then close their lock session; also close the local listener. Safe to call repeatedly. */
92
92
  stop(): Promise<void>;
93
93
  }
94
94
  export interface NodeHost {
@@ -133,12 +133,13 @@ export function createNodeHost(deps) {
133
133
  return { assertBundleReady, serveNode };
134
134
  }
135
135
  async function stopNodeHost(scheduler, advisoryLock, httpRuntime) {
136
- const timers = Promise.resolve().then(() => scheduler?.stopAll());
136
+ const timers = scheduler?.stopAll() ?? Promise.resolve();
137
137
  const server = httpRuntime.server;
138
138
  const listener = server
139
139
  ? new Promise((resolveClose, reject) => server.close((error) => error ? reject(error) : resolveClose()))
140
140
  : Promise.resolve();
141
- const results = await Promise.allSettled([timers, advisoryLock?.close(), listener]);
141
+ const lock = timers.then(() => advisoryLock?.close(), () => advisoryLock?.close());
142
+ const results = await Promise.allSettled([timers, lock, listener]);
142
143
  const failures = results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
143
144
  if (failures.length > 0)
144
145
  throw new AggregateError(failures, "node_host_shutdown_failed");
@@ -0,0 +1,52 @@
1
+ /** Exact UTF-8 scaffold files; every unlisted path remains application-owned. */
2
+ export interface AppVersionSnapshot {
3
+ version: string;
4
+ schema: {
5
+ min: number;
6
+ max: number;
7
+ };
8
+ files: Readonly<Record<string, string>>;
9
+ }
10
+ export interface AppUpgradeInput {
11
+ /** Absolute canonical path (as returned by realpath) of an existing app directory. */
12
+ root: string;
13
+ from: AppVersionSnapshot;
14
+ to: AppVersionSnapshot;
15
+ /** Observed by the caller; planning and applying execute no database work. */
16
+ schemaVersion: number;
17
+ }
18
+ export type AppUpgradeAction = "create" | "replace" | "remove" | "unchanged" | "preserved" | "already-applied";
19
+ export interface AppUpgradeConflict {
20
+ path: string;
21
+ reason: string;
22
+ }
23
+ export interface AppUpgradePlan {
24
+ fromVersion: string;
25
+ toVersion: string;
26
+ schemaVersion: number;
27
+ files: {
28
+ path: string;
29
+ action: AppUpgradeAction;
30
+ }[];
31
+ conflicts: AppUpgradeConflict[];
32
+ }
33
+ export declare class AppUpgradeConflictError extends Error {
34
+ readonly conflicts: readonly AppUpgradeConflict[];
35
+ constructor(conflicts: readonly AppUpgradeConflict[]);
36
+ }
37
+ /** Read-only comparison of explicit predecessor and candidate snapshots. */
38
+ export declare function planAppUpgrade(input: AppUpgradeInput): Promise<AppUpgradePlan>;
39
+ /**
40
+ * The caller must hold an exclusive maintenance window over the application tree.
41
+ * This is not a lock against concurrent editors or hostile filesystem mutation.
42
+ * All conflicts are checked before any writes. A later I/O failure can leave a
43
+ * partial upgrade: rerun the same snapshots to recognize exact completed bytes.
44
+ * Each replacement is atomic on a local filesystem; the complete upgrade is not.
45
+ * An abrupt process termination can leave a uniquely named temporary file; it is
46
+ * never treated as a managed app file or resumed operation.
47
+ * No SQL, dependency install, version marker, deployment or user hook is executed.
48
+ */
49
+ export declare function applyAppUpgrade(input: AppUpgradeInput): Promise<{
50
+ plan: AppUpgradePlan;
51
+ applied: string[];
52
+ }>;
@@ -0,0 +1,180 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { promises as fs } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ export class AppUpgradeConflictError extends Error {
5
+ conflicts;
6
+ constructor(conflicts) {
7
+ super(`Application upgrade refused: ${conflicts.map(({ path, reason }) => `${path || "schema"}: ${reason}`).join("; ")}`);
8
+ this.conflicts = conflicts;
9
+ this.name = "AppUpgradeConflictError";
10
+ }
11
+ }
12
+ /** Read-only comparison of explicit predecessor and candidate snapshots. */
13
+ export async function planAppUpgrade(input) {
14
+ return (await inspectUpgrade(input)).plan;
15
+ }
16
+ /**
17
+ * The caller must hold an exclusive maintenance window over the application tree.
18
+ * This is not a lock against concurrent editors or hostile filesystem mutation.
19
+ * All conflicts are checked before any writes. A later I/O failure can leave a
20
+ * partial upgrade: rerun the same snapshots to recognize exact completed bytes.
21
+ * Each replacement is atomic on a local filesystem; the complete upgrade is not.
22
+ * An abrupt process termination can leave a uniquely named temporary file; it is
23
+ * never treated as a managed app file or resumed operation.
24
+ * No SQL, dependency install, version marker, deployment or user hook is executed.
25
+ */
26
+ export async function applyAppUpgrade(input) {
27
+ const { root, plan, target } = await inspectUpgrade(input);
28
+ if (plan.conflicts.length)
29
+ throw new AppUpgradeConflictError(plan.conflicts);
30
+ const applied = [];
31
+ for (const file of plan.files) {
32
+ const destination = join(root, file.path);
33
+ if (file.action === "remove") {
34
+ await fs.unlink(destination);
35
+ }
36
+ else if (file.action === "create" || file.action === "replace") {
37
+ await fs.mkdir(dirname(destination), { recursive: true });
38
+ const temporary = join(dirname(destination), `.openlup-${randomUUID()}.tmp`);
39
+ // Rename replaces this directory entry rather than modifying a possible hardlink.
40
+ const mode = file.action === "replace" ? (await fs.stat(destination)).mode & 0o777 : 0o600;
41
+ let handle;
42
+ try {
43
+ handle = await fs.open(temporary, "wx", mode);
44
+ if (file.action === "replace")
45
+ await handle.chmod(mode);
46
+ await handle.writeFile(target.get(file.path), "utf8");
47
+ await handle.close();
48
+ await fs.rename(temporary, destination);
49
+ }
50
+ finally {
51
+ if (handle) {
52
+ await handle.close();
53
+ await fs.rm(temporary, { force: true });
54
+ }
55
+ }
56
+ }
57
+ else
58
+ continue;
59
+ applied.push(file.path);
60
+ }
61
+ return { plan, applied };
62
+ }
63
+ async function inspectUpgrade(input) {
64
+ const before = snapshotFiles(input.from), target = snapshotFiles(input.to);
65
+ const paths = [...new Set([...before.keys(), ...target.keys()])].sort();
66
+ const aliases = new Set();
67
+ for (const path of paths) {
68
+ const alias = path.normalize("NFC").toLowerCase().toUpperCase();
69
+ if (aliases.has(alias))
70
+ throw new Error(`Application upgrade path alias: ${path}`);
71
+ aliases.add(alias);
72
+ }
73
+ const directorySpellings = new Map();
74
+ for (const path of paths) {
75
+ const components = path.split("/");
76
+ while (components.length > 1) {
77
+ components.pop();
78
+ const directory = components.join("/"), alias = directory.normalize("NFC").toLowerCase().toUpperCase();
79
+ if (aliases.has(alias))
80
+ throw new Error(`Application upgrade file-parent collision: ${path}`);
81
+ // Parents may not exist yet; both snapshots must still agree on their spelling.
82
+ const spelling = directorySpellings.get(alias);
83
+ if (spelling !== undefined && spelling !== directory)
84
+ throw new Error(`Application upgrade path alias: ${path}`);
85
+ directorySpellings.set(alias, directory);
86
+ }
87
+ }
88
+ if (!Number.isSafeInteger(input.schemaVersion) || input.schemaVersion < 0)
89
+ throw new Error("Application schema version must be a nonnegative safe integer");
90
+ const plan = {
91
+ fromVersion: input.from.version, toVersion: input.to.version,
92
+ schemaVersion: input.schemaVersion, files: [], conflicts: [],
93
+ };
94
+ for (const snapshot of [input.from, input.to]) {
95
+ if (input.schemaVersion < snapshot.schema.min || input.schemaVersion > snapshot.schema.max) {
96
+ plan.conflicts.push({ path: "", reason: `schema ${input.schemaVersion} is outside ${snapshot.version} range ${snapshot.schema.min}..${snapshot.schema.max}` });
97
+ }
98
+ }
99
+ const requestedRoot = resolve(input.root), rootStat = await fs.lstat(requestedRoot);
100
+ if (rootStat.isSymbolicLink() || !rootStat.isDirectory())
101
+ throw new Error("Application upgrade root must be an ordinary directory");
102
+ const root = await fs.realpath(requestedRoot);
103
+ if (input.root !== root)
104
+ throw new Error("Application upgrade root must be an absolute canonical path");
105
+ for (const path of paths) {
106
+ const observed = await readFile(root, path);
107
+ if (typeof observed === "string") {
108
+ plan.conflicts.push({ path, reason: observed });
109
+ continue;
110
+ }
111
+ const oldBytes = before.has(path) ? Buffer.from(before.get(path), "utf8") : null;
112
+ const newBytes = target.has(path) ? Buffer.from(target.get(path), "utf8") : null;
113
+ const equal = (bytes) => observed === null ? bytes === null : bytes !== null && observed.equals(bytes);
114
+ let action;
115
+ if (oldBytes !== null && newBytes !== null && oldBytes.equals(newBytes)) {
116
+ // An upstream-unchanged file carries no write authority over local customization.
117
+ action = equal(oldBytes) ? "unchanged" : "preserved";
118
+ }
119
+ else if (equal(newBytes))
120
+ action = "already-applied";
121
+ else if (equal(oldBytes))
122
+ action = newBytes === null ? "remove" : oldBytes === null ? "create" : "replace";
123
+ else {
124
+ plan.conflicts.push({ path, reason: "current bytes match neither predecessor nor candidate" });
125
+ continue;
126
+ }
127
+ plan.files.push({ path, action });
128
+ }
129
+ return { root, plan, target };
130
+ }
131
+ function snapshotFiles(snapshot) {
132
+ if (!snapshot || typeof snapshot.version !== "string" || !snapshot.version.trim())
133
+ throw new Error("Application snapshot needs an explicit version");
134
+ const range = snapshot.schema;
135
+ if (!range || !Number.isSafeInteger(range.min) || !Number.isSafeInteger(range.max) || range.min < 0 || range.max < range.min)
136
+ throw new Error("Application snapshot needs an ordered nonnegative schema range");
137
+ if (!snapshot.files || typeof snapshot.files !== "object" || Array.isArray(snapshot.files))
138
+ throw new Error("Application snapshot files must be a record");
139
+ const result = new Map();
140
+ for (const [path, content] of Object.entries(snapshot.files)) {
141
+ const segments = path.split("/");
142
+ if (!path || Buffer.from(path, "utf8").toString("utf8") !== path || /[\\:]/u.test(path) || [...path].some(character => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127) || segments.some(segment => !segment || segment === "." || segment === ".." || /[. ]$/u.test(segment)) || segments.some(segment => [".git", "node_modules"].includes(segment.toLowerCase()))) {
143
+ throw new Error(`Unsafe application upgrade path: ${path}`);
144
+ }
145
+ if (typeof content !== "string" || Buffer.from(content, "utf8").toString("utf8") !== content)
146
+ throw new Error(`Application upgrade content must be valid UTF-8: ${path}`);
147
+ result.set(path, content);
148
+ }
149
+ return result;
150
+ }
151
+ /** A string is a structural refusal; null means an absent path. Never follow links. */
152
+ async function readFile(root, path) {
153
+ const segments = path.split("/");
154
+ let current = root;
155
+ for (const [index, segment] of segments.entries()) {
156
+ const parent = current;
157
+ current = join(current, segment);
158
+ let stat;
159
+ try {
160
+ stat = await fs.lstat(current);
161
+ }
162
+ catch (error) {
163
+ if (error.code === "ENOENT")
164
+ return null;
165
+ throw error;
166
+ }
167
+ // A filesystem alias is not ownership of the differently spelled entry.
168
+ // Check only after lstat: ENOENT on a case-sensitive store stays absence.
169
+ if (!(await fs.readdir(parent)).includes(segment))
170
+ return "managed path spelling differs from directory entry";
171
+ if (stat.isSymbolicLink())
172
+ return "symlink in managed path";
173
+ const last = index === segments.length - 1;
174
+ if (last)
175
+ return stat.isFile() ? fs.readFile(current) : "managed path is not a regular file";
176
+ if (!stat.isDirectory())
177
+ return "managed parent is not a directory";
178
+ }
179
+ return null;
180
+ }
@@ -0,0 +1,2 @@
1
+ export { planAppUpgrade, applyAppUpgrade, AppUpgradeConflictError, type AppVersionSnapshot, type AppUpgradeInput, type AppUpgradeAction, type AppUpgradeConflict, type AppUpgradePlan, } from "./appUpgrade.js";
2
+ export { inspectLegacyPostgresInstallRoles, type PostgresInstallQuery, type LegacyPostgresInstallRequest, type PostgresInstallRoleObservation, type PostgresInstallMembershipObservation, type LegacyPostgresInstallBlocker, type LegacyPostgresInstallRoleReport, } from "./postgresInstallQualification.js";
@@ -0,0 +1,2 @@
1
+ export { planAppUpgrade, applyAppUpgrade, AppUpgradeConflictError, } from "./appUpgrade.js";
2
+ export { inspectLegacyPostgresInstallRoles, } from "./postgresInstallQualification.js";