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
@@ -0,0 +1,49 @@
1
+ import { PROVIDER_DEFINITIONS, PROVIDER_EVENT_PATTERN, PROVIDER_IDENTITY_PATTERN, PROVIDER_TEXT_PATTERN, PROVIDER_WEBHOOK_PATTERN, } from './definitions.js';
2
+ function fieldSchema(field) {
3
+ if (field.kind === 'events')
4
+ return {
5
+ type: 'array',
6
+ minItems: 1,
7
+ maxItems: 100,
8
+ uniqueItems: true,
9
+ items: { type: 'string', pattern: PROVIDER_EVENT_PATTERN },
10
+ };
11
+ return {
12
+ type: 'string',
13
+ maxLength: 255,
14
+ ...(field.choices
15
+ ? { enum: field.choices }
16
+ : {
17
+ pattern: field.kind === 'identity'
18
+ ? PROVIDER_IDENTITY_PATTERN
19
+ : field.kind === 'https-url'
20
+ ? PROVIDER_WEBHOOK_PATTERN
21
+ : PROVIDER_TEXT_PATTERN,
22
+ }),
23
+ };
24
+ }
25
+ /** Editor schema validates syntax; selected-stack and provisioning readiness checks
26
+ * remain application rules, since templates deliberately contain empty choices. */
27
+ export const PROVIDER_CONFIGURATION_SCHEMA = {
28
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
29
+ title: 'Anhedral provider configuration',
30
+ description: 'Nonsecret resource choices. Supply credentials through the environment.',
31
+ type: 'object',
32
+ additionalProperties: false,
33
+ required: ['schemaVersion', 'providers'],
34
+ properties: {
35
+ schemaVersion: { const: 1 },
36
+ providers: {
37
+ type: 'object',
38
+ additionalProperties: false,
39
+ properties: Object.fromEntries(Object.entries(PROVIDER_DEFINITIONS).map(([id, definition]) => [
40
+ id,
41
+ {
42
+ type: 'object',
43
+ additionalProperties: false,
44
+ properties: Object.fromEntries(Object.entries(definition.fields).map(([key, field]) => [key, fieldSchema(field)])),
45
+ },
46
+ ])),
47
+ },
48
+ },
49
+ };
@@ -0,0 +1,56 @@
1
+ import { atomicJson, type DeliveryRunner } from '../project-state.js';
2
+ import { type ApiObject, type ApiRequest } from './api.js';
3
+ import { type MinimalReceipt } from './receipts.js';
4
+ import { type IntegrationProvider, type ProviderSettings } from './config.js';
5
+ export interface OperationDescriptor {
6
+ key: string;
7
+ route: string;
8
+ }
9
+ export declare function privateStateDirectory(root: string, run: DeliveryRunner): Promise<string>;
10
+ export declare function readProviderStateStatus(directory: string): {
11
+ schemaVersion: number;
12
+ providers: {
13
+ provider: string;
14
+ operations: {
15
+ recovery: string;
16
+ descriptor?: OperationDescriptor | undefined;
17
+ resources?: MinimalReceipt | undefined;
18
+ operation: string;
19
+ status: "pending" | "complete";
20
+ }[];
21
+ }[];
22
+ };
23
+ /** Store credentials before completing a minimal receipt. A crash leaves a pending
24
+ * operation, never permission to replay a provider mutation with an unknown outcome. */
25
+ export declare class ProvisioningState {
26
+ private readonly write;
27
+ private readonly file;
28
+ private readonly credentialsFile;
29
+ private readonly state;
30
+ private readonly credentials;
31
+ constructor(directory: string, write?: typeof atomicJson);
32
+ private assertCredentials;
33
+ private retainCredentials;
34
+ private migrate;
35
+ /** No configurations, credentials, provider payloads or mutation bodies in status. */
36
+ status(): {
37
+ schemaVersion: number;
38
+ providers: {
39
+ provider: string;
40
+ operations: {
41
+ operation: string;
42
+ status: "pending" | "complete";
43
+ resources: MinimalReceipt;
44
+ }[];
45
+ }[];
46
+ };
47
+ context(id: IntegrationProvider, settings: ProviderSettings): void;
48
+ recoverResource(id: IntegrationProvider, operation: string, resource: string, settings: ProviderSettings, api: ApiRequest, beforeCommit?: () => void): Promise<{
49
+ provider: keyof import("./definitions.js").ProviderSettingsById;
50
+ operation: string;
51
+ status: string;
52
+ resources: MinimalReceipt;
53
+ mutation: string;
54
+ }>;
55
+ create(id: IntegrationProvider, key: string, api: ApiRequest, route: string, body: ApiObject): Promise<ApiObject>;
56
+ }
@@ -0,0 +1,329 @@
1
+ import path from 'node:path';
2
+ import { existsSync, lstatSync, mkdirSync, realpathSync } from 'node:fs';
3
+ import { readBoundedFile, MAX_PROJECT_JSON_BYTES } from '../bounded-file.js';
4
+ import { atomicJson } from '../project-state.js';
5
+ import { digest, object, ProviderRequestError } from './api.js';
6
+ import { verifyRecoveredResource } from './recovery-verification.js';
7
+ import { splitReceipt, validReceipt, validCredentials, restoreReceipt, } from './receipts.js';
8
+ import { INTEGRATION_PROVIDERS, } from './config.js';
9
+ function validDescriptor(value) {
10
+ return (record(value) &&
11
+ Object.keys(value).every((key) => ['key', 'route'].includes(key)) &&
12
+ typeof value.key === 'string' &&
13
+ /^(project|application|domain|web-app|ios-app|android-app|sending-key|webhook|service-account|client-key)$/.test(value.key) &&
14
+ typeof value.route === 'string' &&
15
+ value.route.length <= 1024 &&
16
+ /^\/[a-zA-Z0-9_/-]+$/.test(value.route) &&
17
+ !value.route.includes('//'));
18
+ }
19
+ function record(value) {
20
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
21
+ }
22
+ /** Validate saved operations before trusting their replay protection or resource values. */
23
+ function validState(value) {
24
+ if (!record(value) ||
25
+ !(value.schemaVersion === 1 || value.schemaVersion === 2) ||
26
+ Object.keys(value).some((key) => !['schemaVersion', 'providers'].includes(key)) ||
27
+ !record(value.providers))
28
+ return false;
29
+ return Object.entries(value.providers).every(([id, provider]) => {
30
+ if (!INTEGRATION_PROVIDERS.includes(id) ||
31
+ !record(provider) ||
32
+ Object.keys(provider).some((key) => !['config', 'operations'].includes(key)) ||
33
+ typeof provider.config !== 'string' ||
34
+ !/^[a-f0-9]{64}$/.test(provider.config) ||
35
+ !record(provider.operations))
36
+ return false;
37
+ return Object.entries(provider.operations).every(([key, operation]) => {
38
+ if (!/^[a-f0-9]{64}$/.test(key) ||
39
+ !record(operation) ||
40
+ Object.keys(operation).some((field) => !(value.schemaVersion === 1
41
+ ? ['status', 'value']
42
+ : ['status', 'value', 'credentialFields', 'descriptor']).includes(field)))
43
+ return false;
44
+ if (operation.descriptor !== undefined && !validDescriptor(operation.descriptor))
45
+ return false;
46
+ if (operation.status === 'pending')
47
+ return !Object.hasOwn(operation, 'value') && !Object.hasOwn(operation, 'credentialFields');
48
+ if (operation.status !== 'complete' || !record(operation.value))
49
+ return false;
50
+ if (value.schemaVersion === 1)
51
+ return true;
52
+ return (validReceipt(id, operation.value) &&
53
+ (operation.credentialFields === undefined ||
54
+ (Array.isArray(operation.credentialFields) &&
55
+ new Set(operation.credentialFields).size === operation.credentialFields.length &&
56
+ operation.credentialFields.every((field) => typeof field === 'string' &&
57
+ validCredentials(id, { [field]: 'validation-placeholder' })))));
58
+ });
59
+ });
60
+ }
61
+ export async function privateStateDirectory(root, run) {
62
+ root = realpathSync(root);
63
+ const directory = path.join(root, '.anhedral');
64
+ if (existsSync(directory) &&
65
+ (lstatSync(directory).isSymbolicLink() || !lstatSync(directory).isDirectory()))
66
+ throw new Error('Provider state directory must be a regular directory.');
67
+ const environment = { ...process.env, CI: '1', NO_COLOR: '1' };
68
+ if (realpathSync((await run('git', ['rev-parse', '--show-toplevel'], root, environment)).trim()) !==
69
+ root)
70
+ throw new Error('Provider configuration requires its own application repository.');
71
+ for (const file of [
72
+ '.anhedral/providers.json',
73
+ '.anhedral/provider-credentials.json',
74
+ '.anhedral/runtime.json',
75
+ ]) {
76
+ if ((await run('git', ['ls-files', '--', file], root, environment)).trim())
77
+ throw new Error('Provider state must not be tracked.');
78
+ if ((await run('git', ['check-ignore', '--no-index', '--', file], root, environment)).trim() !==
79
+ file)
80
+ throw new Error('Ignore .anhedral/ before provisioning providers.');
81
+ }
82
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
83
+ if (realpathSync(directory) !== directory)
84
+ throw new Error('Provider state directory escapes the project.');
85
+ return directory;
86
+ }
87
+ function validCredentialsState(value) {
88
+ return (record(value) &&
89
+ value.schemaVersion === 1 &&
90
+ !Object.keys(value).some((key) => !['schemaVersion', 'providers'].includes(key)) &&
91
+ record(value.providers) &&
92
+ Object.entries(value.providers).every(([id, operations]) => INTEGRATION_PROVIDERS.includes(id) &&
93
+ record(operations) &&
94
+ Object.entries(operations).every(([key, entries]) => /^[a-f0-9]{64}$/.test(key) && validCredentials(id, entries))));
95
+ }
96
+ function privateJson(file) {
97
+ const stat = lstatSync(file);
98
+ if (!stat.isFile() || stat.isSymbolicLink())
99
+ throw new Error('Provider state must be a regular private file.');
100
+ if (process.platform !== 'win32' && stat.mode & 0o077)
101
+ throw new Error('Provider state contains credentials; restrict its permissions to 0600.');
102
+ try {
103
+ return JSON.parse(readBoundedFile(file, MAX_PROJECT_JSON_BYTES).toString('utf8'));
104
+ }
105
+ catch {
106
+ throw new Error('Invalid provider state.');
107
+ }
108
+ }
109
+ export function readProviderStateStatus(directory) {
110
+ const file = path.join(directory, 'providers.json');
111
+ if (!existsSync(file))
112
+ return { schemaVersion: 2, providers: [] };
113
+ if (lstatSync(directory).isSymbolicLink() || !lstatSync(directory).isDirectory())
114
+ throw new Error('Provider state directory must be a regular private directory.');
115
+ const state = privateJson(file);
116
+ if (!validState(state))
117
+ throw new Error('Invalid provider state.');
118
+ return {
119
+ schemaVersion: 2,
120
+ providers: Object.entries(state.providers).map(([id, provider]) => ({
121
+ provider: id,
122
+ operations: Object.entries(provider.operations).map(([operation, saved]) => ({
123
+ operation,
124
+ status: saved.status,
125
+ ...(saved.value
126
+ ? { resources: splitReceipt(id, saved.value).receipt }
127
+ : {}),
128
+ ...(saved.descriptor ? { descriptor: saved.descriptor } : {}),
129
+ recovery: !saved.descriptor
130
+ ? 'unknown-descriptor'
131
+ : ['sending-key', 'webhook', 'service-account'].includes(saved.descriptor.key)
132
+ ? 'requires-one-time-credential'
133
+ : ['project', 'application', 'domain', 'web-app', 'ios-app', 'android-app'].includes(saved.descriptor.key)
134
+ ? 'verified-resource'
135
+ : 'unsupported-operation',
136
+ })),
137
+ })),
138
+ };
139
+ }
140
+ /** Store credentials before completing a minimal receipt. A crash leaves a pending
141
+ * operation, never permission to replay a provider mutation with an unknown outcome. */
142
+ export class ProvisioningState {
143
+ write;
144
+ file;
145
+ credentialsFile;
146
+ state;
147
+ credentials;
148
+ constructor(directory, write = atomicJson) {
149
+ this.write = write;
150
+ if (lstatSync(directory).isSymbolicLink() || !lstatSync(directory).isDirectory())
151
+ throw new Error('Provider state directory must be a regular private directory.');
152
+ directory = realpathSync(directory);
153
+ this.file = path.join(directory, 'providers.json');
154
+ this.credentialsFile = path.join(directory, 'provider-credentials.json');
155
+ const credentials = existsSync(this.credentialsFile)
156
+ ? privateJson(this.credentialsFile)
157
+ : { schemaVersion: 1, providers: {} };
158
+ if (!validCredentialsState(credentials))
159
+ throw new Error('Invalid provider credential state.');
160
+ this.credentials = credentials;
161
+ const state = existsSync(this.file)
162
+ ? privateJson(this.file)
163
+ : { schemaVersion: 2, providers: {} };
164
+ if (!validState(state))
165
+ throw new Error('Invalid provider state.');
166
+ this.state = state;
167
+ if (state.schemaVersion === 1)
168
+ this.migrate();
169
+ this.assertCredentials();
170
+ }
171
+ assertCredentials() {
172
+ for (const [id, provider] of Object.entries(this.state.providers)) {
173
+ for (const [operation, saved] of Object.entries(provider.operations)) {
174
+ const credentials = this.credentials.providers[id]?.[operation] ?? {};
175
+ if (saved.credentialFields?.some((field) => !credentials[field]))
176
+ throw new Error('Provider creation credentials are missing. Restore private state before retrying.');
177
+ }
178
+ }
179
+ }
180
+ retainCredentials(id, operation, entries) {
181
+ if (!Object.keys(entries).length)
182
+ return;
183
+ const provider = (this.credentials.providers[id] ??= {});
184
+ const previous = provider[operation];
185
+ if (previous && digest(previous) !== digest(entries))
186
+ throw new Error('Provider credential state conflicts with the saved creation result.');
187
+ provider[operation] = entries;
188
+ }
189
+ migrate() {
190
+ const migrated = { schemaVersion: 2, providers: {} };
191
+ for (const id of INTEGRATION_PROVIDERS) {
192
+ const provider = this.state.providers[id];
193
+ if (!provider)
194
+ continue;
195
+ const operations = {};
196
+ for (const [key, saved] of Object.entries(provider.operations)) {
197
+ if (saved.status === 'pending')
198
+ operations[key] = { status: 'pending' };
199
+ else {
200
+ const { receipt, credentials } = splitReceipt(id, saved.value);
201
+ this.retainCredentials(id, key, credentials);
202
+ operations[key] = {
203
+ status: 'complete',
204
+ value: receipt,
205
+ ...(Object.keys(credentials).length
206
+ ? { credentialFields: Object.keys(credentials) }
207
+ : {}),
208
+ };
209
+ }
210
+ }
211
+ migrated.providers[id] = { config: provider.config, operations };
212
+ }
213
+ // Credentials first: if the receipt write fails, the original schema 1 response
214
+ // remains intact and a retry can safely repeat this local migration.
215
+ this.write(this.credentialsFile, this.credentials);
216
+ this.write(this.file, migrated);
217
+ this.state.schemaVersion = 2;
218
+ this.state.providers = migrated.providers;
219
+ }
220
+ /** No configurations, credentials, provider payloads or mutation bodies in status. */
221
+ status() {
222
+ return {
223
+ schemaVersion: 2,
224
+ providers: Object.entries(this.state.providers).map(([id, provider]) => ({
225
+ provider: id,
226
+ operations: Object.entries(provider.operations).map(([operation, saved]) => ({
227
+ operation,
228
+ status: saved.status,
229
+ resources: saved.value ?? {},
230
+ })),
231
+ })),
232
+ };
233
+ }
234
+ context(id, settings) {
235
+ const hash = digest(settings);
236
+ const saved = this.state.providers[id];
237
+ if (saved && saved.config !== hash)
238
+ throw new Error(`${id}: desired configuration changed. Reconcile saved resource IDs before changing provider state.`);
239
+ if (!saved)
240
+ this.state.providers[id] = { config: hash, operations: {} };
241
+ }
242
+ async recoverResource(id, operation, resource, settings, api, beforeCommit = () => { }) {
243
+ this.context(id, settings);
244
+ const provider = this.state.providers[id];
245
+ const saved = provider.operations[operation];
246
+ if (!saved || saved.status !== 'pending')
247
+ throw new Error('Only an explicitly identified pending provider operation can be reconciled.');
248
+ if (!saved.descriptor)
249
+ throw new Error('This legacy pending operation has no trusted descriptor. Inspect the provider and retain its journal; automatic recovery is unavailable.');
250
+ const receipt = await verifyRecoveredResource(id, saved.descriptor, resource, settings, api);
251
+ beforeCommit();
252
+ const complete = {
253
+ status: 'complete',
254
+ descriptor: saved.descriptor,
255
+ value: receipt,
256
+ };
257
+ provider.operations[operation] = complete;
258
+ try {
259
+ this.write(this.file, this.state);
260
+ }
261
+ catch (error) {
262
+ provider.operations[operation] = saved;
263
+ throw error;
264
+ }
265
+ return {
266
+ provider: id,
267
+ operation,
268
+ status: 'complete',
269
+ resources: receipt,
270
+ mutation: 'local-receipt-only',
271
+ };
272
+ }
273
+ async create(id, key, api, route, body) {
274
+ const state = this.state.providers[id];
275
+ if (!state)
276
+ throw new Error('Select a provider configuration before creating resources.');
277
+ const operation = digest({ key, route, body });
278
+ const saved = state.operations[operation];
279
+ if (saved?.status === 'complete')
280
+ return restoreReceipt(saved.value, this.credentials.providers[id]?.[operation] ?? {});
281
+ if (saved)
282
+ throw new Error(`${id}: interrupted ${key} creation has an unknown outcome. Inspect the provider and reconcile the saved operation before retrying.`);
283
+ const descriptor = { key, route };
284
+ if (!validDescriptor(descriptor))
285
+ throw new Error('Unsupported provider operation descriptor.');
286
+ state.operations[operation] = { status: 'pending', descriptor };
287
+ this.write(this.file, this.state);
288
+ let value;
289
+ try {
290
+ value = object(await api(route, 'POST', body, operation));
291
+ }
292
+ catch (error) {
293
+ if (error instanceof ProviderRequestError && error.rejected) {
294
+ delete state.operations[operation];
295
+ this.write(this.file, this.state);
296
+ }
297
+ throw error;
298
+ }
299
+ const { receipt, credentials } = splitReceipt(id, value);
300
+ const expected = id === 'resend' && key === 'sending-key'
301
+ ? 'token'
302
+ : id === 'stripe' && key === 'webhook'
303
+ ? 'secret'
304
+ : id === 'openai' && key === 'service-account'
305
+ ? 'api_key'
306
+ : undefined;
307
+ if (expected && !credentials[expected])
308
+ throw new Error('Provider omitted the one-time creation credential. Inspect the pending operation before retrying.');
309
+ this.retainCredentials(id, operation, credentials);
310
+ if (Object.keys(credentials).length)
311
+ this.write(this.credentialsFile, this.credentials);
312
+ state.operations[operation] = {
313
+ status: 'complete',
314
+ descriptor,
315
+ value: receipt,
316
+ ...(Object.keys(credentials).length
317
+ ? { credentialFields: Object.keys(credentials) }
318
+ : {}),
319
+ };
320
+ try {
321
+ this.write(this.file, this.state);
322
+ }
323
+ catch (error) {
324
+ state.operations[operation] = { status: 'pending', descriptor };
325
+ throw error;
326
+ }
327
+ return restoreReceipt(receipt, credentials);
328
+ }
329
+ }
@@ -1,7 +1,7 @@
1
1
  import { type DeliveryRunner } from './project-state.js';
