agentlock-shared 0.2.0 → 0.3.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/dist/__tests__/billing.test.d.ts +2 -0
- package/dist/__tests__/billing.test.d.ts.map +1 -0
- package/dist/__tests__/billing.test.js +31 -0
- package/dist/__tests__/billing.test.js.map +1 -0
- package/dist/__tests__/dns-pinning.test.d.ts +2 -0
- package/dist/__tests__/dns-pinning.test.d.ts.map +1 -0
- package/dist/__tests__/dns-pinning.test.js +33 -0
- package/dist/__tests__/dns-pinning.test.js.map +1 -0
- package/dist/__tests__/llm-classifier-cache-store.test.d.ts +2 -0
- package/dist/__tests__/llm-classifier-cache-store.test.d.ts.map +1 -0
- package/dist/__tests__/llm-classifier-cache-store.test.js +65 -0
- package/dist/__tests__/llm-classifier-cache-store.test.js.map +1 -0
- package/dist/__tests__/llm-classifier-cache.test.d.ts +2 -0
- package/dist/__tests__/llm-classifier-cache.test.d.ts.map +1 -0
- package/dist/__tests__/llm-classifier-cache.test.js +44 -0
- package/dist/__tests__/llm-classifier-cache.test.js.map +1 -0
- package/dist/__tests__/llm-classifier.test.d.ts +2 -0
- package/dist/__tests__/llm-classifier.test.d.ts.map +1 -0
- package/dist/__tests__/llm-classifier.test.js +167 -0
- package/dist/__tests__/llm-classifier.test.js.map +1 -0
- package/dist/__tests__/plans-classifier-limits.test.d.ts +2 -0
- package/dist/__tests__/plans-classifier-limits.test.d.ts.map +1 -0
- package/dist/__tests__/plans-classifier-limits.test.js +22 -0
- package/dist/__tests__/plans-classifier-limits.test.js.map +1 -0
- package/dist/__tests__/policy-category-floor.test.d.ts +2 -0
- package/dist/__tests__/policy-category-floor.test.d.ts.map +1 -0
- package/dist/__tests__/policy-category-floor.test.js +46 -0
- package/dist/__tests__/policy-category-floor.test.js.map +1 -0
- package/dist/__tests__/policy-claude-bash.test.d.ts +2 -0
- package/dist/__tests__/policy-claude-bash.test.d.ts.map +1 -0
- package/dist/__tests__/policy-claude-bash.test.js +401 -0
- package/dist/__tests__/policy-claude-bash.test.js.map +1 -0
- package/dist/__tests__/policy-llm-floor.test.d.ts +2 -0
- package/dist/__tests__/policy-llm-floor.test.d.ts.map +1 -0
- package/dist/__tests__/policy-llm-floor.test.js +107 -0
- package/dist/__tests__/policy-llm-floor.test.js.map +1 -0
- package/dist/__tests__/policy-ssh-e2e.test.d.ts +2 -0
- package/dist/__tests__/policy-ssh-e2e.test.d.ts.map +1 -0
- package/dist/__tests__/policy-ssh-e2e.test.js +89 -0
- package/dist/__tests__/policy-ssh-e2e.test.js.map +1 -0
- package/dist/__tests__/policy-ssh-sessions.test.d.ts +2 -0
- package/dist/__tests__/policy-ssh-sessions.test.d.ts.map +1 -0
- package/dist/__tests__/policy-ssh-sessions.test.js +139 -0
- package/dist/__tests__/policy-ssh-sessions.test.js.map +1 -0
- package/dist/__tests__/policy-ssh.test.d.ts +2 -0
- package/dist/__tests__/policy-ssh.test.d.ts.map +1 -0
- package/dist/__tests__/policy-ssh.test.js +180 -0
- package/dist/__tests__/policy-ssh.test.js.map +1 -0
- package/dist/__tests__/policy.test.js +400 -2
- package/dist/__tests__/policy.test.js.map +1 -1
- package/dist/__tests__/redact.test.js +76 -0
- package/dist/__tests__/redact.test.js.map +1 -1
- package/dist/__tests__/signing.test.js +89 -0
- package/dist/__tests__/signing.test.js.map +1 -1
- package/dist/__tests__/ssh-fingerprint.test.d.ts +2 -0
- package/dist/__tests__/ssh-fingerprint.test.d.ts.map +1 -0
- package/dist/__tests__/ssh-fingerprint.test.js +19 -0
- package/dist/__tests__/ssh-fingerprint.test.js.map +1 -0
- package/dist/__tests__/vpn-route.test.d.ts +2 -0
- package/dist/__tests__/vpn-route.test.d.ts.map +1 -0
- package/dist/__tests__/vpn-route.test.js +72 -0
- package/dist/__tests__/vpn-route.test.js.map +1 -0
- package/dist/__tests__/wireguard.test.d.ts +2 -0
- package/dist/__tests__/wireguard.test.d.ts.map +1 -0
- package/dist/__tests__/wireguard.test.js +114 -0
- package/dist/__tests__/wireguard.test.js.map +1 -0
- package/dist/billing.d.ts +12 -0
- package/dist/billing.d.ts.map +1 -0
- package/dist/billing.js +41 -0
- package/dist/billing.js.map +1 -0
- package/dist/crypto.d.ts +5 -0
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +80 -23
- package/dist/crypto.js.map +1 -1
- package/dist/dns-pinning.d.ts +28 -0
- package/dist/dns-pinning.d.ts.map +1 -0
- package/dist/dns-pinning.js +113 -0
- package/dist/dns-pinning.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/llm-classifier-cache-store.d.ts +49 -0
- package/dist/llm-classifier-cache-store.d.ts.map +1 -0
- package/dist/llm-classifier-cache-store.js +63 -0
- package/dist/llm-classifier-cache-store.js.map +1 -0
- package/dist/llm-classifier-cache.d.ts +6 -0
- package/dist/llm-classifier-cache.d.ts.map +1 -0
- package/dist/llm-classifier-cache.js +52 -0
- package/dist/llm-classifier-cache.js.map +1 -0
- package/dist/llm-classifier.d.ts +29 -0
- package/dist/llm-classifier.d.ts.map +1 -0
- package/dist/llm-classifier.js +191 -0
- package/dist/llm-classifier.js.map +1 -0
- package/dist/observability.d.ts +36 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +75 -0
- package/dist/observability.js.map +1 -0
- package/dist/plans.d.ts +17 -0
- package/dist/plans.d.ts.map +1 -1
- package/dist/plans.js +36 -14
- package/dist/plans.js.map +1 -1
- package/dist/policy.d.ts +173 -3
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +910 -42
- package/dist/policy.js.map +1 -1
- package/dist/redact.d.ts.map +1 -1
- package/dist/redact.js +83 -3
- package/dist/redact.js.map +1 -1
- package/dist/regex-safety.d.ts +21 -0
- package/dist/regex-safety.d.ts.map +1 -0
- package/dist/regex-safety.js +49 -0
- package/dist/regex-safety.js.map +1 -0
- package/dist/sanitize.d.ts +31 -0
- package/dist/sanitize.d.ts.map +1 -0
- package/dist/sanitize.js +54 -0
- package/dist/sanitize.js.map +1 -0
- package/dist/schemas.d.ts +202 -10
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +91 -1
- package/dist/schemas.js.map +1 -1
- package/dist/signing.d.ts +15 -0
- package/dist/signing.d.ts.map +1 -1
- package/dist/signing.js +53 -4
- package/dist/signing.js.map +1 -1
- package/dist/ssh-fingerprint.d.ts +10 -0
- package/dist/ssh-fingerprint.d.ts.map +1 -0
- package/dist/ssh-fingerprint.js +52 -0
- package/dist/ssh-fingerprint.js.map +1 -0
- package/dist/ssrf.d.ts +36 -0
- package/dist/ssrf.d.ts.map +1 -0
- package/dist/ssrf.js +140 -0
- package/dist/ssrf.js.map +1 -0
- package/dist/types.d.ts +130 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/wireguard.d.ts +63 -0
- package/dist/wireguard.d.ts.map +1 -0
- package/dist/wireguard.js +226 -0
- package/dist/wireguard.js.map +1 -0
- package/package.json +42 -29
- package/.turbo/turbo-build.log +0 -4
- package/.turbo/turbo-test.log +0 -76
- package/dist/__tests__/content-crypto.test.d.ts +0 -2
- package/dist/__tests__/content-crypto.test.d.ts.map +0 -1
- package/dist/__tests__/content-crypto.test.js +0 -117
- package/dist/__tests__/content-crypto.test.js.map +0 -1
- package/dist/__tests__/signing.test (# Edit conflict 2026-04-01 z3etfmC #).js +0 -51
- package/dist/__tests__/signing.test.js (# Edit conflict 2026-04-01 4rndy9C #).map +0 -1
- package/dist/content-crypto.d.ts +0 -24
- package/dist/content-crypto.d.ts.map +0 -1
- package/dist/content-crypto.js +0 -58
- package/dist/content-crypto.js.map +0 -1
- package/src/__tests__/crypto.test.ts +0 -169
- package/src/__tests__/messaging.test.ts +0 -83
- package/src/__tests__/policy.test.ts +0 -222
- package/src/__tests__/redact.test.ts +0 -41
- package/src/__tests__/signing.test.ts +0 -55
- package/src/crypto.ts +0 -235
- package/src/index.ts +0 -8
- package/src/mcp-catalog.ts +0 -181
- package/src/plans.ts +0 -116
- package/src/policy.ts +0 -216
- package/src/redact.ts +0 -131
- package/src/schemas.ts +0 -121
- package/src/signing.ts +0 -120
- package/src/types.ts +0 -213
- package/test-gateway.mjs +0 -47
- package/tsconfig.json +0 -10
- package/vitest.config.ts +0 -8
package/dist/content-crypto.js
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.PAYLOAD_VERSION_WCK = exports.PAYLOAD_VERSION_LEGACY = void 0;
|
|
7
|
-
exports.generateWCK = generateWCK;
|
|
8
|
-
exports.wrapKey = wrapKey;
|
|
9
|
-
exports.unwrapKey = unwrapKey;
|
|
10
|
-
exports.deriveKEK = deriveKEK;
|
|
11
|
-
exports.wrapWCKForUser = wrapWCKForUser;
|
|
12
|
-
exports.unwrapWCKFromUser = unwrapWCKFromUser;
|
|
13
|
-
const tweetnacl_1 = __importDefault(require("tweetnacl"));
|
|
14
|
-
const tweetnacl_util_1 = require("tweetnacl-util");
|
|
15
|
-
const scrypt_1 = require("@noble/hashes/scrypt");
|
|
16
|
-
/** Payload encrypted with MASTER_KEY (legacy) */
|
|
17
|
-
exports.PAYLOAD_VERSION_LEGACY = 0;
|
|
18
|
-
/** Payload encrypted with per-workspace WCK */
|
|
19
|
-
exports.PAYLOAD_VERSION_WCK = 1;
|
|
20
|
-
/** Generate a random 32-byte Workspace Content Key */
|
|
21
|
-
function generateWCK() {
|
|
22
|
-
return tweetnacl_1.default.randomBytes(32);
|
|
23
|
-
}
|
|
24
|
-
/** Wrap a WCK using a wrapping key (e.g. MASTER_KEY). Returns base64 ciphertext + nonce. */
|
|
25
|
-
function wrapKey(wck, wrappingKey) {
|
|
26
|
-
const nonce = tweetnacl_1.default.randomBytes(tweetnacl_1.default.secretbox.nonceLength);
|
|
27
|
-
const box = tweetnacl_1.default.secretbox(wck, nonce, wrappingKey);
|
|
28
|
-
return { ciphertext: (0, tweetnacl_util_1.encodeBase64)(box), nonce: (0, tweetnacl_util_1.encodeBase64)(nonce) };
|
|
29
|
-
}
|
|
30
|
-
/** Unwrap a WCK using a wrapping key. Returns the raw 32-byte WCK. */
|
|
31
|
-
function unwrapKey(ciphertext, nonce, wrappingKey) {
|
|
32
|
-
const box = (0, tweetnacl_util_1.decodeBase64)(ciphertext);
|
|
33
|
-
const n = (0, tweetnacl_util_1.decodeBase64)(nonce);
|
|
34
|
-
const result = tweetnacl_1.default.secretbox.open(box, n, wrappingKey);
|
|
35
|
-
if (!result) {
|
|
36
|
-
throw new Error('unwrapKey failed: invalid wrapping key or corrupted data');
|
|
37
|
-
}
|
|
38
|
-
return result;
|
|
39
|
-
}
|
|
40
|
-
/** Derive a Key Encryption Key from a passphrase using scrypt. */
|
|
41
|
-
function deriveKEK(passphrase, salt) {
|
|
42
|
-
const pw = new TextEncoder().encode(passphrase);
|
|
43
|
-
return (0, scrypt_1.scrypt)(pw, salt, { N: 2 ** 15, r: 8, p: 1, dkLen: 32 });
|
|
44
|
-
}
|
|
45
|
-
/** Wrap a WCK for a user using their passphrase. Returns base64 strings. */
|
|
46
|
-
function wrapWCKForUser(wck, passphrase) {
|
|
47
|
-
const salt = tweetnacl_1.default.randomBytes(32);
|
|
48
|
-
const kek = deriveKEK(passphrase, salt);
|
|
49
|
-
const { ciphertext, nonce } = wrapKey(wck, kek);
|
|
50
|
-
return { ciphertext, nonce, salt: (0, tweetnacl_util_1.encodeBase64)(salt) };
|
|
51
|
-
}
|
|
52
|
-
/** Unwrap a WCK using a user's passphrase. Returns the raw 32-byte WCK. */
|
|
53
|
-
function unwrapWCKFromUser(ciphertext, nonce, salt, passphrase) {
|
|
54
|
-
const saltBytes = (0, tweetnacl_util_1.decodeBase64)(salt);
|
|
55
|
-
const kek = deriveKEK(passphrase, saltBytes);
|
|
56
|
-
return unwrapKey(ciphertext, nonce, kek);
|
|
57
|
-
}
|
|
58
|
-
//# sourceMappingURL=content-crypto.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"content-crypto.js","sourceRoot":"","sources":["../src/content-crypto.ts"],"names":[],"mappings":";;;;;;AAUA,kCAEC;AAGD,0BAOC;AAGD,8BAYC;AAGD,8BAMC;AAGD,wCAQC;AAGD,8CASC;AArED,0DAA6B;AAC7B,mDAA4D;AAC5D,iDAA8C;AAE9C,iDAAiD;AACpC,QAAA,sBAAsB,GAAG,CAAC,CAAC;AACxC,+CAA+C;AAClC,QAAA,mBAAmB,GAAG,CAAC,CAAC;AAErC,sDAAsD;AACtD,SAAgB,WAAW;IACzB,OAAO,mBAAI,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;AAC9B,CAAC;AAED,4FAA4F;AAC5F,SAAgB,OAAO,CACrB,GAAe,EACf,WAAuB;IAEvB,MAAM,KAAK,GAAG,mBAAI,CAAC,WAAW,CAAC,mBAAI,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;IAC3D,MAAM,GAAG,GAAG,mBAAI,CAAC,SAAS,CAAC,GAAG,EAAE,KAAK,EAAE,WAAW,CAAC,CAAC;IACpD,OAAO,EAAE,UAAU,EAAE,IAAA,6BAAY,EAAC,GAAG,CAAC,EAAE,KAAK,EAAE,IAAA,6BAAY,EAAC,KAAK,CAAC,EAAE,CAAC;AACvE,CAAC;AAED,sEAAsE;AACtE,SAAgB,SAAS,CACvB,UAAkB,EAClB,KAAa,EACb,WAAuB;IAEvB,MAAM,GAAG,GAAG,IAAA,6BAAY,EAAC,UAAU,CAAC,CAAC;IACrC,MAAM,CAAC,GAAG,IAAA,6BAAY,EAAC,KAAK,CAAC,CAAC;IAC9B,MAAM,MAAM,GAAG,mBAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,WAAW,CAAC,CAAC;IACxD,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,0DAA0D,CAAC,CAAC;IAC9E,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,kEAAkE;AAClE,SAAgB,SAAS,CACvB,UAAkB,EAClB,IAAgB;IAEhB,MAAM,EAAE,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IAChD,OAAO,IAAA,eAAM,EAAC,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;AACjE,CAAC;AAED,4EAA4E;AAC5E,SAAgB,cAAc,CAC5B,GAAe,EACf,UAAkB;IAElB,MAAM,IAAI,GAAG,mBAAI,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;IAClC,MAAM,GAAG,GAAG,SAAS,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;IACxC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IAChD,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,IAAI,EAAE,IAAA,6BAAY,EAAC,IAAI,CAAC,EAAE,CAAC;AACzD,CAAC;AAED,2EAA2E;AAC3E,SAAgB,iBAAiB,CAC/B,UAAkB,EAClB,KAAa,EACb,IAAY,EACZ,UAAkB;IAElB,MAAM,SAAS,GAAG,IAAA,6BAAY,EAAC,IAAI,CAAC,CAAC;IACrC,MAAM,GAAG,GAAG,SAAS,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;IAC7C,OAAO,SAAS,CAAC,UAAU,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;AAC3C,CAAC"}
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import nacl from 'tweetnacl';
|
|
3
|
-
import { encodeBase64 } from 'tweetnacl-util';
|
|
4
|
-
import {
|
|
5
|
-
encrypt,
|
|
6
|
-
decrypt,
|
|
7
|
-
envelopeEncrypt,
|
|
8
|
-
envelopeDecrypt,
|
|
9
|
-
generateKey,
|
|
10
|
-
encryptCredential,
|
|
11
|
-
decryptCredential,
|
|
12
|
-
} from '../crypto.js';
|
|
13
|
-
|
|
14
|
-
// Deterministic test key (not from env -- tests must not depend on MASTER_KEY)
|
|
15
|
-
const testMasterKey = nacl.randomBytes(32);
|
|
16
|
-
|
|
17
|
-
describe('encrypt / decrypt (low-level)', () => {
|
|
18
|
-
it('round-trips a string', () => {
|
|
19
|
-
const data = 'hello world';
|
|
20
|
-
const encrypted = encrypt(data, testMasterKey);
|
|
21
|
-
expect(encrypted).not.toBe(data);
|
|
22
|
-
expect(decrypt(encrypted, testMasterKey)).toBe(data);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('produces different ciphertext each call (random nonce)', () => {
|
|
26
|
-
const data = 'deterministic?';
|
|
27
|
-
const a = encrypt(data, testMasterKey);
|
|
28
|
-
const b = encrypt(data, testMasterKey);
|
|
29
|
-
expect(a).not.toBe(b);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('rejects wrong key', () => {
|
|
33
|
-
const data = 'secret';
|
|
34
|
-
const encrypted = encrypt(data, testMasterKey);
|
|
35
|
-
const wrongKey = nacl.randomBytes(32);
|
|
36
|
-
expect(() => decrypt(encrypted, wrongKey)).toThrow('Decryption failed');
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('handles empty string', () => {
|
|
40
|
-
const encrypted = encrypt('', testMasterKey);
|
|
41
|
-
expect(decrypt(encrypted, testMasterKey)).toBe('');
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('handles unicode', () => {
|
|
45
|
-
const data = 'Hello unicode world';
|
|
46
|
-
const encrypted = encrypt(data, testMasterKey);
|
|
47
|
-
expect(decrypt(encrypted, testMasterKey)).toBe(data);
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
describe('envelopeEncrypt / envelopeDecrypt', () => {
|
|
52
|
-
it('round-trips a string', () => {
|
|
53
|
-
const data = 'envelope test data';
|
|
54
|
-
const encrypted = envelopeEncrypt(data, testMasterKey);
|
|
55
|
-
expect(encrypted).toMatch(/^env1:/);
|
|
56
|
-
expect(envelopeDecrypt(encrypted, testMasterKey)).toBe(data);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('produces different ciphertext each call (fresh DEK + nonce)', () => {
|
|
60
|
-
const data = 'same data';
|
|
61
|
-
const a = envelopeEncrypt(data, testMasterKey);
|
|
62
|
-
const b = envelopeEncrypt(data, testMasterKey);
|
|
63
|
-
expect(a).not.toBe(b);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('rejects wrong master key', () => {
|
|
67
|
-
const data = 'secret';
|
|
68
|
-
const encrypted = envelopeEncrypt(data, testMasterKey);
|
|
69
|
-
const wrongKey = nacl.randomBytes(32);
|
|
70
|
-
expect(() => envelopeDecrypt(encrypted, wrongKey)).toThrow();
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('handles empty string', () => {
|
|
74
|
-
const encrypted = envelopeEncrypt('', testMasterKey);
|
|
75
|
-
expect(envelopeDecrypt(encrypted, testMasterKey)).toBe('');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('handles large data', () => {
|
|
79
|
-
const data = 'x'.repeat(100_000);
|
|
80
|
-
const encrypted = envelopeEncrypt(data, testMasterKey);
|
|
81
|
-
expect(envelopeDecrypt(encrypted, testMasterKey)).toBe(data);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('handles JSON data', () => {
|
|
85
|
-
const obj = { tool: 'http', payload: { url: 'https://example.com' }, nested: [1, 2, 3] };
|
|
86
|
-
const data = JSON.stringify(obj);
|
|
87
|
-
const encrypted = envelopeEncrypt(data, testMasterKey);
|
|
88
|
-
const decrypted = JSON.parse(envelopeDecrypt(encrypted, testMasterKey));
|
|
89
|
-
expect(decrypted).toEqual(obj);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('envelopeDecrypt rejects non-envelope data', () => {
|
|
93
|
-
const legacy = encrypt('legacy data', testMasterKey);
|
|
94
|
-
expect(() => envelopeDecrypt(legacy, testMasterKey)).toThrow('missing env1: prefix');
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('rejects malformed envelope (no separator)', () => {
|
|
98
|
-
expect(() => envelopeDecrypt('env1:nodatahere', testMasterKey)).toThrow('missing DEK/payload separator');
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('rejects malformed envelope (empty parts)', () => {
|
|
102
|
-
// env1:: has withoutPrefix=":" where lastIndexOf(':') returns 0,
|
|
103
|
-
// caught by the separatorIndex <= 0 check
|
|
104
|
-
expect(() => envelopeDecrypt('env1::', testMasterKey)).toThrow('missing DEK/payload separator');
|
|
105
|
-
// env1:abc: has empty payload after separator
|
|
106
|
-
expect(() => envelopeDecrypt('env1:abc:', testMasterKey)).toThrow('empty DEK or payload');
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
describe('decrypt() backward compatibility', () => {
|
|
111
|
-
it('transparently decrypts envelope-encrypted data', () => {
|
|
112
|
-
const data = 'new envelope data';
|
|
113
|
-
const encrypted = envelopeEncrypt(data, testMasterKey);
|
|
114
|
-
// decrypt() should auto-detect the env1: prefix and handle it
|
|
115
|
-
expect(decrypt(encrypted, testMasterKey)).toBe(data);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('still decrypts legacy direct-encrypted data', () => {
|
|
119
|
-
const data = 'legacy direct data';
|
|
120
|
-
const encrypted = encrypt(data, testMasterKey);
|
|
121
|
-
// Legacy data should still work with decrypt()
|
|
122
|
-
expect(decrypt(encrypted, testMasterKey)).toBe(data);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('handles mixed legacy and envelope data in sequence', () => {
|
|
126
|
-
const legacyData = 'old format';
|
|
127
|
-
const envelopeData = 'new format';
|
|
128
|
-
|
|
129
|
-
const legacyEncrypted = encrypt(legacyData, testMasterKey);
|
|
130
|
-
const envelopeEncrypted = envelopeEncrypt(envelopeData, testMasterKey);
|
|
131
|
-
|
|
132
|
-
// Both should decrypt through the same decrypt() function
|
|
133
|
-
expect(decrypt(legacyEncrypted, testMasterKey)).toBe(legacyData);
|
|
134
|
-
expect(decrypt(envelopeEncrypted, testMasterKey)).toBe(envelopeData);
|
|
135
|
-
});
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
describe('encryptCredential / decryptCredential (existing DEK pattern)', () => {
|
|
139
|
-
it('still works correctly after envelope changes', () => {
|
|
140
|
-
const payload = { api_key: 'sk-test-12345', name: 'test-cred' };
|
|
141
|
-
const { encryptedDEK, encryptedPayload } = encryptCredential(payload, testMasterKey);
|
|
142
|
-
const decrypted = decryptCredential(encryptedDEK, encryptedPayload, testMasterKey);
|
|
143
|
-
expect(decrypted).toEqual(payload);
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
describe('envelope format structure', () => {
|
|
148
|
-
it('has exactly the env1:wrappedDEK:payload format', () => {
|
|
149
|
-
const encrypted = envelopeEncrypt('test', testMasterKey);
|
|
150
|
-
const parts = encrypted.split(':');
|
|
151
|
-
// Should be: "env1", wrappedDEK (base64), payload (base64)
|
|
152
|
-
expect(parts[0]).toBe('env1');
|
|
153
|
-
expect(parts.length).toBe(3);
|
|
154
|
-
// Both parts after prefix should be valid base64
|
|
155
|
-
expect(parts[1].length).toBeGreaterThan(0);
|
|
156
|
-
expect(parts[2].length).toBeGreaterThan(0);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('base64 parts do not contain colons', () => {
|
|
160
|
-
// Run multiple times to catch edge cases with different random nonces
|
|
161
|
-
for (let i = 0; i < 20; i++) {
|
|
162
|
-
const encrypted = envelopeEncrypt(`test data ${i}`, testMasterKey);
|
|
163
|
-
const withoutPrefix = encrypted.slice(5); // remove "env1:"
|
|
164
|
-
const colonCount = (withoutPrefix.match(/:/g) || []).length;
|
|
165
|
-
// Exactly one colon separating wrappedDEK and payload
|
|
166
|
-
expect(colonCount).toBe(1);
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
});
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { SendMessageSchema, AgentSendMessageSchema, ApproveRequestSchema } from '../schemas';
|
|
3
|
-
|
|
4
|
-
describe('SendMessageSchema', () => {
|
|
5
|
-
it('accepts valid message with content only', () => {
|
|
6
|
-
const result = SendMessageSchema.safeParse({ content: 'Hello agent' });
|
|
7
|
-
expect(result.success).toBe(true);
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it('accepts message with thread_id and expires_at', () => {
|
|
11
|
-
const result = SendMessageSchema.safeParse({
|
|
12
|
-
content: 'Do this task',
|
|
13
|
-
thread_id: '550e8400-e29b-41d4-a716-446655440000',
|
|
14
|
-
expires_at: '2026-04-01T12:00:00Z',
|
|
15
|
-
});
|
|
16
|
-
expect(result.success).toBe(true);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('rejects empty content', () => {
|
|
20
|
-
const result = SendMessageSchema.safeParse({ content: '' });
|
|
21
|
-
expect(result.success).toBe(false);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('rejects content over 4096 chars', () => {
|
|
25
|
-
const result = SendMessageSchema.safeParse({ content: 'x'.repeat(4097) });
|
|
26
|
-
expect(result.success).toBe(false);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('rejects invalid thread_id', () => {
|
|
30
|
-
const result = SendMessageSchema.safeParse({ content: 'hi', thread_id: 'not-a-uuid' });
|
|
31
|
-
expect(result.success).toBe(false);
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
describe('AgentSendMessageSchema', () => {
|
|
36
|
-
it('requires thread_id', () => {
|
|
37
|
-
const result = AgentSendMessageSchema.safeParse({ content: 'Reply' });
|
|
38
|
-
expect(result.success).toBe(false);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('accepts valid reply', () => {
|
|
42
|
-
const result = AgentSendMessageSchema.safeParse({
|
|
43
|
-
content: 'I found 3 files',
|
|
44
|
-
thread_id: '550e8400-e29b-41d4-a716-446655440000',
|
|
45
|
-
});
|
|
46
|
-
expect(result.success).toBe(true);
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe('ApproveRequestSchema with reply_message', () => {
|
|
51
|
-
it('accepts approve without reply', () => {
|
|
52
|
-
const result = ApproveRequestSchema.safeParse({ action: 'approve' });
|
|
53
|
-
expect(result.success).toBe(true);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('accepts approve with reply_message', () => {
|
|
57
|
-
const result = ApproveRequestSchema.safeParse({
|
|
58
|
-
action: 'approve',
|
|
59
|
-
reply_message: 'Only delete files older than 7 days',
|
|
60
|
-
});
|
|
61
|
-
expect(result.success).toBe(true);
|
|
62
|
-
if (result.success) {
|
|
63
|
-
expect(result.data.reply_message).toBe('Only delete files older than 7 days');
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('accepts deny with reason and reply_message', () => {
|
|
68
|
-
const result = ApproveRequestSchema.safeParse({
|
|
69
|
-
action: 'deny',
|
|
70
|
-
reason: 'Too risky',
|
|
71
|
-
reply_message: 'Try a safer approach',
|
|
72
|
-
});
|
|
73
|
-
expect(result.success).toBe(true);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('rejects reply_message over 2000 chars', () => {
|
|
77
|
-
const result = ApproveRequestSchema.safeParse({
|
|
78
|
-
action: 'approve',
|
|
79
|
-
reply_message: 'x'.repeat(2001),
|
|
80
|
-
});
|
|
81
|
-
expect(result.success).toBe(false);
|
|
82
|
-
});
|
|
83
|
-
});
|
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { evaluatePolicy, DEFAULT_POLICY_RULES } from '../policy.js';
|
|
3
|
-
import type { AgentActionRequest } from '../types.js';
|
|
4
|
-
|
|
5
|
-
describe('Policy Engine', () => {
|
|
6
|
-
it('should ALLOW read actions by default', () => {
|
|
7
|
-
// Use a non-http tool to test the default read policy (http without URL is now correctly blocked)
|
|
8
|
-
const action: AgentActionRequest = { action_type: 'read', tool: 'demo.list', payload: {} };
|
|
9
|
-
expect(evaluatePolicy(action, DEFAULT_POLICY_RULES).decision).toBe('ALLOW');
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it('should REQUIRE_APPROVAL for write actions', () => {
|
|
13
|
-
const action: AgentActionRequest = { action_type: 'write', tool: 'demo', payload: {} };
|
|
14
|
-
expect(evaluatePolicy(action, DEFAULT_POLICY_RULES).decision).toBe('REQUIRE_APPROVAL');
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it('should BLOCK admin actions', () => {
|
|
18
|
-
const action: AgentActionRequest = { action_type: 'admin', tool: 'system', payload: {} };
|
|
19
|
-
expect(evaluatePolicy(action, DEFAULT_POLICY_RULES).decision).toBe('BLOCK');
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('should REQUIRE_APPROVAL for financial actions', () => {
|
|
23
|
-
const action: AgentActionRequest = { action_type: 'financial', tool: 'stripe', payload: {} };
|
|
24
|
-
expect(evaluatePolicy(action, DEFAULT_POLICY_RULES).decision).toBe('REQUIRE_APPROVAL');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('should BLOCK disallowed HTTP domains', () => {
|
|
28
|
-
const action: AgentActionRequest = {
|
|
29
|
-
action_type: 'read',
|
|
30
|
-
tool: 'http',
|
|
31
|
-
payload: { url: 'https://evil.com/data', method: 'GET' },
|
|
32
|
-
};
|
|
33
|
-
const rules = {
|
|
34
|
-
...DEFAULT_POLICY_RULES,
|
|
35
|
-
http: { allowedDomains: ['trusted.com'], allowedMethods: ['GET'], blockList: [] },
|
|
36
|
-
};
|
|
37
|
-
expect(evaluatePolicy(action, rules).decision).toBe('BLOCK');
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('should BLOCK explicitly blocklisted domains', () => {
|
|
41
|
-
const action: AgentActionRequest = {
|
|
42
|
-
action_type: 'read',
|
|
43
|
-
tool: 'http',
|
|
44
|
-
payload: { url: 'https://evil.com/data', method: 'GET' },
|
|
45
|
-
};
|
|
46
|
-
const rules = {
|
|
47
|
-
...DEFAULT_POLICY_RULES,
|
|
48
|
-
http: { allowedDomains: [], allowedMethods: ['GET'], blockList: ['evil.com'] },
|
|
49
|
-
};
|
|
50
|
-
expect(evaluatePolicy(action, rules).decision).toBe('BLOCK');
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('should REQUIRE_APPROVAL for admin actions even when defaultMode is allow', () => {
|
|
54
|
-
const action: AgentActionRequest = { action_type: 'admin', tool: 'system', payload: {} };
|
|
55
|
-
const rules = { ...DEFAULT_POLICY_RULES, defaultMode: 'allow' as const, rules: [] };
|
|
56
|
-
const result = evaluatePolicy(action, rules);
|
|
57
|
-
expect(result.decision).toBe('REQUIRE_APPROVAL');
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('should REQUIRE_APPROVAL for financial actions even when defaultMode is allow', () => {
|
|
61
|
-
const action: AgentActionRequest = { action_type: 'financial', tool: 'stripe', payload: {} };
|
|
62
|
-
const rules = { ...DEFAULT_POLICY_RULES, defaultMode: 'allow' as const, rules: [] };
|
|
63
|
-
const result = evaluatePolicy(action, rules);
|
|
64
|
-
expect(result.decision).toBe('REQUIRE_APPROVAL');
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('should REQUIRE_APPROVAL for admin action even with explicit ALLOW rule', () => {
|
|
68
|
-
const action: AgentActionRequest = { action_type: 'admin', tool: 'admin.delete_user', payload: {} };
|
|
69
|
-
const rules = {
|
|
70
|
-
...DEFAULT_POLICY_RULES,
|
|
71
|
-
defaultMode: 'allow' as const,
|
|
72
|
-
rules: [{ action_type: 'admin' as const, decision: 'ALLOW' as const }],
|
|
73
|
-
};
|
|
74
|
-
const result = evaluatePolicy(action, rules);
|
|
75
|
-
expect(result.decision).toBe('REQUIRE_APPROVAL');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('should REQUIRE_APPROVAL for financial action even with explicit ALLOW rule', () => {
|
|
79
|
-
const action: AgentActionRequest = { action_type: 'financial', tool: 'stripe.charge', payload: {} };
|
|
80
|
-
const rules = {
|
|
81
|
-
...DEFAULT_POLICY_RULES,
|
|
82
|
-
defaultMode: 'allow' as const,
|
|
83
|
-
rules: [{ action_type: 'financial' as const, decision: 'ALLOW' as const }],
|
|
84
|
-
};
|
|
85
|
-
const result = evaluatePolicy(action, rules);
|
|
86
|
-
expect(result.decision).toBe('REQUIRE_APPROVAL');
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('should still respect explicit rules for admin even with defaultMode allow', () => {
|
|
90
|
-
const action: AgentActionRequest = { action_type: 'admin', tool: 'system', payload: {} };
|
|
91
|
-
const rules = {
|
|
92
|
-
...DEFAULT_POLICY_RULES,
|
|
93
|
-
defaultMode: 'allow' as const,
|
|
94
|
-
rules: [{ action_type: 'admin' as const, decision: 'BLOCK' as const }],
|
|
95
|
-
};
|
|
96
|
-
expect(evaluatePolicy(action, rules).decision).toBe('BLOCK');
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('should BLOCK when cost exceeds limit', () => {
|
|
100
|
-
const action: AgentActionRequest = {
|
|
101
|
-
action_type: 'financial',
|
|
102
|
-
tool: 'stripe',
|
|
103
|
-
payload: {},
|
|
104
|
-
cost_estimate: 1000,
|
|
105
|
-
};
|
|
106
|
-
const rules = { ...DEFAULT_POLICY_RULES, limits: { maxCostPerAction: 100 } };
|
|
107
|
-
expect(evaluatePolicy(action, rules).decision).toBe('BLOCK');
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// --- Case-sensitivity bypass tests ---
|
|
111
|
-
|
|
112
|
-
it('should enforce HTTP domain allowlist even with mixed-case tool name', () => {
|
|
113
|
-
const action: AgentActionRequest = {
|
|
114
|
-
action_type: 'read',
|
|
115
|
-
tool: 'Http.get',
|
|
116
|
-
payload: { url: 'https://evil.com/data', method: 'GET' },
|
|
117
|
-
};
|
|
118
|
-
const rules = {
|
|
119
|
-
...DEFAULT_POLICY_RULES,
|
|
120
|
-
http: { allowedDomains: ['trusted.com'], allowedMethods: ['GET'], blockList: [] },
|
|
121
|
-
};
|
|
122
|
-
expect(evaluatePolicy(action, rules).decision).toBe('BLOCK');
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('should match tool-specific rules case-insensitively', () => {
|
|
126
|
-
const action: AgentActionRequest = {
|
|
127
|
-
action_type: 'read',
|
|
128
|
-
tool: 'MCP.list_tools',
|
|
129
|
-
payload: {},
|
|
130
|
-
};
|
|
131
|
-
const result = evaluatePolicy(action, DEFAULT_POLICY_RULES);
|
|
132
|
-
expect(result.decision).toBe('ALLOW');
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// --- URL edge cases ---
|
|
136
|
-
|
|
137
|
-
it('should not match subdomain of blocklisted domain (e.g., notevil.com vs evil.com)', () => {
|
|
138
|
-
const action: AgentActionRequest = {
|
|
139
|
-
action_type: 'read',
|
|
140
|
-
tool: 'http.get',
|
|
141
|
-
payload: { url: 'https://notevil.com/data', method: 'GET' },
|
|
142
|
-
};
|
|
143
|
-
const rules = {
|
|
144
|
-
...DEFAULT_POLICY_RULES,
|
|
145
|
-
http: { allowedDomains: ['notevil.com', 'trusted.com'], allowedMethods: ['GET'], blockList: ['evil.com'] },
|
|
146
|
-
};
|
|
147
|
-
expect(evaluatePolicy(action, rules).decision).not.toBe('BLOCK');
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it('should BLOCK HTTP tool with no URL in payload', () => {
|
|
151
|
-
const action: AgentActionRequest = {
|
|
152
|
-
action_type: 'read',
|
|
153
|
-
tool: 'http.get',
|
|
154
|
-
payload: {},
|
|
155
|
-
};
|
|
156
|
-
const rules = {
|
|
157
|
-
...DEFAULT_POLICY_RULES,
|
|
158
|
-
http: { allowedDomains: ['trusted.com'], allowedMethods: ['GET'], blockList: [] },
|
|
159
|
-
};
|
|
160
|
-
expect(evaluatePolicy(action, rules).decision).toBe('BLOCK');
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
// --- Browser tool policy ---
|
|
164
|
-
|
|
165
|
-
it('should REQUIRE_APPROVAL for browser.open', () => {
|
|
166
|
-
const action: AgentActionRequest = {
|
|
167
|
-
action_type: 'write',
|
|
168
|
-
tool: 'browser.open',
|
|
169
|
-
payload: { url: 'https://example.com' },
|
|
170
|
-
};
|
|
171
|
-
const result = evaluatePolicy(action, DEFAULT_POLICY_RULES);
|
|
172
|
-
expect(result.decision).toBe('REQUIRE_APPROVAL');
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('should BLOCK browser.* actions without a session (reaching policy engine)', () => {
|
|
176
|
-
const action: AgentActionRequest = {
|
|
177
|
-
action_type: 'write',
|
|
178
|
-
tool: 'browser.click',
|
|
179
|
-
payload: {},
|
|
180
|
-
};
|
|
181
|
-
const result = evaluatePolicy(action, DEFAULT_POLICY_RULES);
|
|
182
|
-
expect(result.decision).toBe('BLOCK');
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
// --- Unknown action_type ---
|
|
186
|
-
|
|
187
|
-
it('should BLOCK unknown action types', () => {
|
|
188
|
-
const action = {
|
|
189
|
-
action_type: 'delete' as 'read',
|
|
190
|
-
tool: 'custom.tool',
|
|
191
|
-
payload: {},
|
|
192
|
-
};
|
|
193
|
-
const result = evaluatePolicy(action, DEFAULT_POLICY_RULES);
|
|
194
|
-
expect(result.decision).toBe('BLOCK');
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
// --- cost_estimate omission ---
|
|
198
|
-
|
|
199
|
-
it('should not enforce budget check when cost_estimate is omitted', () => {
|
|
200
|
-
const action: AgentActionRequest = {
|
|
201
|
-
action_type: 'write',
|
|
202
|
-
tool: 'demo',
|
|
203
|
-
payload: {},
|
|
204
|
-
// cost_estimate intentionally omitted
|
|
205
|
-
};
|
|
206
|
-
const rules = { ...DEFAULT_POLICY_RULES, limits: { maxCostPerAction: 100 } };
|
|
207
|
-
// Should match action_type 'write' rule, not be BLOCK-ed by cost limit
|
|
208
|
-
expect(evaluatePolicy(action, rules).decision).toBe('REQUIRE_APPROVAL');
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
// --- HTTP allowlist not configured ---
|
|
212
|
-
|
|
213
|
-
it('should REQUIRE_APPROVAL when HTTP allowlist is empty (safe default)', () => {
|
|
214
|
-
const action: AgentActionRequest = {
|
|
215
|
-
action_type: 'read',
|
|
216
|
-
tool: 'http.get',
|
|
217
|
-
payload: { url: 'https://any-site.com/data', method: 'GET' },
|
|
218
|
-
};
|
|
219
|
-
const result = evaluatePolicy(action, DEFAULT_POLICY_RULES);
|
|
220
|
-
expect(result.decision).toBe('REQUIRE_APPROVAL');
|
|
221
|
-
});
|
|
222
|
-
});
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { redact, redactHeaders } from '../redact.js';
|
|
3
|
-
|
|
4
|
-
describe('Redaction', () => {
|
|
5
|
-
it('should redact api_key fields', () => {
|
|
6
|
-
const obj = { name: 'test', api_key: 'super-secret-123', data: { token: 'abc' } };
|
|
7
|
-
const result = redact(obj) as Record<string, unknown>;
|
|
8
|
-
expect(result.api_key).toBe('[REDACTED]');
|
|
9
|
-
expect((result.data as Record<string, unknown>).token).toBe('[REDACTED]');
|
|
10
|
-
expect(result.name).toBe('test');
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it('should redact Authorization headers', () => {
|
|
14
|
-
const headers = { Authorization: 'Bearer secret', 'Content-Type': 'application/json' };
|
|
15
|
-
const result = redactHeaders(headers);
|
|
16
|
-
expect(result['Authorization']).toBe('[REDACTED]');
|
|
17
|
-
expect(result['Content-Type']).toBe('application/json');
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('should handle nested objects', () => {
|
|
21
|
-
const obj = { level1: { level2: { password: 'secret', safe: 'ok' } } };
|
|
22
|
-
const result = redact(obj) as { level1: { level2: { password: string; safe: string } } };
|
|
23
|
-
expect(result.level1.level2.password).toBe('[REDACTED]');
|
|
24
|
-
expect(result.level1.level2.safe).toBe('ok');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('should handle arrays', () => {
|
|
28
|
-
const obj = { items: [{ token: 'secret1' }, { token: 'secret2' }] };
|
|
29
|
-
const result = redact(obj) as { items: { token: string }[] };
|
|
30
|
-
expect(result.items[0].token).toBe('[REDACTED]');
|
|
31
|
-
expect(result.items[1].token).toBe('[REDACTED]');
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('should not redact safe fields', () => {
|
|
35
|
-
const obj = { name: 'test', url: 'https://example.com', status: 200 };
|
|
36
|
-
const result = redact(obj) as typeof obj;
|
|
37
|
-
expect(result.name).toBe('test');
|
|
38
|
-
expect(result.url).toBe('https://example.com');
|
|
39
|
-
expect(result.status).toBe(200);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { generateKeypair, signRequest, verifyRequest, canonicalStringify } from '../signing.js';
|
|
3
|
-
|
|
4
|
-
describe('Ed25519 Signing', () => {
|
|
5
|
-
it('should generate valid keypair', () => {
|
|
6
|
-
const kp = generateKeypair();
|
|
7
|
-
expect(kp.publicKey).toBeTruthy();
|
|
8
|
-
expect(kp.privateKey).toBeTruthy();
|
|
9
|
-
expect(kp.publicKey.length).toBeGreaterThan(0);
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it('should sign and verify a request', () => {
|
|
13
|
-
const kp = generateKeypair();
|
|
14
|
-
const body = { action_type: 'write', tool: 'demo', payload: { key: 'value' } };
|
|
15
|
-
const headers = signRequest(body, 'agent-123', kp.privateKey);
|
|
16
|
-
expect(() => verifyRequest(body, headers, kp.publicKey)).not.toThrow();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('should reject tampered body (top-level field)', () => {
|
|
20
|
-
const kp = generateKeypair();
|
|
21
|
-
const body = { action_type: 'write', tool: 'demo', payload: { key: 'value' } };
|
|
22
|
-
const headers = signRequest(body, 'agent-123', kp.privateKey);
|
|
23
|
-
const tampered = { ...body, tool: 'admin' };
|
|
24
|
-
expect(() => verifyRequest(tampered, headers, kp.publicKey)).toThrow('Invalid signature');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('should reject tampered nested payload field', () => {
|
|
28
|
-
const kp = generateKeypair();
|
|
29
|
-
const body = { action_type: 'write', tool: 'demo', payload: { key: 'value', amount: 100 } };
|
|
30
|
-
const headers = signRequest(body, 'agent-123', kp.privateKey);
|
|
31
|
-
const tampered = { ...body, payload: { key: 'value', amount: 999999 } };
|
|
32
|
-
expect(() => verifyRequest(tampered, headers, kp.publicKey)).toThrow('Invalid signature');
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('canonicalStringify should sort keys recursively at all nesting levels', () => {
|
|
36
|
-
const obj = { z: 1, a: { y: 2, b: 3 }, m: [{ q: 4, c: 5 }] };
|
|
37
|
-
const result = canonicalStringify(obj);
|
|
38
|
-
// All object levels must have sorted keys
|
|
39
|
-
expect(result).toBe('{"a":{"b":3,"y":2},"m":[{"c":5,"q":4}],"z":1}');
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('should reject missing headers', () => {
|
|
43
|
-
const kp = generateKeypair();
|
|
44
|
-
const body = { action_type: 'write', tool: 'demo', payload: {} };
|
|
45
|
-
expect(() => verifyRequest(body, {}, kp.publicKey)).toThrow('Missing required signature headers');
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('should reject stale timestamp', () => {
|
|
49
|
-
const kp = generateKeypair();
|
|
50
|
-
const body = { action_type: 'write', tool: 'demo', payload: {} };
|
|
51
|
-
const headers = signRequest(body, 'agent-123', kp.privateKey);
|
|
52
|
-
headers['x-timestamp'] = String(Date.now() - 10 * 60 * 1000);
|
|
53
|
-
expect(() => verifyRequest(body, headers, kp.publicKey)).toThrow('Timestamp skew');
|
|
54
|
-
});
|
|
55
|
-
});
|