@shipfox/expression 1.2.1 → 2.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 (53) hide show
  1. package/README.md +25 -12
  2. package/dist/evaluator/errors.d.ts.map +1 -1
  3. package/dist/evaluator/index.d.ts +1 -0
  4. package/dist/evaluator/index.d.ts.map +1 -1
  5. package/dist/evaluator/index.js +1 -0
  6. package/dist/evaluator/index.js.map +1 -1
  7. package/dist/evaluator/rehydrate-json-expression.d.ts +4 -0
  8. package/dist/evaluator/rehydrate-json-expression.d.ts.map +1 -0
  9. package/dist/evaluator/rehydrate-json-expression.js +44 -0
  10. package/dist/evaluator/rehydrate-json-expression.js.map +1 -0
  11. package/dist/expression/create-workflow-expression.d.ts.map +1 -1
  12. package/dist/expression/create-workflow-expression.js +23 -1
  13. package/dist/expression/create-workflow-expression.js.map +1 -1
  14. package/dist/expression/errors.d.ts.map +1 -1
  15. package/dist/index.d.ts +5 -3
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +4 -2
  18. package/dist/index.js.map +1 -1
  19. package/dist/plan/context-key-access.d.ts +2 -0
  20. package/dist/plan/context-key-access.d.ts.map +1 -1
  21. package/dist/plan/context-key-access.js +10 -4
  22. package/dist/plan/context-key-access.js.map +1 -1
  23. package/dist/plan/evaluate-planned-predicate.d.ts.map +1 -1
  24. package/dist/plan/evaluate-planned-predicate.js +2 -2
  25. package/dist/plan/evaluate-planned-predicate.js.map +1 -1
  26. package/dist/resolver/errors.d.ts.map +1 -1
  27. package/dist/run/hoist-run-command.d.ts +8 -0
  28. package/dist/run/hoist-run-command.d.ts.map +1 -1
  29. package/dist/run/hoist-run-command.js +28 -3
  30. package/dist/run/hoist-run-command.js.map +1 -1
  31. package/dist/run/shell-code-position.d.ts +21 -0
  32. package/dist/run/shell-code-position.d.ts.map +1 -0
  33. package/dist/run/shell-code-position.js +623 -0
  34. package/dist/run/shell-code-position.js.map +1 -0
  35. package/dist/run/shell-quoting.d.ts +5 -1
  36. package/dist/run/shell-quoting.d.ts.map +1 -1
  37. package/dist/run/shell-quoting.js +37 -6
  38. package/dist/run/shell-quoting.js.map +1 -1
  39. package/dist/template/errors.d.ts.map +1 -1
  40. package/dist/tsconfig.test.tsbuildinfo +1 -1
  41. package/dist/workflow-context/workflow-context-docs.d.ts +185 -0
  42. package/dist/workflow-context/workflow-context-docs.d.ts.map +1 -0
  43. package/dist/workflow-context/workflow-context-docs.js +168 -0
  44. package/dist/workflow-context/workflow-context-docs.js.map +1 -0
  45. package/dist/workflow-context/workflow-context.d.ts +234 -249
  46. package/dist/workflow-context/workflow-context.d.ts.map +1 -1
  47. package/dist/workflow-context/workflow-context.js +201 -85
  48. package/dist/workflow-context/workflow-context.js.map +1 -1
  49. package/package.json +2 -2
  50. package/dist/template/extract-cel-untrusted-path-accesses.d.ts +0 -5
  51. package/dist/template/extract-cel-untrusted-path-accesses.d.ts.map +0 -1
  52. package/dist/template/extract-cel-untrusted-path-accesses.js +0 -140
  53. package/dist/template/extract-cel-untrusted-path-accesses.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  import { evaluateWorkflowPredicateFailClosed } from '../evaluator/evaluate-workflow-expression.js';
2
- import { getWorkflowPredicateFieldMinimumFillTarget } from '../workflow-context/workflow-context.js';
2
+ import { getWorkflowPredicateFieldMinimumFillTarget, projectWorkflowPredicateContext } from '../workflow-context/workflow-context.js';
3
3
  import { shouldFillAtSite } from './fill.js';
4
4
  import { routeExpression } from './route-expression.js';
5
5
  export function evaluatePlannedPredicateAtSite(params) {
@@ -12,7 +12,7 @@ export function evaluatePlannedPredicateAtSite(params) {
12
12
  };
13
13
  }
14
14
  return {
15
- ...evaluateWorkflowPredicateFailClosed(params.expression, params.context),
15
+ ...evaluateWorkflowPredicateFailClosed(params.expression, projectWorkflowPredicateContext(params.field, params.context)),
16
16
  route
17
17
  };
