@openlup/platform 0.0.0-stage → 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.
- package/CHANGELOG.md +19 -0
- package/LICENSE +204 -0
- package/README.md +125 -2
- package/dist/bff/dispatch.d.ts +27 -0
- package/dist/bff/dispatch.js +47 -0
- package/dist/bff/envelope.d.ts +46 -0
- package/dist/bff/envelope.js +80 -0
- package/dist/bff/health.d.ts +24 -0
- package/dist/bff/health.js +19 -0
- package/dist/bff/index.d.ts +6 -0
- package/dist/bff/index.js +6 -0
- package/dist/bff/lazyRoute.d.ts +4 -0
- package/dist/bff/lazyRoute.js +13 -0
- package/dist/bff/requestContext.d.ts +17 -0
- package/dist/bff/requestContext.js +26 -0
- package/dist/bff/routerRewrite.d.ts +2 -0
- package/dist/bff/routerRewrite.js +31 -0
- package/dist/blob/binding.d.ts +16 -0
- package/dist/blob/binding.js +37 -0
- package/dist/blob/filesystem.d.ts +8 -0
- package/dist/blob/filesystem.js +71 -0
- package/dist/blob/index.d.ts +6 -0
- package/dist/blob/index.js +6 -0
- package/dist/blob/s3.d.ts +32 -0
- package/dist/blob/s3.js +56 -0
- package/dist/blob/s3Client.d.ts +25 -0
- package/dist/blob/s3Client.js +76 -0
- package/dist/blob/supabase.d.ts +40 -0
- package/dist/blob/supabase.js +59 -0
- package/dist/blob/vercel.d.ts +63 -0
- package/dist/blob/vercel.js +69 -0
- package/dist/composition/composeBundle.d.ts +47 -0
- package/dist/composition/composeBundle.js +45 -0
- package/dist/composition/httpBinding.d.ts +8 -0
- package/dist/composition/httpBinding.js +18 -0
- package/dist/composition/index.d.ts +3 -0
- package/dist/composition/index.js +3 -0
- package/dist/composition/nodeHost.d.ts +103 -0
- package/dist/composition/nodeHost.js +145 -0
- package/dist/data/binding.d.ts +34 -0
- package/dist/data/binding.js +92 -0
- package/dist/data/index.d.ts +5 -0
- package/dist/data/index.js +5 -0
- package/dist/data/postgresGateway.d.ts +42 -0
- package/dist/data/postgresGateway.js +135 -0
- package/dist/data/queryBuilder.d.ts +54 -0
- package/dist/data/queryBuilder.js +240 -0
- package/dist/data/queryBuilderSql.d.ts +76 -0
- package/dist/data/queryBuilderSql.js +145 -0
- package/dist/data/supabaseClientFactory.d.ts +34 -0
- package/dist/data/supabaseClientFactory.js +56 -0
- package/dist/data/supabaseGateway.d.ts +22 -0
- package/dist/data/supabaseGateway.js +49 -0
- package/dist/http/deadline.d.ts +20 -0
- package/dist/http/deadline.js +93 -0
- package/dist/http/index.d.ts +6 -0
- package/dist/http/index.js +5 -0
- package/dist/http/requestAdapter.d.ts +38 -0
- package/dist/http/requestAdapter.js +203 -0
- package/dist/http/runtime.d.ts +83 -0
- package/dist/http/runtime.js +199 -0
- package/dist/http/security.d.ts +44 -0
- package/dist/http/security.js +114 -0
- package/dist/http/staticAssets.d.ts +3 -0
- package/dist/http/staticAssets.js +42 -0
- package/dist/http/types.d.ts +23 -0
- package/dist/http/types.js +6 -0
- package/dist/job-ledger/index.d.ts +5 -0
- package/dist/job-ledger/index.js +3 -0
- package/dist/job-ledger/ports.d.ts +10 -0
- package/dist/job-ledger/ports.js +7 -0
- package/dist/job-ledger/postgres.d.ts +10 -0
- package/dist/job-ledger/postgres.js +101 -0
- package/dist/job-ledger/sql.d.ts +7 -0
- package/dist/job-ledger/sql.js +10 -0
- package/dist/job-ledger/supabase.d.ts +77 -0
- package/dist/job-ledger/supabase.js +202 -0
- package/dist/kernel/index.d.ts +1 -0
- package/dist/kernel/index.js +1 -0
- package/dist/kernel/platformKernel.d.ts +34 -0
- package/dist/kernel/platformKernel.js +115 -0
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.js +1 -0
- package/dist/migrations/postgresRunner.d.ts +50 -0
- package/dist/migrations/postgresRunner.js +176 -0
- package/dist/scheduler/advisoryLock.d.ts +44 -0
- package/dist/scheduler/advisoryLock.js +114 -0
- package/dist/scheduler/cronGuard.d.ts +10 -0
- package/dist/scheduler/cronGuard.js +19 -0
- package/dist/scheduler/cronSecret.d.ts +5 -0
- package/dist/scheduler/cronSecret.js +23 -0
- package/dist/scheduler/handlers.d.ts +9 -0
- package/dist/scheduler/handlers.js +10 -0
- package/dist/scheduler/httpEntry.d.ts +16 -0
- package/dist/scheduler/httpEntry.js +31 -0
- package/dist/scheduler/index.d.ts +9 -0
- package/dist/scheduler/index.js +9 -0
- package/dist/scheduler/jobRegistry.d.ts +21 -0
- package/dist/scheduler/jobRegistry.js +39 -0
- package/dist/scheduler/jobs.d.ts +48 -0
- package/dist/scheduler/jobs.js +92 -0
- package/dist/scheduler/nodeScheduler.d.ts +42 -0
- package/dist/scheduler/nodeScheduler.js +73 -0
- package/dist/scheduler/vercelScheduler.d.ts +29 -0
- package/dist/scheduler/vercelScheduler.js +51 -0
- package/package.json +108 -4
- package/release-gates.json +121 -0
- package/sql/20260809150000_job_run_ledger.sql +301 -0
- package/sql/20260809150001_job_run_ledger_v3.sql +70 -0
- package/src/bff/dispatch.ts +81 -0
- package/src/bff/envelope.ts +142 -0
- package/src/bff/health.ts +37 -0
- package/src/bff/index.ts +30 -0
- package/src/bff/lazyRoute.ts +28 -0
- package/src/bff/requestContext.ts +48 -0
- package/src/bff/routerRewrite.ts +38 -0
- package/src/blob/binding.ts +55 -0
- package/src/blob/filesystem.ts +86 -0
- package/src/blob/index.ts +17 -0
- package/src/blob/s3.ts +87 -0
- package/src/blob/s3Client.ts +91 -0
- package/src/blob/supabase.ts +88 -0
- package/src/blob/vercel.ts +127 -0
- package/src/composition/composeBundle.ts +109 -0
- package/src/composition/httpBinding.ts +30 -0
- package/src/composition/index.ts +17 -0
- package/src/composition/nodeHost.ts +289 -0
- package/src/data/binding.ts +157 -0
- package/src/data/index.ts +33 -0
- package/src/data/postgresGateway.ts +186 -0
- package/src/data/queryBuilder.ts +300 -0
- package/src/data/queryBuilderSql.ts +183 -0
- package/src/data/supabaseClientFactory.ts +92 -0
- package/src/data/supabaseGateway.ts +76 -0
- package/src/http/deadline.ts +117 -0
- package/src/http/index.ts +36 -0
- package/src/http/requestAdapter.ts +215 -0
- package/src/http/runtime.ts +319 -0
- package/src/http/security.ts +155 -0
- package/src/http/staticAssets.ts +42 -0
- package/src/http/types.ts +31 -0
- package/src/job-ledger/index.ts +25 -0
- package/src/job-ledger/ports.ts +27 -0
- package/src/job-ledger/postgres.ts +121 -0
- package/src/job-ledger/sql.ts +12 -0
- package/src/job-ledger/supabase.ts +294 -0
- package/src/kernel/index.ts +17 -0
- package/src/kernel/platformKernel.ts +153 -0
- package/src/migrations/index.ts +10 -0
- package/src/migrations/postgresRunner.ts +255 -0
- package/src/scheduler/advisoryLock.ts +145 -0
- package/src/scheduler/cronGuard.ts +30 -0
- package/src/scheduler/cronSecret.ts +26 -0
- package/src/scheduler/handlers.ts +24 -0
- package/src/scheduler/httpEntry.ts +49 -0
- package/src/scheduler/index.ts +47 -0
- package/src/scheduler/jobRegistry.ts +57 -0
- package/src/scheduler/jobs.ts +119 -0
- package/src/scheduler/nodeScheduler.ts +123 -0
- package/src/scheduler/vercelScheduler.ts +71 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { PlatformHttpRequest, SchedulerPort } from "@openlup/core/platform-runtime";
|
|
2
|
+
import type { ScheduledJobDeclaration } from "./jobRegistry.js";
|
|
3
|
+
type CronEnv = Record<string, string | undefined> & {
|
|
4
|
+
CRON_SECRET?: string;
|
|
5
|
+
};
|
|
6
|
+
export { bearerToken } from "./cronSecret.js";
|
|
7
|
+
/**
|
|
8
|
+
* Verify a cron invocation is authentic. Fail-CLOSED:
|
|
9
|
+
* - CRON_SECRET unset -> false (never authorize when no secret configured)
|
|
10
|
+
* - presented Bearer token != CRON_SECRET -> false
|
|
11
|
+
* - presented Bearer token == CRON_SECRET -> true
|
|
12
|
+
* This is the single canonical check that replaces the per-handler inline comparisons.
|
|
13
|
+
*/
|
|
14
|
+
export declare function verifyCronAuth(req: PlatformHttpRequest, env: CronEnv): boolean;
|
|
15
|
+
export interface VercelSchedulerAdapter extends SchedulerPort {
|
|
16
|
+
/** The jobs this scheduler knows about, derived from the platform-runtime single source. */
|
|
17
|
+
readonly jobs: readonly ScheduledJobDeclaration[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Build the Vercel cron scheduler adapter.
|
|
21
|
+
*
|
|
22
|
+
* `register()` validates the job is part of the declared registry (so callers can't silently
|
|
23
|
+
* register a job Vercel will never fire) but does NOT start a timer — Vercel's cron does that.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createVercelScheduler(options: {
|
|
26
|
+
env: CronEnv;
|
|
27
|
+
/** The deployment's declared jobs (see `loadJobRegistry`). */
|
|
28
|
+
jobs: readonly ScheduledJobDeclaration[];
|
|
29
|
+
}): VercelSchedulerAdapter;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Vercel cron SchedulerPort adapter.
|
|
2
|
+
//
|
|
3
|
+
// On Vercel, scheduling is declarative: an adopter-owned `crons` manifest generated from the
|
|
4
|
+
// platform-runtime configuration (the same single source as `jobs`) tells Vercel to GET
|
|
5
|
+
// each `/api/cron/*` endpoint on its schedule. So `register()` is a no-op verification step here —
|
|
6
|
+
// the platform owns the timer. What this adapter OWNS is invocation authenticity: every cron
|
|
7
|
+
// endpoint must verify `Authorization: Bearer ${CRON_SECRET}`, fail-closed when the secret is unset.
|
|
8
|
+
//
|
|
9
|
+
// `verifyCronAuth(req, env)` is the reusable form of a bearer check against CRON_SECRET; cron
|
|
10
|
+
// handlers and the Node host call it instead of re-implementing the comparison.
|
|
11
|
+
import { bearerToken, cronSecretMatches } from "./cronSecret.js";
|
|
12
|
+
export { bearerToken } from "./cronSecret.js";
|
|
13
|
+
/**
|
|
14
|
+
* Verify a cron invocation is authentic. Fail-CLOSED:
|
|
15
|
+
* - CRON_SECRET unset -> false (never authorize when no secret configured)
|
|
16
|
+
* - presented Bearer token != CRON_SECRET -> false
|
|
17
|
+
* - presented Bearer token == CRON_SECRET -> true
|
|
18
|
+
* This is the single canonical check that replaces the per-handler inline comparisons.
|
|
19
|
+
*/
|
|
20
|
+
export function verifyCronAuth(req, env) {
|
|
21
|
+
const expected = env.CRON_SECRET;
|
|
22
|
+
if (!expected) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
return cronSecretMatches(bearerToken(req), expected);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Build the Vercel cron scheduler adapter.
|
|
29
|
+
*
|
|
30
|
+
* `register()` validates the job is part of the declared registry (so callers can't silently
|
|
31
|
+
* register a job Vercel will never fire) but does NOT start a timer — Vercel's cron does that.
|
|
32
|
+
*/
|
|
33
|
+
export function createVercelScheduler(options) {
|
|
34
|
+
const env = options.env;
|
|
35
|
+
const jobs = options.jobs;
|
|
36
|
+
const knownIds = new Set(jobs.map((job) => job.jobId));
|
|
37
|
+
return {
|
|
38
|
+
jobs,
|
|
39
|
+
register(input) {
|
|
40
|
+
// Vercel owns the timer through the adopter manifest. We only guard that the job is declared,
|
|
41
|
+
// surfacing drift between code and the single-source registry at registration time.
|
|
42
|
+
if (!knownIds.has(input.jobId)) {
|
|
43
|
+
throw new Error(`vercel-scheduler: job "${input.jobId}" is not declared in config/platform-runtime.json crons`);
|
|
44
|
+
}
|
|
45
|
+
// No-op timer: the handler at the cron path is invoked by Vercel directly.
|
|
46
|
+
},
|
|
47
|
+
verifyInvocation(req) {
|
|
48
|
+
return verifyCronAuth(req, env);
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,110 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openlup/platform",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.15.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Runnable server substrate: Node HTTP runtime, BFF dispatch, scheduler, storage, data gateways and bundle composition; development preview.",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": "24.x"
|
|
10
|
+
},
|
|
11
|
+
"packageManager": "npm@11.19.0",
|
|
12
|
+
"sideEffects": false,
|
|
13
|
+
"files": [
|
|
14
|
+
"dist/**",
|
|
15
|
+
"src/**",
|
|
16
|
+
"sql/**",
|
|
17
|
+
"README.md",
|
|
18
|
+
"CHANGELOG.md",
|
|
19
|
+
"LICENSE",
|
|
20
|
+
"release-gates.json"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json",
|
|
24
|
+
"test": "vitest run test",
|
|
25
|
+
"ci": "npm run build && npm run test && npm run api:check",
|
|
26
|
+
"api:check": "node --experimental-strip-types ./scripts/api-contract.ts",
|
|
27
|
+
"test:consumer": "node --experimental-strip-types ./scripts/consumer-proof.ts",
|
|
28
|
+
"ci:required": "npm run test:consumer",
|
|
29
|
+
"prepack": "npm run build",
|
|
30
|
+
"prepublishOnly": "node -e \"throw new Error('PLATFORM_DIRECTORY_PUBLISH_REFUSED')\"",
|
|
31
|
+
"api:update": "npm run build && node --experimental-strip-types ./scripts/api-contract.ts --update"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@openlup/core": "0.15.0",
|
|
35
|
+
"pg": "^8.22.0",
|
|
36
|
+
"@aws-sdk/client-s3": "^3.1144.0",
|
|
37
|
+
"@types/pg": "^8.20.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependenciesMeta": {
|
|
40
|
+
"pg": {
|
|
41
|
+
"optional": true
|
|
42
|
+
},
|
|
43
|
+
"@aws-sdk/client-s3": {
|
|
44
|
+
"optional": true
|
|
45
|
+
},
|
|
46
|
+
"@types/pg": {
|
|
47
|
+
"optional": true
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"zod": "^4.4.3"
|
|
52
|
+
},
|
|
53
|
+
"exports": {
|
|
54
|
+
"./http": {
|
|
55
|
+
"types": "./dist/http/index.d.ts",
|
|
56
|
+
"import": "./dist/http/index.js",
|
|
57
|
+
"default": "./dist/http/index.js"
|
|
58
|
+
},
|
|
59
|
+
"./bff": {
|
|
60
|
+
"types": "./dist/bff/index.d.ts",
|
|
61
|
+
"import": "./dist/bff/index.js",
|
|
62
|
+
"default": "./dist/bff/index.js"
|
|
63
|
+
},
|
|
64
|
+
"./scheduler": {
|
|
65
|
+
"types": "./dist/scheduler/index.d.ts",
|
|
66
|
+
"import": "./dist/scheduler/index.js",
|
|
67
|
+
"default": "./dist/scheduler/index.js"
|
|
68
|
+
},
|
|
69
|
+
"./blob": {
|
|
70
|
+
"types": "./dist/blob/index.d.ts",
|
|
71
|
+
"import": "./dist/blob/index.js",
|
|
72
|
+
"default": "./dist/blob/index.js"
|
|
73
|
+
},
|
|
74
|
+
"./data": {
|
|
75
|
+
"types": "./dist/data/index.d.ts",
|
|
76
|
+
"import": "./dist/data/index.js",
|
|
77
|
+
"default": "./dist/data/index.js"
|
|
78
|
+
},
|
|
79
|
+
"./migrations": {
|
|
80
|
+
"types": "./dist/migrations/index.d.ts",
|
|
81
|
+
"import": "./dist/migrations/index.js",
|
|
82
|
+
"default": "./dist/migrations/index.js"
|
|
83
|
+
},
|
|
84
|
+
"./job-ledger": {
|
|
85
|
+
"types": "./dist/job-ledger/index.d.ts",
|
|
86
|
+
"import": "./dist/job-ledger/index.js",
|
|
87
|
+
"default": "./dist/job-ledger/index.js"
|
|
88
|
+
},
|
|
89
|
+
"./kernel": {
|
|
90
|
+
"types": "./dist/kernel/index.d.ts",
|
|
91
|
+
"import": "./dist/kernel/index.js",
|
|
92
|
+
"default": "./dist/kernel/index.js"
|
|
93
|
+
},
|
|
94
|
+
"./composition": {
|
|
95
|
+
"types": "./dist/composition/index.d.ts",
|
|
96
|
+
"import": "./dist/composition/index.js",
|
|
97
|
+
"default": "./dist/composition/index.js"
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"repository": {
|
|
101
|
+
"type": "git",
|
|
102
|
+
"url": "git+https://github.com/openlup/openlup.git",
|
|
103
|
+
"directory": "packages/platform"
|
|
104
|
+
},
|
|
105
|
+
"publishConfig": {
|
|
106
|
+
"access": "public",
|
|
107
|
+
"provenance": true,
|
|
108
|
+
"tag": "preview"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 5,
|
|
3
|
+
"kind": "rail",
|
|
4
|
+
"compatibilityPolicies": {
|
|
5
|
+
"internal-candidate": "Development-preview API; no stable compatibility claim."
|
|
6
|
+
},
|
|
7
|
+
"packageSurface": {
|
|
8
|
+
"./http": {
|
|
9
|
+
"role": "rail",
|
|
10
|
+
"maturity": "experimental",
|
|
11
|
+
"compatibilityPolicy": "internal-candidate",
|
|
12
|
+
"packageSmokeEvidence": [
|
|
13
|
+
"test/packageBoundary.test.ts",
|
|
14
|
+
"test/http/runtime.test.ts"
|
|
15
|
+
],
|
|
16
|
+
"entrypoint": "dist/http/index.d.ts",
|
|
17
|
+
"snapshot": "api/http.api.md"
|
|
18
|
+
},
|
|
19
|
+
"./bff": {
|
|
20
|
+
"role": "rail",
|
|
21
|
+
"maturity": "experimental",
|
|
22
|
+
"compatibilityPolicy": "internal-candidate",
|
|
23
|
+
"packageSmokeEvidence": [
|
|
24
|
+
"test/packageBoundary.test.ts",
|
|
25
|
+
"test/bff/dispatch.test.ts"
|
|
26
|
+
],
|
|
27
|
+
"entrypoint": "dist/bff/index.d.ts",
|
|
28
|
+
"snapshot": "api/bff.api.md"
|
|
29
|
+
},
|
|
30
|
+
"./scheduler": {
|
|
31
|
+
"role": "rail",
|
|
32
|
+
"maturity": "experimental",
|
|
33
|
+
"compatibilityPolicy": "internal-candidate",
|
|
34
|
+
"packageSmokeEvidence": [
|
|
35
|
+
"test/packageBoundary.test.ts",
|
|
36
|
+
"test/scheduler/jobs.test.ts"
|
|
37
|
+
],
|
|
38
|
+
"entrypoint": "dist/scheduler/index.d.ts",
|
|
39
|
+
"snapshot": "api/scheduler.api.md"
|
|
40
|
+
},
|
|
41
|
+
"./blob": {
|
|
42
|
+
"role": "rail",
|
|
43
|
+
"maturity": "experimental",
|
|
44
|
+
"compatibilityPolicy": "internal-candidate",
|
|
45
|
+
"packageSmokeEvidence": [
|
|
46
|
+
"test/packageBoundary.test.ts",
|
|
47
|
+
"test/blob/binding.test.ts"
|
|
48
|
+
],
|
|
49
|
+
"entrypoint": "dist/blob/index.d.ts",
|
|
50
|
+
"snapshot": "api/blob.api.md"
|
|
51
|
+
},
|
|
52
|
+
"./data": {
|
|
53
|
+
"role": "rail",
|
|
54
|
+
"maturity": "experimental",
|
|
55
|
+
"compatibilityPolicy": "internal-candidate",
|
|
56
|
+
"packageSmokeEvidence": [
|
|
57
|
+
"test/packageBoundary.test.ts",
|
|
58
|
+
"test/data/binding.test.ts"
|
|
59
|
+
],
|
|
60
|
+
"entrypoint": "dist/data/index.d.ts",
|
|
61
|
+
"snapshot": "api/data.api.md"
|
|
62
|
+
},
|
|
63
|
+
"./migrations": {
|
|
64
|
+
"role": "rail",
|
|
65
|
+
"maturity": "experimental",
|
|
66
|
+
"compatibilityPolicy": "internal-candidate",
|
|
67
|
+
"packageSmokeEvidence": [
|
|
68
|
+
"test/packageBoundary.test.ts",
|
|
69
|
+
"test/migrations/postgresRunner.test.ts"
|
|
70
|
+
],
|
|
71
|
+
"entrypoint": "dist/migrations/index.d.ts",
|
|
72
|
+
"snapshot": "api/migrations.api.md"
|
|
73
|
+
},
|
|
74
|
+
"./job-ledger": {
|
|
75
|
+
"role": "rail",
|
|
76
|
+
"maturity": "experimental",
|
|
77
|
+
"compatibilityPolicy": "internal-candidate",
|
|
78
|
+
"packageSmokeEvidence": [
|
|
79
|
+
"test/packageBoundary.test.ts",
|
|
80
|
+
"test/job-ledger/postgres.test.ts"
|
|
81
|
+
],
|
|
82
|
+
"entrypoint": "dist/job-ledger/index.d.ts",
|
|
83
|
+
"snapshot": "api/job-ledger.api.md"
|
|
84
|
+
},
|
|
85
|
+
"./kernel": {
|
|
86
|
+
"role": "rail",
|
|
87
|
+
"maturity": "experimental",
|
|
88
|
+
"compatibilityPolicy": "internal-candidate",
|
|
89
|
+
"packageSmokeEvidence": [
|
|
90
|
+
"test/packageBoundary.test.ts",
|
|
91
|
+
"test/kernel/platformKernel.test.ts"
|
|
92
|
+
],
|
|
93
|
+
"entrypoint": "dist/kernel/index.d.ts",
|
|
94
|
+
"snapshot": "api/kernel.api.md"
|
|
95
|
+
},
|
|
96
|
+
"./composition": {
|
|
97
|
+
"role": "rail",
|
|
98
|
+
"maturity": "experimental",
|
|
99
|
+
"compatibilityPolicy": "internal-candidate",
|
|
100
|
+
"packageSmokeEvidence": [
|
|
101
|
+
"test/packageBoundary.test.ts",
|
|
102
|
+
"test/composition/composeBundle.test.ts"
|
|
103
|
+
],
|
|
104
|
+
"entrypoint": "dist/composition/index.d.ts",
|
|
105
|
+
"snapshot": "api/composition.api.md"
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
"pack": {
|
|
109
|
+
"code": {
|
|
110
|
+
"maxPackedBytes": 131072,
|
|
111
|
+
"maxFiles": 240
|
|
112
|
+
},
|
|
113
|
+
"docs": {
|
|
114
|
+
"maxPackedBytes": 16384,
|
|
115
|
+
"maxFiles": 12
|
|
116
|
+
},
|
|
117
|
+
"maxPackedBytes": 196608,
|
|
118
|
+
"maxUnpackedBytes": 1048576,
|
|
119
|
+
"maxFiles": 256
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
-- Public platform job-run ledger: the two relations a scheduled job needs to be
|
|
2
|
+
-- mutually exclusive across runners, and the two verbs its runtime actually calls.
|
|
3
|
+
--
|
|
4
|
+
-- WHAT THIS FORWARD SHIPS, AND WHY IT IS TWO VERBS AND NOT THREE. The second bundle
|
|
5
|
+
-- already has reads, a queue, a customer mutation, an operator mutation and a shipment
|
|
6
|
+
-- machine. What it has never had is the thing that lets a *scheduled* job run at all:
|
|
7
|
+
-- somewhere to say "this attempt is mine, nobody else start", and somewhere to say
|
|
8
|
+
-- "it is finished, and this is what it did". The consumer of this ledger asks for
|
|
9
|
+
-- exactly those two things -- a claim and a finish -- and for nothing else. There is
|
|
10
|
+
-- no heartbeat, no renew and no extend anywhere in the runtime that drives it, so
|
|
11
|
+
-- there is none here: a lease is issued once, and it is released by finishing or by
|
|
12
|
+
-- expiring. A failure is a finish with status 'failed', not a separate verb.
|
|
13
|
+
--
|
|
14
|
+
-- EXCLUSIVITY IS THE CONTROL ROW, NOT AN ADVISORY LOCK. The claim upserts the job's
|
|
15
|
+
-- control row and then takes `FOR UPDATE` on it. A second claimer arriving in the same
|
|
16
|
+
-- instant blocks on that row rather than racing past it, and once the first claimer
|
|
17
|
+
-- commits it reads a live lease and is refused. This is deliberate and it is the
|
|
18
|
+
-- property the harness proves with two concurrent connections. An advisory lock would
|
|
19
|
+
-- be a different machine: it is not carried by the row, so it would vanish with the
|
|
20
|
+
-- session and leave the ledger claiming a run that nobody holds. The lease, in
|
|
21
|
+
-- contrast, outlives the connection and is what makes a crashed runner recoverable --
|
|
22
|
+
-- after the lease elapses, and not before.
|
|
23
|
+
--
|
|
24
|
+
-- WHAT A FINISH IS ALLOWED TO DO. A finish always stamps its own run row, because a
|
|
25
|
+
-- runner that lost its lease still owes the ledger the truth about what it did. It
|
|
26
|
+
-- releases the *job* only when the control row still carries its own token. So a
|
|
27
|
+
-- zombie -- a runner whose lease expired and whose job was re-claimed by somebody else
|
|
28
|
+
-- -- writes its own epitaph and returns false, and cannot clear a lease it no longer
|
|
29
|
+
-- holds. That returned boolean is the fencing signal, and the harness asserts it.
|
|
30
|
+
--
|
|
31
|
+
-- WHERE THE SEMANTICS COME FROM, AND EVERY PLACE THIS KERNEL DEPARTS FROM THEM. The
|
|
32
|
+
-- behaviour above is derived from the managed chain's own claim/finish pair, read in
|
|
33
|
+
-- full rather than summarised: its two relations and its first begin/finish, the later
|
|
34
|
+
-- migration that added the second finish and the driver/lease/counter columns, the
|
|
35
|
+
-- migration that rewrote the claim into its current 199-line body, and the migration
|
|
36
|
+
-- that introduced a neutral trigger vocabulary. Five departures, each named because a
|
|
37
|
+
-- silent one is how a kernel and its origin quietly stop being the same machine:
|
|
38
|
+
--
|
|
39
|
+
-- 1. THE RUNNER VOCABULARY IS NEUTRAL: 'worker', 'scheduler', 'operator'. The chain's
|
|
40
|
+
-- set enumerates two named cron mechanisms, one of which is the scheduler of a
|
|
41
|
+
-- specific hosting vendor. Which scheduler a deployment happens to run is a fact
|
|
42
|
+
-- about that deployment, never a fact about a platform, and this kernel may not
|
|
43
|
+
-- carry a vendor's name in a CHECK constraint. The vocabulary chosen is not
|
|
44
|
+
-- invented here either: it is the one the chain's own newest control-plane
|
|
45
|
+
-- migration introduced, in its words, so that a deployment adapter is EVIDENCE
|
|
46
|
+
-- rather than an authorization decision. The concrete adapter label survives
|
|
47
|
+
-- losslessly in `p_metadata`, which is caller-supplied jsonb this kernel stores
|
|
48
|
+
-- and never interprets.
|
|
49
|
+
-- 2. THE FIRST CLAIMER DEFINES THE JOB'S ACTIVE RUNNER. Upstream the control row is
|
|
50
|
+
-- created with a fixed default runner, so on a fresh install the first tick of a
|
|
51
|
+
-- long-running worker is refused as the wrong runner until an operator edits a
|
|
52
|
+
-- row by hand. That is a migration artefact of a live cutover, not a platform
|
|
53
|
+
-- rule. Here the auto-created row adopts the runner that created it; changing
|
|
54
|
+
-- runners afterwards stays an explicit operator UPDATE, and the refusal below
|
|
55
|
+
-- still protects a job whose runner has been pinned.
|
|
56
|
+
-- 3. ONE LEASE COLUMN AND ONE COUNTER PAIR. Upstream carries `lease_until` and
|
|
57
|
+
-- `lease_expires_at` for one lease, and `checked`/`checked_count`,
|
|
58
|
+
-- `updated`/`updated_count` for one pair of counts, writing both of each on every
|
|
59
|
+
-- call. Those are compatibility shadows of an earlier signature. This kernel has
|
|
60
|
+
-- no earlier signature, so it ships one of each. For the same reason the run row
|
|
61
|
+
-- records its runner once, where upstream writes the same value into three
|
|
62
|
+
-- columns.
|
|
63
|
+
-- 4. NO `SECURITY DEFINER`, NO `GRANT`/`REVOKE`, NO ROW-LEVEL SECURITY, matching
|
|
64
|
+
-- every forward this catalogue already carries. This kernel creates zero roles.
|
|
65
|
+
-- Said out loud rather than assumed: ON THIS KERNEL THE FUNCTIONS BELOW ARE NOT A
|
|
66
|
+
-- SECURITY BOUNDARY -- the host application is what authenticates, and a caller
|
|
67
|
+
-- that can reach these functions can already reach the tables. `SET search_path`
|
|
68
|
+
-- is kept, because that one is a correctness property and not a role property.
|
|
69
|
+
-- 5. THE FINISH KEEPS ITS `_v2` SUFFIX. There is no v1 in this kernel and the suffix
|
|
70
|
+
-- describes the version history of a different codebase. It is kept anyway: it is
|
|
71
|
+
-- the name the shipped consumer calls, and giving a public kernel a second,
|
|
72
|
+
-- prettier name for one verb would be worse than carrying one inherited one.
|
|
73
|
+
--
|
|
74
|
+
-- WHAT THIS FORWARD DELIBERATELY DOES NOT SHIP, enumerated so nobody has to infer it:
|
|
75
|
+
-- * no runtime binding. Not one line of the application runtime is rewired onto this
|
|
76
|
+
-- ledger by this forward; the files that would do it belong to another line of work
|
|
77
|
+
-- in flight, and the falsifier for this kernel is its own parity harness driving
|
|
78
|
+
-- the shipped port's two calls;
|
|
79
|
+
-- * no job seed. A job NAME is a deployment fact, so this kernel names none. The
|
|
80
|
+
-- claim creates the control row it needs on first contact;
|
|
81
|
+
-- * no lease extension and no reaper. Neither exists upstream either. A lease that
|
|
82
|
+
-- elapses is simply claimable again, and a job that must not be re-entered before
|
|
83
|
+
-- its own work finishes must ask for a lease long enough to cover it;
|
|
84
|
+
-- * no attempt counter and no poison-pill policy. Upstream has none on this ledger,
|
|
85
|
+
-- and inventing one here would be a policy this kernel has no way to falsify;
|
|
86
|
+
-- * no scheduling. WHEN a job runs is the host's problem. This ledger only answers
|
|
87
|
+
-- whether an attempt that has already begun is allowed to proceed.
|
|
88
|
+
|
|
89
|
+
CREATE TABLE IF NOT EXISTS public.platform_job_controls (
|
|
90
|
+
job_name text PRIMARY KEY,
|
|
91
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
92
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
93
|
+
enabled boolean NOT NULL DEFAULT true,
|
|
94
|
+
active_driver text NOT NULL
|
|
95
|
+
CHECK (active_driver IN ('worker', 'scheduler', 'operator')),
|
|
96
|
+
lease_owner text,
|
|
97
|
+
lease_token uuid,
|
|
98
|
+
lease_until timestamptz,
|
|
99
|
+
last_started_at timestamptz,
|
|
100
|
+
last_finished_at timestamptz,
|
|
101
|
+
last_success_at timestamptz,
|
|
102
|
+
last_status text
|
|
103
|
+
CHECK (last_status IS NULL OR last_status IN ('running', 'success', 'failed', 'skipped')),
|
|
104
|
+
last_run_id uuid,
|
|
105
|
+
metadata jsonb NOT NULL DEFAULT '{}'::jsonb
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
CREATE TABLE IF NOT EXISTS public.platform_job_runs (
|
|
109
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
110
|
+
job_name text NOT NULL REFERENCES public.platform_job_controls(job_name),
|
|
111
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
112
|
+
started_at timestamptz NOT NULL DEFAULT now(),
|
|
113
|
+
finished_at timestamptz,
|
|
114
|
+
status text NOT NULL DEFAULT 'running'
|
|
115
|
+
CHECK (status IN ('running', 'success', 'failed', 'skipped')),
|
|
116
|
+
driver text NOT NULL
|
|
117
|
+
CHECK (driver IN ('worker', 'scheduler', 'operator')),
|
|
118
|
+
lease_until timestamptz,
|
|
119
|
+
checked integer CHECK (checked IS NULL OR checked >= 0),
|
|
120
|
+
updated integer CHECK (updated IS NULL OR updated >= 0),
|
|
121
|
+
error text,
|
|
122
|
+
support_code text,
|
|
123
|
+
metadata jsonb NOT NULL DEFAULT '{}'::jsonb
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
CREATE INDEX IF NOT EXISTS idx_platform_job_runs_job_started
|
|
127
|
+
ON public.platform_job_runs (job_name, started_at DESC);
|
|
128
|
+
|
|
129
|
+
CREATE INDEX IF NOT EXISTS idx_platform_job_runs_status_started
|
|
130
|
+
ON public.platform_job_runs (status, started_at DESC);
|
|
131
|
+
|
|
132
|
+
COMMENT ON TABLE public.platform_job_controls IS
|
|
133
|
+
'One row per scheduled job: whether it may run, which runner class owns it, and the live lease.';
|
|
134
|
+
COMMENT ON TABLE public.platform_job_runs IS
|
|
135
|
+
'Append-only attempt ledger. A refused attempt is recorded as a skipped run, so a job that never runs is visible.';
|
|
136
|
+
COMMENT ON COLUMN public.platform_job_runs.driver IS
|
|
137
|
+
'Neutral runner class of the attempt. The concrete adapter, if any, lives in metadata as evidence.';
|
|
138
|
+
|
|
139
|
+
CREATE OR REPLACE FUNCTION public.platform_claim_job_run(
|
|
140
|
+
p_job_name text,
|
|
141
|
+
p_driver text,
|
|
142
|
+
p_lease_seconds integer DEFAULT 900,
|
|
143
|
+
p_metadata jsonb DEFAULT '{}'::jsonb
|
|
144
|
+
)
|
|
145
|
+
RETURNS TABLE (
|
|
146
|
+
acquired boolean,
|
|
147
|
+
run_id uuid,
|
|
148
|
+
reason text,
|
|
149
|
+
lease_until timestamptz
|
|
150
|
+
)
|
|
151
|
+
LANGUAGE plpgsql
|
|
152
|
+
SET search_path = public, pg_catalog
|
|
153
|
+
AS $$
|
|
154
|
+
DECLARE
|
|
155
|
+
v_control public.platform_job_controls%ROWTYPE;
|
|
156
|
+
v_job_name text := btrim(COALESCE(p_job_name, ''));
|
|
157
|
+
v_driver text := COALESCE(NULLIF(btrim(p_driver), ''), 'worker');
|
|
158
|
+
v_lease_seconds integer;
|
|
159
|
+
v_run_id uuid;
|
|
160
|
+
v_now timestamptz := now();
|
|
161
|
+
v_metadata jsonb := COALESCE(p_metadata, '{}'::jsonb);
|
|
162
|
+
v_lease_until timestamptz;
|
|
163
|
+
v_refusal text;
|
|
164
|
+
BEGIN
|
|
165
|
+
IF v_job_name = '' THEN
|
|
166
|
+
RAISE EXCEPTION 'platform_job_name_required' USING ERRCODE = '22023';
|
|
167
|
+
END IF;
|
|
168
|
+
|
|
169
|
+
IF v_driver NOT IN ('worker', 'scheduler', 'operator') THEN
|
|
170
|
+
RAISE EXCEPTION 'platform_job_invalid_driver' USING ERRCODE = '22023';
|
|
171
|
+
END IF;
|
|
172
|
+
|
|
173
|
+
v_lease_seconds := LEAST(GREATEST(COALESCE(p_lease_seconds, 900), 60), 3600);
|
|
174
|
+
|
|
175
|
+
INSERT INTO public.platform_job_controls (job_name, active_driver)
|
|
176
|
+
VALUES (v_job_name, v_driver)
|
|
177
|
+
ON CONFLICT (job_name) DO NOTHING;
|
|
178
|
+
|
|
179
|
+
-- The row lock IS the exclusivity. Everything below runs with the control row held.
|
|
180
|
+
SELECT * INTO v_control
|
|
181
|
+
FROM public.platform_job_controls
|
|
182
|
+
WHERE public.platform_job_controls.job_name = v_job_name
|
|
183
|
+
FOR UPDATE;
|
|
184
|
+
|
|
185
|
+
v_lease_until := v_control.lease_until;
|
|
186
|
+
v_refusal := CASE
|
|
187
|
+
WHEN NOT v_control.enabled THEN 'job_disabled'
|
|
188
|
+
WHEN v_control.active_driver <> v_driver THEN 'inactive_driver'
|
|
189
|
+
WHEN v_lease_until IS NOT NULL AND v_lease_until > v_now THEN 'lease_active'
|
|
190
|
+
ELSE NULL
|
|
191
|
+
END;
|
|
192
|
+
|
|
193
|
+
IF v_refusal IS NOT NULL THEN
|
|
194
|
+
-- A refusal is still an attempt, and an attempt nobody records is an outage
|
|
195
|
+
-- nobody can see. The refused run carries WHY, and the control row records that
|
|
196
|
+
-- somebody tried; the live lease is left exactly as it was found.
|
|
197
|
+
INSERT INTO public.platform_job_runs (
|
|
198
|
+
job_name, status, driver, started_at, finished_at, lease_until, error, metadata
|
|
199
|
+
)
|
|
200
|
+
VALUES (
|
|
201
|
+
v_job_name, 'skipped', v_driver, v_now, v_now,
|
|
202
|
+
CASE WHEN v_refusal = 'lease_active' THEN v_lease_until ELSE NULL END,
|
|
203
|
+
v_refusal,
|
|
204
|
+
v_metadata || jsonb_build_object('activeDriver', v_control.active_driver, 'leaseOwner', v_control.lease_owner)
|
|
205
|
+
)
|
|
206
|
+
RETURNING id INTO v_run_id;
|
|
207
|
+
|
|
208
|
+
UPDATE public.platform_job_controls
|
|
209
|
+
SET updated_at = v_now,
|
|
210
|
+
last_started_at = v_now,
|
|
211
|
+
last_finished_at = v_now,
|
|
212
|
+
last_status = 'skipped',
|
|
213
|
+
last_run_id = v_run_id
|
|
214
|
+
WHERE public.platform_job_controls.job_name = v_job_name;
|
|
215
|
+
|
|
216
|
+
RETURN QUERY SELECT false, v_run_id, v_refusal,
|
|
217
|
+
CASE WHEN v_refusal = 'lease_active' THEN v_lease_until ELSE NULL::timestamptz END;
|
|
218
|
+
RETURN;
|
|
219
|
+
END IF;
|
|
220
|
+
|
|
221
|
+
v_run_id := gen_random_uuid();
|
|
222
|
+
v_lease_until := v_now + make_interval(secs => v_lease_seconds);
|
|
223
|
+
|
|
224
|
+
UPDATE public.platform_job_controls
|
|
225
|
+
SET updated_at = v_now,
|
|
226
|
+
lease_owner = v_driver,
|
|
227
|
+
lease_token = v_run_id,
|
|
228
|
+
lease_until = v_lease_until,
|
|
229
|
+
last_started_at = v_now,
|
|
230
|
+
last_status = 'running',
|
|
231
|
+
last_run_id = v_run_id
|
|
232
|
+
WHERE public.platform_job_controls.job_name = v_job_name;
|
|
233
|
+
|
|
234
|
+
INSERT INTO public.platform_job_runs (
|
|
235
|
+
id, job_name, status, driver, started_at, lease_until, metadata
|
|
236
|
+
)
|
|
237
|
+
VALUES (
|
|
238
|
+
v_run_id, v_job_name, 'running', v_driver, v_now, v_lease_until, v_metadata
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
RETURN QUERY SELECT true, v_run_id, 'acquired', v_lease_until;
|
|
242
|
+
END;
|
|
243
|
+
$$;
|
|
244
|
+
|
|
245
|
+
CREATE OR REPLACE FUNCTION public.platform_finish_job_run_v2(
|
|
246
|
+
p_job_name text,
|
|
247
|
+
p_run_id uuid,
|
|
248
|
+
p_status text,
|
|
249
|
+
p_checked integer DEFAULT NULL,
|
|
250
|
+
p_updated integer DEFAULT NULL,
|
|
251
|
+
p_error text DEFAULT NULL,
|
|
252
|
+
p_support_code text DEFAULT NULL,
|
|
253
|
+
p_metadata jsonb DEFAULT '{}'::jsonb
|
|
254
|
+
)
|
|
255
|
+
RETURNS boolean
|
|
256
|
+
LANGUAGE plpgsql
|
|
257
|
+
SET search_path = public, pg_catalog
|
|
258
|
+
AS $$
|
|
259
|
+
DECLARE
|
|
260
|
+
v_now timestamptz := now();
|
|
261
|
+
v_job_name text := btrim(COALESCE(p_job_name, ''));
|
|
262
|
+
v_metadata jsonb := COALESCE(p_metadata, '{}'::jsonb);
|
|
263
|
+
v_released boolean;
|
|
264
|
+
BEGIN
|
|
265
|
+
IF p_status IS NULL OR p_status NOT IN ('success', 'failed') THEN
|
|
266
|
+
RAISE EXCEPTION 'platform_job_invalid_finish_status' USING ERRCODE = '22023';
|
|
267
|
+
END IF;
|
|
268
|
+
|
|
269
|
+
-- The run row is always stamped, even by a runner whose lease is gone: a runner that
|
|
270
|
+
-- lost its lease still owes the ledger the truth about what it did. Deliberately NOT
|
|
271
|
+
-- guarded on `finished_at IS NULL` -- upstream re-stamps too, and a kernel that
|
|
272
|
+
-- silently ignored a second finish would answer a different last-writer than the
|
|
273
|
+
-- bundle it is compared against.
|
|
274
|
+
UPDATE public.platform_job_runs
|
|
275
|
+
SET status = p_status,
|
|
276
|
+
finished_at = v_now,
|
|
277
|
+
checked = p_checked,
|
|
278
|
+
updated = p_updated,
|
|
279
|
+
error = p_error,
|
|
280
|
+
support_code = p_support_code,
|
|
281
|
+
metadata = public.platform_job_runs.metadata || v_metadata
|
|
282
|
+
WHERE id = p_run_id
|
|
283
|
+
AND public.platform_job_runs.job_name = v_job_name;
|
|
284
|
+
|
|
285
|
+
-- The lease is released ONLY by the token that holds it. This predicate is the fence.
|
|
286
|
+
UPDATE public.platform_job_controls
|
|
287
|
+
SET updated_at = v_now,
|
|
288
|
+
lease_owner = NULL,
|
|
289
|
+
lease_token = NULL,
|
|
290
|
+
lease_until = NULL,
|
|
291
|
+
last_finished_at = v_now,
|
|
292
|
+
last_success_at = CASE WHEN p_status = 'success' THEN v_now ELSE last_success_at END,
|
|
293
|
+
last_status = p_status,
|
|
294
|
+
last_run_id = p_run_id
|
|
295
|
+
WHERE public.platform_job_controls.job_name = v_job_name
|
|
296
|
+
AND lease_token = p_run_id
|
|
297
|
+
RETURNING true INTO v_released;
|
|
298
|
+
|
|
299
|
+
RETURN COALESCE(v_released, false);
|
|
300
|
+
END;
|
|
301
|
+
$$;
|