@agentproto/secrets 0.2.6 → 1.0.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 (40) hide show
  1. package/dist/{chunk-Q6K3AT3V.mjs → chunk-2FHFW2AU.mjs} +3 -3
  2. package/dist/{chunk-Q6K3AT3V.mjs.map → chunk-2FHFW2AU.mjs.map} +1 -1
  3. package/dist/chunk-3YRYO6LK.mjs +222 -0
  4. package/dist/chunk-3YRYO6LK.mjs.map +1 -0
  5. package/dist/chunk-432SC3XS.mjs +100 -0
  6. package/dist/chunk-432SC3XS.mjs.map +1 -0
  7. package/dist/chunk-5HUHYBPR.mjs +69 -0
  8. package/dist/chunk-5HUHYBPR.mjs.map +1 -0
  9. package/dist/{chunk-NLZ5HXGO.mjs → chunk-7V6U6DII.mjs} +4 -4
  10. package/dist/{chunk-NLZ5HXGO.mjs.map → chunk-7V6U6DII.mjs.map} +1 -1
  11. package/dist/chunk-JNTDSOZQ.mjs +623 -0
  12. package/dist/chunk-JNTDSOZQ.mjs.map +1 -0
  13. package/dist/chunk-NQX3KBPW.mjs +50 -0
  14. package/dist/chunk-NQX3KBPW.mjs.map +1 -0
  15. package/dist/chunk-PCFPJ47T.mjs +28 -0
  16. package/dist/chunk-PCFPJ47T.mjs.map +1 -0
  17. package/dist/cli.mjs +10 -7
  18. package/dist/cli.mjs.map +1 -1
  19. package/dist/core-CIn0-z-Y.d.ts +42 -0
  20. package/dist/core-D8zVNPDb.d.ts +58 -0
  21. package/dist/identity/index.d.ts +25 -46
  22. package/dist/identity/index.mjs +76 -1
  23. package/dist/identity/index.mjs.map +1 -1
  24. package/dist/pairing/browser.d.ts +18 -0
  25. package/dist/pairing/browser.mjs +6 -0
  26. package/dist/pairing/browser.mjs.map +1 -0
  27. package/dist/pairing/index.d.ts +45 -440
  28. package/dist/pairing/index.mjs +33 -465
  29. package/dist/pairing/index.mjs.map +1 -1
  30. package/dist/provision/index.mjs +5 -2
  31. package/dist/provision/recipe/index.mjs +6 -3
  32. package/dist/seal/index.d.ts +21 -31
  33. package/dist/seal/index.mjs +4 -1
  34. package/dist/types-B2ZfEhsU.d.ts +57 -0
  35. package/dist/webcrypto-BU57p0_g.d.ts +652 -0
  36. package/package.json +8 -2
  37. package/dist/chunk-2DL6W33G.mjs +0 -126
  38. package/dist/chunk-2DL6W33G.mjs.map +0 -1
  39. package/dist/chunk-MVHOJPML.mjs +0 -132
  40. package/dist/chunk-MVHOJPML.mjs.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentproto/secrets",
3
- "version": "0.2.6",
3
+ "version": "1.0.0",
4
4
  "description": "@agentproto/secrets — AIP-19 SECRETS.md reference implementation. A workspace-level manifest format for declaring secret slugs, their purpose, access grants, and audit metadata — without ever storing the values themselves. Hosts resolve slugs against a real vault at reveal time.",
5
5
  "keywords": [
6
6
  "agentproto",
@@ -55,6 +55,11 @@
55
55
  "import": "./dist/pairing/index.mjs",
56
56
  "default": "./dist/pairing/index.mjs"
57
57
  },
