@stackwright-services/compiler 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1995 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ compileHelmTarget: () => compileHelmTarget,
24
+ compileLambdaTarget: () => compileLambdaTarget,
25
+ compileOpenApiTarget: () => compileOpenApiTarget,
26
+ compileStepFunctionsTarget: () => compileStepFunctionsTarget,
27
+ deriveBuildCoherence: () => deriveBuildCoherence,
28
+ deriveFlowAuthRequirements: () => deriveFlowAuthRequirements,
29
+ deriveFlowPermissions: () => deriveFlowPermissions,
30
+ deriveWorkflowAuthRequirements: () => deriveWorkflowAuthRequirements,
31
+ deriveWorkflowPermissions: () => deriveWorkflowPermissions,
32
+ detectDefinitionType: () => detectDefinitionType,
33
+ generateFlowInstrumentation: () => generateFlowInstrumentation,
34
+ generateInstrumentedFlowCode: () => generateInstrumentedFlowCode,
35
+ generateInstrumentedStateHandlerCode: () => generateInstrumentedStateHandlerCode,
36
+ generateInstrumentedStepCode: () => generateInstrumentedStepCode,
37
+ generateWorkflowInstrumentation: () => generateWorkflowInstrumentation,
38
+ getTriggerPermissions: () => getTriggerPermissions,
39
+ iso8601ToSeconds: () => iso8601ToSeconds,
40
+ sanitizeChartName: () => sanitizeChartName,
41
+ sanitizeIdentifier: () => sanitizeIdentifier,
42
+ toBuildKeyEnv: () => toBuildKeyEnv,
43
+ toBuildKeyMiddleware: () => toBuildKeyMiddleware,
44
+ toHelmAuthAnnotations: () => toHelmAuthAnnotations,
45
+ toIamPolicy: () => toIamPolicy,
46
+ toLambdaAuthorizerConfig: () => toLambdaAuthorizerConfig,
47
+ toRbacRules: () => toRbacRules,
48
+ toStepFunctionsIamPolicy: () => toStepFunctionsIamPolicy,
49
+ validate: () => validate,
50
+ validateFlow: () => validateFlow,
51
+ validateWorkflow: () => validateWorkflow
52
+ });
53
+ module.exports = __toCommonJS(index_exports);
54
+
55
+ // src/validate.ts
56
+ var import_runtime = require("@stackwright-services/runtime");
57
+ var import_types = require("@stackwright-services/types");
58
+ var import_yaml = require("yaml");
59
+ function detectDefinitionType(obj) {
60
+ if (typeof obj !== "object" || obj === null) return "unknown";
61
+ const record = obj;
62
+ if ("steps" in record && "trigger" in record) return "flow";
63
+ if ("states" in record && "initial" in record) return "workflow";
64
+ return "unknown";
65
+ }
66
+ function validate(yamlContent, registry, options) {
67
+ let parsed;
68
+ try {
69
+ parsed = (0, import_yaml.parse)(yamlContent);
70
+ } catch (err) {
71
+ return {
72
+ success: false,
73
+ errors: [{ code: "YAML_PARSE_ERROR", message: `Invalid YAML: ${err.message}` }],
74
+ warnings: []
75
+ };
76
+ }
77
+ if (parsed === null || parsed === void 0) {
78
+ return {
79
+ success: false,
80
+ errors: [{ code: "EMPTY_DOCUMENT", message: "YAML document is empty" }],
81
+ warnings: []
82
+ };
83
+ }
84
+ const defType = detectDefinitionType(parsed);
85
+ if (defType === "flow") {
86
+ return validateFlow(parsed, registry, options);
87
+ } else if (defType === "workflow") {
88
+ return validateWorkflow(parsed, registry);
89
+ } else {
90
+ return {
91
+ success: false,
92
+ errors: [
93
+ {
94
+ code: "UNKNOWN_DEFINITION_TYPE",
95
+ message: "Could not detect definition type. Expected a flow (with trigger + steps) or workflow (with states + initial)."
96
+ }
97
+ ],
98
+ warnings: []
99
+ };
100
+ }
101
+ }
102
+ function validateFlow(parsed, registry, options) {
103
+ const result = import_types.FlowDefinition.safeParse(parsed);
104
+ if (!result.success) {
105
+ const errors2 = result.error.issues.map((issue) => ({
106
+ code: "SCHEMA_VALIDATION_ERROR",
107
+ message: issue.message,
108
+ path: issue.path.join(".")
109
+ }));
110
+ return { success: false, errors: errors2, warnings: [] };
111
+ }
112
+ const flow = result.data;
113
+ const errors = [];
114
+ const warnings = [];
115
+ if (registry) {
116
+ for (const step of flow.steps) {
117
+ if (!registry.has(step.use)) {
118
+ errors.push({
119
+ code: "UNKNOWN_CAPABILITY",
120
+ message: `Step "${step.name}" references unknown capability "${step.use}"`,
121
+ path: `steps.${step.name}.use`
122
+ });
123
+ }
124
+ }
125
+ }
126
+ const stepNames = flow.steps.map((s) => s.name);
127
+ const seen = /* @__PURE__ */ new Set();
128
+ for (const name of stepNames) {
129
+ if (seen.has(name)) {
130
+ warnings.push({
131
+ code: "DUPLICATE_STEP_NAME",
132
+ message: `Duplicate step name "${name}" -- step names should be unique for observability`,
133
+ path: `steps.${name}`
134
+ });
135
+ }
136
+ seen.add(name);
137
+ }
138
+ const sourceRefs = [];
139
+ for (const step of flow.steps) {
140
+ const sourceVal = step.with?.["source"];
141
+ if (typeof sourceVal === "string" && (sourceVal.startsWith("openapi:") || sourceVal.startsWith("pulse:"))) {
142
+ sourceRefs.push({ ref: sourceVal, stepName: step.name });
143
+ }
144
+ }
145
+ if (sourceRefs.length > 0) {
146
+ const doScan = options?.scanDataSources ?? import_runtime.scanDataSources;
147
+ const scanResult = doScan();
148
+ for (const { ref, stepName } of sourceRefs) {
149
+ const sourceId = ref.slice(ref.indexOf(":") + 1);
150
+ if (scanResult.detectedProviders.length === 0) {
151
+ warnings.push({
152
+ code: "SOURCE_UNVERIFIABLE",
153
+ message: `source '${ref}' cannot be verified: no DataSourceProvider is installed`,
154
+ path: `steps.${stepName}.with.source`
155
+ });
156
+ } else if (!scanResult.sources.some((s) => s.id === sourceId)) {
157
+ warnings.push({
158
+ code: "SOURCE_NOT_REGISTERED",
159
+ message: `source '${ref}' is not registered by any installed provider`,
160
+ path: `steps.${stepName}.with.source`
161
+ });
162
+ }
163
+ }
164
+ }
165
+ return {
166
+ success: errors.length === 0,
167
+ data: flow,
168
+ errors,
169
+ warnings
170
+ };
171
+ }
172
+ function validateWorkflow(parsed, registry) {
173
+ const result = import_types.WorkflowDefinition.safeParse(parsed);
174
+ if (!result.success) {
175
+ const errors2 = result.error.issues.map((issue) => ({
176
+ code: "SCHEMA_VALIDATION_ERROR",
177
+ message: issue.message,
178
+ path: issue.path.join(".")
179
+ }));
180
+ return { success: false, errors: errors2, warnings: [] };
181
+ }
182
+ const workflow = result.data;
183
+ const errors = [];
184
+ const warnings = [];
185
+ const stateNames = Object.keys(workflow.states);
186
+ if (!stateNames.includes(workflow.initial)) {
187
+ errors.push({
188
+ code: "INITIAL_STATE_NOT_FOUND",
189
+ message: `Initial state "${workflow.initial}" does not exist in states map`,
190
+ path: "initial"
191
+ });
192
+ }
193
+ for (const [stateName, state] of Object.entries(workflow.states)) {
194
+ if (state.transitions) {
195
+ for (const transition of state.transitions) {
196
+ if (!stateNames.includes(transition.to)) {
197
+ errors.push({
198
+ code: "TRANSITION_TARGET_NOT_FOUND",
199
+ message: `State "${stateName}" has transition to unknown state "${transition.to}"`,
200
+ path: `states.${stateName}.transitions`
201
+ });
202
+ }
203
+ }
204
+ }
205
+ if (registry && state.on_enter) {
206
+ if (!registry.has(state.on_enter.use)) {
207
+ errors.push({
208
+ code: "UNKNOWN_CAPABILITY",
209
+ message: `State "${stateName}" on_enter references unknown capability "${state.on_enter.use}"`,
210
+ path: `states.${stateName}.on_enter.use`
211
+ });
212
+ }
213
+ }
214
+ }
215
+ const terminalStates = stateNames.filter((name) => workflow.states[name]?.type === "terminal");
216
+ if (terminalStates.length === 0 && stateNames.length > 0) {
217
+ errors.push({
218
+ code: "NO_TERMINAL_STATE",
219
+ message: "Workflow has no terminal state -- workflows must have at least one terminal state"
220
+ });
221
+ }
222
+ for (const [stateName, state] of Object.entries(workflow.states)) {
223
+ if (state.type !== "terminal" && (!state.transitions || state.transitions.length === 0)) {
224
+ errors.push({
225
+ code: "DEADLOCK_STATE",
226
+ message: `State "${stateName}" is not terminal but has no transitions \u2014 workflow will deadlock here`,
227
+ path: `states.${stateName}`
228
+ });
229
+ }
230
+ }
231
+ if (stateNames.includes(workflow.initial)) {
232
+ const reachable = /* @__PURE__ */ new Set();
233
+ const queue = [workflow.initial];
234
+ while (queue.length > 0) {
235
+ const current = queue.shift();
236
+ if (reachable.has(current)) continue;
237
+ reachable.add(current);
238
+ const state = workflow.states[current];
239
+ if (state?.transitions) {
240
+ for (const transition of state.transitions) {
241
+ if (!reachable.has(transition.to)) {
242
+ queue.push(transition.to);
243
+ }
244
+ }
245
+ }
246
+ }
247
+ for (const stateName of stateNames) {
248
+ if (!reachable.has(stateName)) {
249
+ warnings.push({
250
+ code: "UNREACHABLE_STATE",
251
+ message: `State "${stateName}" is not reachable from initial state "${workflow.initial}"`,
252
+ path: `states.${stateName}`
253
+ });
254
+ }
255
+ }
256
+ }
257
+ if (terminalStates.length > 0) {
258
+ const canReachTerminal = new Set(terminalStates);
259
+ const reverseQueue = [...terminalStates];
260
+ const reverseAdj = /* @__PURE__ */ new Map();
261
+ for (const name of stateNames) {
262
+ reverseAdj.set(name, []);
263
+ }
264
+ for (const [stateName, state] of Object.entries(workflow.states)) {
265
+ if (state.transitions) {
266
+ for (const transition of state.transitions) {
267
+ reverseAdj.get(transition.to)?.push(stateName);
268
+ }
269
+ }
270
+ }
271
+ while (reverseQueue.length > 0) {
272
+ const current = reverseQueue.shift();
273
+ for (const predecessor of reverseAdj.get(current) ?? []) {
274
+ if (!canReachTerminal.has(predecessor)) {
275
+ canReachTerminal.add(predecessor);
276
+ reverseQueue.push(predecessor);
277
+ }
278
+ }
279
+ }
280
+ for (const stateName of stateNames) {
281
+ if (workflow.states[stateName]?.type !== "terminal" && !canReachTerminal.has(stateName)) {
282
+ warnings.push({
283
+ code: "NO_PATH_TO_TERMINAL",
284
+ message: `State "${stateName}" has no path to any terminal state`,
285
+ path: `states.${stateName}`
286
+ });
287
+ }
288
+ }
289
+ }
290
+ if (workflow.signals) {
291
+ const declaredSignals = new Set(workflow.signals.map((s) => s.name));
292
+ const usedSignals = /* @__PURE__ */ new Set();
293
+ for (const [stateName, state] of Object.entries(workflow.states)) {
294
+ if (state.transitions) {
295
+ for (const transition of state.transitions) {
296
+ usedSignals.add(transition.on);
297
+ if (!declaredSignals.has(transition.on)) {
298
+ warnings.push({
299
+ code: "UNKNOWN_SIGNAL",
300
+ message: `Transition in state "${stateName}" uses signal "${transition.on}" which is not declared in workflow signals`,
301
+ path: `states.${stateName}.transitions`
302
+ });
303
+ }
304
+ }
305
+ }
306
+ }
307
+ for (const signal of workflow.signals) {
308
+ if (!usedSignals.has(signal.name)) {
309
+ warnings.push({
310
+ code: "UNUSED_SIGNAL",
311
+ message: `Signal "${signal.name}" is declared but never used by any transition`,
312
+ path: "signals"
313
+ });
314
+ }
315
+ }
316
+ }
317
+ for (const [stateName, state] of Object.entries(workflow.states)) {
318
+ if (state.transitions) {
319
+ const unguardedSignals = /* @__PURE__ */ new Set();
320
+ for (const transition of state.transitions) {
321
+ if (!transition.when) {
322
+ if (unguardedSignals.has(transition.on)) {
323
+ warnings.push({
324
+ code: "AMBIGUOUS_TRANSITION",
325
+ message: `State "${stateName}" has multiple unguarded transitions for signal "${transition.on}" \u2014 only the first will match`,
326
+ path: `states.${stateName}.transitions`
327
+ });
328
+ }
329
+ unguardedSignals.add(transition.on);
330
+ }
331
+ }
332
+ }
333
+ }
334
+ if (registry && workflow.timeouts?.on_timeout) {
335
+ if (!registry.has(workflow.timeouts.on_timeout.use)) {
336
+ errors.push({
337
+ code: "UNKNOWN_CAPABILITY",
338
+ message: `Timeout on_timeout references unknown capability "${workflow.timeouts.on_timeout.use}"`,
339
+ path: "timeouts.on_timeout.use"
340
+ });
341
+ }
342
+ }
343
+ return {
344
+ success: errors.length === 0,
345
+ data: workflow,
346
+ errors,
347
+ warnings
348
+ };
349
+ }
350
+
351
+ // src/permissions.ts
352
+ function resolvePermissionVariables(permission, stepParams) {
353
+ if (!stepParams) return permission;
354
+ let { resource } = permission;
355
+ const variablePattern = /\$([a-zA-Z_][a-zA-Z0-9_]*)/g;
356
+ resource = resource.replace(variablePattern, (_match, varName) => {
357
+ const value = stepParams[varName];
358
+ if (typeof value === "string") return value;
359
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
360
+ return _match;
361
+ });
362
+ return { ...permission, resource };
363
+ }
364
+ function deriveFlowPermissions(flow, registry) {
365
+ const sources = [];
366
+ for (const step of flow.steps) {
367
+ const capability = registry.lookup(step.use);
368
+ if (!capability) continue;
369
+ if (capability.definition.kind === "effect") {
370
+ for (const perm of capability.definition.requiredPermissions) {
371
+ const resolved = resolvePermissionVariables(perm, step.with);
372
+ sources.push({
373
+ permission: resolved,
374
+ capabilityName: step.use,
375
+ location: `steps.${step.name}`
376
+ });
377
+ }
378
+ }
379
+ }
380
+ return {
381
+ permissions: deduplicatePermissions(sources.map((s) => s.permission)),
382
+ sources
383
+ };
384
+ }
385
+ function deriveWorkflowPermissions(workflow, registry) {
386
+ const sources = [];
387
+ for (const [stateName, state] of Object.entries(workflow.states)) {
388
+ if (state.on_enter) {
389
+ const capability = registry.lookup(state.on_enter.use);
390
+ if (!capability) continue;
391
+ if (capability.definition.kind === "effect") {
392
+ for (const perm of capability.definition.requiredPermissions) {
393
+ sources.push({
394
+ permission: perm,
395
+ capabilityName: state.on_enter.use,
396
+ location: `states.${stateName}.on_enter`
397
+ });
398
+ }
399
+ }
400
+ }
401
+ }
402
+ if (workflow.timeouts?.on_timeout) {
403
+ const capability = registry.lookup(workflow.timeouts.on_timeout.use);
404
+ if (capability && capability.definition.kind === "effect") {
405
+ for (const perm of capability.definition.requiredPermissions) {
406
+ sources.push({
407
+ permission: perm,
408
+ capabilityName: workflow.timeouts.on_timeout.use,
409
+ location: "timeouts.on_timeout"
410
+ });
411
+ }
412
+ }
413
+ }
414
+ return {
415
+ permissions: deduplicatePermissions(sources.map((s) => s.permission)),
416
+ sources
417
+ };
418
+ }
419
+ function deduplicatePermissions(permissions) {
420
+ const seen = /* @__PURE__ */ new Set();
421
+ const unique = [];
422
+ for (const perm of permissions) {
423
+ const key = `${perm.resource}:${perm.action}:${perm.scope ?? ""}`;
424
+ if (!seen.has(key)) {
425
+ seen.add(key);
426
+ unique.push(perm);
427
+ }
428
+ }
429
+ return unique;
430
+ }
431
+ function toIamPolicy(derived) {
432
+ if (derived.permissions.length === 0) {
433
+ return {
434
+ Version: "2012-10-17",
435
+ Statement: []
436
+ };
437
+ }
438
+ return {
439
+ Version: "2012-10-17",
440
+ Statement: derived.permissions.map((perm) => ({
441
+ Effect: "Allow",
442
+ Action: `stackwright:${perm.action}`,
443
+ Resource: perm.resource,
444
+ ...perm.scope ? { Condition: { StringEquals: { "stackwright:scope": perm.scope } } } : {}
445
+ }))
446
+ };
447
+ }
448
+ function toStepFunctionsIamPolicy(workflow, derived) {
449
+ const statements = [];
450
+ const statesWithActions = Object.entries(workflow.states).filter(([, state]) => state.on_enter !== void 0).map(([name]) => name);
451
+ if (statesWithActions.length > 0) {
452
+ statements.push({
453
+ Sid: "InvokeStateLambdas",
454
+ Effect: "Allow",
455
+ Action: ["lambda:InvokeFunction"],
456
+ Resource: statesWithActions.map(
457
+ (name) => `arn:aws:lambda:*:*:function:${workflow.id}-${name}`
458
+ )
459
+ });
460
+ }
461
+ if (derived.permissions.length > 0) {
462
+ statements.push(
463
+ ...derived.permissions.map((perm) => ({
464
+ Effect: "Allow",
465
+ Action: `stackwright:${perm.action}`,
466
+ Resource: perm.resource,
467
+ ...perm.scope ? { Condition: { StringEquals: { "stackwright:scope": perm.scope } } } : {}
468
+ }))
469
+ );
470
+ }
471
+ if (workflow.timeouts?.pending_after || workflow.timeouts?.absolute) {
472
+ statements.push({
473
+ Sid: "TimeoutScheduling",
474
+ Effect: "Allow",
475
+ Action: ["events:PutRule", "events:PutTargets", "events:DeleteRule", "events:RemoveTargets"],
476
+ Resource: `arn:aws:events:*:*:rule/${workflow.id}-timeout-*`
477
+ });
478
+ }
479
+ statements.push({
480
+ Sid: "ExecutionLogging",
481
+ Effect: "Allow",
482
+ Action: ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
483
+ Resource: `arn:aws:logs:*:*:log-group:/aws/stepfunctions/${workflow.id}:*`
484
+ });
485
+ return {
486
+ Version: "2012-10-17",
487
+ Statement: statements
488
+ };
489
+ }
490
+ function toRbacRules(derived) {
491
+ if (derived.permissions.length === 0) return [];
492
+ const grouped = /* @__PURE__ */ new Map();
493
+ for (const perm of derived.permissions) {
494
+ const verbs = grouped.get(perm.resource) ?? [];
495
+ if (!verbs.includes(perm.action)) {
496
+ verbs.push(perm.action);
497
+ }
498
+ grouped.set(perm.resource, verbs);
499
+ }
500
+ return Array.from(grouped.entries()).map(([resource, verbs]) => ({
501
+ apiGroups: ["stackwright.io"],
502
+ resources: [resource],
503
+ verbs
504
+ }));
505
+ }
506
+
507
+ // src/observability.ts
508
+ function generateFlowInstrumentation(flow) {
509
+ return flow.steps.map((step) => ({
510
+ stepName: step.name,
511
+ capabilityName: step.use,
512
+ flowId: flow.id,
513
+ metrics: {
514
+ latency: "flow.step.latency",
515
+ invocations: "flow.step.invocations",
516
+ errors: "flow.step.errors"
517
+ }
518
+ }));
519
+ }
520
+ function sanitizeIdentifier(name) {
521
+ return name.replace(/[^a-zA-Z0-9]/g, "_");
522
+ }
523
+ function generateInstrumentedStepCode(instrumentation) {
524
+ const { stepName, capabilityName, flowId, metrics } = instrumentation;
525
+ const fnName = sanitizeIdentifier(stepName);
526
+ return [
527
+ `async function executeStep_${fnName}(`,
528
+ " input: unknown,",
529
+ " context: CapabilityContext,",
530
+ " provider: ObservabilityProvider,",
531
+ " auditProvider: AuditProvider,",
532
+ " handler: CapabilityHandler,",
533
+ "): Promise<unknown> {",
534
+ ` const span = provider.startSpan('step.${stepName}', {`,
535
+ ` 'capability.name': '${capabilityName}',`,
536
+ ` 'flow.id': '${flowId}',`,
537
+ ` 'step.name': '${stepName}',`,
538
+ " });",
539
+ " const startTime = Date.now();",
540
+ "",
541
+ " try {",
542
+ " const result = await handler(input, context);",
543
+ ` provider.recordMetric('${metrics.invocations}', 1, { step: '${stepName}', capability: '${capabilityName}', status: 'success' });`,
544
+ " auditProvider.record({",
545
+ " type: 'capability_invocation',",
546
+ " traceId: context.traceId,",
547
+ " timestamp: new Date().toISOString(),",
548
+ ` flowId: '${flowId}',`,
549
+ ` stepName: '${stepName}',`,
550
+ ` capabilityName: '${capabilityName}',`,
551
+ " inputHash: auditHash(input),",
552
+ " outputHash: auditHash(result),",
553
+ " durationMs: Date.now() - startTime,",
554
+ " status: 'success',",
555
+ " });",
556
+ " return result;",
557
+ " } catch (error) {",
558
+ " span.setError(error instanceof Error ? error : new Error(String(error)));",
559
+ ` provider.recordMetric('${metrics.errors}', 1, { step: '${stepName}', capability: '${capabilityName}' });`,
560
+ " auditProvider.record({",
561
+ " type: 'capability_invocation',",
562
+ " traceId: context.traceId,",
563
+ " timestamp: new Date().toISOString(),",
564
+ ` flowId: '${flowId}',`,
565
+ ` stepName: '${stepName}',`,
566
+ ` capabilityName: '${capabilityName}',`,
567
+ " inputHash: auditHash(input),",
568
+ " outputHash: '',",
569
+ " durationMs: Date.now() - startTime,",
570
+ " status: 'error',",
571
+ " error: error instanceof Error ? error.message : String(error),",
572
+ " });",
573
+ " throw error;",
574
+ " } finally {",
575
+ " const duration = Date.now() - startTime;",
576
+ ` provider.recordMetric('${metrics.latency}', duration, { step: '${stepName}', capability: '${capabilityName}' });`,
577
+ " span.end();",
578
+ " }",
579
+ "}"
580
+ ].join("\n");
581
+ }
582
+ function generateInstrumentedFlowCode(flow) {
583
+ const instrumentations = generateFlowInstrumentation(flow);
584
+ const stepFunctions = instrumentations.map(generateInstrumentedStepCode);
585
+ const stepCalls = instrumentations.map(
586
+ (inst) => ` stepResult = await executeStep_${sanitizeIdentifier(inst.stepName)}(stepResult, context, provider, auditProvider, registry.get('${inst.capabilityName}').handler);`
587
+ ).join("\n");
588
+ const header = [
589
+ "// Auto-generated by @stackwright-services/compiler",
590
+ `// Flow: ${flow.id}`,
591
+ `// Description: ${flow.description}`,
592
+ "//",
593
+ "// DO NOT EDIT -- regenerate from the source YAML.",
594
+ "",
595
+ "import type { CapabilityContext, CapabilityHandler } from '@stackwright-services/capabilities';",
596
+ "import type { ObservabilityProvider } from '@stackwright-services/compiler';",
597
+ "import type { AuditProvider, AuditEvent } from '@stackwright-services/runtime';",
598
+ "import { auditHash } from '@stackwright-services/runtime';",
599
+ ""
600
+ ].join("\n");
601
+ const executeFlow = [
602
+ "export async function executeFlow(",
603
+ " input: unknown,",
604
+ " context: CapabilityContext,",
605
+ " provider: ObservabilityProvider,",
606
+ " auditProvider: AuditProvider,",
607
+ " registry: { get(name: string): { handler: CapabilityHandler } },",
608
+ "): Promise<unknown> {",
609
+ " auditProvider.record({",
610
+ " type: 'flow_start',",
611
+ " traceId: context.traceId,",
612
+ " timestamp: new Date().toISOString(),",
613
+ ` flowId: '${flow.id}',`,
614
+ " stepName: '',",
615
+ " capabilityName: '',",
616
+ " inputHash: auditHash(input),",
617
+ " outputHash: '',",
618
+ " durationMs: 0,",
619
+ " status: 'success',",
620
+ " });",
621
+ ` const flowSpan = provider.startSpan('flow.${flow.id}', {`,
622
+ ` 'flow.id': '${flow.id}',`,
623
+ ` 'flow.steps': ${flow.steps.length},`,
624
+ " });",
625
+ " const flowStart = Date.now();",
626
+ "",
627
+ " try {",
628
+ " let stepResult: unknown = input;",
629
+ stepCalls,
630
+ " auditProvider.record({",
631
+ " type: 'flow_end',",
632
+ " traceId: context.traceId,",
633
+ " timestamp: new Date().toISOString(),",
634
+ ` flowId: '${flow.id}',`,
635
+ " stepName: '',",
636
+ " capabilityName: '',",
637
+ " inputHash: auditHash(input),",
638
+ " outputHash: auditHash(stepResult),",
639
+ " durationMs: Date.now() - flowStart,",
640
+ " status: 'success',",
641
+ " });",
642
+ ` provider.recordMetric('flow.invocations', 1, { flow: '${flow.id}', status: 'success' });`,
643
+ " return stepResult;",
644
+ " } catch (error) {",
645
+ " flowSpan.setError(error instanceof Error ? error : new Error(String(error)));",
646
+ ` provider.recordMetric('flow.errors', 1, { flow: '${flow.id}' });`,
647
+ " auditProvider.record({",
648
+ " type: 'flow_end',",
649
+ " traceId: context.traceId,",
650
+ " timestamp: new Date().toISOString(),",
651
+ ` flowId: '${flow.id}',`,
652
+ " stepName: '',",
653
+ " capabilityName: '',",
654
+ " inputHash: auditHash(input),",
655
+ " outputHash: '',",
656
+ " durationMs: Date.now() - flowStart,",
657
+ " status: 'error',",
658
+ " error: error instanceof Error ? error.message : String(error),",
659
+ " });",
660
+ " throw error;",
661
+ " } finally {",
662
+ ` provider.recordMetric('flow.latency', Date.now() - flowStart, { flow: '${flow.id}' });`,
663
+ " flowSpan.end();",
664
+ " }",
665
+ "}",
666
+ ""
667
+ ].join("\n");
668
+ return header + stepFunctions.join("\n\n") + "\n\n" + executeFlow;
669
+ }
670
+
671
+ // src/workflow-observability.ts
672
+ function generateWorkflowInstrumentation(workflow) {
673
+ return Object.entries(workflow.states).filter(([, state]) => !!state.on_enter).map(([stateName, state]) => ({
674
+ stateName,
675
+ // filter above guarantees on_enter exists; fallback silences linter.
676
+ capabilityName: state.on_enter?.use ?? "unknown",
677
+ workflowId: workflow.id,
678
+ isTerminal: state.type === "terminal",
679
+ metrics: {
680
+ invocations: "workflow.state.invocations",
681
+ errors: "workflow.state.errors",
682
+ latency: "workflow.state.latency",
683
+ transitions: "workflow.state.transitions"
684
+ }
685
+ }));
686
+ }
687
+ function generateInstrumentedStateHandlerCode(instrumentation, capabilityParams) {
688
+ const { stateName, capabilityName, workflowId, metrics } = instrumentation;
689
+ const paramsJson = JSON.stringify(capabilityParams);
690
+ const header = [
691
+ "// Generated by @stackwright-services/compiler \u2014 DO NOT EDIT",
692
+ `// Workflow: ${workflowId}, State: ${stateName}`,
693
+ `// Capability: ${capabilityName}`,
694
+ "//",
695
+ "// DO NOT EDIT \u2014 regenerate from the source YAML.",
696
+ "",
697
+ "import type { Context } from 'aws-lambda';",
698
+ "import type { ObservabilityProvider } from '@stackwright-services/compiler';",
699
+ "import type { AuditProvider } from '@stackwright-services/runtime';",
700
+ "import { auditHash, noopAuditProvider } from '@stackwright-services/runtime';",
701
+ "",
702
+ "// Noop observability \u2014 replace with real implementation in production bootstrap.",
703
+ "const noopProvider: ObservabilityProvider = {",
704
+ " startSpan: () => ({ setAttribute: () => {}, setError: () => {}, end: () => {} }),",
705
+ " recordMetric: () => {},",
706
+ "};",
707
+ ""
708
+ ].join("\n");
709
+ const handlerLines = [
710
+ "export async function handler(event: unknown, _context: Context): Promise<unknown> {",
711
+ " // TODO: Replace noop providers with real implementations via DI bootstrap.",
712
+ " const provider: ObservabilityProvider = noopProvider;",
713
+ " const auditProvider: AuditProvider = noopAuditProvider;",
714
+ ` // Capability params: ${paramsJson}`,
715
+ "",
716
+ ` const span = provider.startSpan('workflow.state.${stateName}', {`,
717
+ ` 'workflow.id': '${workflowId}',`,
718
+ ` 'state.name': '${stateName}',`,
719
+ ` 'capability.name': '${capabilityName}',`,
720
+ " });",
721
+ " const startTime = Date.now();",
722
+ "",
723
+ " // Transition count \u2014 which signal drove entry to this state.",
724
+ " const incomingSignal = (event as Record<string, unknown>)?.['signal'] as string | undefined;",
725
+ ` provider.recordMetric('${metrics.transitions}', 1, {`,
726
+ ` workflow: '${workflowId}',`,
727
+ ` state: '${stateName}',`,
728
+ " signal: incomingSignal ?? 'initial',",
729
+ " });",
730
+ "",
731
+ " try {",
732
+ ` // TODO: Wire capability invocation for '${capabilityName}'.`,
733
+ " const result = {",
734
+ " ...(event as Record<string, unknown>),",
735
+ ` __state: '${stateName}',`,
736
+ ` __capability: '${capabilityName}',`,
737
+ " __timestamp: new Date().toISOString(),",
738
+ " };",
739
+ "",
740
+ ` provider.recordMetric('${metrics.invocations}', 1, {`,
741
+ ` workflow: '${workflowId}',`,
742
+ ` state: '${stateName}',`,
743
+ ` capability: '${capabilityName}',`,
744
+ " status: 'success',",
745
+ " });",
746
+ " auditProvider.record({",
747
+ " type: 'capability_invocation',",
748
+ " traceId: _context.awsRequestId,",
749
+ " timestamp: new Date().toISOString(),",
750
+ ` flowId: '${workflowId}',`,
751
+ ` stepName: '${stateName}',`,
752
+ ` capabilityName: '${capabilityName}',`,
753
+ " inputHash: auditHash(event),",
754
+ " outputHash: auditHash(result),",
755
+ " durationMs: Date.now() - startTime,",
756
+ " status: 'success',",
757
+ " });",
758
+ " return result;",
759
+ " } catch (error) {",
760
+ " span.setError(error instanceof Error ? error : new Error(String(error)));",
761
+ ` provider.recordMetric('${metrics.errors}', 1, {`,
762
+ ` workflow: '${workflowId}',`,
763
+ ` state: '${stateName}',`,
764
+ " });",
765
+ " auditProvider.record({",
766
+ " type: 'capability_invocation',",
767
+ " traceId: _context.awsRequestId,",
768
+ " timestamp: new Date().toISOString(),",
769
+ ` flowId: '${workflowId}',`,
770
+ ` stepName: '${stateName}',`,
771
+ ` capabilityName: '${capabilityName}',`,
772
+ " inputHash: auditHash(event),",
773
+ " outputHash: '',",
774
+ " durationMs: Date.now() - startTime,",
775
+ " status: 'error',",
776
+ " error: error instanceof Error ? error.message : String(error),",
777
+ " });",
778
+ " throw error;",
779
+ " } finally {",
780
+ " const duration = Date.now() - startTime;",
781
+ ` provider.recordMetric('${metrics.latency}', duration, {`,
782
+ ` workflow: '${workflowId}',`,
783
+ ` state: '${stateName}',`,
784
+ " });",
785
+ " span.end();",
786
+ " }",
787
+ "}",
788
+ ""
789
+ ].join("\n");
790
+ return header + handlerLines;
791
+ }
792
+
793
+ // src/targets/lambda.ts
794
+ function compileLambdaTarget(flow, permissions, instrumentedCode) {
795
+ const files = {};
796
+ files["handler.ts"] = generateHandlerTs(flow);
797
+ files["flow.ts"] = instrumentedCode;
798
+ files["Dockerfile"] = generateDockerfile(flow);
799
+ files["iam-policy.json"] = generateIamPolicy(flow.trigger, permissions);
800
+ files["package.json"] = generatePackageJson(flow);
801
+ const triggerFiles = generateTriggerFiles(flow);
802
+ for (const [path, content] of Object.entries(triggerFiles)) {
803
+ files[path] = content;
804
+ }
805
+ return { files };
806
+ }
807
+ function generateHandlerTs(flow) {
808
+ const trigger = flow.trigger;
809
+ let routeSetup;
810
+ if (trigger.type === "http") {
811
+ const method = trigger.method.toLowerCase();
812
+ const inputExpr = trigger.method === "GET" ? "c.req.query()" : "await c.req.json()";
813
+ routeSetup = `
814
+ app.${method}('${trigger.path}', async (c) => {
815
+ const input = ${inputExpr};
816
+ const context = createContext(c);
817
+ const provider = getObservabilityProvider();
818
+
819
+ try {
820
+ const result = await executeFlow(input, context, provider, registry);
821
+ return c.json(result);
822
+ } catch (error) {
823
+ context.logger.error('Flow execution failed', { error: String(error) });
824
+ return c.json({ error: 'Internal server error' }, 500);
825
+ }
826
+ });`;
827
+ } else {
828
+ routeSetup = `
829
+ // Non-HTTP trigger: ${trigger.type}
830
+ // This handler expects to be invoked by the trigger adapter
831
+ app.post('/_invoke', async (c) => {
832
+ const input = await c.req.json();
833
+ const context = createContext(c);
834
+ const provider = getObservabilityProvider();
835
+
836
+ try {
837
+ const result = await executeFlow(input, context, provider, registry);
838
+ return c.json(result);
839
+ } catch (error) {
840
+ context.logger.error('Flow execution failed', { error: String(error) });
841
+ return c.json({ error: 'Internal server error' }, 500);
842
+ }
843
+ });`;
844
+ }
845
+ return `// Auto-generated Lambda handler for flow: ${flow.id}
846
+ // DO NOT EDIT -- regenerate from source YAML.
847
+
848
+ import { Hono } from 'hono';
849
+ import { executeFlow } from './flow';
850
+ import { CapabilityRegistry, registerAllCapabilities } from '@stackwright-services/capabilities';
851
+ import type { CapabilityContext } from '@stackwright-services/capabilities';
852
+
853
+ // Initialize registry with all built-in capabilities
854
+ const registry = new CapabilityRegistry();
855
+ registerAllCapabilities(registry);
856
+
857
+ const app = new Hono();
858
+
859
+ // Health check
860
+ app.get('/health', (c) => c.json({ status: 'ok', flow: '${flow.id}' }));
861
+ ${routeSetup}
862
+
863
+ function createContext(c: any): CapabilityContext {
864
+ return {
865
+ traceId: c.req.header('x-trace-id') ?? crypto.randomUUID(),
866
+ logger: {
867
+ debug: (msg: string, data?: Record<string, unknown>) => console.debug(JSON.stringify({ level: 'debug', msg, ...data })),
868
+ info: (msg: string, data?: Record<string, unknown>) => console.info(JSON.stringify({ level: 'info', msg, ...data })),
869
+ warn: (msg: string, data?: Record<string, unknown>) => console.warn(JSON.stringify({ level: 'warn', msg, ...data })),
870
+ error: (msg: string, data?: Record<string, unknown>) => console.error(JSON.stringify({ level: 'error', msg, ...data })),
871
+ },
872
+ };
873
+ }
874
+
875
+ function getObservabilityProvider() {
876
+ // No-op provider by default -- swap with OTel provider when configured
877
+ return {
878
+ startSpan: () => ({
879
+ setAttribute: () => {},
880
+ setError: () => {},
881
+ end: () => {},
882
+ }),
883
+ recordMetric: () => {},
884
+ };
885
+ }
886
+
887
+ export default app;
888
+ `;
889
+ }
890
+ function generateDockerfile(flow) {
891
+ return `# Auto-generated Dockerfile for flow: ${flow.id}
892
+ # Uses AWS Lambda Web Adapter for Hono compatibility
893
+ # DO NOT EDIT -- regenerate from source YAML.
894
+
895
+ FROM public.ecr.aws/docker/library/node:22-slim AS builder
896
+
897
+ WORKDIR /app
898
+ COPY package.json ./
899
+ RUN npm install --production=false
900
+ COPY . .
901
+ RUN npm run build
902
+
903
+ FROM public.ecr.aws/docker/library/node:22-slim AS runner
904
+
905
+ # Install AWS Lambda Web Adapter
906
+ COPY --from=public.ecr.aws/awsguru/aws-lambda-web-adapter:0.8.4 /lambda-adapter /opt/extensions/lambda-adapter
907
+
908
+ WORKDIR /app
909
+ COPY --from=builder /app/dist ./dist
910
+ COPY --from=builder /app/node_modules ./node_modules
911
+ COPY --from=builder /app/package.json ./
912
+
913
+ ENV PORT=8080
914
+ ENV AWS_LWA_PORT=8080
915
+ ENV NODE_ENV=production
916
+
917
+ CMD ["node", "dist/handler.js"]
918
+ `;
919
+ }
920
+ function generatePackageJson(flow) {
921
+ return JSON.stringify(
922
+ {
923
+ name: `${flow.id}-lambda`,
924
+ version: "0.0.1",
925
+ private: true,
926
+ scripts: {
927
+ build: "tsc",
928
+ start: "node dist/handler.js"
929
+ },
930
+ dependencies: {
931
+ hono: "^4",
932
+ "@stackwright-services/capabilities": "workspace:*"
933
+ },
934
+ devDependencies: {
935
+ typescript: "^6"
936
+ }
937
+ },
938
+ null,
939
+ 2
940
+ ) + "\n";
941
+ }
942
+ function getTriggerPermissions(trigger) {
943
+ switch (trigger.type) {
944
+ case "event":
945
+ return [
946
+ {
947
+ Effect: "Allow",
948
+ Action: ["sns:Subscribe", "sns:Unsubscribe"],
949
+ Resource: "${TOPIC_ARN}"
950
+ }
951
+ ];
952
+ case "schedule":
953
+ return [
954
+ {
955
+ Effect: "Allow",
956
+ Action: ["events:PutRule", "events:PutTargets", "events:DeleteRule"],
957
+ Resource: "*"
958
+ }
959
+ ];
960
+ case "queue":
961
+ return [
962
+ {
963
+ Effect: "Allow",
964
+ Action: ["sqs:ReceiveMessage", "sqs:DeleteMessage", "sqs:GetQueueAttributes"],
965
+ Resource: "${QUEUE_ARN}"
966
+ }
967
+ ];
968
+ case "http":
969
+ default:
970
+ return [];
971
+ }
972
+ }
973
+ function generateIamPolicy(trigger, permissions) {
974
+ const base = toIamPolicy(permissions);
975
+ const triggerStatements = getTriggerPermissions(trigger);
976
+ base.Statement = [...base.Statement, ...triggerStatements];
977
+ return JSON.stringify(base, null, 2) + "\n";
978
+ }
979
+ function generateTriggerFiles(flow) {
980
+ const trigger = flow.trigger;
981
+ switch (trigger.type) {
982
+ case "event": {
983
+ const source = trigger.source.replace(/^bus:/, "");
984
+ return {
985
+ "sns-subscription.json": JSON.stringify(
986
+ {
987
+ _comment: `Auto-generated SNS subscription for flow: ${flow.id}. Bind TOPIC_ARN and FUNCTION_ARN at deploy time.`,
988
+ TopicArn: "${TOPIC_ARN}",
989
+ Protocol: "lambda",
990
+ Endpoint: "${FUNCTION_ARN}",
991
+ Attributes: {
992
+ RawMessageDelivery: "true"
993
+ },
994
+ source
995
+ },
996
+ null,
997
+ 2
998
+ ) + "\n"
999
+ };
1000
+ }
1001
+ case "schedule":
1002
+ return {
1003
+ "eventbridge-rule.json": JSON.stringify(
1004
+ {
1005
+ _comment: `Auto-generated EventBridge rule for flow: ${flow.id}. Bind FUNCTION_ARN at deploy time.`,
1006
+ Name: `${flow.id}-schedule`,
1007
+ ScheduleExpression: `cron(${trigger.cron})`,
1008
+ State: "ENABLED",
1009
+ Targets: [
1010
+ {
1011
+ Id: `${flow.id}-target`,
1012
+ Arn: "${FUNCTION_ARN}"
1013
+ }
1014
+ ]
1015
+ },
1016
+ null,
1017
+ 2
1018
+ ) + "\n"
1019
+ };
1020
+ case "queue":
1021
+ return {
1022
+ "sqs-event-source-mapping.json": JSON.stringify(
1023
+ {
1024
+ _comment: `Auto-generated SQS event source mapping for flow: ${flow.id}. Bind QUEUE_ARN and FUNCTION_ARN at deploy time.`,
1025
+ EventSourceArn: "${QUEUE_ARN}",
1026
+ FunctionName: "${FUNCTION_ARN}",
1027
+ BatchSize: 10,
1028
+ Enabled: true,
1029
+ queueName: trigger.name
1030
+ },
1031
+ null,
1032
+ 2
1033
+ ) + "\n"
1034
+ };
1035
+ case "http":
1036
+ default:
1037
+ return {};
1038
+ }
1039
+ }
1040
+
1041
+ // src/targets/helm.ts
1042
+ function compileHelmTarget(flow, permissions) {
1043
+ const files = {};
1044
+ const chartName = sanitizeChartName(flow.id);
1045
+ const trigger = flow.trigger;
1046
+ files["Chart.yaml"] = generateChartYaml(chartName, flow);
1047
+ files["values.yaml"] = generateValuesYaml(chartName, flow);
1048
+ files["templates/service.yaml"] = generateServiceYaml(chartName);
1049
+ files["templates/serviceaccount.yaml"] = generateServiceAccountYaml(chartName);
1050
+ files["templates/_helpers.tpl"] = generateHelpersTpl(chartName);
1051
+ if (trigger.type === "schedule") {
1052
+ files["templates/cronjob.yaml"] = generateCronJobYaml(chartName, trigger.cron);
1053
+ } else {
1054
+ files["templates/deployment.yaml"] = generateDeploymentYaml(chartName, trigger);
1055
+ if (trigger.type === "http") {
1056
+ files["templates/ingress.yaml"] = generateIngressYaml(chartName);
1057
+ }
1058
+ }
1059
+ if (trigger.type === "event") {
1060
+ files["templates/bus-subscription.yaml"] = generateBusConfigYaml(chartName, trigger.source);
1061
+ }
1062
+ if (trigger.type === "queue") {
1063
+ files["templates/queue-config.yaml"] = generateQueueConfigYaml(chartName, trigger.name);
1064
+ }
1065
+ if (permissions.permissions.length > 0) {
1066
+ files["templates/rbac.yaml"] = generateRbacYaml(chartName, permissions);
1067
+ }
1068
+ return { files };
1069
+ }
1070
+ function sanitizeChartName(name) {
1071
+ return name.replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1072
+ }
1073
+ function generateChartYaml(chartName, flow) {
1074
+ return `# Auto-generated Helm chart for flow: ${flow.id}
1075
+ # Defense Unicorns compatible
1076
+ # DO NOT EDIT -- regenerate from source YAML.
1077
+ apiVersion: v2
1078
+ name: ${chartName}
1079
+ description: ${flow.description}
1080
+ type: application
1081
+ version: 0.1.0
1082
+ appVersion: "0.0.1"
1083
+ `;
1084
+ }
1085
+ function generateValuesYaml(chartName, flow) {
1086
+ const trigger = flow.trigger;
1087
+ let triggerValues = "";
1088
+ if (trigger.type === "event") {
1089
+ const topic = trigger.source.replace(/^bus:/, "").toUpperCase().replace(/-/g, "_");
1090
+ triggerValues = `
1091
+ # Event trigger configuration
1092
+ trigger:
1093
+ type: "event"
1094
+ busTopicArn: "\${BUS_TOPIC_${topic}_ARN}"
1095
+ `;
1096
+ } else if (trigger.type === "schedule") {
1097
+ triggerValues = `
1098
+ # Schedule trigger configuration
1099
+ trigger:
1100
+ type: "schedule"
1101
+ schedule: "${trigger.cron}"
1102
+ `;
1103
+ } else if (trigger.type === "queue") {
1104
+ const queueKey = trigger.name.toUpperCase().replace(/-/g, "_");
1105
+ triggerValues = `
1106
+ # Queue trigger configuration
1107
+ trigger:
1108
+ type: "queue"
1109
+ queueUrl: "\${BUS_QUEUE_${queueKey}_URL}"
1110
+ `;
1111
+ }
1112
+ const ingressSection = trigger.type === "http" ? `
1113
+ ingress:
1114
+ enabled: false
1115
+ className: ""
1116
+ annotations: {}
1117
+ hosts:
1118
+ - host: ${chartName}.local
1119
+ paths:
1120
+ - path: /
1121
+ pathType: ImplementationSpecific
1122
+ tls: []
1123
+ ` : "";
1124
+ return `# Default values for ${chartName}
1125
+ # DO NOT EDIT -- regenerate from source YAML.
1126
+
1127
+ replicaCount: 1
1128
+
1129
+ image:
1130
+ repository: ""
1131
+ tag: "latest"
1132
+ pullPolicy: IfNotPresent
1133
+
1134
+ imagePullSecrets: []
1135
+
1136
+ serviceAccount:
1137
+ create: true
1138
+ name: ""
1139
+ annotations: {}
1140
+
1141
+ service:
1142
+ type: ClusterIP
1143
+ port: 8080
1144
+ ${ingressSection}
1145
+ resources:
1146
+ limits:
1147
+ cpu: 500m
1148
+ memory: 256Mi
1149
+ requests:
1150
+ cpu: 100m
1151
+ memory: 128Mi
1152
+
1153
+ env: []
1154
+
1155
+ nodeSelector: {}
1156
+ tolerations: []
1157
+ affinity: {}
1158
+
1159
+ # Flow metadata
1160
+ flow:
1161
+ id: "${flow.id}"
1162
+ description: "${flow.description}"
1163
+ ${triggerValues}`;
1164
+ }
1165
+ function generateDeploymentYaml(chartName, trigger) {
1166
+ let annotations = "";
1167
+ if (trigger.type === "event") {
1168
+ annotations = ` annotations:
1169
+ stackwright.io/trigger-type: event
1170
+ stackwright.io/bus-topic: "${trigger.source.replace(/^bus:/, "")}"
1171
+ `;
1172
+ } else if (trigger.type === "queue") {
1173
+ annotations = ` annotations:
1174
+ stackwright.io/trigger-type: queue
1175
+ stackwright.io/queue-name: "${trigger.name}"
1176
+ `;
1177
+ }
1178
+ return `# Auto-generated Deployment for ${chartName}
1179
+ apiVersion: apps/v1
1180
+ kind: Deployment
1181
+ metadata:
1182
+ name: {{ include "${chartName}.fullname" . }}
1183
+ labels:
1184
+ {{- include "${chartName}.labels" . | nindent 4 }}
1185
+ ${annotations}spec:
1186
+ replicas: {{ .Values.replicaCount }}
1187
+ selector:
1188
+ matchLabels:
1189
+ {{- include "${chartName}.selectorLabels" . | nindent 6 }}
1190
+ template:
1191
+ metadata:
1192
+ labels:
1193
+ {{- include "${chartName}.selectorLabels" . | nindent 8 }}
1194
+ spec:
1195
+ {{- with .Values.imagePullSecrets }}
1196
+ imagePullSecrets:
1197
+ {{- toYaml . | nindent 8 }}
1198
+ {{- end }}
1199
+ serviceAccountName: {{ include "${chartName}.serviceAccountName" . }}
1200
+ containers:
1201
+ - name: {{ .Chart.Name }}
1202
+ image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
1203
+ imagePullPolicy: {{ .Values.image.pullPolicy }}
1204
+ ports:
1205
+ - name: http
1206
+ containerPort: 8080
1207
+ protocol: TCP
1208
+ livenessProbe:
1209
+ httpGet:
1210
+ path: /health
1211
+ port: http
1212
+ initialDelaySeconds: 5
1213
+ periodSeconds: 10
1214
+ readinessProbe:
1215
+ httpGet:
1216
+ path: /health
1217
+ port: http
1218
+ initialDelaySeconds: 5
1219
+ periodSeconds: 10
1220
+ resources:
1221
+ {{- toYaml .Values.resources | nindent 12 }}
1222
+ env:
1223
+ - name: PORT
1224
+ value: "8080"
1225
+ - name: NODE_ENV
1226
+ value: "production"
1227
+ - name: FLOW_ID
1228
+ value: {{ .Values.flow.id | quote }}
1229
+ {{- with .Values.env }}
1230
+ {{- toYaml . | nindent 12 }}
1231
+ {{- end }}
1232
+ {{- with .Values.nodeSelector }}
1233
+ nodeSelector:
1234
+ {{- toYaml . | nindent 8 }}
1235
+ {{- end }}
1236
+ {{- with .Values.tolerations }}
1237
+ tolerations:
1238
+ {{- toYaml . | nindent 8 }}
1239
+ {{- end }}
1240
+ {{- with .Values.affinity }}
1241
+ affinity:
1242
+ {{- toYaml . | nindent 8 }}
1243
+ {{- end }}
1244
+ `;
1245
+ }
1246
+ function generateServiceYaml(chartName) {
1247
+ return `# Auto-generated Service for ${chartName}
1248
+ apiVersion: v1
1249
+ kind: Service
1250
+ metadata:
1251
+ name: {{ include "${chartName}.fullname" . }}
1252
+ labels:
1253
+ {{- include "${chartName}.labels" . | nindent 4 }}
1254
+ spec:
1255
+ type: {{ .Values.service.type }}
1256
+ ports:
1257
+ - port: {{ .Values.service.port }}
1258
+ targetPort: http
1259
+ protocol: TCP
1260
+ name: http
1261
+ selector:
1262
+ {{- include "${chartName}.selectorLabels" . | nindent 4 }}
1263
+ `;
1264
+ }
1265
+ function generateIngressYaml(chartName) {
1266
+ return `# Auto-generated Ingress for ${chartName}
1267
+ {{- if .Values.ingress.enabled }}
1268
+ apiVersion: networking.k8s.io/v1
1269
+ kind: Ingress
1270
+ metadata:
1271
+ name: {{ include "${chartName}.fullname" . }}
1272
+ labels:
1273
+ {{- include "${chartName}.labels" . | nindent 4 }}
1274
+ {{- with .Values.ingress.annotations }}
1275
+ annotations:
1276
+ {{- toYaml . | nindent 4 }}
1277
+ {{- end }}
1278
+ spec:
1279
+ {{- if .Values.ingress.className }}
1280
+ ingressClassName: {{ .Values.ingress.className }}
1281
+ {{- end }}
1282
+ {{- if .Values.ingress.tls }}
1283
+ tls:
1284
+ {{- range .Values.ingress.tls }}
1285
+ - hosts:
1286
+ {{- range .hosts }}
1287
+ - {{ . | quote }}
1288
+ {{- end }}
1289
+ secretName: {{ .secretName }}
1290
+ {{- end }}
1291
+ {{- end }}
1292
+ rules:
1293
+ {{- range .Values.ingress.hosts }}
1294
+ - host: {{ .host | quote }}
1295
+ http:
1296
+ paths:
1297
+ {{- range .paths }}
1298
+ - path: {{ .path }}
1299
+ pathType: {{ .pathType }}
1300
+ backend:
1301
+ service:
1302
+ name: {{ include "${chartName}.fullname" $ }}
1303
+ port:
1304
+ name: http
1305
+ {{- end }}
1306
+ {{- end }}
1307
+ {{- end }}
1308
+ `;
1309
+ }
1310
+ function generateServiceAccountYaml(chartName) {
1311
+ return `# Auto-generated ServiceAccount for ${chartName}
1312
+ {{- if .Values.serviceAccount.create }}
1313
+ apiVersion: v1
1314
+ kind: ServiceAccount
1315
+ metadata:
1316
+ name: {{ include "${chartName}.serviceAccountName" . }}
1317
+ labels:
1318
+ {{- include "${chartName}.labels" . | nindent 4 }}
1319
+ {{- with .Values.serviceAccount.annotations }}
1320
+ annotations:
1321
+ {{- toYaml . | nindent 4 }}
1322
+ {{- end }}
1323
+ {{- end }}
1324
+ `;
1325
+ }
1326
+ function generateHelpersTpl(chartName) {
1327
+ return `{{/*
1328
+ Auto-generated helpers for ${chartName}
1329
+ */}}
1330
+
1331
+ {{- define "${chartName}.name" -}}
1332
+ {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
1333
+ {{- end }}
1334
+
1335
+ {{- define "${chartName}.fullname" -}}
1336
+ {{- if .Values.fullnameOverride }}
1337
+ {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
1338
+ {{- else }}
1339
+ {{- $name := default .Chart.Name .Values.nameOverride }}
1340
+ {{- if contains $name .Release.Name }}
1341
+ {{- .Release.Name | trunc 63 | trimSuffix "-" }}
1342
+ {{- else }}
1343
+ {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
1344
+ {{- end }}
1345
+ {{- end }}
1346
+ {{- end }}
1347
+
1348
+ {{- define "${chartName}.labels" -}}
1349
+ helm.sh/chart: {{ include "${chartName}.name" . }}-{{ .Chart.Version | replace "+" "_" }}
1350
+ {{ include "${chartName}.selectorLabels" . }}
1351
+ app.kubernetes.io/managed-by: {{ .Release.Service }}
1352
+ app.kubernetes.io/part-of: stackwright-services
1353
+ {{- end }}
1354
+
1355
+ {{- define "${chartName}.selectorLabels" -}}
1356
+ app.kubernetes.io/name: {{ include "${chartName}.name" . }}
1357
+ app.kubernetes.io/instance: {{ .Release.Name }}
1358
+ {{- end }}
1359
+
1360
+ {{- define "${chartName}.serviceAccountName" -}}
1361
+ {{- if .Values.serviceAccount.create }}
1362
+ {{- default (include "${chartName}.fullname" .) .Values.serviceAccount.name }}
1363
+ {{- else }}
1364
+ {{- default "default" .Values.serviceAccount.name }}
1365
+ {{- end }}
1366
+ {{- end }}
1367
+ `;
1368
+ }
1369
+ function generateCronJobYaml(chartName, cron) {
1370
+ return `# Auto-generated CronJob for ${chartName}
1371
+ # DO NOT EDIT -- regenerate from source YAML.
1372
+ apiVersion: batch/v1
1373
+ kind: CronJob
1374
+ metadata:
1375
+ name: {{ include "${chartName}.fullname" . }}
1376
+ labels:
1377
+ {{- include "${chartName}.labels" . | nindent 4 }}
1378
+ spec:
1379
+ schedule: "${cron}"
1380
+ concurrencyPolicy: Forbid
1381
+ successfulJobsHistoryLimit: 3
1382
+ failedJobsHistoryLimit: 3
1383
+ jobTemplate:
1384
+ spec:
1385
+ template:
1386
+ metadata:
1387
+ labels:
1388
+ {{- include "${chartName}.selectorLabels" . | nindent 12 }}
1389
+ spec:
1390
+ serviceAccountName: {{ include "${chartName}.serviceAccountName" . }}
1391
+ containers:
1392
+ - name: {{ .Chart.Name }}
1393
+ image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
1394
+ imagePullPolicy: {{ .Values.image.pullPolicy }}
1395
+ env:
1396
+ - name: PORT
1397
+ value: "8080"
1398
+ - name: NODE_ENV
1399
+ value: "production"
1400
+ - name: TRIGGER_MODE
1401
+ value: "schedule"
1402
+ - name: FLOW_ID
1403
+ value: {{ .Values.flow.id | quote }}
1404
+ {{- with .Values.env }}
1405
+ {{- toYaml . | nindent 16 }}
1406
+ {{- end }}
1407
+ resources:
1408
+ {{- toYaml .Values.resources | nindent 16 }}
1409
+ restartPolicy: OnFailure
1410
+ `;
1411
+ }
1412
+ function generateBusConfigYaml(chartName, source) {
1413
+ const topic = source.replace(/^bus:/, "");
1414
+ return `# Auto-generated bus subscription config for ${chartName}
1415
+ # The concrete subscription mechanism depends on the cluster's message bus operator.
1416
+ # DO NOT EDIT -- regenerate from source YAML.
1417
+ apiVersion: v1
1418
+ kind: ConfigMap
1419
+ metadata:
1420
+ name: {{ include "${chartName}.fullname" . }}-bus-config
1421
+ labels:
1422
+ {{- include "${chartName}.labels" . | nindent 4 }}
1423
+ data:
1424
+ trigger-type: "event"
1425
+ bus-topic: "${topic}"
1426
+ source: "${source}"
1427
+ `;
1428
+ }
1429
+ function generateQueueConfigYaml(chartName, queueName) {
1430
+ return `# Auto-generated queue config for ${chartName}
1431
+ # The concrete queue binding depends on the cluster's message bus operator.
1432
+ # DO NOT EDIT -- regenerate from source YAML.
1433
+ apiVersion: v1
1434
+ kind: ConfigMap
1435
+ metadata:
1436
+ name: {{ include "${chartName}.fullname" . }}-queue-config
1437
+ labels:
1438
+ {{- include "${chartName}.labels" . | nindent 4 }}
1439
+ data:
1440
+ trigger-type: "queue"
1441
+ queue-name: "${queueName}"
1442
+ `;
1443
+ }
1444
+ function generateRbacYaml(chartName, permissions) {
1445
+ const rules = toRbacRules(permissions);
1446
+ const rulesYaml = rules.map(
1447
+ (r) => ` - apiGroups: [${r.apiGroups.map((g) => `"${g}"`).join(", ")}]
1448
+ resources: [${r.resources.map((res) => `"${res}"`).join(", ")}]
1449
+ verbs: [${r.verbs.map((v) => `"${v}"`).join(", ")}]`
1450
+ ).join("\n");
1451
+ return `# Auto-generated RBAC for ${chartName}
1452
+ # Derived from least-privilege permission analysis
1453
+ apiVersion: rbac.authorization.k8s.io/v1
1454
+ kind: Role
1455
+ metadata:
1456
+ name: {{ include "${chartName}.fullname" . }}
1457
+ labels:
1458
+ {{- include "${chartName}.labels" . | nindent 4 }}
1459
+ rules:
1460
+ ${rulesYaml}
1461
+ ---
1462
+ apiVersion: rbac.authorization.k8s.io/v1
1463
+ kind: RoleBinding
1464
+ metadata:
1465
+ name: {{ include "${chartName}.fullname" . }}
1466
+ labels:
1467
+ {{- include "${chartName}.labels" . | nindent 4 }}
1468
+ roleRef:
1469
+ apiGroup: rbac.authorization.k8s.io
1470
+ kind: Role
1471
+ name: {{ include "${chartName}.fullname" . }}
1472
+ subjects:
1473
+ - kind: ServiceAccount
1474
+ name: {{ include "${chartName}.serviceAccountName" . }}
1475
+ namespace: {{ .Release.Namespace }}
1476
+ `;
1477
+ }
1478
+
1479
+ // src/targets/stepfunctions.ts
1480
+ function compileStepFunctionsTarget(workflow, permissions) {
1481
+ const files = {};
1482
+ const asl = generateAslDefinition(workflow);
1483
+ files["asl-definition.json"] = JSON.stringify(asl, null, 2);
1484
+ files["iam-execution-role.json"] = JSON.stringify(
1485
+ toStepFunctionsIamPolicy(workflow, permissions),
1486
+ null,
1487
+ 2
1488
+ );
1489
+ for (const inst of generateWorkflowInstrumentation(workflow)) {
1490
+ const params = workflow.states[inst.stateName]?.on_enter?.with ?? {};
1491
+ files[`handlers/${inst.stateName}.ts`] = generateInstrumentedStateHandlerCode(inst, params);
1492
+ }
1493
+ files["package.json"] = generatePackageJson2(workflow);
1494
+ return { files };
1495
+ }
1496
+ function generateAslDefinition(workflow) {
1497
+ const states = {};
1498
+ for (const [name, state] of Object.entries(workflow.states)) {
1499
+ Object.assign(states, generateAslStates(name, state, workflow));
1500
+ }
1501
+ const definition = {
1502
+ Comment: `Stackwright workflow: ${workflow.description}`,
1503
+ StartAt: workflow.initial,
1504
+ States: states
1505
+ };
1506
+ if (workflow.timeouts?.absolute) {
1507
+ definition["TimeoutSeconds"] = iso8601ToSeconds(workflow.timeouts.absolute);
1508
+ }
1509
+ return definition;
1510
+ }
1511
+ function generateAslStates(name, state, workflow) {
1512
+ const isTerminal = state.type === "terminal";
1513
+ const hasOnEnter = !!state.on_enter;
1514
+ const transitions = state.transitions ?? [];
1515
+ if (isTerminal && hasOnEnter) {
1516
+ const succeedName = `${name}__succeed`;
1517
+ return {
1518
+ [name]: buildTaskState(workflow.id, name, succeedName),
1519
+ [succeedName]: { Type: "Succeed" }
1520
+ };
1521
+ }
1522
+ if (isTerminal) {
1523
+ return { [name]: { Type: "Succeed" } };
1524
+ }
1525
+ if (transitions.length === 0) {
1526
+ return hasOnEnter ? { [name]: buildTaskState(workflow.id, name) } : { [name]: { Type: "Pass", End: true } };
1527
+ }
1528
+ const first = transitions[0];
1529
+ if (transitions.length === 1 && first && !first.when) {
1530
+ return hasOnEnter ? { [name]: buildTaskState(workflow.id, name, first.to) } : { [name]: { Type: "Pass", Next: first.to } };
1531
+ }
1532
+ if (hasOnEnter) {
1533
+ const choiceName = `${name}__choice`;
1534
+ return {
1535
+ [name]: buildTaskState(workflow.id, name, choiceName),
1536
+ [choiceName]: buildChoiceState(transitions)
1537
+ };
1538
+ }
1539
+ return { [name]: buildChoiceState(transitions) };
1540
+ }
1541
+ function buildTaskState(workflowId, stateName, next) {
1542
+ const task = {
1543
+ Type: "Task",
1544
+ Resource: "arn:aws:states:::lambda:invoke",
1545
+ Parameters: {
1546
+ FunctionName: `${workflowId}-${stateName}`,
1547
+ "Payload.$": "$"
1548
+ }
1549
+ };
1550
+ if (next) {
1551
+ task["Next"] = next;
1552
+ } else {
1553
+ task["End"] = true;
1554
+ }
1555
+ return task;
1556
+ }
1557
+ function buildChoiceState(transitions) {
1558
+ const choices = [];
1559
+ let defaultTarget;
1560
+ const lastNoWhenIdx = findLastIndex(transitions, (t) => !t.when);
1561
+ for (const [i, t] of transitions.entries()) {
1562
+ if (i === lastNoWhenIdx) {
1563
+ defaultTarget = t.to;
1564
+ continue;
1565
+ }
1566
+ choices.push(transitionToChoiceRule(t));
1567
+ }
1568
+ const choiceState = {
1569
+ Type: "Choice",
1570
+ Choices: choices
1571
+ };
1572
+ if (defaultTarget) {
1573
+ choiceState["Default"] = defaultTarget;
1574
+ }
1575
+ return choiceState;
1576
+ }
1577
+ function transitionToChoiceRule(transition) {
1578
+ const signalCheck = { Variable: "$.signal", StringEquals: transition.on };
1579
+ if (!transition.when || transition.when.length === 0) {
1580
+ return { ...signalCheck, Next: transition.to };
1581
+ }
1582
+ return {
1583
+ And: [signalCheck, ...transition.when.map(predicateToChoiceRule)],
1584
+ Next: transition.to
1585
+ };
1586
+ }
1587
+ function predicateToChoiceRule(predicate) {
1588
+ const variable = `$.${predicate.field}`;
1589
+ switch (predicate.op) {
1590
+ case "equals":
1591
+ return buildEqualsRule(variable, predicate.value);
1592
+ case "not_equals":
1593
+ return { Not: buildEqualsRule(variable, predicate.value) };
1594
+ case "less_than":
1595
+ return { Variable: variable, NumericLessThan: predicate.value };
1596
+ case "greater_than":
1597
+ return { Variable: variable, NumericGreaterThan: predicate.value };
1598
+ case "less_than_or_equal":
1599
+ return { Variable: variable, NumericLessThanEquals: predicate.value };
1600
+ case "greater_than_or_equal":
1601
+ return { Variable: variable, NumericGreaterThanEquals: predicate.value };
1602
+ case "matches_prefix":
1603
+ return { Variable: variable, StringMatches: `${String(predicate.value)}*` };
1604
+ case "contains":
1605
+ return { Variable: variable, StringMatches: `*${String(predicate.value)}*` };
1606
+ case "in":
1607
+ return buildInRule(variable, predicate.value);
1608
+ case "not_in":
1609
+ return { Not: buildInRule(variable, predicate.value) };
1610
+ case "less_than_field":
1611
+ case "greater_than_field":
1612
+ case "equals_field":
1613
+ return { Variable: variable, StringEquals: `__UNSUPPORTED_FIELD_OP_${predicate.op}__` };
1614
+ }
1615
+ }
1616
+ function buildEqualsRule(variable, value) {
1617
+ if (typeof value === "string") return { Variable: variable, StringEquals: value };
1618
+ if (typeof value === "number") return { Variable: variable, NumericEquals: value };
1619
+ if (typeof value === "boolean") return { Variable: variable, BooleanEquals: value };
1620
+ return { Variable: variable, StringEquals: String(value) };
1621
+ }
1622
+ function buildInRule(variable, value) {
1623
+ if (!Array.isArray(value)) return buildEqualsRule(variable, value);
1624
+ return { Or: value.map((v) => buildEqualsRule(variable, v)) };
1625
+ }
1626
+ function iso8601ToSeconds(duration) {
1627
+ const match = duration.match(
1628
+ /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/
1629
+ );
1630
+ if (!match) {
1631
+ throw new Error(`Invalid ISO 8601 duration: ${duration}`);
1632
+ }
1633
+ const [, years, months, weeks, days, hours, minutes, seconds] = match;
1634
+ let total = 0;
1635
+ if (years) total += parseInt(years, 10) * 365 * 86400;
1636
+ if (months) total += parseInt(months, 10) * 30 * 86400;
1637
+ if (weeks) total += parseInt(weeks, 10) * 7 * 86400;
1638
+ if (days) total += parseInt(days, 10) * 86400;
1639
+ if (hours) total += parseInt(hours, 10) * 3600;
1640
+ if (minutes) total += parseInt(minutes, 10) * 60;
1641
+ if (seconds) total += parseInt(seconds, 10);
1642
+ return total;
1643
+ }
1644
+ function generatePackageJson2(workflow) {
1645
+ return JSON.stringify(
1646
+ {
1647
+ name: `${workflow.id}-stepfunctions`,
1648
+ version: "0.0.1",
1649
+ private: true,
1650
+ scripts: {
1651
+ build: "tsc"
1652
+ },
1653
+ dependencies: {
1654
+ "@stackwright-services/capabilities": "workspace:*"
1655
+ },
1656
+ devDependencies: {
1657
+ "@types/aws-lambda": "^8",
1658
+ typescript: "^6"
1659
+ }
1660
+ },
1661
+ null,
1662
+ 2
1663
+ ) + "\n";
1664
+ }
1665
+ function findLastIndex(arr, predicate) {
1666
+ for (let i = arr.length - 1; i >= 0; i--) {
1667
+ if (predicate(arr[i])) return i;
1668
+ }
1669
+ return -1;
1670
+ }
1671
+
1672
+ // src/targets/openapi.ts
1673
+ function compileOpenApiTarget(flow, options = {}) {
1674
+ if (flow.trigger.type !== "http") {
1675
+ return { files: {} };
1676
+ }
1677
+ const trigger = flow.trigger;
1678
+ const version = options.version ?? "0.0.1";
1679
+ const spec = buildOpenApiSpec(flow, trigger, version, options);
1680
+ return {
1681
+ files: {
1682
+ "openapi.yaml": serializeYaml(spec)
1683
+ }
1684
+ };
1685
+ }
1686
+ function buildOpenApiSpec(flow, trigger, version, options) {
1687
+ const operationId = generateOperationId(flow.id, trigger.method);
1688
+ const spec = {
1689
+ openapi: "3.1.0",
1690
+ info: {
1691
+ title: flow.id,
1692
+ description: flow.description,
1693
+ version
1694
+ },
1695
+ paths: {
1696
+ [trigger.path]: {
1697
+ [trigger.method.toLowerCase()]: {
1698
+ operationId,
1699
+ summary: flow.description,
1700
+ ...trigger.method === "GET" ? { parameters: buildQueryParameters(trigger) } : { requestBody: buildRequestBody(trigger) },
1701
+ responses: buildResponses(trigger)
1702
+ }
1703
+ }
1704
+ }
1705
+ };
1706
+ if (options.serverUrl) {
1707
+ spec.servers = [{ url: options.serverUrl }];
1708
+ }
1709
+ if (options.buildKey) {
1710
+ spec.components = {
1711
+ ...spec.components,
1712
+ securitySchemes: {
1713
+ buildKey: {
1714
+ type: "apiKey",
1715
+ in: "header",
1716
+ name: "x-stackwright-build-key",
1717
+ description: "Build-coherence key. Auto-generated at compile time. Ensures frontend/backend version match."
1718
+ }
1719
+ }
1720
+ };
1721
+ spec.security = [{ buildKey: [] }];
1722
+ spec["x-stackwright-build-key"] = options.buildKey;
1723
+ }
1724
+ spec.components = {
1725
+ ...spec.components,
1726
+ schemas: {
1727
+ [trigger.input_schema]: {
1728
+ type: "object",
1729
+ description: `Input schema: ${trigger.input_schema} (resolve from Zod schema at build time)`
1730
+ },
1731
+ [trigger.output_schema]: {
1732
+ type: "object",
1733
+ description: `Output schema: ${trigger.output_schema} (resolve from Zod schema at build time)`
1734
+ }
1735
+ }
1736
+ };
1737
+ return spec;
1738
+ }
1739
+ function generateOperationId(flowId, method) {
1740
+ const camelCase = flowId.replace(/[^a-zA-Z0-9]+(.)/g, (_, ch) => ch.toUpperCase()).replace(/^[A-Z]/, (ch) => ch.toLowerCase());
1741
+ const prefix = method.toLowerCase();
1742
+ return `${prefix}${camelCase.charAt(0).toUpperCase()}${camelCase.slice(1)}`;
1743
+ }
1744
+ function buildQueryParameters(trigger) {
1745
+ return [
1746
+ {
1747
+ name: trigger.input_schema,
1748
+ in: "query",
1749
+ description: `Query parameters per ${trigger.input_schema} schema`,
1750
+ required: false,
1751
+ schema: { $ref: `#/components/schemas/${trigger.input_schema}` }
1752
+ }
1753
+ ];
1754
+ }
1755
+ function buildRequestBody(trigger) {
1756
+ return {
1757
+ required: true,
1758
+ content: {
1759
+ "application/json": {
1760
+ schema: { $ref: `#/components/schemas/${trigger.input_schema}` }
1761
+ }
1762
+ }
1763
+ };
1764
+ }
1765
+ function buildResponses(trigger) {
1766
+ return {
1767
+ "200": {
1768
+ description: "Successful response",
1769
+ content: {
1770
+ "application/json": {
1771
+ schema: { $ref: `#/components/schemas/${trigger.output_schema}` }
1772
+ }
1773
+ }
1774
+ },
1775
+ "400": {
1776
+ description: "Validation error",
1777
+ content: {
1778
+ "application/json": {
1779
+ schema: {
1780
+ type: "object",
1781
+ properties: {
1782
+ error: { type: "string" },
1783
+ code: { type: "string" },
1784
+ details: { type: "object" }
1785
+ }
1786
+ }
1787
+ }
1788
+ }
1789
+ },
1790
+ "500": {
1791
+ description: "Internal server error",
1792
+ content: {
1793
+ "application/json": {
1794
+ schema: {
1795
+ type: "object",
1796
+ properties: {
1797
+ error: { type: "string" },
1798
+ code: { type: "string" }
1799
+ }
1800
+ }
1801
+ }
1802
+ }
1803
+ }
1804
+ };
1805
+ }
1806
+ function serializeYaml(obj, indent = 0) {
1807
+ const pad = " ".repeat(indent);
1808
+ if (obj === null || obj === void 0) return "null";
1809
+ if (typeof obj === "string") {
1810
+ if (needsQuoting(obj)) {
1811
+ return `'${obj.replace(/'/g, "''")}'`;
1812
+ }
1813
+ return obj;
1814
+ }
1815
+ if (typeof obj === "number" || typeof obj === "boolean") return String(obj);
1816
+ if (Array.isArray(obj)) {
1817
+ if (obj.length === 0) return "[]";
1818
+ return obj.map((item) => {
1819
+ if (typeof item === "object" && item !== null) {
1820
+ const inner = serializeYaml(item, indent + 1);
1821
+ const lines = inner.split("\n");
1822
+ const firstLine = lines[0] ?? "";
1823
+ return `${pad}- ${firstLine.trimStart()}
1824
+ ${lines.slice(1).join("\n")}`.trimEnd();
1825
+ }
1826
+ return `${pad}- ${serializeYaml(item)}`;
1827
+ }).join("\n");
1828
+ }
1829
+ if (typeof obj === "object") {
1830
+ const entries = Object.entries(obj);
1831
+ if (entries.length === 0) return "{}";
1832
+ return entries.map(([key, val]) => {
1833
+ const safeKey = keyNeedsQuoting(key) ? `'${key}'` : key;
1834
+ if (typeof val === "object" && val !== null && !Array.isArray(val) && Object.keys(val).length > 0) {
1835
+ return `${pad}${safeKey}:
1836
+ ${serializeYaml(val, indent + 1)}`;
1837
+ }
1838
+ if (Array.isArray(val)) {
1839
+ if (val.length === 0) return `${pad}${safeKey}: []`;
1840
+ return `${pad}${safeKey}:
1841
+ ${serializeYaml(val, indent + 1)}`;
1842
+ }
1843
+ return `${pad}${safeKey}: ${serializeYaml(val)}`;
1844
+ }).join("\n");
1845
+ }
1846
+ return String(obj);
1847
+ }
1848
+ function needsQuoting(str) {
1849
+ return /[:{},&*#?|<>=!%@`\n[\]]/.test(str) || str === "" || str === "true" || str === "false";
1850
+ }
1851
+ function keyNeedsQuoting(key) {
1852
+ return /^\d+$/.test(key) || key === "true" || key === "false" || key === "null" || key === "";
1853
+ }
1854
+
1855
+ // src/auth-permissions.ts
1856
+ function deriveFlowAuthRequirements(flow) {
1857
+ void flow;
1858
+ return {
1859
+ requiresAuth: false,
1860
+ requiredRoles: [],
1861
+ sources: []
1862
+ };
1863
+ }
1864
+ function deriveWorkflowAuthRequirements(workflow) {
1865
+ const sources = [];
1866
+ for (const [stateName, state] of Object.entries(workflow.states)) {
1867
+ if (state.transitions) {
1868
+ for (const transition of state.transitions) {
1869
+ if (transition.guard_role) {
1870
+ sources.push({
1871
+ role: transition.guard_role,
1872
+ location: `states.${stateName}.transitions[on=${transition.on}].guard_role`
1873
+ });
1874
+ }
1875
+ }
1876
+ }
1877
+ }
1878
+ const uniqueRoles = [...new Set(sources.map((s) => s.role))];
1879
+ return {
1880
+ requiresAuth: sources.length > 0,
1881
+ requiredRoles: uniqueRoles,
1882
+ sources
1883
+ };
1884
+ }
1885
+ function toLambdaAuthorizerConfig(auth) {
1886
+ if (!auth.requiresAuth) return null;
1887
+ return {
1888
+ type: "REQUEST",
1889
+ identitySource: "method.request.header.Authorization",
1890
+ authorizerResultTtlInSeconds: 300,
1891
+ // The actual authorizer Lambda is provisioned separately —
1892
+ // this config tells API Gateway to require it
1893
+ requiredRoles: auth.requiredRoles
1894
+ };
1895
+ }
1896
+ function toHelmAuthAnnotations(auth) {
1897
+ if (!auth.requiresAuth) return {};
1898
+ return {
1899
+ "stackwright.io/auth-required": "true",
1900
+ "stackwright.io/auth-roles": auth.requiredRoles.join(","),
1901
+ "stackwright.io/auth-provider": "oidc"
1902
+ // default, overridable via values.yaml
1903
+ };
1904
+ }
1905
+
1906
+ // src/build-coherence.ts
1907
+ var import_node_crypto = require("crypto");
1908
+ function deriveBuildCoherence(flow, options = {}) {
1909
+ const enabled = options.enabled ?? true;
1910
+ const buildTimestamp = options.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
1911
+ const contentHash = (0, import_node_crypto.createHash)("sha256").update(JSON.stringify(flow)).digest("hex").slice(0, 16);
1912
+ const buildKey = (0, import_node_crypto.createHash)("sha256").update(`${contentHash}:${buildTimestamp}`).digest("hex").slice(0, 32);
1913
+ return {
1914
+ enabled,
1915
+ buildKey,
1916
+ contentHash,
1917
+ buildTimestamp
1918
+ };
1919
+ }
1920
+ function toBuildKeyMiddleware(config) {
1921
+ if (!config.enabled) return "";
1922
+ return `
1923
+ // \u2500\u2500\u2500 Build Coherence \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1924
+ // Auto-generated by the Stackwright Services compiler.
1925
+ // Ensures this backend matches the frontend that was compiled with it.
1926
+ // NOT authentication \u2014 that's AuthProvider's job.
1927
+ const STACKWRIGHT_BUILD_KEY = process.env.STACKWRIGHT_BUILD_KEY ?? '${config.buildKey}';
1928
+ const STACKWRIGHT_CONTENT_HASH = '${config.contentHash}';
1929
+
1930
+ app.use('*', async (c, next) => {
1931
+ // Skip health check
1932
+ if (c.req.path === '/health') return next();
1933
+
1934
+ const clientKey = c.req.header('x-stackwright-build-key');
1935
+ if (!clientKey) {
1936
+ return c.json({
1937
+ error: 'Missing build-coherence key',
1938
+ code: 'BUILD_KEY_MISSING',
1939
+ hint: 'Include x-stackwright-build-key header. This is auto-set by the generated client.',
1940
+ }, 403);
1941
+ }
1942
+ if (clientKey !== STACKWRIGHT_BUILD_KEY) {
1943
+ return c.json({
1944
+ error: 'Build mismatch \u2014 frontend and backend were compiled from different builds',
1945
+ code: 'BUILD_COHERENCE_FAILURE',
1946
+ expected: STACKWRIGHT_BUILD_KEY.slice(0, 8) + '\u2026',
1947
+ received: clientKey.slice(0, 8) + '\u2026',
1948
+ contentHash: STACKWRIGHT_CONTENT_HASH,
1949
+ }, 403);
1950
+ }
1951
+ await next();
1952
+ });
1953
+ `;
1954
+ }
1955
+ function toBuildKeyEnv(config) {
1956
+ if (!config.enabled) return {};
1957
+ return {
1958
+ STACKWRIGHT_BUILD_KEY: config.buildKey,
1959
+ STACKWRIGHT_CONTENT_HASH: config.contentHash,
1960
+ STACKWRIGHT_BUILD_TIMESTAMP: config.buildTimestamp
1961
+ };
1962
+ }
1963
+ // Annotate the CommonJS export names for ESM import in node:
1964
+ 0 && (module.exports = {
1965
+ compileHelmTarget,
1966
+ compileLambdaTarget,
1967
+ compileOpenApiTarget,
1968
+ compileStepFunctionsTarget,
1969
+ deriveBuildCoherence,
1970
+ deriveFlowAuthRequirements,
1971
+ deriveFlowPermissions,
1972
+ deriveWorkflowAuthRequirements,
1973
+ deriveWorkflowPermissions,
1974
+ detectDefinitionType,
1975
+ generateFlowInstrumentation,
1976
+ generateInstrumentedFlowCode,
1977
+ generateInstrumentedStateHandlerCode,
1978
+ generateInstrumentedStepCode,
1979
+ generateWorkflowInstrumentation,
1980
+ getTriggerPermissions,
1981
+ iso8601ToSeconds,
1982
+ sanitizeChartName,
1983
+ sanitizeIdentifier,
1984
+ toBuildKeyEnv,
1985
+ toBuildKeyMiddleware,
1986
+ toHelmAuthAnnotations,
1987
+ toIamPolicy,
1988
+ toLambdaAuthorizerConfig,
1989
+ toRbacRules,
1990
+ toStepFunctionsIamPolicy,
1991
+ validate,
1992
+ validateFlow,
1993
+ validateWorkflow
1994
+ });
1995
+ //# sourceMappingURL=index.js.map