@relayflows/sdk 2.0.23 → 2.0.25
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/authored-flow-executor.d.ts +3 -0
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +29 -1
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-flow-loader.d.ts +15 -2
- package/dist/authored-flow-loader.d.ts.map +1 -1
- package/dist/authored-flow-loader.js +33 -4
- package/dist/authored-flow-loader.js.map +1 -1
- package/dist/authored-hooks.d.ts +29 -0
- package/dist/authored-hooks.d.ts.map +1 -0
- package/dist/authored-hooks.js +117 -0
- package/dist/authored-hooks.js.map +1 -0
- package/dist/authored-node-entry.js +1 -0
- package/dist/authored-node-entry.js.map +1 -1
- package/dist/authored-root.d.ts +7 -0
- package/dist/authored-root.d.ts.map +1 -1
- package/dist/authored-root.js +14 -1
- package/dist/authored-root.js.map +1 -1
- package/dist/authored-source-authority.d.ts.map +1 -1
- package/dist/authored-source-authority.js +6 -0
- package/dist/authored-source-authority.js.map +1 -1
- package/dist/bundle-extensions.d.ts +19 -0
- package/dist/bundle-extensions.d.ts.map +1 -0
- package/dist/bundle-extensions.js +95 -0
- package/dist/bundle-extensions.js.map +1 -0
- package/dist/bundle-typescript.js +1 -1
- package/dist/bundle-typescript.js.map +1 -1
- package/dist/bundle.d.ts +11 -0
- package/dist/bundle.d.ts.map +1 -1
- package/dist/bundle.js +13 -4
- package/dist/bundle.js.map +1 -1
- package/dist/cli/add-extension.d.ts +28 -0
- package/dist/cli/add-extension.d.ts.map +1 -0
- package/dist/cli/add-extension.js +135 -0
- package/dist/cli/add-extension.js.map +1 -0
- package/dist/cli/add.d.ts +3 -0
- package/dist/cli/add.d.ts.map +1 -1
- package/dist/cli/add.js +8 -0
- package/dist/cli/add.js.map +1 -1
- package/dist/cli/build.d.ts.map +1 -1
- package/dist/cli/build.js +7 -2
- package/dist/cli/build.js.map +1 -1
- package/dist/cli/check-helper-body.d.ts.map +1 -1
- package/dist/cli/check-helper-body.js +4 -0
- package/dist/cli/check-helper-body.js.map +1 -1
- package/dist/cli/check-triggers.d.ts.map +1 -1
- package/dist/cli/check-triggers.js +25 -2
- package/dist/cli/check-triggers.js.map +1 -1
- package/dist/cli/check-typescript.d.ts.map +1 -1
- package/dist/cli/check-typescript.js +5 -1
- package/dist/cli/check-typescript.js.map +1 -1
- package/dist/cli/check.d.ts +24 -0
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/cloud-deploy.d.ts +1 -0
- package/dist/cli/cloud-deploy.d.ts.map +1 -1
- package/dist/cli/cloud-deploy.js +11 -1
- package/dist/cli/cloud-deploy.js.map +1 -1
- package/dist/cli/plugin.d.ts +50 -0
- package/dist/cli/plugin.d.ts.map +1 -0
- package/dist/cli/plugin.js +295 -0
- package/dist/cli/plugin.js.map +1 -0
- package/dist/cli-commands.d.ts +46 -3
- package/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +32 -2
- package/dist/cli-commands.js.map +1 -1
- package/dist/cli.d.ts +2 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +26 -1
- package/dist/cli.js.map +1 -1
- package/dist/cloud-deploy.d.ts +5 -0
- package/dist/cloud-deploy.d.ts.map +1 -1
- package/dist/cloud-deploy.js +7 -6
- package/dist/cloud-deploy.js.map +1 -1
- package/dist/cloud-run.d.ts +2 -0
- package/dist/cloud-run.d.ts.map +1 -1
- package/dist/cloud-run.js +14 -4
- package/dist/cloud-run.js.map +1 -1
- package/dist/failure-kinds.d.ts +2 -2
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/flow-extension-compat.d.ts +18 -0
- package/dist/flow-extension-compat.d.ts.map +1 -0
- package/dist/flow-extension-compat.js +33 -0
- package/dist/flow-extension-compat.js.map +1 -0
- package/dist/flow-extension-loader.d.ts +61 -0
- package/dist/flow-extension-loader.d.ts.map +1 -0
- package/dist/flow-extension-loader.js +173 -0
- package/dist/flow-extension-loader.js.map +1 -0
- package/dist/flow-extension-manifest.d.ts +61 -0
- package/dist/flow-extension-manifest.d.ts.map +1 -0
- package/dist/flow-extension-manifest.js +188 -0
- package/dist/flow-extension-manifest.js.map +1 -0
- package/dist/flow-extension-submit.d.ts +38 -0
- package/dist/flow-extension-submit.d.ts.map +1 -0
- package/dist/flow-extension-submit.js +94 -0
- package/dist/flow-extension-submit.js.map +1 -0
- package/dist/flow-requirements.d.ts +13 -3
- package/dist/flow-requirements.d.ts.map +1 -1
- package/dist/flow-requirements.js +30 -0
- package/dist/flow-requirements.js.map +1 -1
- package/dist/plugin-github.d.ts +35 -0
- package/dist/plugin-github.d.ts.map +1 -0
- package/dist/plugin-github.js +92 -0
- package/dist/plugin-github.js.map +1 -0
- package/dist/plugin-loader.d.ts.map +1 -1
- package/dist/plugin-loader.js +5 -1
- package/dist/plugin-loader.js.map +1 -1
- package/dist/plugin-lock.d.ts +79 -0
- package/dist/plugin-lock.d.ts.map +1 -0
- package/dist/plugin-lock.js +203 -0
- package/dist/plugin-lock.js.map +1 -0
- package/dist/plugin-manifest.d.ts +5 -1
- package/dist/plugin-manifest.d.ts.map +1 -1
- package/dist/plugin-manifest.js +17 -0
- package/dist/plugin-manifest.js.map +1 -1
- package/dist/plugin-source.d.ts +38 -0
- package/dist/plugin-source.d.ts.map +1 -0
- package/dist/plugin-source.js +84 -0
- package/dist/plugin-source.js.map +1 -0
- package/dist/plugin-store.d.ts +30 -0
- package/dist/plugin-store.d.ts.map +1 -0
- package/dist/plugin-store.js +133 -0
- package/dist/plugin-store.js.map +1 -0
- package/dist/semver-range.d.ts +10 -0
- package/dist/semver-range.d.ts.map +1 -0
- package/dist/semver-range.js +88 -0
- package/dist/semver-range.js.map +1 -0
- package/package.json +2 -2
- package/src/authored-flow-executor.ts +36 -1
- package/src/authored-flow-loader.ts +49 -6
- package/src/authored-hooks.ts +136 -0
- package/src/authored-node-entry.ts +1 -0
- package/src/authored-root.ts +22 -1
- package/src/authored-source-authority.ts +6 -0
- package/src/bundle-extensions.ts +85 -0
- package/src/bundle-typescript.ts +1 -1
- package/src/bundle.ts +14 -4
- package/src/cli/add-extension.ts +134 -0
- package/src/cli/add.ts +9 -0
- package/src/cli/build.ts +7 -2
- package/src/cli/check-helper-body.ts +4 -0
- package/src/cli/check-triggers.ts +27 -2
- package/src/cli/check-typescript.ts +5 -1
- package/src/cli/check.ts +23 -0
- package/src/cli/cloud-deploy.ts +11 -1
- package/src/cli/plugin.ts +286 -0
- package/src/cli-commands.ts +32 -2
- package/src/cli.ts +23 -1
- package/src/cloud-deploy.ts +18 -8
- package/src/cloud-run.ts +16 -6
- package/src/flow-extension-compat.ts +35 -0
- package/src/flow-extension-loader.ts +217 -0
- package/src/flow-extension-manifest.ts +190 -0
- package/src/flow-extension-submit.ts +120 -0
- package/src/flow-requirements.ts +44 -3
- package/src/plugin-github.ts +98 -0
- package/src/plugin-loader.ts +5 -1
- package/src/plugin-lock.ts +194 -0
- package/src/plugin-manifest.ts +17 -0
- package/src/plugin-source.ts +91 -0
- package/src/plugin-store.ts +105 -0
- package/src/semver-range.ts +74 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface Parsed {
|
|
2
|
+
readonly triple: readonly [number, number, number];
|
|
3
|
+
readonly pre?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function parseVersion(value: string): Parsed | undefined;
|
|
6
|
+
export declare function isVersionRange(value: string): boolean;
|
|
7
|
+
/** Whether `version` satisfies `range`; false for malformed input rather than a throw. */
|
|
8
|
+
export declare function satisfiesRange(version: string, range: string): boolean;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=semver-range.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"semver-range.d.ts","sourceRoot":"","sources":["../src/semver-range.ts"],"names":[],"mappings":"AAYA,UAAU,MAAM;IAAG,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE;AAE9F,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAM9D;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAErD;AAgCD,0FAA0F;AAC1F,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAgBtE"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The small semver subset plugin `compat` ranges may use. Deliberately not a
|
|
3
|
+
* dependency on `semver`: a plugin's compatibility claim has to be checkable by
|
|
4
|
+
* every reader — Cloud, the CLI, a catalog — from one short, obvious rule.
|
|
5
|
+
*
|
|
6
|
+
* Accepted: `*`, `x.y.z`, `^x.y.z`, `~x.y.z`, `>=x.y.z`, `>=x.y.z <a.b.c`.
|
|
7
|
+
* Prerelease tags are ordered lexically after the numeric triple and only
|
|
8
|
+
* compare against a range whose bounds name the same triple.
|
|
9
|
+
*/
|
|
10
|
+
const VERSION = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
|
|
11
|
+
const RANGE = /^(\*|(?:[\^~]|>=)?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?: <\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)?)$/;
|
|
12
|
+
export function parseVersion(value) {
|
|
13
|
+
const m = VERSION.exec(value);
|
|
14
|
+
if (!m)
|
|
15
|
+
return undefined;
|
|
16
|
+
const triple = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
17
|
+
if (triple.some(n => !Number.isSafeInteger(n)))
|
|
18
|
+
return undefined;
|
|
19
|
+
return m[4] === undefined ? { triple } : { triple, pre: m[4] };
|
|
20
|
+
}
|
|
21
|
+
export function isVersionRange(value) {
|
|
22
|
+
return RANGE.test(value);
|
|
23
|
+
}
|
|
24
|
+
function compare(a, b) {
|
|
25
|
+
for (let i = 0; i < 3; i++)
|
|
26
|
+
if (a.triple[i] !== b.triple[i])
|
|
27
|
+
return a.triple[i] - b.triple[i];
|
|
28
|
+
if (a.pre === b.pre)
|
|
29
|
+
return 0;
|
|
30
|
+
if (a.pre === undefined)
|
|
31
|
+
return 1;
|
|
32
|
+
if (b.pre === undefined)
|
|
33
|
+
return -1;
|
|
34
|
+
return comparePrerelease(a.pre, b.pre);
|
|
35
|
+
}
|
|
36
|
+
function comparePrerelease(a, b) {
|
|
37
|
+
const as = a.split('.');
|
|
38
|
+
const bs = b.split('.');
|
|
39
|
+
const n = Math.max(as.length, bs.length);
|
|
40
|
+
for (let i = 0; i < n; i++) {
|
|
41
|
+
const left = as[i];
|
|
42
|
+
const right = bs[i];
|
|
43
|
+
if (left === undefined)
|
|
44
|
+
return -1;
|
|
45
|
+
if (right === undefined)
|
|
46
|
+
return 1;
|
|
47
|
+
const leftNum = /^\d+$/.test(left) ? Number(left) : undefined;
|
|
48
|
+
const rightNum = /^\d+$/.test(right) ? Number(right) : undefined;
|
|
49
|
+
if (leftNum !== undefined && rightNum !== undefined) {
|
|
50
|
+
if (leftNum !== rightNum)
|
|
51
|
+
return leftNum - rightNum;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (leftNum !== undefined)
|
|
55
|
+
return -1;
|
|
56
|
+
if (rightNum !== undefined)
|
|
57
|
+
return 1;
|
|
58
|
+
if (left !== right)
|
|
59
|
+
return left < right ? -1 : 1;
|
|
60
|
+
}
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
/** Whether `version` satisfies `range`; false for malformed input rather than a throw. */
|
|
64
|
+
export function satisfiesRange(version, range) {
|
|
65
|
+
const v = parseVersion(version);
|
|
66
|
+
if (v === undefined || !isVersionRange(range))
|
|
67
|
+
return false;
|
|
68
|
+
if (range === '*')
|
|
69
|
+
return true;
|
|
70
|
+
const [lowerText, upperText] = range.split(' ');
|
|
71
|
+
const operator = /^[\^~]|^>=/.exec(lowerText)?.[0] ?? '';
|
|
72
|
+
const lower = parseVersion(lowerText.slice(operator.length));
|
|
73
|
+
if (compare(v, lower) < 0)
|
|
74
|
+
return false;
|
|
75
|
+
if (operator === '')
|
|
76
|
+
return compare(v, lower) === 0;
|
|
77
|
+
let upper;
|
|
78
|
+
if (upperText !== undefined)
|
|
79
|
+
upper = parseVersion(upperText.slice(1));
|
|
80
|
+
else if (operator === '^') {
|
|
81
|
+
const [major, minor] = lower.triple;
|
|
82
|
+
upper = major > 0 ? { triple: [major + 1, 0, 0] } : minor > 0 ? { triple: [0, minor + 1, 0] } : { triple: [0, 0, lower.triple[2] + 1] };
|
|
83
|
+
}
|
|
84
|
+
else if (operator === '~')
|
|
85
|
+
upper = { triple: [lower.triple[0], lower.triple[1] + 1, 0] };
|
|
86
|
+
return upper === undefined || compare(v, upper) < 0;
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=semver-range.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"semver-range.js","sourceRoot":"","sources":["../src/semver-range.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,OAAO,GAAG,6CAA6C,CAAC;AAC9D,MAAM,KAAK,GAAG,+FAA+F,CAAC;AAI9G,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC9B,IAAI,CAAC,CAAC;QAAE,OAAO,SAAS,CAAC;IACzB,MAAM,MAAM,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAU,CAAC;IACnE,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;QAAE,OAAO,SAAS,CAAC;IACjE,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AACjE,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC3B,CAAC;AAED,SAAS,OAAO,CAAC,CAAS,EAAE,CAAS;IACnC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE;QAAE,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;YAAE,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAE,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC;IAChG,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,GAAG;QAAE,OAAO,CAAC,CAAC;IAC9B,IAAI,CAAC,CAAC,GAAG,KAAK,SAAS;QAAE,OAAO,CAAC,CAAC;IAClC,IAAI,CAAC,CAAC,GAAG,KAAK,SAAS;QAAE,OAAO,CAAC,CAAC,CAAC;IACnC,OAAO,iBAAiB,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC;AACzC,CAAC;AAED,SAAS,iBAAiB,CAAC,CAAS,EAAE,CAAS;IAC7C,MAAM,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACxB,MAAM,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACxB,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC;IACzC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3B,MAAM,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;QACnB,MAAM,KAAK,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;QACpB,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,CAAC,CAAC,CAAC;QAClC,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,CAAC,CAAC;QAClC,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAC9D,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACjE,IAAI,OAAO,KAAK,SAAS,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YACpD,IAAI,OAAO,KAAK,QAAQ;gBAAE,OAAO,OAAO,GAAG,QAAQ,CAAC;YACpD,SAAS;QACX,CAAC;QACD,IAAI,OAAO,KAAK,SAAS;YAAE,OAAO,CAAC,CAAC,CAAC;QACrC,IAAI,QAAQ,KAAK,SAAS;YAAE,OAAO,CAAC,CAAC;QACrC,IAAI,IAAI,KAAK,KAAK;YAAE,OAAO,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACnD,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,0FAA0F;AAC1F,MAAM,UAAU,cAAc,CAAC,OAAe,EAAE,KAAa;IAC3D,MAAM,CAAC,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;IAChC,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC5D,IAAI,KAAK,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IAC/B,MAAM,CAAC,SAAS,EAAE,SAAS,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAiC,CAAC;IAChF,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACzD,MAAM,KAAK,GAAG,YAAY,CAAC,SAAS,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAE,CAAC;IAC9D,IAAI,OAAO,CAAC,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IACxC,IAAI,QAAQ,KAAK,EAAE;QAAE,OAAO,OAAO,CAAC,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;IACpD,IAAI,KAAyB,CAAC;IAC9B,IAAI,SAAS,KAAK,SAAS;QAAE,KAAK,GAAG,YAAY,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;SACjE,IAAI,QAAQ,KAAK,GAAG,EAAE,CAAC;QAC1B,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC;QACpC,KAAK,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;IAC1I,CAAC;SAAM,IAAI,QAAQ,KAAK,GAAG;QAAE,KAAK,GAAG,EAAE,MAAM,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;IAC3F,OAAO,KAAK,KAAK,SAAS,IAAI,OAAO,CAAC,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;AACtD,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@relayflows/sdk",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.25",
|
|
4
4
|
"description": "TypeScript-first authoring SDK for Relayflows. Compiles specs; speaks the journal protocol.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
50
50
|
"@relayfile/adapter-core": "0.5.26",
|
|
51
51
|
"@relayfile/relay-helpers": "0.4.11",
|
|
52
|
-
"@relayflows/surface": "2.0.
|
|
52
|
+
"@relayflows/surface": "2.0.25",
|
|
53
53
|
"@types/js-yaml": "^4.0.9",
|
|
54
54
|
"ai-hist": "0.4.1",
|
|
55
55
|
"ajv": "^8.17.1",
|
|
@@ -44,6 +44,8 @@ import {
|
|
|
44
44
|
} from './authored-flow-operation.js';
|
|
45
45
|
import { AuthoredFlowLifecycle } from './authored-flow-lifecycle.js';
|
|
46
46
|
import { JournalClient } from './journal-client.js';
|
|
47
|
+
import { createHookEvaluator } from './authored-hooks.js';
|
|
48
|
+
import { probeFlowExtension, type LoadedFlowExtension } from './flow-extension-loader.js';
|
|
47
49
|
import type {
|
|
48
50
|
CompletionReason as ProtocolCompletionReason,
|
|
49
51
|
RunCompletionReason as ProtocolRunCompletionReason,
|
|
@@ -148,6 +150,8 @@ export interface ExecuteAuthoredFlowOptions {
|
|
|
148
150
|
readonly localAgentStream?: string;
|
|
149
151
|
/** Durable kernel root that owns this body's child admission identities. */
|
|
150
152
|
readonly rootRunId?: string;
|
|
153
|
+
/** Installed flow-extension plugins, in lock order, so `f.hook` can AND-compose them. */
|
|
154
|
+
readonly extensions?: readonly LoadedFlowExtension[];
|
|
151
155
|
}
|
|
152
156
|
|
|
153
157
|
export async function executeAuthoredFlow<Input = undefined>(
|
|
@@ -171,7 +175,7 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
171
175
|
...(options.onWait !== undefined ? { onWait: options.onWait } : {}),
|
|
172
176
|
};
|
|
173
177
|
const definition = getDefinition<Input>(handle);
|
|
174
|
-
const headerFields = Object.keys(definition.header).filter(key => key !== 'tools' && key !== 'budget' && key !== 'memory');
|
|
178
|
+
const headerFields = Object.keys(definition.header).filter(key => key !== 'tools' && key !== 'budget' && key !== 'memory' && key !== 'version' && key !== 'hooks');
|
|
175
179
|
if (definition.header.tools && Object.keys(definition.header.tools).some(key => !['mcp', ...helperProviders.map(p => p.namespace)].includes(key))) headerFields.push('tools');
|
|
176
180
|
if (definition.header.tools?.relayfile !== undefined) headerFields.push('tools.relayfile');
|
|
177
181
|
const helperPreflight = checkSlackHelpers(definition);
|
|
@@ -188,6 +192,9 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
188
192
|
|
|
189
193
|
const checkedMcp = await checkMcpHeader(definition, flowPath);
|
|
190
194
|
if (!checkedMcp.report.ok) throw new McpPreflightError(checkedMcp.report);
|
|
195
|
+
for (const extension of options.extensions ?? []) {
|
|
196
|
+
if (extension.manifest !== undefined) await probeFlowExtension(extension.manifest);
|
|
197
|
+
}
|
|
191
198
|
|
|
192
199
|
const budget = new AuthoredBudget(definition.header.budget);
|
|
193
200
|
if (definition.header.memory?.agent === true) {
|
|
@@ -349,6 +356,17 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
349
356
|
));
|
|
350
357
|
}
|
|
351
358
|
|
|
359
|
+
const evaluateHook = createHookEvaluator({
|
|
360
|
+
journal,
|
|
361
|
+
...(options.rootRunId === undefined ? {} : { rootRunId: options.rootRunId }),
|
|
362
|
+
flowName: definition.name,
|
|
363
|
+
declared: definition.header?.hooks ?? [],
|
|
364
|
+
extensions: options.extensions ?? [],
|
|
365
|
+
peekStep: () => nextStep,
|
|
366
|
+
restoreStep: (step) => { nextStep = step; },
|
|
367
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
368
|
+
});
|
|
369
|
+
|
|
352
370
|
const context: Ctx = {
|
|
353
371
|
...createHelpers(<T>(call: HelperCall): Step<T> => {
|
|
354
372
|
const verb = `${call.provider}.${call.verb}`;
|
|
@@ -477,6 +495,23 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
477
495
|
assertOperationAllowed('dispatch', definition.name, requestedCompletion);
|
|
478
496
|
throw unsupportedVerb('dispatch');
|
|
479
497
|
},
|
|
498
|
+
hook(name, input) {
|
|
499
|
+
assertOperationAllowed('hook', definition.name, requestedCompletion);
|
|
500
|
+
const id = `hook-${nextStep++}`;
|
|
501
|
+
const snapshot = snapshotJsonValue(input, 'f.hook input');
|
|
502
|
+
return trackStep(authoredSteps, new AuthoredFlowOperation<boolean>(
|
|
503
|
+
id, 'hook',
|
|
504
|
+
() => assertOperationAllowed('hook', definition.name, requestedCompletion),
|
|
505
|
+
async () => {
|
|
506
|
+
const verdict = await evaluateHook(id, name, snapshot, context);
|
|
507
|
+
const record = { hook: name, step: id, verdict: verdict ? 'pass' : 'fail' };
|
|
508
|
+
const literal = `'${JSON.stringify(record).replaceAll("'", "'\\''")}'`;
|
|
509
|
+
await observeStep(id, 'deterministic', () => lowerDeterministic(id, `printf '%s' ${literal}`, false), options.onProgress);
|
|
510
|
+
return verdict;
|
|
511
|
+
},
|
|
512
|
+
lifecycle,
|
|
513
|
+
));
|
|
514
|
+
},
|
|
480
515
|
done(reason) {
|
|
481
516
|
if (!isSurfaceFlowCompletionReason(reason)) {
|
|
482
517
|
throw new AuthoredFlowExecutionError(
|
|
@@ -9,6 +9,9 @@ import {
|
|
|
9
9
|
type AuthoredFlowDefinition,
|
|
10
10
|
type FlowHandle,
|
|
11
11
|
} from './authored-flow.js';
|
|
12
|
+
import { canonicalize } from './canonical.js';
|
|
13
|
+
import { composeDefinition, loadFlowExtensions, parseHooksExport, type ImportedFlow, type LoadedFlowExtension } from './flow-extension-loader.js';
|
|
14
|
+
import type { RuntimeVersions } from './flow-extension-compat.js';
|
|
12
15
|
|
|
13
16
|
export class AuthoredFlowLoadError extends Error {
|
|
14
17
|
constructor(message: string, readonly kind: 'invalid_spec' | 'use_not_found' | 'use_invalid' | 'use_cycle' = 'invalid_spec') {
|
|
@@ -40,8 +43,20 @@ export interface LoadedAuthoredFlow {
|
|
|
40
43
|
readonly getDefinition: GetFlowDefinition;
|
|
41
44
|
/** Exact Surface package/runtime that owns the handle's WeakMap identity. */
|
|
42
45
|
readonly surfaceAuthority: SurfaceModuleAuthority;
|
|
43
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Dependency-first load order, each canonical absolute path appearing once.
|
|
48
|
+
* Flow-extension entries follow the root, so a graph of one node still
|
|
49
|
+
* means "one self-contained source" to the hosted paths that require it.
|
|
50
|
+
*/
|
|
44
51
|
readonly graph: readonly LoadedAuthoredFlowNode[];
|
|
52
|
+
/** Schema-2 flow extensions composed onto the root, in lock order; empty when the project declares none. */
|
|
53
|
+
readonly extensions: readonly LoadedFlowExtension[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface LoadAuthoredFlowOptions {
|
|
57
|
+
/** `compose` (default) verifies and appends the project's flow extensions; `none` loads the root alone. */
|
|
58
|
+
readonly extensions?: 'compose' | 'none';
|
|
59
|
+
readonly versions?: RuntimeVersions;
|
|
45
60
|
}
|
|
46
61
|
|
|
47
62
|
export interface LoadedAuthoredFlowNode {
|
|
@@ -53,7 +68,7 @@ export interface LoadedAuthoredFlowNode {
|
|
|
53
68
|
}
|
|
54
69
|
|
|
55
70
|
/** Import and validate a direct-run module without executing its authored body. */
|
|
56
|
-
export async function loadAuthoredFlow(path: string): Promise<LoadedAuthoredFlow> {
|
|
71
|
+
export async function loadAuthoredFlow(path: string, options: LoadAuthoredFlowOptions = {}): Promise<LoadedAuthoredFlow> {
|
|
57
72
|
const loaded = new Map<string, LoadedAuthoredFlowNode>();
|
|
58
73
|
const visiting = new Set<string>();
|
|
59
74
|
async function visit(sourcePath: string, isRoot = false): Promise<LoadedAuthoredFlowNode> {
|
|
@@ -98,11 +113,39 @@ export async function loadAuthoredFlow(path: string): Promise<LoadedAuthoredFlow
|
|
|
98
113
|
}
|
|
99
114
|
}
|
|
100
115
|
const root = await visit(resolve(path), true);
|
|
101
|
-
|
|
102
|
-
|
|
116
|
+
const graph = [...loaded.values()];
|
|
117
|
+
if (options.extensions === 'none') {
|
|
118
|
+
return Object.freeze({ sourcePath: root.path, handle: root.handle, getDefinition: root.getDefinition,
|
|
119
|
+
surfaceAuthority: root.surfaceAuthority, graph: Object.freeze(graph), extensions: Object.freeze([]) });
|
|
120
|
+
}
|
|
121
|
+
// Extensions are verified (lock, digest, manifest hash, compat) before their
|
|
122
|
+
// entry is imported, and are composed after the root in lock order. The
|
|
123
|
+
// composed definition is served for the root handle only; every other
|
|
124
|
+
// handle keeps the surface copy's own answer.
|
|
125
|
+
const baseDefinition = root.getDefinition(root.handle);
|
|
126
|
+
const extensions = await loadFlowExtensions(root.path, { definition: baseDefinition, surfaceAuthority: root.surfaceAuthority }, {
|
|
127
|
+
importFlow: importAuthoredFlow,
|
|
128
|
+
sameAuthority: (a, b) => canonicalize(a) === canonicalize(b),
|
|
129
|
+
...(options.versions === undefined ? {} : { versions: options.versions }),
|
|
130
|
+
});
|
|
131
|
+
if (extensions.length === 0) {
|
|
132
|
+
return Object.freeze({ sourcePath: root.path, handle: root.handle, getDefinition: root.getDefinition,
|
|
133
|
+
surfaceAuthority: root.surfaceAuthority, graph: Object.freeze(graph), extensions });
|
|
134
|
+
}
|
|
135
|
+
const composed = composeDefinition(baseDefinition, extensions);
|
|
136
|
+
const getDefinition: GetFlowDefinition = <Input = unknown>(handle: FlowHandle) =>
|
|
137
|
+
(handle === root.handle ? composed : root.getDefinition(handle)) as AuthoredFlowDefinition<Input>;
|
|
138
|
+
for (const extension of extensions) {
|
|
139
|
+
// The node carries the accessor the entry's own surface copy handed back:
|
|
140
|
+
// the root's accessor answers for the root's WeakMap only.
|
|
141
|
+
graph.push(Object.freeze({ path: extension.entryPath, handle: extension.handle, getDefinition: extension.getDefinition,
|
|
142
|
+
surfaceAuthority: root.surfaceAuthority, use: Object.freeze([]) }));
|
|
143
|
+
}
|
|
144
|
+
return Object.freeze({ sourcePath: root.path, handle: root.handle, getDefinition,
|
|
145
|
+
surfaceAuthority: root.surfaceAuthority, graph: Object.freeze(graph), extensions });
|
|
103
146
|
}
|
|
104
147
|
|
|
105
|
-
async function importAuthoredFlow(path: string): Promise<
|
|
148
|
+
async function importAuthoredFlow(path: string): Promise<ImportedFlow<SurfaceModuleAuthority>> {
|
|
106
149
|
const absolutePath = resolve(path);
|
|
107
150
|
try {
|
|
108
151
|
accessSync(absolutePath, constants.R_OK);
|
|
@@ -128,7 +171,7 @@ async function importAuthoredFlow(path: string): Promise<Pick<LoadedAuthoredFlow
|
|
|
128
171
|
`Flow "${path}" must default-export flow(...): ${errorMessage(error)}`,
|
|
129
172
|
);
|
|
130
173
|
}
|
|
131
|
-
return { handle, getDefinition, surfaceAuthority };
|
|
174
|
+
return { handle, getDefinition, surfaceAuthority, hooks: parseHooksExport(authoredModule, path) };
|
|
132
175
|
}
|
|
133
176
|
|
|
134
177
|
/**
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { Ctx } from '@relayflows/surface';
|
|
2
|
+
import { AuthoredFlowExecutionError } from './authored-flow-error.js';
|
|
3
|
+
import type { LoadedFlowExtension } from './flow-extension-loader.js';
|
|
4
|
+
import type { JournalClient } from './journal-client.js';
|
|
5
|
+
|
|
6
|
+
const HOOK_STREAM = 'hooks';
|
|
7
|
+
|
|
8
|
+
export interface HookRecord {
|
|
9
|
+
hook: string;
|
|
10
|
+
step: string;
|
|
11
|
+
plugin: string | null;
|
|
12
|
+
verdict: 'pass' | 'fail' | 'noop';
|
|
13
|
+
because?: string;
|
|
14
|
+
/** Parent `nextStep` after this verdict, so resume does not reuse inner step ids. */
|
|
15
|
+
afterStep?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function recordKey(record: HookRecord): string {
|
|
19
|
+
return `${record.step}:${record.plugin ?? 'noop'}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* AND-compose installed hook implementations in lock order. Verdicts are
|
|
24
|
+
* appended to the root run's `hooks` stream before the boolean is returned,
|
|
25
|
+
* so a resume reads the recorded verdict and never re-runs the closure.
|
|
26
|
+
* No implementations is a journaled no-op returning true. A false short-circuits.
|
|
27
|
+
*/
|
|
28
|
+
export function createHookEvaluator(options: {
|
|
29
|
+
readonly journal: JournalClient;
|
|
30
|
+
readonly rootRunId?: string;
|
|
31
|
+
readonly flowName: string;
|
|
32
|
+
readonly declared: readonly string[];
|
|
33
|
+
readonly extensions: readonly LoadedFlowExtension[];
|
|
34
|
+
readonly peekStep?: () => number;
|
|
35
|
+
readonly restoreStep?: (step: number) => void;
|
|
36
|
+
readonly signal?: AbortSignal;
|
|
37
|
+
}): (id: string, name: string, input: unknown, context: Ctx) => Promise<boolean> {
|
|
38
|
+
let recorded: Promise<Map<string, HookRecord>> | undefined;
|
|
39
|
+
|
|
40
|
+
async function load(): Promise<Map<string, HookRecord>> {
|
|
41
|
+
const verdicts = new Map<string, HookRecord>();
|
|
42
|
+
if (options.rootRunId === undefined) return verdicts;
|
|
43
|
+
let offset = 0;
|
|
44
|
+
for (;;) {
|
|
45
|
+
const page = await options.journal.streamRead(options.rootRunId, HOOK_STREAM, offset, 1000);
|
|
46
|
+
for (const message of page.messages) {
|
|
47
|
+
const raw = (message as { message?: unknown }).message ?? message;
|
|
48
|
+
if (typeof raw !== 'object' || raw === null) continue;
|
|
49
|
+
const record = raw as HookRecord;
|
|
50
|
+
if (typeof record.hook !== 'string' || typeof record.step !== 'string') continue;
|
|
51
|
+
if (record.verdict !== 'pass' && record.verdict !== 'fail' && record.verdict !== 'noop') continue;
|
|
52
|
+
if (record.plugin !== null && typeof record.plugin !== 'string') continue;
|
|
53
|
+
verdicts.set(recordKey(record), record);
|
|
54
|
+
}
|
|
55
|
+
if (page.messages.length === 0 || page.next_offset <= offset) break;
|
|
56
|
+
offset = page.next_offset;
|
|
57
|
+
}
|
|
58
|
+
return verdicts;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function lookup(key: string): Promise<HookRecord | undefined> {
|
|
62
|
+
if (options.rootRunId === undefined) return undefined;
|
|
63
|
+
recorded ??= load();
|
|
64
|
+
return (await recorded).get(key);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function append(record: HookRecord): Promise<void> {
|
|
68
|
+
if (options.rootRunId === undefined) return;
|
|
69
|
+
recorded ??= load();
|
|
70
|
+
await options.journal.streamAppend(options.rootRunId, HOOK_STREAM, record);
|
|
71
|
+
(await recorded).set(recordKey(record), record);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return async (id, name, input, context) => {
|
|
75
|
+
if (!options.declared.includes(name)) {
|
|
76
|
+
throw new AuthoredFlowExecutionError(
|
|
77
|
+
'unsupported_verb',
|
|
78
|
+
`hook "${name}" is not declared by flow "${options.flowName}"`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
const impls = options.extensions.filter(extension => extension.hooks[name] !== undefined);
|
|
82
|
+
if (impls.length === 0) {
|
|
83
|
+
const existing = await lookup(`${id}:noop`);
|
|
84
|
+
const record = existing ?? { hook: name, step: id, plugin: null, verdict: 'noop' as const };
|
|
85
|
+
if (existing === undefined) await append(record);
|
|
86
|
+
else if (existing.afterStep !== undefined) options.restoreStep?.(existing.afterStep);
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
for (const extension of impls) {
|
|
90
|
+
const key = `${id}:${extension.name}`;
|
|
91
|
+
let record = await lookup(key);
|
|
92
|
+
if (record === undefined) {
|
|
93
|
+
let verdict = false;
|
|
94
|
+
let because: string | undefined;
|
|
95
|
+
try {
|
|
96
|
+
verdict = await boundHook(extension.hooks[name]!(context, input), options.signal) === true;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
// Cancellation is control-plane state, not a plugin verdict. The
|
|
99
|
+
// shared execution signal also stops tracked f.run/f.agent work;
|
|
100
|
+
// rethrow so a later resume may run the hook again instead of
|
|
101
|
+
// replaying a permanent decline caused by an interrupted attempt.
|
|
102
|
+
if (options.signal?.aborted) throw error;
|
|
103
|
+
verdict = false;
|
|
104
|
+
because = error instanceof Error ? error.message : String(error);
|
|
105
|
+
}
|
|
106
|
+
record = {
|
|
107
|
+
hook: name, step: id, plugin: extension.name,
|
|
108
|
+
verdict: verdict ? 'pass' : 'fail',
|
|
109
|
+
...(because === undefined ? {} : { because }),
|
|
110
|
+
...(options.peekStep === undefined ? {} : { afterStep: options.peekStep() }),
|
|
111
|
+
};
|
|
112
|
+
await append(record);
|
|
113
|
+
} else if (record.afterStep !== undefined) {
|
|
114
|
+
options.restoreStep?.(record.afterStep);
|
|
115
|
+
}
|
|
116
|
+
if (record.verdict === 'fail') return false;
|
|
117
|
+
}
|
|
118
|
+
return true;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function boundHook(run: Promise<boolean>, signal?: AbortSignal): Promise<boolean> {
|
|
123
|
+
// The parent run's wallclock budget owns the deadline. A second timer here
|
|
124
|
+
// would not be the signal captured by the hook's Ctx operations, allowing
|
|
125
|
+
// those operations to continue after this wrapper returned.
|
|
126
|
+
if (signal === undefined) return await run;
|
|
127
|
+
if (signal.aborted) throw signal.reason ?? new Error('hook cancelled');
|
|
128
|
+
return await new Promise<boolean>((resolve, reject) => {
|
|
129
|
+
const onAbort = () => reject(signal.reason ?? new Error('hook cancelled'));
|
|
130
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
131
|
+
run.then(
|
|
132
|
+
value => { signal.removeEventListener('abort', onAbort); resolve(value); },
|
|
133
|
+
error => { signal.removeEventListener('abort', onAbort); reject(error); },
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
@@ -72,6 +72,7 @@ try {
|
|
|
72
72
|
request.metadata.inputPresent ? request.metadata.input : undefined, {
|
|
73
73
|
getDefinition: loaded.getDefinition, dataDir: request.dataDir,
|
|
74
74
|
flowPath: request.metadata.flowPath, rootRunId: request.rootRunId,
|
|
75
|
+
extensions: loaded.extensions,
|
|
75
76
|
localAgentStream: request.localAgentStream, signal: controller.signal,
|
|
76
77
|
onProgress: event => send({ type: 'progress', event }),
|
|
77
78
|
onWait: event => send({ type: 'wait', event }),
|
package/src/authored-root.ts
CHANGED
|
@@ -21,6 +21,12 @@ import { isSurfaceCompletionReason } from './authored-step-output.js';
|
|
|
21
21
|
|
|
22
22
|
const ROOT_KIND = 'relayflows.authored-root.v1';
|
|
23
23
|
|
|
24
|
+
export interface AuthoredRootExtension {
|
|
25
|
+
readonly name: string;
|
|
26
|
+
readonly digest: string;
|
|
27
|
+
readonly ref: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
24
30
|
export interface AuthoredRootMetadata {
|
|
25
31
|
readonly kind: typeof ROOT_KIND;
|
|
26
32
|
readonly flowName: string;
|
|
@@ -28,6 +34,8 @@ export interface AuthoredRootMetadata {
|
|
|
28
34
|
readonly sourceSha256: string;
|
|
29
35
|
readonly surface: SurfaceModuleAuthority;
|
|
30
36
|
readonly sources: readonly AuthoredRootSourceAuthority[];
|
|
37
|
+
/** Plugin-level provenance in lock order; empty when the project declares none. */
|
|
38
|
+
readonly extensions: readonly AuthoredRootExtension[];
|
|
31
39
|
readonly localAgentStream?: string;
|
|
32
40
|
readonly inputPresent: boolean;
|
|
33
41
|
readonly input?: unknown;
|
|
@@ -68,6 +76,9 @@ export async function executeDurableAuthoredFlow(
|
|
|
68
76
|
sourceSha256: sha256(source),
|
|
69
77
|
surface: loaded.surfaceAuthority,
|
|
70
78
|
sources: Object.freeze(sources),
|
|
79
|
+
extensions: Object.freeze((loaded.extensions ?? []).map(extension => Object.freeze({
|
|
80
|
+
name: extension.name, digest: extension.digest, ref: extension.ref,
|
|
81
|
+
}))),
|
|
71
82
|
...(options.localAgentStream === undefined ? {} : { localAgentStream: options.localAgentStream }),
|
|
72
83
|
inputPresent: input !== undefined,
|
|
73
84
|
...(input === undefined ? {} : { input: jsonSnapshot(input, 'authored root input') }),
|
|
@@ -172,7 +183,7 @@ export async function readAuthoredRootMetadata(
|
|
|
172
183
|
if (!isRootMetadata(value)) {
|
|
173
184
|
throw new Error('authored root journal has malformed authority metadata');
|
|
174
185
|
}
|
|
175
|
-
return value;
|
|
186
|
+
return Object.freeze({ ...value, extensions: value.extensions ?? [] });
|
|
176
187
|
}
|
|
177
188
|
|
|
178
189
|
async function driveRoot(
|
|
@@ -203,6 +214,7 @@ async function driveRoot(
|
|
|
203
214
|
flowPath: metadata.flowPath,
|
|
204
215
|
localAgentStream: options.localAgentStream,
|
|
205
216
|
rootRunId: dispatch.run_id,
|
|
217
|
+
extensions: loaded.extensions,
|
|
206
218
|
...options.lifecycle,
|
|
207
219
|
signal: callerSignal === undefined
|
|
208
220
|
? rootSignal
|
|
@@ -409,10 +421,19 @@ function isRootMetadata(value: unknown): value is AuthoredRootMetadata {
|
|
|
409
421
|
&& typeof surface.version === 'string' && /^[a-f0-9]{64}$/.test(surface.packageSha256 ?? '')
|
|
410
422
|
&& /^[a-f0-9]{64}$/.test(surface.runtimeSha256 ?? '')
|
|
411
423
|
&& Array.isArray(sources) && sources.length > 0 && sources.every(isRootSourceAuthority)
|
|
424
|
+
&& (root.extensions === undefined || (Array.isArray(root.extensions) && root.extensions.every(isRootExtension)))
|
|
412
425
|
&& (root.localAgentStream === undefined
|
|
413
426
|
|| /^local-agent-[a-f0-9-]+$/.test(root.localAgentStream));
|
|
414
427
|
}
|
|
415
428
|
|
|
429
|
+
function isRootExtension(value: unknown): value is AuthoredRootExtension {
|
|
430
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
|
|
431
|
+
const extension = value as Partial<AuthoredRootExtension>;
|
|
432
|
+
return typeof extension.name === 'string' && extension.name.length > 0
|
|
433
|
+
&& /^[a-f0-9]{64}$/.test(extension.digest ?? '')
|
|
434
|
+
&& typeof extension.ref === 'string' && extension.ref.startsWith('github:');
|
|
435
|
+
}
|
|
436
|
+
|
|
416
437
|
function isRootSourceAuthority(value: unknown): value is AuthoredRootSourceAuthority {
|
|
417
438
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
|
|
418
439
|
const source = value as Partial<AuthoredRootSourceAuthority>;
|
|
@@ -35,6 +35,12 @@ export async function loadPinnedAuthoredSource(metadata: AuthoredRootMetadata, i
|
|
|
35
35
|
if (canonicalize(loadedSources) !== canonicalize(metadata.sources)) {
|
|
36
36
|
throw new Error('authored root declared source graph authority mismatch');
|
|
37
37
|
}
|
|
38
|
+
const loadedExtensions = (loaded.extensions ?? []).map(extension => ({
|
|
39
|
+
name: extension.name, digest: extension.digest, ref: extension.ref,
|
|
40
|
+
}));
|
|
41
|
+
if (canonicalize(loadedExtensions) !== canonicalize(metadata.extensions ?? [])) {
|
|
42
|
+
throw new Error('authored root declared extension authority mismatch');
|
|
43
|
+
}
|
|
38
44
|
return loaded;
|
|
39
45
|
}
|
|
40
46
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import type { BundleFile } from './bundle.js';
|
|
4
|
+
import { safePath, sha256 } from './bundle.js';
|
|
5
|
+
import { findPluginProject } from './plugin-loader.js';
|
|
6
|
+
import { parsePluginLock, readPluginLock, reconcileDeclaredExtensions, type PluginLock } from './plugin-lock.js';
|
|
7
|
+
import { PluginError } from './plugin-manifest.js';
|
|
8
|
+
import { pluginStoreDirectory, readStoredPluginFiles, verifyStoredPlugin } from './plugin-store.js';
|
|
9
|
+
|
|
10
|
+
const EMPTY_LOCK: PluginLock = Object.freeze({ version: 2, plugins: Object.freeze([]) });
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Plugin provenance and bytes to put in a sealed bundle: `lockfile.json` is
|
|
14
|
+
* the same v2 lock the project records, and each extension's materialized
|
|
15
|
+
* files land under `plugins/<name>/…`. No project / no github: plugins →
|
|
16
|
+
* `{ version: 2, plugins: [] }` and no files. Fail closed on lock/store drift.
|
|
17
|
+
*/
|
|
18
|
+
export async function collectBundleExtensions(flowPath: string): Promise<{ lock: PluginLock; files: BundleFile[] }> {
|
|
19
|
+
const root = findPluginProject(dirname(flowPath));
|
|
20
|
+
if (root === undefined) return { lock: EMPTY_LOCK, files: [] };
|
|
21
|
+
const locked = reconcileDeclaredExtensions(root);
|
|
22
|
+
const files: BundleFile[] = [];
|
|
23
|
+
for (const { entry } of locked) {
|
|
24
|
+
const stored = await readStoredPluginFiles(pluginStoreDirectory(root, entry.name, entry.digest), entry.digest);
|
|
25
|
+
for (const file of stored) files.push({ path: `plugins/${entry.name}/${file.path}`, data: file.data });
|
|
26
|
+
}
|
|
27
|
+
return { lock: readPluginLock(root), files };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* After `verifyBundle` has hashed every payload file, check that `lockfile.json`
|
|
32
|
+
* is a v2 plugin lock (or a legacy v1 `{version:1, adapters}` with no plugins)
|
|
33
|
+
* and that each locked extension's store digest matches `plugins/<name>/manifest.json`.
|
|
34
|
+
*/
|
|
35
|
+
export async function verifyBundlePluginLock(bundle: string): Promise<void> {
|
|
36
|
+
let parsed: unknown;
|
|
37
|
+
try { parsed = JSON.parse(await readFile(join(bundle, 'lockfile.json'), 'utf8')); }
|
|
38
|
+
catch { throw new Error('lockfile.json: not valid JSON'); }
|
|
39
|
+
if (isLegacyV1Lock(parsed) || isLegacyNpmLock(parsed)) {
|
|
40
|
+
await assertNoLegacyPluginPayload(bundle);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
let lock: PluginLock;
|
|
44
|
+
try { lock = parsePluginLock(parsed); }
|
|
45
|
+
catch (error) {
|
|
46
|
+
throw new Error(error instanceof PluginError ? error.message : 'lockfile.json: not a plugin lock');
|
|
47
|
+
}
|
|
48
|
+
for (const entry of lock.plugins) {
|
|
49
|
+
if (!safePath(entry.name) || entry.name.includes('/')) {
|
|
50
|
+
throw new Error(`lockfile.json: plugin name ${entry.name} is not a safe path component`);
|
|
51
|
+
}
|
|
52
|
+
const directory = join(bundle, 'plugins', entry.name);
|
|
53
|
+
await verifyStoredPlugin(directory, entry.digest);
|
|
54
|
+
let pluginManifest: Buffer;
|
|
55
|
+
try { pluginManifest = await readFile(join(directory, 'flows-plugin.json')); }
|
|
56
|
+
catch { throw new Error(`lockfile.json: plugin ${entry.name} is missing plugins/${entry.name}/flows-plugin.json`); }
|
|
57
|
+
if (sha256(pluginManifest) !== entry.manifestSha256) {
|
|
58
|
+
throw new Error(`lockfile.json: plugin ${entry.name} flows-plugin.json does not match the lockfile manifest hash`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function assertNoLegacyPluginPayload(bundle: string): Promise<void> {
|
|
64
|
+
let entries: string[];
|
|
65
|
+
try { entries = await readdir(join(bundle, 'plugins')); }
|
|
66
|
+
catch (error) {
|
|
67
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return;
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
if (entries.length > 0) {
|
|
71
|
+
throw new Error('lockfile.json: legacy locks cannot authenticate plugins/ payloads');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isLegacyV1Lock(value: unknown): boolean {
|
|
76
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
77
|
+
&& (value as { version?: unknown }).version === 1
|
|
78
|
+
&& Array.isArray((value as { adapters?: unknown }).adapters);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isLegacyNpmLock(value: unknown): boolean {
|
|
82
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
|
|
83
|
+
const lock = value as { lockfileVersion?: unknown; packages?: unknown };
|
|
84
|
+
return (lock.lockfileVersion === 2 || lock.lockfileVersion === 3) && typeof lock.packages === 'object' && lock.packages !== null;
|
|
85
|
+
}
|
package/src/bundle-typescript.ts
CHANGED
|
@@ -60,7 +60,7 @@ process.stdout.write(JSON.stringify({ spec, authored }));
|
|
|
60
60
|
'--no-compile-autoload-bunfig', '--outfile', executable, input], directory);
|
|
61
61
|
const files: BundleFile[] = [
|
|
62
62
|
{ path: 'flow', data: await readFile(executable) },
|
|
63
|
-
{ path: '
|
|
63
|
+
{ path: 'package-lock.json', data: canonicalize(lock) },
|
|
64
64
|
];
|
|
65
65
|
return { spec, authored: inspected.authored === true, compiler, files };
|
|
66
66
|
} finally { await rm(staging, { recursive: true, force: true }); }
|
package/src/bundle.ts
CHANGED
|
@@ -18,12 +18,24 @@ export function sha256(data: Uint8Array | string): string {
|
|
|
18
18
|
return createHash('sha256').update(data).digest('hex');
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
/** A bundle-relative path: no empty, `.`, or `..` component, no backslash, NUL, or drive colon. */
|
|
22
|
+
export function safePath(path: string): boolean {
|
|
22
23
|
return path.length > 0 && !path.includes('\\') && !path.includes('\0')
|
|
23
24
|
&& path.split('/').every(part => part !== '' && part !== '.' && part !== '..')
|
|
24
25
|
&& !path.includes(':');
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
/**
|
|
29
|
+
* The canonical entry list whose sha256 is a payload's content digest. Shared
|
|
30
|
+
* by sealed flow bundles and materialized plugins so `@sha256:` means one
|
|
31
|
+
* thing everywhere: the digest of `[{bytes,path,sha256}]`, sorted by path.
|
|
32
|
+
*/
|
|
33
|
+
export function payloadManifest(files: readonly { path: string; data: Uint8Array }[]): string {
|
|
34
|
+
return canonicalize([...files]
|
|
35
|
+
.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0)
|
|
36
|
+
.map(file => ({ path: file.path, sha256: sha256(file.data), bytes: file.data.length })));
|
|
37
|
+
}
|
|
38
|
+
|
|
27
39
|
/** Manifest and identity are envelopes, excluded to avoid circular hashing. */
|
|
28
40
|
export async function sealBundle(options: BundleOptions): Promise<string> {
|
|
29
41
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(options.name)) {
|
|
@@ -41,9 +53,7 @@ export async function sealBundle(options: BundleOptions): Promise<string> {
|
|
|
41
53
|
for (const required of ['spec.canonical.json', 'preflight.json', 'lockfile.json']) {
|
|
42
54
|
if (!paths.has(required)) throw new Error(`${required}: missing bundle file`);
|
|
43
55
|
}
|
|
44
|
-
const manifest =
|
|
45
|
-
path: file.path, sha256: sha256(file.data), bytes: file.data.length,
|
|
46
|
-
})));
|
|
56
|
+
const manifest = payloadManifest(files);
|
|
47
57
|
const digest = sha256(manifest);
|
|
48
58
|
const out = resolve(options.out);
|
|
49
59
|
const target = join(out, `${options.name}@sha256:${digest}`);
|