@wtfalch/foundry 0.60.0 → 0.62.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/dist/apps/cloudflare.d.ts +173 -0
- package/dist/apps/cloudflare.js +17 -0
- package/dist/apps/deploy.d.ts +116 -0
- package/dist/apps/deploy.js +791 -0
- package/dist/apps/github.d.ts +47 -0
- package/dist/apps/github.js +201 -0
- package/dist/apps/index.d.ts +9 -0
- package/dist/apps/index.js +9 -0
- package/dist/apps/migrations.d.ts +110 -0
- package/dist/apps/migrations.js +458 -0
- package/dist/audit/connector.d.ts +43 -0
- package/dist/audit/connector.js +22 -0
- package/dist/audit/evidence.d.ts +49 -0
- package/dist/audit/evidence.js +56 -0
- package/dist/audit/index.d.ts +9 -0
- package/dist/audit/index.js +9 -0
- package/dist/audit/migrations.d.ts +5 -0
- package/dist/audit/migrations.js +5 -0
- package/dist/audit/registry.d.ts +49 -0
- package/dist/audit/registry.js +44 -0
- package/dist/audit/seal.d.ts +49 -0
- package/dist/audit/seal.js +131 -0
- package/dist/audit/sealer-role.d.ts +33 -0
- package/dist/audit/sealer-role.js +56 -0
- package/dist/audit/signer.d.ts +40 -0
- package/dist/audit/signer.js +78 -0
- package/dist/audit/tick.d.ts +36 -0
- package/dist/audit/tick.js +87 -0
- package/dist/catalogue/copy-en.d.ts +1 -0
- package/dist/catalogue/copy-en.js +129 -0
- package/dist/catalogue/index.d.ts +9 -0
- package/dist/catalogue/index.js +21 -0
- package/dist/catalogue/integrations.d.ts +4 -0
- package/dist/catalogue/integrations.js +104 -0
- package/dist/catalogue/systems.d.ts +7 -0
- package/dist/catalogue/systems.js +31 -0
- package/dist/catalogue/types.d.ts +110 -0
- package/dist/catalogue/types.js +1 -0
- package/dist/entry-rules.d.ts +53 -0
- package/dist/entry-rules.js +71 -0
- package/dist/hosted-label.d.ts +13 -0
- package/dist/hosted-label.js +81 -0
- package/dist/index.d.ts +14 -5
- package/dist/index.js +7 -3
- package/dist/migrations/0062_hosting_github.sql +6 -0
- package/dist/migrations/0063_boule_apps.sql +7 -0
- package/dist/migrations/0064_requested_changes_boule_apps.sql +6 -0
- package/dist/migrations/0065_native_build_runs.sql +32 -0
- package/dist/migrations/0066_org_kosmos.sql +10 -0
- package/dist/modules/ai.d.ts +3 -0
- package/dist/modules/ai.js +64 -0
- package/dist/modules/assistants.d.ts +3 -0
- package/dist/modules/assistants.js +72 -0
- package/dist/modules/audit-ledger.d.ts +29 -0
- package/dist/modules/audit-ledger.js +49 -0
- package/dist/modules/audit.d.ts +7 -0
- package/dist/modules/audit.js +44 -0
- package/dist/modules/database.d.ts +23 -0
- package/dist/modules/database.js +189 -0
- package/dist/modules/email.d.ts +3 -0
- package/dist/modules/email.js +18 -0
- package/dist/modules/entry-parts.d.ts +23 -0
- package/dist/modules/entry-parts.js +63 -0
- package/dist/modules/files.d.ts +3 -0
- package/dist/modules/files.js +64 -0
- package/dist/modules/index.d.ts +74 -0
- package/dist/modules/index.js +774 -0
- package/dist/modules/jobs.d.ts +3 -0
- package/dist/modules/jobs.js +23 -0
- package/dist/modules/organisations.d.ts +3 -0
- package/dist/modules/organisations.js +20 -0
- package/dist/modules/outputs.d.ts +9 -0
- package/dist/modules/outputs.js +38 -0
- package/dist/modules/sign-in.d.ts +3 -0
- package/dist/modules/sign-in.js +197 -0
- package/dist/modules/signed-records.d.ts +3 -0
- package/dist/modules/signed-records.js +32 -0
- package/dist/modules/types.d.ts +194 -0
- package/dist/modules/types.js +1 -0
- package/dist/modules/workspaces.d.ts +3 -0
- package/dist/modules/workspaces.js +75 -0
- package/dist/native-build.d.ts +70 -0
- package/dist/native-build.js +92 -0
- package/dist/org.d.ts +184 -24
- package/dist/org.js +444 -49
- package/dist/plan/agora.d.ts +1 -1
- package/dist/plan/agora.js +4 -0
- package/dist/plan/apps-db-route.d.ts +90 -0
- package/dist/plan/apps-db-route.js +168 -0
- package/dist/plan/apps-zones.d.ts +56 -0
- package/dist/plan/apps-zones.js +118 -0
- package/dist/plan/archon-blue-green.js +11 -1
- package/dist/plan/blue-green-apply.d.ts +37 -1
- package/dist/plan/blue-green-apply.js +55 -15
- package/dist/plan/blue-green.d.ts +60 -8
- package/dist/plan/blue-green.js +115 -10
- package/dist/plan/boule.d.ts +23 -1
- package/dist/plan/boule.js +35 -2
- package/dist/plan/broker-entries.d.ts +28 -3
- package/dist/plan/broker-entries.js +74 -3
- package/dist/plan/broker-registrar.d.ts +168 -0
- package/dist/plan/broker-registrar.js +440 -0
- package/dist/plan/broker-workspaces-database.d.ts +48 -0
- package/dist/plan/broker-workspaces-database.js +87 -0
- package/dist/plan/deploy-health.js +9 -2
- package/dist/plan/desktop-callback.js +15 -7
- package/dist/plan/diff.d.ts +8 -2
- package/dist/plan/diff.js +552 -104
- package/dist/plan/edge-access.d.ts +6 -0
- package/dist/plan/edge-access.js +1 -0
- package/dist/plan/edge-dns.d.ts +6 -0
- package/dist/plan/edge-dns.js +9 -7
- package/dist/plan/entry-ai-key.d.ts +1 -1
- package/dist/plan/entry-ai-key.js +2 -2
- package/dist/plan/entry-audit.d.ts +58 -0
- package/dist/plan/entry-audit.js +122 -0
- package/dist/plan/entry-customer-grants.d.ts +75 -0
- package/dist/plan/entry-customer-grants.js +154 -0
- package/dist/plan/entry-edge-key.d.ts +26 -1
- package/dist/plan/entry-edge-key.js +42 -3
- package/dist/plan/entry-jobs.d.ts +64 -0
- package/dist/plan/entry-jobs.js +92 -0
- package/dist/plan/entry-repo-key-apply.d.ts +55 -0
- package/dist/plan/entry-repo-key-apply.js +185 -0
- package/dist/plan/entry-repo-key.d.ts +92 -0
- package/dist/plan/entry-repo-key.js +235 -0
- package/dist/plan/entry-workspaces.d.ts +55 -0
- package/dist/plan/entry-workspaces.js +90 -0
- package/dist/plan/executor.d.ts +231 -19
- package/dist/plan/executor.js +1648 -129
- package/dist/plan/gates.d.ts +1 -1
- package/dist/plan/gates.js +62 -9
- package/dist/plan/github-deploy-keys.d.ts +78 -0
- package/dist/plan/github-deploy-keys.js +305 -0
- package/dist/plan/github-source.d.ts +53 -0
- package/dist/plan/github-source.js +140 -0
- package/dist/plan/held-key-refs.d.ts +12 -0
- package/dist/plan/held-key-refs.js +12 -0
- package/dist/plan/hostnames.d.ts +9 -0
- package/dist/plan/hostnames.js +28 -1
- package/dist/plan/index.d.ts +9 -0
- package/dist/plan/index.js +6 -0
- package/dist/plan/jobs-scheduler-http.js +3 -0
- package/dist/plan/keys-registrar-http.d.ts +32 -0
- package/dist/plan/keys-registrar-http.js +105 -0
- package/dist/plan/keys-registrar.d.ts +67 -0
- package/dist/plan/keys-registrar.js +103 -0
- package/dist/plan/kosmos.d.ts +56 -0
- package/dist/plan/kosmos.js +84 -0
- package/dist/plan/live.d.ts +1 -1
- package/dist/plan/locked-hostnames.d.ts +2 -2
- package/dist/plan/locked-hostnames.js +7 -4
- package/dist/plan/login-host.js +11 -5
- package/dist/plan/machine-paths.d.ts +36 -0
- package/dist/plan/machine-paths.js +137 -0
- package/dist/plan/mcp.d.ts +1 -1
- package/dist/plan/mcp.js +49 -17
- package/dist/plan/org-apps-admin.d.ts +108 -0
- package/dist/plan/org-apps-admin.js +434 -0
- package/dist/plan/org-names.d.ts +3 -3
- package/dist/plan/org-names.js +15 -2
- package/dist/plan/org-postgres-migrate.js +1 -0
- package/dist/plan/own-diff.d.ts +23 -0
- package/dist/plan/own-diff.js +53 -0
- package/dist/plan/own-hostnames.d.ts +2 -0
- package/dist/plan/own-hostnames.js +1 -0
- package/dist/plan/own-jobs-route.d.ts +47 -0
- package/dist/plan/own-jobs-route.js +91 -0
- package/dist/plan/own-limits.d.ts +6 -0
- package/dist/plan/own-limits.js +1 -0
- package/dist/plan/own-services.d.ts +4 -0
- package/dist/plan/own-services.js +4 -0
- package/dist/plan/plan-apply.d.ts +39 -0
- package/dist/plan/plan-apply.js +163 -9
- package/dist/plan/platform-diff.d.ts +2 -2
- package/dist/plan/platform-diff.js +7 -2
- package/dist/plan/platform-org.d.ts +30 -0
- package/dist/plan/platform-org.js +12 -1
- package/dist/plan/platform-providers.d.ts +2 -0
- package/dist/plan/platform-providers.js +13 -2
- package/dist/plan/post-logout-uris.d.ts +1 -1
- package/dist/plan/post-logout-uris.js +12 -3
- package/dist/plan/private-pages.d.ts +26 -0
- package/dist/plan/private-pages.js +39 -0
- package/dist/plan/providers-readers.js +20 -1
- package/dist/plan/proxy-labels.d.ts +12 -1
- package/dist/plan/proxy-labels.js +32 -2
- package/dist/plan/release-host.d.ts +6 -2
- package/dist/plan/release-host.js +36 -8
- package/dist/plan/stage-env.d.ts +19 -1
- package/dist/plan/stage-env.js +25 -1
- package/dist/plan/stage-guards.d.ts +20 -0
- package/dist/plan/stage-guards.js +24 -0
- package/dist/plan/stage-isolation.d.ts +24 -0
- package/dist/plan/stage-isolation.js +29 -1
- package/dist/plan/worker-app-database.d.ts +46 -42
- package/dist/plan/worker-app-database.js +150 -153
- package/dist/plan/worker-app.d.ts +5 -65
- package/dist/plan/worker-app.js +28 -105
- package/dist/plan/worker.d.ts +2 -2
- package/dist/plan/worker.js +2 -1
- package/dist/plan/workspace-client.d.ts +25 -0
- package/dist/plan/workspace-client.js +48 -0
- package/dist/plan/workspace-sign-in.d.ts +55 -0
- package/dist/plan/workspace-sign-in.js +273 -0
- package/dist/plan/workspace-worker.d.ts +4 -0
- package/dist/plan/workspace-worker.js +13 -0
- package/dist/plan/workspace.d.ts +101 -0
- package/dist/plan/workspace.js +305 -0
- package/dist/release/decide.d.ts +30 -1
- package/dist/release/decide.js +73 -11
- package/dist/release/health.d.ts +8 -3
- package/dist/release/health.js +58 -5
- package/dist/release/migration-compatibility.d.ts +66 -0
- package/dist/release/migration-compatibility.js +340 -0
- package/dist/release/run.d.ts +2 -5
- package/dist/release/run.js +4 -0
- package/dist/release/state.d.ts +3 -1
- package/dist/release/state.js +6 -0
- package/dist/release/types.d.ts +20 -0
- package/dist/requested-changes.d.ts +11 -2
- package/dist/requested-changes.js +13 -1
- package/dist/service-labels.d.ts +4 -0
- package/dist/service-labels.js +11 -0
- package/dist/slug.d.ts +2 -0
- package/dist/slug.js +20 -7
- package/dist/store/postgres.js +25 -2
- package/dist/store/schema.d.ts +73 -1
- package/dist/store/schema.js +10 -0
- package/dist/worker-app-config.d.ts +1 -11
- package/dist/worker-app-config.js +4 -17
- package/dist/worker-app-migrations.d.ts +105 -0
- package/dist/worker-app-migrations.js +166 -0
- package/dist/worker-app-upload.d.ts +103 -0
- package/dist/worker-app-upload.js +128 -0
- package/package.json +19 -2
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
export const WORKER_APP_MIGRATION_TIMEOUT_MS = 300_000;
|
|
3
|
+
export const MIGRATIONS_ASSET_NAME = 'migrations.json';
|
|
4
|
+
const MIGRATIONS_MAX_CHARS = 4_000_000;
|
|
5
|
+
const MIGRATIONS_MAX_COUNT = 1000;
|
|
6
|
+
const MIGRATION_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
7
|
+
// ---- parsing ---------------------------------------------------------------
|
|
8
|
+
/**
|
|
9
|
+
* Reads a release's `migrations.json`. `null` text is an absent file: no migrations. A file that is
|
|
10
|
+
* not a list of `{id, sql}` with unique ids is refused, naming no SQL.
|
|
11
|
+
*/
|
|
12
|
+
export function parseMigrations(text) {
|
|
13
|
+
if (text === null)
|
|
14
|
+
return [];
|
|
15
|
+
if (text.length > MIGRATIONS_MAX_CHARS) {
|
|
16
|
+
throw new Error(`${MIGRATIONS_ASSET_NAME} is larger than ${MIGRATIONS_MAX_CHARS} characters`);
|
|
17
|
+
}
|
|
18
|
+
let parsed;
|
|
19
|
+
try {
|
|
20
|
+
parsed = JSON.parse(text);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
throw new Error(`${MIGRATIONS_ASSET_NAME} is not JSON`);
|
|
24
|
+
}
|
|
25
|
+
if (!Array.isArray(parsed) || parsed.length > MIGRATIONS_MAX_COUNT) {
|
|
26
|
+
throw new Error(`${MIGRATIONS_ASSET_NAME} must be a list of at most ${MIGRATIONS_MAX_COUNT} migrations`);
|
|
27
|
+
}
|
|
28
|
+
const seen = new Set();
|
|
29
|
+
return parsed.map((item, index) => {
|
|
30
|
+
const record = item;
|
|
31
|
+
if (typeof item !== 'object' ||
|
|
32
|
+
item === null ||
|
|
33
|
+
Array.isArray(item) ||
|
|
34
|
+
Object.keys(item).some((key) => key !== 'id' && key !== 'sql') ||
|
|
35
|
+
typeof record?.id !== 'string' ||
|
|
36
|
+
!MIGRATION_ID.test(record.id) ||
|
|
37
|
+
typeof record.sql !== 'string' ||
|
|
38
|
+
record.sql.trim() === '') {
|
|
39
|
+
throw new Error(`${MIGRATIONS_ASSET_NAME} entry ${index} must be { id, sql } with an id of letters, digits, '.', '_' or '-' and a non-empty sql`);
|
|
40
|
+
}
|
|
41
|
+
if (seen.has(record.id)) {
|
|
42
|
+
throw new Error(`${MIGRATIONS_ASSET_NAME} names migration '${record.id}' twice`);
|
|
43
|
+
}
|
|
44
|
+
seen.add(record.id);
|
|
45
|
+
return { id: record.id, sql: record.sql };
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A present `migrations.json` runs as the database owner, so its bytes must be the ones the
|
|
50
|
+
* declaration pinned. No file needs no digest; a file with no declared digest, or a different
|
|
51
|
+
* one, is refused before it is parsed. The message names neither the SQL nor the wanted digest.
|
|
52
|
+
*/
|
|
53
|
+
export function verifyMigrationsDigest(text, declared) {
|
|
54
|
+
if (text === null)
|
|
55
|
+
return;
|
|
56
|
+
if (!declared) {
|
|
57
|
+
throw new Error(`${MIGRATIONS_ASSET_NAME} is present and workerApp.database.migrationsSha256 is not declared`);
|
|
58
|
+
}
|
|
59
|
+
if (createHash('sha256').update(text, 'utf8').digest('hex') !== declared) {
|
|
60
|
+
throw new Error(`${MIGRATIONS_ASSET_NAME} does not match workerApp.database.migrationsSha256`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** The URL of `migrations.json` beside the module: the same release, another file. */
|
|
64
|
+
export const migrationsUrl = (artifactUrl) => artifactUrl.slice(0, artifactUrl.lastIndexOf('/') + 1) + MIGRATIONS_ASSET_NAME;
|
|
65
|
+
/**
|
|
66
|
+
* The migrations the database does not hold, in order. The applied ledger must be the first
|
|
67
|
+
* migrations of the file: a missing or reordered one is refused (migrations are append-only).
|
|
68
|
+
*/
|
|
69
|
+
export function unappliedDatabaseMigrations(all, applied) {
|
|
70
|
+
const appliedSet = new Set(applied);
|
|
71
|
+
const head = new Set(all.slice(0, applied.length).map((m) => m.id));
|
|
72
|
+
for (const id of appliedSet) {
|
|
73
|
+
if (!head.has(id)) {
|
|
74
|
+
throw new Error(`migration '${id}' was applied and is no longer in the same place in ${MIGRATIONS_ASSET_NAME}; migrations are append-only`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return all.filter((m) => !appliedSet.has(m.id));
|
|
78
|
+
}
|
|
79
|
+
/** Line endings (CRLF, CR) and trailing whitespace do not make two copies of the same SQL different. */
|
|
80
|
+
const normalisedSql = (sql) => sql
|
|
81
|
+
.replace(/\r\n?/g, '\n')
|
|
82
|
+
.split('\n')
|
|
83
|
+
.map((line) => line.replace(/[ \t]+$/, ''))
|
|
84
|
+
.join('\n')
|
|
85
|
+
.trim();
|
|
86
|
+
const sqlDigest = (sql) => createHash('sha256').update(normalisedSql(sql), 'utf8').digest('hex');
|
|
87
|
+
/**
|
|
88
|
+
* The platform migrations still to run, in order, and the ones skipped because the release's own list carries
|
|
89
|
+
* them (same id, or the same SQL under another id: Candor ships its own copy of the audit migrations).
|
|
90
|
+
* A platform migration the database's ledger already holds is neither: it ran before. Never applies twice.
|
|
91
|
+
*/
|
|
92
|
+
export function pendingPlatformMigrations(platform, own, applied) {
|
|
93
|
+
const ownIds = new Set(own.map((m) => m.id));
|
|
94
|
+
const ownDigests = new Set(own.map((m) => sqlDigest(m.sql)));
|
|
95
|
+
const done = new Set(applied);
|
|
96
|
+
const pending = [];
|
|
97
|
+
const skipped = [];
|
|
98
|
+
for (const migration of platform) {
|
|
99
|
+
if (ownIds.has(migration.id))
|
|
100
|
+
skipped.push({ id: migration.id, why: 'same id' });
|
|
101
|
+
else if (ownDigests.has(sqlDigest(migration.sql))) {
|
|
102
|
+
skipped.push({ id: migration.id, why: 'same sql' });
|
|
103
|
+
}
|
|
104
|
+
else if (!done.has(migration.id))
|
|
105
|
+
pending.push(migration);
|
|
106
|
+
}
|
|
107
|
+
return { pending, skipped };
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The build's database changes, shared by the platform apply and `@wtfalch/foundry/apps`:
|
|
111
|
+
* `text` (the release's `migrations.json`, `null` when it has none) is checked against
|
|
112
|
+
* `want.migrationsSha256`, parsed, the unapplied migrations run in order as the owner (each
|
|
113
|
+
* recorded by the runner's ledger), then the runtime role is made true. A failed migration
|
|
114
|
+
* throws; those before it stay applied. Messages start with `at` and name no SQL.
|
|
115
|
+
*/
|
|
116
|
+
export async function runWorkerAppMigrations(runner, owner, text, want, runtime, opts) {
|
|
117
|
+
const { at, call } = opts;
|
|
118
|
+
let migrations;
|
|
119
|
+
try {
|
|
120
|
+
verifyMigrationsDigest(text, want.migrationsSha256);
|
|
121
|
+
migrations = parseMigrations(text);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
// The message names the file and the entry index, never SQL.
|
|
125
|
+
throw new Error(`${at}: ${error.message}`);
|
|
126
|
+
}
|
|
127
|
+
const applied = await call('read the applied migrations', () => runner.appliedMigrationIds(owner));
|
|
128
|
+
const platformIds = new Set((opts.platform ?? []).map((m) => m.id));
|
|
129
|
+
// A release copy (another id) of a platform migration the platform already ran must not run again: the
|
|
130
|
+
// platform's one stands for it. It is counted as applied, and the log says so.
|
|
131
|
+
const platformDigests = new Set((opts.platform ?? []).filter((m) => applied.includes(m.id)).map((m) => sqlDigest(m.sql)));
|
|
132
|
+
const covered = migrations.filter((m) => !platformIds.has(m.id) && !applied.includes(m.id) && platformDigests.has(sqlDigest(m.sql)));
|
|
133
|
+
for (const m of covered) {
|
|
134
|
+
opts.log?.(`${at}: release migration '${m.id}' not run, the platform already ran the same SQL (counted as applied)`);
|
|
135
|
+
}
|
|
136
|
+
const coveredIds = new Set(covered.map((m) => m.id));
|
|
137
|
+
const releaseMigrations = migrations.filter((m) => !coveredIds.has(m.id));
|
|
138
|
+
let pending;
|
|
139
|
+
try {
|
|
140
|
+
// The ledger also holds the platform migrations; they are not part of the release's own head.
|
|
141
|
+
pending = unappliedDatabaseMigrations(releaseMigrations, applied.filter((id) => !platformIds.has(id) || migrations.some((m) => m.id === id)));
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
throw new Error(`${at}: ${error.message}`);
|
|
145
|
+
}
|
|
146
|
+
const platform = pendingPlatformMigrations(opts.platform ?? [], migrations, applied);
|
|
147
|
+
for (const skip of platform.skipped) {
|
|
148
|
+
opts.log?.(`${at}: platform migration '${skip.id}' not applied, the release ships it (${skip.why})`);
|
|
149
|
+
}
|
|
150
|
+
const done = [...applied, ...covered.map((m) => m.id)];
|
|
151
|
+
const ran = [];
|
|
152
|
+
for (const migration of [...platform.pending, ...pending]) {
|
|
153
|
+
await call(`apply migration '${migration.id}'`, () => runner.applyMigration(owner, migration), opts.migrationTimeoutMs ?? WORKER_APP_MIGRATION_TIMEOUT_MS);
|
|
154
|
+
done.push(migration.id);
|
|
155
|
+
ran.push(migration.id);
|
|
156
|
+
await opts.onApplied?.([...done]);
|
|
157
|
+
}
|
|
158
|
+
// After the migrations, so the tables exist for the revokes.
|
|
159
|
+
await call('ensure the runtime role', () => runner.ensureRuntimeRole({
|
|
160
|
+
owner,
|
|
161
|
+
runtime,
|
|
162
|
+
appendOnly: [...want.appendOnly],
|
|
163
|
+
grants: [...(want.grants ?? [])],
|
|
164
|
+
}));
|
|
165
|
+
return { applied: done, ran, skipped: platform.skipped };
|
|
166
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { type WorkerAppConfig } from './worker-app-config.js';
|
|
2
|
+
/**
|
|
3
|
+
* The pure parts of a Worker app deploy, shared by the org planner (`plan/worker-app.ts`, the
|
|
4
|
+
* platform account through edge) and `@wtfalch/foundry/apps` (a company's own account through
|
|
5
|
+
* `@wtfalch/providers/cloudflare`): what the upload carries, which Durable Object migrations are
|
|
6
|
+
* still to run, the container list, the registry rule and the call timeout. No network.
|
|
7
|
+
*/
|
|
8
|
+
/** The compatibility date a Worker gets when its declaration names none. */
|
|
9
|
+
export declare const WORKER_COMPATIBILITY_DATE = "2026-09-12";
|
|
10
|
+
export type WorkerAppBinding = {
|
|
11
|
+
type: 'durable_object_namespace';
|
|
12
|
+
name: string;
|
|
13
|
+
class_name: string;
|
|
14
|
+
} | {
|
|
15
|
+
type: 'plain_text';
|
|
16
|
+
name: string;
|
|
17
|
+
text: string;
|
|
18
|
+
} | {
|
|
19
|
+
type: 'hyperdrive';
|
|
20
|
+
name: string;
|
|
21
|
+
id: string;
|
|
22
|
+
};
|
|
23
|
+
export interface WorkerAppMigrations {
|
|
24
|
+
/** The tag the script holds now. Absent for a script with none. */
|
|
25
|
+
oldTag?: string;
|
|
26
|
+
newTag: string;
|
|
27
|
+
steps: {
|
|
28
|
+
newSqliteClasses?: string[];
|
|
29
|
+
deletedClasses?: string[];
|
|
30
|
+
renamedClasses?: {
|
|
31
|
+
from: string;
|
|
32
|
+
to: string;
|
|
33
|
+
}[];
|
|
34
|
+
}[];
|
|
35
|
+
}
|
|
36
|
+
export interface WorkerAppUpload {
|
|
37
|
+
name: string;
|
|
38
|
+
artifactUrl: string;
|
|
39
|
+
artifactSha256: string;
|
|
40
|
+
compatibilityDate: string;
|
|
41
|
+
compatibilityFlags: string[];
|
|
42
|
+
/** All of them: a redeploy replaces the list. Secrets survive a redeploy. */
|
|
43
|
+
bindings: WorkerAppBinding[];
|
|
44
|
+
/** Only the migrations not yet applied; absent when there are none. */
|
|
45
|
+
migrations?: WorkerAppMigrations;
|
|
46
|
+
/** Durable Object classes the upload marks container-enabled; absent when the entry declares no container. */
|
|
47
|
+
containers?: string[];
|
|
48
|
+
/** Workers Logs: always on, every request sampled. The adapter sends it as the upload metadata's `observability`. */
|
|
49
|
+
observability: {
|
|
50
|
+
enabled: true;
|
|
51
|
+
head_sampling_rate: 1;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export interface WorkerAppContainer {
|
|
55
|
+
className: string;
|
|
56
|
+
/** `<repository>@sha256:<digest>`. */
|
|
57
|
+
image: string;
|
|
58
|
+
instanceType: string;
|
|
59
|
+
maxInstances: number;
|
|
60
|
+
}
|
|
61
|
+
/** What a Worker app's declaration says the script is, sorted, before names, hosts and env. */
|
|
62
|
+
export interface WorkerAppShape {
|
|
63
|
+
artifactUrl: string;
|
|
64
|
+
artifactSha256: string;
|
|
65
|
+
compatibilityDate: string;
|
|
66
|
+
compatibilityFlags: string[];
|
|
67
|
+
classes: {
|
|
68
|
+
binding: string;
|
|
69
|
+
className: string;
|
|
70
|
+
}[];
|
|
71
|
+
migrations: WorkerAppConfig['durableObjects']['migrations'];
|
|
72
|
+
containers: WorkerAppContainer[];
|
|
73
|
+
}
|
|
74
|
+
export declare function workerAppShape(app: WorkerAppConfig): WorkerAppShape;
|
|
75
|
+
/** The registry prefix a container image of an account must start with. */
|
|
76
|
+
export declare const workerAppRegistryPrefix: (accountId: string) => string;
|
|
77
|
+
/**
|
|
78
|
+
* The first declared container whose image is not in `accountId`'s registry (or holds a `..`
|
|
79
|
+
* segment), or `undefined` when every one is. With no account id every container is stray.
|
|
80
|
+
*/
|
|
81
|
+
export declare function strayContainerImage(app: Pick<WorkerAppConfig, 'containers'>, accountId: string | undefined): NonNullable<WorkerAppConfig['containers']>[number] | undefined;
|
|
82
|
+
/**
|
|
83
|
+
* The Durable Object migrations the live script does not hold yet, from the tag it holds. A live
|
|
84
|
+
* tag that is not in the declared list is refused: migrations are append-only.
|
|
85
|
+
*/
|
|
86
|
+
export declare function pendingWorkerAppMigrations(declared: WorkerAppConfig['durableObjects']['migrations'], liveTag: string | undefined): {
|
|
87
|
+
ok: true;
|
|
88
|
+
migrations?: WorkerAppMigrations;
|
|
89
|
+
} | {
|
|
90
|
+
ok: false;
|
|
91
|
+
reason: string;
|
|
92
|
+
};
|
|
93
|
+
/** The upload of `shape` as script `name`: Durable Object classes, then plain vars, then Hyperdrive. */
|
|
94
|
+
export declare function workerAppUpload(name: string, shape: Omit<WorkerAppShape, 'migrations'>, vars: readonly (readonly [string, string])[], migrations: WorkerAppMigrations | undefined, hyperdriveId: string | undefined): WorkerAppUpload;
|
|
95
|
+
/** The record a Worker route needs on a zone: Cloudflare's documented dummy, proxied. */
|
|
96
|
+
export declare const WORKER_APP_DNS_RECORD: {
|
|
97
|
+
readonly type: "AAAA";
|
|
98
|
+
readonly content: "100::";
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Races `task` against a timer. An error names the call and edge's error code, never the cause.
|
|
102
|
+
*/
|
|
103
|
+
export declare function withCallTimeout<T>(ms: number, label: string, task: () => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { WORKER_APP_HYPERDRIVE_BINDING } from './worker-app-config.js';
|
|
2
|
+
/**
|
|
3
|
+
* The pure parts of a Worker app deploy, shared by the org planner (`plan/worker-app.ts`, the
|
|
4
|
+
* platform account through edge) and `@wtfalch/foundry/apps` (a company's own account through
|
|
5
|
+
* `@wtfalch/providers/cloudflare`): what the upload carries, which Durable Object migrations are
|
|
6
|
+
* still to run, the container list, the registry rule and the call timeout. No network.
|
|
7
|
+
*/
|
|
8
|
+
/** The compatibility date a Worker gets when its declaration names none. */
|
|
9
|
+
export const WORKER_COMPATIBILITY_DATE = '2026-09-12';
|
|
10
|
+
export function workerAppShape(app) {
|
|
11
|
+
return {
|
|
12
|
+
artifactUrl: app.artifactUrl,
|
|
13
|
+
artifactSha256: app.artifactSha256,
|
|
14
|
+
compatibilityDate: app.compatibilityDate ?? WORKER_COMPATIBILITY_DATE,
|
|
15
|
+
compatibilityFlags: [...(app.compatibilityFlags ?? [])].sort(),
|
|
16
|
+
classes: Object.entries(app.durableObjects.classes)
|
|
17
|
+
.map(([binding, className]) => ({ binding, className }))
|
|
18
|
+
.sort((a, b) => a.binding.localeCompare(b.binding)),
|
|
19
|
+
migrations: app.durableObjects.migrations,
|
|
20
|
+
containers: (app.containers ?? [])
|
|
21
|
+
.map((c) => ({
|
|
22
|
+
className: c.className,
|
|
23
|
+
image: `${c.image.repository}@${c.image.digest}`,
|
|
24
|
+
instanceType: c.instanceType,
|
|
25
|
+
maxInstances: c.maxInstances,
|
|
26
|
+
}))
|
|
27
|
+
.sort((a, b) => a.className.localeCompare(b.className)),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** The registry prefix a container image of an account must start with. */
|
|
31
|
+
export const workerAppRegistryPrefix = (accountId) => `registry.cloudflare.com/${accountId}/`;
|
|
32
|
+
/**
|
|
33
|
+
* The first declared container whose image is not in `accountId`'s registry (or holds a `..`
|
|
34
|
+
* segment), or `undefined` when every one is. With no account id every container is stray.
|
|
35
|
+
*/
|
|
36
|
+
export function strayContainerImage(app, accountId) {
|
|
37
|
+
return (app.containers ?? []).find((c) => accountId === undefined ||
|
|
38
|
+
!c.image.repository.startsWith(workerAppRegistryPrefix(accountId)) ||
|
|
39
|
+
c.image.repository.split('/').some((part) => /^\.+$/.test(part)));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The Durable Object migrations the live script does not hold yet, from the tag it holds. A live
|
|
43
|
+
* tag that is not in the declared list is refused: migrations are append-only.
|
|
44
|
+
*/
|
|
45
|
+
export function pendingWorkerAppMigrations(declared, liveTag) {
|
|
46
|
+
let appliedCount = 0;
|
|
47
|
+
if (liveTag !== undefined) {
|
|
48
|
+
const index = declared.findIndex((m) => m.tag === liveTag);
|
|
49
|
+
if (index < 0) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
reason: `the live Worker holds migration '${liveTag}', which is not in the declared list; migrations are append-only and foundry will not guess`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
appliedCount = index + 1;
|
|
56
|
+
}
|
|
57
|
+
const pending = declared.slice(appliedCount);
|
|
58
|
+
if (pending.length === 0)
|
|
59
|
+
return { ok: true };
|
|
60
|
+
return {
|
|
61
|
+
ok: true,
|
|
62
|
+
migrations: {
|
|
63
|
+
...(liveTag !== undefined ? { oldTag: liveTag } : {}),
|
|
64
|
+
newTag: pending[pending.length - 1]?.tag,
|
|
65
|
+
steps: pending.map((m) => ({
|
|
66
|
+
...(m.newSqliteClasses ? { newSqliteClasses: m.newSqliteClasses } : {}),
|
|
67
|
+
...(m.deletedClasses ? { deletedClasses: m.deletedClasses } : {}),
|
|
68
|
+
...(m.renamedClasses ? { renamedClasses: m.renamedClasses } : {}),
|
|
69
|
+
})),
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** The upload of `shape` as script `name`: Durable Object classes, then plain vars, then Hyperdrive. */
|
|
74
|
+
export function workerAppUpload(name, shape, vars, migrations, hyperdriveId) {
|
|
75
|
+
return {
|
|
76
|
+
name,
|
|
77
|
+
artifactUrl: shape.artifactUrl,
|
|
78
|
+
artifactSha256: shape.artifactSha256,
|
|
79
|
+
compatibilityDate: shape.compatibilityDate,
|
|
80
|
+
compatibilityFlags: shape.compatibilityFlags,
|
|
81
|
+
bindings: [
|
|
82
|
+
...shape.classes.map((c) => ({
|
|
83
|
+
type: 'durable_object_namespace',
|
|
84
|
+
name: c.binding,
|
|
85
|
+
class_name: c.className,
|
|
86
|
+
})),
|
|
87
|
+
...vars.map(([varName, text]) => ({ type: 'plain_text', name: varName, text })),
|
|
88
|
+
...(hyperdriveId
|
|
89
|
+
? [{ type: 'hyperdrive', name: WORKER_APP_HYPERDRIVE_BINDING, id: hyperdriveId }]
|
|
90
|
+
: []),
|
|
91
|
+
],
|
|
92
|
+
observability: { enabled: true, head_sampling_rate: 1 },
|
|
93
|
+
...(migrations ? { migrations } : {}),
|
|
94
|
+
...(shape.containers.length > 0
|
|
95
|
+
? { containers: shape.containers.map((c) => c.className) }
|
|
96
|
+
: {}),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/** The record a Worker route needs on a zone: Cloudflare's documented dummy, proxied. */
|
|
100
|
+
export const WORKER_APP_DNS_RECORD = {
|
|
101
|
+
type: 'AAAA',
|
|
102
|
+
content: '100::',
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Races `task` against a timer. An error names the call and edge's error code, never the cause.
|
|
106
|
+
*/
|
|
107
|
+
export function withCallTimeout(ms, label, task) {
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
const timer = setTimeout(() => reject(new Error(`${label}: no answer within ${ms} ms`)), ms);
|
|
110
|
+
task().then((value) => {
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
resolve(value);
|
|
113
|
+
}, (error) => {
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
// The cause is dropped: a provider error can echo a request body. The
|
|
116
|
+
// HTTP status stays, because callers tell "already gone" by it. So does
|
|
117
|
+
// edge's error code (a fixed lower-case enum, never a secret), to say why.
|
|
118
|
+
const { status, code } = (error ?? {});
|
|
119
|
+
const known = typeof code === 'string' && /^[a-z_]{1,40}$/.test(code) ? code : undefined;
|
|
120
|
+
const failure = new Error(known ? `${label} failed (${known})` : `${label} failed`);
|
|
121
|
+
if (typeof status === 'number')
|
|
122
|
+
failure.status = status;
|
|
123
|
+
if (known)
|
|
124
|
+
failure.code = known;
|
|
125
|
+
reject(failure);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wtfalch/foundry",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.62.0",
|
|
4
4
|
"description": "Foundry's SDK: the declared org's types, the plan and apply contracts, and the state store's interface.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -15,6 +15,22 @@
|
|
|
15
15
|
"types": "./dist/index.d.ts",
|
|
16
16
|
"default": "./dist/index.js"
|
|
17
17
|
},
|
|
18
|
+
"./modules": {
|
|
19
|
+
"types": "./dist/modules/index.d.ts",
|
|
20
|
+
"default": "./dist/modules/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./catalogue": {
|
|
23
|
+
"types": "./dist/catalogue/index.d.ts",
|
|
24
|
+
"default": "./dist/catalogue/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./audit": {
|
|
27
|
+
"types": "./dist/audit/index.d.ts",
|
|
28
|
+
"default": "./dist/audit/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./apps": {
|
|
31
|
+
"types": "./dist/apps/index.d.ts",
|
|
32
|
+
"default": "./dist/apps/index.js"
|
|
33
|
+
},
|
|
18
34
|
"./migrations-dir": {
|
|
19
35
|
"types": "./dist/migrations-dir.d.ts",
|
|
20
36
|
"default": "./dist/migrations-dir.js"
|
|
@@ -43,8 +59,9 @@
|
|
|
43
59
|
},
|
|
44
60
|
"dependencies": {
|
|
45
61
|
"@wtfalch/audit": "0.6.0",
|
|
62
|
+
"@wtfalch/audit-sealing": "npm:@wtfalch/audit@0.14.0",
|
|
46
63
|
"@wtfalch/auth": "^0.26.0",
|
|
47
|
-
"@wtfalch/edge": "0.
|
|
64
|
+
"@wtfalch/edge": "0.21.0",
|
|
48
65
|
"@wtfalch/email": "^0.20.0",
|
|
49
66
|
"@wtfalch/providers": "0.12.0",
|
|
50
67
|
"ssh2": "1.17.0",
|