@jini-ai/agent-plugins 0.4.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/CHANGELOG.md +57 -0
- package/LICENSE +201 -0
- package/README.md +261 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/lifecycle/activation.d.ts +177 -0
- package/dist/lifecycle/activation.js +874 -0
- package/dist/lifecycle/bundled-digests.d.ts +27 -0
- package/dist/lifecycle/bundled-digests.js +149 -0
- package/dist/lifecycle/bundled-source-archive.d.ts +13 -0
- package/dist/lifecycle/bundled-source-archive.js +96 -0
- package/dist/lifecycle/capability-projection.d.ts +11 -0
- package/dist/lifecycle/capability-projection.js +60 -0
- package/dist/lifecycle/fetch-archive.d.ts +88 -0
- package/dist/lifecycle/fetch-archive.js +215 -0
- package/dist/lifecycle/index.d.ts +174 -0
- package/dist/lifecycle/index.js +94 -0
- package/dist/lifecycle/install-from-url.d.ts +78 -0
- package/dist/lifecycle/install-from-url.js +73 -0
- package/dist/lifecycle/install.d.ts +52 -0
- package/dist/lifecycle/install.js +286 -0
- package/dist/lifecycle/layout.d.ts +20 -0
- package/dist/lifecycle/layout.js +30 -0
- package/dist/lifecycle/manifest.d.ts +214 -0
- package/dist/lifecycle/manifest.js +409 -0
- package/dist/lifecycle/mcp-provisioning.d.ts +20 -0
- package/dist/lifecycle/mcp-provisioning.js +14 -0
- package/dist/lifecycle/node.d.ts +11 -0
- package/dist/lifecycle/node.js +34 -0
- package/dist/lifecycle/package-paths.d.ts +12 -0
- package/dist/lifecycle/package-paths.js +74 -0
- package/dist/lifecycle/ports.d.ts +81 -0
- package/dist/lifecycle/ports.js +2 -0
- package/dist/lifecycle/resolve-agent-plugin-refs.d.ts +18 -0
- package/dist/lifecycle/resolve-agent-plugin-refs.js +143 -0
- package/dist/lifecycle/retire-bundled.d.ts +36 -0
- package/dist/lifecycle/retire-bundled.js +69 -0
- package/dist/lifecycle/search.d.ts +23 -0
- package/dist/lifecycle/search.js +50 -0
- package/dist/lifecycle/seed-bundled.d.ts +29 -0
- package/dist/lifecycle/seed-bundled.js +118 -0
- package/dist/lifecycle/set-enabled.d.ts +21 -0
- package/dist/lifecycle/set-enabled.js +39 -0
- package/dist/lifecycle/trusted-plugin-files.d.ts +29 -0
- package/dist/lifecycle/trusted-plugin-files.js +82 -0
- package/dist/lifecycle/types.d.ts +20 -0
- package/dist/lifecycle/types.js +2 -0
- package/dist/lifecycle/uninstall.d.ts +41 -0
- package/dist/lifecycle/uninstall.js +148 -0
- package/dist/lifecycle/yauzl-archive-reader.d.ts +62 -0
- package/dist/lifecycle/yauzl-archive-reader.js +73 -0
- package/dist/manifest.d.ts +68 -0
- package/dist/manifest.js +38 -0
- package/package.json +92 -0
- package/ui-ux-design/com.anthropic.claude-code/hooks/hooks.json +4 -0
- package/ui-ux-design/mcp.json +4 -0
- package/ui-ux-design/plugin.json +5 -0
- package/ui-ux-design/skills/frontend-accessibility/SKILL.md +60 -0
- package/ui-ux-design/skills/gstack-design/SKILL.md +53 -0
- package/ui-ux-design/skills/gstack-design/references/consultation.md +30 -0
- package/ui-ux-design/skills/gstack-design/references/html.md +29 -0
- package/ui-ux-design/skills/gstack-design/references/review.md +28 -0
- package/ui-ux-design/skills/gstack-design/references/shotgun.md +33 -0
- package/ui-ux-design/skills/gstack-design/references/upstream-notes.md +23 -0
- package/ui-ux-design/skills/interface-design/ORIGINAL.md +16 -0
- package/ui-ux-design/skills/interface-design/SKILL.md +41 -0
- package/ui-ux-design/skills/interface-design/references/commands/audit.md +40 -0
- package/ui-ux-design/skills/interface-design/references/commands/critique.md +21 -0
- package/ui-ux-design/skills/interface-design/references/commands/extract.md +34 -0
- package/ui-ux-design/skills/interface-design/references/commands/init.md +31 -0
- package/ui-ux-design/skills/interface-design/references/commands/status.md +22 -0
- package/ui-ux-design/skills/interface-design/references/examples/system-precision.md +29 -0
- package/ui-ux-design/skills/interface-design/references/examples/system-warmth.md +29 -0
- package/ui-ux-design/skills/interface-design/references/system-template.md +57 -0
- package/ui-ux-design/skills/shadcn-ui/README.md +244 -0
- package/ui-ux-design/skills/shadcn-ui/SKILL.md +326 -0
- package/ui-ux-design/skills/shadcn-ui/examples/auth-layout.tsx +177 -0
- package/ui-ux-design/skills/shadcn-ui/examples/data-table.tsx +313 -0
- package/ui-ux-design/skills/shadcn-ui/examples/form-pattern.tsx +177 -0
- package/ui-ux-design/skills/shadcn-ui/resources/component-catalog.md +481 -0
- package/ui-ux-design/skills/shadcn-ui/resources/customization-guide.md +516 -0
- package/ui-ux-design/skills/shadcn-ui/resources/migration-guide.md +463 -0
- package/ui-ux-design/skills/shadcn-ui/resources/setup-guide.md +412 -0
- package/ui-ux-design/skills/shadcn-ui/scripts/verify-setup.sh +134 -0
- package/ui-ux-design/skills/ui-ux-design/SKILL.md +77 -0
- package/ui-ux-design/skills/ui-ux-design/references/brand-and-voice.md +72 -0
- package/ui-ux-design/skills/ui-ux-design/references/components-and-states.md +107 -0
- package/ui-ux-design/skills/ui-ux-design/references/delight-and-motion.md +65 -0
- package/ui-ux-design/skills/ui-ux-design/references/foundations.md +144 -0
- package/ui-ux-design/skills/ui-ux-design/references/inclusive-ai-imagery.md +57 -0
- package/ui-ux-design/skills/ui-ux-design/references/kole-jain-uiux-concepts.md +102 -0
- package/ui-ux-design/skills/ui-ux-design/references/openai-frontend-skill.md +139 -0
- package/ui-ux-design/skills/ui-ux-design/references/premium-ui.md +63 -0
- package/ui-ux-design/skills/ui-ux-design/references/research-and-validation.md +78 -0
- package/ui-ux-design/skills/ui-ux-design/references/sam-crawford-premium-websites.md +93 -0
- package/ui-ux-design/skills/ui-ux-design/references/self-made-web-designer-core-skills.md +123 -0
- package/ui-ux-design/skills/ui-ux-design/references/visual-storytelling.md +57 -0
- package/ui-ux-design/skills/vercel-web-design-guidelines/SKILL.md +39 -0
- package/ui-ux-design/skills/web-compliance/SKILL.md +129 -0
- package/ui-ux-design/skills/web-compliance/references/audit-evidence.md +59 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AgentPluginLifecyclePorts } from "./ports.js";
|
|
2
|
+
export declare const BUNDLED_DIGESTS_FILENAME = "bundled-digests.json";
|
|
3
|
+
export type BundledAgentPluginDigests = ReadonlyMap<string, string>;
|
|
4
|
+
export interface SeededBundledAgentPluginDigest {
|
|
5
|
+
readonly pluginId: string;
|
|
6
|
+
readonly archiveDigest: string;
|
|
7
|
+
}
|
|
8
|
+
export interface InstalledDigestIdentity {
|
|
9
|
+
readonly pluginId: string;
|
|
10
|
+
readonly archiveDigest: string;
|
|
11
|
+
}
|
|
12
|
+
/** Internal composition; one state/queue instance per injected host context. */
|
|
13
|
+
export declare function createBundledDigestsModule(ports: AgentPluginLifecyclePorts): {
|
|
14
|
+
readBundledAgentPluginDigests: (workspaceRoot: string) => Promise<BundledAgentPluginDigests>;
|
|
15
|
+
normalizeBundledDigests: (value: unknown) => BundledAgentPluginDigests;
|
|
16
|
+
recordBundledAgentPluginDigests: (required: {
|
|
17
|
+
readonly workspaceRoot: string;
|
|
18
|
+
readonly seeded: readonly SeededBundledAgentPluginDigest[];
|
|
19
|
+
readonly now?: ((required: Record<string, never>) => Date) | undefined;
|
|
20
|
+
}) => Promise<void>;
|
|
21
|
+
removeBundledAgentPluginDigest: (required: {
|
|
22
|
+
readonly workspaceRoot: string;
|
|
23
|
+
readonly pluginId: string;
|
|
24
|
+
}) => Promise<boolean>;
|
|
25
|
+
preferBundledAgentPluginDigests: <T extends InstalledDigestIdentity>(installed: readonly T[], bundled: BundledAgentPluginDigests) => readonly T[];
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=bundled-digests.d.ts.map
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/** Extracted from the host plugin lifecycle. Effects are supplied per host context. */
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export const BUNDLED_DIGESTS_FILENAME = "bundled-digests.json";
|
|
4
|
+
function buildModule(ports) {
|
|
5
|
+
const open = ports.filesystem.open.bind(ports.filesystem);
|
|
6
|
+
const readFile = ports.filesystem.readFile.bind(ports.filesystem);
|
|
7
|
+
const rename = ports.filesystem.rename.bind(ports.filesystem);
|
|
8
|
+
const rm = ports.filesystem.rm.bind(ports.filesystem);
|
|
9
|
+
const randomUUID = () => ports.ids.newId();
|
|
10
|
+
const SHA256_DIGEST_PATTERN = /^[a-f0-9]{64}$/;
|
|
11
|
+
const SAFE_PLUGIN_ID_PATTERN = /^[a-z0-9]+(?:[-.][a-z0-9]+)*$/;
|
|
12
|
+
const EMPTY_LEDGER = new Map();
|
|
13
|
+
async function readBundledAgentPluginDigests(workspaceRoot) {
|
|
14
|
+
let raw;
|
|
15
|
+
try {
|
|
16
|
+
raw = await readFile(path.join(workspaceRoot, BUNDLED_DIGESTS_FILENAME), "utf8");
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return EMPTY_LEDGER;
|
|
20
|
+
}
|
|
21
|
+
let parsed;
|
|
22
|
+
try {
|
|
23
|
+
parsed = JSON.parse(raw);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return EMPTY_LEDGER;
|
|
27
|
+
}
|
|
28
|
+
return normalizeBundledDigests(parsed);
|
|
29
|
+
}
|
|
30
|
+
function normalizeBundledDigests(value) {
|
|
31
|
+
if (!isPlainObject(value) || value.schemaVersion !== 1 || !isPlainObject(value.plugins))
|
|
32
|
+
return EMPTY_LEDGER;
|
|
33
|
+
const digests = new Map();
|
|
34
|
+
for (const [pluginId, entry] of Object.entries(value.plugins)) {
|
|
35
|
+
if (!SAFE_PLUGIN_ID_PATTERN.test(pluginId))
|
|
36
|
+
continue;
|
|
37
|
+
if (!isPlainObject(entry))
|
|
38
|
+
continue;
|
|
39
|
+
const { archiveDigest } = entry;
|
|
40
|
+
if (typeof archiveDigest !== "string" || !SHA256_DIGEST_PATTERN.test(archiveDigest))
|
|
41
|
+
continue;
|
|
42
|
+
digests.set(pluginId, archiveDigest);
|
|
43
|
+
}
|
|
44
|
+
return digests;
|
|
45
|
+
}
|
|
46
|
+
async function recordBundledAgentPluginDigests(required) {
|
|
47
|
+
if (required.seeded.length === 0)
|
|
48
|
+
return;
|
|
49
|
+
const existing = await readBundledAgentPluginDigests(required.workspaceRoot);
|
|
50
|
+
const merged = new Map(existing);
|
|
51
|
+
const seededAt = (required.now?.({}) ?? new Date(ports.clock.nowMs())).toISOString();
|
|
52
|
+
for (const [pluginId, archiveDigest] of resolveThisBootsDigests(required.seeded)) {
|
|
53
|
+
if (archiveDigest === undefined)
|
|
54
|
+
merged.delete(pluginId);
|
|
55
|
+
else
|
|
56
|
+
merged.set(pluginId, archiveDigest);
|
|
57
|
+
}
|
|
58
|
+
const plugins = Object.fromEntries([...merged].map(([pluginId, archiveDigest]) => [pluginId, { archiveDigest, seededAt }]));
|
|
59
|
+
await writeLedgerAtomically(required.workspaceRoot, plugins);
|
|
60
|
+
}
|
|
61
|
+
function resolveThisBootsDigests(seeded) {
|
|
62
|
+
const thisBoot = new Map();
|
|
63
|
+
for (const { pluginId, archiveDigest } of seeded) {
|
|
64
|
+
if (!SAFE_PLUGIN_ID_PATTERN.test(pluginId) || !SHA256_DIGEST_PATTERN.test(archiveDigest))
|
|
65
|
+
continue;
|
|
66
|
+
if (thisBoot.has(pluginId) && thisBoot.get(pluginId) !== archiveDigest)
|
|
67
|
+
thisBoot.set(pluginId, undefined);
|
|
68
|
+
else
|
|
69
|
+
thisBoot.set(pluginId, archiveDigest);
|
|
70
|
+
}
|
|
71
|
+
return thisBoot;
|
|
72
|
+
}
|
|
73
|
+
async function removeBundledAgentPluginDigest(required) {
|
|
74
|
+
let parsed;
|
|
75
|
+
try {
|
|
76
|
+
parsed = JSON.parse(await readFile(path.join(required.workspaceRoot, BUNDLED_DIGESTS_FILENAME), "utf8"));
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
if (!isPlainObject(parsed) || parsed.schemaVersion !== 1 || !isPlainObject(parsed.plugins))
|
|
82
|
+
return false;
|
|
83
|
+
if (!Object.hasOwn(parsed.plugins, required.pluginId))
|
|
84
|
+
return false;
|
|
85
|
+
const remaining = { ...parsed.plugins };
|
|
86
|
+
delete remaining[required.pluginId];
|
|
87
|
+
await writeLedgerAtomically(required.workspaceRoot, remaining);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
async function writeLedgerAtomically(workspaceRoot, plugins) {
|
|
91
|
+
const finalPath = path.join(workspaceRoot, BUNDLED_DIGESTS_FILENAME);
|
|
92
|
+
const tempPath = `${finalPath}.tmp-${ports.process.pid}-${randomUUID()}`;
|
|
93
|
+
try {
|
|
94
|
+
const handle = await open(tempPath, "wx", 0o600);
|
|
95
|
+
try {
|
|
96
|
+
await handle.writeFile(`${JSON.stringify({ schemaVersion: 1, plugins }, null, 2)}\n`, "utf8");
|
|
97
|
+
await handle.sync();
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
await handle.close();
|
|
101
|
+
}
|
|
102
|
+
await rename(tempPath, finalPath);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
await rm(tempPath, { force: true });
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function preferBundledAgentPluginDigests(installed, bundled) {
|
|
110
|
+
if (bundled.size === 0)
|
|
111
|
+
return installed;
|
|
112
|
+
const supersededIds = idsNarrowableToTheirBundledDigest(installed, bundled);
|
|
113
|
+
if (supersededIds.size === 0)
|
|
114
|
+
return installed;
|
|
115
|
+
return installed.filter((plugin) => !supersededIds.has(plugin.pluginId) || plugin.archiveDigest === bundled.get(plugin.pluginId));
|
|
116
|
+
}
|
|
117
|
+
function idsNarrowableToTheirBundledDigest(installed, bundled) {
|
|
118
|
+
const digestsByPluginId = new Map();
|
|
119
|
+
for (const plugin of installed) {
|
|
120
|
+
const digests = digestsByPluginId.get(plugin.pluginId) ?? new Set();
|
|
121
|
+
digests.add(plugin.archiveDigest);
|
|
122
|
+
digestsByPluginId.set(plugin.pluginId, digests);
|
|
123
|
+
}
|
|
124
|
+
const narrowable = new Set();
|
|
125
|
+
for (const [pluginId, digests] of digestsByPluginId) {
|
|
126
|
+
if (digests.size < 2)
|
|
127
|
+
continue;
|
|
128
|
+
const bundledDigest = bundled.get(pluginId);
|
|
129
|
+
if (bundledDigest !== undefined && digests.has(bundledDigest))
|
|
130
|
+
narrowable.add(pluginId);
|
|
131
|
+
}
|
|
132
|
+
return narrowable;
|
|
133
|
+
}
|
|
134
|
+
function isPlainObject(value) {
|
|
135
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
136
|
+
}
|
|
137
|
+
return { readBundledAgentPluginDigests, normalizeBundledDigests, recordBundledAgentPluginDigests, removeBundledAgentPluginDigest, preferBundledAgentPluginDigests };
|
|
138
|
+
}
|
|
139
|
+
const instances = new WeakMap();
|
|
140
|
+
/** Internal composition; one state/queue instance per injected host context. */
|
|
141
|
+
export function createBundledDigestsModule(ports) {
|
|
142
|
+
let instance = instances.get(ports);
|
|
143
|
+
if (!instance) {
|
|
144
|
+
instance = buildModule(ports);
|
|
145
|
+
instances.set(ports, instance);
|
|
146
|
+
}
|
|
147
|
+
return instance;
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=bundled-digests.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AgentPluginArchiveReaderPort } from "./install.js";
|
|
2
|
+
import type { AgentPluginLifecyclePorts } from "./ports.js";
|
|
3
|
+
export interface PackedAgentPluginArchive {
|
|
4
|
+
readonly bytes: Uint8Array;
|
|
5
|
+
readonly sha256: string;
|
|
6
|
+
readonly files: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
/** Internal composition; one state/queue instance per injected host context. */
|
|
9
|
+
export declare function createBundledSourceArchiveModule(ports: AgentPluginLifecyclePorts): {
|
|
10
|
+
packAgentPluginDirectory: (sourceDir: string) => Promise<PackedAgentPluginArchive>;
|
|
11
|
+
createBundledSourceArchiveReader: () => AgentPluginArchiveReaderPort;
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=bundled-source-archive.d.ts.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** Extracted from the host plugin lifecycle. Effects are supplied per host context. */
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
function buildModule(ports) {
|
|
5
|
+
const readdir = ports.filesystem.readdir.bind(ports.filesystem);
|
|
6
|
+
const readFile = ports.filesystem.readFile.bind(ports.filesystem);
|
|
7
|
+
const MAGIC = ports.bundledArchiveMagic;
|
|
8
|
+
const MAX_SOURCE_FILES = 2048;
|
|
9
|
+
async function packAgentPluginDirectory(sourceDir) {
|
|
10
|
+
const relativePaths = (await listRegularFiles(sourceDir, "")).sort();
|
|
11
|
+
if (relativePaths.length > MAX_SOURCE_FILES) {
|
|
12
|
+
throw new Error(`bundled agent plugin at '${sourceDir}' has ${relativePaths.length} files, over the ${MAX_SOURCE_FILES}-file cap — is this the right directory?`);
|
|
13
|
+
}
|
|
14
|
+
const chunks = [Buffer.from(MAGIC, "utf8")];
|
|
15
|
+
for (const relativePath of relativePaths) {
|
|
16
|
+
const content = await readFile(path.join(sourceDir, relativePath));
|
|
17
|
+
const pathBytes = Buffer.from(relativePath, "utf8");
|
|
18
|
+
chunks.push(Buffer.from(`F ${pathBytes.byteLength} ${content.byteLength}\n`, "utf8"), pathBytes, content);
|
|
19
|
+
}
|
|
20
|
+
const bytes = Buffer.concat(chunks);
|
|
21
|
+
return {
|
|
22
|
+
bytes: new Uint8Array(bytes),
|
|
23
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
24
|
+
files: relativePaths,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function createBundledSourceArchiveReader() {
|
|
28
|
+
return {
|
|
29
|
+
entries({ archive }) {
|
|
30
|
+
return readEntries(archive);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async function* readEntries(archive) {
|
|
35
|
+
const buffer = Buffer.from(archive.buffer, archive.byteOffset, archive.byteLength);
|
|
36
|
+
const magic = Buffer.from(MAGIC, "utf8");
|
|
37
|
+
if (buffer.byteLength < magic.byteLength || !buffer.subarray(0, magic.byteLength).equals(magic)) {
|
|
38
|
+
throw new Error("bundled agent plugin archive: bad magic — this reader only understands the configured bundled archive format");
|
|
39
|
+
}
|
|
40
|
+
let offset = magic.byteLength;
|
|
41
|
+
while (offset < buffer.byteLength) {
|
|
42
|
+
const newlineIndex = buffer.indexOf(0x0a, offset);
|
|
43
|
+
if (newlineIndex === -1)
|
|
44
|
+
throw new Error("bundled agent plugin archive: truncated entry header");
|
|
45
|
+
const header = buffer.subarray(offset, newlineIndex).toString("utf8");
|
|
46
|
+
const match = /^F (\d+) (\d+)$/.exec(header);
|
|
47
|
+
if (!match)
|
|
48
|
+
throw new Error(`bundled agent plugin archive: malformed entry header '${header}'`);
|
|
49
|
+
const pathLength = Number(match[1]);
|
|
50
|
+
const contentLength = Number(match[2]);
|
|
51
|
+
const pathStart = newlineIndex + 1;
|
|
52
|
+
const contentStart = pathStart + pathLength;
|
|
53
|
+
const contentEnd = contentStart + contentLength;
|
|
54
|
+
if (contentEnd > buffer.byteLength)
|
|
55
|
+
throw new Error("bundled agent plugin archive: truncated entry body");
|
|
56
|
+
const entryPath = buffer.subarray(pathStart, contentStart).toString("utf8");
|
|
57
|
+
const content = buffer.subarray(contentStart, contentEnd);
|
|
58
|
+
yield {
|
|
59
|
+
kind: "file",
|
|
60
|
+
entryPath,
|
|
61
|
+
declaredSize: contentLength,
|
|
62
|
+
openReadStream: () => onceAsyncIterable(new Uint8Array(content)),
|
|
63
|
+
};
|
|
64
|
+
offset = contentEnd;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function* onceAsyncIterable(chunk) {
|
|
68
|
+
yield chunk;
|
|
69
|
+
}
|
|
70
|
+
async function listRegularFiles(dir, prefix) {
|
|
71
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
72
|
+
const files = [];
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
75
|
+
if (entry.isDirectory()) {
|
|
76
|
+
files.push(...(await listRegularFiles(path.join(dir, entry.name), relative)));
|
|
77
|
+
}
|
|
78
|
+
else if (entry.isFile()) {
|
|
79
|
+
files.push(relative);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return files;
|
|
83
|
+
}
|
|
84
|
+
return { packAgentPluginDirectory, createBundledSourceArchiveReader };
|
|
85
|
+
}
|
|
86
|
+
const instances = new WeakMap();
|
|
87
|
+
/** Internal composition; one state/queue instance per injected host context. */
|
|
88
|
+
export function createBundledSourceArchiveModule(ports) {
|
|
89
|
+
let instance = instances.get(ports);
|
|
90
|
+
if (!instance) {
|
|
91
|
+
instance = buildModule(ports);
|
|
92
|
+
instances.set(ports, instance);
|
|
93
|
+
}
|
|
94
|
+
return instance;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=bundled-source-archive.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Extracted from the host plugin lifecycle. Effects are supplied per host context. */
|
|
2
|
+
import type { McpServerConfig } from "./manifest.js";
|
|
3
|
+
import type { AgentPluginLifecyclePorts } from "./ports.js";
|
|
4
|
+
/** Internal composition; one state/queue instance per injected host context. */
|
|
5
|
+
export declare function createCapabilityProjectionModule(ports: AgentPluginLifecyclePorts): {
|
|
6
|
+
classifyAgentPluginMcpServerTrust: (server: Pick<McpServerConfig, "type">) => "auto-admit" | "requires-confirmation";
|
|
7
|
+
readInstalledSkillMarkdown: (packageRoot: string, skillPath: string) => Promise<string>;
|
|
8
|
+
readInstalledMcpServerIds: (packageRoot: string) => Promise<readonly string[]>;
|
|
9
|
+
readInstalledMcpServers: (packageRoot: string) => Promise<Readonly<Record<string, McpServerConfig>>>;
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=capability-projection.d.ts.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { parseAgentPluginMcpConfig as parseAgentPluginMcpConfigValue } from "./manifest.js";
|
|
2
|
+
import { createPackagePathsModule } from "./package-paths.js";
|
|
3
|
+
function buildModule(ports) {
|
|
4
|
+
const readFile = ports.filesystem.readFile.bind(ports.filesystem);
|
|
5
|
+
const parseAgentPluginMcpConfig = (value, pluginManifest) => parseAgentPluginMcpConfigValue({ value, extensionNamespace: ports.extensionNamespace }, { pluginManifest, readServerMetadata: ports.readServerMetadata });
|
|
6
|
+
const { assertContainedOnDisk } = createPackagePathsModule(ports);
|
|
7
|
+
function classifyAgentPluginMcpServerTrust(server) {
|
|
8
|
+
return server.type === "stdio" ? "requires-confirmation" : "auto-admit";
|
|
9
|
+
}
|
|
10
|
+
async function readInstalledSkillMarkdown(packageRoot, skillPath) {
|
|
11
|
+
const absolute = await assertContainedOnDisk(packageRoot, skillPath);
|
|
12
|
+
return readFile(absolute, "utf8");
|
|
13
|
+
}
|
|
14
|
+
async function readInstalledMcpConfig(packageRoot) {
|
|
15
|
+
let raw;
|
|
16
|
+
try {
|
|
17
|
+
const absolute = await assertContainedOnDisk(packageRoot, "mcp.json");
|
|
18
|
+
raw = await readFile(absolute, "utf8");
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
let parsedJson;
|
|
24
|
+
try {
|
|
25
|
+
parsedJson = JSON.parse(raw);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
let manifest;
|
|
31
|
+
try {
|
|
32
|
+
const absolute = await assertContainedOnDisk(packageRoot, "plugin.json");
|
|
33
|
+
manifest = JSON.parse(await readFile(absolute, "utf8"));
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
}
|
|
37
|
+
const result = parseAgentPluginMcpConfig(parsedJson, manifest);
|
|
38
|
+
return result.ok ? result.config : null;
|
|
39
|
+
}
|
|
40
|
+
async function readInstalledMcpServerIds(packageRoot) {
|
|
41
|
+
const config = await readInstalledMcpConfig(packageRoot);
|
|
42
|
+
return config?.serverIds ?? [];
|
|
43
|
+
}
|
|
44
|
+
async function readInstalledMcpServers(packageRoot) {
|
|
45
|
+
const config = await readInstalledMcpConfig(packageRoot);
|
|
46
|
+
return config?.servers ?? {};
|
|
47
|
+
}
|
|
48
|
+
return { classifyAgentPluginMcpServerTrust, readInstalledSkillMarkdown, readInstalledMcpServerIds, readInstalledMcpServers };
|
|
49
|
+
}
|
|
50
|
+
const instances = new WeakMap();
|
|
51
|
+
/** Internal composition; one state/queue instance per injected host context. */
|
|
52
|
+
export function createCapabilityProjectionModule(ports) {
|
|
53
|
+
let instance = instances.get(ports);
|
|
54
|
+
if (!instance) {
|
|
55
|
+
instance = buildModule(ports);
|
|
56
|
+
instances.set(ports, instance);
|
|
57
|
+
}
|
|
58
|
+
return instance;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=capability-projection.js.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Historical lifecycle rationale, carried with the implementation during consumer adoption.
|
|
3
|
+
* @file The missing front half of the Agent Plugin install pipeline: URL -> verified bytes.
|
|
4
|
+
*
|
|
5
|
+
* `install.ts` deliberately takes `archive: Uint8Array` and never a URL — it is a pure
|
|
6
|
+
* verify/extract/publish step with no network surface at all. This module is the one place that
|
|
7
|
+
* talks to the network, and it hands `install.ts` exactly what it already accepts. The split is
|
|
8
|
+
* kept on purpose: every hostile-archive guard in `install.ts` (`DIGEST_MISMATCH`, zip-slip,
|
|
9
|
+
* decompression caps) stays reachable from a plain in-memory buffer in tests, with no fetch mock.
|
|
10
|
+
*
|
|
11
|
+
* What this module does NOT do:
|
|
12
|
+
* - It does not decide whether a digest is trustworthy. `expectedSha256` is verified by
|
|
13
|
+
* `install.ts`, not here; {@link fetchAgentPluginArchive} only reports the digest of what
|
|
14
|
+
* actually arrived so a caller can pin it.
|
|
15
|
+
* - It does not follow a redirect chain by hand. `fetch` handles redirects; the scheme guard below
|
|
16
|
+
* re-runs on the FINAL response URL so an `https://` start cannot be redirected onto a
|
|
17
|
+
* non-network scheme.
|
|
18
|
+
* - It does not trust `Content-Length`. That header is attacker-controlled for a hostile host; the
|
|
19
|
+
* cap below is enforced against bytes actually read, and `Content-Length` is used only as an
|
|
20
|
+
* early reject so an obviously-oversized download is not started at all.
|
|
21
|
+
*/
|
|
22
|
+
import type { AgentPluginOutboundGuardPort, AgentPluginFetchPort } from "./ports.js";
|
|
23
|
+
export type AgentPluginFetchErrorCode = "UNSUPPORTED_URL" | "REQUEST_FAILED" | "HTTP_ERROR" | "ARCHIVE_TOO_LARGE" | "EMPTY_BODY";
|
|
24
|
+
/**
|
|
25
|
+
* The URL did not parse, or its scheme is not in {@link ALLOWED_PROTOCOLS}.
|
|
26
|
+
*
|
|
27
|
+
* The request never produced a response — DNS failure, connection refused, TLS failure, abort.
|
|
28
|
+
*
|
|
29
|
+
* A response arrived with a non-2xx status.
|
|
30
|
+
*
|
|
31
|
+
* The response body exceeded {@link MAX_ARCHIVE_BYTES}, by header or by bytes actually read.
|
|
32
|
+
*
|
|
33
|
+
* A 2xx response with no body at all — never a valid archive, and a clearer error than letting
|
|
34
|
+
* the zip reader fail on zero bytes.
|
|
35
|
+
*/
|
|
36
|
+
export declare class AgentPluginFetchError extends Error {
|
|
37
|
+
readonly code: AgentPluginFetchErrorCode;
|
|
38
|
+
constructor({ code, message }: {
|
|
39
|
+
readonly code: AgentPluginFetchErrorCode;
|
|
40
|
+
readonly message: string;
|
|
41
|
+
}, options?: {
|
|
42
|
+
cause?: unknown;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
export interface FetchAgentPluginArchiveRequired {
|
|
46
|
+
readonly url: string;
|
|
47
|
+
readonly fetch: AgentPluginFetchPort;
|
|
48
|
+
readonly outboundGuard: AgentPluginOutboundGuardPort;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Absolute `https:` or `http:` URL of a plugin `.zip`.
|
|
52
|
+
*/
|
|
53
|
+
export interface FetchAgentPluginArchiveOptional {
|
|
54
|
+
readonly maxBytes?: (number) | undefined;
|
|
55
|
+
readonly signal?: (AbortSignal) | undefined;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Injectable for tests and for a caller that needs its own agent/proxy. Defaults to global `fetch`.
|
|
59
|
+
*
|
|
60
|
+
* Lowered by a caller that wants a tighter cap than the module default. Never raised above
|
|
61
|
+
* {@link MAX_ARCHIVE_BYTES} — a larger value is clamped, because `install.ts` would reject it anyway.
|
|
62
|
+
*
|
|
63
|
+
* Forwarded to `fetch`, so a caller can time out or cancel a slow download.
|
|
64
|
+
*/
|
|
65
|
+
export interface FetchedAgentPluginArchive {
|
|
66
|
+
readonly archive: Uint8Array;
|
|
67
|
+
readonly sha256: string;
|
|
68
|
+
readonly resolvedUrl: string;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Lowercase hex SHA-256 of `archive` as received. Pass to `installAgentPlugin` as
|
|
72
|
+
* `expectedSha256` only if the caller has independently decided to trust these bytes — this value
|
|
73
|
+
* is computed from the download itself and therefore proves nothing about origin on its own.
|
|
74
|
+
*
|
|
75
|
+
* The FINAL URL after redirects, which may differ from the requested one.
|
|
76
|
+
*
|
|
77
|
+
* Downloads one Agent Plugin archive into memory, bounded and digested.
|
|
78
|
+
*
|
|
79
|
+
* @throws {AgentPluginFetchError} For every expected failure. A caller distinguishes them by `code`.
|
|
80
|
+
* @complexity O(b) in bytes downloaded, bounded by `maxBytes`.
|
|
81
|
+
*/
|
|
82
|
+
export declare function fetchAgentPluginArchive(required: FetchAgentPluginArchiveRequired, optional?: FetchAgentPluginArchiveOptional): Promise<FetchedAgentPluginArchive>;
|
|
83
|
+
/**
|
|
84
|
+
* The module's own cap, exported so a caller (or a test pinning it against `install.ts`'s
|
|
85
|
+
* `LIMITS.maxArchiveBytes`) can read it without duplicating the number.
|
|
86
|
+
*/
|
|
87
|
+
export declare function maxAgentPluginArchiveBytes(_required: Record<string, never>): number;
|
|
88
|
+
//# sourceMappingURL=fetch-archive.d.ts.map
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { STATUS_CODES } from "node:http";
|
|
3
|
+
/**
|
|
4
|
+
* Mirrors `install.ts`'s own `LIMITS.maxArchiveBytes` — a download this module would accept but
|
|
5
|
+
* `installAgentPlugin` would then reject with `ARCHIVE_TOO_LARGE` is wasted bandwidth, so the cap
|
|
6
|
+
* is enforced at the earliest point it can be. Kept as its own constant rather than imported so
|
|
7
|
+
* this module has no dependency on `install.ts`; the pairing is asserted by a unit test instead.
|
|
8
|
+
*/
|
|
9
|
+
const MAX_ARCHIVE_BYTES = 32 * 1024 * 1024;
|
|
10
|
+
/**
|
|
11
|
+
* `http:` is permitted alongside `https:` because a self-hosted the host's first registry is realistically
|
|
12
|
+
* a machine on its own network (and because the local end-to-end proof of this pipeline serves over
|
|
13
|
+
* loopback). Transport confidentiality is NOT what protects an install here — `expectedSha256` is.
|
|
14
|
+
* Every other scheme is refused: `file:`/`data:` would turn a "download a plugin" call into an
|
|
15
|
+
* arbitrary local-file read reachable from whatever supplies the URL.
|
|
16
|
+
*/
|
|
17
|
+
const ALLOWED_PROTOCOLS = new Set(["https:", "http:"]);
|
|
18
|
+
/**
|
|
19
|
+
* The URL did not parse, or its scheme is not in {@link ALLOWED_PROTOCOLS}.
|
|
20
|
+
*
|
|
21
|
+
* The request never produced a response — DNS failure, connection refused, TLS failure, abort.
|
|
22
|
+
*
|
|
23
|
+
* A response arrived with a non-2xx status.
|
|
24
|
+
*
|
|
25
|
+
* The response body exceeded {@link MAX_ARCHIVE_BYTES}, by header or by bytes actually read.
|
|
26
|
+
*
|
|
27
|
+
* A 2xx response with no body at all — never a valid archive, and a clearer error than letting
|
|
28
|
+
* the zip reader fail on zero bytes.
|
|
29
|
+
*/
|
|
30
|
+
export class AgentPluginFetchError extends Error {
|
|
31
|
+
code;
|
|
32
|
+
constructor({ code, message }, options = {}) {
|
|
33
|
+
super(message, options);
|
|
34
|
+
this.name = "AgentPluginFetchError";
|
|
35
|
+
this.code = code;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Lowercase hex SHA-256 of `archive` as received. Pass to `installAgentPlugin` as
|
|
40
|
+
* `expectedSha256` only if the caller has independently decided to trust these bytes — this value
|
|
41
|
+
* is computed from the download itself and therefore proves nothing about origin on its own.
|
|
42
|
+
*
|
|
43
|
+
* The FINAL URL after redirects, which may differ from the requested one.
|
|
44
|
+
*
|
|
45
|
+
* Downloads one Agent Plugin archive into memory, bounded and digested.
|
|
46
|
+
*
|
|
47
|
+
* @throws {AgentPluginFetchError} For every expected failure. A caller distinguishes them by `code`.
|
|
48
|
+
* @complexity O(b) in bytes downloaded, bounded by `maxBytes`.
|
|
49
|
+
*/
|
|
50
|
+
export async function fetchAgentPluginArchive(required, optional = {}) {
|
|
51
|
+
/**
|
|
52
|
+
* Re-checked against the FINAL url: `redirect: "follow"` means the scheme validated above is not
|
|
53
|
+
* necessarily the scheme the bytes came from.
|
|
54
|
+
*/
|
|
55
|
+
const maxBytes = Math.min(optional.maxBytes ?? MAX_ARCHIVE_BYTES, MAX_ARCHIVE_BYTES);
|
|
56
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0)
|
|
57
|
+
throw new RangeError("maxBytes must be a positive integer");
|
|
58
|
+
assertAllowedUrl(required.url, "requested");
|
|
59
|
+
const diagnosticUrl = redactArchiveUrl({ value: required.url });
|
|
60
|
+
let response;
|
|
61
|
+
let finalRequestedUrl = required.url;
|
|
62
|
+
try {
|
|
63
|
+
const downloaded = await fetchWithGuard(required, optional);
|
|
64
|
+
response = downloaded.response;
|
|
65
|
+
finalRequestedUrl = downloaded.resolvedUrl;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Dependency messages and nested causes can contain signed redirect URLs or bare tokens.
|
|
69
|
+
// Keep neither: sanitizing only the requested URL still exposes secrets through error.cause.
|
|
70
|
+
throw new AgentPluginFetchError({ code: "REQUEST_FAILED", message: `could not fetch '${diagnosticUrl}': archive request failed` }, {
|
|
71
|
+
cause: new Error("archive request failed"),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
// A server-controlled reason phrase can reflect secrets; use the standard status label.
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
throw new AgentPluginFetchError({ code: "HTTP_ERROR", message: `'${diagnosticUrl}' returned HTTP ${response.status} ${STATUS_CODES[response.status] ?? ''}`.trimEnd() });
|
|
77
|
+
}
|
|
78
|
+
const resolvedUrl = response.url || finalRequestedUrl;
|
|
79
|
+
assertAllowedUrl(resolvedUrl, "redirected-to");
|
|
80
|
+
assertDeclaredSizeWithinCap(response, maxBytes, diagnosticUrl);
|
|
81
|
+
let archive;
|
|
82
|
+
try {
|
|
83
|
+
archive = await readBodyWithinCap(response, maxBytes, diagnosticUrl);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
// Preserve our bounded-download error; stream failures must not escape as raw transport text.
|
|
87
|
+
if (error instanceof AgentPluginFetchError && error.code === "ARCHIVE_TOO_LARGE") {
|
|
88
|
+
throw new AgentPluginFetchError({ code: "ARCHIVE_TOO_LARGE", message: `'${diagnosticUrl}' body exceeded the ${maxBytes}-byte cap` });
|
|
89
|
+
}
|
|
90
|
+
throw new AgentPluginFetchError({ code: "REQUEST_FAILED", message: `could not read '${diagnosticUrl}': archive response body failed` }, {
|
|
91
|
+
cause: new Error("archive response body failed"),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (archive.byteLength === 0) {
|
|
95
|
+
throw new AgentPluginFetchError({ code: "EMPTY_BODY", message: `'${diagnosticUrl}' returned an empty body` });
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
archive,
|
|
99
|
+
sha256: createHash("sha256").update(archive).digest("hex"),
|
|
100
|
+
resolvedUrl,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The module's own cap, exported so a caller (or a test pinning it against `install.ts`'s
|
|
105
|
+
* `LIMITS.maxArchiveBytes`) can read it without duplicating the number.
|
|
106
|
+
*/
|
|
107
|
+
export function maxAgentPluginArchiveBytes(_required) {
|
|
108
|
+
return MAX_ARCHIVE_BYTES;
|
|
109
|
+
}
|
|
110
|
+
function assertAllowedUrl(value, position) {
|
|
111
|
+
let parsed;
|
|
112
|
+
try {
|
|
113
|
+
parsed = new URL(value);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
throw new AgentPluginFetchError({ code: "UNSUPPORTED_URL", message: `'${redactArchiveUrl({ value })}' is not an absolute URL` });
|
|
117
|
+
}
|
|
118
|
+
if (!ALLOWED_PROTOCOLS.has(parsed.protocol)) {
|
|
119
|
+
throw new AgentPluginFetchError({ code: "UNSUPPORTED_URL", message: `${position} URL '${redactArchiveUrl({ value })}' uses unsupported scheme '${parsed.protocol}' — only https: and http: are allowed` });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Early reject on a self-declared oversized body. Advisory only — {@link readBodyWithinCap} is the
|
|
124
|
+
* enforcement, because a hostile host can under-report or omit this header entirely.
|
|
125
|
+
*/
|
|
126
|
+
function assertDeclaredSizeWithinCap(response, maxBytes, url) {
|
|
127
|
+
const header = response.headers.get("content-length");
|
|
128
|
+
if (header === null)
|
|
129
|
+
return;
|
|
130
|
+
const declared = Number(header);
|
|
131
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
132
|
+
throw new AgentPluginFetchError({ code: "ARCHIVE_TOO_LARGE", message: `'${url}' declares ${declared} bytes, over the ${maxBytes}-byte cap` });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Reads the body chunk by chunk, aborting the moment the running total would exceed the cap — so a
|
|
137
|
+
* hostile endpoint streaming an unbounded body cannot drive this process out of memory. Buffering
|
|
138
|
+
* the whole response first (`await response.arrayBuffer()`) would defeat the cap entirely.
|
|
139
|
+
*/
|
|
140
|
+
async function readBodyWithinCap(response, maxBytes, url) {
|
|
141
|
+
if (response.body === null)
|
|
142
|
+
return new Uint8Array(0);
|
|
143
|
+
const chunks = [];
|
|
144
|
+
let total = 0;
|
|
145
|
+
/**
|
|
146
|
+
* Releases the connection on the throw path too; without it an aborted oversized download
|
|
147
|
+
* leaves the socket held until GC.
|
|
148
|
+
*/
|
|
149
|
+
const reader = response.body.getReader();
|
|
150
|
+
try {
|
|
151
|
+
for (;;) {
|
|
152
|
+
const { done, value } = await reader.read();
|
|
153
|
+
if (done)
|
|
154
|
+
break;
|
|
155
|
+
if (value === undefined)
|
|
156
|
+
continue;
|
|
157
|
+
total += value.byteLength;
|
|
158
|
+
if (total > maxBytes) {
|
|
159
|
+
throw new AgentPluginFetchError({ code: "ARCHIVE_TOO_LARGE", message: `'${url}' body exceeded the ${maxBytes}-byte cap` });
|
|
160
|
+
}
|
|
161
|
+
chunks.push(value);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
await reader.cancel().catch(() => { });
|
|
166
|
+
}
|
|
167
|
+
const archive = new Uint8Array(total);
|
|
168
|
+
let offset = 0;
|
|
169
|
+
for (const chunk of chunks) {
|
|
170
|
+
archive.set(chunk, offset);
|
|
171
|
+
offset += chunk.byteLength;
|
|
172
|
+
}
|
|
173
|
+
return archive;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Diagnostics retain only the URL's public location; requests and resolvedUrl retain the original.
|
|
177
|
+
* Unparseable input is never echoed because its credential boundary cannot be identified safely.
|
|
178
|
+
* @complexity O(n) time and space in the URL length.
|
|
179
|
+
*/
|
|
180
|
+
function redactArchiveUrl({ value }) {
|
|
181
|
+
let parsed;
|
|
182
|
+
try {
|
|
183
|
+
parsed = new URL(value);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return "[invalid URL]";
|
|
187
|
+
}
|
|
188
|
+
if (!parsed.username && !parsed.password && !parsed.search && !parsed.hash)
|
|
189
|
+
return value;
|
|
190
|
+
parsed.username = "";
|
|
191
|
+
parsed.password = "";
|
|
192
|
+
parsed.search = "";
|
|
193
|
+
parsed.hash = "";
|
|
194
|
+
return parsed.href;
|
|
195
|
+
}
|
|
196
|
+
/** Redirects are followed only after guarding each destination; never contact an unguarded hop. */
|
|
197
|
+
async function fetchWithGuard(required, optional) {
|
|
198
|
+
let url = required.url;
|
|
199
|
+
for (let hop = 0; hop <= 10; hop++) {
|
|
200
|
+
assertAllowedUrl(url, 'requested');
|
|
201
|
+
await required.outboundGuard.assertAllowed({ url });
|
|
202
|
+
const response = await required.fetch({ url }, {
|
|
203
|
+
redirect: 'manual', ...(optional.signal ? { signal: optional.signal } : {}),
|
|
204
|
+
});
|
|
205
|
+
if (![301, 302, 303, 307, 308].includes(response.status))
|
|
206
|
+
return { response, resolvedUrl: url };
|
|
207
|
+
const location = response.headers.get('location');
|
|
208
|
+
await response.body?.cancel();
|
|
209
|
+
if (!location)
|
|
210
|
+
throw new Error('Archive redirect has no Location header');
|
|
211
|
+
url = new URL(location, url).href;
|
|
212
|
+
}
|
|
213
|
+
throw new Error('Archive redirect limit exceeded');
|
|
214
|
+
}
|
|
215
|
+
//# sourceMappingURL=fetch-archive.js.map
|