@package-broker/cli 0.19.7 → 2.0.0-rc.1
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/LICENSE +661 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +12 -0
- package/dist/bin.js.map +1 -0
- package/dist/build-artifacts.d.ts +27 -0
- package/dist/build-artifacts.d.ts.map +1 -0
- package/dist/build-artifacts.js +169 -0
- package/dist/build-artifacts.js.map +1 -0
- package/dist/build-assets.d.ts +36 -0
- package/dist/build-assets.d.ts.map +1 -0
- package/dist/build-assets.js +224 -0
- package/dist/build-assets.js.map +1 -0
- package/dist/build.d.ts +31 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +277 -0
- package/dist/build.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +801 -0
- package/dist/cli.js.map +1 -0
- package/dist/cloudflare/d1-http-executor.d.ts +30 -0
- package/dist/cloudflare/d1-http-executor.d.ts.map +1 -0
- package/dist/cloudflare/d1-http-executor.js +230 -0
- package/dist/cloudflare/d1-http-executor.js.map +1 -0
- package/dist/cloudflare/d1-http-response.d.ts +4 -0
- package/dist/cloudflare/d1-http-response.d.ts.map +1 -0
- package/dist/cloudflare/d1-http-response.js +113 -0
- package/dist/cloudflare/d1-http-response.js.map +1 -0
- package/dist/commands/admin.d.ts +47 -0
- package/dist/commands/admin.d.ts.map +1 -0
- package/dist/commands/admin.js +447 -0
- package/dist/commands/admin.js.map +1 -0
- package/dist/commands/built-manifest.d.ts +13 -0
- package/dist/commands/built-manifest.d.ts.map +1 -0
- package/dist/commands/built-manifest.js +197 -0
- package/dist/commands/built-manifest.js.map +1 -0
- package/dist/commands/cloudflare/api.d.ts +129 -0
- package/dist/commands/cloudflare/api.d.ts.map +1 -0
- package/dist/commands/cloudflare/api.js +480 -0
- package/dist/commands/cloudflare/api.js.map +1 -0
- package/dist/commands/cloudflare/deploy.d.ts +81 -0
- package/dist/commands/cloudflare/deploy.d.ts.map +1 -0
- package/dist/commands/cloudflare/deploy.js +1848 -0
- package/dist/commands/cloudflare/deploy.js.map +1 -0
- package/dist/commands/cloudflare/inventory.d.ts +243 -0
- package/dist/commands/cloudflare/inventory.d.ts.map +1 -0
- package/dist/commands/cloudflare/inventory.js +786 -0
- package/dist/commands/cloudflare/inventory.js.map +1 -0
- package/dist/commands/cloudflare/janitor.d.ts +10 -0
- package/dist/commands/cloudflare/janitor.d.ts.map +1 -0
- package/dist/commands/cloudflare/janitor.js +84 -0
- package/dist/commands/cloudflare/janitor.js.map +1 -0
- package/dist/commands/cloudflare/migrations-check.d.ts +5 -0
- package/dist/commands/cloudflare/migrations-check.d.ts.map +1 -0
- package/dist/commands/cloudflare/migrations-check.js +10 -0
- package/dist/commands/cloudflare/migrations-check.js.map +1 -0
- package/dist/commands/cloudflare/provision.d.ts +79 -0
- package/dist/commands/cloudflare/provision.d.ts.map +1 -0
- package/dist/commands/cloudflare/provision.js +961 -0
- package/dist/commands/cloudflare/provision.js.map +1 -0
- package/dist/commands/cloudflare/queue-consumer.d.ts +4 -0
- package/dist/commands/cloudflare/queue-consumer.d.ts.map +1 -0
- package/dist/commands/cloudflare/queue-consumer.js +57 -0
- package/dist/commands/cloudflare/queue-consumer.js.map +1 -0
- package/dist/commands/cloudflare/setup.d.ts +48 -0
- package/dist/commands/cloudflare/setup.d.ts.map +1 -0
- package/dist/commands/cloudflare/setup.js +836 -0
- package/dist/commands/cloudflare/setup.js.map +1 -0
- package/dist/commands/cloudflare/smoke.d.ts +29 -0
- package/dist/commands/cloudflare/smoke.d.ts.map +1 -0
- package/dist/commands/cloudflare/smoke.js +715 -0
- package/dist/commands/cloudflare/smoke.js.map +1 -0
- package/dist/commands/cloudflare/teardown.d.ts +62 -0
- package/dist/commands/cloudflare/teardown.d.ts.map +1 -0
- package/dist/commands/cloudflare/teardown.js +906 -0
- package/dist/commands/cloudflare/teardown.js.map +1 -0
- package/dist/commands/cloudflare/worker-settings.d.ts +4 -0
- package/dist/commands/cloudflare/worker-settings.d.ts.map +1 -0
- package/dist/commands/cloudflare/worker-settings.js +93 -0
- package/dist/commands/cloudflare/worker-settings.js.map +1 -0
- package/dist/commands/cloudflare.d.ts +55 -0
- package/dist/commands/cloudflare.d.ts.map +1 -0
- package/dist/commands/cloudflare.js +393 -0
- package/dist/commands/cloudflare.js.map +1 -0
- package/dist/commands/import-cutover.d.ts +5 -0
- package/dist/commands/import-cutover.d.ts.map +1 -0
- package/dist/commands/import-cutover.js +25 -0
- package/dist/commands/import-cutover.js.map +1 -0
- package/dist/commands/import-private-packagist.d.ts +38 -0
- package/dist/commands/import-private-packagist.d.ts.map +1 -0
- package/dist/commands/import-private-packagist.js +232 -0
- package/dist/commands/import-private-packagist.js.map +1 -0
- package/dist/commands/import-satis.d.ts +11 -0
- package/dist/commands/import-satis.d.ts.map +1 -0
- package/dist/commands/import-satis.js +112 -0
- package/dist/commands/import-satis.js.map +1 -0
- package/dist/commands/import.d.ts +22 -0
- package/dist/commands/import.d.ts.map +1 -0
- package/dist/commands/import.js +343 -0
- package/dist/commands/import.js.map +1 -0
- package/dist/commands/openapi-specs.d.ts +4 -0
- package/dist/commands/openapi-specs.d.ts.map +1 -0
- package/dist/commands/openapi-specs.js +94 -0
- package/dist/commands/openapi-specs.js.map +1 -0
- package/dist/commands/openapi.d.ts +12 -0
- package/dist/commands/openapi.d.ts.map +1 -0
- package/dist/commands/openapi.js +44 -0
- package/dist/commands/openapi.js.map +1 -0
- package/dist/commands/source.d.ts +8 -0
- package/dist/commands/source.d.ts.map +1 -0
- package/dist/commands/source.js +61 -0
- package/dist/commands/source.js.map +1 -0
- package/dist/commands/sqlite.d.ts +11 -0
- package/dist/commands/sqlite.d.ts.map +1 -0
- package/dist/commands/sqlite.js +76 -0
- package/dist/commands/sqlite.js.map +1 -0
- package/dist/import/client.d.ts +71 -0
- package/dist/import/client.d.ts.map +1 -0
- package/dist/import/client.js +348 -0
- package/dist/import/client.js.map +1 -0
- package/dist/import/credential-refs.d.ts +2 -0
- package/dist/import/credential-refs.d.ts.map +1 -0
- package/dist/import/credential-refs.js +22 -0
- package/dist/import/credential-refs.js.map +1 -0
- package/dist/import/cutover.d.ts +64 -0
- package/dist/import/cutover.d.ts.map +1 -0
- package/dist/import/cutover.js +544 -0
- package/dist/import/cutover.js.map +1 -0
- package/dist/import/private-packagist/api.d.ts +57 -0
- package/dist/import/private-packagist/api.d.ts.map +1 -0
- package/dist/import/private-packagist/api.js +246 -0
- package/dist/import/private-packagist/api.js.map +1 -0
- package/dist/import/private-packagist/dates.d.ts +30 -0
- package/dist/import/private-packagist/dates.d.ts.map +1 -0
- package/dist/import/private-packagist/dates.js +189 -0
- package/dist/import/private-packagist/dates.js.map +1 -0
- package/dist/import/private-packagist/map.d.ts +48 -0
- package/dist/import/private-packagist/map.d.ts.map +1 -0
- package/dist/import/private-packagist/map.js +162 -0
- package/dist/import/private-packagist/map.js.map +1 -0
- package/dist/import/private-packagist/plan.d.ts +61 -0
- package/dist/import/private-packagist/plan.d.ts.map +1 -0
- package/dist/import/private-packagist/plan.js +936 -0
- package/dist/import/private-packagist/plan.js.map +1 -0
- package/dist/import/private-packagist/snapshot.d.ts +31 -0
- package/dist/import/private-packagist/snapshot.d.ts.map +1 -0
- package/dist/import/private-packagist/snapshot.js +406 -0
- package/dist/import/private-packagist/snapshot.js.map +1 -0
- package/dist/import/satis.d.ts +93 -0
- package/dist/import/satis.d.ts.map +1 -0
- package/dist/import/satis.js +873 -0
- package/dist/import/satis.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -96
- package/dist/index.js.map +1 -1
- package/dist/manifest.d.ts +55 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +125 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations-collect.d.ts +6 -0
- package/dist/migrations-collect.d.ts.map +1 -0
- package/dist/migrations-collect.js +39 -0
- package/dist/migrations-collect.js.map +1 -0
- package/dist/prebuilt-expected.json +7 -0
- package/dist/prebuilt.d.ts +27 -0
- package/dist/prebuilt.d.ts.map +1 -0
- package/dist/prebuilt.js +391 -0
- package/dist/prebuilt.js.map +1 -0
- package/package.json +21 -25
- package/README.md +0 -29
- package/dist/__tests__/migrate.test.d.ts +0 -2
- package/dist/__tests__/migrate.test.d.ts.map +0 -1
- package/dist/__tests__/migrate.test.js +0 -116
- package/dist/__tests__/migrate.test.js.map +0 -1
- package/dist/migrate.d.ts +0 -40
- package/dist/migrate.d.ts.map +0 -1
- package/dist/migrate.js +0 -335
- package/dist/migrate.js.map +0 -1
|
@@ -0,0 +1,961 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { createInterface } from 'node:readline/promises';
|
|
5
|
+
import { validatePlatformPorts } from '@package-broker/core/platform/port-config';
|
|
6
|
+
import { PLUGIN_BINDABLE_PORTS } from '@package-broker/core/plugin';
|
|
7
|
+
import { assertPrebuiltConfiguration, materializePrebuilt, prebuiltEnvelope, removePrebuilt, } from '../../prebuilt.js';
|
|
8
|
+
import { queueConsumerScript } from './queue-consumer.js';
|
|
9
|
+
import { CREATE_SETTLE_MS, CloudflareApi, CloudflareError, cloudflareCredentials, commandEnvelope, failCommand, object, } from './api.js';
|
|
10
|
+
import { InventoryStore, InventoryTransaction, digest, emptyInventory, installIdentity, abortOnlyRule, lifecycleRule, validResourceId, } from './inventory.js';
|
|
11
|
+
const QUEUE_ACCOUNT_LIMIT = 10_000;
|
|
12
|
+
export const cloudflareProviderContract = {
|
|
13
|
+
async capabilities(api, required, identity) {
|
|
14
|
+
const evidence = {};
|
|
15
|
+
const probe = async (check, read) => {
|
|
16
|
+
try {
|
|
17
|
+
await read();
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (check !== 'analytics_engine' &&
|
|
22
|
+
error instanceof CloudflareError &&
|
|
23
|
+
(error.status === 403 || (check === 'queues' && error.providerCode === 10253)))
|
|
24
|
+
return false;
|
|
25
|
+
throw new CloudflareError('capability_unknown', 1, false, 'provision', null, check);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
if (required.queues) {
|
|
29
|
+
let count = 0;
|
|
30
|
+
const supported = await probe('queues', async () => {
|
|
31
|
+
count = (await api.list('/queues')).length;
|
|
32
|
+
});
|
|
33
|
+
// https://developers.cloudflare.com/queues/platform/limits/
|
|
34
|
+
evidence.queues = {
|
|
35
|
+
supported,
|
|
36
|
+
remaining: supported ? Math.max(0, QUEUE_ACCOUNT_LIMIT - count) : 0,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (required.r2)
|
|
40
|
+
evidence.r2 = {
|
|
41
|
+
supported: await probe('r2', async () => {
|
|
42
|
+
await api.list('/r2/buckets', 'buckets', {
|
|
43
|
+
'cf-r2-jurisdiction': identity.jurisdiction,
|
|
44
|
+
});
|
|
45
|
+
}),
|
|
46
|
+
};
|
|
47
|
+
if (required.analytics) {
|
|
48
|
+
let paid = null;
|
|
49
|
+
try {
|
|
50
|
+
const subscriptions = await api.snapshot('/subscriptions');
|
|
51
|
+
// WORKERS_PAID is documented at /tenant/reference/subscriptions/.
|
|
52
|
+
paid = subscriptions.some((subscription) => {
|
|
53
|
+
const plan = object(subscription.rate_plan);
|
|
54
|
+
return (typeof plan.id === 'string' &&
|
|
55
|
+
plan.id.toUpperCase() === 'WORKERS_PAID' &&
|
|
56
|
+
plan.scope === 'account' &&
|
|
57
|
+
(subscription.state === 'Paid' || subscription.state === 'Provisioned'));
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Billing permission is optional; unreadable or incomplete subscriptions are no veto.
|
|
62
|
+
}
|
|
63
|
+
if (paid !== null)
|
|
64
|
+
evidence.analytics = { supported: paid, source: 'subscriptions' };
|
|
65
|
+
else {
|
|
66
|
+
try {
|
|
67
|
+
await api.analyticsEngineTables();
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
const unknown = new CloudflareError('capability_unknown', 1, false, 'provision', null, 'analytics_engine', null, 'analytics_engine_sql');
|
|
71
|
+
if (error instanceof CloudflareError && (error.status === 401 || error.status === 403))
|
|
72
|
+
unknown.message =
|
|
73
|
+
'Analytics Engine SQL access was denied. Verify the token has Account Analytics Read permission for the selected account, then rerun. Billing permission is optional.';
|
|
74
|
+
throw unknown;
|
|
75
|
+
}
|
|
76
|
+
evidence.analytics = { supported: true, source: 'analytics_engine_sql' };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return evidence;
|
|
80
|
+
},
|
|
81
|
+
// No bounded creation-visibility guarantee is documented. Journal timing alone is not ownership.
|
|
82
|
+
pendingCreation: () => false,
|
|
83
|
+
exactNameUnique: (kind) => ['d1', 'r2', 'queue', 'worker'].includes(kind),
|
|
84
|
+
};
|
|
85
|
+
const kindPath = (kind) => kind === 'd1' ? '/d1/database' : kind === 'r2' ? '/r2/buckets' : '/queues';
|
|
86
|
+
const jurisdictionHeaders = (spec) => spec.kind === 'r2' && spec.jurisdiction !== null
|
|
87
|
+
? { 'cf-r2-jurisdiction': spec.jurisdiction }
|
|
88
|
+
: undefined;
|
|
89
|
+
function specsFor(identity, required) {
|
|
90
|
+
const base = identity.script_name;
|
|
91
|
+
return [
|
|
92
|
+
{ key: 'd1', kind: 'd1', name: `${base}-db`, jurisdiction: identity.jurisdiction },
|
|
93
|
+
...(required.r2
|
|
94
|
+
? [
|
|
95
|
+
{
|
|
96
|
+
key: 'r2',
|
|
97
|
+
kind: 'r2',
|
|
98
|
+
name: `${base}-blobs`,
|
|
99
|
+
jurisdiction: identity.jurisdiction,
|
|
100
|
+
},
|
|
101
|
+
]
|
|
102
|
+
: []),
|
|
103
|
+
...(required.queues
|
|
104
|
+
? [
|
|
105
|
+
{
|
|
106
|
+
key: 'dlq',
|
|
107
|
+
kind: 'queue',
|
|
108
|
+
name: `${base}-jobs-dlq`,
|
|
109
|
+
jurisdiction: null,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
key: 'jobs',
|
|
113
|
+
kind: 'queue',
|
|
114
|
+
name: `${base}-jobs`,
|
|
115
|
+
jurisdiction: null,
|
|
116
|
+
},
|
|
117
|
+
]
|
|
118
|
+
: []),
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
class UnsupportedRequirementsError extends CloudflareError {
|
|
122
|
+
requirements;
|
|
123
|
+
constructor(requirements) {
|
|
124
|
+
super('invalid_input', 2);
|
|
125
|
+
this.requirements = requirements;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async function selectedBuild(outDir) {
|
|
129
|
+
try {
|
|
130
|
+
const manifest = object(JSON.parse(await readFile(resolve(outDir, 'generated/manifest.json'), 'utf8')));
|
|
131
|
+
const info = object(JSON.parse(await readFile(resolve(outDir, 'build-info.json'), 'utf8')));
|
|
132
|
+
if (manifest.schemaVersion !== 1 ||
|
|
133
|
+
manifest.platform !== 'cloudflare' ||
|
|
134
|
+
info.schemaVersion !== 1 ||
|
|
135
|
+
info.target !== 'cloudflare' ||
|
|
136
|
+
typeof info.core !== 'string' ||
|
|
137
|
+
!Array.isArray(manifest.bindings) ||
|
|
138
|
+
!Array.isArray(manifest.plugins))
|
|
139
|
+
throw new Error();
|
|
140
|
+
const unsupported = [];
|
|
141
|
+
for (const binding of manifest.bindings) {
|
|
142
|
+
const port = object(binding).port;
|
|
143
|
+
if (typeof port !== 'string' ||
|
|
144
|
+
!PLUGIN_BINDABLE_PORTS.some((supported) => supported === port))
|
|
145
|
+
unsupported.push(typeof port === 'string' && /^[a-z][a-z0-9_-]{0,63}$/.test(port)
|
|
146
|
+
? port
|
|
147
|
+
: 'unrecognized_requirement');
|
|
148
|
+
}
|
|
149
|
+
const ports = manifest.platformPorts;
|
|
150
|
+
if (ports && typeof ports === 'object') {
|
|
151
|
+
for (const [key, adapter] of Object.entries({
|
|
152
|
+
blob: 'r2',
|
|
153
|
+
cache: 'cache-api',
|
|
154
|
+
jobs: 'queues',
|
|
155
|
+
observability: 'workers',
|
|
156
|
+
})) {
|
|
157
|
+
const section = object(ports)[key];
|
|
158
|
+
if (section && object(section).adapter !== adapter)
|
|
159
|
+
unsupported.push(key);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (unsupported.length)
|
|
163
|
+
throw new UnsupportedRequirementsError(unsupported);
|
|
164
|
+
if (ports !== null &&
|
|
165
|
+
(ports === undefined || validatePlatformPorts({ cloudflare: ports }, 'cloudflare').length > 0))
|
|
166
|
+
throw new Error();
|
|
167
|
+
const selected = ports;
|
|
168
|
+
// Only allow build-info fields with their declared types; never echo arbitrary manifest data.
|
|
169
|
+
if (typeof info.core !== 'string' || !/^[a-zA-Z0-9.+-]{1,64}$/.test(info.core))
|
|
170
|
+
throw new Error();
|
|
171
|
+
const plugins = object(info.plugins);
|
|
172
|
+
for (const [key, value] of Object.entries(plugins))
|
|
173
|
+
if (!/^[@a-zA-Z0-9/_.-]{1,128}$/.test(key) ||
|
|
174
|
+
typeof value !== 'string' ||
|
|
175
|
+
!/^[a-zA-Z0-9.+-]{1,64}$/.test(value))
|
|
176
|
+
throw new Error();
|
|
177
|
+
if (info.lockfileSha256 !== null &&
|
|
178
|
+
(typeof info.lockfileSha256 !== 'string' || !/^[a-f0-9]{64}$/.test(info.lockfileSha256)))
|
|
179
|
+
throw new Error();
|
|
180
|
+
if (info.gitSha !== null &&
|
|
181
|
+
(typeof info.gitSha !== 'string' || !/^[a-f0-9]{40,64}$/.test(info.gitSha)))
|
|
182
|
+
throw new Error();
|
|
183
|
+
if (info.dirty !== undefined && info.dirty !== null && typeof info.dirty !== 'boolean')
|
|
184
|
+
throw new Error();
|
|
185
|
+
return {
|
|
186
|
+
required: {
|
|
187
|
+
r2: selected?.blob.adapter === 'r2',
|
|
188
|
+
queues: selected?.jobs.adapter === 'queues',
|
|
189
|
+
analytics: selected?.observability.analyticsBinding !== undefined,
|
|
190
|
+
},
|
|
191
|
+
ports: selected,
|
|
192
|
+
info: {
|
|
193
|
+
schemaVersion: 1,
|
|
194
|
+
target: 'cloudflare',
|
|
195
|
+
core: info.core,
|
|
196
|
+
plugins: plugins,
|
|
197
|
+
lockfileSha256: info.lockfileSha256,
|
|
198
|
+
gitSha: info.gitSha,
|
|
199
|
+
...(info.dirty === undefined ? {} : { dirty: info.dirty }),
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
if (error instanceof UnsupportedRequirementsError)
|
|
205
|
+
throw error;
|
|
206
|
+
throw new CloudflareError('invalid_input', 2);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function parseRemote(kind, raw) {
|
|
210
|
+
const record = object(raw);
|
|
211
|
+
const id = kind === 'd1' ? record.uuid : kind === 'r2' ? record.name : record.queue_id;
|
|
212
|
+
const name = kind === 'queue' ? record.queue_name : record.name;
|
|
213
|
+
if (!validResourceId(kind, id) ||
|
|
214
|
+
typeof name !== 'string' ||
|
|
215
|
+
!/^[a-z0-9][a-z0-9-]{0,62}$/.test(name))
|
|
216
|
+
throw new CloudflareError('provider_failure');
|
|
217
|
+
const jurisdiction = record.jurisdiction === 'eu' || record.jurisdiction === 'default' ? record.jurisdiction : null;
|
|
218
|
+
const timestamp = record.created_at ?? record.created_on;
|
|
219
|
+
return {
|
|
220
|
+
kind,
|
|
221
|
+
id,
|
|
222
|
+
name,
|
|
223
|
+
jurisdiction,
|
|
224
|
+
created_at: typeof timestamp === 'string' && Number.isFinite(Date.parse(timestamp)) ? timestamp : null,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
async function readWorker(api, identity) {
|
|
228
|
+
try {
|
|
229
|
+
const raw = object((await api.request(`/workers/scripts/${identity.script_name}/settings`)).result);
|
|
230
|
+
if (!Array.isArray(raw.tags) ||
|
|
231
|
+
raw.tags.some((tag) => typeof tag !== 'string') ||
|
|
232
|
+
!Array.isArray(raw.bindings))
|
|
233
|
+
throw new CloudflareError('capability_unknown');
|
|
234
|
+
const names = raw.bindings.map((binding) => object(binding).name);
|
|
235
|
+
if (new Set(names).size !== names.length)
|
|
236
|
+
throw new CloudflareError('configuration_conflict', 8);
|
|
237
|
+
return {
|
|
238
|
+
tags: raw.tags,
|
|
239
|
+
bindings: raw.bindings.map((b) => {
|
|
240
|
+
const binding = object(b);
|
|
241
|
+
if (typeof binding.type !== 'string' || typeof binding.name !== 'string')
|
|
242
|
+
throw new CloudflareError('capability_unknown');
|
|
243
|
+
return {
|
|
244
|
+
type: binding.type,
|
|
245
|
+
name: binding.name,
|
|
246
|
+
id: typeof binding.id === 'string' ? binding.id : null,
|
|
247
|
+
text: typeof binding.text === 'string' ? binding.text : null,
|
|
248
|
+
resource_name: typeof (binding.bucket_name ?? binding.queue_name ?? binding.dataset) === 'string'
|
|
249
|
+
? String(binding.bucket_name ?? binding.queue_name ?? binding.dataset)
|
|
250
|
+
: null,
|
|
251
|
+
};
|
|
252
|
+
}),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
if (error instanceof CloudflareError && error.status === 404)
|
|
257
|
+
return null;
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function ownedWorker(worker, identity) {
|
|
262
|
+
return (worker !== null &&
|
|
263
|
+
worker.tags.includes('pb-install-v1') &&
|
|
264
|
+
worker.tags.includes(`pb-owner-${identity.owner_digest}`));
|
|
265
|
+
}
|
|
266
|
+
function bindingOwns(worker, identity, spec, remote) {
|
|
267
|
+
if (!ownedWorker(worker, identity))
|
|
268
|
+
return false;
|
|
269
|
+
return worker.bindings.some((b) => spec.kind === 'd1'
|
|
270
|
+
? b.type === 'd1' && b.id === remote.id
|
|
271
|
+
: spec.kind === 'r2'
|
|
272
|
+
? b.type === 'r2_bucket' && b.resource_name === remote.name
|
|
273
|
+
: b.type === 'queue' && (b.id === remote.id || b.resource_name === remote.name));
|
|
274
|
+
}
|
|
275
|
+
function verify(spec, remote) {
|
|
276
|
+
if (spec.name !== remote.name || spec.kind !== remote.kind)
|
|
277
|
+
throw new CloudflareError('configuration_conflict', 8);
|
|
278
|
+
if (spec.jurisdiction !== null && spec.jurisdiction !== remote.jurisdiction)
|
|
279
|
+
throw new CloudflareError('jurisdiction_conflict', 8);
|
|
280
|
+
}
|
|
281
|
+
function pendingEvidence(contract, remote, op, now) {
|
|
282
|
+
return (remote.created_at !== null &&
|
|
283
|
+
Date.parse(remote.created_at) >= Date.parse(op.absence_observed_at) &&
|
|
284
|
+
Date.parse(remote.created_at) >= Date.parse(op.started_at) &&
|
|
285
|
+
Date.parse(remote.created_at) <= now &&
|
|
286
|
+
contract.pendingCreation(remote, op, now));
|
|
287
|
+
}
|
|
288
|
+
function specHash(spec) {
|
|
289
|
+
return digest(JSON.stringify({ kind: spec.kind, name: spec.name, jurisdiction: spec.jurisdiction }));
|
|
290
|
+
}
|
|
291
|
+
function receipt(remote, source, now, operationId) {
|
|
292
|
+
return {
|
|
293
|
+
name: remote.name,
|
|
294
|
+
id: remote.id,
|
|
295
|
+
...(remote.kind === 'd1' || remote.kind === 'r2' ? { jurisdiction: remote.jurisdiction } : {}),
|
|
296
|
+
state: 'complete',
|
|
297
|
+
evidence: { source, operation_id: operationId, verified_at: new Date(now).toISOString() },
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
async function allResources(api, specs, queueConsumers = new Map()) {
|
|
301
|
+
const result = new Map();
|
|
302
|
+
const lists = new Map();
|
|
303
|
+
for (const spec of specs) {
|
|
304
|
+
if (!lists.has(spec.kind))
|
|
305
|
+
lists.set(spec.kind, await api.list(kindPath(spec.kind), spec.kind === 'r2' ? 'buckets' : undefined, jurisdictionHeaders(spec)));
|
|
306
|
+
const matches = lists
|
|
307
|
+
.get(spec.kind)
|
|
308
|
+
.filter((raw) => (spec.kind === 'queue' ? raw.queue_name : raw.name) === spec.name);
|
|
309
|
+
if (matches.length > 1)
|
|
310
|
+
throw new CloudflareError('ownership_conflict', 8);
|
|
311
|
+
const resources = [];
|
|
312
|
+
for (const raw of matches) {
|
|
313
|
+
const listed = parseRemote(spec.kind, raw);
|
|
314
|
+
const detail = parseRemote(spec.kind, (await api.request(`${kindPath(spec.kind)}/${encodeURIComponent(listed.id)}`, 'GET', undefined, jurisdictionHeaders(spec))).result);
|
|
315
|
+
if (detail.id !== listed.id)
|
|
316
|
+
throw new CloudflareError('ownership_conflict', 8);
|
|
317
|
+
verify(spec, detail);
|
|
318
|
+
if (spec.kind === 'queue') {
|
|
319
|
+
const consumers = await api.snapshot(`/queues/${detail.id}/consumers`);
|
|
320
|
+
queueConsumers.set(detail.id, consumers);
|
|
321
|
+
const script = spec.name.slice(0, spec.key === 'dlq' ? -9 : -5);
|
|
322
|
+
if ((spec.key === 'dlq' && consumers.length > 0) ||
|
|
323
|
+
consumers.length > 1 ||
|
|
324
|
+
consumers.some((consumer) => consumer.type !== 'worker' ||
|
|
325
|
+
queueConsumerScript(consumer) !== script ||
|
|
326
|
+
(spec.key === 'jobs' &&
|
|
327
|
+
consumer.dead_letter_queue !== undefined &&
|
|
328
|
+
consumer.dead_letter_queue !== null &&
|
|
329
|
+
consumer.dead_letter_queue !== `${script}-jobs-dlq`)))
|
|
330
|
+
throw new CloudflareError('configuration_conflict', 8);
|
|
331
|
+
}
|
|
332
|
+
resources.push(detail);
|
|
333
|
+
}
|
|
334
|
+
result.set(spec.name, resources);
|
|
335
|
+
}
|
|
336
|
+
return result;
|
|
337
|
+
}
|
|
338
|
+
async function preflight(api, o, required, missingQueues, contract, identity, checks) {
|
|
339
|
+
// Optional identification has no bearing on authoritative service checks.
|
|
340
|
+
let planStatus = 'readable_unverified';
|
|
341
|
+
try {
|
|
342
|
+
await api.request('/subscriptions');
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
planStatus = 'unavailable_optional';
|
|
346
|
+
}
|
|
347
|
+
const planCheck = checks.find((check) => check.name === 'plan_identification');
|
|
348
|
+
if (planCheck)
|
|
349
|
+
planCheck.status = planStatus;
|
|
350
|
+
else
|
|
351
|
+
checks.push({ name: 'plan_identification', status: planStatus });
|
|
352
|
+
if (o.domain === undefined || o.gradualProbe === true) {
|
|
353
|
+
let subdomain;
|
|
354
|
+
try {
|
|
355
|
+
subdomain = object((await api.request('/workers/subdomain')).result).subdomain;
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
if (!(error instanceof CloudflareError && error.status === 404))
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
if (typeof subdomain !== 'string' || !/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(subdomain))
|
|
362
|
+
throw new CloudflareError('workers_dev_subdomain_missing');
|
|
363
|
+
}
|
|
364
|
+
const evidence = await contract.capabilities(api, required, identity);
|
|
365
|
+
if (required.queues) {
|
|
366
|
+
if (!evidence.queues || typeof evidence.queues.supported !== 'boolean')
|
|
367
|
+
throw new CloudflareError('capability_unknown', 1, false, 'provision', null, 'queues');
|
|
368
|
+
if (!evidence.queues.supported)
|
|
369
|
+
throw new CloudflareError('queues_unavailable', 1, false, 'provision', null, 'queues');
|
|
370
|
+
if (!Number.isSafeInteger(evidence.queues.remaining) || evidence.queues.remaining < 0)
|
|
371
|
+
throw new CloudflareError('capability_unknown', 1, false, 'provision', null, 'queues');
|
|
372
|
+
if (missingQueues > evidence.queues.remaining)
|
|
373
|
+
throw new CloudflareError('capacity_exceeded', 1, false, 'provision', null, 'queues');
|
|
374
|
+
}
|
|
375
|
+
if (required.r2) {
|
|
376
|
+
if (typeof evidence.r2?.supported !== 'boolean')
|
|
377
|
+
throw new CloudflareError('capability_unknown', 1, false, 'provision', null, 'r2');
|
|
378
|
+
if (!evidence.r2.supported)
|
|
379
|
+
throw new CloudflareError('r2_unavailable', 1, false, 'provision', null, 'r2');
|
|
380
|
+
}
|
|
381
|
+
if (required.analytics) {
|
|
382
|
+
if (typeof evidence.analytics?.supported !== 'boolean')
|
|
383
|
+
throw new CloudflareError('capability_unknown', 1, false, 'provision', null, 'analytics_engine');
|
|
384
|
+
if (evidence.analytics.source)
|
|
385
|
+
checks.push({
|
|
386
|
+
name: 'analytics_engine',
|
|
387
|
+
status: evidence.analytics.supported ? 'supported' : 'unsupported',
|
|
388
|
+
source: evidence.analytics.source,
|
|
389
|
+
});
|
|
390
|
+
if (!evidence.analytics.supported)
|
|
391
|
+
throw new CloudflareError('analytics_engine_unavailable', 1, false, 'provision', null, 'analytics_engine');
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
function checkAdoptions(values) {
|
|
395
|
+
const set = new Set();
|
|
396
|
+
for (const value of values) {
|
|
397
|
+
const [kind, id, extra] = value.split(':');
|
|
398
|
+
if (extra !== undefined ||
|
|
399
|
+
!kind ||
|
|
400
|
+
!id ||
|
|
401
|
+
(kind === 'worker'
|
|
402
|
+
? !/^[a-z0-9-]{1,37}$/.test(id)
|
|
403
|
+
: !['d1', 'r2', 'queue'].includes(kind) || !validResourceId(kind, id)) ||
|
|
404
|
+
set.has(value))
|
|
405
|
+
throw new CloudflareError('invalid_input', 2);
|
|
406
|
+
set.add(value);
|
|
407
|
+
}
|
|
408
|
+
return set;
|
|
409
|
+
}
|
|
410
|
+
async function confirmedGone(api, spec, id) {
|
|
411
|
+
const paths = spec.kind === 'r2' ? ['eu', 'default'] : [null];
|
|
412
|
+
for (const jurisdiction of paths) {
|
|
413
|
+
try {
|
|
414
|
+
await api.request(`${kindPath(spec.kind)}/${encodeURIComponent(id)}`, 'GET', undefined, jurisdiction === null ? undefined : { 'cf-r2-jurisdiction': jurisdiction });
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
if (!(error instanceof CloudflareError && error.status === 404))
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
function localFailure(error) {
|
|
425
|
+
const code = error?.code;
|
|
426
|
+
return !(error instanceof CloudflareError) &&
|
|
427
|
+
typeof code === 'string' &&
|
|
428
|
+
/^E[A-Z0-9]+$/.test(code)
|
|
429
|
+
? new CloudflareError('state_io_failure')
|
|
430
|
+
: error;
|
|
431
|
+
}
|
|
432
|
+
async function defaultPrompt(message) {
|
|
433
|
+
const prompt = createInterface({ input: process.stdin, output: process.stderr });
|
|
434
|
+
try {
|
|
435
|
+
return await prompt.question(message);
|
|
436
|
+
}
|
|
437
|
+
finally {
|
|
438
|
+
prompt.close();
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
export async function provision(o, operations = {}) {
|
|
442
|
+
let prebuiltOut;
|
|
443
|
+
const output = commandEnvelope('provision', o.dryRun ?? false);
|
|
444
|
+
let release;
|
|
445
|
+
let inventory;
|
|
446
|
+
const setOutputs = () => {
|
|
447
|
+
output.database_id = inventory?.resources.d1?.id ?? null;
|
|
448
|
+
output.r2_bucket = inventory?.resources.r2?.id ?? null;
|
|
449
|
+
output.queue_names = [inventory?.resources.jobs, inventory?.resources.dlq]
|
|
450
|
+
.filter((r) => r?.id !== null && r !== undefined)
|
|
451
|
+
.map((r) => r.name);
|
|
452
|
+
};
|
|
453
|
+
try {
|
|
454
|
+
if (o.prebuilt)
|
|
455
|
+
await assertPrebuiltConfiguration(resolve(o.project ?? dirname(resolve(o.configPath ?? 'broker.config.ts'))), o.configPath);
|
|
456
|
+
const auth = cloudflareCredentials(o.env);
|
|
457
|
+
const identity = installIdentity({
|
|
458
|
+
...auth,
|
|
459
|
+
workerName: o.workerName,
|
|
460
|
+
...(o.environment === undefined ? {} : { environment: o.environment }),
|
|
461
|
+
...(o.deploymentId === undefined ? {} : { deploymentId: o.deploymentId }),
|
|
462
|
+
...(o.runIdentity === undefined ? {} : { runIdentity: o.runIdentity }),
|
|
463
|
+
...(o.jurisdiction === undefined ? {} : { jurisdiction: o.jurisdiction }),
|
|
464
|
+
...(o.locationHint === undefined ? {} : { locationHint: o.locationHint }),
|
|
465
|
+
});
|
|
466
|
+
if (o.domain !== undefined &&
|
|
467
|
+
!/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,63}$/.test(o.domain))
|
|
468
|
+
throw new CloudflareError('invalid_input', 2);
|
|
469
|
+
const adoptions = checkAdoptions(o.adoptResource ?? []);
|
|
470
|
+
const project = resolve(o.project ?? dirname(resolve(o.configPath ?? 'broker.config.ts')));
|
|
471
|
+
let outDir = o.prebuilt
|
|
472
|
+
? resolve(project, o.outDir ?? '.pb/prebuilt/cloudflare')
|
|
473
|
+
: resolve(o.outDir ?? '.pb/cloudflare');
|
|
474
|
+
if (o.prebuilt) {
|
|
475
|
+
const materialized = await (operations.materializePrebuilt ?? materializePrebuilt)({
|
|
476
|
+
project,
|
|
477
|
+
outDir,
|
|
478
|
+
});
|
|
479
|
+
outDir = prebuiltOut = materialized.outDir;
|
|
480
|
+
output.prebuilt = prebuiltEnvelope(materialized);
|
|
481
|
+
output.artifact_sha256 = output.prebuilt.artifact_sha256;
|
|
482
|
+
}
|
|
483
|
+
const store = await InventoryStore.create({
|
|
484
|
+
project,
|
|
485
|
+
outDir,
|
|
486
|
+
identity,
|
|
487
|
+
...(o.inventory === undefined ? {} : { inventory: o.inventory }),
|
|
488
|
+
});
|
|
489
|
+
const build = await selectedBuild(outDir);
|
|
490
|
+
output.build_info = build.info;
|
|
491
|
+
const now = o.now ?? Date.now;
|
|
492
|
+
// After confirmation: inventory + capacity, then absence + read-back for each of two queues.
|
|
493
|
+
// Each scan can take 100 pages at the client's one-request-per-second rate.
|
|
494
|
+
const provisionBudgetMs = 600_000 + (build.required.queues ? 6 * Math.ceil(QUEUE_ACCOUNT_LIMIT / 100) * 1000 : 0);
|
|
495
|
+
const api = new CloudflareApi({
|
|
496
|
+
...auth,
|
|
497
|
+
...(o.fetch === undefined ? {} : { fetch: o.fetch }),
|
|
498
|
+
now,
|
|
499
|
+
...(o.sleep === undefined ? {} : { sleep: o.sleep }),
|
|
500
|
+
});
|
|
501
|
+
api.restartDeadline(provisionBudgetMs);
|
|
502
|
+
const contract = o.contract ?? cloudflareProviderContract;
|
|
503
|
+
if (o.transaction)
|
|
504
|
+
o.transaction.assertStore(store);
|
|
505
|
+
else if (!o.dryRun)
|
|
506
|
+
release = await store.lock(now());
|
|
507
|
+
inventory = (await store.read()) ?? emptyInventory(identity, now());
|
|
508
|
+
const journal = inventory;
|
|
509
|
+
if (journal.identity.location_hint !== identity.location_hint &&
|
|
510
|
+
Object.keys(journal.resources).length)
|
|
511
|
+
output.checks.push({ name: 'location_hint', status: 'ignored_for_existing_resources' });
|
|
512
|
+
const save = async () => {
|
|
513
|
+
journal.revision++;
|
|
514
|
+
journal.updated_at = new Date(now()).toISOString();
|
|
515
|
+
await store.write(journal);
|
|
516
|
+
};
|
|
517
|
+
const specs = specsFor(identity, build.required);
|
|
518
|
+
for (const key of Object.keys(journal.resources)) {
|
|
519
|
+
if (key !== 'analytics' && key !== 'worker' && !specs.some((spec) => spec.key === key))
|
|
520
|
+
throw new CloudflareError('configuration_conflict', 8);
|
|
521
|
+
if (key === 'analytics' && !build.required.analytics)
|
|
522
|
+
throw new CloudflareError('configuration_conflict', 8);
|
|
523
|
+
}
|
|
524
|
+
const inspect = async () => {
|
|
525
|
+
const queueConsumers = new Map();
|
|
526
|
+
const resources = await allResources(api, specs, queueConsumers);
|
|
527
|
+
const worker = await readWorker(api, identity);
|
|
528
|
+
if (worker &&
|
|
529
|
+
!ownedWorker(worker, identity) &&
|
|
530
|
+
!(journal.resources.worker?.id === identity.script_name &&
|
|
531
|
+
journal.resources.worker.evidence?.source === 'adopted') &&
|
|
532
|
+
!adoptions.has(`worker:${identity.script_name}`))
|
|
533
|
+
throw new CloudflareError('ownership_conflict', 8);
|
|
534
|
+
if (worker &&
|
|
535
|
+
worker.tags.some((tag) => tag.startsWith('pb-owner-') && tag !== `pb-owner-${identity.owner_digest}`))
|
|
536
|
+
throw new CloudflareError('ownership_conflict', 8);
|
|
537
|
+
// A foreign binding is not overridden by adoption or a local receipt.
|
|
538
|
+
if (worker) {
|
|
539
|
+
const expectedBindings = [
|
|
540
|
+
{ name: 'DB', type: 'd1' },
|
|
541
|
+
...(build.ports
|
|
542
|
+
? [
|
|
543
|
+
{ name: build.ports.blob.binding, type: 'r2_bucket' },
|
|
544
|
+
{ name: build.ports.jobs.binding, type: 'queue' },
|
|
545
|
+
]
|
|
546
|
+
: []),
|
|
547
|
+
];
|
|
548
|
+
for (const binding of expectedBindings) {
|
|
549
|
+
const b = worker.bindings.find((b) => b.name === binding.name);
|
|
550
|
+
if (b && b.type !== binding.type)
|
|
551
|
+
throw new CloudflareError('configuration_conflict', 8);
|
|
552
|
+
const spec = specs.find((s) => binding.type === 'd1'
|
|
553
|
+
? s.key === 'd1'
|
|
554
|
+
: binding.type === 'r2_bucket'
|
|
555
|
+
? s.key === 'r2'
|
|
556
|
+
: s.key === 'jobs');
|
|
557
|
+
const found = spec ? resources.get(spec.name)?.[0] : undefined;
|
|
558
|
+
if (b && !found)
|
|
559
|
+
throw new CloudflareError('configuration_conflict', 8);
|
|
560
|
+
if (b &&
|
|
561
|
+
found &&
|
|
562
|
+
(binding.type === 'd1' ? b.id !== found.id : b.resource_name !== found.name))
|
|
563
|
+
throw new CloudflareError('ownership_conflict', 8);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (worker) {
|
|
567
|
+
const deployment = worker.bindings.find((binding) => binding.name === 'DEPLOYMENT_ID');
|
|
568
|
+
if (deployment &&
|
|
569
|
+
(deployment.type !== 'plain_text' || deployment.text !== identity.deployment_id))
|
|
570
|
+
throw new CloudflareError('configuration_conflict', 8);
|
|
571
|
+
if (build.required.analytics) {
|
|
572
|
+
const analytics = worker.bindings.find((binding) => binding.name === build.ports?.observability.analyticsBinding);
|
|
573
|
+
if (analytics &&
|
|
574
|
+
(analytics.type !== 'analytics_engine' ||
|
|
575
|
+
analytics.resource_name !== `${identity.script_name.replaceAll('-', '_')}_metrics`))
|
|
576
|
+
throw new CloudflareError('configuration_conflict', 8);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
const resourceOwned = (spec, remote) => {
|
|
580
|
+
if (bindingOwns(worker, identity, spec, remote))
|
|
581
|
+
return true;
|
|
582
|
+
if (spec.key !== 'dlq' || !ownedWorker(worker, identity))
|
|
583
|
+
return false;
|
|
584
|
+
const jobs = resources.get(`${identity.script_name}-jobs`)?.[0];
|
|
585
|
+
if (!jobs)
|
|
586
|
+
return false;
|
|
587
|
+
const consumers = queueConsumers.get(jobs.id) ?? [];
|
|
588
|
+
return (worker.bindings.some((binding) => binding.name === build.ports?.jobs.binding &&
|
|
589
|
+
binding.type === 'queue' &&
|
|
590
|
+
(binding.id === jobs.id || binding.resource_name === jobs.name)) &&
|
|
591
|
+
consumers.length === 1 &&
|
|
592
|
+
consumers[0].type === 'worker' &&
|
|
593
|
+
queueConsumerScript(consumers[0]) === identity.script_name &&
|
|
594
|
+
consumers[0].dead_letter_queue === remote.name);
|
|
595
|
+
};
|
|
596
|
+
for (const spec of specs) {
|
|
597
|
+
const remote = resources.get(spec.name)?.[0];
|
|
598
|
+
const recorded = journal.resources[spec.key];
|
|
599
|
+
const pending = journal.operations.find((op) => op.name === spec.name && (op.state === 'pending' || op.state === 'unknown'));
|
|
600
|
+
if (pending && pending.spec_sha256 !== specHash(spec))
|
|
601
|
+
throw new CloudflareError('configuration_conflict', 8);
|
|
602
|
+
if (recorded?.state === 'complete' && (!remote || remote.id !== recorded.id))
|
|
603
|
+
throw new CloudflareError('ownership_conflict', 8);
|
|
604
|
+
if (!remote)
|
|
605
|
+
continue;
|
|
606
|
+
const adopted = adoptions.has(`${spec.kind}:${remote.id}`);
|
|
607
|
+
if (pending &&
|
|
608
|
+
!adopted &&
|
|
609
|
+
!(recorded?.id === remote.id &&
|
|
610
|
+
recorded.evidence?.source === 'created' &&
|
|
611
|
+
recorded.evidence.operation_id === pending.id) &&
|
|
612
|
+
!pendingEvidence(contract, remote, pending, now()) &&
|
|
613
|
+
!resourceOwned(spec, remote))
|
|
614
|
+
throw new CloudflareError('ownership_conflict', 8);
|
|
615
|
+
if (!pending && recorded?.state !== 'complete' && !adopted && !resourceOwned(spec, remote))
|
|
616
|
+
throw new CloudflareError('ownership_conflict', 8);
|
|
617
|
+
}
|
|
618
|
+
const missing = specs.filter((spec) => !resources.get(spec.name)?.length);
|
|
619
|
+
await preflight(api, o, build.required, missing.filter((spec) => spec.kind === 'queue').length, contract, identity, output.checks);
|
|
620
|
+
return { resources, worker, missing, queueConsumers };
|
|
621
|
+
};
|
|
622
|
+
let state = await inspect();
|
|
623
|
+
if (o.abandonIntent !== undefined) {
|
|
624
|
+
const target = journal.operations.find((op) => op.id === o.abandonIntent &&
|
|
625
|
+
(op.state === 'pending' || op.state === 'unknown') &&
|
|
626
|
+
specs.some((spec) => spec.name === op.name));
|
|
627
|
+
if (!target)
|
|
628
|
+
throw new CloudflareError('invalid_input', 2);
|
|
629
|
+
output.checks.push({ name: 'abandon_intent', status: target.id });
|
|
630
|
+
}
|
|
631
|
+
for (const flag of adoptions) {
|
|
632
|
+
const [kind, id] = flag.split(':');
|
|
633
|
+
if (kind === 'worker'
|
|
634
|
+
? state.worker === null || id !== identity.script_name
|
|
635
|
+
: !specs.some((spec) => spec.kind === kind))
|
|
636
|
+
throw new CloudflareError('invalid_input', 2);
|
|
637
|
+
}
|
|
638
|
+
const abandoned = [];
|
|
639
|
+
// Resolve every old intent before planning additional creates. Read-only runs never repair the journal.
|
|
640
|
+
for (const spec of specs) {
|
|
641
|
+
const pending = journal.operations.find((op) => op.name === spec.name && ['pending', 'unknown'].includes(op.state));
|
|
642
|
+
if (!pending)
|
|
643
|
+
continue;
|
|
644
|
+
const remote = state.resources.get(spec.name)?.[0];
|
|
645
|
+
const abandoning = pending.id === o.abandonIntent;
|
|
646
|
+
if (abandoning && remote)
|
|
647
|
+
throw new CloudflareError('ownership_conflict', 8);
|
|
648
|
+
if (!remote) {
|
|
649
|
+
if (abandoning) {
|
|
650
|
+
if (now() - Date.parse(pending.started_at) <= CREATE_SETTLE_MS)
|
|
651
|
+
throw new CloudflareError('outcome_unknown', 6);
|
|
652
|
+
if (pending.resolved_id !== null &&
|
|
653
|
+
!(await confirmedGone(api, spec, pending.resolved_id)))
|
|
654
|
+
throw new CloudflareError('ownership_conflict', 8);
|
|
655
|
+
abandoned.push({ operation: pending, key: spec.key });
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
output.checks.push({ name: 'pending_intent', status: pending.id });
|
|
659
|
+
if (!o.dryRun) {
|
|
660
|
+
pending.state = 'unknown';
|
|
661
|
+
await save();
|
|
662
|
+
}
|
|
663
|
+
throw new CloudflareError('outcome_unknown', 6);
|
|
664
|
+
}
|
|
665
|
+
if (!o.dryRun) {
|
|
666
|
+
const recorded = journal.resources[spec.key];
|
|
667
|
+
pending.state = 'confirmed';
|
|
668
|
+
pending.resolved_id = remote.id;
|
|
669
|
+
journal.resources[spec.key] = receipt(remote, recorded?.state === 'complete' && recorded.id === remote.id && recorded.evidence
|
|
670
|
+
? recorded.evidence.source
|
|
671
|
+
: adoptions.has(`${spec.kind}:${remote.id}`)
|
|
672
|
+
? 'adopted'
|
|
673
|
+
: 'pending_create', now(), pending.id);
|
|
674
|
+
await save();
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
const plan = () => specs.map((spec) => ({
|
|
678
|
+
stage: 'provision',
|
|
679
|
+
action: state.missing.some((s) => s.key === spec.key) ? 'create' : 'reuse',
|
|
680
|
+
kind: spec.kind,
|
|
681
|
+
name: spec.name,
|
|
682
|
+
...(spec.jurisdiction === null ? {} : { jurisdiction: spec.jurisdiction }),
|
|
683
|
+
}));
|
|
684
|
+
output.plan = plan();
|
|
685
|
+
if (build.required.analytics)
|
|
686
|
+
output.plan.push({
|
|
687
|
+
stage: 'provision',
|
|
688
|
+
action: 'binding_pending',
|
|
689
|
+
kind: 'analytics',
|
|
690
|
+
name: `${identity.script_name.replaceAll('-', '_')}_metrics`,
|
|
691
|
+
});
|
|
692
|
+
o.progress?.(`${JSON.stringify(output.plan)}\nAccount: [redacted]. Resources are retained; no deletes. Lost responses require durable private inventory or exact-ID adoption.`);
|
|
693
|
+
if (state.missing.length && !o.dryRun) {
|
|
694
|
+
if (!o.yes) {
|
|
695
|
+
if (o.json || !(o.interactive ?? (process.stdin.isTTY && process.stderr.isTTY)))
|
|
696
|
+
throw new CloudflareError('confirmation_required', 2);
|
|
697
|
+
if ((await (o.prompt ?? defaultPrompt)('Type yes to create these resources: ')) !== 'yes')
|
|
698
|
+
throw new CloudflareError('confirmation_declined', 2);
|
|
699
|
+
}
|
|
700
|
+
api.restartDeadline(provisionBudgetMs);
|
|
701
|
+
const signature = JSON.stringify({
|
|
702
|
+
resources: [...state.resources.entries()],
|
|
703
|
+
worker: state.worker,
|
|
704
|
+
queueConsumers: [...state.queueConsumers.entries()],
|
|
705
|
+
});
|
|
706
|
+
const confirmed = await inspect();
|
|
707
|
+
let confirmedBuild;
|
|
708
|
+
try {
|
|
709
|
+
confirmedBuild = await selectedBuild(outDir);
|
|
710
|
+
}
|
|
711
|
+
catch {
|
|
712
|
+
throw new CloudflareError('plan_changed', 8);
|
|
713
|
+
}
|
|
714
|
+
if (JSON.stringify(confirmedBuild) !== JSON.stringify(build) ||
|
|
715
|
+
JSON.stringify({
|
|
716
|
+
resources: [...confirmed.resources.entries()],
|
|
717
|
+
worker: confirmed.worker,
|
|
718
|
+
queueConsumers: [...confirmed.queueConsumers.entries()],
|
|
719
|
+
}) !== signature)
|
|
720
|
+
throw new CloudflareError('plan_changed', 8);
|
|
721
|
+
state = confirmed;
|
|
722
|
+
}
|
|
723
|
+
const resolvedIds = [...state.resources.values()].flat();
|
|
724
|
+
for (const flag of adoptions) {
|
|
725
|
+
const [kind, id] = flag.split(':');
|
|
726
|
+
const matched = kind === 'worker'
|
|
727
|
+
? state.worker !== null && id === identity.script_name
|
|
728
|
+
: resolvedIds.some((remote) => remote.kind === kind && remote.id === id);
|
|
729
|
+
if (!matched)
|
|
730
|
+
throw new CloudflareError('invalid_input', 2);
|
|
731
|
+
}
|
|
732
|
+
if (!o.dryRun && abandoned.length) {
|
|
733
|
+
for (const { operation, key } of abandoned) {
|
|
734
|
+
operation.state = 'failed';
|
|
735
|
+
const intended = journal.resources[key];
|
|
736
|
+
if (intended && intended.state !== 'complete')
|
|
737
|
+
intended.state = 'planned';
|
|
738
|
+
}
|
|
739
|
+
await save();
|
|
740
|
+
}
|
|
741
|
+
if (!o.dryRun && state.worker && adoptions.has(`worker:${identity.script_name}`)) {
|
|
742
|
+
journal.resources.worker = {
|
|
743
|
+
name: identity.script_name,
|
|
744
|
+
id: identity.script_name,
|
|
745
|
+
state: 'complete',
|
|
746
|
+
evidence: {
|
|
747
|
+
source: 'adopted',
|
|
748
|
+
operation_id: null,
|
|
749
|
+
verified_at: new Date(now()).toISOString(),
|
|
750
|
+
},
|
|
751
|
+
};
|
|
752
|
+
await save();
|
|
753
|
+
}
|
|
754
|
+
for (const spec of specs) {
|
|
755
|
+
let remote = state.resources.get(spec.name)?.[0];
|
|
756
|
+
if (remote) {
|
|
757
|
+
if (!o.dryRun && journal.resources[spec.key]?.state !== 'complete') {
|
|
758
|
+
journal.resources[spec.key] = receipt(remote, adoptions.has(`${spec.kind}:${remote.id}`) ? 'adopted' : 'worker_binding', now(), null);
|
|
759
|
+
await save();
|
|
760
|
+
}
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
if (o.dryRun)
|
|
764
|
+
continue;
|
|
765
|
+
// Observe exact absence at this boundary, after the acknowledged plan was checked.
|
|
766
|
+
if ((await allResources(api, [spec])).get(spec.name)?.length)
|
|
767
|
+
throw new CloudflareError('plan_changed', 8);
|
|
768
|
+
api.assertCreateBudget();
|
|
769
|
+
const started = new Date(now()).toISOString();
|
|
770
|
+
const operation = {
|
|
771
|
+
id: randomUUID(),
|
|
772
|
+
kind: spec.kind,
|
|
773
|
+
name: spec.name,
|
|
774
|
+
spec_sha256: specHash(spec),
|
|
775
|
+
absence_observed_at: started,
|
|
776
|
+
started_at: started,
|
|
777
|
+
state: 'pending',
|
|
778
|
+
resolved_id: null,
|
|
779
|
+
};
|
|
780
|
+
journal.operations.push(operation);
|
|
781
|
+
journal.resources[spec.key] = {
|
|
782
|
+
name: spec.name,
|
|
783
|
+
id: null,
|
|
784
|
+
...(spec.jurisdiction === null ? {} : { jurisdiction: spec.jurisdiction }),
|
|
785
|
+
state: 'creating',
|
|
786
|
+
evidence: null,
|
|
787
|
+
};
|
|
788
|
+
await save();
|
|
789
|
+
let returnedId;
|
|
790
|
+
let posted = false;
|
|
791
|
+
try {
|
|
792
|
+
const body = spec.kind === 'queue'
|
|
793
|
+
? {
|
|
794
|
+
queue_name: spec.name,
|
|
795
|
+
...(identity.jurisdiction === 'default'
|
|
796
|
+
? {}
|
|
797
|
+
: { jurisdiction: identity.jurisdiction }),
|
|
798
|
+
}
|
|
799
|
+
: {
|
|
800
|
+
name: spec.name,
|
|
801
|
+
...(spec.kind === 'd1' && spec.jurisdiction !== 'default'
|
|
802
|
+
? { jurisdiction: spec.jurisdiction }
|
|
803
|
+
: {}),
|
|
804
|
+
...(identity.location_hint === null
|
|
805
|
+
? {}
|
|
806
|
+
: spec.kind === 'd1'
|
|
807
|
+
? { primary_location_hint: identity.location_hint }
|
|
808
|
+
: { locationHint: identity.location_hint }),
|
|
809
|
+
};
|
|
810
|
+
const response = await api.request(kindPath(spec.kind), 'POST', body, jurisdictionHeaders(spec));
|
|
811
|
+
posted = true;
|
|
812
|
+
const created = parseRemote(spec.kind, response.result);
|
|
813
|
+
returnedId = created.id;
|
|
814
|
+
operation.resolved_id = created.id;
|
|
815
|
+
await save();
|
|
816
|
+
verify(spec, created);
|
|
817
|
+
journal.resources[spec.key] = receipt(created, 'created', now(), operation.id);
|
|
818
|
+
await save();
|
|
819
|
+
remote = (await allResources(api, [spec])).get(spec.name)?.[0];
|
|
820
|
+
if (!remote || remote.id !== returnedId)
|
|
821
|
+
throw new CloudflareError('outcome_unknown', 6);
|
|
822
|
+
}
|
|
823
|
+
catch (error) {
|
|
824
|
+
if (!(error instanceof CloudflareError))
|
|
825
|
+
throw error;
|
|
826
|
+
if (error.code === 'deadline_exceeded' && !posted) {
|
|
827
|
+
operation.state = 'failed';
|
|
828
|
+
const intended = journal.resources[spec.key];
|
|
829
|
+
if (intended && intended.state !== 'complete')
|
|
830
|
+
intended.state = 'planned';
|
|
831
|
+
await save();
|
|
832
|
+
throw error;
|
|
833
|
+
}
|
|
834
|
+
if (error.code === 'jurisdiction_conflict' || error.code === 'configuration_conflict') {
|
|
835
|
+
operation.state = 'unknown';
|
|
836
|
+
await save();
|
|
837
|
+
throw error;
|
|
838
|
+
}
|
|
839
|
+
if (!posted &&
|
|
840
|
+
error.status !== null &&
|
|
841
|
+
error.status >= 400 &&
|
|
842
|
+
error.status < 500 &&
|
|
843
|
+
![408, 409, 429].includes(error.status)) {
|
|
844
|
+
operation.state = 'failed';
|
|
845
|
+
await save();
|
|
846
|
+
throw error;
|
|
847
|
+
}
|
|
848
|
+
// Never resend a POST: even 409 and truncated successes require exact reconciliation.
|
|
849
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
850
|
+
try {
|
|
851
|
+
await api.pause(1000 * 2 ** attempt);
|
|
852
|
+
remote = (await allResources(api, [spec])).get(spec.name)?.[0];
|
|
853
|
+
}
|
|
854
|
+
catch (readError) {
|
|
855
|
+
if (readError instanceof CloudflareError &&
|
|
856
|
+
['ownership_conflict', 'jurisdiction_conflict'].includes(readError.code))
|
|
857
|
+
throw readError;
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
if (remote)
|
|
861
|
+
break;
|
|
862
|
+
}
|
|
863
|
+
if (!remote ||
|
|
864
|
+
(returnedId !== undefined
|
|
865
|
+
? returnedId !== remote.id
|
|
866
|
+
: !pendingEvidence(contract, remote, operation, now()))) {
|
|
867
|
+
operation.state = 'unknown';
|
|
868
|
+
await save();
|
|
869
|
+
throw new CloudflareError(remote ? 'ownership_conflict' : 'outcome_unknown', remote ? 8 : 6);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
verify(spec, remote);
|
|
873
|
+
operation.state = 'confirmed';
|
|
874
|
+
operation.resolved_id = remote.id;
|
|
875
|
+
journal.resources[spec.key] = receipt(remote, returnedId === remote.id ? 'created' : 'pending_create', now(), operation.id);
|
|
876
|
+
await save();
|
|
877
|
+
}
|
|
878
|
+
const bucket = journal.resources.r2;
|
|
879
|
+
if (!o.dryRun &&
|
|
880
|
+
bucket?.id &&
|
|
881
|
+
bucket.state === 'complete' &&
|
|
882
|
+
bucket.lifecycle_baseline === undefined &&
|
|
883
|
+
bucket.evidence &&
|
|
884
|
+
['created', 'pending_create'].includes(bucket.evidence.source)) {
|
|
885
|
+
let baseline;
|
|
886
|
+
try {
|
|
887
|
+
const live = object((await api.request(`/r2/buckets/${encodeURIComponent(bucket.id)}/lifecycle`, 'GET', undefined, { 'cf-r2-jurisdiction': identity.jurisdiction })).result).rules;
|
|
888
|
+
if (Array.isArray(live) && live.length <= 16 && live.every(lifecycleRule))
|
|
889
|
+
baseline = live.map(abortOnlyRule);
|
|
890
|
+
}
|
|
891
|
+
catch {
|
|
892
|
+
baseline = undefined;
|
|
893
|
+
}
|
|
894
|
+
if (baseline) {
|
|
895
|
+
bucket.lifecycle_baseline = baseline;
|
|
896
|
+
await save();
|
|
897
|
+
}
|
|
898
|
+
else
|
|
899
|
+
output.checks.push({ name: 'lifecycle_baseline', status: 'unrecorded' });
|
|
900
|
+
}
|
|
901
|
+
if (!o.dryRun && build.required.analytics && journal.resources.analytics === undefined) {
|
|
902
|
+
journal.resources.analytics = {
|
|
903
|
+
name: `${identity.script_name.replaceAll('-', '_')}_metrics`,
|
|
904
|
+
id: null,
|
|
905
|
+
state: 'binding_pending',
|
|
906
|
+
evidence: null,
|
|
907
|
+
};
|
|
908
|
+
await save();
|
|
909
|
+
}
|
|
910
|
+
// Dry-run exposes resolved non-secret IDs without writing adoption receipts.
|
|
911
|
+
if (o.dryRun)
|
|
912
|
+
for (const spec of specs) {
|
|
913
|
+
const remote = state.resources.get(spec.name)?.[0];
|
|
914
|
+
if (remote)
|
|
915
|
+
journal.resources[spec.key] = receipt(remote, 'worker_binding', now(), null);
|
|
916
|
+
}
|
|
917
|
+
if (o.domain)
|
|
918
|
+
output.worker_url = `https://${o.domain}`;
|
|
919
|
+
output.status = o.dryRun ? 'planned' : 'ok';
|
|
920
|
+
output.checks.push({
|
|
921
|
+
name: 'provider_semantics',
|
|
922
|
+
status: contract === cloudflareProviderContract ? 'documented' : 'custom',
|
|
923
|
+
});
|
|
924
|
+
setOutputs();
|
|
925
|
+
await release?.();
|
|
926
|
+
release = undefined;
|
|
927
|
+
return { exitCode: 0, output };
|
|
928
|
+
}
|
|
929
|
+
catch (error) {
|
|
930
|
+
if (error instanceof UnsupportedRequirementsError)
|
|
931
|
+
output.checks.push(...error.requirements.map((name) => ({ name, status: 'unsupported' })));
|
|
932
|
+
setOutputs();
|
|
933
|
+
if (release) {
|
|
934
|
+
try {
|
|
935
|
+
await release();
|
|
936
|
+
}
|
|
937
|
+
catch {
|
|
938
|
+
/* Preserve the original safe failure and journal. */
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
return failCommand(output, localFailure(error));
|
|
942
|
+
}
|
|
943
|
+
finally {
|
|
944
|
+
if (prebuiltOut)
|
|
945
|
+
await removePrebuilt(prebuiltOut).catch(() => undefined);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
export async function runCloudflareProvision(o) {
|
|
949
|
+
const result = await provision({
|
|
950
|
+
...o,
|
|
951
|
+
progress: o.progress ?? ((message) => console.error(message)),
|
|
952
|
+
});
|
|
953
|
+
if (o.json)
|
|
954
|
+
console.log(JSON.stringify(result.output));
|
|
955
|
+
else if (result.output.error)
|
|
956
|
+
console.error(`${result.output.error.code}: ${result.output.error.message}`);
|
|
957
|
+
else
|
|
958
|
+
console.log(JSON.stringify(result.output));
|
|
959
|
+
return result.exitCode;
|
|
960
|
+
}
|
|
961
|
+
//# sourceMappingURL=provision.js.map
|