@openlup/platform 0.0.0 → 0.15.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 (160) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/LICENSE +204 -0
  3. package/README.md +124 -1
  4. package/dist/bff/dispatch.d.ts +27 -0
  5. package/dist/bff/dispatch.js +47 -0
  6. package/dist/bff/envelope.d.ts +46 -0
  7. package/dist/bff/envelope.js +80 -0
  8. package/dist/bff/health.d.ts +24 -0
  9. package/dist/bff/health.js +19 -0
  10. package/dist/bff/index.d.ts +6 -0
  11. package/dist/bff/index.js +6 -0
  12. package/dist/bff/lazyRoute.d.ts +4 -0
  13. package/dist/bff/lazyRoute.js +13 -0
  14. package/dist/bff/requestContext.d.ts +17 -0
  15. package/dist/bff/requestContext.js +26 -0
  16. package/dist/bff/routerRewrite.d.ts +2 -0
  17. package/dist/bff/routerRewrite.js +31 -0
  18. package/dist/blob/binding.d.ts +16 -0
  19. package/dist/blob/binding.js +37 -0
  20. package/dist/blob/filesystem.d.ts +8 -0
  21. package/dist/blob/filesystem.js +71 -0
  22. package/dist/blob/index.d.ts +6 -0
  23. package/dist/blob/index.js +6 -0
  24. package/dist/blob/s3.d.ts +32 -0
  25. package/dist/blob/s3.js +56 -0
  26. package/dist/blob/s3Client.d.ts +25 -0
  27. package/dist/blob/s3Client.js +76 -0
  28. package/dist/blob/supabase.d.ts +40 -0
  29. package/dist/blob/supabase.js +59 -0
  30. package/dist/blob/vercel.d.ts +63 -0
  31. package/dist/blob/vercel.js +69 -0
  32. package/dist/composition/composeBundle.d.ts +47 -0
  33. package/dist/composition/composeBundle.js +45 -0
  34. package/dist/composition/httpBinding.d.ts +8 -0
  35. package/dist/composition/httpBinding.js +18 -0
  36. package/dist/composition/index.d.ts +3 -0
  37. package/dist/composition/index.js +3 -0
  38. package/dist/composition/nodeHost.d.ts +103 -0
  39. package/dist/composition/nodeHost.js +145 -0
  40. package/dist/data/binding.d.ts +34 -0
  41. package/dist/data/binding.js +92 -0
  42. package/dist/data/index.d.ts +5 -0
  43. package/dist/data/index.js +5 -0
  44. package/dist/data/postgresGateway.d.ts +42 -0
  45. package/dist/data/postgresGateway.js +135 -0
  46. package/dist/data/queryBuilder.d.ts +54 -0
  47. package/dist/data/queryBuilder.js +240 -0
  48. package/dist/data/queryBuilderSql.d.ts +76 -0
  49. package/dist/data/queryBuilderSql.js +145 -0
  50. package/dist/data/supabaseClientFactory.d.ts +34 -0
  51. package/dist/data/supabaseClientFactory.js +56 -0
  52. package/dist/data/supabaseGateway.d.ts +22 -0
  53. package/dist/data/supabaseGateway.js +49 -0
  54. package/dist/http/deadline.d.ts +20 -0
  55. package/dist/http/deadline.js +93 -0
  56. package/dist/http/index.d.ts +6 -0
  57. package/dist/http/index.js +5 -0
  58. package/dist/http/requestAdapter.d.ts +38 -0
  59. package/dist/http/requestAdapter.js +203 -0
  60. package/dist/http/runtime.d.ts +83 -0
  61. package/dist/http/runtime.js +199 -0
  62. package/dist/http/security.d.ts +44 -0
  63. package/dist/http/security.js +114 -0
  64. package/dist/http/staticAssets.d.ts +3 -0
  65. package/dist/http/staticAssets.js +42 -0
  66. package/dist/http/types.d.ts +23 -0
  67. package/dist/http/types.js +6 -0
  68. package/dist/job-ledger/index.d.ts +5 -0
  69. package/dist/job-ledger/index.js +3 -0
  70. package/dist/job-ledger/ports.d.ts +10 -0
  71. package/dist/job-ledger/ports.js +7 -0
  72. package/dist/job-ledger/postgres.d.ts +10 -0
  73. package/dist/job-ledger/postgres.js +101 -0
  74. package/dist/job-ledger/sql.d.ts +7 -0
  75. package/dist/job-ledger/sql.js +10 -0
  76. package/dist/job-ledger/supabase.d.ts +77 -0
  77. package/dist/job-ledger/supabase.js +202 -0
  78. package/dist/kernel/index.d.ts +1 -0
  79. package/dist/kernel/index.js +1 -0
  80. package/dist/kernel/platformKernel.d.ts +34 -0
  81. package/dist/kernel/platformKernel.js +115 -0
  82. package/dist/migrations/index.d.ts +1 -0
  83. package/dist/migrations/index.js +1 -0
  84. package/dist/migrations/postgresRunner.d.ts +50 -0
  85. package/dist/migrations/postgresRunner.js +176 -0
  86. package/dist/scheduler/advisoryLock.d.ts +44 -0
  87. package/dist/scheduler/advisoryLock.js +114 -0
  88. package/dist/scheduler/cronGuard.d.ts +10 -0
  89. package/dist/scheduler/cronGuard.js +19 -0
  90. package/dist/scheduler/cronSecret.d.ts +5 -0
  91. package/dist/scheduler/cronSecret.js +23 -0
  92. package/dist/scheduler/handlers.d.ts +9 -0
  93. package/dist/scheduler/handlers.js +10 -0
  94. package/dist/scheduler/httpEntry.d.ts +16 -0
  95. package/dist/scheduler/httpEntry.js +31 -0
  96. package/dist/scheduler/index.d.ts +9 -0
  97. package/dist/scheduler/index.js +9 -0
  98. package/dist/scheduler/jobRegistry.d.ts +21 -0
  99. package/dist/scheduler/jobRegistry.js +39 -0
  100. package/dist/scheduler/jobs.d.ts +48 -0
  101. package/dist/scheduler/jobs.js +92 -0
  102. package/dist/scheduler/nodeScheduler.d.ts +42 -0
  103. package/dist/scheduler/nodeScheduler.js +73 -0
  104. package/dist/scheduler/vercelScheduler.d.ts +29 -0
  105. package/dist/scheduler/vercelScheduler.js +51 -0
  106. package/package.json +106 -3
  107. package/release-gates.json +121 -0
  108. package/sql/20260809150000_job_run_ledger.sql +301 -0
  109. package/sql/20260809150001_job_run_ledger_v3.sql +70 -0
  110. package/src/bff/dispatch.ts +81 -0
  111. package/src/bff/envelope.ts +142 -0
  112. package/src/bff/health.ts +37 -0
  113. package/src/bff/index.ts +30 -0
  114. package/src/bff/lazyRoute.ts +28 -0
  115. package/src/bff/requestContext.ts +48 -0
  116. package/src/bff/routerRewrite.ts +38 -0
  117. package/src/blob/binding.ts +55 -0
  118. package/src/blob/filesystem.ts +86 -0
  119. package/src/blob/index.ts +17 -0
  120. package/src/blob/s3.ts +87 -0
  121. package/src/blob/s3Client.ts +91 -0
  122. package/src/blob/supabase.ts +88 -0
  123. package/src/blob/vercel.ts +127 -0
  124. package/src/composition/composeBundle.ts +109 -0
  125. package/src/composition/httpBinding.ts +30 -0
  126. package/src/composition/index.ts +17 -0
  127. package/src/composition/nodeHost.ts +289 -0
  128. package/src/data/binding.ts +157 -0
  129. package/src/data/index.ts +33 -0
  130. package/src/data/postgresGateway.ts +186 -0
  131. package/src/data/queryBuilder.ts +300 -0
  132. package/src/data/queryBuilderSql.ts +183 -0
  133. package/src/data/supabaseClientFactory.ts +92 -0
  134. package/src/data/supabaseGateway.ts +76 -0
  135. package/src/http/deadline.ts +117 -0
  136. package/src/http/index.ts +36 -0
  137. package/src/http/requestAdapter.ts +215 -0
  138. package/src/http/runtime.ts +319 -0
  139. package/src/http/security.ts +155 -0
  140. package/src/http/staticAssets.ts +42 -0
  141. package/src/http/types.ts +31 -0
  142. package/src/job-ledger/index.ts +25 -0
  143. package/src/job-ledger/ports.ts +27 -0
  144. package/src/job-ledger/postgres.ts +121 -0
  145. package/src/job-ledger/sql.ts +12 -0
  146. package/src/job-ledger/supabase.ts +294 -0
  147. package/src/kernel/index.ts +17 -0
  148. package/src/kernel/platformKernel.ts +153 -0
  149. package/src/migrations/index.ts +10 -0
  150. package/src/migrations/postgresRunner.ts +255 -0
  151. package/src/scheduler/advisoryLock.ts +145 -0
  152. package/src/scheduler/cronGuard.ts +30 -0
  153. package/src/scheduler/cronSecret.ts +26 -0
  154. package/src/scheduler/handlers.ts +24 -0
  155. package/src/scheduler/httpEntry.ts +49 -0
  156. package/src/scheduler/index.ts +47 -0
  157. package/src/scheduler/jobRegistry.ts +57 -0
  158. package/src/scheduler/jobs.ts +119 -0
  159. package/src/scheduler/nodeScheduler.ts +123 -0
  160. package/src/scheduler/vercelScheduler.ts +71 -0
