@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.
Files changed (129) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +130 -0
  3. package/dist/auth/api-login.d.ts +69 -0
  4. package/dist/auth/api-login.js +95 -0
  5. package/dist/auth/browser-session.d.ts +28 -0
  6. package/dist/auth/browser-session.js +43 -0
  7. package/dist/auth/cookie-jar.d.ts +58 -0
  8. package/dist/auth/cookie-jar.js +212 -0
  9. package/dist/auth/email-otp.d.ts +210 -0
  10. package/dist/auth/email-otp.js +166 -0
  11. package/dist/auth/http-basic.d.ts +5 -0
  12. package/dist/auth/http-basic.js +17 -0
  13. package/dist/auth/index.d.ts +47 -0
  14. package/dist/auth/index.js +137 -0
  15. package/dist/auth/jwt-injection.d.ts +153 -0
  16. package/dist/auth/jwt-injection.js +136 -0
  17. package/dist/auth/manual-capture.d.ts +35 -0
  18. package/dist/auth/manual-capture.js +30 -0
  19. package/dist/auth/mtls.d.ts +15 -0
  20. package/dist/auth/mtls.js +53 -0
  21. package/dist/auth/pat-header.d.ts +19 -0
  22. package/dist/auth/pat-header.js +34 -0
  23. package/dist/auth/storage-state-cache.d.ts +38 -0
  24. package/dist/auth/storage-state-cache.js +143 -0
  25. package/dist/auth/test-backdoor.d.ts +25 -0
  26. package/dist/auth/test-backdoor.js +51 -0
  27. package/dist/auth/totp.d.ts +39 -0
  28. package/dist/auth/totp.js +108 -0
  29. package/dist/auth/types.d.ts +86 -0
  30. package/dist/auth/types.js +57 -0
  31. package/dist/auth/ui-form.d.ts +204 -0
  32. package/dist/auth/ui-form.js +153 -0
  33. package/dist/auth/webauthn.d.ts +88 -0
  34. package/dist/auth/webauthn.js +67 -0
  35. package/dist/auth.d.ts +1 -0
  36. package/dist/auth.js +3 -0
  37. package/dist/backend-client-contracts.d.ts +88 -0
  38. package/dist/backend-client-contracts.js +19 -0
  39. package/dist/backend-client-oauth-login.d.ts +7 -0
  40. package/dist/backend-client-oauth-login.js +90 -0
  41. package/dist/backend-client-state-cache.d.ts +73 -0
  42. package/dist/backend-client-state-cache.js +185 -0
  43. package/dist/backend-client-token.d.ts +18 -0
  44. package/dist/backend-client-token.js +94 -0
  45. package/dist/backend-client-transport.d.ts +66 -0
  46. package/dist/backend-client-transport.js +181 -0
  47. package/dist/backend-client.d.ts +5 -0
  48. package/dist/backend-client.js +18 -0
  49. package/dist/calibrate.d.ts +31 -0
  50. package/dist/calibrate.js +68 -0
  51. package/dist/cli-commands-authoring.d.ts +5 -0
  52. package/dist/cli-commands-authoring.js +403 -0
  53. package/dist/cli-commands-backend.d.ts +5 -0
  54. package/dist/cli-commands-backend.js +211 -0
  55. package/dist/cli-commands-docpack.d.ts +5 -0
  56. package/dist/cli-commands-docpack.js +280 -0
  57. package/dist/cli-commands-session.d.ts +4 -0
  58. package/dist/cli-commands-session.js +398 -0
  59. package/dist/cli-shared.d.ts +5 -0
  60. package/dist/cli-shared.js +45 -0
  61. package/dist/cli-usage.d.ts +1 -0
  62. package/dist/cli-usage.js +137 -0
  63. package/dist/cli.d.ts +2 -0
  64. package/dist/cli.js +77 -0
  65. package/dist/diagnose.d.ts +50 -0
  66. package/dist/diagnose.js +168 -0
  67. package/dist/diff-compute.d.ts +13 -0
  68. package/dist/diff-compute.js +378 -0
  69. package/dist/diff-report.d.ts +7 -0
  70. package/dist/diff-report.js +125 -0
  71. package/dist/diff-types.d.ts +125 -0
  72. package/dist/diff-types.js +15 -0
  73. package/dist/diff.d.ts +3 -0
  74. package/dist/diff.js +16 -0
  75. package/dist/doc-pack-io.d.ts +30 -0
  76. package/dist/doc-pack-io.js +182 -0
  77. package/dist/doc-pack.d.ts +1814 -0
  78. package/dist/doc-pack.js +328 -0
  79. package/dist/doctor-checks-plugins.d.ts +2 -0
  80. package/dist/doctor-checks-plugins.js +136 -0
  81. package/dist/doctor-checks.d.ts +56 -0
  82. package/dist/doctor-checks.js +367 -0
  83. package/dist/doctor.d.ts +7 -0
  84. package/dist/doctor.js +62 -0
  85. package/dist/export/adf.d.ts +57 -0
  86. package/dist/export/adf.js +323 -0
  87. package/dist/export/playwright-test.d.ts +26 -0
  88. package/dist/export/playwright-test.js +221 -0
  89. package/dist/flow-file.d.ts +21 -0
  90. package/dist/flow-file.js +180 -0
  91. package/dist/flow-lint.d.ts +24 -0
  92. package/dist/flow-lint.js +203 -0
  93. package/dist/flow-runtime.d.ts +113 -0
  94. package/dist/flow-runtime.js +273 -0
  95. package/dist/flow-tree.d.ts +19 -0
  96. package/dist/flow-tree.js +104 -0
  97. package/dist/index.d.ts +27 -0
  98. package/dist/index.js +31 -0
  99. package/dist/playwright-driver.d.ts +105 -0
  100. package/dist/playwright-driver.js +363 -0
  101. package/dist/playwright-instrumented-browser.d.ts +51 -0
  102. package/dist/playwright-instrumented-browser.js +189 -0
  103. package/dist/plugins/load.d.ts +22 -0
  104. package/dist/plugins/load.js +99 -0
  105. package/dist/plugins/lock.d.ts +40 -0
  106. package/dist/plugins/lock.js +122 -0
  107. package/dist/plugins/manifest.d.ts +70 -0
  108. package/dist/plugins/manifest.js +115 -0
  109. package/dist/plugins/plan.d.ts +51 -0
  110. package/dist/plugins/plan.js +279 -0
  111. package/dist/plugins/registry.d.ts +59 -0
  112. package/dist/plugins/registry.js +71 -0
  113. package/dist/plugins/runtime.d.ts +7 -0
  114. package/dist/plugins/runtime.js +27 -0
  115. package/dist/plugins/types.d.ts +58 -0
  116. package/dist/plugins/types.js +4 -0
  117. package/dist/plugins-cli.d.ts +1 -0
  118. package/dist/plugins-cli.js +191 -0
  119. package/dist/redact.d.ts +16 -0
  120. package/dist/redact.js +72 -0
  121. package/dist/style.d.ts +46 -0
  122. package/dist/style.js +151 -0
  123. package/dist/viewer-bin.d.ts +20 -0
  124. package/dist/viewer-bin.js +97 -0
  125. package/dist/workspace.d.ts +60 -0
  126. package/dist/workspace.js +172 -0
  127. package/dist/zip.d.ts +17 -0
  128. package/dist/zip.js +113 -0
  129. package/package.json +64 -0
