anhedral 0.10.0 → 0.11.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 (120) hide show
  1. package/README.md +78 -39
  2. package/assets/{anhedral-cli-init-technical.svg → anhedral-stack-technical.svg} +6 -6
  3. package/assets/{anhedral-cli-init.svg → anhedral-stack.svg} +7 -7
  4. package/assets/security/audit-deps.mjs +6 -1
  5. package/dist/bin.js +271 -154
  6. package/dist/bounded-file.d.ts +1 -0
  7. package/dist/bounded-file.js +1 -0
  8. package/dist/capabilities.d.ts +4 -4
  9. package/dist/capabilities.js +1 -1
  10. package/dist/capability-contract.d.ts +47 -0
  11. package/dist/capability-contract.js +136 -0
  12. package/dist/cli-arguments.d.ts +16 -0
  13. package/dist/cli-arguments.js +115 -0
  14. package/dist/cli-output.d.ts +8 -0
  15. package/dist/cli-output.js +211 -0
  16. package/dist/cloudflare.d.ts +7 -2
  17. package/dist/cloudflare.js +166 -36
  18. package/dist/command-schema.d.ts +21 -0
  19. package/dist/command-schema.js +374 -0
  20. package/dist/delivery.d.ts +2 -2
  21. package/dist/delivery.js +21 -4
  22. package/dist/dependencies.d.ts +32 -0
  23. package/dist/dependencies.js +47 -1
  24. package/dist/index.d.ts +23 -1
  25. package/dist/index.js +15 -1
  26. package/dist/inspector-model.d.ts +115 -0
  27. package/dist/inspector-model.js +1 -0
  28. package/dist/lifecycle-model.d.ts +15 -0
  29. package/dist/lifecycle-model.js +20 -0
  30. package/dist/lifecycle.d.ts +12 -0
  31. package/dist/lifecycle.js +92 -0
  32. package/dist/mobile-delivery.d.ts +100 -0
  33. package/dist/mobile-delivery.js +319 -0
  34. package/dist/platforms/desktop.js +4 -5
  35. package/dist/platforms/extension.js +9 -73
  36. package/dist/platforms/mobile.js +14 -52
  37. package/dist/project-inspector.d.ts +4 -0
  38. package/dist/project-inspector.js +646 -0
  39. package/dist/project-state.d.ts +17 -0
  40. package/dist/project-state.js +239 -11
  41. package/dist/provider-auth.d.ts +41 -0
  42. package/dist/provider-auth.js +210 -0
  43. package/dist/providers/adapters.d.ts +17 -0
  44. package/dist/providers/adapters.js +269 -0
  45. package/dist/providers/api.d.ts +28 -0
  46. package/dist/providers/api.js +252 -0
  47. package/dist/providers/config.d.ts +15 -0
  48. package/dist/providers/config.js +184 -0
  49. package/dist/providers/definitions.d.ts +318 -0
  50. package/dist/providers/definitions.js +127 -0
  51. package/dist/providers/provision.d.ts +24 -0
  52. package/dist/providers/provision.js +76 -0
  53. package/dist/providers/receipts.d.ts +20 -0
  54. package/dist/providers/receipts.js +66 -0
  55. package/dist/providers/recovery-verification.d.ts +7 -0
  56. package/dist/providers/recovery-verification.js +112 -0
  57. package/dist/providers/recovery.d.ts +30 -0
  58. package/dist/providers/recovery.js +34 -0
  59. package/dist/providers/runtime.d.ts +10 -0
  60. package/dist/providers/runtime.js +231 -0
  61. package/dist/providers/schema.d.ts +28 -0
  62. package/dist/providers/schema.js +49 -0
  63. package/dist/providers/state.d.ts +56 -0
  64. package/dist/providers/state.js +329 -0
  65. package/dist/provision.d.ts +1 -1
  66. package/dist/provision.js +61 -3
  67. package/dist/readiness.js +11 -3
  68. package/dist/registry/add.d.ts +6 -0
  69. package/dist/registry/add.js +35 -4
  70. package/dist/registry/commands.d.ts +3 -2
  71. package/dist/registry/commands.js +53 -14
  72. package/dist/registry/options.js +3 -2
  73. package/dist/registry/preview.d.ts +11 -0
  74. package/dist/registry/preview.js +91 -0
  75. package/dist/registry/receipt.js +1 -0
  76. package/dist/registry/resolve.js +19 -13
  77. package/dist/registry/types.d.ts +1 -0
  78. package/dist/registry/usage.d.ts +10 -0
  79. package/dist/registry/usage.js +22 -37
  80. package/dist/render.d.ts +2 -0
  81. package/dist/render.js +10 -0
  82. package/dist/scaffold/api.js +20 -3
  83. package/dist/scaffold/auth.js +68 -28
  84. package/dist/scaffold/bootstrap.d.ts +2 -0
  85. package/dist/scaffold/bootstrap.js +9 -0
  86. package/dist/scaffold/database.js +12 -6
  87. package/dist/scaffold/documentation.js +11 -8
  88. package/dist/scaffold/environment.d.ts +2 -0
  89. package/dist/scaffold/environment.js +18 -1
  90. package/dist/scaffold/files.d.ts +1 -0
  91. package/dist/scaffold/files.js +15 -4
  92. package/dist/scaffold/services.js +37 -13
  93. package/dist/scaffold/stack-documentation.d.ts +3 -0
  94. package/dist/scaffold/stack-documentation.js +52 -0
  95. package/dist/scaffold/styling.d.ts +3 -0
  96. package/dist/scaffold/styling.js +84 -0
  97. package/dist/scaffold/web.js +31 -11
  98. package/dist/scaffold/workspace.js +49 -22
  99. package/dist/ship.d.ts +320 -0
  100. package/dist/ship.js +211 -0
  101. package/dist/stack-registry.d.ts +2 -2
  102. package/dist/stack-registry.js +44 -11
  103. package/dist/standard.d.ts +2 -1
  104. package/dist/standard.js +13 -12
  105. package/dist/vercel.d.ts +3 -3
  106. package/dist/vercel.js +27 -1
  107. package/dist/version.d.ts +1 -1
  108. package/dist/version.js +1 -1
  109. package/dist/vinext.js +9 -18
  110. package/dist/workspace-graph.js +3 -2
  111. package/docs/application-stack-standard.md +51 -80
  112. package/docs/cli-design.md +67 -0
  113. package/docs/delivery-workflow.md +29 -15
  114. package/docs/plugin-ui.md +61 -0
  115. package/docs/provider-configuration.md +117 -0
  116. package/package.json +16 -12
  117. package/dist/managed-hosting.d.ts +0 -49
  118. package/dist/managed-hosting.js +0 -373
  119. package/docs/managed-hosting-architecture.md +0 -87
  120. package/docs/sponsorships.md +0 -25
