@agexhq/core 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.
- package/package.json +31 -0
- package/src/crypto/ecdh.js +112 -0
- package/src/crypto/index.js +14 -0
- package/src/crypto/keys.js +119 -0
- package/src/index.js +40 -0
- package/src/policy/index.js +133 -0
- package/src/protocol/index.js +107 -0
- package/src/schemas/index.js +153 -0
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agexhq/core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AGEX protocol primitives — crypto, schemas, policy engine",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js",
|
|
9
|
+
"./crypto": "./src/crypto/index.js",
|
|
10
|
+
"./schemas": "./src/schemas/index.js",
|
|
11
|
+
"./protocol": "./src/protocol/index.js",
|
|
12
|
+
"./policy": "./src/policy/index.js"
|
|
13
|
+
},
|
|
14
|
+
"publishConfig": { "access": "public" },
|
|
15
|
+
"files": ["src/", "README.md", "LICENSE"],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "node --test src/**/*.test.js || true"
|
|
18
|
+
},
|
|
19
|
+
"keywords": ["agex", "ai-agents", "authentication", "credentials", "protocol", "ed25519"],
|
|
20
|
+
"author": "AGEX Foundation <hello@agexhq.com>",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@noble/ed25519": "^2.0.0",
|
|
24
|
+
"@noble/hashes": "^1.3.3",
|
|
25
|
+
"@noble/curves": "^1.3.0",
|
|
26
|
+
"@noble/ciphers": "^0.5.0",
|
|
27
|
+
"jose": "^5.2.3",
|
|
28
|
+
"uuid": "^9.0.0",
|
|
29
|
+
"zod": "^3.22.4"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @agexhq/core — ECDH-ES+AES256GCM Credential Envelope Encryption
|
|
3
|
+
* Implements SR-1 (Credential Confidentiality) and SR-8 (Hub Zero-Knowledge)
|
|
4
|
+
* Pure JavaScript — works on Termux without native compilation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { x25519 } from '@noble/curves/ed25519'
|
|
8
|
+
import { hkdf } from '@noble/hashes/hkdf'
|
|
9
|
+
import { sha256 } from '@noble/hashes/sha256'
|
|
10
|
+
import { randomBytes } from '@noble/hashes/utils'
|
|
11
|
+
import { base64url } from 'jose'
|
|
12
|
+
|
|
13
|
+
// We use Web Crypto API for AES-GCM (available in Node 18+ and Termux)
|
|
14
|
+
const subtle = globalThis.crypto?.subtle
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Encrypt a credential value for a recipient agent.
|
|
18
|
+
* The Hub calls this when brokering credentials — it never sees the plaintext
|
|
19
|
+
* because the SP encrypts for the agent's public key directly.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} plaintext — credential value (API key, token, etc.)
|
|
22
|
+
* @param {Uint8Array} recipientX25519Public — agent's X25519 public key
|
|
23
|
+
* @returns {object} AGEX credential envelope
|
|
24
|
+
*/
|
|
25
|
+
export async function encryptEnvelope (plaintext, recipientX25519Public) {
|
|
26
|
+
// Generate ephemeral X25519 keypair
|
|
27
|
+
const ephPriv = randomBytes(32)
|
|
28
|
+
const ephPub = x25519.getPublicKey(ephPriv)
|
|
29
|
+
|
|
30
|
+
// ECDH shared secret
|
|
31
|
+
const shared = x25519.getSharedSecret(ephPriv, recipientX25519Public)
|
|
32
|
+
|
|
33
|
+
// HKDF key derivation
|
|
34
|
+
const key = hkdf(sha256, shared, /* salt */ new Uint8Array(0), 'AGEX-CLC-v1', 32)
|
|
35
|
+
|
|
36
|
+
// AES-256-GCM via Web Crypto
|
|
37
|
+
const nonce = randomBytes(12)
|
|
38
|
+
const plainBytes = new TextEncoder().encode(plaintext)
|
|
39
|
+
|
|
40
|
+
const cryptoKey = await subtle.importKey('raw', key, 'AES-GCM', false, ['encrypt'])
|
|
41
|
+
const sealed = await subtle.encrypt({ name: 'AES-GCM', iv: nonce }, cryptoKey, plainBytes)
|
|
42
|
+
|
|
43
|
+
// Web Crypto appends tag to ciphertext
|
|
44
|
+
const sealedBytes = new Uint8Array(sealed)
|
|
45
|
+
const ciphertext = sealedBytes.slice(0, -16)
|
|
46
|
+
const tag = sealedBytes.slice(-16)
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
algorithm: 'ECDH-ES+AES256GCM',
|
|
50
|
+
epk: { kty: 'OKP', crv: 'X25519', x: base64url.encode(ephPub) },
|
|
51
|
+
ciphertext: base64url.encode(ciphertext),
|
|
52
|
+
nonce: base64url.encode(nonce),
|
|
53
|
+
tag: base64url.encode(tag)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Decrypt a credential envelope using the agent's X25519 private key.
|
|
59
|
+
*
|
|
60
|
+
* @param {object} envelope — AGEX credential envelope
|
|
61
|
+
* @param {Uint8Array} recipientX25519Private — agent's X25519 private key bytes
|
|
62
|
+
* @returns {string} decrypted credential value
|
|
63
|
+
*/
|
|
64
|
+
export async function decryptEnvelope (envelope, recipientX25519Private) {
|
|
65
|
+
const ephPub = base64url.decode(envelope.epk.x)
|
|
66
|
+
|
|
67
|
+
// ECDH shared secret
|
|
68
|
+
const shared = x25519.getSharedSecret(recipientX25519Private, ephPub)
|
|
69
|
+
|
|
70
|
+
// Same HKDF derivation
|
|
71
|
+
const key = hkdf(sha256, shared, new Uint8Array(0), 'AGEX-CLC-v1', 32)
|
|
72
|
+
|
|
73
|
+
const nonce = base64url.decode(envelope.nonce)
|
|
74
|
+
const ciphertext = base64url.decode(envelope.ciphertext)
|
|
75
|
+
const tag = base64url.decode(envelope.tag)
|
|
76
|
+
|
|
77
|
+
// Reconstruct sealed = ciphertext || tag
|
|
78
|
+
const sealed = new Uint8Array(ciphertext.length + tag.length)
|
|
79
|
+
sealed.set(ciphertext)
|
|
80
|
+
sealed.set(tag, ciphertext.length)
|
|
81
|
+
|
|
82
|
+
const cryptoKey = await subtle.importKey('raw', key, 'AES-GCM', false, ['decrypt'])
|
|
83
|
+
const plain = await subtle.decrypt({ name: 'AES-GCM', iv: nonce }, cryptoKey, sealed)
|
|
84
|
+
|
|
85
|
+
return new TextDecoder().decode(plain)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Convert an Ed25519 private key to X25519 for ECDH.
|
|
90
|
+
* Ed25519 and X25519 share the same underlying curve (Curve25519).
|
|
91
|
+
*/
|
|
92
|
+
export function ed25519PrivateToX25519 (ed25519PrivateBase64url) {
|
|
93
|
+
const privBytes = base64url.decode(ed25519PrivateBase64url)
|
|
94
|
+
// Hash the Ed25519 seed to get the scalar, then clamp for X25519
|
|
95
|
+
const hash = sha256(privBytes)
|
|
96
|
+
const scalar = hash.slice(0, 32)
|
|
97
|
+
scalar[0] &= 248
|
|
98
|
+
scalar[31] &= 127
|
|
99
|
+
scalar[31] |= 64
|
|
100
|
+
return scalar
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Convert an Ed25519 public key JWK to X25519 public key bytes.
|
|
105
|
+
* Uses the birational map from Ed25519 to X25519 (Montgomery form).
|
|
106
|
+
*/
|
|
107
|
+
export function ed25519PublicToX25519 (publicKeyJWK) {
|
|
108
|
+
// @noble/curves provides this conversion
|
|
109
|
+
const { edwardsToMontgomeryPub } = await import('@noble/curves/ed25519')
|
|
110
|
+
const edPub = base64url.decode(publicKeyJWK.x)
|
|
111
|
+
return edwardsToMontgomeryPub(edPub)
|
|
112
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export {
|
|
2
|
+
generateKeypair, publicKeyToJWK, jwkToPublicKeyBytes,
|
|
3
|
+
sign, verify,
|
|
4
|
+
canonicalJson,
|
|
5
|
+
sha3Hash, sha256Hash,
|
|
6
|
+
computeEventHash,
|
|
7
|
+
generateId, generateNonce,
|
|
8
|
+
base64url, randomBytes
|
|
9
|
+
} from './keys.js'
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
encryptEnvelope, decryptEnvelope,
|
|
13
|
+
ed25519PrivateToX25519
|
|
14
|
+
} from './ecdh.js'
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @agexhq/core — Cryptographic primitives
|
|
3
|
+
* Ed25519 key generation, signing, verification
|
|
4
|
+
* All pure JavaScript via @noble — zero native deps
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as ed from '@noble/ed25519'
|
|
8
|
+
import { sha256 } from '@noble/hashes/sha256'
|
|
9
|
+
import { sha3_256 } from '@noble/hashes/sha3'
|
|
10
|
+
import { randomBytes } from '@noble/hashes/utils'
|
|
11
|
+
import { base64url } from 'jose'
|
|
12
|
+
import { v4 as uuidv4 } from 'uuid'
|
|
13
|
+
|
|
14
|
+
// ── Key Generation ────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export function generateKeypair () {
|
|
17
|
+
const privateKey = ed.utils.randomPrivateKey()
|
|
18
|
+
const publicKey = ed.getPublicKeySync(privateKey)
|
|
19
|
+
return {
|
|
20
|
+
privateKey: base64url.encode(privateKey),
|
|
21
|
+
publicKey: base64url.encode(publicKey),
|
|
22
|
+
jwk: publicKeyToJWK(publicKey)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function publicKeyToJWK (publicKeyBytes) {
|
|
27
|
+
return { kty: 'OKP', crv: 'Ed25519', x: base64url.encode(publicKeyBytes) }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function jwkToPublicKeyBytes (jwk) {
|
|
31
|
+
return base64url.decode(jwk.x)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── Signing ───────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
export async function sign (message, privateKeyBase64url) {
|
|
37
|
+
const privBytes = base64url.decode(privateKeyBase64url)
|
|
38
|
+
const msgBytes = typeof message === 'string'
|
|
39
|
+
? new TextEncoder().encode(message)
|
|
40
|
+
: message
|
|
41
|
+
const sig = await ed.signAsync(msgBytes, privBytes)
|
|
42
|
+
return base64url.encode(sig)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function verify (message, signatureBase64url, publicKeyJWK) {
|
|
46
|
+
try {
|
|
47
|
+
const pubBytes = jwkToPublicKeyBytes(publicKeyJWK)
|
|
48
|
+
const sigBytes = base64url.decode(signatureBase64url)
|
|
49
|
+
const msgBytes = typeof message === 'string'
|
|
50
|
+
? new TextEncoder().encode(message)
|
|
51
|
+
: message
|
|
52
|
+
return await ed.verifyAsync(sigBytes, msgBytes, pubBytes)
|
|
53
|
+
} catch {
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── Canonical JSON (RFC 8785 — fixed edge cases from audit 3.3) ──────────
|
|
59
|
+
|
|
60
|
+
export function canonicalJson (obj) {
|
|
61
|
+
if (obj === null) return 'null'
|
|
62
|
+
if (obj === undefined) return undefined
|
|
63
|
+
if (typeof obj === 'boolean') return String(obj)
|
|
64
|
+
if (typeof obj === 'number') {
|
|
65
|
+
if (!Number.isFinite(obj)) return 'null'
|
|
66
|
+
return Object.is(obj, -0) ? '0' : String(obj)
|
|
67
|
+
}
|
|
68
|
+
if (typeof obj === 'string') return JSON.stringify(obj)
|
|
69
|
+
if (obj instanceof Date) return JSON.stringify(obj.toISOString())
|
|
70
|
+
if (Array.isArray(obj)) {
|
|
71
|
+
return '[' + obj.map(v => {
|
|
72
|
+
const s = canonicalJson(v)
|
|
73
|
+
return s === undefined ? 'null' : s
|
|
74
|
+
}).join(',') + ']'
|
|
75
|
+
}
|
|
76
|
+
if (typeof obj === 'object') {
|
|
77
|
+
const pairs = Object.keys(obj)
|
|
78
|
+
.sort()
|
|
79
|
+
.map(k => {
|
|
80
|
+
const v = canonicalJson(obj[k])
|
|
81
|
+
if (v === undefined) return undefined
|
|
82
|
+
return JSON.stringify(k) + ':' + v
|
|
83
|
+
})
|
|
84
|
+
.filter(p => p !== undefined)
|
|
85
|
+
return '{' + pairs.join(',') + '}'
|
|
86
|
+
}
|
|
87
|
+
return 'null'
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Hashing ───────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
export function sha3Hash (data) {
|
|
93
|
+
const bytes = typeof data === 'string'
|
|
94
|
+
? new TextEncoder().encode(data) : data
|
|
95
|
+
return Buffer.from(sha3_256(bytes)).toString('hex')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function sha256Hash (data) {
|
|
99
|
+
const bytes = typeof data === 'string'
|
|
100
|
+
? new TextEncoder().encode(data) : data
|
|
101
|
+
return Buffer.from(sha256(bytes)).toString('hex')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── Audit Hash Chain ──────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
export function computeEventHash (event, prevHash) {
|
|
107
|
+
const payload = canonicalJson({ ...event, prev_hash: prevHash })
|
|
108
|
+
return sha3Hash(payload)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── ID Generation ─────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
export function generateId () { return uuidv4() }
|
|
114
|
+
|
|
115
|
+
export function generateNonce () {
|
|
116
|
+
return base64url.encode(randomBytes(16))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export { base64url, randomBytes }
|
package/src/index.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @agexhq/core — AGEX Protocol Primitives
|
|
3
|
+
* The foundation package for the entire AGEX ecosystem.
|
|
4
|
+
* Pure JavaScript — zero native dependencies — works everywhere including Termux.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ── Crypto ────────────────────────────────────────────────────────────────
|
|
8
|
+
export {
|
|
9
|
+
generateKeypair, publicKeyToJWK, jwkToPublicKeyBytes,
|
|
10
|
+
sign, verify,
|
|
11
|
+
canonicalJson,
|
|
12
|
+
sha3Hash, sha256Hash,
|
|
13
|
+
computeEventHash,
|
|
14
|
+
generateId, generateNonce,
|
|
15
|
+
base64url, randomBytes
|
|
16
|
+
} from './crypto/keys.js'
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
encryptEnvelope, decryptEnvelope,
|
|
20
|
+
ed25519PrivateToX25519
|
|
21
|
+
} from './crypto/ecdh.js'
|
|
22
|
+
|
|
23
|
+
// ── Schemas ───────────────────────────────────────────────────────────────
|
|
24
|
+
export {
|
|
25
|
+
AIDSchema, ManifestSchema, CLCSchema,
|
|
26
|
+
ServiceProviderSchema,
|
|
27
|
+
PolicyDocSchema, PolicyRuleSchema, PolicyConditionSchema
|
|
28
|
+
} from './schemas/index.js'
|
|
29
|
+
|
|
30
|
+
// ── Protocol ──────────────────────────────────────────────────────────────
|
|
31
|
+
export {
|
|
32
|
+
verifyAIDSignature, selfSignAID,
|
|
33
|
+
signManifest, verifyManifest,
|
|
34
|
+
buildAgexHeaders, signRequest, verifyRequest,
|
|
35
|
+
AgexError,
|
|
36
|
+
AGEX_VERSION, AUDIT_EVENTS
|
|
37
|
+
} from './protocol/index.js'
|
|
38
|
+
|
|
39
|
+
// ── Policy Engine ─────────────────────────────────────────────────────────
|
|
40
|
+
export { evaluatePolicy, evaluateCondition } from './policy/index.js'
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @agexhq/core — AGEX Policy Language (APL) Evaluation Engine
|
|
3
|
+
* Deterministic, stateless policy evaluation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function evaluatePolicy (policy, aid, manifest) {
|
|
7
|
+
const rules = [...(policy.rules || [])].sort((a, b) => (a.priority || 99) - (b.priority || 99))
|
|
8
|
+
|
|
9
|
+
for (const rule of rules) {
|
|
10
|
+
const matched = evaluateCondition(rule.condition, aid, manifest)
|
|
11
|
+
if (matched) {
|
|
12
|
+
return {
|
|
13
|
+
action: rule.action,
|
|
14
|
+
rule_id: rule.rule_id,
|
|
15
|
+
reason: rule.description || `Matched rule: ${rule.rule_id}`
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
action: policy.default_action || 'reject',
|
|
22
|
+
reason: 'No rules matched; applying default_action'
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function evaluateCondition (condition, aid, manifest) {
|
|
27
|
+
if (!condition) return true
|
|
28
|
+
|
|
29
|
+
switch (condition.type) {
|
|
30
|
+
|
|
31
|
+
case 'trust_tier': {
|
|
32
|
+
const tier = typeof aid.trust_tier === 'number' ? aid.trust_tier : parseInt(aid.trust_tier)
|
|
33
|
+
switch (condition.operator) {
|
|
34
|
+
case 'gte': return tier >= condition.value
|
|
35
|
+
case 'lte': return tier <= condition.value
|
|
36
|
+
case 'eq': return tier === condition.value
|
|
37
|
+
case 'gt': return tier > condition.value
|
|
38
|
+
case 'lt': return tier < condition.value
|
|
39
|
+
default: return false
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
case 'scope': {
|
|
44
|
+
const requested = manifest.target?.requested_scopes || []
|
|
45
|
+
if (condition.operator === 'contains_any') {
|
|
46
|
+
return condition.values.some(s => requested.includes(s))
|
|
47
|
+
}
|
|
48
|
+
if (condition.operator === 'contains_all') {
|
|
49
|
+
return condition.values.every(s => requested.includes(s))
|
|
50
|
+
}
|
|
51
|
+
if (condition.operator === 'subset_of') {
|
|
52
|
+
return requested.every(s => condition.values.includes(s))
|
|
53
|
+
}
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
case 'intent_type': {
|
|
58
|
+
const taskType = manifest.intent?.task_type
|
|
59
|
+
if (condition.operator === 'eq') return taskType === condition.value
|
|
60
|
+
if (condition.operator === 'in') return (condition.values || []).includes(taskType)
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
case 'data_classification': {
|
|
65
|
+
const dc = manifest.intent?.data_classification
|
|
66
|
+
const levels = ['public', 'internal', 'confidential', 'restricted']
|
|
67
|
+
const reqLevel = levels.indexOf(dc)
|
|
68
|
+
const condLevel = levels.indexOf(condition.value)
|
|
69
|
+
if (condition.operator === 'lte') return reqLevel <= condLevel
|
|
70
|
+
if (condition.operator === 'gte') return reqLevel >= condLevel
|
|
71
|
+
if (condition.operator === 'eq') return reqLevel === condLevel
|
|
72
|
+
return false
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
case 'pii_processing':
|
|
76
|
+
return manifest.data_handling?.pii_processing === condition.value
|
|
77
|
+
|
|
78
|
+
case 'environment': {
|
|
79
|
+
const env = manifest.target?.environment
|
|
80
|
+
if (condition.operator === 'eq') return env === condition.value
|
|
81
|
+
if (condition.operator === 'in') return (condition.values || []).includes(env)
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
case 'time_window': {
|
|
86
|
+
const now = new Date()
|
|
87
|
+
const hour = now.getUTCHours()
|
|
88
|
+
const day = now.getUTCDay()
|
|
89
|
+
const inHours = hour >= (condition.start_hour || 0) && hour < (condition.end_hour || 24)
|
|
90
|
+
const inDays = !condition.days_of_week ||
|
|
91
|
+
condition.days_of_week.includes(['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'][day])
|
|
92
|
+
return inHours && inDays
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// FIX: audit 2.6 — clarified geography semantics
|
|
96
|
+
case 'geography': {
|
|
97
|
+
const agentJurisdiction = aid.agent?.principal?.jurisdiction ||
|
|
98
|
+
(typeof aid.restrictions === 'string' ? JSON.parse(aid.restrictions) : aid.restrictions)?.geo_restrictions?.[0]
|
|
99
|
+
const allowedRegions = (typeof aid.restrictions === 'string' ? JSON.parse(aid.restrictions) : aid.restrictions)?.geo_restrictions || []
|
|
100
|
+
|
|
101
|
+
if (condition.operator === 'agent_in') {
|
|
102
|
+
return condition.values.includes(agentJurisdiction)
|
|
103
|
+
}
|
|
104
|
+
if (condition.operator === 'agent_not_in') {
|
|
105
|
+
return !condition.values.includes(agentJurisdiction)
|
|
106
|
+
}
|
|
107
|
+
if (condition.operator === 'restricted_to') {
|
|
108
|
+
return condition.values.every(v => allowedRegions.includes(v))
|
|
109
|
+
}
|
|
110
|
+
// Backwards compat with old in/not_in operators
|
|
111
|
+
if (condition.operator === 'in') {
|
|
112
|
+
return condition.values.some(v => allowedRegions.includes(v))
|
|
113
|
+
}
|
|
114
|
+
if (condition.operator === 'not_in') {
|
|
115
|
+
return !condition.values.some(v => allowedRegions.includes(v))
|
|
116
|
+
}
|
|
117
|
+
return false
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
case 'and':
|
|
121
|
+
return (condition.conditions || []).every(c => evaluateCondition(c, aid, manifest))
|
|
122
|
+
|
|
123
|
+
case 'or':
|
|
124
|
+
return (condition.conditions || []).some(c => evaluateCondition(c, aid, manifest))
|
|
125
|
+
|
|
126
|
+
case 'not':
|
|
127
|
+
return !evaluateCondition(condition.condition, aid, manifest)
|
|
128
|
+
|
|
129
|
+
default:
|
|
130
|
+
console.warn(`[APL] Unknown condition type: ${condition.type}`)
|
|
131
|
+
return false
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @agexhq/core — Protocol operations
|
|
3
|
+
* AID verification, manifest signing, AGEX header construction
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { sign, verify, canonicalJson, generateId } from '../crypto/keys.js'
|
|
7
|
+
|
|
8
|
+
// ── AID Signature Verification (FIX: audit 1.1) ──────────────────────────
|
|
9
|
+
// ALWAYS verifies — never silently bypasses
|
|
10
|
+
|
|
11
|
+
export async function verifyAIDSignature (aid, iaPublicKeyJWK) {
|
|
12
|
+
if (!iaPublicKeyJWK) {
|
|
13
|
+
throw new AgexError('IA_KEY_REQUIRED', 'IA public key required for AID verification', 400)
|
|
14
|
+
}
|
|
15
|
+
const { ia_signature, ...aidBody } = aid
|
|
16
|
+
const canonical = canonicalJson(aidBody)
|
|
17
|
+
const valid = await verify(canonical, ia_signature, iaPublicKeyJWK)
|
|
18
|
+
if (!valid) {
|
|
19
|
+
throw new AgexError('IA_SIGNATURE_INVALID', 'AID ia_signature failed verification', 401)
|
|
20
|
+
}
|
|
21
|
+
return true
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Self-sign an AID using the Hub's own key (dev/self-signed IA mode).
|
|
26
|
+
* The AID is still cryptographically signed — just by the hub instead of an external IA.
|
|
27
|
+
*/
|
|
28
|
+
export async function selfSignAID (aidBody, hubPrivateKey) {
|
|
29
|
+
const { ia_signature, ...body } = aidBody
|
|
30
|
+
const canonical = canonicalJson(body)
|
|
31
|
+
const signature = await sign(canonical, hubPrivateKey)
|
|
32
|
+
return { ...body, ia_signature: signature }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── Manifest Signing (FIX: audit 1.3) ─────────────────────────────────────
|
|
36
|
+
// Real Ed25519 signatures instead of placeholder strings
|
|
37
|
+
|
|
38
|
+
export async function signManifest (manifest, privateKey) {
|
|
39
|
+
const { agent_signature, ...body } = manifest
|
|
40
|
+
const canonical = canonicalJson(body)
|
|
41
|
+
return await sign(canonical, privateKey)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function verifyManifest (manifest, publicKeyJWK) {
|
|
45
|
+
const { agent_signature, ...body } = manifest
|
|
46
|
+
if (!agent_signature || agent_signature === 'sdk-placeholder') return false
|
|
47
|
+
const canonical = canonicalJson(body)
|
|
48
|
+
return await verify(canonical, agent_signature, publicKeyJWK)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── AGEX Request Headers ──────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
export function buildAgexHeaders (aidId, version = '1.0') {
|
|
54
|
+
return {
|
|
55
|
+
'X-AGEX-Version': version,
|
|
56
|
+
'X-AGEX-Request-ID': generateId(),
|
|
57
|
+
'X-AGEX-Timestamp': new Date().toISOString(),
|
|
58
|
+
'X-AGEX-AID': aidId
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function signRequest (body, timestamp, requestId, privateKey) {
|
|
63
|
+
const bodyStr = body ? canonicalJson(body) : ''
|
|
64
|
+
const sigInput = `${bodyStr}|${timestamp}|${requestId}`
|
|
65
|
+
return await sign(sigInput, privateKey)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function verifyRequest (body, timestamp, requestId, signature, publicKeyJWK) {
|
|
69
|
+
const bodyStr = body ? canonicalJson(body) : ''
|
|
70
|
+
const sigInput = `${bodyStr}|${timestamp}|${requestId}`
|
|
71
|
+
return await verify(sigInput, signature, publicKeyJWK)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── Error class ───────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
export class AgexError extends Error {
|
|
77
|
+
constructor (code, message, statusCode = 400) {
|
|
78
|
+
super(message)
|
|
79
|
+
this.name = 'AgexError'
|
|
80
|
+
this.code = code
|
|
81
|
+
this.statusCode = statusCode
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Constants ─────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
export const AGEX_VERSION = '1.0'
|
|
88
|
+
|
|
89
|
+
export const AUDIT_EVENTS = {
|
|
90
|
+
AID_REGISTERED: 'aid.registered',
|
|
91
|
+
AID_REVOKED: 'aid.revoked',
|
|
92
|
+
CREDENTIAL_REQUESTED: 'credential.requested',
|
|
93
|
+
CREDENTIAL_ISSUED: 'credential.issued',
|
|
94
|
+
CREDENTIAL_REJECTED: 'credential.rejected',
|
|
95
|
+
CREDENTIAL_PENDING: 'credential.pending_approval',
|
|
96
|
+
ROTATION_INITIATED: 'rotation.initiated',
|
|
97
|
+
ROTATION_COMPLETED: 'rotation.completed',
|
|
98
|
+
ROTATION_FAILED: 'rotation.failed',
|
|
99
|
+
DELEGATION_CREATED: 'delegation.created',
|
|
100
|
+
ERS_INITIATED: 'ers.initiated',
|
|
101
|
+
ERS_COMPLETED: 'ers.completed',
|
|
102
|
+
CLC_REVOKED: 'clc.revoked',
|
|
103
|
+
CLC_SUSPENDED: 'clc.suspended',
|
|
104
|
+
CLC_RESUMED: 'clc.resumed',
|
|
105
|
+
APPROVAL_GRANTED: 'approval.granted',
|
|
106
|
+
APPROVAL_REJECTED: 'approval.rejected'
|
|
107
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @agexhq/core — Zod schemas for all AGEX protocol objects
|
|
3
|
+
* Single source of truth — used by hub, SDK, and CLI
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
|
|
8
|
+
// ── AID Schema (Agent Identity Document) ──────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export const AIDSchema = z.object({
|
|
11
|
+
aid_version: z.literal('1.0'),
|
|
12
|
+
aid_id: z.string().uuid(),
|
|
13
|
+
issuer: z.object({
|
|
14
|
+
ia_id: z.string(),
|
|
15
|
+
ia_name: z.string(),
|
|
16
|
+
ia_cert_id: z.string()
|
|
17
|
+
}),
|
|
18
|
+
issued_at: z.string().datetime(),
|
|
19
|
+
expires_at: z.string().datetime(),
|
|
20
|
+
agent: z.object({
|
|
21
|
+
name: z.string().optional(),
|
|
22
|
+
type: z.enum(['orchestrator', 'worker', 'specialist', 'gateway']),
|
|
23
|
+
capabilities: z.array(z.string()).default([]),
|
|
24
|
+
principal: z.object({
|
|
25
|
+
organisation: z.string(),
|
|
26
|
+
org_id: z.string(),
|
|
27
|
+
contact: z.string().email(),
|
|
28
|
+
jurisdiction: z.string()
|
|
29
|
+
})
|
|
30
|
+
}),
|
|
31
|
+
trust_tier: z.number().int().min(0).max(3),
|
|
32
|
+
public_key: z.object({ kty: z.string(), crv: z.string(), x: z.string() }),
|
|
33
|
+
restrictions: z.object({
|
|
34
|
+
allowed_services: z.array(z.string()).optional(),
|
|
35
|
+
max_clc_duration_seconds: z.number().int().optional(),
|
|
36
|
+
max_delegation_depth: z.number().int().min(0).max(5).optional(),
|
|
37
|
+
geo_restrictions: z.array(z.string()).optional()
|
|
38
|
+
}).default({}),
|
|
39
|
+
ia_signature: z.string()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// ── Intent Manifest Schema ────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
export const ManifestSchema = z.object({
|
|
45
|
+
manifest_version: z.literal('1.0'),
|
|
46
|
+
manifest_id: z.string().uuid(),
|
|
47
|
+
requesting_aid: z.string().uuid(),
|
|
48
|
+
target: z.object({
|
|
49
|
+
service_id: z.string(),
|
|
50
|
+
requested_scopes: z.array(z.string()).min(1),
|
|
51
|
+
minimum_scopes: z.array(z.string()).optional(),
|
|
52
|
+
environment: z.enum(['production', 'staging', 'development']).default('production')
|
|
53
|
+
}),
|
|
54
|
+
intent: z.object({
|
|
55
|
+
summary: z.string().max(500),
|
|
56
|
+
task_type: z.enum(['read', 'write', 'read_write', 'admin', 'transact', 'notify']),
|
|
57
|
+
data_classification: z.enum(['public', 'internal', 'confidential', 'restricted']).default('internal'),
|
|
58
|
+
automated: z.boolean().default(true),
|
|
59
|
+
reversible: z.boolean().default(true),
|
|
60
|
+
human_visible: z.boolean().default(false)
|
|
61
|
+
}),
|
|
62
|
+
duration: z.object({
|
|
63
|
+
max_duration_seconds: z.number().int().min(60).max(604800),
|
|
64
|
+
idle_timeout_seconds: z.number().int().min(60).default(3600)
|
|
65
|
+
}),
|
|
66
|
+
data_handling: z.object({
|
|
67
|
+
pii_processing: z.boolean().default(false),
|
|
68
|
+
cross_border_transfer: z.boolean().default(false),
|
|
69
|
+
deletion_on_completion: z.boolean().default(false)
|
|
70
|
+
}).default({}),
|
|
71
|
+
agent_signature: z.string()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
// ── CLC Schema (Credential Lifecycle Contract) ────────────────────────────
|
|
75
|
+
|
|
76
|
+
export const CLCSchema = z.object({
|
|
77
|
+
clc_version: z.literal('1.0'),
|
|
78
|
+
clc_id: z.string().uuid(),
|
|
79
|
+
beneficiary_aid: z.string().uuid(),
|
|
80
|
+
manifest_id: z.string().uuid(),
|
|
81
|
+
manifest_hash: z.string(),
|
|
82
|
+
credential_envelope: z.object({
|
|
83
|
+
algorithm: z.string(),
|
|
84
|
+
epk: z.object({ kty: z.string(), crv: z.string(), x: z.string() }),
|
|
85
|
+
ciphertext: z.string(),
|
|
86
|
+
nonce: z.string(),
|
|
87
|
+
tag: z.string()
|
|
88
|
+
}),
|
|
89
|
+
granted_scopes: z.array(z.string()),
|
|
90
|
+
scope_ceiling: z.array(z.string()),
|
|
91
|
+
validity: z.object({
|
|
92
|
+
not_before: z.string().datetime(),
|
|
93
|
+
not_after: z.string().datetime(),
|
|
94
|
+
idle_timeout_seconds: z.number().int().default(3600)
|
|
95
|
+
}),
|
|
96
|
+
rotation_policy: z.object({
|
|
97
|
+
rotation_interval_seconds: z.number().int(),
|
|
98
|
+
rotation_overlap_seconds: z.number().int(),
|
|
99
|
+
key_derivation_function: z.string().default('HKDF-SHA256')
|
|
100
|
+
}),
|
|
101
|
+
delegation_policy: z.object({
|
|
102
|
+
delegation_permitted: z.boolean(),
|
|
103
|
+
max_delegation_depth: z.number().int().min(0).max(5),
|
|
104
|
+
max_further_delegation: z.number().int().min(0).default(0)
|
|
105
|
+
}),
|
|
106
|
+
chain_provenance: z.array(z.string()).default([]),
|
|
107
|
+
provider_signature: z.string(),
|
|
108
|
+
hub_binding: z.object({
|
|
109
|
+
hub_id: z.string(),
|
|
110
|
+
hub_signature: z.string()
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// ── Service Provider Schema (NEW — fixes audit 3.1) ──────────────────────
|
|
115
|
+
|
|
116
|
+
export const ServiceProviderSchema = z.object({
|
|
117
|
+
sp_id: z.string().min(1).max(128).regex(/^[a-z0-9][a-z0-9._-]*$/, 'sp_id must be lowercase alphanumeric with dots/hyphens/underscores'),
|
|
118
|
+
sp_name: z.string().min(1).max(256),
|
|
119
|
+
service_url: z.string().url(),
|
|
120
|
+
credential_endpoint: z.string().url(),
|
|
121
|
+
policy_endpoint: z.string().url().optional(),
|
|
122
|
+
public_key_jwk: z.object({ kty: z.string(), crv: z.string(), x: z.string() }),
|
|
123
|
+
supported_scopes: z.array(z.string()).default([])
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// ── APL Policy Schema ─────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
export const PolicyConditionSchema = z.lazy(() => z.discriminatedUnion('type', [
|
|
129
|
+
z.object({ type: z.literal('trust_tier'), operator: z.enum(['eq', 'gt', 'gte', 'lt', 'lte']), value: z.number() }),
|
|
130
|
+
z.object({ type: z.literal('scope'), operator: z.enum(['contains_any', 'contains_all', 'subset_of']), values: z.array(z.string()) }),
|
|
131
|
+
z.object({ type: z.literal('intent_type'), operator: z.enum(['eq', 'in']), value: z.string().optional(), values: z.array(z.string()).optional() }),
|
|
132
|
+
z.object({ type: z.literal('data_classification'), operator: z.enum(['eq', 'gte', 'lte']), value: z.string() }),
|
|
133
|
+
z.object({ type: z.literal('pii_processing'), value: z.boolean() }),
|
|
134
|
+
z.object({ type: z.literal('environment'), operator: z.enum(['eq', 'in']), value: z.string().optional(), values: z.array(z.string()).optional() }),
|
|
135
|
+
z.object({ type: z.literal('time_window'), start_hour: z.number().optional(), end_hour: z.number().optional(), days_of_week: z.array(z.string()).optional() }),
|
|
136
|
+
z.object({ type: z.literal('geography'), operator: z.enum(['agent_in', 'agent_not_in', 'restricted_to']), values: z.array(z.string()) }),
|
|
137
|
+
z.object({ type: z.literal('and'), conditions: z.array(PolicyConditionSchema) }),
|
|
138
|
+
z.object({ type: z.literal('or'), conditions: z.array(PolicyConditionSchema) }),
|
|
139
|
+
z.object({ type: z.literal('not'), condition: PolicyConditionSchema }),
|
|
140
|
+
]))
|
|
141
|
+
|
|
142
|
+
export const PolicyRuleSchema = z.object({
|
|
143
|
+
rule_id: z.string(),
|
|
144
|
+
priority: z.number().int().default(99),
|
|
145
|
+
condition: PolicyConditionSchema.optional(),
|
|
146
|
+
action: z.enum(['approve', 'reject', 'review']),
|
|
147
|
+
description: z.string().optional()
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
export const PolicyDocSchema = z.object({
|
|
151
|
+
rules: z.array(PolicyRuleSchema),
|
|
152
|
+
default_action: z.enum(['approve', 'reject', 'review']).default('reject')
|
|
153
|
+
})
|