2
2
  import { type CloudflareRequest } from './cloudflare.js';
3
3
  interface Resource {
4
- kind: 'd1' | 'r2' | 'kv' | 'queue';
4
+ kind: 'd1' | 'r2' | 'kv' | 'queue' | 'stream';
5
5
  name: string;
6
6
  id?: string;
7
7
  target: string;
package/dist/provision.js CHANGED
@@ -8,9 +8,28 @@ const resourceTypes = {
8
8
  r2: { endpoint: '/r2/buckets', nameField: 'name', idField: 'name' },
9
9
  kv: { endpoint: '/storage/kv/namespaces', nameField: 'title', idField: 'id' },
10
10
  queue: { endpoint: '/queues', nameField: 'queue_name', idField: 'queue_id' },
11
+ stream: { endpoint: '/pipelines/v1/streams', nameField: 'name', idField: 'id' },
11
12
  };
12
13
  async function listResources(request, kind) {
13
14
  const { endpoint } = resourceTypes[kind];
15
+ if (kind === 'stream') {
16
+ const streams = [];
17
+ let bytes = 0;
18
+ for (let page = 0; page < 1000; page++) {
19
+ const response = await request('GET', `${endpoint}?page=${page}&per_page=100`);
20
+ if (!Array.isArray(response.result))
21
+ throw new Error('Unsupported stream listing.');
22
+ if (response.result.some((item) => !item || typeof item !== 'object' || Array.isArray(item)))
23
+ throw new Error('Unsupported stream resource.');
24
+ streams.push(...response.result);
25
+ bytes += Buffer.byteLength(JSON.stringify(response.result));
26
+ if (streams.length > 10_000 || bytes > 10 * 1024 * 1024)
27
+ throw new Error('Stream listing exceeded collection limit; select resource IDs explicitly.');
28
+ if (response.result.length < 100)
29
+ return streams;
30
+ }
31
+ throw new Error('Stream listing exceeded pagination limit.');
32
+ }
14
33
  if (kind !== 'r2')
15
34
  return listCloudflare(request, endpoint);
16
35
  const value = (await request('GET', endpoint)).result;
@@ -18,6 +37,15 @@ async function listResources(request, kind) {
18
37
  throw new Error('Unsupported or truncated R2 bucket listing; select resources explicitly with provider tooling.');
19
38
  return value.buckets;
20
39
  }
40
+ async function assertPrivateStream(request, id) {
41
+ if (!/^[a-f0-9]{32}$/i.test(id))
42
+ throw new Error('Invalid Basin stream ID.');
43
+ const stream = (await request('GET', `${resourceTypes.stream.endpoint}/${id}`)).result;
44
+ if (stream?.http?.enabled !== false ||
45
+ stream.worker_binding?.enabled !== true ||
46
+ stream.format?.type !== 'json')
47
+ throw new Error('Basin stream must use a private Worker binding with JSON events and HTTP ingestion disabled.');
48
+ }
21
49
  /** Provisioning and recovery use the same independently verified private-storage policy. */
22
50
  async function assertPrivateBucket(request, name) {
23
51
  const endpoint = `${resourceTypes.r2.endpoint}/${encodeURIComponent(name)}/domains`;
@@ -79,12 +107,30 @@ export function planProvisioning(root) {
79
107
  target: target.id,
80
108
  });
81
109
  }
110
+ if (config.pipelines !== undefined) {
111
+ if (!Array.isArray(config.pipelines))
112
+ throw new Error('Invalid Basin stream bindings.');
113
+ config.pipelines.forEach((entry, index) => {
114
+ if (!entry ||
115
+ typeof entry.binding !== 'string' ||
116
+ typeof entry.stream !== 'string' ||
117
+ entry.pipeline)
118
+ throw new Error('Use explicit Basin stream bindings.');
119
+ resources.push({
120
+ kind: 'stream',
121
+ name: resourceName(`${project.name}-${entry.binding.toLowerCase()}`),
122
+ target: target.id,
123
+ id: entry.stream.startsWith('REPLACE_WITH_') ? undefined : entry.stream,
124
+ field: ['pipelines', index, 'stream'],
125
+ });
126
+ });
127
+ }
82
128
  }
