| 1 | import { unzip, zip } from "../lib/archive.js"; |
| 2 | |
| 3 | /** The URL the plugins' "Diffinery URL" setting (userConfig `url`) defaults to in their sources. */ |
| 4 | export const PLUGIN_DEFAULT_URL = "https://diffinery.com"; |
| 5 | const MANIFEST = ".claude-plugin/plugin.json"; |
| 6 | // The fixed timestamp scripts/pack-plugins.py gives every entry, so a rewrite of the same zip is the same bytes. |
| 7 | const STAMP = Date.UTC(2026, 0, 1) / 1000; |
| 8 | |
| 9 | const hex = (b: ArrayBuffer) => [...new Uint8Array(b)].map((x) => x.toString(16).padStart(2, "0")).join(""); |
| 10 | |
| 11 | /** |
| 12 | * A Claude Code plugin zip as `origin` serves it. A self-hosted instance's copy defaults the plugin's |
| 13 | * Diffinery URL setting to that instance, so a plugin uploaded to claude.ai or set up by `diffinery setup` |
| 14 | * from there reaches it with nothing to configure; diffinery.com serves the zip as packed. |
| 15 | */ |
| 16 | export async function pluginZipFor(origin: string, packed: Uint8Array, sha256: string): Promise<{ bytes: Uint8Array; sha256: string }> { |
| 17 | if (origin === PLUGIN_DEFAULT_URL) return { bytes: packed, sha256 }; |
| 18 | const entries = await unzip(packed); |
| 19 | let changed = false; |
| 20 | const out = entries.map((e) => { |
| 21 | if ("dir" in e || e.path !== MANIFEST) return e; |
| 22 | const manifest = JSON.parse(new TextDecoder().decode(e.content)) as { userConfig?: { url?: { default?: unknown } } }; |
| 23 | const url = manifest.userConfig?.url; |
| 24 | if (!url || url.default !== PLUGIN_DEFAULT_URL) return e; |
| 25 | url.default = origin; |
| 26 | changed = true; |
| 27 | return { ...e, content: new TextEncoder().encode(`${JSON.stringify(manifest, null, 2)}\n`) }; |
| 28 | }); |
| 29 | if (!changed) return { bytes: packed, sha256 }; |
| 30 | const bytes = await zip(out, STAMP); |
| 31 | return { bytes, sha256: hex(await crypto.subtle.digest("SHA-256", bytes as Uint8Array<ArrayBuffer>)) }; |
| 32 | } |