@twin3-ai/agent-id 0.1.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/bin/agent-id-sync.js +80 -0
- package/bin/agent-id.js +3397 -0
- package/browser-sdk.js +149 -0
- package/cloudflare-worker-adapter.js +100 -0
- package/domain-proof.js +208 -0
- package/enterprise-identity.js +202 -0
- package/installer.js +722 -0
- package/local-policy.js +118 -0
- package/package.json +44 -0
- package/production-preflight.js +123 -0
- package/release-verifier.js +83 -0
- package/repository-connector.js +306 -0
- package/runtime-config.js +10 -0
- package/site-agent.js +1010 -0
- package/sync-service.js +312 -0
- package/task-executor.js +129 -0
- package/telemetry-collector.js +163 -0
- package/trust-verifier.js +206 -0
package/local-policy.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const crypto = require("node:crypto");
|
|
6
|
+
|
|
7
|
+
const POLICY_SCHEMA = "agentx-local-policy-v1";
|
|
8
|
+
const MAX_BYTES = 1024 * 1024;
|
|
9
|
+
const OPERATIONS = new Set(["upsert", "replace", "delete"]);
|
|
10
|
+
const SENSITIVE_PATHS = /(^|\/)(\.env(?:\.|$)|\.git|id_rsa|id_ed25519|secrets?|credentials?|private[-_.]?key|package-lock\.json)(\/|$)/i;
|
|
11
|
+
const EXECUTABLE_PATHS = /\.(?:sh|bash|zsh|fish|cmd|bat|ps1|exe|dll|so|dylib|js|mjs|cjs|py|rb|php|pl)$/i;
|
|
12
|
+
const SECRET_CONTENT = /(?:-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:SITE_AGENT_KEY|AEO_AGENT_KEY|API_KEY|ACCESS_TOKEN|SECRET)\s*[:=]|\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b)/i;
|
|
13
|
+
|
|
14
|
+
function policyError(code) { const error = new Error(code); error.code = code; return error; }
|
|
15
|
+
function stableValue(value) {
|
|
16
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
17
|
+
if (!value || typeof value !== "object") return value;
|
|
18
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableValue(value[key])]));
|
|
19
|
+
}
|
|
20
|
+
function digest(policy) {
|
|
21
|
+
const material = { ...policy };
|
|
22
|
+
delete material.digest;
|
|
23
|
+
return `sha256:${crypto.createHash("sha256").update(JSON.stringify(stableValue(material))).digest("hex")}`;
|
|
24
|
+
}
|
|
25
|
+
function writePolicy(filePath, policy) {
|
|
26
|
+
const target = path.resolve(String(filePath || ""));
|
|
27
|
+
fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
28
|
+
const temporary = `${target}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
29
|
+
try {
|
|
30
|
+
fs.writeFileSync(temporary, JSON.stringify(policy, null, 2) + "\n", { mode: 0o600, flag: "wx" });
|
|
31
|
+
fs.renameSync(temporary, target);
|
|
32
|
+
fs.chmodSync(target, 0o600);
|
|
33
|
+
} finally { try { fs.rmSync(temporary, { force: true }); } catch (_error) {} }
|
|
34
|
+
}
|
|
35
|
+
function initializeLocalPolicy(filePath) {
|
|
36
|
+
const policy = { schema: POLICY_SCHEMA, version: 1, default: "deny", rules: [], approvals: [] };
|
|
37
|
+
policy.digest = digest(policy);
|
|
38
|
+
writePolicy(filePath, policy);
|
|
39
|
+
Object.defineProperty(policy, "_path", { value: path.resolve(filePath), enumerable: false });
|
|
40
|
+
return policy;
|
|
41
|
+
}
|
|
42
|
+
function loadLocalPolicy(filePath) {
|
|
43
|
+
if (fs.statSync(filePath).size > MAX_BYTES) throw policyError("policy_file_too_large");
|
|
44
|
+
const policy = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
45
|
+
if (!policy || policy.schema !== POLICY_SCHEMA || policy.default !== "deny" || !Number.isInteger(policy.version) || !Array.isArray(policy.rules) || !Array.isArray(policy.approvals || [])) throw policyError("invalid_policy");
|
|
46
|
+
const expected = digest(policy);
|
|
47
|
+
if (typeof policy.digest !== "string" || policy.digest !== expected) throw policyError("invalid_policy_digest");
|
|
48
|
+
policy.digest = expected;
|
|
49
|
+
Object.defineProperty(policy, "_path", { value: path.resolve(filePath), enumerable: false });
|
|
50
|
+
return policy;
|
|
51
|
+
}
|
|
52
|
+
function validRule(rule) {
|
|
53
|
+
return rule && typeof rule.connector === "string" && /^[a-z][a-z0-9_-]+$/.test(rule.connector) && rule.connector !== "*"
|
|
54
|
+
&& typeof rule.environment === "string" && /^[a-z][a-z0-9_-]+$/.test(rule.environment) && rule.environment !== "*"
|
|
55
|
+
&& typeof rule.resource_type === "string" && rule.resource_type !== "*"
|
|
56
|
+
&& typeof rule.resource_id === "string" && rule.resource_id.length > 0 && rule.resource_id !== "*"
|
|
57
|
+
&& safeRelative(rule.path_prefix) && !rule.path_prefix.includes("*")
|
|
58
|
+
&& Array.isArray(rule.allowed_operations) && rule.allowed_operations.length > 0 && rule.allowed_operations.every((item) => OPERATIONS.has(item))
|
|
59
|
+
&& Number.isInteger(rule.max_bytes) && rule.max_bytes > 0 && rule.max_bytes <= MAX_BYTES
|
|
60
|
+
&& rule.approval_mode === "explicit" && Number.isInteger(rule.version) && rule.version > 0;
|
|
61
|
+
}
|
|
62
|
+
function approvePolicyRule(policy, rule, { approved = false, approvedBy = "", now = Math.floor(Date.now() / 1000) } = {}) {
|
|
63
|
+
if (approved !== true || typeof approvedBy !== "string" || !approvedBy.trim()) throw policyError("policy_approval_required");
|
|
64
|
+
if (!validRule(rule)) throw policyError("invalid_policy_rule");
|
|
65
|
+
const updated = JSON.parse(JSON.stringify(policy));
|
|
66
|
+
if (updated.default !== "deny") throw policyError("invalid_policy");
|
|
67
|
+
updated.version += 1;
|
|
68
|
+
updated.rules.push({ ...rule, allowed_operations: [...new Set(rule.allowed_operations)].sort() });
|
|
69
|
+
updated.approvals = [...(updated.approvals || []), { policy_version: updated.version, rule_version: rule.version, approved_by: approvedBy.trim(), approved_at: now, scope_digest: `sha256:${crypto.createHash("sha256").update(JSON.stringify(stableValue(rule))).digest("hex")}` }];
|
|
70
|
+
updated.digest = digest(updated);
|
|
71
|
+
if (policy._path) writePolicy(policy._path, updated);
|
|
72
|
+
if (policy._path) Object.defineProperty(updated, "_path", { value: policy._path, enumerable: false });
|
|
73
|
+
return updated;
|
|
74
|
+
}
|
|
75
|
+
function safeRelative(value) {
|
|
76
|
+
return typeof value === "string" && value.length > 0 && !path.isAbsolute(value)
|
|
77
|
+
&& !value.split(/[\\/]+/).includes("..") && !/[%\0-\x1f\x7f]/.test(value);
|
|
78
|
+
}
|
|
79
|
+
function pathWithinPrefix(value, prefix) {
|
|
80
|
+
const normalizedValue = value.replace(/\\/g, "/");
|
|
81
|
+
const normalizedPrefix = prefix.replace(/\\/g, "/");
|
|
82
|
+
return normalizedValue === normalizedPrefix
|
|
83
|
+
|| (normalizedPrefix.endsWith("/") && normalizedValue.startsWith(normalizedPrefix));
|
|
84
|
+
}
|
|
85
|
+
function immutableGuards(task, currentResource) {
|
|
86
|
+
if (!task || typeof task !== "object" || !currentResource || typeof currentResource !== "object" || !Array.isArray(task.operations) || !task.operations.length) throw policyError("unsafe_task");
|
|
87
|
+
if (currentResource.real_path && currentResource.root_path) {
|
|
88
|
+
const rootInput = path.resolve(currentResource.root_path);
|
|
89
|
+
const realInput = path.resolve(currentResource.real_path);
|
|
90
|
+
const root = fs.existsSync(rootInput) ? fs.realpathSync.native(rootInput) : rootInput;
|
|
91
|
+
const real = fs.existsSync(realInput) ? fs.realpathSync.native(realInput) : realInput;
|
|
92
|
+
if (real !== root && !real.startsWith(root + path.sep)) throw policyError("unsafe_task_symlink_escape");
|
|
93
|
+
}
|
|
94
|
+
for (const operation of task.operations) {
|
|
95
|
+
if (!operation || !OPERATIONS.has(operation.operation) || !safeRelative(operation.path)) throw policyError("unsafe_task_operation");
|
|
96
|
+
const normalized = operation.path.replace(/\\/g, "/");
|
|
97
|
+
if (SENSITIVE_PATHS.test(normalized) || EXECUTABLE_PATHS.test(normalized)) throw policyError("unsafe_task_sensitive_path");
|
|
98
|
+
const value = operation.value == null ? "" : operation.value;
|
|
99
|
+
if (typeof value !== "string") throw policyError("unsafe_task_payload");
|
|
100
|
+
if (Buffer.byteLength(value, "utf8") > MAX_BYTES || SECRET_CONTENT.test(value) || /^\s*(?:#!.*(?:sh|bash|node|python)|(?:powershell|cmd\.exe)\b)/im.test(value)) throw policyError("unsafe_task_payload");
|
|
101
|
+
if (normalized === "robots.txt" && /^\s*Disallow:\s*\/\s*$/im.test(value)) throw policyError("unsafe_task_robots_global_disallow");
|
|
102
|
+
if (operation.expected_hash !== currentResource.sha256) throw policyError("unsafe_task_stale_hash");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function authorizeTask(policy, task, currentResource) {
|
|
106
|
+
immutableGuards(task, currentResource);
|
|
107
|
+
if (!policy || policy.default !== "deny" || policy.digest !== digest(policy) || task.policy_digest !== policy.digest) throw policyError("policy_denied");
|
|
108
|
+
const resource = task.resource;
|
|
109
|
+
if (!resource || task.connector !== currentResource.connector || task.environment !== currentResource.environment || resource.resource_type !== currentResource.resource_type || resource.resource_id !== currentResource.resource_id || resource.path !== currentResource.path) throw policyError("policy_denied");
|
|
110
|
+
const rule = policy.rules.find((item) => item.connector === task.connector && item.environment === task.environment && item.resource_type === resource.resource_type && item.resource_id === resource.resource_id && pathWithinPrefix(resource.path, item.path_prefix));
|
|
111
|
+
if (!rule || !validRule(rule)) throw policyError("policy_denied");
|
|
112
|
+
for (const operation of task.operations) {
|
|
113
|
+
if (!rule.allowed_operations.includes(operation.operation) || !pathWithinPrefix(operation.path, rule.path_prefix) || Buffer.byteLength(operation.value || "", "utf8") > rule.max_bytes) throw policyError("policy_denied");
|
|
114
|
+
}
|
|
115
|
+
return { authorized: true, policy_version: policy.version, policy_digest: policy.digest, rule_version: rule.version, approval_mode: rule.approval_mode };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = { POLICY_SCHEMA, initializeLocalPolicy, loadLocalPolicy, approvePolicyRule, authorizeTask };
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@twin3-ai/agent-id",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Domain-bound Agent identity, AEO evidence, and website-Agent integration SDK.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"private": false,
|
|
7
|
+
"files": [
|
|
8
|
+
"bin",
|
|
9
|
+
"*.js"
|
|
10
|
+
],
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=20",
|
|
16
|
+
"npm": ">=10"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"prepublishOnly": "node --check bin/agent-id.js && node --check installer.js && node --check enterprise-identity.js && node --check release-verifier.js && node --check production-preflight.js"
|
|
20
|
+
},
|
|
21
|
+
"main": "site-agent.js",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": "./site-agent.js",
|
|
24
|
+
"./site-agent": "./site-agent.js",
|
|
25
|
+
"./runtime-config": "./runtime-config.js",
|
|
26
|
+
"./browser-sdk": "./browser-sdk.js",
|
|
27
|
+
"./sync-service": "./sync-service.js",
|
|
28
|
+
"./installer": "./installer.js",
|
|
29
|
+
"./enterprise-identity": "./enterprise-identity.js",
|
|
30
|
+
"./domain-proof": "./domain-proof.js",
|
|
31
|
+
"./local-policy": "./local-policy.js",
|
|
32
|
+
"./task-executor": "./task-executor.js",
|
|
33
|
+
"./telemetry-collector": "./telemetry-collector.js",
|
|
34
|
+
"./trust-verifier": "./trust-verifier.js",
|
|
35
|
+
"./repository-connector": "./repository-connector.js",
|
|
36
|
+
"./cloudflare-worker-adapter": "./cloudflare-worker-adapter.js",
|
|
37
|
+
"./release-verifier": "./release-verifier.js",
|
|
38
|
+
"./production-preflight": "./production-preflight.js"
|
|
39
|
+
},
|
|
40
|
+
"bin": {
|
|
41
|
+
"agent-id": "bin/agent-id.js",
|
|
42
|
+
"agent-id-sync": "bin/agent-id-sync.js"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const net = require("node:net");
|
|
4
|
+
const { verifyReleaseManifest } = require("./release-verifier.js");
|
|
5
|
+
|
|
6
|
+
const PACKAGE_NAME = "@twin3-ai/agent-id";
|
|
7
|
+
|
|
8
|
+
function stableProductionOrigin(value) {
|
|
9
|
+
try {
|
|
10
|
+
const parsed = new URL(String(value || ""));
|
|
11
|
+
const host = parsed.hostname.toLowerCase().replace(/\.$/, "");
|
|
12
|
+
if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.port || parsed.pathname !== "/" || parsed.search || parsed.hash) return "";
|
|
13
|
+
if (!host.includes(".") || net.isIP(host)) return "";
|
|
14
|
+
if (host === "localhost" || host.endsWith(".local") || host.endsWith(".internal") || host.endsWith(".test") || host.endsWith(".invalid") || host.endsWith(".example")) return "";
|
|
15
|
+
if (host.endsWith(".run.app") || host.endsWith(".a.run.app") || host.endsWith(".appspot.com")) return "";
|
|
16
|
+
return `https://${host}`;
|
|
17
|
+
} catch (_error) {
|
|
18
|
+
return "";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function fetchJson(fetchImpl, url) {
|
|
23
|
+
try {
|
|
24
|
+
const response = await fetchImpl(url, { headers: { accept: "application/json" } });
|
|
25
|
+
const text = await response.text();
|
|
26
|
+
let body = null;
|
|
27
|
+
try { body = JSON.parse(text); } catch (_error) { body = null; }
|
|
28
|
+
return { ok: response.ok, status: response.status, body };
|
|
29
|
+
} catch (error) {
|
|
30
|
+
return { ok: false, status: 0, body: null, error: String(error && error.message || error) };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function runProductionPreflight({ endpoint, packageVersion, expectedIssuerKeySha256, fetchImpl = fetch }) {
|
|
35
|
+
const origin = stableProductionOrigin(endpoint);
|
|
36
|
+
if (!origin) {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
schema: "agentx-production-preflight-v1",
|
|
40
|
+
endpoint: String(endpoint || ""),
|
|
41
|
+
package: `${PACKAGE_NAME}@${String(packageVersion || "")}`,
|
|
42
|
+
checks: {
|
|
43
|
+
stable_api_origin_format: false,
|
|
44
|
+
api_https_reachable: false,
|
|
45
|
+
stable_api_origin: false,
|
|
46
|
+
},
|
|
47
|
+
blockers: ["stable_api_origin_required"],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const packageId = encodeURIComponent(PACKAGE_NAME);
|
|
52
|
+
const version = String(packageVersion || "");
|
|
53
|
+
const [manifestResponse, issuerResponse, registryResponse, healthResponse, cardResponse] = await Promise.all([
|
|
54
|
+
fetchJson(fetchImpl, `${origin}/.well-known/agent-id-release.json`),
|
|
55
|
+
fetchJson(fetchImpl, `${origin}/.well-known/agentx-issuer-key.json`),
|
|
56
|
+
fetchJson(fetchImpl, `https://registry.npmjs.org/${packageId}/${encodeURIComponent(version)}`),
|
|
57
|
+
fetchJson(fetchImpl, `${origin}/agent/health.json`),
|
|
58
|
+
fetchJson(fetchImpl, `${origin}/.well-known/agent-card.json`),
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
const manifest = manifestResponse.body;
|
|
62
|
+
const registry = registryResponse.body;
|
|
63
|
+
const release = manifestResponse.ok && manifest
|
|
64
|
+
? verifyReleaseManifest({
|
|
65
|
+
manifest,
|
|
66
|
+
issuerDescriptor: issuerResponse.body,
|
|
67
|
+
expectedOrigin: origin,
|
|
68
|
+
packageVersion: version,
|
|
69
|
+
expectedIssuerKeySha256,
|
|
70
|
+
})
|
|
71
|
+
: {
|
|
72
|
+
ok: false,
|
|
73
|
+
signature_verified: false,
|
|
74
|
+
blockers: Array.isArray(manifest?.blockers) ? manifest.blockers : [],
|
|
75
|
+
};
|
|
76
|
+
const registryIdentity = registryResponse.ok && registry?.name === PACKAGE_NAME && registry?.version === version;
|
|
77
|
+
const registryIntegrity = registryIdentity
|
|
78
|
+
&& typeof registry?.dist?.integrity === "string"
|
|
79
|
+
&& registry.dist.integrity === manifest?.package?.integrity;
|
|
80
|
+
const health = healthResponse.ok && healthResponse.body && healthResponse.body.ok !== false;
|
|
81
|
+
const agentCard = cardResponse.ok && typeof cardResponse.body?.name === "string" && cardResponse.body.name.length > 0;
|
|
82
|
+
const apiReachable = [manifestResponse, issuerResponse, healthResponse, cardResponse]
|
|
83
|
+
.some(response => Number(response.status) > 0);
|
|
84
|
+
const checks = {
|
|
85
|
+
stable_api_origin_format: true,
|
|
86
|
+
api_https_reachable: apiReachable,
|
|
87
|
+
stable_api_origin: apiReachable,
|
|
88
|
+
release_manifest_http: manifestResponse.ok,
|
|
89
|
+
issuer_descriptor_http: issuerResponse.ok,
|
|
90
|
+
release_signature: release.signature_verified === true,
|
|
91
|
+
release_contract: release.ok === true,
|
|
92
|
+
npm_package_identity: registryIdentity,
|
|
93
|
+
registry_integrity: registryIntegrity,
|
|
94
|
+
health_endpoint: health,
|
|
95
|
+
agent_card: agentCard,
|
|
96
|
+
};
|
|
97
|
+
const blockers = [...release.blockers];
|
|
98
|
+
if (!apiReachable) blockers.push("stable_api_unreachable");
|
|
99
|
+
if (apiReachable && !manifestResponse.ok) blockers.push("release_manifest_unavailable");
|
|
100
|
+
if (apiReachable && !issuerResponse.ok) blockers.push("issuer_descriptor_unavailable");
|
|
101
|
+
if (!registryIdentity) blockers.push("npm_package_unpublished_or_version_mismatch");
|
|
102
|
+
if (manifestResponse.ok && registryIdentity && !registryIntegrity) blockers.push("npm_integrity_mismatch");
|
|
103
|
+
if (apiReachable && !health) blockers.push("health_endpoint_unavailable");
|
|
104
|
+
if (apiReachable && !agentCard) blockers.push("agent_card_unavailable");
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
ok: blockers.length === 0,
|
|
108
|
+
schema: "agentx-production-preflight-v1",
|
|
109
|
+
endpoint: origin,
|
|
110
|
+
package: `${PACKAGE_NAME}@${version}`,
|
|
111
|
+
checks,
|
|
112
|
+
blockers: [...new Set(blockers)],
|
|
113
|
+
install_template: `npm exec --yes --package=${PACKAGE_NAME}@${version} -- agent-id install https://customer.example --apply`,
|
|
114
|
+
release: {
|
|
115
|
+
commit: manifest?.release_commit || "",
|
|
116
|
+
published_at: manifest?.published_at || "",
|
|
117
|
+
integrity: manifest?.package?.integrity || "",
|
|
118
|
+
signature_verified: release.signature_verified === true,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = { runProductionPreflight, stableProductionOrigin };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("node:crypto");
|
|
4
|
+
|
|
5
|
+
const SCHEMA = "agentx-release-manifest-v1";
|
|
6
|
+
const PACKAGE_NAME = "@twin3-ai/agent-id";
|
|
7
|
+
|
|
8
|
+
function stableValue(value) {
|
|
9
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
10
|
+
if (!value || typeof value !== "object") return value;
|
|
11
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableValue(value[key])]));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function canonical(value) {
|
|
15
|
+
return JSON.stringify(stableValue(value));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function sha256(value) {
|
|
19
|
+
return `sha256:${crypto.createHash("sha256").update(canonical(value)).digest("hex")}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizedOrigin(value) {
|
|
23
|
+
try {
|
|
24
|
+
const parsed = new URL(String(value || ""));
|
|
25
|
+
if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.port || parsed.pathname !== "/" || parsed.search || parsed.hash) return "";
|
|
26
|
+
return parsed.origin.toLowerCase();
|
|
27
|
+
} catch (_error) {
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function decodeProof(value) {
|
|
33
|
+
const raw = String(value || "").replace(/^base64url:/, "");
|
|
34
|
+
return Buffer.from(raw, "base64url");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function verifyReleaseManifest({ manifest, issuerDescriptor, expectedOrigin, packageVersion, expectedIssuerKeySha256 }) {
|
|
38
|
+
const blockers = [];
|
|
39
|
+
const expected = normalizedOrigin(expectedOrigin);
|
|
40
|
+
if (!manifest || manifest.schema !== SCHEMA) blockers.push("invalid_release_manifest_schema");
|
|
41
|
+
if (!manifest || manifest.ready !== true) blockers.push(...(Array.isArray(manifest && manifest.blockers) ? manifest.blockers : ["release_not_ready"]));
|
|
42
|
+
if (!expected || normalizedOrigin(manifest && manifest.api_origin) !== expected) blockers.push("api_origin_mismatch");
|
|
43
|
+
if (!manifest || manifest.package?.name !== PACKAGE_NAME) blockers.push("package_name_mismatch");
|
|
44
|
+
if (!manifest || manifest.package?.version !== String(packageVersion || "")) blockers.push("package_version_mismatch");
|
|
45
|
+
if (!/^sha512-[A-Za-z0-9+/=]{80,120}$/.test(String(manifest?.package?.integrity || ""))) blockers.push("npm_integrity_missing");
|
|
46
|
+
const expectedVersionUrl = `https://www.npmjs.com/package/${PACKAGE_NAME}/v/${String(packageVersion || "")}`;
|
|
47
|
+
if (manifest?.package?.version_url !== expectedVersionUrl) blockers.push("npm_version_url_mismatch");
|
|
48
|
+
if (manifest?.supply_chain?.npm_provenance !== "not_claimed_private_repository") blockers.push("npm_provenance_claim_mismatch");
|
|
49
|
+
|
|
50
|
+
const proof = manifest && typeof manifest.proof === "object" ? manifest.proof : {};
|
|
51
|
+
const unsigned = manifest && typeof manifest === "object"
|
|
52
|
+
? Object.fromEntries(Object.entries(manifest).filter(([key]) => !["proof", "content_hash"].includes(key)))
|
|
53
|
+
: {};
|
|
54
|
+
if (manifest?.content_hash !== sha256(unsigned)) blockers.push("manifest_content_hash_mismatch");
|
|
55
|
+
if (proof.verification_method !== `${expected}/.well-known/agentx-issuer-key.json#ed25519-2026`) blockers.push("verification_method_mismatch");
|
|
56
|
+
|
|
57
|
+
let signatureVerified = false;
|
|
58
|
+
const publicKey = String(issuerDescriptor?.public_key_pem || "");
|
|
59
|
+
if (!publicKey) {
|
|
60
|
+
blockers.push("issuer_public_key_missing");
|
|
61
|
+
} else {
|
|
62
|
+
const issuerKeySha256 = `sha256:${crypto.createHash("sha256").update(publicKey).digest("hex")}`;
|
|
63
|
+
if (!expectedIssuerKeySha256 || issuerKeySha256 !== expectedIssuerKeySha256) blockers.push("issuer_key_pin_mismatch");
|
|
64
|
+
if (manifest?.authority?.issuer_key_sha256 !== issuerKeySha256) blockers.push("manifest_issuer_key_mismatch");
|
|
65
|
+
try {
|
|
66
|
+
signatureVerified = crypto.verify(null, Buffer.from(canonical(unsigned)), crypto.createPublicKey(publicKey), decodeProof(proof.proof_value));
|
|
67
|
+
} catch (_error) {
|
|
68
|
+
signatureVerified = false;
|
|
69
|
+
}
|
|
70
|
+
if (!signatureVerified) blockers.push("release_signature_invalid");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
ok: blockers.length === 0,
|
|
75
|
+
signature_verified: signatureVerified,
|
|
76
|
+
expected_origin: expected,
|
|
77
|
+
expected_package: `${PACKAGE_NAME}@${String(packageVersion || "")}`,
|
|
78
|
+
expected_issuer_key_sha256: String(expectedIssuerKeySha256 || ""),
|
|
79
|
+
blockers: [...new Set(blockers)],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = { verifyReleaseManifest };
|