18
18
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/plan/evaluate-planned-predicate.ts"],"sourcesContent":["import {\n evaluateWorkflowPredicateFailClosed,\n type FailClosedPredicateOutcome,\n type WorkflowExpressionEvaluationContext,\n} from '../evaluator/evaluate-workflow-expression.js';\nimport type {WorkflowExpression} from '../expression/workflow-expression.js';\nimport type {\n AvailabilitySite,\n FillTarget,\n WorkflowPredicateField,\n} from '../workflow-context/workflow-context.js';\nimport {getWorkflowPredicateFieldMinimumFillTarget} from '../workflow-context/workflow-context.js';\nimport {shouldFillAtSite} from './fill.js';\nimport {type RoutedExpression, routeExpression} from './route-expression.js';\n\nexport interface PlannedPredicateEvaluationResult extends FailClosedPredicateOutcome {\n readonly route: RoutedExpression;\n}\n\nexport function evaluatePlannedPredicateAtSite(params: {\n readonly expression: WorkflowExpression;\n readonly field: WorkflowPredicateField;\n readonly site: AvailabilitySite;\n readonly context: WorkflowExpressionEvaluationContext;\n}): PlannedPredicateEvaluationResult {\n const route = routePredicateExpression(params.expression, params.field);\n if (!shouldFillAtSite(route.fillTarget, params.site)) {\n return {value: false, evaluationFailed: true, route};\n }\n\n return {...evaluateWorkflowPredicateFailClosed(params.expression, params.context), route};\n}\n\nfunction routePredicateExpression(\n expression: WorkflowExpression,\n field: WorkflowPredicateField,\n): RoutedExpression {\n const route = routeExpression(expression);\n return {\n ...route,\n fillTarget: laterFillTarget(\n route.fillTarget,\n getWorkflowPredicateFieldMinimumFillTarget(field),\n ),\n };\n}\n\nfunction laterFillTarget(left: FillTarget, right: AvailabilitySite): FillTarget {\n if (left === 'runner-fill') return left;\n return shouldFillAtSite(right, left) ? left : right;\n}\n"],"names":["evaluateWorkflowPredicateFailClosed","getWorkflowPredicateFieldMinimumFillTarget","shouldFillAtSite","routeExpression","evaluatePlannedPredicateAtSite","params","route","routePredicateExpression","expression","field","fillTarget","site","value","evaluationFailed","context","laterFillTarget","left","right"],"mappings":"AAAA,SACEA,mCAAmC,QAG9B,+CAA+C;AAOtD,SAAQC,0CAA0C,QAAO,0CAA0C;AACnG,SAAQC,gBAAgB,QAAO,YAAY;AAC3C,SAA+BC,eAAe,QAAO,wBAAwB;AAM7E,OAAO,SAASC,+BAA+BC,MAK9C;IACC,MAAMC,QAAQC,yBAAyBF,OAAOG,UAAU,EAAEH,OAAOI,KAAK;IACtE,IAAI,CAACP,iBAAiBI,MAAMI,UAAU,EAAEL,OAAOM,IAAI,GAAG;QACpD,OAAO;YAACC,OAAO;YAAOC,kBAAkB;YAAMP;QAAK;IACrD;IAEA,OAAO;QAAC,GAAGN,oCAAoCK,OAAOG,UAAU,EAAEH,OAAOS,OAAO,CAAC;QAAER;IAAK;AAC1F;AAEA,SAASC,yBACPC,UAA8B,EAC9BC,KAA6B;IAE7B,MAAMH,QAAQH,gBAAgBK;IAC9B,OAAO;QACL,GAAGF,KAAK;QACRI,YAAYK,gBACVT,MAAMI,UAAU,EAChBT,2CAA2CQ;IAE/C;AACF;AAEA,SAASM,gBAAgBC,IAAgB,EAAEC,KAAuB;IAChE,IAAID,SAAS,eAAe,OAAOA;IACnC,OAAOd,iBAAiBe,OAAOD,QAAQA,OAAOC;AAChD"}
1
+ {"version":3,"sources":["../../src/plan/evaluate-planned-predicate.ts"],"sourcesContent":["import {\n evaluateWorkflowPredicateFailClosed,\n type FailClosedPredicateOutcome,\n type WorkflowExpressionEvaluationContext,\n} from '../evaluator/evaluate-workflow-expression.js';\nimport type {WorkflowExpression} from '../expression/workflow-expression.js';\nimport type {\n AvailabilitySite,\n FillTarget,\n WorkflowPredicateField,\n} from '../workflow-context/workflow-context.js';\nimport {\n getWorkflowPredicateFieldMinimumFillTarget,\n projectWorkflowPredicateContext,\n} from '../workflow-context/workflow-context.js';\nimport {shouldFillAtSite} from './fill.js';\nimport {type RoutedExpression, routeExpression} from './route-expression.js';\n\nexport interface PlannedPredicateEvaluationResult extends FailClosedPredicateOutcome {\n readonly route: RoutedExpression;\n}\n\nexport function evaluatePlannedPredicateAtSite(params: {\n readonly expression: WorkflowExpression;\n readonly field: WorkflowPredicateField;\n readonly site: AvailabilitySite;\n readonly context: WorkflowExpressionEvaluationContext;\n}): PlannedPredicateEvaluationResult {\n const route = routePredicateExpression(params.expression, params.field);\n if (!shouldFillAtSite(route.fillTarget, params.site)) {\n return {value: false, evaluationFailed: true, route};\n }\n\n return {\n ...evaluateWorkflowPredicateFailClosed(\n params.expression,\n projectWorkflowPredicateContext(params.field, params.context),\n ),\n route,\n };\n}\n\nfunction routePredicateExpression(\n expression: WorkflowExpression,\n field: WorkflowPredicateField,\n): RoutedExpression {\n const route = routeExpression(expression);\n return {\n ...route,\n fillTarget: laterFillTarget(\n route.fillTarget,\n getWorkflowPredicateFieldMinimumFillTarget(field),\n ),\n };\n}\n\nfunction laterFillTarget(left: FillTarget, right: AvailabilitySite): FillTarget {\n if (left === 'runner-fill') return left;\n return shouldFillAtSite(right, left) ? left : right;\n}\n"],"names":["evaluateWorkflowPredicateFailClosed","getWorkflowPredicateFieldMinimumFillTarget","projectWorkflowPredicateContext","shouldFillAtSite","routeExpression","evaluatePlannedPredicateAtSite","params","route","routePredicateExpression","expression","field","fillTarget","site","value","evaluationFailed","context","laterFillTarget","left","right"],"mappings":"AAAA,SACEA,mCAAmC,QAG9B,+CAA+C;AAOtD,SACEC,0CAA0C,EAC1CC,+BAA+B,QAC1B,0CAA0C;AACjD,SAAQC,gBAAgB,QAAO,YAAY;AAC3C,SAA+BC,eAAe,QAAO,wBAAwB;AAM7E,OAAO,SAASC,+BAA+BC,MAK9C;IACC,MAAMC,QAAQC,yBAAyBF,OAAOG,UAAU,EAAEH,OAAOI,KAAK;IACtE,IAAI,CAACP,iBAAiBI,MAAMI,UAAU,EAAEL,OAAOM,IAAI,GAAG;QACpD,OAAO;YAACC,OAAO;YAAOC,kBAAkB;YAAMP;QAAK;IACrD;IAEA,OAAO;QACL,GAAGP,oCACDM,OAAOG,UAAU,EACjBP,gCAAgCI,OAAOI,KAAK,EAAEJ,OAAOS,OAAO,EAC7D;QACDR;IACF;AACF;AAEA,SAASC,yBACPC,UAA8B,EAC9BC,KAA6B;IAE7B,MAAMH,QAAQH,gBAAgBK;IAC9B,OAAO;QACL,GAAGF,KAAK;QACRI,YAAYK,gBACVT,MAAMI,UAAU,EAChBV,2CAA2CS;IAE/C;AACF;AAEA,SAASM,gBAAgBC,IAAgB,EAAEC,KAAuB;IAChE,IAAID,SAAS,eAAe,OAAOA;IACnC,OAAOd,iBAAiBe,OAAOD,QAAQA,OAAOC;AAChD"}
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/resolver/errors.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,mCAAmC,wCAAwC,CAAC;AAEzF,qBAAa,+BAAgC,SAAQ,KAAK;IACxD,QAAQ,CAAC,IAAI,yCAAuC;IACpD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBAEZ,MAAM,EAAE;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAC;CAKrD"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/resolver/errors.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,mCAAmC,wCAAwC,CAAC;AAEzF,qBAAa,+BAAgC,SAAQ,KAAK;IACxD,QAAQ,CAAC,IAAI,yCAAuC;IACpD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAExB,YAAY,MAAM,EAAE;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAC,EAInD;CACF"}
@@ -12,13 +12,21 @@ export interface HoistedPlannedRunCommand {
12
12
  readonly command: string;
13
13
  readonly bindings: readonly PlannedRunCommandBinding[];
14
14
  }
