@mizchi/k1c 0.1.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 (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +150 -0
  3. package/dist/canary/dispatcher-template.d.ts +17 -0
  4. package/dist/canary/dispatcher-template.js +42 -0
  5. package/dist/canary/effects-cloudflare.d.ts +9 -0
  6. package/dist/canary/effects-cloudflare.js +66 -0
  7. package/dist/canary/rollout-command.d.ts +15 -0
  8. package/dist/canary/rollout-command.js +92 -0
  9. package/dist/canary/runtime.d.ts +59 -0
  10. package/dist/canary/runtime.js +138 -0
  11. package/dist/canary/state-machine.d.ts +72 -0
  12. package/dist/canary/state-machine.js +161 -0
  13. package/dist/cli/args.d.ts +51 -0
  14. package/dist/cli/args.js +239 -0
  15. package/dist/cli/canary-integration.d.ts +11 -0
  16. package/dist/cli/canary-integration.js +101 -0
  17. package/dist/cli/format.d.ts +4 -0
  18. package/dist/cli/format.js +44 -0
  19. package/dist/cli/main.d.ts +3 -0
  20. package/dist/cli/main.js +158 -0
  21. package/dist/cli/run.d.ts +16 -0
  22. package/dist/cli/run.js +246 -0
  23. package/dist/manifest/lower.d.ts +22 -0
  24. package/dist/manifest/lower.js +913 -0
  25. package/dist/manifest/parse.d.ts +22 -0
  26. package/dist/manifest/parse.js +106 -0
  27. package/dist/manifest/schemas.d.ts +10359 -0
  28. package/dist/manifest/schemas.js +309 -0
  29. package/dist/manifest/types.d.ts +246 -0
  30. package/dist/manifest/types.js +12 -0
  31. package/dist/providers/configmap.d.ts +8 -0
  32. package/dist/providers/configmap.js +29 -0
  33. package/dist/providers/custom-domain.d.ts +11 -0
  34. package/dist/providers/custom-domain.js +120 -0
  35. package/dist/providers/d1-database.d.ts +9 -0
  36. package/dist/providers/d1-database.js +106 -0
  37. package/dist/providers/dispatch-namespace.d.ts +8 -0
  38. package/dist/providers/dispatch-namespace.js +100 -0
  39. package/dist/providers/dns-record.d.ts +14 -0
  40. package/dist/providers/dns-record.js +136 -0
  41. package/dist/providers/errors.d.ts +8 -0
  42. package/dist/providers/errors.js +64 -0
  43. package/dist/providers/hyperdrive.d.ts +27 -0
  44. package/dist/providers/hyperdrive.js +168 -0
  45. package/dist/providers/index.d.ts +6 -0
  46. package/dist/providers/index.js +36 -0
  47. package/dist/providers/kv-namespace.d.ts +8 -0
  48. package/dist/providers/kv-namespace.js +90 -0
  49. package/dist/providers/logpush-job.d.ts +17 -0
  50. package/dist/providers/logpush-job.js +181 -0
  51. package/dist/providers/queue.d.ts +10 -0
  52. package/dist/providers/queue.js +124 -0
  53. package/dist/providers/r2-bucket.d.ts +11 -0
  54. package/dist/providers/r2-bucket.js +98 -0
  55. package/dist/providers/registry.d.ts +9 -0
  56. package/dist/providers/registry.js +22 -0
  57. package/dist/providers/secret.d.ts +8 -0
  58. package/dist/providers/secret.js +30 -0
  59. package/dist/providers/types.d.ts +69 -0
  60. package/dist/providers/types.js +12 -0
  61. package/dist/providers/vectorize.d.ts +11 -0
  62. package/dist/providers/vectorize.js +110 -0
  63. package/dist/providers/worker.d.ts +106 -0
  64. package/dist/providers/worker.js +430 -0
  65. package/dist/providers/workflow.d.ts +10 -0
  66. package/dist/providers/workflow.js +103 -0
  67. package/dist/reconciler/apply.d.ts +10 -0
  68. package/dist/reconciler/apply.js +114 -0
  69. package/dist/reconciler/fake-provider.d.ts +48 -0
  70. package/dist/reconciler/fake-provider.js +83 -0
  71. package/dist/reconciler/plan.d.ts +6 -0
  72. package/dist/reconciler/plan.js +124 -0
  73. package/dist/reconciler/topo.d.ts +10 -0
  74. package/dist/reconciler/topo.js +53 -0
  75. package/dist/reconciler/types.d.ts +54 -0
  76. package/dist/reconciler/types.js +8 -0
  77. package/package.json +61 -0
@@ -0,0 +1,430 @@
1
+ import { z } from 'zod';
2
+ import { NotFound } from "./types.js";
3
+ import { toProviderError } from "./errors.js";
4
+ export const workerSchema = z.object({
5
+ scriptName: z.string(),
6
+ entrypoint: z.string(),
7
+ compatibilityDate: z.string(),
8
+ compatibilityFlags: z.array(z.string()).optional(),
9
+ vars: z.record(z.string()).optional(),
10
+ secrets: z.record(z.string()).optional(),
11
+ bindings: z
12
+ .array(z.discriminatedUnion('type', [
13
+ z.object({
14
+ type: z.literal('r2_bucket'),
15
+ name: z.string(),
16
+ bucketName: z.string(),
17
+ }),
18
+ z.object({
19
+ type: z.literal('kv_namespace'),
20
+ name: z.string(),
21
+ namespaceId: z.string(),
22
+ }),
23
+ z.object({
24
+ type: z.literal('service'),
25
+ name: z.string(),
26
+ service: z.string(),
27
+ }),
28
+ z.object({
29
+ type: z.literal('dispatch_namespace'),
30
+ name: z.string(),
31
+ dispatchNamespace: z.string(),
32
+ }),
33
+ z.object({
34
+ type: z.literal('hyperdrive'),
35
+ name: z.string(),
36
+ hyperdriveId: z.string(),
37
+ }),
38
+ z.object({
39
+ type: z.literal('d1'),
40
+ name: z.string(),
41
+ databaseId: z.string(),
42
+ }),
43
+ z.object({
44
+ type: z.literal('queue'),
45
+ name: z.string(),
46
+ queueName: z.string(),
47
+ }),
48
+ z.object({
49
+ type: z.literal('durable_object_namespace'),
50
+ name: z.string(),
51
+ className: z.string(),
52
+ scriptName: z.string().optional(),
53
+ }),
54
+ z.object({
55
+ type: z.literal('vectorize'),
56
+ name: z.string(),
57
+ indexName: z.string(),
58
+ }),
59
+ z.object({ type: z.literal('ai'), name: z.string() }),
60
+ z.object({ type: z.literal('browser'), name: z.string() }),
61
+ z.object({ type: z.literal('version_metadata'), name: z.string() }),
62
+ z.object({
63
+ type: z.literal('analytics_engine'),
64
+ name: z.string(),
65
+ dataset: z.string(),
66
+ }),
67
+ ]))
68
+ .optional(),
69
+ observability: z.object({ enabled: z.boolean() }).optional(),
70
+ placement: z.object({ mode: z.literal('smart') }).optional(),
71
+ dispatchNamespace: z.string().optional(),
72
+ entrypointContent: z.string().optional(),
73
+ entrypointHash: z.string().optional(),
74
+ cronSchedules: z.array(z.string()).optional(),
75
+ durableObjectClasses: z.array(z.string()).optional(),
76
+ });
77
+ const NAME_PREFIX = 'k1c--';
78
+ const SEPARATOR = '--';
79
+ const MAIN_MODULE = 'worker.mjs';
80
+ function parseLabel(scriptName) {
81
+ if (!scriptName.startsWith(NAME_PREFIX))
82
+ return null;
83
+ const rest = scriptName.slice(NAME_PREFIX.length);
84
+ const sepIdx = rest.indexOf(SEPARATOR);
85
+ if (sepIdx <= 0 || sepIdx + SEPARATOR.length === rest.length)
86
+ return null;
87
+ const namespace = rest.slice(0, sepIdx);
88
+ const name = rest.slice(sepIdx + SEPARATOR.length);
89
+ if (!name)
90
+ return null;
91
+ return `${namespace}/${name}`;
92
+ }
93
+ function buildBindings(props) {
94
+ const out = [];
95
+ for (const [name, text] of Object.entries(props.vars ?? {})) {
96
+ out.push({ type: 'plain_text', name, text });
97
+ }
98
+ for (const [name, text] of Object.entries(props.secrets ?? {})) {
99
+ out.push({ type: 'secret_text', name, text });
100
+ }
101
+ for (const b of props.bindings ?? []) {
102
+ if (b.type === 'r2_bucket') {
103
+ out.push({ type: 'r2_bucket', name: b.name, bucket_name: b.bucketName });
104
+ }
105
+ else if (b.type === 'kv_namespace') {
106
+ out.push({ type: 'kv_namespace', name: b.name, namespace_id: b.namespaceId });
107
+ }
108
+ else if (b.type === 'service') {
109
+ out.push({ type: 'service', name: b.name, service: b.service });
110
+ }
111
+ else if (b.type === 'dispatch_namespace') {
112
+ out.push({ type: 'dispatch_namespace', name: b.name, namespace: b.dispatchNamespace });
113
+ }
114
+ else if (b.type === 'hyperdrive') {
115
+ out.push({ type: 'hyperdrive', name: b.name, id: b.hyperdriveId });
116
+ }
117
+ else if (b.type === 'd1') {
118
+ out.push({ type: 'd1', name: b.name, id: b.databaseId });
119
+ }
120
+ else if (b.type === 'queue') {
121
+ out.push({ type: 'queue', name: b.name, queue_name: b.queueName });
122
+ }
123
+ else if (b.type === 'durable_object_namespace') {
124
+ out.push({
125
+ type: 'durable_object_namespace',
126
+ name: b.name,
127
+ class_name: b.className,
128
+ ...(b.scriptName !== undefined ? { script_name: b.scriptName } : {}),
129
+ });
130
+ }
131
+ else if (b.type === 'vectorize') {
132
+ out.push({ type: 'vectorize', name: b.name, index_name: b.indexName });
133
+ }
134
+ else if (b.type === 'ai' || b.type === 'browser' || b.type === 'version_metadata') {
135
+ out.push({ type: b.type, name: b.name });
136
+ }
137
+ else if (b.type === 'analytics_engine') {
138
+ out.push({ type: 'analytics_engine', name: b.name, dataset: b.dataset });
139
+ }
140
+ }
141
+ return out;
142
+ }
143
+ const CONTENT_HASH_TAG_PREFIX = 'k1c.io/content-hash=';
144
+ const DO_CLASSES_TAG_PREFIX = 'k1c.io/do-classes=';
145
+ function buildMetadata(ctx, props) {
146
+ const tags = [ctx.managedByLabel];
147
+ if (props.entrypointHash !== undefined) {
148
+ tags.push(`${CONTENT_HASH_TAG_PREFIX}${props.entrypointHash}`);
149
+ }
150
+ const classes = [...(props.durableObjectClasses ?? [])].sort();
151
+ if (classes.length > 0) {
152
+ tags.push(`${DO_CLASSES_TAG_PREFIX}${classes.join(',')}`);
153
+ }
154
+ // Auto-include self-pointing DO bindings for every declared class so the Worker
155
+ // can address its own DO instances by name (e.g. `env.<class>.idFromName(id)`).
156
+ const selfDoBindings = classes.map((className) => ({
157
+ type: 'durable_object_namespace',
158
+ name: className,
159
+ className,
160
+ }));
161
+ const allBindings = buildBindings({
162
+ ...props,
163
+ bindings: [...(props.bindings ?? []), ...selfDoBindings],
164
+ });
165
+ return {
166
+ main_module: MAIN_MODULE,
167
+ compatibility_date: props.compatibilityDate,
168
+ ...(props.compatibilityFlags !== undefined
169
+ ? { compatibility_flags: [...props.compatibilityFlags] }
170
+ : {}),
171
+ bindings: allBindings,
172
+ tags,
173
+ ...(props.observability !== undefined
174
+ ? { observability: { enabled: props.observability.enabled } }
175
+ : {}),
176
+ ...(props.placement !== undefined ? { placement: { mode: props.placement.mode } } : {}),
177
+ ...(classes.length > 0
178
+ ? {
179
+ migrations: {
180
+ new_sqlite_classes: classes,
181
+ new_tag: props.entrypointHash ?? 'k1c-initial',
182
+ },
183
+ }
184
+ : {}),
185
+ };
186
+ }
187
+ function extractContentHash(tags) {
188
+ if (!tags)
189
+ return undefined;
190
+ for (const tag of tags) {
191
+ if (tag.startsWith(CONTENT_HASH_TAG_PREFIX))
192
+ return tag.slice(CONTENT_HASH_TAG_PREFIX.length);
193
+ }
194
+ return undefined;
195
+ }
196
+ function extractDoClasses(tags) {
197
+ if (!tags)
198
+ return undefined;
199
+ for (const tag of tags) {
200
+ if (tag.startsWith(DO_CLASSES_TAG_PREFIX)) {
201
+ const list = tag.slice(DO_CLASSES_TAG_PREFIX.length);
202
+ if (!list)
203
+ return undefined;
204
+ return list.split(',').filter((s) => s.length > 0);
205
+ }
206
+ }
207
+ return undefined;
208
+ }
209
+ async function readEntrypoint(ctx, props) {
210
+ if (props.entrypointContent !== undefined) {
211
+ return new TextEncoder().encode(props.entrypointContent);
212
+ }
213
+ const reader = ctx.readFile ??
214
+ (async (p) => {
215
+ const fs = await import('node:fs/promises');
216
+ return fs.readFile(p);
217
+ });
218
+ return reader(props.entrypoint);
219
+ }
220
+ async function uploadAndDeploy(ctx, props) {
221
+ if (props.dispatchNamespace !== undefined) {
222
+ return uploadToDispatchNamespace(ctx, props, props.dispatchNamespace);
223
+ }
224
+ return uploadVersionAndDeploy(ctx, props);
225
+ }
226
+ async function uploadVersionAndDeploy(ctx, props) {
227
+ const content = await readEntrypoint(ctx, props);
228
+ const file = new File([content], MAIN_MODULE, { type: 'application/javascript+module' });
229
+ // 1. Upload a new immutable version (does not deploy to traffic).
230
+ const versionResult = await ctx.cloudflare.workers.scripts.versions.create(props.scriptName, {
231
+ account_id: ctx.accountId,
232
+ metadata: buildMetadata(ctx, props),
233
+ [MAIN_MODULE]: [file],
234
+ });
235
+ const versionId = versionResult.id;
236
+ if (!versionId) {
237
+ throw new Error('versions.create did not return a version id');
238
+ }
239
+ // 2. Cut over: route 100% of traffic to the new version (cutover semantics).
240
+ // canary.steps and blueGreen with manual promotion are not yet implemented (v0.1.2).
241
+ await ctx.cloudflare.workers.scripts.deployments.create(props.scriptName, {
242
+ account_id: ctx.accountId,
243
+ strategy: 'percentage',
244
+ versions: [{ version_id: versionId, percentage: 100 }],
245
+ });
246
+ // 3. Sync cron triggers if the manifest declared any (CronJob path).
247
+ await syncCronSchedules(ctx, props);
248
+ return { scriptId: props.scriptName, versionId };
249
+ }
250
+ async function syncCronSchedules(ctx, props) {
251
+ const schedules = props.cronSchedules ?? [];
252
+ await ctx.cloudflare.workers.scripts.schedules.update(props.scriptName, {
253
+ account_id: ctx.accountId,
254
+ body: schedules.map((cron) => ({ cron })),
255
+ });
256
+ }
257
+ async function uploadToDispatchNamespace(ctx, props, dispatchNamespace) {
258
+ const content = await readEntrypoint(ctx, props);
259
+ const file = new File([content], MAIN_MODULE, { type: 'application/javascript+module' });
260
+ // Dispatch-namespace scripts do not currently flow through the Versions/Deployments API.
261
+ // The dispatcher Worker invokes them by name on each request, so a single mutable script
262
+ // per name is the right model.
263
+ await ctx.cloudflare.workersForPlatforms.dispatch.namespaces.scripts.update(dispatchNamespace, props.scriptName, {
264
+ account_id: ctx.accountId,
265
+ metadata: buildMetadata(ctx, props),
266
+ files: { [MAIN_MODULE]: file },
267
+ });
268
+ return { scriptId: props.scriptName, versionId: 'dispatched' };
269
+ }
270
+ function fromCFBinding(b) {
271
+ if (b.type === 'r2_bucket' && b.bucket_name !== undefined) {
272
+ return { type: 'r2_bucket', name: b.name, bucketName: b.bucket_name };
273
+ }
274
+ if (b.type === 'kv_namespace' && b.namespace_id !== undefined) {
275
+ return { type: 'kv_namespace', name: b.name, namespaceId: b.namespace_id };
276
+ }
277
+ if (b.type === 'service' && b.service !== undefined) {
278
+ return { type: 'service', name: b.name, service: b.service };
279
+ }
280
+ if (b.type === 'dispatch_namespace' && b.namespace !== undefined) {
281
+ return { type: 'dispatch_namespace', name: b.name, dispatchNamespace: b.namespace };
282
+ }
283
+ if (b.type === 'hyperdrive' && b.id !== undefined) {
284
+ return { type: 'hyperdrive', name: b.name, hyperdriveId: b.id };
285
+ }
286
+ if (b.type === 'd1' && b.id !== undefined) {
287
+ return { type: 'd1', name: b.name, databaseId: b.id };
288
+ }
289
+ if (b.type === 'queue' && b.queue_name !== undefined) {
290
+ return { type: 'queue', name: b.name, queueName: b.queue_name };
291
+ }
292
+ if (b.type === 'durable_object_namespace' && b.class_name !== undefined) {
293
+ return {
294
+ type: 'durable_object_namespace',
295
+ name: b.name,
296
+ className: b.class_name,
297
+ ...(b.script_name !== undefined ? { scriptName: b.script_name } : {}),
298
+ };
299
+ }
300
+ if (b.type === 'vectorize' && b.index_name !== undefined) {
301
+ return { type: 'vectorize', name: b.name, indexName: b.index_name };
302
+ }
303
+ if (b.type === 'ai' || b.type === 'browser' || b.type === 'version_metadata') {
304
+ return { type: b.type, name: b.name };
305
+ }
306
+ if (b.type === 'analytics_engine' && b.dataset !== undefined) {
307
+ return { type: 'analytics_engine', name: b.name, dataset: b.dataset };
308
+ }
309
+ return null;
310
+ }
311
+ export const workerProvider = {
312
+ resourceType: 'Worker',
313
+ schema: workerSchema,
314
+ async *list(ctx) {
315
+ let iter;
316
+ try {
317
+ iter = ctx.cloudflare.workers.scripts.list({ account_id: ctx.accountId });
318
+ }
319
+ catch (raw) {
320
+ throw toProviderError(raw);
321
+ }
322
+ try {
323
+ for await (const script of iter) {
324
+ const id = script.id;
325
+ if (!id)
326
+ continue;
327
+ const label = parseLabel(id);
328
+ if (label === null)
329
+ continue;
330
+ yield { nativeId: id, label };
331
+ }
332
+ }
333
+ catch (raw) {
334
+ throw toProviderError(raw);
335
+ }
336
+ },
337
+ async read(ctx, nativeId) {
338
+ let response;
339
+ try {
340
+ response = await ctx.cloudflare.workers.scripts.scriptAndVersionSettings.get(nativeId, {
341
+ account_id: ctx.accountId,
342
+ });
343
+ }
344
+ catch (raw) {
345
+ const err = toProviderError(raw);
346
+ if (err.code === 'NotFound')
347
+ return NotFound;
348
+ throw err;
349
+ }
350
+ const settings = response;
351
+ const entrypointHash = extractContentHash(settings.tags);
352
+ const durableObjectClasses = extractDoClasses(settings.tags);
353
+ let cronSchedules;
354
+ try {
355
+ const sched = await ctx.cloudflare.workers.scripts.schedules.get(nativeId, {
356
+ account_id: ctx.accountId,
357
+ });
358
+ const list = sched.schedules ?? [];
359
+ const crons = list.map((s) => s.cron).filter((c) => typeof c === 'string');
360
+ if (crons.length > 0)
361
+ cronSchedules = crons;
362
+ }
363
+ catch {
364
+ // schedules.get may fail (no triggers, transient error) — treat as no schedules.
365
+ }
366
+ const vars = {};
367
+ const bindings = [];
368
+ for (const b of settings.bindings ?? []) {
369
+ if (b.type === 'plain_text' && b.text !== undefined) {
370
+ vars[b.name] = b.text;
371
+ continue;
372
+ }
373
+ if (b.type === 'secret_text') {
374
+ // Secret values are never returned by Cloudflare; skip silently.
375
+ continue;
376
+ }
377
+ const translated = fromCFBinding(b);
378
+ if (translated !== null)
379
+ bindings.push(translated);
380
+ }
381
+ const props = {
382
+ scriptName: nativeId,
383
+ entrypoint: '<read-from-cluster>',
384
+ compatibilityDate: settings.compatibility_date ?? '2025-01-01',
385
+ ...(settings.compatibility_flags !== undefined
386
+ ? { compatibilityFlags: settings.compatibility_flags }
387
+ : {}),
388
+ ...(Object.keys(vars).length > 0 ? { vars } : {}),
389
+ ...(bindings.length > 0 ? { bindings } : {}),
390
+ ...(settings.observability?.enabled !== undefined
391
+ ? { observability: { enabled: settings.observability.enabled } }
392
+ : {}),
393
+ ...(settings.placement?.mode === 'smart'
394
+ ? { placement: { mode: 'smart' } }
395
+ : {}),
396
+ ...(entrypointHash !== undefined ? { entrypointHash } : {}),
397
+ ...(cronSchedules !== undefined ? { cronSchedules } : {}),
398
+ ...(durableObjectClasses !== undefined ? { durableObjectClasses } : {}),
399
+ };
400
+ return props;
401
+ },
402
+ async create(ctx, _label, desired) {
403
+ try {
404
+ const { scriptId } = await uploadAndDeploy(ctx, desired);
405
+ return { kind: 'sync', nativeId: scriptId, properties: desired };
406
+ }
407
+ catch (raw) {
408
+ throw toProviderError(raw);
409
+ }
410
+ },
411
+ async update(ctx, _nativeId, _prior, desired) {
412
+ try {
413
+ const { scriptId } = await uploadAndDeploy(ctx, desired);
414
+ return { kind: 'sync', nativeId: scriptId, properties: desired };
415
+ }
416
+ catch (raw) {
417
+ throw toProviderError(raw);
418
+ }
419
+ },
420
+ async delete(ctx, nativeId) {
421
+ try {
422
+ await ctx.cloudflare.workers.scripts.delete(nativeId, { account_id: ctx.accountId });
423
+ return { kind: 'sync' };
424
+ }
425
+ catch (raw) {
426
+ throw toProviderError(raw);
427
+ }
428
+ },
429
+ };
430
+ //# sourceMappingURL=worker.js.map
@@ -0,0 +1,10 @@
1
+ import { z } from 'zod';
2
+ import type { CloudflareResourceProvider } from './types.ts';
3
+ export interface WorkflowProperties {
4
+ readonly workflowName: string;
5
+ readonly className: string;
6
+ readonly scriptName: string;
7
+ }
8
+ export declare const workflowPropsSchema: z.ZodType<WorkflowProperties>;
9
+ export declare const workflowProvider: CloudflareResourceProvider<WorkflowProperties>;
10
+ //# sourceMappingURL=workflow.d.ts.map
@@ -0,0 +1,103 @@
1
+ import { z } from 'zod';
2
+ import { NotFound } from "./types.js";
3
+ import { toProviderError } from "./errors.js";
4
+ export const workflowPropsSchema = z.object({
5
+ workflowName: z.string(),
6
+ className: z.string(),
7
+ scriptName: z.string(),
8
+ });
9
+ const NAME_PREFIX = 'k1c-';
10
+ function parseLabel(name) {
11
+ if (!name.startsWith(NAME_PREFIX))
12
+ return null;
13
+ const rest = name.slice(NAME_PREFIX.length);
14
+ const dash = rest.indexOf('-');
15
+ if (dash <= 0 || dash === rest.length - 1)
16
+ return null;
17
+ return `${rest.slice(0, dash)}/${rest.slice(dash + 1)}`;
18
+ }
19
+ export const workflowProvider = {
20
+ resourceType: 'Workflow',
21
+ schema: workflowPropsSchema,
22
+ async *list(ctx) {
23
+ let iter;
24
+ try {
25
+ iter = ctx.cloudflare.workflows.list({ account_id: ctx.accountId });
26
+ }
27
+ catch (raw) {
28
+ throw toProviderError(raw);
29
+ }
30
+ try {
31
+ for await (const wf of iter) {
32
+ const name = wf.name;
33
+ if (!name)
34
+ continue;
35
+ const label = parseLabel(name);
36
+ if (label === null)
37
+ continue;
38
+ // Workflows are addressed by name, not by a separate id.
39
+ yield { nativeId: name, label };
40
+ }
41
+ }
42
+ catch (raw) {
43
+ throw toProviderError(raw);
44
+ }
45
+ },
46
+ async read(ctx, nativeId) {
47
+ try {
48
+ const wf = await ctx.cloudflare.workflows.get(nativeId, { account_id: ctx.accountId });
49
+ const w = wf;
50
+ if (!w.name || !w.class_name || !w.script_name)
51
+ return NotFound;
52
+ return {
53
+ workflowName: w.name,
54
+ className: w.class_name,
55
+ scriptName: w.script_name,
56
+ };
57
+ }
58
+ catch (raw) {
59
+ const err = toProviderError(raw);
60
+ if (err.code === 'NotFound')
61
+ return NotFound;
62
+ throw err;
63
+ }
64
+ },
65
+ async create(ctx, _label, desired) {
66
+ // Cloudflare's Workflow registration uses PUT (the `update` method) for both
67
+ // create and update — there is no separate POST endpoint.
68
+ try {
69
+ await ctx.cloudflare.workflows.update(desired.workflowName, {
70
+ account_id: ctx.accountId,
71
+ class_name: desired.className,
72
+ script_name: desired.scriptName,
73
+ });
74
+ return { kind: 'sync', nativeId: desired.workflowName, properties: desired };
75
+ }
76
+ catch (raw) {
77
+ throw toProviderError(raw);
78
+ }
79
+ },
80
+ async update(ctx, nativeId, _prior, desired) {
81
+ try {
82
+ await ctx.cloudflare.workflows.update(nativeId, {
83
+ account_id: ctx.accountId,
84
+ class_name: desired.className,
85
+ script_name: desired.scriptName,
86
+ });
87
+ return { kind: 'sync', nativeId, properties: desired };
88
+ }
89
+ catch (raw) {
90
+ throw toProviderError(raw);
91
+ }
92
+ },
93
+ async delete(ctx, nativeId) {
94
+ try {
95
+ await ctx.cloudflare.workflows.delete(nativeId, { account_id: ctx.accountId });
96
+ return { kind: 'sync' };
97
+ }
98
+ catch (raw) {
99
+ throw toProviderError(raw);
100
+ }
101
+ },
102
+ };
103
+ //# sourceMappingURL=workflow.js.map
@@ -0,0 +1,10 @@
1
+ import type { ProviderContext } from '../providers/types.ts';
2
+ import type { ProviderRegistry } from '../providers/registry.ts';
3
+ import type { ApplyReport, Plan } from './types.ts';
4
+ export interface ApplyOptions {
5
+ readonly retries?: number;
6
+ readonly backoffMs?: number;
7
+ readonly dryRun?: boolean;
8
+ }
9
+ export declare function apply(plan: Plan, registry: ProviderRegistry, ctx: ProviderContext, options?: ApplyOptions): Promise<ApplyReport>;
10
+ //# sourceMappingURL=apply.d.ts.map
@@ -0,0 +1,114 @@
1
+ const DEFAULT_OPTIONS = {
2
+ retries: 3,
3
+ backoffMs: 200,
4
+ dryRun: false,
5
+ };
6
+ export async function apply(plan, registry, ctx, options) {
7
+ const opts = { ...DEFAULT_OPTIONS, ...options };
8
+ // plan() returns operations already topologically ordered:
9
+ // (creates+updates in dependency order) → noops → deletes.
10
+ // apply runs them in that order; hand-constructed plans take responsibility for ordering.
11
+ const results = [];
12
+ let aborted = false;
13
+ for (const op of plan.operations) {
14
+ if (aborted) {
15
+ results.push({ op, status: 'skipped' });
16
+ continue;
17
+ }
18
+ if (opts.dryRun) {
19
+ results.push({ op, status: 'skipped' });
20
+ continue;
21
+ }
22
+ if (op.kind === 'noop') {
23
+ results.push({ op, status: 'succeeded' });
24
+ continue;
25
+ }
26
+ const result = await runOperation(op, registry, ctx, opts);
27
+ results.push(result);
28
+ if (result.status === 'failed')
29
+ aborted = true;
30
+ }
31
+ return summarize(results);
32
+ }
33
+ async function runOperation(op, registry, ctx, opts) {
34
+ let attempt = 0;
35
+ let lastError;
36
+ while (attempt <= opts.retries) {
37
+ try {
38
+ const nativeId = await execute(op, registry, ctx);
39
+ return { op, status: 'succeeded', ...(nativeId ? { nativeId } : {}) };
40
+ }
41
+ catch (raw) {
42
+ const err = toProviderError(raw);
43
+ lastError = err;
44
+ if (!err.recoverable) {
45
+ return { op, status: 'failed', error: err };
46
+ }
47
+ attempt += 1;
48
+ if (attempt > opts.retries)
49
+ break;
50
+ if (opts.backoffMs > 0)
51
+ await sleep(opts.backoffMs * attempt);
52
+ }
53
+ }
54
+ return { op, status: 'failed', error: lastError ?? unknownError() };
55
+ }
56
+ async function execute(op, registry, ctx) {
57
+ switch (op.kind) {
58
+ case 'create': {
59
+ const provider = registry.get(op.resourceType);
60
+ const result = await provider.create(ctx, op.label, op.properties);
61
+ return result.nativeId;
62
+ }
63
+ case 'update': {
64
+ const provider = registry.get(op.resourceType);
65
+ const result = await provider.update(ctx, op.nativeId, op.prior, op.properties);
66
+ if (result.kind === 'noop')
67
+ return op.nativeId;
68
+ return result.nativeId;
69
+ }
70
+ case 'delete': {
71
+ const provider = registry.get(op.resourceType);
72
+ await provider.delete(ctx, op.nativeId);
73
+ return op.nativeId;
74
+ }
75
+ case 'noop':
76
+ return undefined;
77
+ }
78
+ }
79
+ function toProviderError(raw) {
80
+ if (raw && typeof raw === 'object' && 'code' in raw && 'recoverable' in raw) {
81
+ return raw;
82
+ }
83
+ return {
84
+ code: 'ServiceInternalError',
85
+ recoverable: true,
86
+ message: raw instanceof Error ? raw.message : String(raw),
87
+ cause: raw,
88
+ };
89
+ }
90
+ function unknownError() {
91
+ return {
92
+ code: 'ServiceInternalError',
93
+ recoverable: false,
94
+ message: 'unknown error after retries',
95
+ };
96
+ }
97
+ function summarize(results) {
98
+ let succeeded = 0;
99
+ let failed = 0;
100
+ let skipped = 0;
101
+ for (const r of results) {
102
+ if (r.status === 'succeeded')
103
+ succeeded += 1;
104
+ else if (r.status === 'failed')
105
+ failed += 1;
106
+ else
107
+ skipped += 1;
108
+ }
109
+ return { results, succeeded, failed, skipped };
110
+ }
111
+ function sleep(ms) {
112
+ return new Promise((resolve) => setTimeout(resolve, ms));
113
+ }
114
+ //# sourceMappingURL=apply.js.map