anhedral 0.8.5 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +140 -34
  2. package/assets/anhedral-cli-init-technical.svg +8 -8
  3. package/assets/anhedral-cli-init.svg +9 -9
  4. package/assets/security/LICENSES.txt +63 -0
  5. package/assets/security/audit-deps.mjs +295 -0
  6. package/assets/security/braces-regression.mjs +102 -0
  7. package/assets/security/braces@3.0.3.patch +285 -0
  8. package/assets/security/decode-uri-component@0.2.2.patch +149 -0
  9. package/assets/security/decoder-regression.mjs +37 -0
  10. package/assets/security/forge-regression.mjs +46 -0
  11. package/assets/security/manifest.json +53 -0
  12. package/assets/security/node-forge@1.4.0.patch +26 -0
  13. package/dist/access.d.ts +10 -0
  14. package/dist/access.js +93 -0
  15. package/dist/auth-schema-snapshot.d.ts +346 -0
  16. package/dist/auth-schema-snapshot.js +347 -0
  17. package/dist/auth-schema.d.ts +1 -0
  18. package/dist/auth-schema.js +57 -0
  19. package/dist/bin.js +353 -10
  20. package/dist/bounded-file.d.ts +2 -0
  21. package/dist/bounded-file.js +30 -0
  22. package/dist/capabilities.d.ts +19 -33
  23. package/dist/capabilities.js +73 -71
  24. package/dist/cloudflare.d.ts +14 -0
  25. package/dist/cloudflare.js +66 -0
  26. package/dist/command.js +12 -3
  27. package/dist/delivery.d.ts +118 -0
  28. package/dist/delivery.js +611 -0
  29. package/dist/dependencies.d.ts +22 -0
  30. package/dist/dependencies.js +109 -67
  31. package/dist/dependency-order.d.ts +2 -0
  32. package/dist/dependency-order.js +24 -0
  33. package/dist/index.d.ts +18 -1
  34. package/dist/index.js +12 -1
  35. package/dist/managed-hosting.d.ts +49 -0
  36. package/dist/managed-hosting.js +373 -0
  37. package/dist/management.d.ts +3 -0
  38. package/dist/management.js +47 -0
  39. package/dist/platforms/desktop.d.ts +2 -2
  40. package/dist/platforms/desktop.js +22 -12
  41. package/dist/platforms/extension.d.ts +2 -2
  42. package/dist/platforms/extension.js +12 -4
  43. package/dist/platforms/mobile.d.ts +2 -2
  44. package/dist/platforms/mobile.js +13 -7
  45. package/dist/pnpm-policy.d.ts +4 -0
  46. package/dist/pnpm-policy.js +66 -0
  47. package/dist/project-state.d.ts +28 -0
  48. package/dist/project-state.js +226 -0
  49. package/dist/provider-readiness.d.ts +30 -0
  50. package/dist/provider-readiness.js +87 -0
  51. package/dist/provision.d.ts +46 -0
  52. package/dist/provision.js +197 -0
  53. package/dist/readiness.d.ts +4 -1
  54. package/dist/readiness.js +80 -8
  55. package/dist/registry/add.d.ts +22 -0
  56. package/dist/registry/add.js +198 -0
  57. package/dist/registry/bootstrap.d.ts +2 -0
  58. package/dist/registry/bootstrap.js +14 -0
  59. package/dist/registry/commands.d.ts +2 -0
  60. package/dist/registry/commands.js +99 -0
  61. package/dist/registry/context.d.ts +3 -0
  62. package/dist/registry/context.js +21 -0
  63. package/dist/registry/options.d.ts +2 -0
  64. package/dist/registry/options.js +65 -0
  65. package/dist/registry/receipt.d.ts +3 -0
  66. package/dist/registry/receipt.js +73 -0
  67. package/dist/registry/remote.d.ts +25 -0
  68. package/dist/registry/remote.js +168 -0
  69. package/dist/registry/resolve.d.ts +10 -0
  70. package/dist/registry/resolve.js +98 -0
  71. package/dist/registry/types.d.ts +64 -0
  72. package/dist/registry/usage.d.ts +1 -0
  73. package/dist/registry/usage.js +38 -0
  74. package/dist/render.js +3 -3
  75. package/dist/repository.d.ts +24 -0
  76. package/dist/repository.js +96 -0
  77. package/dist/scaffold/api.d.ts +2 -0
  78. package/dist/scaffold/api.js +13 -0
  79. package/dist/scaffold/auth.d.ts +3 -0
  80. package/dist/scaffold/auth.js +95 -0
  81. package/dist/scaffold/bootstrap.d.ts +2 -0
  82. package/dist/scaffold/bootstrap.js +34 -0
  83. package/dist/scaffold/compose.d.ts +4 -0
  84. package/dist/scaffold/compose.js +34 -0
  85. package/dist/scaffold/database.d.ts +2 -0
  86. package/dist/scaffold/database.js +43 -0
  87. package/dist/scaffold/documentation.d.ts +3 -0
  88. package/dist/scaffold/documentation.js +91 -0
  89. package/dist/scaffold/environment.d.ts +2 -0
  90. package/dist/scaffold/environment.js +5 -0
  91. package/dist/scaffold/files.d.ts +25 -0
  92. package/dist/scaffold/files.js +108 -0
  93. package/dist/scaffold/integrations.d.ts +3 -0
  94. package/dist/scaffold/integrations.js +7 -0
  95. package/dist/scaffold/services.d.ts +5 -0
  96. package/dist/scaffold/services.js +84 -0
  97. package/dist/scaffold/web.d.ts +2 -0
  98. package/dist/scaffold/web.js +102 -0
  99. package/dist/scaffold/workspace.d.ts +3 -0
  100. package/dist/scaffold/workspace.js +150 -0
  101. package/dist/security-backports.d.ts +3 -0
  102. package/dist/security-backports.js +26 -0
  103. package/dist/stack-registry.d.ts +80 -0
  104. package/dist/stack-registry.js +424 -0
  105. package/dist/standard.d.ts +6 -17
  106. package/dist/standard.js +41 -585
  107. package/dist/transaction.js +46 -26
  108. package/dist/util.js +12 -2
  109. package/dist/vercel.d.ts +98 -0
  110. package/dist/vercel.js +796 -0
  111. package/dist/version.d.ts +1 -1
  112. package/dist/version.js +1 -1
  113. package/dist/vinext.d.ts +1 -0
  114. package/dist/vinext.js +83 -0
  115. package/dist/workspace-graph.d.ts +7 -0
  116. package/dist/workspace-graph.js +73 -0
  117. package/docs/application-stack-standard.md +75 -674
  118. package/docs/delivery-workflow.md +83 -0
  119. package/docs/managed-hosting-architecture.md +87 -0
  120. package/docs/sponsorships.md +25 -0
  121. package/package.json +23 -28
  122. package/dist/project.d.ts +0 -4
  123. package/dist/standard-products.d.ts +0 -2
  124. package/dist/standard-products.js +0 -6
  125. /package/assets/{images/svg/logo-white-subtract.svg → anhedral-mark.svg} +0 -0
  126. /package/dist/{project.js → registry/types.js} +0 -0
