@relayflows/sdk 2.0.24 → 2.0.26
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 +8 -0
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +40 -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/babysitter-catalog-export.d.ts +9 -0
- package/dist/babysitter-catalog-export.d.ts.map +1 -0
- package/dist/babysitter-catalog-export.js +36 -0
- package/dist/babysitter-catalog-export.js.map +1 -0
- 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 +92 -0
- package/dist/flow-extension-loader.d.ts.map +1 -0
- package/dist/flow-extension-loader.js +241 -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 +18 -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 +9 -4
- package/src/authored-flow-executor.ts +60 -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/babysitter-catalog-export.ts +44 -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 +304 -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 +18 -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,84 @@
|
|
|
1
|
+
import { safePath } from './bundle.js';
|
|
2
|
+
import { PluginError } from './plugin-manifest.js';
|
|
3
|
+
const OWNER = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/;
|
|
4
|
+
const REPO = /^[A-Za-z0-9_.-]{1,100}$/;
|
|
5
|
+
const REF = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,254}$/;
|
|
6
|
+
export const SHA = /^[0-9a-f]{40}$/;
|
|
7
|
+
function invalid(message) {
|
|
8
|
+
throw new PluginError('plugin_source_invalid', message);
|
|
9
|
+
}
|
|
10
|
+
export function isGithubPluginRef(value) {
|
|
11
|
+
return value.startsWith('github:') || /^https:\/\/github\.com\//.test(value);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Accepts `github:<owner>/<repo>@<ref>#<path>`,
|
|
15
|
+
* `https://github.com/<owner>/<repo>/tree/<ref>/<path>` (or `/blob/`), and
|
|
16
|
+
* `github:<owner>/<repo>@<ref>` for a root-level plugin.
|
|
17
|
+
*
|
|
18
|
+
* In the URL form the ref is the single segment after `tree/`: GitHub itself
|
|
19
|
+
* disambiguates `tree/feat/x/dir` against the repository's refs, which an
|
|
20
|
+
* offline parser cannot. A ref containing `/` must use the `github:` form,
|
|
21
|
+
* where `@ref#path` is unambiguous.
|
|
22
|
+
*/
|
|
23
|
+
export function parsePluginSource(input) {
|
|
24
|
+
if (input.length > 2048)
|
|
25
|
+
return invalid('Plugin source is too long.');
|
|
26
|
+
let owner, repo, ref, path = '';
|
|
27
|
+
if (input.startsWith('github:')) {
|
|
28
|
+
const m = /^github:([^/@#]+)\/([^/@#]+)@([^#]+)(?:#(.*))?$/.exec(input);
|
|
29
|
+
if (!m)
|
|
30
|
+
return invalid('Expected github:<owner>/<repo>@<ref>[#<path>].');
|
|
31
|
+
[, owner, repo, ref] = m;
|
|
32
|
+
path = m[4] ?? '';
|
|
33
|
+
}
|
|
34
|
+
else if (/^https:\/\/github\.com\//.test(input)) {
|
|
35
|
+
let url;
|
|
36
|
+
try {
|
|
37
|
+
url = new URL(input);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return invalid('Plugin source is not a valid URL.');
|
|
41
|
+
}
|
|
42
|
+
if (url.username || url.password || url.search || url.hash)
|
|
43
|
+
return invalid('Plugin URL must not carry credentials, a query, or a fragment.');
|
|
44
|
+
const m = /^\/([^/]+)\/([^/]+)\/(?:tree|blob)\/([^/]+)(?:\/(.*))?$/.exec(url.pathname);
|
|
45
|
+
if (!m)
|
|
46
|
+
return invalid('Expected https://github.com/<owner>/<repo>/tree/<ref>/<path>.');
|
|
47
|
+
try {
|
|
48
|
+
[, owner, repo, ref] = m.map(part => part === undefined ? part : decodeURIComponent(part));
|
|
49
|
+
path = m[4] === undefined ? '' : decodeURIComponent(m[4]);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return invalid('Plugin URL contains invalid percent-encoding.');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else
|
|
56
|
+
return invalid('Expected a github: reference or a https://github.com/ URL.');
|
|
57
|
+
if (owner === undefined || !OWNER.test(owner))
|
|
58
|
+
return invalid('Invalid GitHub owner.');
|
|
59
|
+
if (repo === undefined || !REPO.test(repo) || repo === '.' || repo === '..')
|
|
60
|
+
return invalid('Invalid GitHub repository name.');
|
|
61
|
+
if (repo.endsWith('.git'))
|
|
62
|
+
repo = repo.slice(0, -4);
|
|
63
|
+
if (ref === undefined || !REF.test(ref) || ref.includes('..') || ref.endsWith('/') || ref.endsWith('.lock'))
|
|
64
|
+
return invalid('Invalid git ref.');
|
|
65
|
+
path = path.replace(/\/+$/, '');
|
|
66
|
+
if (path !== '' && !safePath(path))
|
|
67
|
+
return invalid('Plugin path must be repository-relative without traversal.');
|
|
68
|
+
if (path.split('/').some(part => part === 'flows-plugin.json'))
|
|
69
|
+
return invalid('Plugin path names the directory holding flows-plugin.json, not the file.');
|
|
70
|
+
return Object.freeze({ host: 'github', owner, repo, ref, path });
|
|
71
|
+
}
|
|
72
|
+
/** The one spelling that is ever persisted. */
|
|
73
|
+
export function canonicalPluginRef(source) {
|
|
74
|
+
return `github:${source.owner}/${source.repo}@${source.sha}${source.path === '' ? '' : `#${source.path}`}`;
|
|
75
|
+
}
|
|
76
|
+
/** Parses a persisted reference; refuses anything but the canonical sha form. */
|
|
77
|
+
export function parseCanonicalPluginRef(value) {
|
|
78
|
+
const parsed = parsePluginSource(value);
|
|
79
|
+
if (!value.startsWith('github:') || !SHA.test(parsed.ref)) {
|
|
80
|
+
throw new PluginError('plugin_source_invalid', `Persisted plugin reference must be github:<owner>/<repo>@<sha>[#<path>], got ${value}.`);
|
|
81
|
+
}
|
|
82
|
+
return Object.freeze({ ...parsed, sha: parsed.ref });
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=plugin-source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-source.js","sourceRoot":"","sources":["../src/plugin-source.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a flow-extension plugin's bytes live inside a project:
|
|
3
|
+
* `<project>/.flows/plugins/<name>@sha256:<digest>/`, content-addressed like
|
|
4
|
+
* the bundle cache and never `node_modules`. `manifest.json` in that directory
|
|
5
|
+
* is the payload manifest whose sha256 is the digest, so a directory can be
|
|
6
|
+
* re-verified without the network — and, because the lockfile records the
|
|
7
|
+
* same digest, drift between what was installed and what is on disk is a
|
|
8
|
+
* refusal, not a surprise.
|
|
9
|
+
*/
|
|
10
|
+
export declare const PLUGIN_STORE = ".flows/plugins";
|
|
11
|
+
export declare function pluginStoreDirectory(root: string, name: string, digest: string): string;
|
|
12
|
+
export interface StoredPluginFile {
|
|
13
|
+
readonly path: string;
|
|
14
|
+
readonly data: Uint8Array;
|
|
15
|
+
}
|
|
16
|
+
/** Write the files atomically; an existing directory is verified instead of overwritten. */
|
|
17
|
+
export declare function materializePlugin(root: string, name: string, files: readonly StoredPluginFile[]): Promise<{
|
|
18
|
+
directory: string;
|
|
19
|
+
digest: string;
|
|
20
|
+
}>;
|
|
21
|
+
/** Re-hash a materialized plugin and compare with the digest the lockfile recorded. */
|
|
22
|
+
export declare function verifyStoredPlugin(directory: string, expectedDigest: string): Promise<void>;
|
|
23
|
+
/** Re-verify, then return every stored file including the payload `manifest.json`. */
|
|
24
|
+
export declare function readStoredPluginFiles(directory: string, expectedDigest: string): Promise<readonly {
|
|
25
|
+
path: string;
|
|
26
|
+
data: Buffer;
|
|
27
|
+
}[]>;
|
|
28
|
+
/** Drop a materialized plugin directory. Missing is a no-op. */
|
|
29
|
+
export declare function removeStoredPlugin(directory: string): Promise<void>;
|
|
30
|
+
//# sourceMappingURL=plugin-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-store.d.ts","sourceRoot":"","sources":["../src/plugin-store.ts"],"names":[],"mappings":"AAKA;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,mBAAmB,CAAC;AAE7C,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAEvF;AAED,MAAM,WAAW,gBAAgB;IAAG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;CAAE;AAEtF,4FAA4F;AAC5F,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,gBAAgB,EAAE,GAAG,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAyBtJ;AAWD,uFAAuF;AACvF,wBAAsB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBjG;AAED,sFAAsF;AACtF,wBAAsB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,CAAC,CAOzI;AAED,gEAAgE;AAChE,wBAAsB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEzE"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { lstat, mkdir, mkdtemp, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { payloadManifest, safePath, sha256 } from './bundle.js';
|
|
4
|
+
import { PluginError } from './plugin-manifest.js';
|
|
5
|
+
/**
|
|
6
|
+
* Where a flow-extension plugin's bytes live inside a project:
|
|
7
|
+
* `<project>/.flows/plugins/<name>@sha256:<digest>/`, content-addressed like
|
|
8
|
+
* the bundle cache and never `node_modules`. `manifest.json` in that directory
|
|
9
|
+
* is the payload manifest whose sha256 is the digest, so a directory can be
|
|
10
|
+
* re-verified without the network — and, because the lockfile records the
|
|
11
|
+
* same digest, drift between what was installed and what is on disk is a
|
|
12
|
+
* refusal, not a surprise.
|
|
13
|
+
*/
|
|
14
|
+
export const PLUGIN_STORE = '.flows/plugins';
|
|
15
|
+
export function pluginStoreDirectory(root, name, digest) {
|
|
16
|
+
return join(resolve(root), PLUGIN_STORE, `${name}@sha256:${digest}`);
|
|
17
|
+
}
|
|
18
|
+
/** Write the files atomically; an existing directory is verified instead of overwritten. */
|
|
19
|
+
export async function materializePlugin(root, name, files) {
|
|
20
|
+
const manifest = payloadManifest(files);
|
|
21
|
+
const digest = sha256(manifest);
|
|
22
|
+
const directory = pluginStoreDirectory(root, name, digest);
|
|
23
|
+
let exists = false;
|
|
24
|
+
try {
|
|
25
|
+
await lstat(directory);
|
|
26
|
+
exists = true;
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error.code !== 'ENOENT')
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
if (exists) {
|
|
33
|
+
await verifyStoredPlugin(directory, digest);
|
|
34
|
+
return { directory, digest };
|
|
35
|
+
}
|
|
36
|
+
const parent = dirname(directory);
|
|
37
|
+
await mkdir(parent, { recursive: true });
|
|
38
|
+
const staging = await mkdtemp(join(parent, '.install-'));
|
|
39
|
+
try {
|
|
40
|
+
for (const file of files) {
|
|
41
|
+
if (!safePath(file.path) || file.path === 'manifest.json')
|
|
42
|
+
throw new PluginError('plugin_path_invalid', `${file.path}: invalid plugin path.`);
|
|
43
|
+
await mkdir(dirname(join(staging, file.path)), { recursive: true });
|
|
44
|
+
await writeFile(join(staging, file.path), file.data, { mode: 0o644 });
|
|
45
|
+
}
|
|
46
|
+
await writeFile(join(staging, 'manifest.json'), manifest);
|
|
47
|
+
try {
|
|
48
|
+
await rename(staging, directory);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (!['EEXIST', 'ENOTEMPTY'].includes(error.code ?? ''))
|
|
52
|
+
throw error;
|
|
53
|
+
await verifyStoredPlugin(directory, digest);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
await rm(staging, { recursive: true, force: true });
|
|
58
|
+
}
|
|
59
|
+
return { directory, digest };
|
|
60
|
+
}
|
|
61
|
+
async function regularFile(root, path) {
|
|
62
|
+
const parts = path.split('/');
|
|
63
|
+
for (let i = 1; i <= parts.length; i++) {
|
|
64
|
+
const stat = await lstat(join(root, ...parts.slice(0, i)));
|
|
65
|
+
if (i === parts.length ? !stat.isFile() : !stat.isDirectory())
|
|
66
|
+
throw new PluginError('plugin_source_drift', `${path}: expected a regular file, without symlinks.`);
|
|
67
|
+
}
|
|
68
|
+
return readFile(join(root, path));
|
|
69
|
+
}
|
|
70
|
+
/** Re-hash a materialized plugin and compare with the digest the lockfile recorded. */
|
|
71
|
+
export async function verifyStoredPlugin(directory, expectedDigest) {
|
|
72
|
+
const drift = (message) => { throw new PluginError('plugin_source_drift', `${directory}: ${message}`); };
|
|
73
|
+
let raw;
|
|
74
|
+
try {
|
|
75
|
+
if (!(await lstat(directory)).isDirectory())
|
|
76
|
+
return drift('not a directory');
|
|
77
|
+
raw = (await regularFile(directory, 'manifest.json')).toString('utf8');
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
return drift(error instanceof PluginError ? error.message : 'manifest.json is missing');
|
|
81
|
+
}
|
|
82
|
+
if (sha256(raw) !== expectedDigest)
|
|
83
|
+
return drift('manifest.json digest differs from the lockfile');
|
|
84
|
+
let entries;
|
|
85
|
+
try {
|
|
86
|
+
entries = JSON.parse(raw);
|
|
87
|
+
if (!Array.isArray(entries))
|
|
88
|
+
throw new Error();
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return drift('manifest.json is not a manifest');
|
|
92
|
+
}
|
|
93
|
+
const paths = new Set();
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
if (typeof entry?.path !== 'string' || !safePath(entry.path))
|
|
96
|
+
return drift('manifest.json lists an invalid path');
|
|
97
|
+
let data;
|
|
98
|
+
try {
|
|
99
|
+
data = await regularFile(directory, entry.path);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
return drift(error instanceof PluginError ? error.message : `${entry.path} is missing`);
|
|
103
|
+
}
|
|
104
|
+
if (data.length !== entry.bytes || sha256(data) !== entry.sha256)
|
|
105
|
+
return drift(`${entry.path} changed since installation`);
|
|
106
|
+
paths.add(entry.path);
|
|
107
|
+
}
|
|
108
|
+
await rejectExtras(directory, '', new Set([...paths, 'manifest.json']), drift);
|
|
109
|
+
}
|
|
110
|
+
/** Re-verify, then return every stored file including the payload `manifest.json`. */
|
|
111
|
+
export async function readStoredPluginFiles(directory, expectedDigest) {
|
|
112
|
+
await verifyStoredPlugin(directory, expectedDigest);
|
|
113
|
+
const manifest = await regularFile(directory, 'manifest.json');
|
|
114
|
+
const entries = JSON.parse(manifest.toString('utf8'));
|
|
115
|
+
const files = [{ path: 'manifest.json', data: manifest }];
|
|
116
|
+
for (const entry of entries)
|
|
117
|
+
files.push({ path: entry.path, data: await regularFile(directory, entry.path) });
|
|
118
|
+
return files;
|
|
119
|
+
}
|
|
120
|
+
/** Drop a materialized plugin directory. Missing is a no-op. */
|
|
121
|
+
export async function removeStoredPlugin(directory) {
|
|
122
|
+
await rm(directory, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
async function rejectExtras(root, prefix, paths, drift) {
|
|
125
|
+
for (const entry of await readdir(join(root, prefix), { withFileTypes: true })) {
|
|
126
|
+
const path = prefix + entry.name;
|
|
127
|
+
if (entry.isDirectory() && [...paths].some(file => file.startsWith(`${path}/`)))
|
|
128
|
+
await rejectExtras(root, `${path}/`, paths, drift);
|
|
129
|
+
else if (!entry.isFile() || !paths.has(path))
|
|
130
|
+
drift(`${path}: unlisted file or unsupported file type`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=plugin-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-store.js","sourceRoot":"","sources":["../src/plugin-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -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":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@relayflows/sdk",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.26",
|
|
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",
|
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
"license": "Apache-2.0",
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
50
|
-
"@relayfile/adapter-core": "0.
|
|
51
|
-
"@relayfile/relay-helpers": "0.4.
|
|
52
|
-
"@relayflows/surface": "2.0.
|
|
50
|
+
"@relayfile/adapter-core": "0.6.2",
|
|
51
|
+
"@relayfile/relay-helpers": "0.4.12",
|
|
52
|
+
"@relayflows/surface": "2.0.26",
|
|
53
53
|
"@types/js-yaml": "^4.0.9",
|
|
54
54
|
"ai-hist": "0.4.1",
|
|
55
55
|
"ajv": "^8.17.1",
|
|
@@ -80,5 +80,10 @@
|
|
|
80
80
|
"@agent-relay/sdk": {
|
|
81
81
|
"optional": true
|
|
82
82
|
}
|
|
83
|
+
},
|
|
84
|
+
"overrides": {
|
|
85
|
+
"@relayflows/surface": {
|
|
86
|
+
"@relayfile/relay-helpers": "$@relayfile/relay-helpers"
|
|
87
|
+
}
|
|
83
88
|
}
|
|
84
89
|
}
|
|
@@ -44,6 +44,14 @@ 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 { PluginError } from './plugin-manifest.js';
|
|
48
|
+
import { createHookEvaluator } from './authored-hooks.js';
|
|
49
|
+
import {
|
|
50
|
+
extensionHandlerForHostedDispatch,
|
|
51
|
+
probeFlowExtension,
|
|
52
|
+
type HostedExtensionDispatch,
|
|
53
|
+
type LoadedFlowExtension,
|
|
54
|
+
} from './flow-extension-loader.js';
|
|
47
55
|
import type {
|
|
48
56
|
CompletionReason as ProtocolCompletionReason,
|
|
49
57
|
RunCompletionReason as ProtocolRunCompletionReason,
|
|
@@ -148,6 +156,13 @@ export interface ExecuteAuthoredFlowOptions {
|
|
|
148
156
|
readonly localAgentStream?: string;
|
|
149
157
|
/** Durable kernel root that owns this body's child admission identities. */
|
|
150
158
|
readonly rootRunId?: string;
|
|
159
|
+
/** Installed flow-extension plugins, in lock order, so `f.hook` can AND-compose them. */
|
|
160
|
+
readonly extensions?: readonly LoadedFlowExtension[];
|
|
161
|
+
/**
|
|
162
|
+
* Server-authenticated integration delivery authority. Never derive this
|
|
163
|
+
* from authored input: direct `/workflows/run` callers control that JSON.
|
|
164
|
+
*/
|
|
165
|
+
readonly extensionDispatch?: HostedExtensionDispatch;
|
|
151
166
|
}
|
|
152
167
|
|
|
153
168
|
export async function executeAuthoredFlow<Input = undefined>(
|
|
@@ -171,7 +186,8 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
171
186
|
...(options.onWait !== undefined ? { onWait: options.onWait } : {}),
|
|
172
187
|
};
|
|
173
188
|
const definition = getDefinition<Input>(handle);
|
|
174
|
-
const
|
|
189
|
+
const hostedHandler = extensionHandlerForHostedDispatch(options.extensionDispatch, options.extensions ?? []);
|
|
190
|
+
const headerFields = Object.keys(definition.header).filter(key => key !== 'tools' && key !== 'budget' && key !== 'memory' && key !== 'version' && key !== 'hooks');
|
|
175
191
|
if (definition.header.tools && Object.keys(definition.header.tools).some(key => !['mcp', ...helperProviders.map(p => p.namespace)].includes(key))) headerFields.push('tools');
|
|
176
192
|
if (definition.header.tools?.relayfile !== undefined) headerFields.push('tools.relayfile');
|
|
177
193
|
const helperPreflight = checkSlackHelpers(definition);
|
|
@@ -188,6 +204,21 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
188
204
|
|
|
189
205
|
const checkedMcp = await checkMcpHeader(definition, flowPath);
|
|
190
206
|
if (!checkedMcp.report.ok) throw new McpPreflightError(checkedMcp.report);
|
|
207
|
+
for (const extension of options.extensions ?? []) {
|
|
208
|
+
if (extension.manifest !== undefined) await probeFlowExtension(extension.manifest);
|
|
209
|
+
}
|
|
210
|
+
if (hostedHandler !== undefined) {
|
|
211
|
+
// A schema-2 entry is ordinary authored JavaScript. Passing it the base
|
|
212
|
+
// context would not constrain direct Node access, f.run, helpers, MCP or
|
|
213
|
+
// agent harnesses to manifest.permissions; those declarations are still
|
|
214
|
+
// explicitly unenforced by gate 8 / #442. Refuse before either body runs.
|
|
215
|
+
// A later isolated runtime may replace this gate only when it can prove
|
|
216
|
+
// the manifest boundary, not merely proxy selected Ctx properties.
|
|
217
|
+
throw new PluginError(
|
|
218
|
+
'plugin_unsupported',
|
|
219
|
+
`${hostedHandler.extension.name}: hosted extension handlers require enforced manifest permission isolation (gate 8 / #442).`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
191
222
|
|
|
192
223
|
const budget = new AuthoredBudget(definition.header.budget);
|
|
193
224
|
if (definition.header.memory?.agent === true) {
|
|
@@ -349,6 +380,17 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
349
380
|
));
|
|
350
381
|
}
|
|
351
382
|
|
|
383
|
+
const evaluateHook = createHookEvaluator({
|
|
384
|
+
journal,
|
|
385
|
+
...(options.rootRunId === undefined ? {} : { rootRunId: options.rootRunId }),
|
|
386
|
+
flowName: definition.name,
|
|
387
|
+
declared: definition.header?.hooks ?? [],
|
|
388
|
+
extensions: options.extensions ?? [],
|
|
389
|
+
peekStep: () => nextStep,
|
|
390
|
+
restoreStep: (step) => { nextStep = step; },
|
|
391
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
392
|
+
});
|
|
393
|
+
|
|
352
394
|
const context: Ctx = {
|
|
353
395
|
...createHelpers(<T>(call: HelperCall): Step<T> => {
|
|
354
396
|
const verb = `${call.provider}.${call.verb}`;
|
|
@@ -477,6 +519,23 @@ export async function executeAuthoredFlow<Input = undefined>(
|
|
|
477
519
|
assertOperationAllowed('dispatch', definition.name, requestedCompletion);
|
|
478
520
|
throw unsupportedVerb('dispatch');
|
|
479
521
|
},
|
|
522
|
+
hook(name, input) {
|
|
523
|
+
assertOperationAllowed('hook', definition.name, requestedCompletion);
|
|
524
|
+
const id = `hook-${nextStep++}`;
|
|
525
|
+
const snapshot = snapshotJsonValue(input, 'f.hook input');
|
|
526
|
+
return trackStep(authoredSteps, new AuthoredFlowOperation<boolean>(
|
|
527
|
+
id, 'hook',
|
|
528
|
+
() => assertOperationAllowed('hook', definition.name, requestedCompletion),
|
|
529
|
+
async () => {
|
|
530
|
+
const verdict = await evaluateHook(id, name, snapshot, context);
|
|
531
|
+
const record = { hook: name, step: id, verdict: verdict ? 'pass' : 'fail' };
|
|
532
|
+
const literal = `'${JSON.stringify(record).replaceAll("'", "'\\''")}'`;
|
|
533
|
+
await observeStep(id, 'deterministic', () => lowerDeterministic(id, `printf '%s' ${literal}`, false), options.onProgress);
|
|
534
|
+
return verdict;
|
|
535
|
+
},
|
|
536
|
+
lifecycle,
|
|
537
|
+
));
|
|
538
|
+
},
|
|
480
539
|
done(reason) {
|
|
481
540
|
if (!isSurfaceFlowCompletionReason(reason)) {
|
|
482
541
|
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
|
/**
|