83
129
  return {
84
130
  projectId: project.id,
85
131
  accountId: project.accountId,
86
132
  resources,
87
- externalIntegrations: ['hyperdrive', 'pipelines'].filter((key) => project.targets.some((target) => key in readConfig(targetDirectory(root, target)))),
133
+ externalIntegrations: ['hyperdrive'].filter((key) => project.targets.some((target) => key in readConfig(targetDirectory(root, target)))),
88
134
  };
89
135
  }
90
136
  export async function provisionApplication(root, dryRun = false, run = defaultRunner, suppliedRequest) {
@@ -125,8 +171,16 @@ export async function provisionApplication(root, dryRun = false, run = defaultRu
125
171
  throw new Error('Configured resource ID does not match its account/name; no replacement was created.');
126
172
  assertConfiguration();
127
173
  if (!value)
128
- value = (await request('POST', endpoint, { [nameField]: resource.name }))
129
- .result;
174
+ value = (await request('POST', endpoint, {
175
+ [nameField]: resource.name,
176
+ ...(resource.kind === 'stream'
177
+ ? {
178
+ http: { enabled: false, authentication: true },
179
+ worker_binding: { enabled: true },
180
+ format: { type: 'json', unstructured: true },
181
+ }
182
+ : {}),
183
+ })).result;
130
184
  if (!value || typeof value[idField] !== 'string' || !value[idField])
131
185
  throw new Error('Provider did not return a resource identity; inspect provider state before retrying.');
132
186
  receipt = {
@@ -148,6 +202,8 @@ export async function provisionApplication(root, dryRun = false, run = defaultRu
148
202
  });
149
203
  if (resource.kind === 'r2')
150
204
  await assertPrivateBucket(request, resource.name);
205
+ if (resource.kind === 'stream')
206
+ await assertPrivateStream(request, receipt.id);
151
207
  }
152
208
  assertConfiguration();
153
209
  if (resource.id && receipt.id !== resource.id)
@@ -191,6 +247,8 @@ export async function inspectApplicationResources(root, request) {
191
247
  throw new Error('Committed resource identity differs from the registered account; recovery will not replace it.');
192
248
  if (resource.kind === 'r2')
193
249
  await assertPrivateBucket(request, resource.name);
250
+ if (resource.kind === 'stream')
251
+ await assertPrivateStream(request, id);
194
252
  receipts.push({ kind: resource.kind, name: resource.name, id, target: resource.target });
195
253
  }
196
254
  return { resources: receipts, externalIntegrations: plan.externalIntegrations };
package/dist/readiness.js CHANGED
@@ -1,4 +1,5 @@
1
- import { existsSync, readFileSync } from 'node:fs';
1
+ import { readBoundedFile, MAX_PROJECT_JSON_BYTES } from './bounded-file.js';
2
+ import { existsSync } from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import { isDeepStrictEqual } from 'node:util';
4
5
  import { inspectWorkspaceGraph } from './workspace-graph.js';
@@ -8,11 +9,16 @@ import { STANDARD_PRODUCTS } from './stack-registry.js';
8
9
  export function inspectProject(root) {
9
10
  const checks = [];
10
11
  const record = (id, ok, detail) => checks.push({ id, ok, detail });
11
- const read = (file) => JSON.parse(readFileSync(path.join(root, file), 'utf8'));
12
+ const read = (file) => JSON.parse(readBoundedFile(path.join(root, file), MAX_PROJECT_JSON_BYTES).toString('utf8'));
12
13
  record('workspace', existsSync(path.join(root, 'pnpm-workspace.yaml')), 'pnpm workspace manifest');
13
14
  record('lockfile', existsSync(path.join(root, 'pnpm-lock.yaml')), 'Committed dependency resolution required');
14
15
  try {
15
16
  const plan = read('anhedral.setup.json');
17
+ const recordedVersion = plan?.cliVersion;
18
+ const compatible = recordedVersion === CAPABILITY_REGISTRY.cliVersion;
19
+ record('cli-compatibility', compatible, /^\d+\.\d+\.\d+$/.test(recordedVersion ?? '')
20
+ ? `Use anhedral@${recordedVersion} for this project or review upgrade differences`
21
+ : 'Use the recorded project CLI version or review upgrade differences');
16
22
  validateSetupPlan(plan);
17
23
  const standard = read('anhedral.standard.json');
18
24
  record('setup-plan', true, `Requirements recorded by CLI ${plan.cliVersion}`);
@@ -40,7 +46,6 @@ export function inspectProject(root) {
40
46
  catch {
41
47
  record('workspace-graph', false, 'Invalid workspace manifests, references or dependency cycles');
42
48
  }
43
- record('cli-compatibility', plan.cliVersion === CAPABILITY_REGISTRY.cliVersion, 'Use the project CLI version or review upgrade differences');
44
49
  record('selection', standard.schemaVersion === 1 &&
45
50
  JSON.stringify(standard.products) === JSON.stringify(plan.products) &&
46
51
  standard.hosting ===
@@ -68,6 +73,9 @@ export function validateSetupPlan(plan) {
68
73
  value.registryVersion !== 1 ||
69
74
  typeof value.cliVersion !== 'string')
70
75
  throw new Error('Unsupported setup plan version.');
76
+ if (/^\d+\.\d+\.\d+$/.test(value.cliVersion) &&
77
+ value.cliVersion !== CAPABILITY_REGISTRY.cliVersion)
78
+ throw new Error(`Use anhedral@${value.cliVersion} for this project or review upgrade differences.`);
71
79
  if (!Array.isArray(value.products) ||
72
80
  !value.products.length ||
73
81
  !value.products.every((id) => STANDARD_PRODUCTS.includes(id)))
@@ -1,11 +1,17 @@
1
1
  import type { StandardRunner } from './types.js';
2
+ import { type SourcePreview } from './preview.js';
2
3
  export interface AddRequest {
3
4
  items: string[];
4
5
  dryRun?: boolean;
5
6
  skipInstall?: boolean;
7
+ preview?: {
8
+ mode: 'diff' | 'view';
9
+ path?: string;
10
+ };
6
11
  }
7
12
  /** Add recipes to an initialized workspace, refusing edits to customer-owned source. */
8
13
  export declare function addStackItems(root: string, request: AddRequest, run?: StandardRunner): Promise<{
14
+ previews?: SourcePreview[] | undefined;
9
15
  schemaVersion: number;
10
16
  root: string;
11
17
  dryRun: boolean;