@@ -0,0 +1,611 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+ import { writeManagementOverview } from './management.js';
6
+ import { reconcileAccess, validatePrivateAudience } from './access.js';
7
+ import { inspectApplicationResources } from './provision.js';
8
+ import { cloudflareRequest } from './cloudflare.js';
9
+ import { atomicJson, authenticate, defaultRunner, environment, readApplication, readConfig, targetDirectory, withApplicationLock, } from './project-state.js';
10
+ export { registerApplication, readApplication, defaultRunner, environment, targetDirectory, } from './project-state.js';
11
+ const manifestName = 'anhedral.project.json';
12
+ const stateDirectory = '.anhedral';
13
+ const accountPattern = /^[a-f0-9]{32}$/i;
14
+ export async function connectApplication(root, accountId, audience, run = defaultRunner, emails = []) {
15
+ root = realpathSync(root);
16
+ return withApplicationLock(root, async () => {
17
+ const project = readApplication(root);
18
+ if (!project.targets.length)
19
+ throw new Error('This project has no Cloudflare deployment targets.');
20
+ accountId ??= project.accountId ?? process.env.CLOUDFLARE_ACCOUNT_ID;
21
+ if (!accountId) {
22
+ const identity = JSON.parse(await run('pnpm', ['exec', 'wrangler', 'whoami', '--json'], targetDirectory(root, project.targets[0]), { ...process.env, CI: 'true', NO_COLOR: '1', FORCE_COLOR: '0' }));
23
+ if (identity.loggedIn !== true || !Array.isArray(identity.accounts))
24
+ throw new Error('Run wrangler login or configure scoped credentials before connecting.');
25
+ const accounts = [
26
+ ...new Set(identity.accounts
27
+ .map((item) => item?.id)
28
+ .filter((id) => typeof id === 'string' && accountPattern.test(id))),
29
+ ];
30
+ if (accounts.length !== 1)
31
+ throw new Error('Select the intended customer account with --account <id>; automatic selection requires exactly one accessible account.');
32
+ accountId = accounts[0];
33
+ }
34
+ if (!accountPattern.test(accountId))
35
+ throw new Error('Cloudflare account ID must be 32 hexadecimal characters.');
36
+ if (project.accountId && project.accountId !== accountId)
37
+ throw new Error('This project already belongs to another account; register a separate environment instead.');
38
+ if (project.audience !== 'unconfigured' && audience && project.audience !== audience)
39
+ throw new Error('Changing an existing audience requires a deliberate provider policy migration; connect will not remove protections.');
40
+ const connected = {
41
+ ...project,
42
+ accountId,
43
+ audience: audience ?? project.audience,
44
+ ...((audience ?? project.audience) === 'private'
45
+ ? {
46
+ access: {
47
+ emails: emails.length
48
+ ? [...new Set(emails.map((email) => email.toLowerCase()))].sort()
49
+ : (project.access?.emails ?? []),
50
+ },
51
+ }
52
+ : {}),
53
+ };
54
+ if (connected.audience === 'private')
55
+ validatePrivateAudience(connected, root);
56
+ else if (emails.length)
57
+ throw new Error('--allow-email applies only to private delivery.');
58
+ await authenticate(root, connected, run);
59
+ atomicJson(path.join(root, manifestName), connected);
60
+ return connected;
61
+ });
62
+ }
63
+ function history(root, project) {
64
+ const file = path.join(root, stateDirectory, 'releases.json');
65
+ if (!existsSync(file))
66
+ return { schemaVersion: 1, projectId: project.id, releases: [] };
67
+ const value = JSON.parse(readFileSync(file, 'utf8'));
68
+ if (value.schemaVersion !== 1 || value.projectId !== project.id || !Array.isArray(value.releases))
69
+ throw new Error('Release history belongs to a different or unsupported project.');
70
+ return value;
71
+ }
72
+ function checkTargets(root, project) {
73
+ if (!project.targets.length)
74
+ throw new Error('No Cloudflare targets; native applications require their platform delivery tools.');
75
+ for (const target of project.targets) {
76
+ const directory = targetDirectory(root, target);
77
+ const config = readConfig(directory);
78
+ if (config.name !== target.workerName ||
79
+ (config.env && Object.keys(config.env).length))
80
+ throw new Error('Worker name or environment changed; reconcile the application manifest before shipping.');
81
+ if (config.account_id && config.account_id !== project.accountId)
82
+ throw new Error('Wrangler configuration account conflicts with the registered account.');
83
+ if (/REPLACE_WITH_|YOUR_[A-Z_]+/.test(readFileSync(path.join(directory, 'wrangler.jsonc'), 'utf8')))
84
+ throw new Error('Configure selected resources before shipping; placeholder bindings remain.');
85
+ const pkg = JSON.parse(readFileSync(path.join(directory, 'package.json'), 'utf8'));
86
+ if (typeof pkg.scripts?.deploy !== 'string')
87
+ throw new Error('Deployment target has no deploy script.');
88
+ }
89
+ }
90
+ function configurationFingerprint(root, project) {
91
+ const hash = createHash('sha256').update(JSON.stringify(project));
92
+ for (const target of project.targets)
93
+ hash.update(readFileSync(path.join(targetDirectory(root, target), 'wrangler.jsonc')));
94
+ return hash.digest('hex');
95
+ }
96
+ export function planDelivery(root) {
97
+ const project = readApplication(root);
98
+ checkTargets(root, project);
99
+ return {
100
+ project,
101
+ checks: [['pnpm', 'run', 'check']],
102
+ deployments: project.targets.map((target) => ({
103
+ target: target.id,
104
+ directory: target.directory,
105
+ command: ['pnpm', 'run', 'deploy'],
106
+ })),
107
+ resourcesProvisioned: false,
108
+ audience: project.audience,
109
+ };
110
+ }
111
+ export function parseDeploymentOutput(source, target) {
112
+ const entries = source
113
+ .split('\n')
114
+ .filter((line) => line.trim())
115
+ .map((line) => JSON.parse(line));
116
+ const deploy = entries.findLast((entry) => entry.type === 'deploy' && entry.version === 1 && entry.worker_name === target.workerName);
117
+ if (!deploy || typeof deploy.version_id !== 'string' || !deploy.version_id)
118
+ throw new Error('No matching structured Wrangler deployment receipt. Deployment may have occurred; inspect provider status before retrying.');
119
+ const urls = Array.isArray(deploy.targets)
120
+ ? deploy.targets.filter((url) => {
121
+ if (typeof url !== 'string')
122
+ return false;
123
+ try {
124
+ const value = new URL(url);
125
+ return value.protocol === 'https:' && !value.username && !value.password;
126
+ }
127
+ catch {
128
+ return false;
129
+ }
130
+ })
131
+ : [];
132
+ return { target: target.id, versionId: deploy.version_id, urls, providerConfirmed: false };
133
+ }
134
+ function activeVersions(value) {
135
+ if (!value || typeof value !== 'object')
136
+ return [];
137
+ const record = value;
138
+ // Read only the active deployment's versions, never arbitrary historical IDs.
139
+ if (!Array.isArray(record.versions))
140
+ throw new Error('Unsupported provider deployment status; active versions are missing.');
141
+ const versions = record.versions
142
+ .map((entry) => {
143
+ if (!entry || typeof entry !== 'object')
144
+ throw new Error('Invalid provider version.');
145
+ const item = entry;
146
+ if (typeof item.version_id !== 'string' ||
147
+ !item.version_id ||
148
+ typeof item.percentage !== 'number' ||
149
+ !Number.isFinite(item.percentage) ||
150
+ item.percentage < 0 ||
151
+ item.percentage > 100)
152
+ throw new Error('Invalid provider traffic allocation.');
153
+ return { versionId: item.version_id, percentage: item.percentage };
154
+ })
155
+ .filter((entry) => entry.percentage > 0);
156
+ if (new Set(versions.map((entry) => entry.versionId)).size !== versions.length ||
157
+ Math.abs(versions.reduce((sum, entry) => sum + entry.percentage, 0) - 100) > 0.001)
158
+ throw new Error('Invalid provider traffic allocation.');
159
+ return versions;
160
+ }
161
+ function fullyActive(versions, versionId) {
162
+ return (versions.length === 1 && versions[0].versionId === versionId && versions[0].percentage === 100);
163
+ }
164
+ function describeArchitecture(root, project) {
165
+ return project.targets.map((target) => {
166
+ const config = readConfig(targetDirectory(root, target));
167
+ const bindings = [];
168
+ for (const kind of [
169
+ 'd1_databases',
170
+ 'r2_buckets',
171
+ 'kv_namespaces',
172
+ 'hyperdrive',
173
+ 'services',
174
+ 'workflows',
175
+ ]) {
176
+ const entries = config[kind];
177
+ if (Array.isArray(entries))
178
+ for (const entry of entries)
179
+ if (typeof entry?.binding === 'string')
180
+ bindings.push({ kind, binding: entry.binding });
181
+ }
182
+ return {
183
+ target: target.id,
184
+ workerName: target.workerName,
185
+ directory: target.directory,
186
+ bindings,
187
+ scheduled: typeof config.triggers === 'object' &&
188
+ config.triggers !== null &&
189
+ Array.isArray(config.triggers.crons)
190
+ ? config.triggers.crons.filter((cron) => typeof cron === 'string')
191
+ : [],
192
+ };
193
+ });
194
+ }
195
+ export async function shipApplication(root, run = defaultRunner, suppliedRequest) {
196
+ root = realpathSync(root);
197
+ const { project } = planDelivery(root);
198
+ if (project.audience === 'unconfigured')
199
+ throw new Error('Select an explicit audience before shipping: public or private with allowed members.');
200
+ const env = environment(project);
201
+ return withApplicationLock(root, async () => {
202
+ const state = path.join(root, stateDirectory);
203
+ const outputDirectory = path.join(tmpdir(), `anhedral-delivery-${randomUUID()}`);
204
+ mkdirSync(outputDirectory, { mode: 0o700 });
205
+ let release;
206
+ let records;
207
+ const save = () => atomicJson(path.join(state, 'releases.json'), records);
208
+ try {
209
+ await authenticate(root, project, run);
210
+ const gitRoot = (await run('git', ['rev-parse', '--show-toplevel'], root, env)).trim();
211
+ if (realpathSync(gitRoot) !== root)
212
+ throw new Error('Application must be its own Git repository.');
213
+ if ((await run('git', ['status', '--porcelain'], root, env)).trim())
214
+ throw new Error('Commit the application source and manifest before shipping.');
215
+ const commit = (await run('git', ['rev-parse', 'HEAD'], root, env)).trim();
216
+ if (!/^[a-f0-9]{40,64}$/.test(commit))
217
+ throw new Error('Invalid source commit.');
218
+ const fingerprint = configurationFingerprint(root, project);
219
+ records = history(root, project);
220
+ release = {
221
+ operation: 'ship',
222
+ id: randomUUID(),
223
+ commit,
224
+ startedAt: new Date().toISOString(),
225
+ state: 'running',
226
+ targets: [],
227
+ };
228
+ records.releases.push(release);
229
+ save();
230
+ await run('pnpm', ['run', 'check'], root, env);
231
+ release.state = 'checked';
232
+ save();
233
+ const accessRequest = project.audience === 'private'
234
+ ? (suppliedRequest ??
235
+ (await cloudflareRequest(project, targetDirectory(root, project.targets[0]), run, env)))
236
+ : undefined;
237
+ if (accessRequest)
238
+ await reconcileAccess(root, project, accessRequest, true);
239
+ for (const target of project.targets) {
240
+ if (configurationFingerprint(root, readApplication(root)) !== fingerprint ||
241
+ (await run('git', ['rev-parse', 'HEAD'], root, env)).trim() !== commit ||
242
+ (await run('git', ['status', '--porcelain'], root, env)).trim())
243
+ throw new Error('Source or configuration changed during delivery; preserve the release record and rebuild.');
244
+ const output = path.join(outputDirectory, `${target.id}.ndjson`);
245
+ await run('pnpm', ['run', 'deploy'], targetDirectory(root, target), {
246
+ ...env,
247
+ WRANGLER_OUTPUT_FILE_PATH: output,
248
+ });
249
+ const receipt = parseDeploymentOutput(readFileSync(output, 'utf8'), target);
250
+ release.targets.push(receipt);
251
+ save();
252
+ const provider = JSON.parse(await run('pnpm', ['exec', 'wrangler', 'deployments', 'status', '--json'], targetDirectory(root, target), env));
253
+ if (!fullyActive(activeVersions(provider), receipt.versionId))
254
+ throw new Error('Provider active version differs from the deployment receipt.');
255
+ if (accessRequest)
256
+ await reconcileAccess(root, project, accessRequest, false);
257
+ receipt.providerConfirmed = true;
258
+ save();
259
+ }
260
+ if (configurationFingerprint(root, readApplication(root)) !== fingerprint ||
261
+ (await run('git', ['rev-parse', 'HEAD'], root, env)).trim() !== commit ||
262
+ (await run('git', ['status', '--porcelain'], root, env)).trim())
263
+ throw new Error('Source or configuration changed during deployment.');
264
+ release.state = 'deployed';
265
+ release.finishedAt = new Date().toISOString();
266
+ save();
267
+ const managementFile = writeManagementOverview(root, await applicationStatus(root));
268
+ return {
269
+ projectId: project.id,
270
+ release,
271
+ managementFile,
272
+ productVerification: 'required',
273
+ management: `https://dash.cloudflare.com/${project.accountId}/workers-and-pages`,
274
+ };
275
+ }
276
+ catch (error) {
277
+ if (release) {
278
+ release.state = 'failed';
279
+ release.finishedAt = new Date().toISOString();
280
+ release.failure =
281
+ 'Delivery did not complete. Inspect provider status before retrying; earlier targets may already be deployed.';
282
+ save();
283
+ }
284
+ throw error;
285
+ }
286
+ finally {
287
+ rmSync(outputDirectory, { recursive: true, force: true });
288
+ }
289
+ });
290
+ }
291
+ export async function applicationStatus(root, refresh = false, run = defaultRunner) {
292
+ const project = readApplication(root);
293
+ const records = history(root, project);
294
+ const current = records.releases.at(-1);
295
+ const provider = [];
296
+ if (refresh) {
297
+ checkTargets(root, project);
298
+ await authenticate(root, project, run);
299
+ for (const target of project.targets) {
300
+ const data = JSON.parse(await run('pnpm', ['exec', 'wrangler', 'deployments', 'status', '--json'], targetDirectory(root, target), environment(project)));
301
+ const receipt = current?.targets.find((item) => item.target === target.id);
302
+ const versions = activeVersions(data);
303
+ provider.push({
304
+ target: target.id,
305
+ activeRecordedVersion: !!receipt && fullyActive(versions, receipt.versionId),
306
+ versions,
307
+ });
308
+ }
309
+ }
310
+ const access = refresh && project.audience === 'private'
311
+ ? await reconcileAccess(root, project, await cloudflareRequest(project, targetDirectory(root, project.targets[0]), run, environment(project)), false)
312
+ : undefined;
313
+ return {
314
+ project,
315
+ access,
316
+ architecture: describeArchitecture(root, project),
317
+ releases: records.releases,
318
+ provider,
319
+ evidence: refresh ? 'provider-refresh' : 'local-history',
320
+ productVerification: current?.verification?.state === 'passed'
321
+ ? refresh && provider.some((target) => !target.activeRecordedVersion)
322
+ ? 'stale-live-tests'
323
+ : 'recorded-live-tests-passed'
324
+ : 'unverified',
325
+ management: project.accountId
326
+ ? `https://dash.cloudflare.com/${project.accountId}/workers-and-pages`
327
+ : null,
328
+ };
329
+ }
330
+ export async function verifyApplication(root, run = defaultRunner) {
331
+ root = realpathSync(root);
332
+ return withApplicationLock(root, async () => {
333
+ const snapshot = await applicationStatus(root, true, run);
334
+ const release = snapshot.releases.at(-1);
335
+ if (!release ||
336
+ release.state !== 'deployed' ||
337
+ !snapshot.provider.every((target) => target.activeRecordedVersion))
338
+ throw new Error('Live verification requires a fully active recorded release.');
339
+ const project = snapshot.project;
340
+ const env = environment(project);
341
+ const primary = release.targets.find((target) => target.target === 'web') ?? release.targets.at(-1);
342
+ if (!primary?.urls.length)
343
+ throw new Error('No recorded application URL for live verification.');
344
+ const pkg = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'));
345
+ if (typeof pkg.scripts?.['test:live'] !== 'string')
346
+ throw new Error('Implement acceptance tests in the application test:live script before running verify. ANHEDRAL_APP_URL supplies the deployed primary URL.');
347
+ if ((await run('git', ['rev-parse', 'HEAD'], root, env)).trim() !== release.commit ||
348
+ (await run('git', ['status', '--porcelain'], root, env)).trim())
349
+ throw new Error('Live tests must use the clean source commit of the active release.');
350
+ const records = history(root, project);
351
+ const stored = records.releases.at(-1);
352
+ try {
353
+ await run('pnpm', ['run', 'test:live'], root, {
354
+ ...env,
355
+ ANHEDRAL_APP_URL: primary.urls[0],
356
+ ANHEDRAL_RELEASE_ID: release.id,
357
+ });
358
+ const after = await applicationStatus(root, true, run);
359
+ if (!after.provider.every((target) => target.activeRecordedVersion) ||
360
+ (await run('git', ['rev-parse', 'HEAD'], root, env)).trim() !== release.commit ||
361
+ (await run('git', ['status', '--porcelain'], root, env)).trim())
362
+ throw new Error('Source or active deployment changed during live tests.');
363
+ stored.verification = {
364
+ state: 'passed',
365
+ checkedAt: new Date().toISOString(),
366
+ command: 'pnpm run test:live',
367
+ };
368
+ }
369
+ catch (error) {
370
+ stored.verification = {
371
+ state: 'failed',
372
+ checkedAt: new Date().toISOString(),
373
+ command: 'pnpm run test:live',
374
+ };
375
+ atomicJson(path.join(root, stateDirectory, 'releases.json'), records);
376
+ writeManagementOverview(root, await applicationStatus(root));
377
+ throw error;
378
+ }
379
+ atomicJson(path.join(root, stateDirectory, 'releases.json'), records);
380
+ const managementFile = writeManagementOverview(root, await applicationStatus(root));
381
+ return {
382
+ projectId: project.id,
383
+ releaseId: release.id,
384
+ verification: stored.verification,
385
+ managementFile,
386
+ };
387
+ });
388
+ }
389
+ export async function rollbackApplication(root, releaseId, dataCompatible, run = defaultRunner, suppliedRequest) {
390
+ root = realpathSync(root);
391
+ if (!dataCompatible)
392
+ throw new Error('Review resource/schema compatibility and pass --data-compatible. Worker rollback does not restore data or resource migrations.');
393
+ return withApplicationLock(root, async () => {
394
+ const project = readApplication(root);
395
+ checkTargets(root, project);
396
+ await authenticate(root, project, run);
397
+ const records = history(root, project);
398
+ const desired = records.releases.find((item) => item.id === releaseId);
399
+ if (!desired ||
400
+ desired.state !== 'deployed' ||
401
+ desired.targets.length !== project.targets.length ||
402
+ project.targets.some((target) => !desired.targets.some((item) => item.target === target.id && item.providerConfirmed)))
403
+ throw new Error('Choose a complete recorded release for every current target.');
404
+ const env = environment(project);
405
+ const request = project.audience === 'private'
406
+ ? (suppliedRequest ??
407
+ (await cloudflareRequest(project, targetDirectory(root, project.targets[0]), run, env)))
408
+ : undefined;
409
+ if (request)
410
+ await reconcileAccess(root, project, request, false);
411
+ // Resolve every version before changing the first target; unavailable resources still make provider rollback fail closed.
412
+ for (const target of project.targets) {
413
+ const version = desired.targets.find((item) => item.target === target.id);
414
+ const data = JSON.parse(await run('pnpm', ['exec', 'wrangler', 'versions', 'view', version.versionId, '--json'], targetDirectory(root, target), env));
415
+ if (data.id !== version.versionId)
416
+ throw new Error('Recorded Worker version is unavailable.');
417
+ }
418
+ const release = {
419
+ operation: 'rollback',
420
+ restoredReleaseId: desired.id,
421
+ id: randomUUID(),
422
+ commit: desired.commit,
423
+ startedAt: new Date().toISOString(),
424
+ state: 'running',
425
+ targets: [],
426
+ };
427
+ records.releases.push(release);
428
+ const save = () => atomicJson(path.join(root, stateDirectory, 'releases.json'), records);
429
+ save();
430
+ try {
431
+ for (const target of [...project.targets].reverse()) {
432
+ const receipt = desired.targets.find((item) => item.target === target.id);
433
+ await run('pnpm', [
434
+ 'exec',
435
+ 'wrangler',
436
+ 'rollback',
437
+ receipt.versionId,
438
+ '--message',
439
+ `Anhedral restore ${desired.id}`,
440
+ ], targetDirectory(root, target), env);
441
+ const restored = { ...receipt, providerConfirmed: false };
442
+ release.targets.push(restored);
443
+ save();
444
+ const provider = JSON.parse(await run('pnpm', ['exec', 'wrangler', 'deployments', 'status', '--json'], targetDirectory(root, target), env));
445
+ if (!fullyActive(activeVersions(provider), receipt.versionId))
446
+ throw new Error('Rollback version did not become fully active.');
447
+ if (request)
448
+ await reconcileAccess(root, project, request, false);
449
+ restored.providerConfirmed = true;
450
+ save();
451
+ }
452
+ release.state = 'deployed';
453
+ release.finishedAt = new Date().toISOString();
454
+ save();
455
+ }
456
+ catch (error) {
457
+ release.state = 'failed';
458
+ release.finishedAt = new Date().toISOString();
459
+ release.failure =
460
+ 'Rollback did not complete; some targets may already be restored. Inspect provider status.';
461
+ save();
462
+ throw error;
463
+ }
464
+ return {
465
+ projectId: project.id,
466
+ release,
467
+ managementFile: writeManagementOverview(root, await applicationStatus(root)),
468
+ productVerification: 'required',
469
+ dataRestored: false,
470
+ };
471
+ });
472
+ }
473
+ /** Recover current provider evidence, never fabricate a historical release or source-to-version mapping. */
474
+ export async function recoverApplication(root, run = defaultRunner, suppliedRequest) {
475
+ root = realpathSync(root);
476
+ return withApplicationLock(root, async () => {
477
+ const project = readApplication(root);
478
+ checkTargets(root, project);
479
+ if (project.audience === 'unconfigured')
480
+ throw new Error('Recovery requires the committed audience configuration.');
481
+ const env = environment(project);
482
+ const source = async () => {
483
+ if (realpathSync((await run('git', ['rev-parse', '--show-toplevel'], root, env)).trim()) !==
484
+ root)
485
+ throw new Error('Application must be its own Git repository.');
486
+ if ((await run('git', ['status', '--porcelain'], root, env)).trim())
487
+ throw new Error('Recovery requires clean committed configuration.');
488
+ const commit = (await run('git', ['rev-parse', 'HEAD'], root, env)).trim();
489
+ if (!/^[a-f0-9]{40,64}$/.test(commit))
490
+ throw new Error('Invalid source commit.');
491
+ return commit;
492
+ };
493
+ const commit = await source();
494
+ const fingerprint = configurationFingerprint(root, project);
495
+ if ((await run('git', ['show', `HEAD:${manifestName}`], root, env)) !==
496
+ readFileSync(path.join(root, manifestName), 'utf8'))
497
+ throw new Error('Recovery requires the exact committed application manifest.');
498
+ for (const target of project.targets) {
499
+ const file = `${target.directory}/wrangler.jsonc`;
500
+ if ((await run('git', ['show', `HEAD:${file}`], root, env)) !==
501
+ readFileSync(path.join(targetDirectory(root, target), 'wrangler.jsonc'), 'utf8'))
502
+ throw new Error('Recovery requires exact committed Worker configuration.');
503
+ }
504
+ await authenticate(root, project, run);
505
+ const request = suppliedRequest ??
506
+ (await cloudflareRequest(project, targetDirectory(root, project.targets[0]), run, env));
507
+ const resources = await inspectApplicationResources(root, request);
508
+ const provider = [];
509
+ const unsupportedBindings = new Set(resources.externalIntegrations);
510
+ for (const target of project.targets) {
511
+ const directory = targetDirectory(root, target);
512
+ const config = readConfig(directory);
513
+ const settings = (await request('GET', `/workers/scripts/${encodeURIComponent(target.workerName)}/settings`)).result;
514
+ if (!Array.isArray(settings?.bindings))
515
+ throw new Error('Provider Worker settings lack binding evidence.');
516
+ for (const [field, type, localKey, remoteKey] of [
517
+ ['d1_databases', 'd1', 'database_id', 'id'],
518
+ ['r2_buckets', 'r2_bucket', 'bucket_name', 'bucket_name'],
519
+ ['kv_namespaces', 'kv_namespace', 'id', 'namespace_id'],
520
+ ['services', 'service', 'service', 'service'],
521
+ ]) {
522
+ const entries = config[field];
523
+ if (entries === undefined)
524
+ continue;
525
+ if (!Array.isArray(entries))
526
+ throw new Error('Unsupported committed binding configuration.');
527
+ for (const entry of entries) {
528
+ const matches = settings.bindings.filter((binding) => binding.name === entry.binding);
529
+ if (field === 'services' && (entry.environment || matches[0]?.environment))
530
+ throw new Error('Service environment binding requires independent provider reconciliation.');
531
+ if (typeof entry.binding !== 'string' ||
532
+ typeof entry[localKey] !== 'string' ||
533
+ matches.length !== 1 ||
534
+ matches[0].type !== type ||
535
+ matches[0][remoteKey] !== entry[localKey])
536
+ throw new Error('Active Worker binding differs from committed configuration.');
537
+ }
538
+ }
539
+ const expected = new Set(['d1_databases', 'r2_buckets', 'kv_namespaces', 'services'].flatMap((field) => Array.isArray(config[field])
540
+ ? config[field].map((entry) => entry.binding)
541
+ : []));
542
+ if (settings.bindings.some((binding) => ['d1', 'r2_bucket', 'kv_namespace', 'service'].includes(String(binding.type)) &&
543
+ !expected.has(String(binding.name))))
544
+ throw new Error('Active Worker contains an undeclared resource binding.');
545
+ for (const field of ['queues', 'durable_objects', 'workflows', 'hyperdrive', 'pipelines'])
546
+ if (config[field])
547
+ unsupportedBindings.add(field);
548
+ const versions = activeVersions(JSON.parse(await run('pnpm', ['exec', 'wrangler', 'deployments', 'status', '--json'], directory, env)));
549
+ if (!versions.length)
550
+ throw new Error('No active Worker deployment to recover.');
551
+ provider.push({
552
+ target: target.id,
553
+ versions,
554
+ bindingsConfirmed: unsupportedBindings.size === 0,
555
+ });
556
+ }
557
+ const access = project.audience === 'private'
558
+ ? await reconcileAccess(root, project, request, false)
559
+ : undefined;
560
+ if ((await source()) !== commit ||
561
+ configurationFingerprint(root, readApplication(root)) !== fingerprint)
562
+ throw new Error('Source or configuration changed during recovery.');
563
+ const records = history(root, project);
564
+ for (const target of project.targets) {
565
+ const after = activeVersions(JSON.parse(await run('pnpm', ['exec', 'wrangler', 'deployments', 'status', '--json'], targetDirectory(root, target), env)));
566
+ if (JSON.stringify(after) !==
567
+ JSON.stringify(provider.find((item) => item.target === target.id).versions))
568
+ throw new Error('Active deployment changed during recovery.');
569
+ }
570
+ const replayable = unsupportedBindings.size
571
+ ? undefined
572
+ : [...records.releases]
573
+ .reverse()
574
+ .find((release) => release.state === 'deployed' &&
575
+ release.targets.length === project.targets.length &&
576
+ provider.every((target) => release.targets.some((receipt) => receipt.target === target.target &&
577
+ receipt.providerConfirmed &&
578
+ fullyActive(target.versions, receipt.versionId))));
579
+ const result = {
580
+ schemaVersion: 1,
581
+ projectId: project.id,
582
+ accountId: project.accountId,
583
+ checkedAt: new Date().toISOString(),
584
+ evidence: 'current-provider-snapshot',
585
+ localCommit: commit,
586
+ sourceProvenance: replayable ? 'existing-local-release' : 'unavailable',
587
+ replayableReleaseId: replayable?.id ?? null,
588
+ productVerification: 'unverified',
589
+ provider,
590
+ resources: resources.resources,
591
+ access,
592
+ audience: project.audience,
593
+ audienceEvidence: access ? 'exact-private-access-policies' : 'committed-public-declaration',
594
+ limitations: [...unsupportedBindings]
595
+ .sort()
596
+ .map((kind) => `Independent active binding reconciliation requires provider tooling: ${kind}.`)
597
+ .concat(project.audience === 'public'
598
+ ? [
599
+ 'Public audience is a committed declaration; hostname-level Access and public route reachability require separate live verification.',
600
+ ]
601
+ : [])
602
+ .concat(replayable
603
+ ? []
604
+ : [
605
+ 'Provider versions do not establish source commit, application URL or prior acceptance tests. Missing release history cannot be reconstructed; ship a checked release before verify or rollback.',
606
+ ]),
607
+ };
608
+ atomicJson(path.join(root, stateDirectory, 'cloudflare-recovery.json'), result);
609
+ return result;
610
+ });
611
+ }
@@ -8,3 +8,25 @@ export declare const MOBILE_APP_DEPENDENCIES: DependencyGroup;
8
8
  export declare const MOBILE_NATIVEWIND_DEPENDENCIES: DependencyGroup;