@@ -0,0 +1,77 @@
1
+ import type { PlatformJobFinishStatus, PlatformJobFinishSummary, PlatformJobInvocation, PlatformJobRunLedgerPort } from "./ports.js";
2
+ export type PlatformJobDriver = "manual_admin" | "pg_cron" | "vercel_cron" | "worker";
3
+ type RpcResult<T> = {
4
+ data: T | null;
5
+ error: {
6
+ message?: string;
7
+ } | null;
8
+ };
9
+ type PlatformJobMetadataQuery = {
10
+ select(columns: "metadata"): PlatformJobMetadataQuery;
11
+ eq(column: "job_name", value: string): PlatformJobMetadataQuery;
12
+ in(column: "status", values: readonly ["success", "failed"]): PlatformJobMetadataQuery;
13
+ not(column: "finished_at", operator: "is", value: null): PlatformJobMetadataQuery;
14
+ order(column: "started_at", options: {
15
+ ascending: false;
16
+ }): PlatformJobMetadataQuery;
17
+ limit(count: 1): PlatformJobMetadataQuery;
18
+ maybeSingle(): PromiseLike<{
19
+ data: {
20
+ metadata?: unknown;
21
+ } | null;
22
+ error: {
23
+ message?: string;
24
+ } | null;
25
+ }>;
26
+ };
27
+ type PlatformJobControlQuery = {
28
+ select(columns: "last_status,last_success_at"): PlatformJobControlQuery;
29
+ eq(column: "job_name", value: string): PlatformJobControlQuery;
30
+ maybeSingle(): PromiseLike<{
31
+ data: {
32
+ last_status?: string | null;
33
+ last_success_at?: string | null;
34
+ } | null;
35
+ error: {
36
+ message?: string;
37
+ } | null;
38
+ }>;
39
+ };
40
+ export interface PlatformJobSupabaseClient {
41
+ rpc<T = unknown>(name: string, args: Record<string, unknown>): PromiseLike<RpcResult<T>>;
42
+ from(table: "platform_job_runs"): PlatformJobMetadataQuery;
43
+ from(table: "platform_job_controls"): PlatformJobControlQuery;
44
+ }
45
+ /** Minimal compatibility client for the legacy free-function surface. */
46
+ export type PlatformJobClient = Pick<PlatformJobSupabaseClient, "rpc">;
47
+ export declare function platformJobInvocationForDriver(driver: PlatformJobDriver): PlatformJobInvocation;
48
+ export interface ManagedJobRunOptions {
49
+ /** Release and deployment identity merged into every claim and finish row's metadata. */
50
+ releaseCorrelation: () => Record<string, string | null>;
51
+ /** Failure reason recorded when a failed run's summary carries none. */
52
+ defaultFailureReason: string;
53
+ }
54
+ export interface SupabasePlatformJobRunLedgerOptions extends ManagedJobRunOptions {
55
+ /** Message of the error thrown when the backstop state cannot be read. */
56
+ backstopReadError: string;
57
+ /** Message of the error thrown when the latest terminal run cannot be read. */
58
+ checkpointReadError: string;
59
+ }
60
+ /** The managed v2/v3 compatibility calls, bound to the deployment's release correlation. */
61
+ export declare function createManagedJobRunCalls(options: ManagedJobRunOptions): {
62
+ claimJobRun: (client: PlatformJobClient, jobName: string, driver: PlatformJobDriver, leaseSeconds?: number) => Promise<{
63
+ acquired: boolean;
64
+ runId: string | null;
65
+ reason: string;
66
+ }>;
67
+ claimJobRunV3: (client: PlatformJobClient, jobName: string, invocation: PlatformJobInvocation, leaseSeconds?: number) => Promise<{
68
+ acquired: boolean;
69
+ runId: string | null;
70
+ reason: string;
71
+ }>;
72
+ finishJobRun: (client: PlatformJobClient, jobName: string, runId: string, status: PlatformJobFinishStatus, result: PlatformJobFinishSummary, extraMetadata?: Record<string, unknown>) => Promise<boolean>;
73
+ finishJobRunV3: (client: PlatformJobClient, jobName: string, runId: string, status: PlatformJobFinishStatus, result: PlatformJobFinishSummary, extraMetadata?: Record<string, unknown>) => Promise<void>;
74
+ };
75
+ /** Managed adapter for the semantic job-run port. */
76
+ export declare function createSupabasePlatformJobRunLedger(client: PlatformJobSupabaseClient, options: SupabasePlatformJobRunLedgerOptions): PlatformJobRunLedgerPort;
77
+ export {};
@@ -0,0 +1,202 @@
1
+ // Managed (Supabase) adapter for the job-run ledger, and the managed v2/v3 compatibility calls.
2
+ //
3
+ // It speaks the managed job-control RPCs (`platform_claim_job_run`, `platform_finish_job_run_v2`
4
+ // and their v3 forms) through an injected client. Every claim and finish row carries the
5
+ // deployment's release correlation; the default failure reason and the two read-failure messages
6
+ // are the deployment's own values.
7
+ function triggerSourceForDriver(driver) {
8
+ switch (driver) {
9
+ case "vercel_cron": return "vercel_cron";
10
+ case "pg_cron": return "pg_cron_scheduler";
11
+ case "worker": return "worker";
12
+ case "manual_admin": return "manual_admin";
13
+ }
14
+ }
15
+ export function platformJobInvocationForDriver(driver) {
16
+ switch (driver) {
17
+ case "manual_admin": return { triggerKind: "operator", invocationSource: driver };
18
+ case "pg_cron":
19
+ case "vercel_cron": return { triggerKind: "scheduler", invocationSource: driver };
20
+ case "worker": return { triggerKind: "worker", invocationSource: driver };
21
+ }
22
+ }
23
+ function managedDriverForInvocation(invocation) {
24
+ if (!["worker", "scheduler", "operator"].includes(invocation.triggerKind)) {
25
+ throw new Error("platform_job_invalid_trigger_kind");
26
+ }
27
+ const source = invocation.invocationSource.trim();
28
+ const driver = source;
29
+ const expected = platformJobInvocationForDriver(driver);
30
+ if (!expected || expected.triggerKind !== invocation.triggerKind) {
31
+ throw new Error("platform_job_invalid_invocation");
32
+ }
33
+ return driver;
34
+ }
35
+ function claimResult(data) {
36
+ const row = Array.isArray(data) ? data[0] : data;
37
+ const record = plainRecord(row);
38
+ return {
39
+ acquired: record?.acquired === true,
40
+ runId: typeof record?.run_id === "string" ? record.run_id : null,
41
+ reason: typeof record?.reason === "string" ? record.reason : "unknown",
42
+ };
43
+ }
44
+ /** The managed v2/v3 compatibility calls, bound to the deployment's release correlation. */
45
+ export function createManagedJobRunCalls(options) {
46
+ /** Compatibility surface for existing managed v2 consumers. */
47
+ async function claimJobRun(client, jobName, driver, leaseSeconds = 900) {
48
+ const { data, error } = await client.rpc("platform_claim_job_run", {
49
+ p_job_name: jobName,
50
+ p_driver: driver,
51
+ p_lease_seconds: leaseSeconds,
52
+ p_metadata: {
53
+ driver,
54
+ triggerSource: driver === "vercel_cron" ? "vercel_cron_manual" : driver,
55
+ ...options.releaseCorrelation(),
56
+ },
57
+ });
58
+ if (error)
59
+ throw new Error(error.message ?? "platform_claim_job_run failed");
60
+ return claimResult(data);
61
+ }
62
+ /** Compatibility surface for existing managed v3 consumers. */
63
+ async function claimJobRunV3(client, jobName, invocation, leaseSeconds = 900) {
64
+ const invocationSource = invocation.invocationSource.trim();
65
+ if (!invocationSource)
66
+ throw new Error("platform_job_invocation_source_required");
67
+ const { data, error } = await client.rpc("platform_claim_job_run_v3", {
68
+ p_job_name: jobName,
69
+ p_trigger_kind: invocation.triggerKind,
70
+ p_invocation_source: invocationSource,
71
+ p_lease_seconds: leaseSeconds,
72
+ p_metadata: {
73
+ triggerKind: invocation.triggerKind,
74
+ invocationSource,
75
+ ...options.releaseCorrelation(),
76
+ },
77
+ });
78
+ if (error)
79
+ throw new Error(error.message ?? "platform_claim_job_run_v3 failed");
80
+ return claimResult(data);
81
+ }
82
+ /** Compatibility surface for existing managed v2 consumers. */
83
+ async function finishJobRun(client, jobName, runId, status, result, extraMetadata) {
84
+ const { data, error } = await client.rpc("platform_finish_job_run_v2", {
85
+ p_job_name: jobName,
86
+ p_run_id: runId,
87
+ p_status: status,
88
+ p_checked: result.checked,
89
+ p_updated: result.updated,
90
+ p_error: status === "failed" ? result.reason ?? options.defaultFailureReason : null,
91
+ p_support_code: null,
92
+ p_metadata: {
93
+ driver: "manual_admin",
94
+ failures: result.failures,
95
+ skipped: result.skipped,
96
+ reason: result.reason ?? null,
97
+ ...options.releaseCorrelation(),
98
+ ...(extraMetadata ?? {}),
99
+ },
100
+ });
101
+ if (error)
102
+ throw new Error(error.message ?? "platform_finish_job_run_v2 failed");
103
+ return data === true;
104
+ }
105
+ /** Compatibility surface for existing managed v3 consumers. */
106
+ async function finishJobRunV3(client, jobName, runId, status, result, extraMetadata) {
107
+ const { error } = await client.rpc("platform_finish_job_run_v3", {
108
+ p_job_name: jobName,
109
+ p_run_id: runId,
110
+ p_status: status,
111
+ p_checked: result.checked,
112
+ p_updated: result.updated,
113
+ p_error: status === "failed" ? result.reason ?? options.defaultFailureReason : null,
114
+ p_support_code: null,
115
+ p_metadata: {
116
+ failures: result.failures,
117
+ skipped: result.skipped,
118
+ reason: result.reason ?? null,
119
+ ...options.releaseCorrelation(),
120
+ ...(extraMetadata ?? {}),
121
+ },
122
+ });
123
+ if (error)
124
+ throw new Error(error.message ?? "platform_finish_job_run_v3 failed");
125
+ }
126
+ return { claimJobRun, claimJobRunV3, finishJobRun, finishJobRunV3 };
127
+ }
128
+ /** Managed adapter for the semantic job-run port. */
129
+ export function createSupabasePlatformJobRunLedger(client, options) {
130
+ return {
131
+ async claimJobRun(jobName, invocation, leaseSeconds = 900) {
132
+ const driver = managedDriverForInvocation(invocation);
133
+ const { data, error } = await client.rpc("platform_claim_job_run", {
134
+ p_job_name: jobName,
135
+ p_driver: driver,
136
+ p_lease_seconds: leaseSeconds,
137
+ p_metadata: {
138
+ driver,
139
+ triggerSource: triggerSourceForDriver(driver),
140
+ ...options.releaseCorrelation(),
141
+ },
142
+ });
143
+ if (error)
144
+ throw new Error(error.message ?? "platform_claim_job_run failed");
145
+ return claimResult(data);
146
+ },
147
+ async finishJobRun(jobName, runId, invocation, status, result, extraMetadata) {
148
+ const driver = managedDriverForInvocation(invocation);
149
+ const { data, error } = await client.rpc("platform_finish_job_run_v2", {
150
+ p_job_name: jobName,
151
+ p_run_id: runId,
152
+ p_status: status,
153
+ p_checked: result.checked,
154
+ p_updated: result.updated,
155
+ p_error: status === "failed" ? result.reason ?? options.defaultFailureReason : null,
156
+ p_support_code: null,
157
+ p_metadata: {
158
+ failures: result.failures,
159
+ skipped: result.skipped,
160
+ reason: result.reason ?? null,
161
+ ...options.releaseCorrelation(),
162
+ ...(extraMetadata ?? {}),
163
+ driver,
164
+ },
165
+ });
166
+ if (error)
167
+ throw new Error(error.message ?? "platform_finish_job_run_v2 failed");
168
+ return data === true;
169
+ },
170
+ async readJobBackstopState(jobName) {
171
+ const { data, error } = await client
172
+ .from("platform_job_controls")
173
+ .select("last_status,last_success_at")
174
+ .eq("job_name", jobName)
175
+ .maybeSingle();
176
+ if (error)
177
+ throw new Error(options.backstopReadError);
178
+ return data ? {
179
+ lastStatus: data.last_status ?? null,
180
+ lastSuccessAt: data.last_success_at ?? null,
181
+ } : null;
182
+ },
183
+ async readLatestTerminalRunMetadata(jobName) {
184
+ const { data, error } = await client
185
+ .from("platform_job_runs")
186
+ .select("metadata")
187
+ .eq("job_name", jobName)
188
+ .in("status", ["success", "failed"])
189
+ .order("started_at", { ascending: false })
190
+ .limit(1)
191
+ .maybeSingle();
192
+ if (error)
193
+ throw new Error(options.checkpointReadError);
194
+ return plainRecord(data?.metadata);
195
+ },
196
+ };
197
+ }
198
+ function plainRecord(value) {
199
+ if (typeof value !== "object" || value === null || Array.isArray(value))
200
+ return null;
201
+ return { ...value };
202
+ }
@@ -0,0 +1 @@
1
+ export { BLOB_CAPABILITIES, DEFAULT_PLATFORM_BUNDLE, ENV_MISSING_CODE, PLATFORM_BUNDLE_DESCRIPTORS, PLATFORM_BUNDLE_IDS, getBundleDescriptor, isPlatformBundleId, platformEnvSchema, resolveBlobCapability, resolveBundleId, type BlobCapability, type PlatformBundleDescriptor, type PlatformBundleId, type PlatformBundleReadiness, type PlatformEnv, } from "./platformKernel.js";
@@ -0,0 +1 @@
1
+ export { BLOB_CAPABILITIES, DEFAULT_PLATFORM_BUNDLE, ENV_MISSING_CODE, PLATFORM_BUNDLE_DESCRIPTORS, PLATFORM_BUNDLE_IDS, getBundleDescriptor, isPlatformBundleId, platformEnvSchema, resolveBlobCapability, resolveBundleId, } from "./platformKernel.js";
@@ -0,0 +1,34 @@
1
+ import { z } from "zod";
2
+ import { type PlatformBundleDescriptor as CorePlatformBundleDescriptor, type PlatformBundleReadiness, type PlatformEnvInput } from "@openlup/core/platform-runtime";
3
+ /** The bundles a deployment can select with `PLATFORM_BUNDLE`. */
4
+ export declare const PLATFORM_BUNDLE_IDS: readonly ["vercel-supabase", "node-supabase", "node-postgres"];
5
+ export type PlatformBundleId = (typeof PLATFORM_BUNDLE_IDS)[number];
6
+ /** The bundle selected when `PLATFORM_BUNDLE` is absent or unknown. */
7
+ export declare const DEFAULT_PLATFORM_BUNDLE: PlatformBundleId;
8
+ export declare const isPlatformBundleId: import("@openlup/core/platform-runtime").PlatformBundleIdGuard<"vercel-supabase" | "node-supabase" | "node-postgres">;
9
+ export type PlatformBundleDescriptor = CorePlatformBundleDescriptor & {
10
+ id: PlatformBundleId;
11
+ };
12
+ export type { PlatformBundleReadiness };
13
+ /** The readiness code a bundle reports when a selected capability lacks its configuration. */
14
+ export declare const ENV_MISSING_CODE = "OPENLUP_E_ENV_MISSING";
15
+ /** The blob capabilities a deployment can select with `PLATFORM_BLOB`. */
16
+ export declare const BLOB_CAPABILITIES: readonly ["filesystem", "s3", "supabase", "vercel-blob"];
17
+ export type BlobCapability = (typeof BLOB_CAPABILITIES)[number];
18
+ /**
19
+ * The blob capability in effect: a valid `PLATFORM_BLOB` selection, else the bundle's own. An
20
+ * invalid selection keeps the bundle's own here and is refused by readiness.
21
+ */
22
+ export declare function resolveBlobCapability(descriptor: Pick<PlatformBundleDescriptor, "capabilities">, env: PlatformEnvInput): string;
23
+ export declare const PLATFORM_BUNDLE_DESCRIPTORS: readonly PlatformBundleDescriptor[];
24
+ export declare function getBundleDescriptor(id: PlatformBundleId): PlatformBundleDescriptor;
25
+ export declare function resolveBundleId(env: PlatformEnvInput): PlatformBundleId;
26
+ export declare const platformEnvSchema: z.ZodObject<{
27
+ APP_BASE_URL: z.ZodOptional<z.ZodString>;
28
+ PLATFORM_BUNDLE: z.ZodOptional<z.ZodEnum<{
29
+ "vercel-supabase": "vercel-supabase";
30
+ "node-supabase": "node-supabase";
31
+ "node-postgres": "node-postgres";
32
+ }>>;
33
+ }, z.core.$strip>;
34
+ export type PlatformEnv = z.infer<typeof platformEnvSchema>;
@@ -0,0 +1,115 @@
1
+ import { z } from "zod";
2
+ import { createPlatformBundleRegistry, platformEnvSchema as corePlatformEnvSchema, createPlatformBundleIdGuard, } from "@openlup/core/platform-runtime";
3
+ /** The bundles a deployment can select with `PLATFORM_BUNDLE`. */
4
+ export const PLATFORM_BUNDLE_IDS = [
5
+ "vercel-supabase",
6
+ "node-supabase",
7
+ "node-postgres",
8
+ ];
9
+ /** The bundle selected when `PLATFORM_BUNDLE` is absent or unknown. */
10
+ export const DEFAULT_PLATFORM_BUNDLE = "vercel-supabase";
11
+ export const isPlatformBundleId = createPlatformBundleIdGuard(PLATFORM_BUNDLE_IDS);
12
+ /** The readiness code a bundle reports when a selected capability lacks its configuration. */
13
+ export const ENV_MISSING_CODE = "OPENLUP_E_ENV_MISSING";
14
+ /** The blob capabilities a deployment can select with `PLATFORM_BLOB`. */
15
+ export const BLOB_CAPABILITIES = ["filesystem", "s3", "supabase", "vercel-blob"];
16
+ /**
17
+ * The blob capability in effect: a valid `PLATFORM_BLOB` selection, else the bundle's own. An
18
+ * invalid selection keeps the bundle's own here and is refused by readiness.
19
+ */
20
+ export function resolveBlobCapability(descriptor, env) {
21
+ const selected = env.PLATFORM_BLOB?.trim();
22
+ return selected && BLOB_CAPABILITIES.includes(selected) ? selected : descriptor.capabilities.blob;
23
+ }
24
+ /** The settings each blob capability needs; an alternative name may stand in for the first. */
25
+ const BLOB_SETTINGS = {
26
+ "vercel-blob": [["BLOB_READ_WRITE_TOKEN"]],
27
+ supabase: [["SUPABASE_URL", "VITE_SUPABASE_URL"], ["SUPABASE_SERVICE_ROLE_KEY"], ["BLOB_SUPABASE_BUCKET"]],
28
+ filesystem: [["BLOB_FS_ROOT"], ["BLOB_FS_BASE_URL"]],
29
+ s3: [["BLOB_S3_BUCKET"], ["BLOB_S3_BASE_URL"]],
30
+ };
31
+ function present(env, name) {
32
+ return typeof env[name] === "string" && env[name].trim() !== "";
33
+ }
34
+ /**
35
+ * Per-capability readiness, in this order: an unknown PLATFORM_BUNDLE refuses (resolution itself
36
+ * keeps defaulting); a self-host bundle needs APP_BASE_URL; then every selected capability's
37
+ * missing settings are reported together under OPENLUP_E_ENV_MISSING: a scheduler needs
38
+ * CRON_SECRET, Node scheduling needs DATABASE_URL for its cluster lock (never a silent no-op lock),
39
+ * and the selected blob capability needs its settings. An unknown PLATFORM_BLOB refuses too.
40
+ */
41
+ function capabilityReadiness(descriptor, selfHost) {
42
+ return (env) => {
43
+ const raw = env.PLATFORM_BUNDLE;
44
+ if (raw !== undefined && raw.trim() !== "" && !isPlatformBundleId(raw)) {
45
+ return { ok: false, error: `unknown PLATFORM_BUNDLE "${raw}"; expected one of ${PLATFORM_BUNDLE_IDS.join(", ")}` };
46
+ }
47
+ const selectedBlob = env.PLATFORM_BLOB;
48
+ if (selectedBlob !== undefined && selectedBlob.trim() !== "" && !BLOB_CAPABILITIES.includes(selectedBlob.trim())) {
49
+ return { ok: false, error: `unknown PLATFORM_BLOB "${selectedBlob}"; expected one of ${BLOB_CAPABILITIES.join(", ")}` };
50
+ }
51
+ if (selfHost && !present(env, "APP_BASE_URL")) {
52
+ return { ok: false, error: "APP_BASE_URL is required for self-host bundles" };
53
+ }
54
+ const { scheduler } = descriptor.capabilities;
55
+ const blob = resolveBlobCapability(descriptor, env);
56
+ const missing = [];
57
+ if (scheduler === "vercel-cron" || scheduler === "node-cron") {
58
+ if (!present(env, "CRON_SECRET"))
59
+ missing.push("CRON_SECRET (scheduler)");
60
+ }
61
+ if (scheduler === "node-cron" && !present(env, "DATABASE_URL"))
62
+ missing.push("DATABASE_URL (scheduler lock)");
63
+ for (const names of BLOB_SETTINGS[blob] ?? []) {
64
+ if (!names.some((name) => present(env, name)))
65
+ missing.push(`${names[0]} (blob: ${blob})`);
66
+ }
67
+ return missing.length === 0 ? { ok: true } : { ok: false, error: `${ENV_MISSING_CODE}: ${missing.join(", ")}` };
68
+ };
69
+ }
70
+ function descriptor(id, capabilities, selfHost) {
71
+ return { id, capabilities, readReadiness: capabilityReadiness({ capabilities }, selfHost) };
72
+ }
73
+ const DESCRIPTORS = [
74
+ descriptor("vercel-supabase", {
75
+ http: "vercel",
76
+ scheduler: "vercel-cron",
77
+ blob: "vercel-blob",
78
+ data: "supabase",
79
+ migrations: "supabase",
80
+ analytics: "vercel",
81
+ transactional: "application",
82
+ }, false),
83
+ descriptor("node-supabase", {
84
+ http: "node",
85
+ scheduler: "node-cron",
86
+ blob: "supabase",
87
+ data: "supabase",
88
+ migrations: "supabase",
89
+ analytics: "gtm",
90
+ transactional: "node",
91
+ }, true),
92
+ descriptor("node-postgres", {
93
+ http: "node",
94
+ scheduler: "node-cron",
95
+ blob: "filesystem",
96
+ data: "postgres",
97
+ migrations: "postgres",
98
+ analytics: "noop",
99
+ transactional: "node",
100
+ }, true),
101
+ ];
102
+ const registry = createPlatformBundleRegistry({
103
+ descriptors: DESCRIPTORS,
104
+ defaultBundleId: DEFAULT_PLATFORM_BUNDLE,
105
+ });
106
+ export const PLATFORM_BUNDLE_DESCRIPTORS = registry.descriptors;
107
+ export function getBundleDescriptor(id) {
108
+ return registry.getBundleDescriptor(id);
109
+ }
110
+ export function resolveBundleId(env) {
111
+ return registry.resolveBundleId(env);
112
+ }
113
+ export const platformEnvSchema = corePlatformEnvSchema.extend({
114
+ PLATFORM_BUNDLE: z.enum(PLATFORM_BUNDLE_IDS).optional(),
115
+ });
@@ -0,0 +1 @@
1
+ export { createPostgresMigrationRunner, resolvePostgresMigrationRunnerEnv, type PlatformMigrationFile, type PlatformMigrationManifest, type PlatformMigrationManifestEntry, type PlatformMigrationSource, type PostgresMigrationRunnerEnv, type PostgresMigrationRunnerOptions, } from "./postgresRunner.js";
@@ -0,0 +1 @@
1
+ export { createPostgresMigrationRunner, resolvePostgresMigrationRunnerEnv, } from "./postgresRunner.js";
@@ -0,0 +1,50 @@
1
+ import type { Pool, PoolClient } from "pg";
2
+ import type { MigrationRunnerPort } from "@openlup/core/platform-runtime";
3
+ export interface PostgresMigrationRunnerEnv {
4
+ connectionString: string;
5
+ }
6
+ /** One migration file of the deployment's platform catalogue. */
7
+ export type PlatformMigrationFile = {
8
+ file: string;
9
+ content: string;
10
+ };
11
+ export type PlatformMigrationManifestEntry = {
12
+ file: string;
13
+ sha256: string;
14
+ };
15
+ /** The validated manifest: a baseline, its ordered forwards and the expected object inventory. */
16
+ export type PlatformMigrationManifest = {
17
+ schemaVersion: 1;
18
+ baseline: PlatformMigrationManifestEntry;
19
+ forward: PlatformMigrationManifestEntry[];
20
+ objectInventorySha256: string;
21
+ };
22
+ /**
23
+ * The deployment's migration catalogue and its validator. `evaluate` returns every refusal for a
24
+ * manifest against the catalogue files and, when given, the live object-inventory payload.
25
+ */
26
+ export interface PlatformMigrationSource {
27
+ readMigrations(root: string): {
28
+ migrations: PlatformMigrationFile[];
29
+ errors: string[];
30
+ };
31
+ readManifest(root: string): unknown | null;
32
+ evaluate(input: {
33
+ manifest: unknown;
34
+ migrations: readonly PlatformMigrationFile[];
35
+ objectInventoryPayload?: string;
36
+ }): string[];
37
+ }
38
+ export interface PostgresMigrationRunnerOptions {
39
+ /** Name of the PostgreSQL advisory lock that serializes migration sessions. */
40
+ lockName: string;
41
+ /** The deployment's migration catalogue. */
42
+ source: PlatformMigrationSource;
43
+ /** Serializes the live object inventory that the manifest's `objectInventorySha256` pins. */
44
+ catalogProjectionPayload: (client: PoolClient) => Promise<string>;
45
+ /** Catalogue root; defaults to process.cwd(). */
46
+ root?: string;
47
+ poolFactory?: (env: PostgresMigrationRunnerEnv) => Pool | Promise<Pool>;
48
+ }
49
+ export declare function createPostgresMigrationRunner(env: PostgresMigrationRunnerEnv, options: PostgresMigrationRunnerOptions): MigrationRunnerPort;
50
+ export declare function resolvePostgresMigrationRunnerEnv(env: Record<string, string | undefined>): PostgresMigrationRunnerEnv;
@@ -0,0 +1,176 @@
1
+ // Public-platform MigrationRunnerPort for the experimental node-postgres bundle.
2
+ // The manifest and its exact SQL bytes are validated before a pool is created.
3
+ // Applied rows are an exact prefix of that catalog; path, digest or position
4
+ // drift is fatal. Only the caller's catalogue is applied; no other migration
5
+ // history is selected or reachable through a default.
6
+ const LEDGER = "public.platform_schema_migrations";
7
+ const ACQUIRE_MIGRATION_LOCK = "SELECT pg_advisory_lock(hashtextextended($1, 0))";
8
+ const RELEASE_MIGRATION_LOCK = "SELECT pg_advisory_unlock(hashtextextended($1, 0)) AS unlocked";
9
+ export function createPostgresMigrationRunner(env, options) {
10
+ const root = options.root ?? process.cwd();
11
+ let pool = null;
12
+ async function getPool() {
13
+ if (pool)
14
+ return pool;
15
+ const factory = options.poolFactory ?? defaultPoolFactory;
16
+ pool = await factory(env);
17
+ return pool;
18
+ }
19
+ return {
20
+ async status() {
21
+ assertConnectionString(env);
22
+ const plan = loadPlan(root, options.source);
23
+ return withMigrationSession(await getPool(), options.lockName, async (client) => {
24
+ const applied = await readAppliedPrefix(client, plan.units);
25
+ if (applied.length === plan.units.length)
26
+ await assertObjectInventory(client, plan, options);
27
+ return {
28
+ pending: plan.units.slice(applied.length).map((unit) => unit.catalogPath),
29
+ applied: applied.map((unit) => unit.catalogPath),
30
+ };
31
+ });
32
+ },
33
+ async apply(opts) {
34
+ assertConnectionString(env);
35
+ const plan = loadPlan(root, options.source);
36
+ return withMigrationSession(await getPool(), options.lockName, async (client) => {
37
+ const already = await readAppliedPrefix(client, plan.units);
38
+ const pending = plan.units.slice(already.length);
39
+ if (opts?.dryRun) {
40
+ if (pending.length === 0)
41
+ await assertObjectInventory(client, plan, options);
42
+ return { applied: pending.map((unit) => unit.catalogPath) };
43
+ }
44
+ const appliedNow = [];
45
+ for (const unit of pending) {
46
+ await client.query("BEGIN");
47
+ try {
48
+ await client.query(unit.sql);
49
+ await client.query(`INSERT INTO ${LEDGER} (catalog_path, sha256, position) VALUES ($1, $2, $3)`, [unit.catalogPath, unit.sha256, unit.position]);
50
+ await client.query("COMMIT");
51
+ appliedNow.push(unit.catalogPath);
52
+ }
53
+ catch (error) {
54
+ await client.query("ROLLBACK").catch(() => { });
55
+ throw new Error(`platform migration ${unit.catalogPath} failed: ${errorMessage(error)}`);
56
+ }
57
+ }
58
+ const complete = await readAppliedPrefix(client, plan.units);
59
+ if (complete.length !== plan.units.length)
60
+ throw new Error("platform migration ledger is incomplete after apply");
61
+ await assertObjectInventory(client, plan, options);
62
+ return { applied: appliedNow };
63
+ });
64
+ },
65
+ };
66
+ }
67
+ async function withMigrationSession(pool, lockName, operation) {
68
+ const client = await pool.connect();
69
+ let locked = false;
70
+ let failed = false;
71
+ let operationFailure;
72
+ let result;
73
+ try {
74
+ await client.query(ACQUIRE_MIGRATION_LOCK, [lockName]);
75
+ locked = true;
76
+ result = await operation(client);
77
+ }
78
+ catch (error) {
79
+ failed = true;
80
+ operationFailure = error;
81
+ }
82
+ let unlockFailure;
83
+ if (locked) {
84
+ try {
85
+ const unlocked = await client.query(RELEASE_MIGRATION_LOCK, [lockName]);
86
+ if (unlocked.rows[0]?.unlocked !== true)
87
+ throw new Error("PostgreSQL did not release the advisory lock");
88
+ }
89
+ catch (error) {
90
+ unlockFailure = error;
91
+ }
92
+ }
93
+ client.release(unlockFailure !== undefined || (failed && !locked));
94
+ if (failed) {
95
+ if (unlockFailure !== undefined) {
96
+ throw new Error(`platform migration operation failed: ${errorMessage(operationFailure)}; advisory lock release failed: ${errorMessage(unlockFailure)}`);
97
+ }
98
+ throw operationFailure;
99
+ }
100
+ if (unlockFailure !== undefined) {
101
+ throw new Error(`platform migration advisory lock release failed: ${errorMessage(unlockFailure)}`);
102
+ }
103
+ return result;
104
+ }
105
+ function assertConnectionString(env) {
106
+ if (env.connectionString.trim() === "") {
107
+ throw new Error("DATABASE_URL is required for node-postgres migrations");
108
+ }
109
+ }
110
+ function loadPlan(root, source) {
111
+ const catalog = source.readMigrations(root);
112
+ const rawManifest = source.readManifest(root);
113
+ const errors = [...catalog.errors];
114
+ if (rawManifest === null)
115
+ errors.push("platform migration manifest is missing");
116
+ else
117
+ errors.push(...source.evaluate({ manifest: rawManifest, migrations: catalog.migrations }));
118
+ if (errors.length > 0)
119
+ throw new Error(`platform migration catalog rejected: ${errors.join("; ")}`);
120
+ const manifest = rawManifest;
121
+ const byPath = new Map(catalog.migrations.map((migration) => [migration.file, migration.content]));
122
+ const entries = [manifest.baseline, ...manifest.forward];
123
+ const units = entries.map((entry, position) => Object.freeze({
124
+ catalogPath: entry.file,
125
+ sha256: entry.sha256,
126
+ position,
127
+ sql: byPath.get(entry.file),
128
+ }));
129
+ return Object.freeze({ manifest, migrations: catalog.migrations, units: Object.freeze(units) });
130
+ }
131
+ async function readAppliedPrefix(client, units) {
132
+ const exists = await client.query("SELECT to_regclass($1) IS NOT NULL AS ledger_exists", [LEDGER]);
133
+ if (exists.rows[0]?.ledger_exists !== true)
134
+ return [];
135
+ const result = await client.query(`SELECT catalog_path, sha256, position FROM ${LEDGER} ORDER BY position`);
136
+ if (result.rows.length > units.length)
137
+ throw new Error("platform migration ledger contains an extra row");
138
+ for (let index = 0; index < result.rows.length; index += 1) {
139
+ const row = result.rows[index];
140
+ const unit = units[index];
141
+ if (row.catalog_path !== unit.catalogPath || row.sha256 !== unit.sha256 || row.position !== unit.position) {
142
+ throw new Error(`platform migration ledger is not the exact manifest prefix at position ${index}`);
143
+ }
144
+ }
145
+ return units.slice(0, result.rows.length);
146
+ }
147
+ async function assertObjectInventory(client, plan, options) {
148
+ const payload = await options.catalogProjectionPayload(client);
149
+ const errors = options.source.evaluate({
150
+ manifest: plan.manifest,
151
+ migrations: plan.migrations,
152
+ objectInventoryPayload: payload,
153
+ });
154
+ if (errors.length > 0)
155
+ throw new Error(`platform migration inventory rejected: ${errors.join("; ")}`);
156
+ }
157
+ function errorMessage(error) {
158
+ return error instanceof Error ? error.message : String(error);
159
+ }
160
+ async function defaultPoolFactory(env) {
161
+ const pg = await import("pg").catch(peerMissing);
162
+ const PoolCtor = (pg.default?.Pool ?? pg.Pool);
163
+ return new PoolCtor({ connectionString: env.connectionString });
164
+ }
165
+ export function resolvePostgresMigrationRunnerEnv(env) {
166
+ return { connectionString: env.DATABASE_URL ?? "" };
167
+ }
168
+ /** Translate only absence of this optional peer; preserve broken SDKs and other failures. */
169
+ function peerMissing(cause) {
170
+ if (cause instanceof Error && "code" in cause
171
+ && (cause.code === "ERR_MODULE_NOT_FOUND" || cause.code === "MODULE_NOT_FOUND")
172
+ && /^Cannot find (?:package|module) ['"]pg['"]/.test(cause.message)) {
173
+ throw new Error("OPENLUP_E_PEER_MISSING: pg is required by the Postgres migration runner; install it alongside @openlup/platform", { cause });
174
+ }
175
+ throw cause;
176
+ }