@@ -1,7 +1,10 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
2
+ import { AsyncLocalStorage } from 'node:async_hooks';
3
+ import { existsSync, lstatSync, fstatSync, openSync, closeSync, unlinkSync, rmdirSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
3
4
  import { spawn } from 'node:child_process';
4
5
  import path from 'node:path';
6
+ import { hostname } from 'node:os';
7
+ import { readBoundedFile } from './bounded-file.js';
5
8
  import { parse } from 'jsonc-parser';
6
9
  import { resolveCommand } from './command.js';
7
10
  const manifestName = 'anhedral.project.json';
@@ -204,23 +207,248 @@ export async function authenticate(root, project, run) {
204
207
  !identity.accounts.some((account) => account.id === project.accountId))
205
208
  throw new Error('Wrangler cannot access the registered Cloudflare account. Run wrangler login or configure scoped credentials.');
206
209
  }
207
- export async function withApplicationLock(root, operation) {
208
- root = realpathSync(root);
210
+ const applicationLocks = new AsyncLocalStorage();
211
+ const lockBusy = () => new Error('Another delivery is running or was interrupted. Inspect provider status and use anhedral recover --lock only after reviewing the recorded operation.');
212
+ const ownerChanged = () => new Error('Delivery lock ownership changed or is invalid. Preserve the lock and inspect local operation evidence before retrying.');
213
+ function privateState(root) {
209
214
  const state = path.join(root, stateDirectory);
210
- mkdirSync(state, { recursive: true });
211
- if (realpathSync(state) !== path.join(root, stateDirectory))
212
- throw new Error('Delivery state directory must not be a symbolic link.');
213
- const lock = path.join(state, 'ship.lock');
214
215
  try {
215
- mkdirSync(lock);
216
+ mkdirSync(state, { recursive: true, mode: 0o700 });
217
+ const stat = lstatSync(state);
218
+ if (!stat.isDirectory() ||
219
+ stat.isSymbolicLink() ||
220
+ realpathSync(state) !== state ||
221
+ (process.platform !== 'win32' && stat.mode & 0o022))
222
+ throw new Error();
223
+ return state;
224
+ }
225
+ catch {
226
+ throw new Error('Delivery state must be a regular directory without shared write permissions.');
227
+ }
228
+ }
229
+ function lockOwner() {
230
+ return {
231
+ schemaVersion: 1,
232
+ pid: process.pid,
233
+ host: hostname(),
234
+ token: randomUUID(),
235
+ createdAt: new Date().toISOString(),
236
+ };
237
+ }
238
+ function privateOwner(file) {
239
+ try {
240
+ const stat = lstatSync(file);
241
+ if (!stat.isFile() ||
242
+ stat.isSymbolicLink() ||
243
+ (process.platform !== 'win32' && (stat.mode & 0o077 || stat.uid !== process.getuid?.())))
244
+ throw new Error();
245
+ const value = JSON.parse(readBoundedFile(file, 4096).toString('utf8'));
246
+ if (!value || typeof value !== 'object' || Array.isArray(value))
247
+ throw new Error();
248
+ const owner = value;
249
+ if (Object.keys(owner).some((key) => !['schemaVersion', 'pid', 'host', 'token', 'createdAt'].includes(key)) ||
250
+ owner.schemaVersion !== 1 ||
251
+ !Number.isSafeInteger(owner.pid) ||
252
+ owner.pid < 1 ||
253
+ owner.pid > 2147483647 ||
254
+ typeof owner.host !== 'string' ||
255
+ !owner.host ||
256
+ owner.host.length > 255 ||
257
+ /[\x00-\x1f\x7f]/.test(owner.host) ||
258
+ typeof owner.token !== 'string' ||
259
+ !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(owner.token) ||
260
+ typeof owner.createdAt !== 'string' ||
261
+ !/^\d{4}-\d{2}-\d{2}T/.test(owner.createdAt) ||
262
+ !Number.isFinite(Date.parse(owner.createdAt)))
263
+ throw new Error();
264
+ return owner;
265
+ }
266
+ catch {
267
+ throw ownerChanged();
268
+ }
269
+ }
270
+ function snapshotLock(lock) {
271
+ try {
272
+ const stat = lstatSync(lock);
273
+ if (!stat.isDirectory() ||
274
+ stat.isSymbolicLink() ||
275
+ (process.platform !== 'win32' && (stat.mode & 0o077 || stat.uid !== process.getuid?.())) ||
276
+ readdirSync(lock).join(',') !== 'owner.json')
277
+ throw new Error();
278
+ const file = path.join(lock, 'owner.json');
279
+ const owner = privateOwner(file);
280
+ const ownerStat = lstatSync(file);
281
+ return {
282
+ identity: {
283
+ device: stat.dev,
284
+ inode: stat.ino,
285
+ ownerDevice: ownerStat.dev,
286
+ ownerInode: ownerStat.ino,
287
+ owner,
288
+ },
289
+ owner,
290
+ };
291
+ }
292
+ catch {
293
+ throw ownerChanged();
294
+ }
295
+ }
296
+ function sameOwner(left, right) {
297
+ return (left.pid === right.pid &&
298
+ left.host === right.host &&
299
+ left.token === right.token &&
300
+ left.createdAt === right.createdAt);
301
+ }
302
+ function sameIdentity(left, right) {
303
+ return (left.device === right.device &&
304
+ left.inode === right.inode &&
305
+ left.ownerDevice === right.ownerDevice &&
306
+ left.ownerInode === right.ownerInode &&
307
+ sameOwner(left.owner, right.owner));
308
+ }
309
+ class DeliveryGuardBusyError extends Error {
310
+ constructor() {
311
+ super('Delivery lock acquisition or recovery is busy. Inspect .anhedral/ship.guard before retrying.');
312
+ }
313
+ }
314
+ /** This synchronous guard covers only filesystem transitions. An uncertain guard
315
+ * after process termination is preserved for operator inspection, never auto-unlinked. */
316
+ function withLockGuard(state, operation) {
317
+ try {
318
+ const stat = lstatSync(state);
319
+ if (!stat.isDirectory() ||
320
+ stat.isSymbolicLink() ||
321
+ realpathSync(state) !== state ||
322
+ (process.platform !== 'win32' && stat.mode & 0o022))
323
+ throw new Error();
216
324
  }
217
325
  catch {
218
- throw new Error('Another delivery is running or was interrupted. Inspect provider status and remove .anhedral/ship.lock only after confirming no delivery is active.');
326
+ throw ownerChanged();
327
+ }
328
+ const file = path.join(state, 'ship.guard');
329
+ let descriptor;
330
+ try {
331
+ descriptor = openSync(file, 'wx', 0o600);
219
332
  }
333
+ catch {
334
+ throw new DeliveryGuardBusyError();
335
+ }
336
+ const stat = fstatSync(descriptor);
337
+ const owner = lockOwner();
338
+ try {
339
+ writeFileSync(descriptor, JSON.stringify(owner) + '\n');
340
+ return operation();
341
+ }
342
+ finally {
343
+ closeSync(descriptor);
344
+ // Another process must never lose its guard because this invocation finishes.
345
+ let current;
346
+ try {
347
+ current = lstatSync(file);
348
+ }
349
+ catch {
350
+ throw ownerChanged();
351
+ }
352
+ if (current.isSymbolicLink() ||
353
+ current.dev !== stat.dev ||
354
+ current.ino !== stat.ino ||
355
+ !sameOwner(privateOwner(file), owner))
356
+ throw ownerChanged();
357
+ unlinkSync(file);
358
+ }
359
+ }
360
+ function removeOwnedLock(lock, identity) {
361
+ if (!sameIdentity(snapshotLock(lock).identity, identity))
362
+ throw ownerChanged();
363
+ // Never recursively remove a lock: unknown additional files are evidence.
364
+ unlinkSync(path.join(lock, 'owner.json'));
365
+ rmdirSync(lock);
366
+ }
367
+ async function releaseApplicationLock(state, lock, identity) {
368
+ // A simultaneous operator inspection may briefly hold the guard. Preserve our
369
+ // lock while waiting; never bypass a guard or reinterpret uncertain ownership.
370
+ for (let attempt = 0;; attempt++) {
371
+ try {
372
+ withLockGuard(state, () => removeOwnedLock(lock, identity));
373
+ return;
374
+ }
375
+ catch (error) {
376
+ if (!(error instanceof DeliveryGuardBusyError) || attempt >= 4)
377
+ throw error;
378
+ await new Promise((resolve) => setTimeout(resolve, 25));
379
+ }
380
+ }
381
+ }
382
+ /** Explicit local cleanup only. A dead same-host PID is required; live, foreign,
383
+ * malformed and legacy locks remain intact. Provider operations are never replayed. */
384
+ export function recoverDeliveryLock(root) {
385
+ root = realpathSync(root);
386
+ const state = privateState(root);
387
+ return withLockGuard(state, () => {
388
+ const lock = path.join(state, 'ship.lock');
389
+ if (!lstatSync(lock, { throwIfNoEntry: false }))
390
+ return {
391
+ schemaVersion: 1,
392
+ state: 'unlocked',
393
+ recovered: false,
394
+ mutation: 'local-lock-only',
395
+ providerReplay: false,
396
+ };
397
+ const snapshot = snapshotLock(lock);
398
+ if (snapshot.owner.host !== hostname())
399
+ throw new Error('Delivery lock belongs to another host; preserve it and inspect operation evidence.');
400
+ let dead = false;
401
+ try {
402
+ process.kill(snapshot.owner.pid, 0);
403
+ }
404
+ catch (error) {
405
+ if (error.code === 'ESRCH')
406
+ dead = true;
407
+ else
408
+ throw new Error('Cannot prove the delivery lock owner exited; preserve the lock.');
409
+ }
410
+ if (!dead)
411
+ throw new Error('Delivery lock owner is still running; no lock or provider state was changed.');
412
+ removeOwnedLock(lock, snapshot.identity);
413
+ return {
414
+ schemaVersion: 1,
415
+ state: 'lock-recovered',
416
+ recovered: true,
417
+ mutation: 'local-lock-only',
418
+ providerReplay: false,
419
+ nextAction: 'Inspect provider status and recorded receipts before retrying ship. No provider operation was replayed.',
420
+ };
421
+ });
422
+ }
423
+ export async function withApplicationLock(root, operation) {
424
+ root = realpathSync(root);
425
+ const held = applicationLocks.getStore();
426
+ const state = privateState(root);
427
+ const lock = path.join(state, 'ship.lock');
428
+ if (held?.active && held.root === root) {
429
+ if (!sameIdentity(snapshotLock(lock).identity, held.identity))
430
+ throw ownerChanged();
431
+ return operation();
432
+ }
433
+ const identity = withLockGuard(state, () => {
434
+ try {
435
+ mkdirSync(lock, { mode: 0o700 });
436
+ }
437
+ catch {
438
+ throw lockBusy();
439
+ }
440
+ writeFileSync(path.join(lock, 'owner.json'), JSON.stringify(lockOwner()) + '\n', {
441
+ flag: 'wx',
442
+ mode: 0o600,
443
+ });
444
+ return snapshotLock(lock).identity;
445
+ });
446
+ const context = { root, active: true, identity };
220
447
  try {
221
- return await operation();
448
+ return await applicationLocks.run(context, operation);
222
449
  }
223
450
  finally {
224
- rmSync(lock, { recursive: true, force: true });
451
+ context.active = false;
452
+ await releaseApplicationLock(state, lock, identity);
225
453
  }
226
454
  }
@@ -0,0 +1,41 @@
1
+ import type { StandardProduct } from './stack-registry.js';
2
+ import { type DeliveryRunner } from './project-state.js';
3
+ export interface ProviderAuth {
4
+ id: string;
5
+ products: readonly StandardProduct[];
6
+ login?: readonly string[];
7
+ identity?: readonly string[];
8
+ credential?: string;
9
+ endpoint?: string;
10
+ runtime?: {
11
+ credential: string;
12
+ endpoint: string;
13
+ };
14
+ regionalEndpoints?: Readonly<Record<string, string>>;
15
+ setup: string;
16
+ }
17
+ /** Deployment access is separate from application-user authentication. */
18
+ export declare const PROVIDER_AUTH: readonly ProviderAuth[];
19
+ export declare function requiredProviders(root: string): ProviderAuth[];
20
+ export type AuthState = 'verified' | 'missing' | 'insufficient-permissions' | 'unverified';
21
+ export interface AuthCheck {
22
+ provider: string;
23
+ state: AuthState;
24
+ evidence: 'cli-session' | 'api-access' | 'none';
25
+ access: 'session' | 'management' | 'runtime' | 'none';
26
+ nextAction: string;
27
+ }
28
+ export interface AuthOptions {
29
+ provider?: string;
30
+ status?: boolean;
31
+ interactive?: boolean;
32
+ }
33
+ export type LoginRunner = (command: readonly string[], root: string) => Promise<void>;
34
+ /** Never return provider output, tokens, user addresses or response bodies. */
35
+ export declare function authenticateProviders(root: string, options?: AuthOptions, run?: DeliveryRunner, send?: typeof fetch, startLogin?: LoginRunner, environment?: NodeJS.ProcessEnv): Promise<{
36
+ schemaVersion: number;
37
+ checks: AuthCheck[];
38
+ authenticated: boolean;
39
+ provisioningReady: boolean;
40
+ note: string;
41
+ }>;
@@ -0,0 +1,210 @@
1
+ import path from 'node:path';
2
+ import { EAS_CLI_VERSION, STACK_DEPENDENCIES } from './dependencies.js';
3
+ import { realpathSync } from 'node:fs';
4
+ import { spawn } from 'node:child_process';
5
+ import { readBoundedFile, MAX_PROJECT_JSON_BYTES } from './bounded-file.js';
6
+ import { validateSetupPlan } from './readiness.js';
7
+ import { resolveCommand } from './command.js';
8
+ import { defaultRunner } from './project-state.js';
9
+ import { providerConfiguration } from './providers/config.js';
10
+ import { INTEGRATION_PROVIDERS, PROVIDER_DEFINITIONS, } from './providers/definitions.js';
11
+ /** Deployment access is separate from application-user authentication. */
12
+ export const PROVIDER_AUTH = [
13
+ {
14
+ id: 'cloudflare',
15
+ products: [
16
+ 'vinext',
17
+ 'hono',
18
+ 'd1',
19
+ 'r2',
20
+ 'kv',
21
+ 'realtime',
22
+ 'queues',
23
+ 'cron',
24
+ 'workflows',
25
+ 'workers-ai',
26
+ 'basin',
27
+ ],
28
+ login: ['pnpm', 'dlx', `wrangler@${STACK_DEPENDENCIES.wrangler}`, 'login'],
29
+ identity: ['pnpm', 'dlx', `wrangler@${STACK_DEPENDENCIES.wrangler}`, 'whoami', '--json'],
30
+ setup: 'Select and connect the intended Cloudflare account before shipping.',
31
+ },
32
+ {
33
+ id: 'vercel',
34
+ products: [],
35
+ login: ['pnpm', 'dlx', 'vercel@62.7.0', 'login'],
36
+ identity: ['pnpm', 'dlx', 'vercel@62.7.0', 'whoami'],
37
+ setup: 'Connect the intended Vercel team and project before shipping.',
38
+ },
39
+ ...INTEGRATION_PROVIDERS.map((id) => {
40
+ const definition = PROVIDER_DEFINITIONS[id];
41
+ const { auth } = definition;
42
+ return {
43
+ id,
44
+ products: auth.products ?? [id],
45
+ credential: definition.credential,
46
+ endpoint: `${definition.origin}${auth.path}`,
47
+ ...(auth.runtime
48
+ ? {
49
+ runtime: {
50
+ credential: auth.runtime.credential,
51
+ endpoint: `${definition.origin}${auth.runtime.path}`,
52
+ },
53
+ }
54
+ : {}),
55
+ ...(definition.regionalOrigins
56
+ ? {
57
+ regionalEndpoints: Object.fromEntries(Object.entries(definition.regionalOrigins).map(([region, origin]) => [
58
+ region,
59
+ `${origin}${auth.path}`,
60
+ ])),
61
+ }
62
+ : {}),
63
+ setup: `Supply ${definition.credential} for management access.${auth.runtime ? ` ${auth.runtime.credential} checks runtime access only.` : ''} ${definition.ownerSteps.join(' ')} A successful probe does not establish all write permissions.`,
64
+ };
65
+ }),
66
+ {
67
+ id: 'expo',
68
+ products: ['expo'],
69
+ login: ['pnpm', 'dlx', `eas-cli@${EAS_CLI_VERSION}`, 'login'],
70
+ identity: ['pnpm', 'dlx', `eas-cli@${EAS_CLI_VERSION}`, 'whoami'],
71
+ setup: 'Authenticate the intended Expo account; store signing and enrollment are separate from EAS access.',
72
+ },
73
+ ];
74
+ export function requiredProviders(root) {
75
+ root = realpathSync(root);
76
+ let plan;
77
+ try {
78
+ plan = JSON.parse(readBoundedFile(path.join(root, 'anhedral.setup.json'), MAX_PROJECT_JSON_BYTES).toString('utf8'));
79
+ validateSetupPlan(plan);
80
+ }
81
+ catch {
82
+ throw new Error('A valid Anhedral setup plan is required before authentication.');
83
+ }
84
+ validateSetupPlan(plan);
85
+ const providers = PROVIDER_AUTH.filter((provider) => provider.products.some((product) => plan.products.includes(product)) ||
86
+ (provider.id === 'cloudflare' &&
87
+ plan.products.includes('next') &&
88
+ plan.hosting === 'cloudflare') ||
89
+ (provider.id === 'vercel' && plan.products.includes('next') && plan.hosting === 'vercel'));
90
+ return providers;
91
+ }
92
+ async function login(command, root) {
93
+ const resolved = resolveCommand(command[0], command.slice(1));
94
+ await new Promise((resolve, reject) => {
95
+ const child = spawn(resolved.command, resolved.args, {
96
+ cwd: root,
97
+ stdio: 'inherit',
98
+ shell: false,
99
+ });
100
+ child.once('error', () => reject(new Error('Provider login tool is unavailable.')));
101
+ child.once('exit', (code) => code === 0 ? resolve() : reject(new Error('Provider login did not complete.')));
102
+ });
103
+ }
104
+ /** Never return provider output, tokens, user addresses or response bodies. */
105
+ export async function authenticateProviders(root, options = {}, run = defaultRunner, send = fetch, startLogin = login, environment = process.env) {
106
+ root = realpathSync(root);
107
+ const required = requiredProviders(root);
108
+ if (options.provider && !required.some((item) => item.id === options.provider))
109
+ throw new Error('Requested provider is not required by this project.');
110
+ const selected = required.filter((item) => !options.provider || item.id === options.provider);
111
+ const configuration = providerConfiguration(root);
112
+ const checks = [];
113
+ for (const provider of selected) {
114
+ let state = 'unverified';
115
+ let evidence = 'none';
116
+ let access = 'none';
117
+ if (provider.identity) {
118
+ const inspect = async () => {
119
+ const output = await run(provider.identity[0], [...provider.identity.slice(1)], root, {
120
+ ...environment,
121
+ CI: '1',
122
+ NO_COLOR: '1',
123
+ });
124
+ if (provider.id === 'cloudflare') {
125
+ const identity = JSON.parse(output);
126
+ return (identity.loggedIn === true &&
127
+ Array.isArray(identity.accounts) &&
128
+ identity.accounts.some((account) => typeof account?.id === 'string' && /^[a-f0-9]{32}$/i.test(account.id)));
129
+ }
130
+ return (output.length <= 1024 &&
131
+ /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,99}$/.test(output.trim()) &&
132
+ !/^(?:unauthenticated|anonymous|null|undefined|error|false)$/i.test(output.trim()));
133
+ };
134
+ try {
135
+ if (await inspect()) {
136
+ state = 'verified';
137
+ evidence = 'cli-session';
138
+ access = 'session';
139
+ }
140
+ else
141
+ state = 'missing';
142
+ }
143
+ catch {
144
+ state = 'missing';
145
+ }
146
+ if (state === 'missing' && !options.status && options.interactive && provider.login) {
147
+ try {
148
+ await startLogin(provider.login, root);
149
+ if (await inspect()) {
150
+ state = 'verified';
151
+ evidence = 'cli-session';
152
+ access = 'session';
153
+ }
154
+ }
155
+ catch {
156
+ state = 'missing';
157
+ }
158
+ }
159
+ }
160
+ else if (provider.credential) {
161
+ let credential = environment[provider.credential];
162
+ let endpoint = provider.endpoint;
163
+ let credentialAccess = 'management';
164
+ if (!credential && provider.runtime) {
165
+ credential = environment[provider.runtime.credential];
166
+ endpoint = provider.runtime.endpoint;
167
+ credentialAccess = 'runtime';
168
+ }
169
+ if (provider.regionalEndpoints) {
170
+ const region = configuration.providers.posthog?.region;
171
+ endpoint = region ? provider.regionalEndpoints[region] : undefined;
172
+ }
173
+ if (!credential)
174
+ state = 'missing';
175
+ else if (endpoint) {
176
+ try {
177
+ const response = await send(endpoint, {
178
+ headers: { Authorization: `Bearer ${credential}` },
179
+ redirect: 'error',
180
+ signal: AbortSignal.timeout(30_000),
181
+ });
182
+ if (response.ok) {
183
+ state = 'verified';
184
+ evidence = 'api-access';
185
+ access = credentialAccess;
186
+ }
187
+ else
188
+ state =
189
+ response.status === 403
190
+ ? 'insufficient-permissions'
191
+ : response.status === 401
192
+ ? 'missing'
193
+ : 'unverified';
194
+ await response.body?.cancel();
195
+ }
196
+ catch {
197
+ state = 'unverified';
198
+ }
199
+ }
200
+ }
201
+ checks.push({ provider: provider.id, state, evidence, access, nextAction: provider.setup });
202
+ }
203
+ return {
204
+ schemaVersion: 1,
205
+ checks,
206
+ authenticated: checks.every((check) => check.state === 'verified'),
207
+ provisioningReady: false,
208
+ note: 'Authentication checks do not establish selected account ownership, all provisioning permissions, configured runtime secrets or product readiness.',
209
+ };
210
+ }
@@ -0,0 +1,17 @@
1
+ import { type ApiRequest } from './api.js';
2
+ import { type IntegrationProvider, type ProviderSettings } from './config.js';
3
+ import type { ProvisioningState } from './state.js';
4
+ export interface AdapterContext {
5
+ id: IntegrationProvider;
6
+ settings: ProviderSettings;
7
+ api: ApiRequest;
8
+ state: ProvisioningState;
9
+ environment: NodeJS.ProcessEnv;
10
+ }
11
+ export interface AdapterResult {
12
+ resources: Record<string, string>;
13
+ runtime: Record<string, string>;
14
+ ready: boolean;
15
+ nextActions: string[];
16
+ }
17
+ export declare const PROVIDER_ADAPTERS: Record<IntegrationProvider, (context: AdapterContext) => Promise<AdapterResult>>;