9
9
  export declare const PACKAGE_MANAGER = "pnpm@10.34.5";
10
10
  export declare const EAS_CLI_VERSION = "21.0.1";
11
+ export declare const SHADCN_VERSION = "4.21.4";
12
+ export declare const STACK_DEPENDENCIES: {
13
+ readonly 'drizzle-orm': "0.45.4";
14
+ readonly '@clerk/nextjs': "7.9.14";
15
+ readonly '@clerk/backend': "3.23.2";
16
+ readonly wrangler: "4.149.0";
17
+ readonly typescript: "5.9.3";
18
+ readonly '@types/node': "22.20.5";
19
+ readonly eslint: "9.39.5";
20
+ readonly 'postcss-load-config': "6.0.1";
21
+ readonly '@opennextjs/cloudflare': "1.20.10";
22
+ readonly 'drizzle-kit': "0.31.11";
23
+ readonly hono: "4.13.13";
24
+ readonly '@hono/zod-openapi': "1.6.3";
25
+ readonly fallow: "3.33.1";
26
+ readonly openai: "6.49.0";
27
+ readonly ai: "6.0.303";
28
+ readonly '@ai-sdk/openai': "3.0.125";
29
+ readonly resend: "6.32.1";
30
+ readonly stripe: "20.4.1";
31
+ readonly 'posthog-node': "5.55.2";
32
+ };