58
+ "./pairing/browser": {
59
+ "types": "./dist/pairing/browser.d.ts",
60
+ "import": "./dist/pairing/browser.mjs",
61
+ "default": "./dist/pairing/browser.mjs"
62
+ },
58
63
  "./provision": {
59
64
  "types": "./dist/provision/index.d.ts",
60
65
  "import": "./dist/provision/index.mjs",
@@ -82,11 +87,12 @@
82
87
  "dependencies": {
83
88
  "gray-matter": "^4.0.3",
84
89
  "zod": "^4.6.5",
85
- "@agentproto/auth": "1.0.3",
90
+ "@agentproto/auth": "1.1.0",
86
91
  "@agentproto/define-doctype": "0.1.2"
87
92
  },
88
93
  "devDependencies": {
89
94
  "@types/node": "^25.9.5",
95
+ "esbuild": "^0.28.2",
90
96
  "tsup": "^8.5.1",
91
97
  "typescript": "^5.9.3",
92
98
  "vitest": "^3.2.7",
@@ -1,126 +0,0 @@
1
- import { generateKeyPairSync, createHash, createPrivateKey, sign, createPublicKey, verify } from 'crypto';
2
- import { readFile, mkdir, writeFile, chmod, rename } from 'fs/promises';
3
- import { dirname, join, basename } from 'path';
4
-
5
- /**
6
- * @agentproto/secrets v0.1.0-alpha
7
- * AIP-19 SECRETS.md `defineSecrets` reference implementation.
8
- */
9
-
10
- var IDENTITY_VERSION = 1;
11
- var IdentityError = class extends Error {
12
- constructor(message) {
13
- super(message);
14
- this.name = "IdentityError";
15
- }
16
- };
17
- function generateIdentity() {
18
- const x = generateKeyPairSync("x25519");
19
- const ed = generateKeyPairSync("ed25519");
20
- return {
21
- v: IDENTITY_VERSION,
22
- x25519: {
23
- pub: x.publicKey.export({ type: "spki", format: "der" }).toString("base64"),
24
- priv: x.privateKey.export({ type: "pkcs8", format: "der" }).toString("base64")
25
- },
26
- ed25519: {
27
- pub: ed.publicKey.export({ type: "spki", format: "der" }).toString("base64"),
28
- priv: ed.privateKey.export({ type: "pkcs8", format: "der" }).toString("base64")
29
- },
30
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
31
- };
32
- }
33
- function identityFingerprint(x25519Pub) {
34
- return createHash("sha256").update(Buffer.from(x25519Pub, "base64")).digest("hex").slice(0, 16);
35
- }
36
- function isEnoent(err) {
37
- return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
38
- }
39
- function isRecord(v) {
40
- return typeof v === "object" && v !== null;
41
- }
42
- function isKeyPair(kp) {
43
- return isRecord(kp) && typeof kp["pub"] === "string" && typeof kp["priv"] === "string";
44
- }
45
- function isDaemonIdentity(value) {
46
- if (!isRecord(value)) return false;
47
- if (value["v"] !== IDENTITY_VERSION) return false;
48
- if (typeof value["createdAt"] !== "string") return false;
49
- return isKeyPair(value["x25519"]) && isKeyPair(value["ed25519"]);
50
- }
51
- async function loadOrCreateIdentity(filePath) {
52
- try {
53
- const raw = await readFile(filePath, "utf8");
54
- let parsed;
55
- try {
56
- parsed = JSON.parse(raw);
57
- } catch {
58
- throw new IdentityError(
59
- `identity file ${filePath} is not valid JSON \u2014 refusing to overwrite; move it aside to regenerate`
60
- );
61
- }
62
- if (!isDaemonIdentity(parsed)) {
63
- throw new IdentityError(
64
- `identity file ${filePath} is malformed or an unsupported version \u2014 refusing to overwrite; move it aside to regenerate`
65
- );
66
- }
67
- return parsed;
68
- } catch (err) {
69
- if (!isEnoent(err)) throw err;
70
- }
71
- const identity = generateIdentity();
72
- await persistIdentity(filePath, identity);
73
- return identity;
74
- }
75
- async function persistIdentity(filePath, identity) {
76
- const dir = dirname(filePath);
77
- await mkdir(dir, { recursive: true });
78
- const tmp = join(dir, `.${basename(filePath)}.tmp-${process.pid}`);
79
- await writeFile(tmp, JSON.stringify(identity, null, 2) + "\n", {
80
- encoding: "utf8",
81
- mode: 384
82
- });
83
- await chmod(tmp, 384).catch(() => {
84
- });
85
- await rename(tmp, filePath);
86
- }
87
- function signTranscript(ed25519Priv, transcript) {
88
- let priv;
89
- try {
90
- priv = createPrivateKey({
91
- key: Buffer.from(ed25519Priv, "base64"),
92
- format: "der",
93
- type: "pkcs8"
94
- });
95
- } catch {
96
- throw new IdentityError("invalid ed25519 private key");
97
- }
98
- return sign(null, Buffer.from(transcript), priv).toString("base64");
99
- }
100
- function verifyTranscript(ed25519Pub, transcript, signature) {
101
- let pub;
102
- try {
103
- pub = createPublicKey({
104
- key: Buffer.from(ed25519Pub, "base64"),
105
- format: "der",
106
- type: "spki"
107
- });
108
- } catch {
109
- throw new IdentityError("invalid ed25519 public key");
110
- }
111
- let sig;
112
- try {
113
- sig = Buffer.from(signature, "base64");
114
- } catch {
115
- return false;
116
- }
117
- try {
118
- return verify(null, Buffer.from(transcript), pub, sig);
119
- } catch {
120
- return false;
121
- }
122
- }
123
-
124
- export { IDENTITY_VERSION, IdentityError, generateIdentity, identityFingerprint, loadOrCreateIdentity, signTranscript, verifyTranscript };
125
- //# sourceMappingURL=chunk-2DL6W33G.mjs.map
126
- //# sourceMappingURL=chunk-2DL6W33G.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/identity/index.ts"],"names":["edSign","edVerify"],"mappings":"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","file":"chunk-2DL6W33G.mjs","sourcesContent":["/**\n * @agentproto/secrets/identity — the daemon's persistent cryptographic\n * identity for E2E pairing (design: PAIRING / DESIGN §1).\n *\n * A daemon that accepts pairings needs a stable identity a client can pin:\n * - an **X25519** keypair for key agreement (the client seals its hello to\n * this key, and it is one of the two ECDH inputs to the session key), and\n * - an **Ed25519** keypair for authenticity (the daemon signs the handshake\n * transcript so the client — who learned the public halves out-of-band via\n * the offer URL — can prove it is really talking to the daemon it scanned,\n * not an evil rendezvous in the middle).\n *\n * Both keypairs are stored base64-DER (SPKI public / PKCS8 private), the exact\n * shape `@agentproto/secrets/seal` already uses, so the identity file travels\n * as plain JSON and the seal box can consume the X25519 public half verbatim.\n *\n * Everything here is `node:crypto` — X25519, Ed25519, SHA-256 — so the package\n * keeps its zero-native-dependency footprint. The private halves never leave\n * the identity file (mode 0600) and are never logged.\n */\n\nimport {\n generateKeyPairSync,\n createPublicKey,\n createPrivateKey,\n createHash,\n sign as edSign,\n verify as edVerify,\n} from \"node:crypto\"\nimport { mkdir, readFile, writeFile, chmod, rename } from \"node:fs/promises\"\nimport { dirname, join, basename } from \"node:path\"\n\n/** Current identity-file schema version. */\nexport const IDENTITY_VERSION = 1 as const\n\n/** Raised for every identity load/generate/sign/verify failure. The message\n * is safe to surface — it never contains private key material. */\nexport class IdentityError extends Error {\n constructor(message: string) {\n super(message)\n this.name = \"IdentityError\"\n }\n}\n\n/** One keypair, both halves base64-DER. Public = SPKI, private = PKCS8. */\nexport interface IdentityKeyPair {\n /** base64 DER (SPKI) public key — publishable. */\n pub: string\n /** base64 DER (PKCS8) private key — secret. */\n priv: string\n}\n\n/**\n * A daemon's persistent identity. Serialized verbatim to\n * `~/.agentproto/identity.json` (0600). The `x25519` public half is the key a\n * client seals its hello to; the `ed25519` public half is the key a client\n * verifies the daemon's transcript signature against. Both public halves are\n * carried in the offer URL.\n */\nexport interface DaemonIdentity {\n v: typeof IDENTITY_VERSION\n /** Encryption / key-agreement keypair. */\n x25519: IdentityKeyPair\n /** Signing / authenticity keypair. */\n ed25519: IdentityKeyPair\n /** ISO-8601 creation timestamp. */\n createdAt: string\n}\n\n/**\n * Mint a fresh daemon identity: one X25519 keypair (encryption) and one\n * Ed25519 keypair (signing). Called lazily on first `pair offer`.\n */\nexport function generateIdentity(): DaemonIdentity {\n const x = generateKeyPairSync(\"x25519\")\n const ed = generateKeyPairSync(\"ed25519\")\n return {\n v: IDENTITY_VERSION,\n x25519: {\n pub: x.publicKey.export({ type: \"spki\", format: \"der\" }).toString(\"base64\"),\n priv: x.privateKey.export({ type: \"pkcs8\", format: \"der\" }).toString(\"base64\"),\n },\n ed25519: {\n pub: ed.publicKey.export({ type: \"spki\", format: \"der\" }).toString(\"base64\"),\n priv: ed.privateKey.export({ type: \"pkcs8\", format: \"der\" }).toString(\"base64\"),\n },\n createdAt: new Date().toISOString(),\n }\n}\n\n/**\n * Stable short identifier for a daemon, derived from its X25519 public key —\n * the same construction as `sealKeyId` in `@agentproto/secrets/seal` (first 16\n * hex of `sha256(pub DER)`). Displayed everywhere a human confirms identity\n * (offer QR, `pair accept`, `pair ls`). Not a secret.\n */\nexport function identityFingerprint(x25519Pub: string): string {\n return createHash(\"sha256\")\n .update(Buffer.from(x25519Pub, \"base64\"))\n .digest(\"hex\")\n .slice(0, 16)\n}\n\n/** True for a filesystem \"no such file\" error, without an unchecked cast. */\nfunction isEnoent(err: unknown): boolean {\n return (\n typeof err === \"object\" &&\n err !== null &&\n \"code\" in err &&\n err.code === \"ENOENT\"\n )\n}\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null\n}\n\nfunction isKeyPair(kp: unknown): boolean {\n return isRecord(kp) && typeof kp[\"pub\"] === \"string\" && typeof kp[\"priv\"] === \"string\"\n}\n\nfunction isDaemonIdentity(value: unknown): value is DaemonIdentity {\n if (!isRecord(value)) return false\n if (value[\"v\"] !== IDENTITY_VERSION) return false\n if (typeof value[\"createdAt\"] !== \"string\") return false\n return isKeyPair(value[\"x25519\"]) && isKeyPair(value[\"ed25519\"])\n}\n\n/**\n * Load the daemon identity from `filePath`, creating it on first use. The\n * caller supplies the path (typically `~/.agentproto/identity.json`) so this\n * module stays free of any home-dir policy.\n *\n * The private halves are secret, so a freshly created file is written 0600 and\n * atomically (temp file in the same directory + `rename`, so a crash mid-write\n * can never leave a half-written identity or a world-readable window). A\n * malformed existing file is a hard error — never silently overwritten, since\n * that would rotate the daemon's identity and orphan every existing pairing.\n */\nexport async function loadOrCreateIdentity(\n filePath: string\n): Promise<DaemonIdentity> {\n try {\n const raw = await readFile(filePath, \"utf8\")\n let parsed: unknown\n try {\n parsed = JSON.parse(raw)\n } catch {\n throw new IdentityError(\n `identity file ${filePath} is not valid JSON — refusing to overwrite; ` +\n `move it aside to regenerate`\n )\n }\n if (!isDaemonIdentity(parsed)) {\n throw new IdentityError(\n `identity file ${filePath} is malformed or an unsupported version — ` +\n `refusing to overwrite; move it aside to regenerate`\n )\n }\n return parsed\n } catch (err) {\n if (!isEnoent(err)) throw err\n // ENOENT → first run; fall through to lazy creation below.\n }\n\n const identity = generateIdentity()\n await persistIdentity(filePath, identity)\n return identity\n}\n\n/** Atomic 0600 write: serialize to a sibling temp file, chmod it before it\n * holds any secret bytes, then rename over the target. */\nasync function persistIdentity(\n filePath: string,\n identity: DaemonIdentity\n): Promise<void> {\n const dir = dirname(filePath)\n await mkdir(dir, { recursive: true })\n const tmp = join(dir, `.${basename(filePath)}.tmp-${process.pid}`)\n await writeFile(tmp, JSON.stringify(identity, null, 2) + \"\\n\", {\n encoding: \"utf8\",\n mode: 0o600,\n })\n // Belt-and-suspenders: writeFile's mode is subject to umask on some\n // platforms, so tighten explicitly before the file is visible at its\n // final name. Windows / mounts that ignore chmod fall back to the\n // already-private user profile dir.\n await chmod(tmp, 0o600).catch(() => {})\n await rename(tmp, filePath)\n}\n\n/**\n * Sign a handshake transcript with the daemon's Ed25519 private key. Returns a\n * base64 signature. `transcript` is the exact bytes both sides agree on\n * (`sha256(e_pub ‖ ct₀ ‖ d_e_pub)`); signing it — not the raw messages — is\n * what binds the daemon's authenticity to the whole exchange.\n */\nexport function signTranscript(\n ed25519Priv: string,\n transcript: Uint8Array\n): string {\n let priv\n try {\n priv = createPrivateKey({\n key: Buffer.from(ed25519Priv, \"base64\"),\n format: \"der\",\n type: \"pkcs8\",\n })\n } catch {\n throw new IdentityError(\"invalid ed25519 private key\")\n }\n // Ed25519 takes a null digest algorithm — it hashes internally.\n return edSign(null, Buffer.from(transcript), priv).toString(\"base64\")\n}\n\n/**\n * Verify a transcript signature against a daemon's Ed25519 public key. Returns\n * a boolean — never throws on a bad signature (only on a structurally invalid\n * key), so callers branch on the result rather than a control-flow exception.\n */\nexport function verifyTranscript(\n ed25519Pub: string,\n transcript: Uint8Array,\n signature: string\n): boolean {\n let pub\n try {\n pub = createPublicKey({\n key: Buffer.from(ed25519Pub, \"base64\"),\n format: \"der\",\n type: \"spki\",\n })\n } catch {\n throw new IdentityError(\"invalid ed25519 public key\")\n }\n let sig: Buffer\n try {\n sig = Buffer.from(signature, \"base64\")\n } catch {\n return false\n }\n try {\n return edVerify(null, Buffer.from(transcript), pub, sig)\n } catch {\n return false\n }\n}\n"]}
@@ -1,132 +0,0 @@
1
- import { generateKeyPairSync, createPrivateKey, createPublicKey, createHash, diffieHellman, randomBytes, createCipheriv, createDecipheriv, hkdfSync } from 'crypto';
2
-
3
- /**
4
- * @agentproto/secrets v0.1.0-alpha
5
- * AIP-19 SECRETS.md `defineSecrets` reference implementation.
6
- */
7
-
8
- var SEAL_ALG = "x25519-hkdf-sha256-aes256gcm";
9
- var SEAL_VERSION = 1;
10
- var HKDF_INFO = "agentproto/secrets/seal v1";
11
- var SealError = class extends Error {
12
- constructor(message) {
13
- super(message);
14
- this.name = "SealError";
15
- }
16
- };
17
- function generateSealKeyPair() {
18
- const { publicKey, privateKey } = generateKeyPairSync("x25519");
19
- return {
20
- publicKey: publicKey.export({ type: "spki", format: "der" }).toString("base64"),
21
- privateKey: privateKey.export({ type: "pkcs8", format: "der" }).toString("base64")
22
- };
23
- }
24
- function sealingPublicKey(privateKey) {
25
- let priv;
26
- try {
27
- priv = createPrivateKey({
28
- key: Buffer.from(privateKey, "base64"),
29
- format: "der",
30
- type: "pkcs8"
31
- });
32
- } catch {
33
- throw new SealError("invalid private key");
34
- }
35
- return createPublicKey(priv).export({ type: "spki", format: "der" }).toString("base64");
36
- }
37
- function sealKeyId(publicKey) {
38
- return createHash("sha256").update(Buffer.from(publicKey, "base64")).digest("hex").slice(0, 16);
39
- }
40
- function deriveKey(shared, epkDer, rpkDer) {
41
- const salt = Buffer.concat([epkDer, rpkDer]);
42
- return Buffer.from(hkdfSync("sha256", shared, salt, HKDF_INFO, 32));
43
- }
44
- function seal(plaintext, recipientPublicKey) {
45
- let recipient;
46
- try {
47
- recipient = createPublicKey({
48
- key: Buffer.from(recipientPublicKey, "base64"),
49
- format: "der",
50
- type: "spki"
51
- });
52
- } catch {
53
- throw new SealError("invalid recipient public key");
54
- }
55
- const ephemeral = generateKeyPairSync("x25519");
56
- const shared = diffieHellman({
57
- privateKey: ephemeral.privateKey,
58
- publicKey: recipient
59
- });
60
- const epkDer = ephemeral.publicKey.export({ type: "spki", format: "der" });
61
- const rpkDer = recipient.export({ type: "spki", format: "der" });
62
- const key = deriveKey(shared, epkDer, rpkDer);
63
- const iv = randomBytes(12);
64
- const cipher = createCipheriv("aes-256-gcm", key, iv);
65
- const pt = typeof plaintext === "string" ? Buffer.from(plaintext, "utf8") : Buffer.from(plaintext);
66
- const ct = Buffer.concat([cipher.update(pt), cipher.final()]);
67
- const tag = cipher.getAuthTag();
68
- const envelope = {
69
- v: SEAL_VERSION,
70
- alg: SEAL_ALG,
71
- epk: epkDer.toString("base64"),
72
- iv: iv.toString("base64"),
73
- ct: ct.toString("base64"),
74
- tag: tag.toString("base64")
75
- };
76
- return Buffer.from(JSON.stringify(envelope), "utf8").toString("base64");
77
- }
78
- function unseal(sealed, privateKey) {
79
- let envelope;
80
- try {
81
- envelope = JSON.parse(Buffer.from(sealed, "base64").toString("utf8"));
82
- } catch {
83
- throw new SealError("malformed sealed envelope");
84
- }
85
- if (envelope.v !== SEAL_VERSION || envelope.alg !== SEAL_ALG) {
86
- throw new SealError(
87
- `unsupported sealed envelope (v=${envelope.v} alg=${envelope.alg})`
88
- );
89
- }
90
- let priv;
91
- try {
92
- priv = createPrivateKey({
93
- key: Buffer.from(privateKey, "base64"),
94
- format: "der",
95
- type: "pkcs8"
96
- });
97
- } catch {
98
- throw new SealError("invalid private key");
99
- }
100
- let ephemeral;
101
- try {
102
- ephemeral = createPublicKey({
103
- key: Buffer.from(envelope.epk, "base64"),
104
- format: "der",
105
- type: "spki"
106
- });
107
- } catch {
108
- throw new SealError("malformed ephemeral key in envelope");
109
- }
110
- const shared = diffieHellman({ privateKey: priv, publicKey: ephemeral });
111
- const rpkDer = createPublicKey(priv).export({ type: "spki", format: "der" });
112
- const key = deriveKey(shared, Buffer.from(envelope.epk, "base64"), rpkDer);
113
- const decipher = createDecipheriv(
114
- "aes-256-gcm",
115
- key,
116
- Buffer.from(envelope.iv, "base64")
117
- );
118
- decipher.setAuthTag(Buffer.from(envelope.tag, "base64"));
119
- try {
120
- const pt = Buffer.concat([
121
- decipher.update(Buffer.from(envelope.ct, "base64")),
122
- decipher.final()
123
- ]);
124
- return pt.toString("utf8");
125
- } catch {
126
- throw new SealError("unseal failed \u2014 wrong key or tampered ciphertext");
127
- }
128
- }
129
-
130
- export { SEAL_ALG, SEAL_VERSION, SealError, generateSealKeyPair, seal, sealKeyId, sealingPublicKey, unseal };
131
- //# sourceMappingURL=chunk-MVHOJPML.mjs.map
132
- //# sourceMappingURL=chunk-MVHOJPML.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/seal/index.ts"],"names":[],"mappings":"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","file":"chunk-MVHOJPML.mjs","sourcesContent":["/**\n * @agentproto/secrets/seal — anonymous public-key sealing for secret values.\n *\n * A \"sealed box\": encrypt a secret TO a recipient's public key such that only\n * the holder of the matching private key can open it. The sender needs only\n * the public key, and the sealed envelope reveals nothing about the sender.\n *\n * The point is custody. A secret value (a CLI subscription token, an API key)\n * can be sealed on the machine that holds it, then handed to ANY intermediary\n * — a relay, an agent, a log — as opaque ciphertext, because only the server's\n * private key can recover the plaintext. The plaintext never has to exist\n * anywhere but the origin machine and, transiently, inside the recipient's\n * unseal boundary.\n *\n * Construction (ECIES, Node built-ins only — no native dep):\n * - X25519 ephemeral key agreement with the recipient's public key\n * - HKDF-SHA256 over the shared secret, salted by both public keys\n * - AES-256-GCM AEAD for the payload (authenticated; tampering fails closed)\n *\n * This is the same shape as libsodium's `crypto_box_seal`, expressed in terms\n * of Node's `crypto` so the package keeps its zero-native-dependency footprint.\n *\n * Sealing (confidentiality) is NOT signing (authenticity): this hides the\n * value, it does not prove who sent it. Bind the sender separately (e.g. an\n * authenticated transport) when provenance matters.\n */\n\nimport {\n generateKeyPairSync,\n createPublicKey,\n createPrivateKey,\n createHash,\n diffieHellman,\n hkdfSync,\n randomBytes,\n createCipheriv,\n createDecipheriv,\n} from \"node:crypto\"\n\n/** Versioned algorithm tag embedded in every envelope so the format can\n * evolve without ambiguity at the unseal site. */\nexport const SEAL_ALG = \"x25519-hkdf-sha256-aes256gcm\" as const\nexport const SEAL_VERSION = 1 as const\n\nconst HKDF_INFO = \"agentproto/secrets/seal v1\"\n\n/** Raised for every seal/unseal failure. The message is safe to surface —\n * it never contains key material or plaintext. */\nexport class SealError extends Error {\n constructor(message: string) {\n super(message)\n this.name = \"SealError\"\n }\n}\n\n/** An X25519 keypair for sealing. Both halves are base64-encoded DER so they\n * travel as plain strings (env vars, JSON, config). The public half is safe\n * to publish; the private half must stay with the unseal boundary. */\nexport interface SealKeyPair {\n /** base64 DER (SPKI) X25519 public key — publishable. */\n publicKey: string\n /** base64 DER (PKCS8) X25519 private key — secret. */\n privateKey: string\n}\n\ninterface SealEnvelope {\n v: number\n alg: string\n /** ephemeral public key, base64 DER (SPKI) */\n epk: string\n /** AES-GCM iv, base64 (12 bytes) */\n iv: string\n /** ciphertext, base64 */\n ct: string\n /** AES-GCM auth tag, base64 (16 bytes) */\n tag: string\n}\n\n/**\n * Mint a fresh sealing keypair. The recipient (e.g. a server) generates this\n * once, stores `privateKey` in its own secret store, and publishes\n * `publicKey` for senders to seal against.\n */\nexport function generateSealKeyPair(): SealKeyPair {\n const { publicKey, privateKey } = generateKeyPairSync(\"x25519\")\n return {\n publicKey: publicKey\n .export({ type: \"spki\", format: \"der\" })\n .toString(\"base64\"),\n privateKey: privateKey\n .export({ type: \"pkcs8\", format: \"der\" })\n .toString(\"base64\"),\n }\n}\n\n/**\n * Derive the publishable public key from a stored private key. Lets a\n * recipient hold only the private half (one secret) and serve the public\n * half on demand — the seal-key a sender needs.\n */\nexport function sealingPublicKey(privateKey: string): string {\n let priv\n try {\n priv = createPrivateKey({\n key: Buffer.from(privateKey, \"base64\"),\n format: \"der\",\n type: \"pkcs8\",\n })\n } catch {\n throw new SealError(\"invalid private key\")\n }\n return createPublicKey(priv)\n .export({ type: \"spki\", format: \"der\" })\n .toString(\"base64\")\n}\n\n/**\n * Stable short identifier for a sealing key, derived from the public key.\n * Lets the seal-key endpoint and the sealed envelope name which key was used\n * so rotation is unambiguous. Not a secret.\n */\nexport function sealKeyId(publicKey: string): string {\n return createHash(\"sha256\")\n .update(Buffer.from(publicKey, \"base64\"))\n .digest(\"hex\")\n .slice(0, 16)\n}\n\n// The key-derivation salt binds the symmetric key to BOTH the ephemeral and\n// the recipient public key (sealed-box style) so a derived key is unique to a\n// single (ephemeral, recipient) pair and can't be transplanted.\nfunction deriveKey(shared: Buffer, epkDer: Buffer, rpkDer: Buffer): Buffer {\n const salt = Buffer.concat([epkDer, rpkDer])\n return Buffer.from(hkdfSync(\"sha256\", shared, salt, HKDF_INFO, 32))\n}\n\n/**\n * Seal a plaintext value to a recipient's public key. Returns a single\n * base64 string (the envelope) that only the matching private key can open.\n * The sender needs nothing but the public key.\n */\nexport function seal(\n plaintext: string | Uint8Array,\n recipientPublicKey: string\n): string {\n let recipient\n try {\n recipient = createPublicKey({\n key: Buffer.from(recipientPublicKey, \"base64\"),\n format: \"der\",\n type: \"spki\",\n })\n } catch {\n throw new SealError(\"invalid recipient public key\")\n }\n const ephemeral = generateKeyPairSync(\"x25519\")\n const shared = diffieHellman({\n privateKey: ephemeral.privateKey,\n publicKey: recipient,\n })\n const epkDer = ephemeral.publicKey.export({ type: \"spki\", format: \"der\" })\n const rpkDer = recipient.export({ type: \"spki\", format: \"der\" })\n const key = deriveKey(shared, epkDer, rpkDer)\n\n const iv = randomBytes(12)\n const cipher = createCipheriv(\"aes-256-gcm\", key, iv)\n const pt =\n typeof plaintext === \"string\"\n ? Buffer.from(plaintext, \"utf8\")\n : Buffer.from(plaintext)\n const ct = Buffer.concat([cipher.update(pt), cipher.final()])\n const tag = cipher.getAuthTag()\n\n const envelope: SealEnvelope = {\n v: SEAL_VERSION,\n alg: SEAL_ALG,\n epk: epkDer.toString(\"base64\"),\n iv: iv.toString(\"base64\"),\n ct: ct.toString(\"base64\"),\n tag: tag.toString(\"base64\"),\n }\n return Buffer.from(JSON.stringify(envelope), \"utf8\").toString(\"base64\")\n}\n\n/**\n * Open a sealed envelope with the recipient's private key, returning the\n * original UTF-8 plaintext. Throws `SealError` on a malformed envelope, an\n * unsupported version/alg, the wrong key, or any tampering (the AEAD tag\n * fails closed — a modified ciphertext never decrypts to garbage, it throws).\n */\nexport function unseal(sealed: string, privateKey: string): string {\n let envelope: SealEnvelope\n try {\n envelope = JSON.parse(Buffer.from(sealed, \"base64\").toString(\"utf8\"))\n } catch {\n throw new SealError(\"malformed sealed envelope\")\n }\n if (envelope.v !== SEAL_VERSION || envelope.alg !== SEAL_ALG) {\n throw new SealError(\n `unsupported sealed envelope (v=${envelope.v} alg=${envelope.alg})`\n )\n }\n\n let priv\n try {\n priv = createPrivateKey({\n key: Buffer.from(privateKey, \"base64\"),\n format: \"der\",\n type: \"pkcs8\",\n })\n } catch {\n throw new SealError(\"invalid private key\")\n }\n let ephemeral\n try {\n ephemeral = createPublicKey({\n key: Buffer.from(envelope.epk, \"base64\"),\n format: \"der\",\n type: \"spki\",\n })\n } catch {\n throw new SealError(\"malformed ephemeral key in envelope\")\n }\n\n const shared = diffieHellman({ privateKey: priv, publicKey: ephemeral })\n const rpkDer = createPublicKey(priv).export({ type: \"spki\", format: \"der\" })\n const key = deriveKey(shared, Buffer.from(envelope.epk, \"base64\"), rpkDer)\n\n const decipher = createDecipheriv(\n \"aes-256-gcm\",\n key,\n Buffer.from(envelope.iv, \"base64\")\n )\n decipher.setAuthTag(Buffer.from(envelope.tag, \"base64\"))\n try {\n const pt = Buffer.concat([\n decipher.update(Buffer.from(envelope.ct, \"base64\")),\n decipher.final(),\n ])\n return pt.toString(\"utf8\")\n } catch {\n throw new SealError(\"unseal failed — wrong key or tampered ciphertext\")\n }\n}\n"]}