@@ -0,0 +1,99 @@
1
+ // Plugin loading — the impure half of the runtime pipeline, and the SOLE place plugin code runs.
2
+ //
3
+ // Given a plan (survivors in topological order), this:
4
+ // 7. Capability subset check against the operator-enabled set (disables, not fatal).
5
+ // 8. Import each register module in topological order; call register(api) exactly once.
6
+ //
7
+ // Plugins are in-process Node modules — NOT sandboxed. Trust is a review signal, not a boundary.
8
+ // A register() failure rolls back that plugin's artifacts and lands as a load-error status.
9
+ // This is the only module that dynamically imports plugin modules and invokes register().
10
+ import { pathToFileURL } from "node:url";
11
+ import { resolveWorkspacePath } from "../workspace.js";
12
+ import { PluginRegistry } from "./registry.js";
13
+ import { candidateRecord } from "./plan.js";
14
+ /**
15
+ * Commit the plan's disabled records, then import + register the surviving plugins in load order.
16
+ * Every failure is a status, never a throw. This is the only place plugin code is imported.
17
+ */
18
+ export async function loadPlugins(opts, plan) {
19
+ const registry = new PluginRegistry(opts.workspaceDir);
20
+ const enabled = new Set(opts.enabledCapabilities ?? []);
21
+ const { disabled, live, loadOrder } = plan;
22
+ for (const record of disabled)
23
+ registry.commit(record);
24
+ for (const name of loadOrder) {
25
+ const c = live.get(name);
26
+ const missing = c.manifest.capabilities.filter((cap) => !enabled.has(cap));
27
+ if (missing.length > 0) {
28
+ registry.commit(candidateRecord(c, "disabled-by-capability-mismatch", `plugin declares capabilities [${missing.join(", ")}] not enabled for this workspace — ` +
29
+ `add them to "plugin_capabilities" in .docsxai.json to opt in`));
30
+ continue;
31
+ }
32
+ const ns = c.manifest.namespace;
33
+ const publishers = new Map();
34
+ const renderers = new Map();
35
+ const authStrategies = new Map();
36
+ const lintRules = [];
37
+ const artifacts = [];
38
+ const ARTIFACT_NAME = /^[a-z][a-z0-9-]*$/;
39
+ const qualify = (kind, bare, taken) => {
40
+ if (!c.manifest.kinds.includes(kind)) {
41
+ throw new Error(`plugin "${c.name}": registered a "${kind}" but the manifest's kinds are [${c.manifest.kinds.join(", ")}] — declare every extension point the plugin registers`);
42
+ }
43
+ if (!ARTIFACT_NAME.test(bare)) {
44
+ throw new Error(`plugin "${c.name}": artifact name "${bare}" must be bare kebab-case (the runtime prefixes "${ns}:")`);
45
+ }
46
+ const qualified = `${ns}:${bare}`;
47
+ if (taken.has(qualified)) {
48
+ throw new Error(`plugin "${c.name}": ${kind} "${qualified}" is already registered`);
49
+ }
50
+ artifacts.push({ kind, name: qualified });
51
+ return qualified;
52
+ };
53
+ const log = {
54
+ info: (message) => process.stderr.write(`[plugin:${ns}] ${message}\n`),
55
+ warn: (message) => process.stderr.write(`[plugin:${ns}] warn: ${message}\n`),
56
+ error: (message) => process.stderr.write(`[plugin:${ns}] error: ${message}\n`),
57
+ };
58
+ const lintRuleNames = new Set();
59
+ const api = {
60
+ namespace: ns,
61
+ declaredKinds: c.manifest.kinds,
62
+ declaredCapabilities: c.manifest.capabilities,
63
+ registerPublisher: (bare, impl) => {
64
+ publishers.set(qualify("publisher", bare, publishers), impl);
65
+ },
66
+ registerRenderer: (bare, impl) => {
67
+ renderers.set(qualify("renderer", bare, renderers), impl);
68
+ },
69
+ registerLintRules: (bare, rules) => {
70
+ const qualified = qualify("lint-rules", bare, lintRuleNames);
71
+ lintRuleNames.add(qualified);
72
+ lintRules.push({ name: qualified, rules: [...rules] });
73
+ },
74
+ registerAuthStrategy: (bare, impl) => {
75
+ authStrategies.set(qualify("auth-strategy", bare, authStrategies), impl);
76
+ },
77
+ log,
78
+ workspacePath: (...segments) => resolveWorkspacePath(opts.workspaceDir, ...segments),
79
+ };
80
+ try {
81
+ const mod = (await import(pathToFileURL(c.registerPath).href));
82
+ const fn = mod.register ?? mod.default;
83
+ if (typeof fn !== "function") {
84
+ throw new Error(`register module ${c.registerPath} must export a register(api) function (named or default)`);
85
+ }
86
+ await fn(api);
87
+ }
88
+ catch (e) {
89
+ // Roll back: nothing this plugin registered survives a failed register().
90
+ registry.commit(candidateRecord(c, "load-error", `register() failed for plugin "${c.name}": ${e.message}`));
91
+ continue;
92
+ }
93
+ const record = candidateRecord(c, "loaded");
94
+ record.artifacts = artifacts;
95
+ const committed = { publishers, renderers, authStrategies, lintRules };
96
+ registry.commit(record, committed);
97
+ }
98
+ return registry;
99
+ }
@@ -0,0 +1,40 @@
1
+ export declare const PLUGINS_LOCK_FILE = "plugins-lock.json";
2
+ export declare const PLUGINS_LOCK_SCHEMA = "docsxai/plugins-lock@1";
3
+ /** Where a plugin comes from: an installed package (resolved via Node) or a local directory. */
4
+ export type PluginSourceSpec = {
5
+ package: string;
6
+ } | {
7
+ path: string;
8
+ };
9
+ export interface PluginsLockEntry {
10
+ source: string;
11
+ version: string;
12
+ /** Hex sha256 of the register module's file bytes. */
13
+ sha256: string;
14
+ }
15
+ export interface PluginsLockFile {
16
+ schema: typeof PLUGINS_LOCK_SCHEMA;
17
+ plugins: Record<string, PluginsLockEntry>;
18
+ }
19
+ export declare class PluginsLockError extends Error {
20
+ constructor(message: string);
21
+ }
22
+ export declare function sha256Hex(bytes: Uint8Array): string;
23
+ /** Read `<workspace>/plugins-lock.json`. `null` when absent; throws on a malformed file. */
24
+ export declare function readPluginsLock(workspaceDir: string): Promise<PluginsLockFile | null>;
25
+ /** Write `<workspace>/plugins-lock.json` (deterministic key order). Returns the path written. */
26
+ export declare function writePluginsLock(workspaceDir: string, lock: PluginsLockFile): Promise<string>;
27
+ /**
28
+ * Verify a resolved plugin against the lock. Returns `null` when the entry matches, or a
29
+ * human-actionable mismatch reason. Callers turn a non-null reason into a `load-error`.
30
+ */
31
+ export declare function verifyLock(lock: PluginsLockFile, namespace: string, registerBytes: Uint8Array | null): string | null;
32
+ export interface WorkspacePluginsConfig {
33
+ sources: PluginSourceSpec[];
34
+ capabilities: string[];
35
+ }
36
+ export declare class PluginsConfigError extends Error {
37
+ constructor(message: string);
38
+ }
39
+ /** Read the plugin keys from `<workspace>/.docsxai.json`. Absent file → empty config. */
40
+ export declare function readWorkspacePluginsConfig(workspaceDir: string): Promise<WorkspacePluginsConfig>;
@@ -0,0 +1,122 @@
1
+ // plugins-lock.json + the plugin keys of `.docsxai.json`.
2
+ //
3
+ // The lock pins the sha256 of each plugin's register-module bytes. When the file exists, every
4
+ // resolve verifies the hash BEFORE importing — a silently-swapped module fails closed with a
5
+ // "run `docsxai plugins sync`" message. `docsxai plugins sync` (re)writes it without ever
6
+ // executing plugin code.
7
+ import { createHash } from "node:crypto";
8
+ import { promises as fs } from "node:fs";
9
+ import { z } from "zod";
10
+ import { resolveWorkspacePath, WORKSPACE_CONFIG_FILE } from "../workspace.js";
11
+ export const PLUGINS_LOCK_FILE = "plugins-lock.json";
12
+ export const PLUGINS_LOCK_SCHEMA = "docsxai/plugins-lock@1";
13
+ export class PluginsLockError extends Error {
14
+ constructor(message) {
15
+ super(message);
16
+ this.name = "PluginsLockError";
17
+ }
18
+ }
19
+ const lockSchema = z
20
+ .object({
21
+ schema: z.literal(PLUGINS_LOCK_SCHEMA),
22
+ plugins: z.record(z.object({ source: z.string(), version: z.string(), sha256: z.string() }).strict()),
23
+ })
24
+ .strict();
25
+ export function sha256Hex(bytes) {
26
+ return createHash("sha256").update(bytes).digest("hex");
27
+ }
28
+ function lockPath(workspaceDir) {
29
+ return resolveWorkspacePath(workspaceDir, PLUGINS_LOCK_FILE);
30
+ }
31
+ /** Read `<workspace>/plugins-lock.json`. `null` when absent; throws on a malformed file. */
32
+ export async function readPluginsLock(workspaceDir) {
33
+ const p = lockPath(workspaceDir);
34
+ let text;
35
+ try {
36
+ text = await fs.readFile(p, "utf8");
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ let raw;
42
+ try {
43
+ raw = JSON.parse(text);
44
+ }
45
+ catch {
46
+ throw new PluginsLockError(`${p} is not valid JSON — fix or delete it, then run \`docsxai plugins sync\``);
47
+ }
48
+ const result = lockSchema.safeParse(raw);
49
+ if (!result.success) {
50
+ throw new PluginsLockError(`${p} does not match schema "${PLUGINS_LOCK_SCHEMA}" — delete it and run \`docsxai plugins sync\``);
51
+ }
52
+ return result.data;
53
+ }
54
+ /** Write `<workspace>/plugins-lock.json` (deterministic key order). Returns the path written. */
55
+ export async function writePluginsLock(workspaceDir, lock) {
56
+ const p = lockPath(workspaceDir);
57
+ const ordered = {
58
+ schema: lock.schema,
59
+ plugins: Object.fromEntries(Object.entries(lock.plugins).sort(([a], [b]) => a.localeCompare(b))),
60
+ };
61
+ await fs.writeFile(p, JSON.stringify(ordered, null, 2) + "\n", "utf8");
62
+ return p;
63
+ }
64
+ /**
65
+ * Verify a resolved plugin against the lock. Returns `null` when the entry matches, or a
66
+ * human-actionable mismatch reason. Callers turn a non-null reason into a `load-error`.
67
+ */
68
+ export function verifyLock(lock, namespace, registerBytes) {
69
+ const entry = lock.plugins[namespace];
70
+ if (!entry) {
71
+ return `plugin "${namespace}" is not in ${PLUGINS_LOCK_FILE} — run \`docsxai plugins sync\``;
72
+ }
73
+ if (registerBytes === null) {
74
+ return `plugin "${namespace}" register module is unreadable — reinstall it, then run \`docsxai plugins sync\``;
75
+ }
76
+ const actual = sha256Hex(registerBytes);
77
+ if (actual !== entry.sha256) {
78
+ return (`lock mismatch for plugin "${namespace}": register module sha256 ${actual} does not match ` +
79
+ `${PLUGINS_LOCK_FILE} (${entry.sha256}) — if the change is intentional, run \`docsxai plugins sync\``);
80
+ }
81
+ return null;
82
+ }
83
+ export class PluginsConfigError extends Error {
84
+ constructor(message) {
85
+ super(message);
86
+ this.name = "PluginsConfigError";
87
+ }
88
+ }
89
+ const sourceSchema = z.union([
90
+ z.object({ package: z.string().min(1) }).strict(),
91
+ z.object({ path: z.string().min(1) }).strict(),
92
+ ]);
93
+ const pluginsConfigSchema = z.object({
94
+ plugins: z.array(sourceSchema).default([]),
95
+ plugin_capabilities: z.array(z.string()).default([]),
96
+ });
97
+ /** Read the plugin keys from `<workspace>/.docsxai.json`. Absent file → empty config. */
98
+ export async function readWorkspacePluginsConfig(workspaceDir) {
99
+ const p = resolveWorkspacePath(workspaceDir, WORKSPACE_CONFIG_FILE);
100
+ let text;
101
+ try {
102
+ text = await fs.readFile(p, "utf8");
103
+ }
104
+ catch {
105
+ return { sources: [], capabilities: [] };
106
+ }
107
+ let raw;
108
+ try {
109
+ raw = JSON.parse(text);
110
+ }
111
+ catch {
112
+ throw new PluginsConfigError(`${p} is not valid JSON`);
113
+ }
114
+ const result = pluginsConfigSchema.safeParse(raw);
115
+ if (!result.success) {
116
+ const detail = result.error.issues
117
+ .map((i) => `${i.path.length ? i.path.join(".") + ": " : ""}${i.message}`)
118
+ .join("; ");
119
+ throw new PluginsConfigError(`${p}: invalid plugin configuration — ${detail}`);
120
+ }
121
+ return { sources: result.data.plugins, capabilities: result.data.plugin_capabilities };
122
+ }
@@ -0,0 +1,70 @@
1
+ import { z } from "zod";
2
+ /** The plugin-runtime contract version this engine build advertises. */
3
+ export declare const RUNTIME_API_VERSION = "1.0.0";
4
+ /**
5
+ * Namespaces plugins can never claim — the engine's own surfaces live here.
6
+ * `site-docs` (the pre-rename CLI/product name) stays reserved defensively so a
7
+ * plugin can never squat the old identity.
8
+ */
9
+ export declare const RESERVED_NAMESPACES: ReadonlyArray<string>;
10
+ export declare const PLUGIN_KINDS: readonly ["publisher", "renderer", "lint-rules", "auth-strategy"];
11
+ export type PluginKind = (typeof PLUGIN_KINDS)[number];
12
+ export type PluginTrust = "kalebtec" | "community" | "local";
13
+ declare const manifestSchema: z.ZodObject<{
14
+ apiVersion: z.ZodString;
15
+ namespace: z.ZodEffects<z.ZodString, string, string>;
16
+ register: z.ZodString;
17
+ kinds: z.ZodArray<z.ZodEnum<["publisher", "renderer", "lint-rules", "auth-strategy"]>, "many">;
18
+ capabilities: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
19
+ dependsOn: z.ZodDefault<z.ZodArray<z.ZodObject<{
20
+ plugin: z.ZodString;
21
+ version: z.ZodString;
22
+ }, "strict", z.ZodTypeAny, {
23
+ version: string;
24
+ plugin: string;
25
+ }, {
26
+ version: string;
27
+ plugin: string;
28
+ }>, "many">>;
29
+ trust: z.ZodDefault<z.ZodEnum<["kalebtec", "community", "local"]>>;
30
+ }, "strict", z.ZodTypeAny, {
31
+ capabilities: string[];
32
+ apiVersion: string;
33
+ namespace: string;
34
+ register: string;
35
+ kinds: ("publisher" | "renderer" | "lint-rules" | "auth-strategy")[];
36
+ dependsOn: {
37
+ version: string;
38
+ plugin: string;
39
+ }[];
40
+ trust: "local" | "kalebtec" | "community";
41
+ }, {
42
+ apiVersion: string;
43
+ namespace: string;
44
+ register: string;
45
+ kinds: ("publisher" | "renderer" | "lint-rules" | "auth-strategy")[];
46
+ capabilities?: string[] | undefined;
47
+ dependsOn?: {
48
+ version: string;
49
+ plugin: string;
50
+ }[] | undefined;
51
+ trust?: "local" | "kalebtec" | "community" | undefined;
52
+ }>;
53
+ export type PluginManifest = z.infer<typeof manifestSchema>;
54
+ export declare class PluginManifestError extends Error {
55
+ constructor(message: string);
56
+ }
57
+ /** Validate the `docsxai` field of a plugin's package.json. Throws {@link PluginManifestError}. */
58
+ export declare function parsePluginManifest(raw: unknown, source: string): PluginManifest;
59
+ /**
60
+ * A plugin's `apiVersion` is compatible when it shares the runtime's major and its minor is ≤
61
+ * the runtime's. A plugin built for `1.0.0` runs under runtime `1.5.0`; a plugin built for
62
+ * `1.6.0` or `2.0.0` does not run under runtime `1.5.0`.
63
+ */
64
+ export declare function isApiVersionCompatible(pluginApiVersion: string, runtimeApiVersion?: string): boolean;
65
+ /**
66
+ * Minimal semver-range check for `dependsOn`: supports `^x.y.z`, `~x.y.z`, and exact `x.y.z`
67
+ * (npm semantics, including the 0.x caret caveats). Anything else returns false.
68
+ */
69
+ export declare function satisfiesRange(version: string, range: string): boolean;
70
+ export {};
@@ -0,0 +1,115 @@
1
+ // Plugin manifest: the `docsxai` field on a plugin package's package.json. Zod-validated; the
2
+ // rejections here are load-errors, not warnings — a plugin with a lying or malformed manifest
3
+ // never reaches `register()`.
4
+ import { z } from "zod";
5
+ /** The plugin-runtime contract version this engine build advertises. */
6
+ export const RUNTIME_API_VERSION = "1.0.0";
7
+ /**
8
+ * Namespaces plugins can never claim — the engine's own surfaces live here.
9
+ * `site-docs` (the pre-rename CLI/product name) stays reserved defensively so a
10
+ * plugin can never squat the old identity.
11
+ */
12
+ export const RESERVED_NAMESPACES = [
13
+ "docsxai",
14
+ "site-docs",
15
+ "core",
16
+ "plugins",
17
+ ];
18
+ export const PLUGIN_KINDS = ["publisher", "renderer", "lint-rules", "auth-strategy"];
19
+ const SEMVER = /^(\d+)\.(\d+)\.(\d+)$/;
20
+ const NAMESPACE = /^[a-z][a-z0-9-]*$/;
21
+ // The only capability family today is target-host egress. Unknown prefixes are rejected so a
22
+ // manifest can't smuggle an undisclosed capability past review.
23
+ const CAPABILITY = /^egress:[a-zA-Z0-9*]([a-zA-Z0-9*.-]*[a-zA-Z0-9*])?$/;
24
+ const manifestSchema = z
25
+ .object({
26
+ apiVersion: z.string().regex(SEMVER, "apiVersion must be exact semver (x.y.z)"),
27
+ namespace: z
28
+ .string()
29
+ .regex(NAMESPACE, "namespace must match /^[a-z][a-z0-9-]*$/ (kebab-case)")
30
+ .refine((ns) => !RESERVED_NAMESPACES.includes(ns), {
31
+ message: `namespace is reserved (${RESERVED_NAMESPACES.join(", ")})`,
32
+ }),
33
+ register: z.string().min(1, "register must be a relative path to the register module"),
34
+ kinds: z.array(z.enum(PLUGIN_KINDS)).min(1, "kinds must declare at least one extension point"),
35
+ capabilities: z
36
+ .array(z
37
+ .string()
38
+ .regex(CAPABILITY, 'capability must match "egress:<host-glob>" (the only family today)'))
39
+ .default([]),
40
+ dependsOn: z
41
+ .array(z.object({ plugin: z.string().min(1), version: z.string().min(1) }).strict())
42
+ .default([]),
43
+ trust: z.enum(["kalebtec", "community", "local"]).default("local"),
44
+ })
45
+ .strict();
46
+ export class PluginManifestError extends Error {
47
+ constructor(message) {
48
+ super(message);
49
+ this.name = "PluginManifestError";
50
+ }
51
+ }
52
+ /** Validate the `docsxai` field of a plugin's package.json. Throws {@link PluginManifestError}. */
53
+ export function parsePluginManifest(raw, source) {
54
+ if (raw === undefined || raw === null) {
55
+ throw new PluginManifestError(`${source}: package.json has no "docsxai" field — not a plugin`);
56
+ }
57
+ const result = manifestSchema.safeParse(raw);
58
+ if (!result.success) {
59
+ const detail = result.error.issues
60
+ .map((i) => `${i.path.length ? i.path.join(".") + ": " : ""}${i.message}`)
61
+ .join("; ");
62
+ throw new PluginManifestError(`${source}: invalid "docsxai" manifest — ${detail}`);
63
+ }
64
+ return result.data;
65
+ }
66
+ function parseSemver(v) {
67
+ const m = SEMVER.exec(v);
68
+ if (!m)
69
+ return null;
70
+ return [Number(m[1]), Number(m[2]), Number(m[3])];
71
+ }
72
+ /**
73
+ * A plugin's `apiVersion` is compatible when it shares the runtime's major and its minor is ≤
74
+ * the runtime's. A plugin built for `1.0.0` runs under runtime `1.5.0`; a plugin built for
75
+ * `1.6.0` or `2.0.0` does not run under runtime `1.5.0`.
76
+ */
77
+ export function isApiVersionCompatible(pluginApiVersion, runtimeApiVersion = RUNTIME_API_VERSION) {
78
+ const plugin = parseSemver(pluginApiVersion);
79
+ const runtime = parseSemver(runtimeApiVersion);
80
+ if (!plugin || !runtime)
81
+ return false;
82
+ return plugin[0] === runtime[0] && plugin[1] <= runtime[1];
83
+ }
84
+ function compareSemver(a, b) {
85
+ for (let i = 0; i < 3; i++) {
86
+ if (a[i] !== b[i])
87
+ return a[i] < b[i] ? -1 : 1;
88
+ }
89
+ return 0;
90
+ }
91
+ /**
92
+ * Minimal semver-range check for `dependsOn`: supports `^x.y.z`, `~x.y.z`, and exact `x.y.z`
93
+ * (npm semantics, including the 0.x caret caveats). Anything else returns false.
94
+ */
95
+ export function satisfiesRange(version, range) {
96
+ const v = parseSemver(version);
97
+ if (!v)
98
+ return false;
99
+ const op = range.startsWith("^") || range.startsWith("~") ? range[0] : "";
100
+ const base = parseSemver(op ? range.slice(1) : range);
101
+ if (!base)
102
+ return false;
103
+ if (compareSemver(v, base) < 0)
104
+ return false;
105
+ if (op === "")
106
+ return compareSemver(v, base) === 0;
107
+ if (op === "~")
108
+ return v[0] === base[0] && v[1] === base[1];
109
+ // Caret: nothing left of the leftmost non-zero digit may change.
110
+ if (base[0] > 0)
111
+ return v[0] === base[0];
112
+ if (base[1] > 0)
113
+ return v[0] === 0 && v[1] === base[1];
114
+ return v[0] === 0 && v[1] === 0 && v[2] === base[2];
115
+ }
@@ -0,0 +1,51 @@
1
+ import { type PluginManifest } from "./manifest.js";
2
+ import { type PluginsLockFile, type PluginSourceSpec } from "./lock.js";
3
+ import type { PluginRecord } from "./registry.js";
4
+ /** A source whose package.json#docsxai validated. Not yet loaded. */
5
+ export interface ResolvedPluginSource {
6
+ name: string;
7
+ version: string;
8
+ dir: string;
9
+ registerPath: string;
10
+ source: string;
11
+ manifest: PluginManifest;
12
+ }
13
+ export type PluginSourceResolution = {
14
+ ok: true;
15
+ candidate: ResolvedPluginSource;
16
+ } | {
17
+ ok: false;
18
+ record: PluginRecord;
19
+ };
20
+ export interface ResolvePluginsOptions {
21
+ /** The workspace all plugin file IO is contained to. */
22
+ workspaceDir: string;
23
+ sources: ReadonlyArray<PluginSourceSpec>;
24
+ /** Operator-enabled capabilities (exact-string subset check). Default: none enabled. */
25
+ enabledCapabilities?: ReadonlyArray<string>;
26
+ /** Parsed plugins-lock.json. When present, register-module bytes are verified before import. */
27
+ lock?: PluginsLockFile | null;
28
+ }
29
+ /** The deterministic output of planning: disabled records, plus survivors in load order. */
30
+ export interface PluginPlan {
31
+ /** Records for every rejected/disabled source — committed before any plugin loads. */
32
+ disabled: PluginRecord[];
33
+ /** Surviving candidates keyed by package name, used by the load half. */
34
+ live: Map<string, ResolvedPluginSource>;
35
+ /** Topological order (dependencies before dependents) over `live`. */
36
+ loadOrder: string[];
37
+ }
38
+ /** Build the PluginRecord for a resolved candidate at a given status. Shared with the load half. */
39
+ export declare function candidateRecord(c: ResolvedPluginSource, status: PluginRecord["status"], reason?: string): PluginRecord;
40
+ /**
41
+ * Stage 1 only: resolve every source to a validated manifest (or a load-error record). Never
42
+ * imports plugin code — `docsxai plugins sync` pins hashes through this without executing
43
+ * anything.
44
+ */
45
+ export declare function resolvePluginSources(workspaceDir: string, sources: ReadonlyArray<PluginSourceSpec>): Promise<PluginSourceResolution[]>;
46
+ /**
47
+ * Run the deterministic planning pipeline (stages 1–6) over the configured sources. Never imports
48
+ * plugin code; the surviving `loadOrder` is consumed by the load half, which performs the
49
+ * capability subset check and the actual import + register().
50
+ */
51
+ export declare function planPlugins(opts: ResolvePluginsOptions): Promise<PluginPlan>;