@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.
- package/CHANGELOG.md +369 -156
- package/README.md +220 -4
- package/dist/composition/index.d.ts +1 -0
- package/dist/composition/index.js +1 -0
- package/dist/composition/marketingContribution.d.ts +21 -0
- package/dist/composition/marketingContribution.js +29 -0
- package/dist/composition/nodeHost.d.ts +1 -1
- package/dist/composition/nodeHost.js +3 -2
- package/dist/lifecycle/appUpgrade.d.ts +52 -0
- package/dist/lifecycle/appUpgrade.js +180 -0
- package/dist/lifecycle/index.d.ts +2 -0
- package/dist/lifecycle/index.js +2 -0
- package/dist/lifecycle/postgresInstallQualification.d.ts +51 -0
- package/dist/lifecycle/postgresInstallQualification.js +74 -0
- package/dist/marketing/acquisitionCaseSchemas.d.ts +103 -0
- package/dist/marketing/acquisitionCaseSchemas.js +53 -0
- package/dist/marketing/acquisitionEvidenceSchemas.d.ts +105 -0
- package/dist/marketing/acquisitionEvidenceSchemas.js +55 -0
- package/dist/marketing/analytics/dataLayer.d.ts +18 -0
- package/dist/marketing/analytics/dataLayer.js +111 -0
- package/dist/marketing/analytics/noop.d.ts +9 -0
- package/dist/marketing/analytics/noop.js +8 -0
- package/dist/marketing/analytics/purchaseFacts.d.ts +4 -0
- package/dist/marketing/analytics/purchaseFacts.js +69 -0
- package/dist/marketing/analytics/routePolicy.d.ts +10 -0
- package/dist/marketing/analytics/routePolicy.js +59 -0
- package/dist/marketing/consentEvidence.d.ts +8 -0
- package/dist/marketing/consentEvidence.js +32 -0
- package/dist/marketing/contactLegal.d.ts +14 -0
- package/dist/marketing/contactLegal.js +19 -0
- package/dist/marketing/contracts/acquisition.d.ts +88 -0
- package/dist/marketing/contracts/acquisition.js +1 -0
- package/dist/marketing/contracts/acquisitionPorts.d.ts +49 -0
- package/dist/marketing/contracts/acquisitionPorts.js +1 -0
- package/dist/marketing/contracts/analytics.d.ts +144 -0
- package/dist/marketing/contracts/analytics.js +1 -0
- package/dist/marketing/contracts/contactLegal.d.ts +53 -0
- package/dist/marketing/contracts/contactLegal.js +1 -0
- package/dist/marketing/contracts/index.d.ts +6 -0
- package/dist/marketing/contracts/index.js +1 -0
- package/dist/marketing/contracts/prelaunch.d.ts +39 -0
- package/dist/marketing/contracts/prelaunch.js +1 -0
- package/dist/marketing/index.d.ts +16 -0
- package/dist/marketing/index.js +14 -0
- package/dist/marketing/prelaunchAcquisitionSchemas.d.ts +116 -0
- package/dist/marketing/prelaunchAcquisitionSchemas.js +51 -0
- package/dist/marketing/seo/jsonLd.d.ts +157 -0
- package/dist/marketing/seo/jsonLd.js +127 -0
- package/dist/marketing/seo/metadata.d.ts +41 -0
- package/dist/marketing/seo/metadata.js +36 -0
- package/dist/marketing/seo/serializeJsonLd.d.ts +1 -0
- package/dist/marketing/seo/serializeJsonLd.js +6 -0
- package/dist/marketing/seo/urls.d.ts +14 -0
- package/dist/marketing/seo/urls.js +23 -0
- package/dist/marketing/server/acquisitionCaseDirect.d.ts +4 -0
- package/dist/marketing/server/acquisitionCaseDirect.js +88 -0
- package/dist/marketing/server/acquisitionCaseHandlers.d.ts +28 -0
- package/dist/marketing/server/acquisitionCaseHandlers.js +40 -0
- package/dist/marketing/server/acquisitionCasePorts.d.ts +42 -0
- package/dist/marketing/server/acquisitionCasePorts.js +1 -0
- package/dist/marketing/server/acquisitionEvidenceDirect.d.ts +5 -0
- package/dist/marketing/server/acquisitionEvidenceDirect.js +100 -0
- package/dist/marketing/server/analyticsContext.d.ts +3 -0
- package/dist/marketing/server/analyticsContext.js +65 -0
- package/dist/marketing/server/analyticsContextPorts.d.ts +31 -0
- package/dist/marketing/server/analyticsContextPorts.js +1 -0
- package/dist/marketing/server/bindings.d.ts +14 -0
- package/dist/marketing/server/bindings.js +18 -0
- package/dist/marketing/server/directPorts.d.ts +26 -0
- package/dist/marketing/server/directPorts.js +1 -0
- package/dist/marketing/server/index.d.ts +14 -0
- package/dist/marketing/server/index.js +11 -0
- package/dist/marketing/server/postgres/acquisitionCaseRoutines.d.ts +10 -0
- package/dist/marketing/server/postgres/acquisitionCaseRoutines.js +204 -0
- package/dist/marketing/server/postgres/acquisitionEvidenceRoutines.d.ts +10 -0
- package/dist/marketing/server/postgres/acquisitionEvidenceRoutines.js +107 -0
- package/dist/marketing/server/postgres/index.d.ts +3 -0
- package/dist/marketing/server/postgres/index.js +3 -0
- package/dist/marketing/server/prelaunchAcquisitionDirect.d.ts +4 -0
- package/dist/marketing/server/prelaunchAcquisitionDirect.js +122 -0
- package/dist/marketing/server/privacyContributor.d.ts +66 -0
- package/dist/marketing/server/privacyContributor.js +87 -0
- package/dist/marketing/server/research/adminSurveyResponsesHandler.d.ts +9 -0
- package/dist/marketing/server/research/adminSurveyResponsesHandler.js +53 -0
- package/dist/marketing/server/research/managedSurveyResponseSubmit.d.ts +39 -0
- package/dist/marketing/server/research/managedSurveyResponseSubmit.js +102 -0
- package/dist/marketing/server/research/surveyResponseSubmitHandler.d.ts +8 -0
- package/dist/marketing/server/research/surveyResponseSubmitHandler.js +41 -0
- package/dist/marketing/validation.d.ts +2 -0
- package/dist/marketing/validation.js +3 -0
- package/dist/migrations/postgresRunner.d.ts +9 -0
- package/dist/migrations/postgresRunner.js +17 -10
- package/dist/operations/adminAlertsSchemas.d.ts +153 -0
- package/dist/operations/adminAlertsSchemas.js +69 -0
- package/dist/operations/adminAlertsView.d.ts +73 -0
- package/dist/operations/adminAlertsView.js +184 -0
- package/dist/operations/alertPagingPolicy.d.ts +15 -0
- package/dist/operations/alertPagingPolicy.js +43 -0
- package/dist/operations/alertThrottlePolicy.d.ts +3 -0
- package/dist/operations/alertThrottlePolicy.js +18 -0
- package/dist/operations/clients.d.ts +17 -0
- package/dist/operations/clients.js +32 -0
- package/dist/operations/contracts/admin.d.ts +93 -0
- package/dist/operations/contracts/admin.js +1 -0
- package/dist/operations/contracts/adminAlertsContracts.d.ts +40 -0
- package/dist/operations/contracts/adminAlertsContracts.js +1 -0
- package/dist/operations/contracts/alertHumanContext.d.ts +7 -0
- package/dist/operations/contracts/alertHumanContext.js +1 -0
- package/dist/operations/contracts/index.d.ts +6 -0
- package/dist/operations/contracts/index.js +6 -0
- package/dist/operations/contracts/observabilityContracts.d.ts +41 -0
- package/dist/operations/contracts/observabilityContracts.js +1 -0
- package/dist/operations/contracts/observabilityPorts.d.ts +18 -0
- package/dist/operations/contracts/observabilityPorts.js +1 -0
- package/dist/operations/contracts/ports.d.ts +19 -0
- package/dist/operations/contracts/ports.js +1 -0
- package/dist/operations/errors.d.ts +6 -0
- package/dist/operations/errors.js +6 -0
- package/dist/operations/index.d.ts +9 -0
- package/dist/operations/index.js +9 -0
- package/dist/operations/schemas.d.ts +138 -0
- package/dist/operations/schemas.js +104 -0
- package/dist/operations/server/adminAlertsHandlers.d.ts +16 -0
- package/dist/operations/server/adminAlertsHandlers.js +35 -0
- package/dist/operations/server/adminPipelineHandlers.d.ts +12 -0
- package/dist/operations/server/adminPipelineHandlers.js +48 -0
- package/dist/operations/server/adminSettingsHandlers.d.ts +14 -0
- package/dist/operations/server/adminSettingsHandlers.js +82 -0
- package/dist/operations/server/controlPlaneHandler.d.ts +7 -0
- package/dist/operations/server/controlPlaneHandler.js +54 -0
- package/dist/operations/server/controlPlaneStore.d.ts +12 -0
- package/dist/operations/server/controlPlaneStore.js +138 -0
- package/dist/operations/server/controlPlaneWatchdog.d.ts +7 -0
- package/dist/operations/server/controlPlaneWatchdog.js +20 -0
- package/dist/operations/server/index.d.ts +12 -0
- package/dist/operations/server/index.js +12 -0
- package/dist/operations/server/migrations.d.ts +14 -0
- package/dist/operations/server/migrations.js +7 -0
- package/dist/operations/server/platformWatchdogDecision.d.ts +34 -0
- package/dist/operations/server/platformWatchdogDecision.js +71 -0
- package/dist/operations/server/platformWatchdogDeliveryPolicy.d.ts +10 -0
- package/dist/operations/server/platformWatchdogDeliveryPolicy.js +53 -0
- package/dist/operations/server/platformWatchdogQueue.d.ts +11 -0
- package/dist/operations/server/platformWatchdogQueue.js +94 -0
- package/dist/operations/server/platformWatchdogService.d.ts +47 -0
- package/dist/operations/server/platformWatchdogService.js +105 -0
- package/dist/operations/validation.d.ts +2 -0
- package/dist/operations/validation.js +3 -0
- package/dist/portability/filesystemObjects.d.ts +12 -0
- package/dist/portability/filesystemObjects.js +191 -0
- package/dist/portability/index.d.ts +5 -0
- package/dist/portability/index.js +4 -0
- package/dist/portability/objects.d.ts +63 -0
- package/dist/portability/objects.js +138 -0
- package/dist/portability/postgresRows.d.ts +64 -0
- package/dist/portability/postgresRows.js +182 -0
- package/dist/scheduler/advisoryLock.js +20 -2
- package/dist/scheduler/nodeScheduler.d.ts +2 -2
- package/dist/scheduler/nodeScheduler.js +35 -6
- package/package.json +47 -2
- package/release-gates.json +143 -9
- package/sql/20260814010000_platform_control_plane.sql +286 -0
- package/src/composition/index.ts +1 -0
- package/src/composition/marketingContribution.ts +37 -0
- package/src/composition/nodeHost.ts +4 -3
- package/src/lifecycle/appUpgrade.ts +186 -0
- package/src/lifecycle/index.ts +19 -0
- package/src/lifecycle/postgresInstallQualification.ts +125 -0
- package/src/marketing/acquisitionCaseSchemas.ts +75 -0
- package/src/marketing/acquisitionEvidenceSchemas.ts +72 -0
- package/src/marketing/analytics/dataLayer.ts +100 -0
- package/src/marketing/analytics/noop.ts +14 -0
- package/src/marketing/analytics/purchaseFacts.ts +63 -0
- package/src/marketing/analytics/routePolicy.ts +55 -0
- package/src/marketing/consentEvidence.ts +36 -0
- package/src/marketing/contactLegal.ts +22 -0
- package/src/marketing/contracts/acquisition.ts +50 -0
- package/src/marketing/contracts/acquisitionPorts.ts +48 -0
- package/src/marketing/contracts/analytics.ts +88 -0
- package/src/marketing/contracts/contactLegal.ts +41 -0
- package/src/marketing/contracts/index.ts +17 -0
- package/src/marketing/contracts/prelaunch.ts +15 -0
- package/src/marketing/index.ts +16 -0
- package/src/marketing/prelaunchAcquisitionSchemas.ts +64 -0
- package/src/marketing/seo/jsonLd.ts +153 -0
- package/src/marketing/seo/metadata.ts +62 -0
- package/src/marketing/seo/serializeJsonLd.ts +6 -0
- package/src/marketing/seo/urls.ts +34 -0
- package/src/marketing/server/acquisitionCaseDirect.ts +67 -0
- package/src/marketing/server/acquisitionCaseHandlers.ts +57 -0
- package/src/marketing/server/acquisitionCasePorts.ts +42 -0
- package/src/marketing/server/acquisitionEvidenceDirect.ts +76 -0
- package/src/marketing/server/analyticsContext.ts +55 -0
- package/src/marketing/server/analyticsContextPorts.ts +28 -0
- package/src/marketing/server/bindings.ts +27 -0
- package/src/marketing/server/directPorts.ts +27 -0
- package/src/marketing/server/index.ts +14 -0
- package/src/marketing/server/postgres/acquisitionCaseRoutines.ts +218 -0
- package/src/marketing/server/postgres/acquisitionEvidenceRoutines.ts +103 -0
- package/src/marketing/server/postgres/index.ts +3 -0
- package/src/marketing/server/prelaunchAcquisitionDirect.ts +121 -0
- package/src/marketing/server/privacyContributor.ts +123 -0
- package/src/marketing/server/research/adminSurveyResponsesHandler.ts +78 -0
- package/src/marketing/server/research/managedSurveyResponseSubmit.ts +158 -0
- package/src/marketing/server/research/surveyResponseSubmitHandler.ts +57 -0
- package/src/marketing/validation.ts +3 -0
- package/src/migrations/postgresRunner.ts +24 -9
- package/src/operations/adminAlertsSchemas.ts +78 -0
- package/src/operations/adminAlertsView.ts +238 -0
- package/src/operations/alertPagingPolicy.ts +50 -0
- package/src/operations/alertThrottlePolicy.ts +22 -0
- package/src/operations/clients.ts +44 -0
- package/src/operations/contracts/admin.ts +24 -0
- package/src/operations/contracts/adminAlertsContracts.ts +13 -0
- package/src/operations/contracts/alertHumanContext.ts +17 -0
- package/src/operations/contracts/index.ts +6 -0
- package/src/operations/contracts/observabilityContracts.ts +40 -0
- package/src/operations/contracts/observabilityPorts.ts +30 -0
- package/src/operations/contracts/ports.ts +22 -0
- package/src/operations/errors.ts +6 -0
- package/src/operations/index.ts +9 -0
- package/src/operations/schemas.ts +122 -0
- package/src/operations/server/adminAlertsHandlers.ts +61 -0
- package/src/operations/server/adminPipelineHandlers.ts +73 -0
- package/src/operations/server/adminSettingsHandlers.ts +114 -0
- package/src/operations/server/controlPlaneHandler.ts +56 -0
- package/src/operations/server/controlPlaneStore.ts +150 -0
- package/src/operations/server/controlPlaneWatchdog.ts +28 -0
- package/src/operations/server/index.ts +12 -0
- package/src/operations/server/migrations.ts +7 -0
- package/src/operations/server/platformWatchdogDecision.ts +93 -0
- package/src/operations/server/platformWatchdogDeliveryPolicy.ts +92 -0
- package/src/operations/server/platformWatchdogQueue.ts +96 -0
- package/src/operations/server/platformWatchdogService.ts +209 -0
- package/src/operations/validation.ts +3 -0
- package/src/portability/filesystemObjects.ts +138 -0
- package/src/portability/index.ts +12 -0
- package/src/portability/objects.ts +160 -0
- package/src/portability/postgresRows.ts +169 -0
- package/src/scheduler/advisoryLock.ts +20 -5
- 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.
|
|
14
|
-
limitations](CHANGELOG.md#
|
|
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.
|
|
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
|
-
/**
|
|
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 =
|
|
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
|
|
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";
|