@docsxai/engine 0.2.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.
- package/LICENSE +202 -0
- package/README.md +130 -0
- package/dist/auth/api-login.d.ts +69 -0
- package/dist/auth/api-login.js +95 -0
- package/dist/auth/browser-session.d.ts +28 -0
- package/dist/auth/browser-session.js +43 -0
- package/dist/auth/cookie-jar.d.ts +58 -0
- package/dist/auth/cookie-jar.js +212 -0
- package/dist/auth/email-otp.d.ts +210 -0
- package/dist/auth/email-otp.js +166 -0
- package/dist/auth/http-basic.d.ts +5 -0
- package/dist/auth/http-basic.js +17 -0
- package/dist/auth/index.d.ts +47 -0
- package/dist/auth/index.js +137 -0
- package/dist/auth/jwt-injection.d.ts +153 -0
- package/dist/auth/jwt-injection.js +136 -0
- package/dist/auth/manual-capture.d.ts +35 -0
- package/dist/auth/manual-capture.js +30 -0
- package/dist/auth/mtls.d.ts +15 -0
- package/dist/auth/mtls.js +53 -0
- package/dist/auth/pat-header.d.ts +19 -0
- package/dist/auth/pat-header.js +34 -0
- package/dist/auth/storage-state-cache.d.ts +38 -0
- package/dist/auth/storage-state-cache.js +143 -0
- package/dist/auth/test-backdoor.d.ts +25 -0
- package/dist/auth/test-backdoor.js +51 -0
- package/dist/auth/totp.d.ts +39 -0
- package/dist/auth/totp.js +108 -0
- package/dist/auth/types.d.ts +86 -0
- package/dist/auth/types.js +57 -0
- package/dist/auth/ui-form.d.ts +204 -0
- package/dist/auth/ui-form.js +153 -0
- package/dist/auth/webauthn.d.ts +88 -0
- package/dist/auth/webauthn.js +67 -0
- package/dist/auth.d.ts +1 -0
- package/dist/auth.js +3 -0
- package/dist/backend-client-contracts.d.ts +88 -0
- package/dist/backend-client-contracts.js +19 -0
- package/dist/backend-client-oauth-login.d.ts +7 -0
- package/dist/backend-client-oauth-login.js +90 -0
- package/dist/backend-client-state-cache.d.ts +73 -0
- package/dist/backend-client-state-cache.js +185 -0
- package/dist/backend-client-token.d.ts +18 -0
- package/dist/backend-client-token.js +94 -0
- package/dist/backend-client-transport.d.ts +66 -0
- package/dist/backend-client-transport.js +181 -0
- package/dist/backend-client.d.ts +5 -0
- package/dist/backend-client.js +18 -0
- package/dist/calibrate.d.ts +31 -0
- package/dist/calibrate.js +68 -0
- package/dist/cli-commands-authoring.d.ts +5 -0
- package/dist/cli-commands-authoring.js +403 -0
- package/dist/cli-commands-backend.d.ts +5 -0
- package/dist/cli-commands-backend.js +211 -0
- package/dist/cli-commands-docpack.d.ts +5 -0
- package/dist/cli-commands-docpack.js +280 -0
- package/dist/cli-commands-session.d.ts +4 -0
- package/dist/cli-commands-session.js +398 -0
- package/dist/cli-shared.d.ts +5 -0
- package/dist/cli-shared.js +45 -0
- package/dist/cli-usage.d.ts +1 -0
- package/dist/cli-usage.js +137 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +77 -0
- package/dist/diagnose.d.ts +50 -0
- package/dist/diagnose.js +168 -0
- package/dist/diff-compute.d.ts +13 -0
- package/dist/diff-compute.js +378 -0
- package/dist/diff-report.d.ts +7 -0
- package/dist/diff-report.js +125 -0
- package/dist/diff-types.d.ts +125 -0
- package/dist/diff-types.js +15 -0
- package/dist/diff.d.ts +3 -0
- package/dist/diff.js +16 -0
- package/dist/doc-pack-io.d.ts +30 -0
- package/dist/doc-pack-io.js +182 -0
- package/dist/doc-pack.d.ts +1814 -0
- package/dist/doc-pack.js +328 -0
- package/dist/doctor-checks-plugins.d.ts +2 -0
- package/dist/doctor-checks-plugins.js +136 -0
- package/dist/doctor-checks.d.ts +56 -0
- package/dist/doctor-checks.js +367 -0
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +62 -0
- package/dist/export/adf.d.ts +57 -0
- package/dist/export/adf.js +323 -0
- package/dist/export/playwright-test.d.ts +26 -0
- package/dist/export/playwright-test.js +221 -0
- package/dist/flow-file.d.ts +21 -0
- package/dist/flow-file.js +180 -0
- package/dist/flow-lint.d.ts +24 -0
- package/dist/flow-lint.js +203 -0
- package/dist/flow-runtime.d.ts +113 -0
- package/dist/flow-runtime.js +273 -0
- package/dist/flow-tree.d.ts +19 -0
- package/dist/flow-tree.js +104 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +31 -0
- package/dist/playwright-driver.d.ts +105 -0
- package/dist/playwright-driver.js +363 -0
- package/dist/playwright-instrumented-browser.d.ts +51 -0
- package/dist/playwright-instrumented-browser.js +189 -0
- package/dist/plugins/load.d.ts +22 -0
- package/dist/plugins/load.js +99 -0
- package/dist/plugins/lock.d.ts +40 -0
- package/dist/plugins/lock.js +122 -0
- package/dist/plugins/manifest.d.ts +70 -0
- package/dist/plugins/manifest.js +115 -0
- package/dist/plugins/plan.d.ts +51 -0
- package/dist/plugins/plan.js +279 -0
- package/dist/plugins/registry.d.ts +59 -0
- package/dist/plugins/registry.js +71 -0
- package/dist/plugins/runtime.d.ts +7 -0
- package/dist/plugins/runtime.js +27 -0
- package/dist/plugins/types.d.ts +58 -0
- package/dist/plugins/types.js +4 -0
- package/dist/plugins-cli.d.ts +1 -0
- package/dist/plugins-cli.js +191 -0
- package/dist/redact.d.ts +16 -0
- package/dist/redact.js +72 -0
- package/dist/style.d.ts +46 -0
- package/dist/style.js +151 -0
- package/dist/viewer-bin.d.ts +20 -0
- package/dist/viewer-bin.js +97 -0
- package/dist/workspace.d.ts +60 -0
- package/dist/workspace.js +172 -0
- package/dist/zip.d.ts +17 -0
- package/dist/zip.js +113 -0
- package/package.json +64 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// Plugin planning — the deterministic, code-free half of the runtime pipeline.
|
|
2
|
+
//
|
|
3
|
+
// Resolves every configured source to a validated manifest, then runs the ordering/rejection
|
|
4
|
+
// stages purely over manifest metadata — NEVER importing or executing plugin code:
|
|
5
|
+
// 1. Resolve each source ({package} via Node resolution, {path} realpath'd) to a manifest.
|
|
6
|
+
// 2. Reject api-version-incompatible manifests (load-error, precise message).
|
|
7
|
+
// 3. Verify plugins-lock.json sha256 of the register-module bytes BEFORE the load half imports.
|
|
8
|
+
// 4. Namespace conflicts: every claimant of a contested namespace is disabled (deterministic).
|
|
9
|
+
// 5. dependsOn presence + version-range checks (fixpoint — a disabled dep disables dependents).
|
|
10
|
+
// 6. Tarjan-SCC cycle rejection: a cycle is refused as a unit, never partially loaded.
|
|
11
|
+
//
|
|
12
|
+
// The output is a plan: the disabled records, plus the surviving candidates in topological load
|
|
13
|
+
// order. The capability subset check and the actual import + register() live in the load half,
|
|
14
|
+
// so `docsxai plugins sync` pins hashes through `resolvePluginSources` without executing anything.
|
|
15
|
+
import { promises as fs } from "node:fs";
|
|
16
|
+
import { createRequire } from "node:module";
|
|
17
|
+
import * as path from "node:path";
|
|
18
|
+
import { isApiVersionCompatible, parsePluginManifest, RUNTIME_API_VERSION, satisfiesRange, } from "./manifest.js";
|
|
19
|
+
import { verifyLock } from "./lock.js";
|
|
20
|
+
function sourceLabel(spec) {
|
|
21
|
+
return "package" in spec ? `package:${spec.package}` : `path:${spec.path}`;
|
|
22
|
+
}
|
|
23
|
+
function failureRecord(name, source, reason) {
|
|
24
|
+
return {
|
|
25
|
+
name,
|
|
26
|
+
version: "0.0.0",
|
|
27
|
+
namespace: "",
|
|
28
|
+
source,
|
|
29
|
+
trust: "local",
|
|
30
|
+
status: "load-error",
|
|
31
|
+
statusReason: reason,
|
|
32
|
+
artifacts: [],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** Build the PluginRecord for a resolved candidate at a given status. Shared with the load half. */
|
|
36
|
+
export function candidateRecord(c, status, reason) {
|
|
37
|
+
return {
|
|
38
|
+
name: c.name,
|
|
39
|
+
version: c.version,
|
|
40
|
+
namespace: c.manifest.namespace,
|
|
41
|
+
source: c.source,
|
|
42
|
+
trust: c.manifest.trust,
|
|
43
|
+
status,
|
|
44
|
+
...(reason ? { statusReason: reason } : {}),
|
|
45
|
+
manifest: c.manifest,
|
|
46
|
+
registerPath: c.registerPath,
|
|
47
|
+
artifacts: [],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Stage 1 only: resolve every source to a validated manifest (or a load-error record). Never
|
|
52
|
+
* imports plugin code — `docsxai plugins sync` pins hashes through this without executing
|
|
53
|
+
* anything.
|
|
54
|
+
*/
|
|
55
|
+
export async function resolvePluginSources(workspaceDir, sources) {
|
|
56
|
+
const require = createRequire(import.meta.url);
|
|
57
|
+
const out = [];
|
|
58
|
+
for (const spec of sources) {
|
|
59
|
+
const label = sourceLabel(spec);
|
|
60
|
+
let pkgJsonPath;
|
|
61
|
+
if ("package" in spec) {
|
|
62
|
+
try {
|
|
63
|
+
pkgJsonPath = require.resolve(`${spec.package}/package.json`);
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
out.push({
|
|
67
|
+
ok: false,
|
|
68
|
+
record: failureRecord(spec.package, label, `cannot resolve package "${spec.package}" — is it installed? (${e.message})`),
|
|
69
|
+
});
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
const abs = path.resolve(workspaceDir, spec.path);
|
|
75
|
+
let real;
|
|
76
|
+
try {
|
|
77
|
+
real = await fs.realpath(abs);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
out.push({
|
|
81
|
+
ok: false,
|
|
82
|
+
record: failureRecord(spec.path, label, `plugin path does not exist: ${abs}`),
|
|
83
|
+
});
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
pkgJsonPath = path.join(real, "package.json");
|
|
87
|
+
}
|
|
88
|
+
let pkg;
|
|
89
|
+
try {
|
|
90
|
+
pkg = JSON.parse(await fs.readFile(pkgJsonPath, "utf8"));
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
out.push({
|
|
94
|
+
ok: false,
|
|
95
|
+
record: failureRecord(label, label, `cannot read ${pkgJsonPath}: ${e.message}`),
|
|
96
|
+
});
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const name = typeof pkg.name === "string" && pkg.name ? pkg.name : label;
|
|
100
|
+
const version = typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
|
|
101
|
+
let manifest;
|
|
102
|
+
try {
|
|
103
|
+
manifest = parsePluginManifest(pkg.docsxai, name);
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
out.push({ ok: false, record: failureRecord(name, label, e.message) });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const dir = path.dirname(pkgJsonPath);
|
|
110
|
+
out.push({
|
|
111
|
+
ok: true,
|
|
112
|
+
candidate: {
|
|
113
|
+
name,
|
|
114
|
+
version,
|
|
115
|
+
dir,
|
|
116
|
+
registerPath: path.resolve(dir, manifest.register),
|
|
117
|
+
source: label,
|
|
118
|
+
manifest,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
/** Tarjan strongly-connected components. Returns SCCs in reverse topological order. */
|
|
125
|
+
function tarjanSccs(nodes, edges) {
|
|
126
|
+
const index = new Map();
|
|
127
|
+
const lowlink = new Map();
|
|
128
|
+
const onStack = new Set();
|
|
129
|
+
const stack = [];
|
|
130
|
+
const sccs = [];
|
|
131
|
+
let counter = 0;
|
|
132
|
+
function strongconnect(v) {
|
|
133
|
+
index.set(v, counter);
|
|
134
|
+
lowlink.set(v, counter);
|
|
135
|
+
counter++;
|
|
136
|
+
stack.push(v);
|
|
137
|
+
onStack.add(v);
|
|
138
|
+
for (const w of edges.get(v) ?? []) {
|
|
139
|
+
if (!index.has(w)) {
|
|
140
|
+
strongconnect(w);
|
|
141
|
+
lowlink.set(v, Math.min(lowlink.get(v), lowlink.get(w)));
|
|
142
|
+
}
|
|
143
|
+
else if (onStack.has(w)) {
|
|
144
|
+
lowlink.set(v, Math.min(lowlink.get(v), index.get(w)));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (lowlink.get(v) === index.get(v)) {
|
|
148
|
+
const scc = [];
|
|
149
|
+
for (;;) {
|
|
150
|
+
const w = stack.pop();
|
|
151
|
+
onStack.delete(w);
|
|
152
|
+
scc.push(w);
|
|
153
|
+
if (w === v)
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
sccs.push(scc);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
for (const v of nodes)
|
|
160
|
+
if (!index.has(v))
|
|
161
|
+
strongconnect(v);
|
|
162
|
+
return sccs;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Run the deterministic planning pipeline (stages 1–6) over the configured sources. Never imports
|
|
166
|
+
* plugin code; the surviving `loadOrder` is consumed by the load half, which performs the
|
|
167
|
+
* capability subset check and the actual import + register().
|
|
168
|
+
*/
|
|
169
|
+
export async function planPlugins(opts) {
|
|
170
|
+
const resolutions = await resolvePluginSources(opts.workspaceDir, opts.sources);
|
|
171
|
+
const disabled = [];
|
|
172
|
+
for (const r of resolutions)
|
|
173
|
+
if (!r.ok)
|
|
174
|
+
disabled.push(r.record);
|
|
175
|
+
let candidates = resolutions.filter((r) => r.ok).map((r) => r.candidate);
|
|
176
|
+
// Duplicate package names break dep-graph identity — refuse both, deterministically.
|
|
177
|
+
const byName = new Map();
|
|
178
|
+
for (const c of candidates)
|
|
179
|
+
byName.set(c.name, [...(byName.get(c.name) ?? []), c]);
|
|
180
|
+
for (const [name, group] of byName) {
|
|
181
|
+
if (group.length > 1) {
|
|
182
|
+
for (const c of group) {
|
|
183
|
+
disabled.push(candidateRecord(c, "load-error", `plugin package "${name}" is configured more than once (${group.map((g) => g.source).join(", ")}) — remove the duplicate`));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
candidates = candidates.filter((c) => byName.get(c.name).length === 1);
|
|
188
|
+
// apiVersion compatibility.
|
|
189
|
+
candidates = candidates.filter((c) => {
|
|
190
|
+
if (isApiVersionCompatible(c.manifest.apiVersion))
|
|
191
|
+
return true;
|
|
192
|
+
disabled.push(candidateRecord(c, "load-error", `plugin apiVersion "${c.manifest.apiVersion}" is incompatible with the runtime apiVersion "${RUNTIME_API_VERSION}" ` +
|
|
193
|
+
`(requires same major and minor ≤ runtime) — upgrade the plugin or the engine`));
|
|
194
|
+
return false;
|
|
195
|
+
});
|
|
196
|
+
// Lock verification — BEFORE any import, so unverified bytes never execute.
|
|
197
|
+
if (opts.lock) {
|
|
198
|
+
const lock = opts.lock;
|
|
199
|
+
const verified = [];
|
|
200
|
+
for (const c of candidates) {
|
|
201
|
+
let bytes;
|
|
202
|
+
try {
|
|
203
|
+
bytes = await fs.readFile(c.registerPath);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
bytes = null;
|
|
207
|
+
}
|
|
208
|
+
const mismatch = verifyLock(lock, c.manifest.namespace, bytes);
|
|
209
|
+
if (mismatch)
|
|
210
|
+
disabled.push(candidateRecord(c, "load-error", mismatch));
|
|
211
|
+
else
|
|
212
|
+
verified.push(c);
|
|
213
|
+
}
|
|
214
|
+
candidates = verified;
|
|
215
|
+
}
|
|
216
|
+
// Namespace conflicts: every claimant disabled — load order must not pick winners.
|
|
217
|
+
const byNamespace = new Map();
|
|
218
|
+
for (const c of candidates) {
|
|
219
|
+
byNamespace.set(c.manifest.namespace, [...(byNamespace.get(c.manifest.namespace) ?? []), c]);
|
|
220
|
+
}
|
|
221
|
+
for (const [ns, group] of byNamespace) {
|
|
222
|
+
if (group.length > 1) {
|
|
223
|
+
for (const c of group) {
|
|
224
|
+
disabled.push(candidateRecord(c, "disabled-by-namespace-conflict", `namespace "${ns}" is claimed by ${group.map((g) => `"${g.name}"`).join(" and ")} — namespaces must be unique; rename one`));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
candidates = candidates.filter((c) => byNamespace.get(c.manifest.namespace).length === 1);
|
|
229
|
+
// dependsOn presence + version-range fixpoint: a disabled dep disables its dependents too.
|
|
230
|
+
const live = new Map(candidates.map((c) => [c.name, c]));
|
|
231
|
+
const dropForDeps = () => {
|
|
232
|
+
let changed = false;
|
|
233
|
+
for (const c of [...live.values()]) {
|
|
234
|
+
for (const dep of c.manifest.dependsOn) {
|
|
235
|
+
const target = live.get(dep.plugin);
|
|
236
|
+
let reason;
|
|
237
|
+
if (!target) {
|
|
238
|
+
reason = `dependsOn "${dep.plugin}" is not loadable — configure it in the workspace "plugins" and ensure it resolves`;
|
|
239
|
+
}
|
|
240
|
+
else if (!satisfiesRange(target.version, dep.version)) {
|
|
241
|
+
reason = `dependsOn "${dep.plugin}" version ${target.version} does not satisfy the declared range "${dep.version}"`;
|
|
242
|
+
}
|
|
243
|
+
if (reason) {
|
|
244
|
+
disabled.push(candidateRecord(c, "disabled-by-dep-missing", reason));
|
|
245
|
+
live.delete(c.name);
|
|
246
|
+
changed = true;
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return changed;
|
|
252
|
+
};
|
|
253
|
+
while (dropForDeps()) {
|
|
254
|
+
/* fixpoint */
|
|
255
|
+
}
|
|
256
|
+
// Tarjan-SCC cycle rejection: refuse every member of a cycle as a unit.
|
|
257
|
+
const edges = new Map();
|
|
258
|
+
for (const c of live.values()) {
|
|
259
|
+
edges.set(c.name, c.manifest.dependsOn.map((d) => d.plugin).filter((n) => live.has(n)));
|
|
260
|
+
}
|
|
261
|
+
const sccs = tarjanSccs([...live.keys()], edges);
|
|
262
|
+
for (const scc of sccs) {
|
|
263
|
+
const selfLoop = scc.length === 1 && (edges.get(scc[0]) ?? []).includes(scc[0]);
|
|
264
|
+
if (scc.length > 1 || selfLoop) {
|
|
265
|
+
const cycle = [...scc].sort();
|
|
266
|
+
for (const name of scc) {
|
|
267
|
+
disabled.push(candidateRecord(live.get(name), "disabled-by-cycle", `dependsOn cycle: ${cycle.join(" → ")} → ${cycle[0]} — break the cycle; none of its members load`));
|
|
268
|
+
live.delete(name);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
while (dropForDeps()) {
|
|
273
|
+
/* dependents of cycle members are now dep-missing */
|
|
274
|
+
}
|
|
275
|
+
// Tarjan emits SCCs in reverse topological order; for a DAG that IS the load order
|
|
276
|
+
// (dependencies before dependents).
|
|
277
|
+
const loadOrder = sccs.map((scc) => scc[0]).filter((name) => live.has(name));
|
|
278
|
+
return { disabled, live, loadOrder };
|
|
279
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { PluginKind, PluginManifest, PluginTrust } from "./manifest.js";
|
|
2
|
+
import type { AuthStrategyPlugin, PluginLintRule, PublisherPlugin, RendererPlugin } from "./types.js";
|
|
3
|
+
export type PluginStatus = "loaded" | "disabled-by-capability-mismatch" | "disabled-by-cycle" | "disabled-by-dep-missing" | "disabled-by-namespace-conflict" | "load-error";
|
|
4
|
+
export interface PluginArtifact {
|
|
5
|
+
kind: PluginKind;
|
|
6
|
+
/** Namespace-qualified name, `<ns>:<name>`. */
|
|
7
|
+
name: string;
|
|
8
|
+
}
|
|
9
|
+
export interface PluginRecord {
|
|
10
|
+
/** Package name, or the source spec when the package never resolved. */
|
|
11
|
+
name: string;
|
|
12
|
+
version: string;
|
|
13
|
+
/** Empty string when the manifest never validated. */
|
|
14
|
+
namespace: string;
|
|
15
|
+
/** `package:<npm-name>` or `path:<absolute-dir>`. */
|
|
16
|
+
source: string;
|
|
17
|
+
trust: PluginTrust;
|
|
18
|
+
status: PluginStatus;
|
|
19
|
+
statusReason?: string;
|
|
20
|
+
manifest?: PluginManifest;
|
|
21
|
+
/** Absolute path of the register module, when the manifest resolved. */
|
|
22
|
+
registerPath?: string;
|
|
23
|
+
artifacts: PluginArtifact[];
|
|
24
|
+
}
|
|
25
|
+
/** Artifacts a successfully-registered plugin contributes; committed atomically per plugin. */
|
|
26
|
+
export interface RegisteredArtifacts {
|
|
27
|
+
publishers: ReadonlyMap<string, PublisherPlugin>;
|
|
28
|
+
renderers: ReadonlyMap<string, RendererPlugin>;
|
|
29
|
+
authStrategies: ReadonlyMap<string, AuthStrategyPlugin>;
|
|
30
|
+
lintRules: ReadonlyArray<{
|
|
31
|
+
name: string;
|
|
32
|
+
rules: ReadonlyArray<PluginLintRule>;
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
export declare class PluginRegistryError extends Error {
|
|
36
|
+
constructor(message: string);
|
|
37
|
+
}
|
|
38
|
+
export declare class PluginRegistry {
|
|
39
|
+
readonly workspaceDir: string;
|
|
40
|
+
private readonly records;
|
|
41
|
+
private readonly publishers;
|
|
42
|
+
private readonly renderers;
|
|
43
|
+
private readonly authStrategies;
|
|
44
|
+
private readonly lintRules;
|
|
45
|
+
constructor(workspaceDir: string);
|
|
46
|
+
/** Workspace-contained path resolution — the only filesystem root plugins may touch. */
|
|
47
|
+
workspacePath(...segments: string[]): string;
|
|
48
|
+
/** Runtime-facing: record a plugin's outcome (and, when loaded, its artifacts) atomically. */
|
|
49
|
+
commit(record: PluginRecord, artifacts?: RegisteredArtifacts): void;
|
|
50
|
+
/** Full status table, sorted by namespace (package name as fallback) for deterministic output. */
|
|
51
|
+
listPlugins(): PluginRecord[];
|
|
52
|
+
/** Manifest + registered artifact names for one plugin, looked up by namespace or package name. */
|
|
53
|
+
pluginsInfo(name: string): PluginRecord | undefined;
|
|
54
|
+
getPublisher(qualifiedName: string): PublisherPlugin;
|
|
55
|
+
getRenderer(qualifiedName: string): RendererPlugin;
|
|
56
|
+
getAuthStrategies(): Map<string, AuthStrategyPlugin>;
|
|
57
|
+
getLintRules(): PluginLintRule[];
|
|
58
|
+
private unknownArtifact;
|
|
59
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// The composed plugin surface a CLI invocation consumes. The runtime resolves + loads plugins
|
|
2
|
+
// once and commits each one here; consumers retrieve artifacts by their namespace-qualified
|
|
3
|
+
// name (`<ns>:<name>`). All plugin file IO is chokepointed through `workspacePath`, which
|
|
4
|
+
// routes through `resolveWorkspacePath` against the workspace this registry was built for.
|
|
5
|
+
import { resolveWorkspacePath } from "../workspace.js";
|
|
6
|
+
export class PluginRegistryError extends Error {
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "PluginRegistryError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class PluginRegistry {
|
|
13
|
+
workspaceDir;
|
|
14
|
+
records = [];
|
|
15
|
+
publishers = new Map();
|
|
16
|
+
renderers = new Map();
|
|
17
|
+
authStrategies = new Map();
|
|
18
|
+
lintRules = [];
|
|
19
|
+
constructor(workspaceDir) {
|
|
20
|
+
this.workspaceDir = workspaceDir;
|
|
21
|
+
}
|
|
22
|
+
/** Workspace-contained path resolution — the only filesystem root plugins may touch. */
|
|
23
|
+
workspacePath(...segments) {
|
|
24
|
+
return resolveWorkspacePath(this.workspaceDir, ...segments);
|
|
25
|
+
}
|
|
26
|
+
/** Runtime-facing: record a plugin's outcome (and, when loaded, its artifacts) atomically. */
|
|
27
|
+
commit(record, artifacts) {
|
|
28
|
+
this.records.push(record);
|
|
29
|
+
if (!artifacts)
|
|
30
|
+
return;
|
|
31
|
+
for (const [name, impl] of artifacts.publishers)
|
|
32
|
+
this.publishers.set(name, impl);
|
|
33
|
+
for (const [name, impl] of artifacts.renderers)
|
|
34
|
+
this.renderers.set(name, impl);
|
|
35
|
+
for (const [name, impl] of artifacts.authStrategies)
|
|
36
|
+
this.authStrategies.set(name, impl);
|
|
37
|
+
for (const set of artifacts.lintRules)
|
|
38
|
+
this.lintRules.push(...set.rules);
|
|
39
|
+
}
|
|
40
|
+
/** Full status table, sorted by namespace (package name as fallback) for deterministic output. */
|
|
41
|
+
listPlugins() {
|
|
42
|
+
return [...this.records].sort((a, b) => (a.namespace || a.name).localeCompare(b.namespace || b.name));
|
|
43
|
+
}
|
|
44
|
+
/** Manifest + registered artifact names for one plugin, looked up by namespace or package name. */
|
|
45
|
+
pluginsInfo(name) {
|
|
46
|
+
return (this.records.find((r) => r.namespace === name) ?? this.records.find((r) => r.name === name));
|
|
47
|
+
}
|
|
48
|
+
getPublisher(qualifiedName) {
|
|
49
|
+
const impl = this.publishers.get(qualifiedName);
|
|
50
|
+
if (!impl)
|
|
51
|
+
throw this.unknownArtifact("publisher", qualifiedName, this.publishers.keys());
|
|
52
|
+
return impl;
|
|
53
|
+
}
|
|
54
|
+
getRenderer(qualifiedName) {
|
|
55
|
+
const impl = this.renderers.get(qualifiedName);
|
|
56
|
+
if (!impl)
|
|
57
|
+
throw this.unknownArtifact("renderer", qualifiedName, this.renderers.keys());
|
|
58
|
+
return impl;
|
|
59
|
+
}
|
|
60
|
+
getAuthStrategies() {
|
|
61
|
+
return new Map(this.authStrategies);
|
|
62
|
+
}
|
|
63
|
+
getLintRules() {
|
|
64
|
+
return [...this.lintRules];
|
|
65
|
+
}
|
|
66
|
+
unknownArtifact(kind, qualifiedName, known) {
|
|
67
|
+
const names = [...known].sort();
|
|
68
|
+
return new PluginRegistryError(`no ${kind} named "${qualifiedName}" is registered` +
|
|
69
|
+
(names.length ? ` (registered: ${names.join(", ")})` : " (no plugins registered one)"));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PluginRegistry } from "./registry.js";
|
|
2
|
+
import { type ResolvePluginsOptions } from "./plan.js";
|
|
3
|
+
export type { PluginSourceResolution, ResolvedPluginSource, ResolvePluginsOptions, } from "./plan.js";
|
|
4
|
+
export { resolvePluginSources } from "./plan.js";
|
|
5
|
+
export type { PluginRegisterApi } from "./load.js";
|
|
6
|
+
/** Resolve, verify, and load the configured plugins. Every failure is a status, never a throw. */
|
|
7
|
+
export declare function resolvePlugins(opts: ResolvePluginsOptions): Promise<PluginRegistry>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Plugin runtime — resolves the configured plugin set once per CLI invocation.
|
|
2
|
+
//
|
|
3
|
+
// This module is the orchestration barrel: it composes the two halves of the pipeline and
|
|
4
|
+
// re-exports their public surface. The split keeps the deterministic, code-free planning half
|
|
5
|
+
// (./plan.ts) apart from the impure load half (./load.ts), which is the SOLE place plugin code
|
|
6
|
+
// is imported and register() runs.
|
|
7
|
+
//
|
|
8
|
+
// Pipeline:
|
|
9
|
+
// 1. Resolve each source ({package} via Node resolution, {path} realpath'd) to a manifest. ┐
|
|
10
|
+
// 2. Reject api-version-incompatible manifests (load-error, precise message). │
|
|
11
|
+
// 3. Verify plugins-lock.json sha256 of the register-module bytes BEFORE importing. │ plan.ts
|
|
12
|
+
// 4. Namespace conflicts: every claimant of a contested namespace is disabled. │
|
|
13
|
+
// 5. dependsOn presence + version-range checks (fixpoint — a disabled dep disables deps). │
|
|
14
|
+
// 6. Tarjan-SCC cycle rejection: a cycle is refused as a unit, never partially loaded. ┘
|
|
15
|
+
// 7. Capability subset check against the operator-enabled set (disables, not fatal). ┐ load.ts
|
|
16
|
+
// 8. Import each register module in topological order; call register(api) exactly once. ┘
|
|
17
|
+
//
|
|
18
|
+
// Plugins are in-process Node modules — NOT sandboxed. Trust is a review signal, not a boundary.
|
|
19
|
+
// A register() failure rolls back that plugin's artifacts and lands as a load-error status.
|
|
20
|
+
import { planPlugins } from "./plan.js";
|
|
21
|
+
import { loadPlugins } from "./load.js";
|
|
22
|
+
export { resolvePluginSources } from "./plan.js";
|
|
23
|
+
/** Resolve, verify, and load the configured plugins. Every failure is a status, never a throw. */
|
|
24
|
+
export async function resolvePlugins(opts) {
|
|
25
|
+
const plan = await planPlugins(opts);
|
|
26
|
+
return loadPlugins(opts, plan);
|
|
27
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { LintRule } from "../flow-lint.js";
|
|
2
|
+
/** Plugin-scoped logger. Writes to stderr prefixed `[plugin:<ns>]` — stdout stays clean for CLI output. */
|
|
3
|
+
export interface PluginLogger {
|
|
4
|
+
info(message: string): void;
|
|
5
|
+
warn(message: string): void;
|
|
6
|
+
error(message: string): void;
|
|
7
|
+
}
|
|
8
|
+
export interface PublisherContext {
|
|
9
|
+
workspaceDir: string;
|
|
10
|
+
projection: unknown;
|
|
11
|
+
artifactsDir: string;
|
|
12
|
+
config: Record<string, unknown>;
|
|
13
|
+
secretsEnv: Record<string, string>;
|
|
14
|
+
log: PluginLogger;
|
|
15
|
+
}
|
|
16
|
+
export interface PublishResult {
|
|
17
|
+
ok: boolean;
|
|
18
|
+
target: string;
|
|
19
|
+
/** `section` echoes the projection section a page belongs to, so callers can rebuild their `{ section → pageId }` map from the result. */
|
|
20
|
+
pages: Array<{
|
|
21
|
+
id: string;
|
|
22
|
+
url?: string;
|
|
23
|
+
action: "created" | "updated" | "unchanged";
|
|
24
|
+
section?: string;
|
|
25
|
+
}>;
|
|
26
|
+
warnings: string[];
|
|
27
|
+
}
|
|
28
|
+
export interface PublisherPlugin {
|
|
29
|
+
publish(ctx: PublisherContext): Promise<PublishResult>;
|
|
30
|
+
}
|
|
31
|
+
export interface RendererContext {
|
|
32
|
+
workspaceDir: string;
|
|
33
|
+
outDir: string;
|
|
34
|
+
flows: string[];
|
|
35
|
+
config: Record<string, unknown>;
|
|
36
|
+
log: PluginLogger;
|
|
37
|
+
}
|
|
38
|
+
export interface RendererResult {
|
|
39
|
+
ok: boolean;
|
|
40
|
+
outputs: string[];
|
|
41
|
+
warnings: string[];
|
|
42
|
+
}
|
|
43
|
+
export interface RendererPlugin {
|
|
44
|
+
render(ctx: RendererContext): Promise<RendererResult>;
|
|
45
|
+
}
|
|
46
|
+
export interface AuthStrategyPlugin {
|
|
47
|
+
authenticate(ctx: {
|
|
48
|
+
creds: Record<string, string>;
|
|
49
|
+
options: Record<string, unknown>;
|
|
50
|
+
baseURL: string;
|
|
51
|
+
workspaceDir: string;
|
|
52
|
+
}): Promise<{
|
|
53
|
+
storageState: unknown;
|
|
54
|
+
expiresAt?: string;
|
|
55
|
+
contextOptions?: Record<string, unknown>;
|
|
56
|
+
}>;
|
|
57
|
+
}
|
|
58
|
+
export type PluginLintRule = LintRule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function pluginsCli(args: string[]): Promise<number>;
|