15
+ export interface UnsafeRunInterpolation {
16
+ readonly region: ShellUnsafeRegion;
17
+ readonly source: string;
18
+ }
15
19
  export declare class UnsafeRunInterpolationError extends Error {
16
20
  readonly code = "unsafe-run-interpolation";
21
+ readonly occurrences: readonly UnsafeRunInterpolation[];
22
+ readonly partial: HoistedPlannedRunCommand | undefined;
17
23
  readonly region: ShellUnsafeRegion;
18
24
  readonly source: string;
19
25
  constructor(params: {
20
26
  readonly region: ShellUnsafeRegion;
21
27
  readonly source: string;
28
+ readonly occurrences?: readonly UnsafeRunInterpolation[];
29
+ readonly partial?: HoistedPlannedRunCommand;
22
30
  });
23
31
  }
24
32
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"hoist-run-command.d.ts","sourceRoot":"","sources":["../../src/run/hoist-run-command.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,aAAa,EAAE,4BAA4B,EAAC,MAAM,2BAA2B,CAAC;AAC3F,OAAO,EAKL,KAAK,iBAAiB,EAEvB,MAAM,oBAAoB,CAAC;AAE5B,eAAO,MAAM,+BAA+B,6BAA6B,CAAC;AAK1E,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,aAAa,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,4BAA4B,CAAC;CAChD;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,SAAS,wBAAwB,EAAE,CAAC;CACxD;AAED,qBAAa,2BAA4B,SAAQ,KAAK;IACpD,QAAQ,CAAC,IAAI,8BAAmC;IAChD,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBAEZ,MAAM,EAAE;QAAC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAC;CAQlF;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE;IAC7C,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAC9B,QAAQ,CAAC,aAAa,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;CAC3C,GAAG,wBAAwB,CAQ3B"}
1
+ {"version":3,"file":"hoist-run-command.d.ts","sourceRoot":"","sources":["../../src/run/hoist-run-command.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,aAAa,EAAE,4BAA4B,EAAC,MAAM,2BAA2B,CAAC;AAC3F,OAAO,EAKL,KAAK,iBAAiB,EAEvB,MAAM,oBAAoB,CAAC;AAE5B,eAAO,MAAM,+BAA+B,6BAA6B,CAAC;AAK1E,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,aAAa,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,4BAA4B,CAAC;CAChD;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,SAAS,wBAAwB,EAAE,CAAC;CACxD;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,2BAA4B,SAAQ,KAAK;IACpD,QAAQ,CAAC,IAAI,8BAAmC;IAChD,QAAQ,CAAC,WAAW,EAAE,SAAS,sBAAsB,EAAE,CAAC;IACxD,QAAQ,CAAC,OAAO,EAAE,wBAAwB,GAAG,SAAS,CAAC;IACvD,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAExB,YAAY,MAAM,EAAE;QAClB,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;QACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,sBAAsB,EAAE,CAAC;QACzD,QAAQ,CAAC,OAAO,CAAC,EAAE,wBAAwB,CAAC;KAC7C,EASA;CACF;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE;IAC7C,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAC9B,QAAQ,CAAC,aAAa,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;CAC3C,GAAG,wBAAwB,CAmB3B"}
@@ -6,6 +6,13 @@ export class UnsafeRunInterpolationError extends Error {
6
6
  constructor(params){
7
7
  super(`Unsafe run interpolation inside ${params.region}. Bind the value to env and reference $VAR instead.`), this.code = unsafeRunInterpolationErrorCode;
8
8
  this.name = 'UnsafeRunInterpolationError';
9
+ this.occurrences = params.occurrences ?? [
10
+ {
11
+ region: params.region,
12
+ source: params.source
13
+ }
14
+ ];
15
+ this.partial = params.partial;
9
16
  this.region = params.region;
10
17
  this.source = params.source;
11
18
  }
@@ -15,7 +22,7 @@ export class UnsafeRunInterpolationError extends Error {
15
22
  * references. Literal segments are trusted authored shell text; only call this
16
23
  * on planned command fields that have not already been filled.
17
24
  */ export function hoistPlannedRunCommand(params) {
18
- return hoistCommandSegments({
25
+ const result = hoistCommandSegments({
19
26
  segments: params.field.segments,
20
27
  options: params.reservedNames === undefined ? {} : {
21
28
  reservedNames: params.reservedNames
@@ -23,11 +30,27 @@ export class UnsafeRunInterpolationError extends Error {
23
30
  literalText: (segment)=>segment.value,
24
31
  isBindingSegment: (segment)=>segment.kind === 'deferred'
25
32
  });
33
+ const firstUnsafeInterpolation = result.unsafeInterpolations[0];
34
+ if (firstUnsafeInterpolation !== undefined) {
35
+ throw new UnsafeRunInterpolationError({
36
+ ...firstUnsafeInterpolation,
37
+ occurrences: result.unsafeInterpolations,
38
+ partial: {
39
+ command: result.command,
40
+ bindings: result.bindings
41
+ }
42
+ });
43
+ }
44
+ return {
45
+ command: result.command,
46
+ bindings: result.bindings
47
+ };
26
48
  }
27
49
  function hoistCommandSegments(params) {
28
50
  let command = '';
29
51
  let state = initialShellScanState;
30
52
  const bindings = [];
53
+ const unsafeInterpolations = [];
31
54
  const reservedNames = new Set(params.options.reservedNames ?? []);
32
55
  let nextIndex = 0;
33
56
  for (const segment of params.segments){
@@ -40,10 +63,11 @@ function hoistCommandSegments(params) {
40
63
  if (!params.isBindingSegment(segment)) continue;
41
64
  const site = classifyShellSite(state);
42
65
  if (site.kind === 'unsafe') {
43
- throw new UnsafeRunInterpolationError({
66
+ unsafeInterpolations.push({
44
67
  region: site.region,
45
68
  source: segment.expression.source
46
69
  });
70
+ continue;
47
71
  }
48
72
  const name = nextGeneratedName(reservedNames, nextIndex);
49
73
  nextIndex = Number(name.slice(generatedNamePrefix.length)) + 1;
@@ -56,7 +80,8 @@ function hoistCommandSegments(params) {
56
80
  }
57
81
  return {
58
82
  command,
59
- bindings
83
+ bindings,
84
+ unsafeInterpolations
60
85
  };
61
86
  }
62
87
  function nextGeneratedName(reservedNames, startIndex) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/run/hoist-run-command.ts"],"sourcesContent":["import type {ResolvedField, ResolvedFieldDeferredSegment} from '../plan/resolved-field.js';\nimport {\n classifyShellSite,\n initialShellScanState,\n type ShellScanState,\n type ShellSiteContext,\n type ShellUnsafeRegion,\n scanShellLiteral,\n} from './shell-quoting.js';\n\nexport const unsafeRunInterpolationErrorCode = 'unsafe-run-interpolation';\n\nconst generatedNamePrefix = '__sf_';\nconst shellIdentifierPattern = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\nexport interface RunCommandHoistOptions {\n readonly reservedNames?: Iterable<string>;\n}\n\nexport interface PlannedRunCommandBinding {\n readonly name: string;\n readonly segment: ResolvedFieldDeferredSegment;\n}\n\nexport interface HoistedPlannedRunCommand {\n readonly command: string;\n readonly bindings: readonly PlannedRunCommandBinding[];\n}\n\nexport class UnsafeRunInterpolationError extends Error {\n readonly code = unsafeRunInterpolationErrorCode;\n readonly region: ShellUnsafeRegion;\n readonly source: string;\n\n constructor(params: {readonly region: ShellUnsafeRegion; readonly source: string}) {\n super(\n `Unsafe run interpolation inside ${params.region}. Bind the value to env and reference $VAR instead.`,\n );\n this.name = 'UnsafeRunInterpolationError';\n this.region = params.region;\n this.source = params.source;\n }\n}\n\n/**\n * Hoists unfilled planned run-command segments into generated shell env\n * references. Literal segments are trusted authored shell text; only call this\n * on planned command fields that have not already been filled.\n */\nexport function hoistPlannedRunCommand(params: {\n readonly field: ResolvedField;\n readonly reservedNames?: Iterable<string>;\n}): HoistedPlannedRunCommand {\n return hoistCommandSegments({\n segments: params.field.segments,\n options: params.reservedNames === undefined ? {} : {reservedNames: params.reservedNames},\n literalText: (segment) => segment.value,\n isBindingSegment: (segment): segment is ResolvedFieldDeferredSegment =>\n segment.kind === 'deferred',\n });\n}\n\nfunction hoistCommandSegments<\n Segment extends {readonly kind: string},\n BindingSegment extends Segment & {readonly expression: {readonly source: string}},\n>(params: {\n readonly segments: readonly Segment[];\n readonly options: RunCommandHoistOptions;\n readonly literalText: (segment: Extract<Segment, {readonly kind: 'literal'}>) => string;\n readonly isBindingSegment: (segment: Segment) => segment is BindingSegment;\n}): {\n readonly command: string;\n readonly bindings: readonly {readonly name: string; readonly segment: BindingSegment}[];\n} {\n let command = '';\n let state: ShellScanState = initialShellScanState;\n const bindings: {name: string; segment: BindingSegment}[] = [];\n const reservedNames = new Set(params.options.reservedNames ?? []);\n let nextIndex = 0;\n\n for (const segment of params.segments) {\n if (segment.kind === 'literal') {\n const text = params.literalText(segment as Extract<Segment, {readonly kind: 'literal'}>);\n command += text;\n state = scanShellLiteral(text, state);\n continue;\n }\n\n if (!params.isBindingSegment(segment)) continue;\n\n const site = classifyShellSite(state);\n if (site.kind === 'unsafe') {\n throw new UnsafeRunInterpolationError({\n region: site.region,\n source: segment.expression.source,\n });\n }\n\n const name = nextGeneratedName(reservedNames, nextIndex);\n nextIndex = Number(name.slice(generatedNamePrefix.length)) + 1;\n reservedNames.add(name);\n bindings.push({name, segment});\n command += shellReference(name, site);\n }\n\n return {command, bindings};\n}\n\nfunction nextGeneratedName(reservedNames: ReadonlySet<string>, startIndex: number): string {\n let index = startIndex;\n\n while (true) {\n const name = `${generatedNamePrefix}${index}`;\n if (!shellIdentifierPattern.test(name)) {\n throw new Error(`Generated run env name is not a shell identifier: ${name}`);\n }\n\n if (!reservedNames.has(name)) return name;\n index += 1;\n }\n}\n\nfunction shellReference(name: string, site: Exclude<ShellSiteContext, {readonly kind: 'unsafe'}>) {\n if (site.kind === 'double') return `\\${${name}}`;\n if (site.kind === 'single') return `'\"\\${${name}}\"'`;\n return `\"\\${${name}}\"`;\n}\n"],"names":["classifyShellSite","initialShellScanState","scanShellLiteral","unsafeRunInterpolationErrorCode","generatedNamePrefix","shellIdentifierPattern","UnsafeRunInterpolationError","Error","params","region","code","name","source","hoistPlannedRunCommand","hoistCommandSegments","segments","field","options","reservedNames","undefined","literalText","segment","value","isBindingSegment","kind","command","state","bindings","Set","nextIndex","text","site","expression","nextGeneratedName","Number","slice","length","add","push","shellReference","startIndex","index","test","has"],"mappings":"AACA,SACEA,iBAAiB,EACjBC,qBAAqB,EAIrBC,gBAAgB,QACX,qBAAqB;AAE5B,OAAO,MAAMC,kCAAkC,2BAA2B;AAE1E,MAAMC,sBAAsB;AAC5B,MAAMC,yBAAyB;AAgB/B,OAAO,MAAMC,oCAAoCC;IAK/C,YAAYC,MAAqE,CAAE;QACjF,KAAK,CACH,CAAC,gCAAgC,EAAEA,OAAOC,MAAM,CAAC,mDAAmD,CAAC,QANhGC,OAAOP;QAQd,IAAI,CAACQ,IAAI,GAAG;QACZ,IAAI,CAACF,MAAM,GAAGD,OAAOC,MAAM;QAC3B,IAAI,CAACG,MAAM,GAAGJ,OAAOI,MAAM;IAC7B;AACF;AAEA;;;;CAIC,GACD,OAAO,SAASC,uBAAuBL,MAGtC;IACC,OAAOM,qBAAqB;QAC1BC,UAAUP,OAAOQ,KAAK,CAACD,QAAQ;QAC/BE,SAAST,OAAOU,aAAa,KAAKC,YAAY,CAAC,IAAI;YAACD,eAAeV,OAAOU,aAAa;QAAA;QACvFE,aAAa,CAACC,UAAYA,QAAQC,KAAK;QACvCC,kBAAkB,CAACF,UACjBA,QAAQG,IAAI,KAAK;IACrB;AACF;AAEA,SAASV,qBAGPN,MAKD;IAIC,IAAIiB,UAAU;IACd,IAAIC,QAAwBzB;IAC5B,MAAM0B,WAAsD,EAAE;IAC9D,MAAMT,gBAAgB,IAAIU,IAAIpB,OAAOS,OAAO,CAACC,aAAa,IAAI,EAAE;IAChE,IAAIW,YAAY;IAEhB,KAAK,MAAMR,WAAWb,OAAOO,QAAQ,CAAE;QACrC,IAAIM,QAAQG,IAAI,KAAK,WAAW;YAC9B,MAAMM,OAAOtB,OAAOY,WAAW,CAACC;YAChCI,WAAWK;YACXJ,QAAQxB,iBAAiB4B,MAAMJ;YAC/B;QACF;QAEA,IAAI,CAAClB,OAAOe,gBAAgB,CAACF,UAAU;QAEvC,MAAMU,OAAO/B,kBAAkB0B;QAC/B,IAAIK,KAAKP,IAAI,KAAK,UAAU;YAC1B,MAAM,IAAIlB,4BAA4B;gBACpCG,QAAQsB,KAAKtB,MAAM;gBACnBG,QAAQS,QAAQW,UAAU,CAACpB,MAAM;YACnC;QACF;QAEA,MAAMD,OAAOsB,kBAAkBf,eAAeW;QAC9CA,YAAYK,OAAOvB,KAAKwB,KAAK,CAAC/B,oBAAoBgC,MAAM,KAAK;QAC7DlB,cAAcmB,GAAG,CAAC1B;QAClBgB,SAASW,IAAI,CAAC;YAAC3B;YAAMU;QAAO;QAC5BI,WAAWc,eAAe5B,MAAMoB;IAClC;IAEA,OAAO;QAACN;QAASE;IAAQ;AAC3B;AAEA,SAASM,kBAAkBf,aAAkC,EAAEsB,UAAkB;IAC/E,IAAIC,QAAQD;IAEZ,MAAO,KAAM;QACX,MAAM7B,OAAO,GAAGP,sBAAsBqC,OAAO;QAC7C,IAAI,CAACpC,uBAAuBqC,IAAI,CAAC/B,OAAO;YACtC,MAAM,IAAIJ,MAAM,CAAC,kDAAkD,EAAEI,MAAM;QAC7E;QAEA,IAAI,CAACO,cAAcyB,GAAG,CAAChC,OAAO,OAAOA;QACrC8B,SAAS;IACX;AACF;AAEA,SAASF,eAAe5B,IAAY,EAAEoB,IAA0D;IAC9F,IAAIA,KAAKP,IAAI,KAAK,UAAU,OAAO,CAAC,GAAG,EAAEb,KAAK,CAAC,CAAC;IAChD,IAAIoB,KAAKP,IAAI,KAAK,UAAU,OAAO,CAAC,KAAK,EAAEb,KAAK,GAAG,CAAC;IACpD,OAAO,CAAC,IAAI,EAAEA,KAAK,EAAE,CAAC;AACxB"}
1
+ {"version":3,"sources":["../../src/run/hoist-run-command.ts"],"sourcesContent":["import type {ResolvedField, ResolvedFieldDeferredSegment} from '../plan/resolved-field.js';\nimport {\n classifyShellSite,\n initialShellScanState,\n type ShellScanState,\n type ShellSiteContext,\n type ShellUnsafeRegion,\n scanShellLiteral,\n} from './shell-quoting.js';\n\nexport const unsafeRunInterpolationErrorCode = 'unsafe-run-interpolation';\n\nconst generatedNamePrefix = '__sf_';\nconst shellIdentifierPattern = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\nexport interface RunCommandHoistOptions {\n readonly reservedNames?: Iterable<string>;\n}\n\nexport interface PlannedRunCommandBinding {\n readonly name: string;\n readonly segment: ResolvedFieldDeferredSegment;\n}\n\nexport interface HoistedPlannedRunCommand {\n readonly command: string;\n readonly bindings: readonly PlannedRunCommandBinding[];\n}\n\nexport interface UnsafeRunInterpolation {\n readonly region: ShellUnsafeRegion;\n readonly source: string;\n}\n\nexport class UnsafeRunInterpolationError extends Error {\n readonly code = unsafeRunInterpolationErrorCode;\n readonly occurrences: readonly UnsafeRunInterpolation[];\n readonly partial: HoistedPlannedRunCommand | undefined;\n readonly region: ShellUnsafeRegion;\n readonly source: string;\n\n constructor(params: {\n readonly region: ShellUnsafeRegion;\n readonly source: string;\n readonly occurrences?: readonly UnsafeRunInterpolation[];\n readonly partial?: HoistedPlannedRunCommand;\n }) {\n super(\n `Unsafe run interpolation inside ${params.region}. Bind the value to env and reference $VAR instead.`,\n );\n this.name = 'UnsafeRunInterpolationError';\n this.occurrences = params.occurrences ?? [{region: params.region, source: params.source}];\n this.partial = params.partial;\n this.region = params.region;\n this.source = params.source;\n }\n}\n\n/**\n * Hoists unfilled planned run-command segments into generated shell env\n * references. Literal segments are trusted authored shell text; only call this\n * on planned command fields that have not already been filled.\n */\nexport function hoistPlannedRunCommand(params: {\n readonly field: ResolvedField;\n readonly reservedNames?: Iterable<string>;\n}): HoistedPlannedRunCommand {\n const result = hoistCommandSegments({\n segments: params.field.segments,\n options: params.reservedNames === undefined ? {} : {reservedNames: params.reservedNames},\n literalText: (segment) => segment.value,\n isBindingSegment: (segment): segment is ResolvedFieldDeferredSegment =>\n segment.kind === 'deferred',\n });\n\n const firstUnsafeInterpolation = result.unsafeInterpolations[0];\n if (firstUnsafeInterpolation !== undefined) {\n throw new UnsafeRunInterpolationError({\n ...firstUnsafeInterpolation,\n occurrences: result.unsafeInterpolations,\n partial: {command: result.command, bindings: result.bindings},\n });\n }\n\n return {command: result.command, bindings: result.bindings};\n}\n\nfunction hoistCommandSegments<\n Segment extends {readonly kind: string},\n BindingSegment extends Segment & {readonly expression: {readonly source: string}},\n>(params: {\n readonly segments: readonly Segment[];\n readonly options: RunCommandHoistOptions;\n readonly literalText: (segment: Extract<Segment, {readonly kind: 'literal'}>) => string;\n readonly isBindingSegment: (segment: Segment) => segment is BindingSegment;\n}): {\n readonly command: string;\n readonly bindings: readonly {readonly name: string; readonly segment: BindingSegment}[];\n readonly unsafeInterpolations: readonly UnsafeRunInterpolation[];\n} {\n let command = '';\n let state: ShellScanState = initialShellScanState;\n const bindings: {name: string; segment: BindingSegment}[] = [];\n const unsafeInterpolations: UnsafeRunInterpolation[] = [];\n const reservedNames = new Set(params.options.reservedNames ?? []);\n let nextIndex = 0;\n\n for (const segment of params.segments) {\n if (segment.kind === 'literal') {\n const text = params.literalText(segment as Extract<Segment, {readonly kind: 'literal'}>);\n command += text;\n state = scanShellLiteral(text, state);\n continue;\n }\n\n if (!params.isBindingSegment(segment)) continue;\n\n const site = classifyShellSite(state);\n if (site.kind === 'unsafe') {\n unsafeInterpolations.push({\n region: site.region,\n source: segment.expression.source,\n });\n continue;\n }\n\n const name = nextGeneratedName(reservedNames, nextIndex);\n nextIndex = Number(name.slice(generatedNamePrefix.length)) + 1;\n reservedNames.add(name);\n bindings.push({name, segment});\n command += shellReference(name, site);\n }\n\n return {command, bindings, unsafeInterpolations};\n}\n\nfunction nextGeneratedName(reservedNames: ReadonlySet<string>, startIndex: number): string {\n let index = startIndex;\n\n while (true) {\n const name = `${generatedNamePrefix}${index}`;\n if (!shellIdentifierPattern.test(name)) {\n throw new Error(`Generated run env name is not a shell identifier: ${name}`);\n }\n\n if (!reservedNames.has(name)) return name;\n index += 1;\n }\n}\n\nfunction shellReference(name: string, site: Exclude<ShellSiteContext, {readonly kind: 'unsafe'}>) {\n if (site.kind === 'double') return `\\${${name}}`;\n if (site.kind === 'single') return `'\"\\${${name}}\"'`;\n return `\"\\${${name}}\"`;\n}\n"],"names":["classifyShellSite","initialShellScanState","scanShellLiteral","unsafeRunInterpolationErrorCode","generatedNamePrefix","shellIdentifierPattern","UnsafeRunInterpolationError","Error","params","region","code","name","occurrences","source","partial","hoistPlannedRunCommand","result","hoistCommandSegments","segments","field","options","reservedNames","undefined","literalText","segment","value","isBindingSegment","kind","firstUnsafeInterpolation","unsafeInterpolations","command","bindings","state","Set","nextIndex","text","site","push","expression","nextGeneratedName","Number","slice","length","add","shellReference","startIndex","index","test","has"],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ export type ShellReevaluatingConstruct = 'eval' | 'sh-c' | 'bash-c' | 'source' | 'let' | 'declare-i' | 'arithmetic' | 'awk' | 'jq' | 'sed' | 'xargs-sh-c';
2
+ export interface ShellCodePositionMatch {
3
+ readonly construct: ShellReevaluatingConstruct;
4
+ readonly name: string;
5
+ }
6
+ export interface ShellCodePositionAnalysis {
7
+ readonly matches: readonly ShellCodePositionMatch[];
8
+ }
9
+ /**
10
+ * Finds workflow-controlled values that are passed to shell constructs which
11
+ * deliberately parse or evaluate their arguments again.
12
+ *
13
+ * The scanner follows direct references in a single shell word only. It does
14
+ * not attempt shell data-flow analysis, and intentionally prefers a missed
15
+ * warning over a false positive.
16
+ */
17
+ export declare function classifyShellCodePosition(params: {
18
+ readonly command: string;
19
+ readonly workflowDataNames: Iterable<string>;
20
+ }): ShellCodePositionAnalysis;
21
+ //# sourceMappingURL=shell-code-position.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell-code-position.d.ts","sourceRoot":"","sources":["../../src/run/shell-code-position.ts"],"names":[],"mappings":"AAsCA,MAAM,MAAM,0BAA0B,GAClC,MAAM,GACN,MAAM,GACN,QAAQ,GACR,QAAQ,GACR,KAAK,GACL,WAAW,GACX,YAAY,GACZ,KAAK,GACL,IAAI,GACJ,KAAK,GACL,YAAY,CAAC;AAEjB,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,SAAS,EAAE,0BAA0B,CAAC;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,OAAO,EAAE,SAAS,sBAAsB,EAAE,CAAC;CACrD;AAiCD;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE;IAChD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,iBAAiB,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;CAC9C,GAAG,yBAAyB,CAgH5B"}