@aroha-sdk/credentials 1.0.0 → 1.2.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/credentials.d.ts +53 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +117 -0
- package/dist/credentials.js.map +1 -0
- package/dist/credentials.test.d.ts +2 -0
- package/dist/credentials.test.d.ts.map +1 -0
- package/dist/credentials.test.js +69 -0
- package/dist/credentials.test.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/mandate.d.ts +59 -0
- package/dist/mandate.d.ts.map +1 -0
- package/dist/mandate.js +180 -0
- package/dist/mandate.js.map +1 -0
- package/dist/mandate.test.d.ts +2 -0
- package/dist/mandate.test.d.ts.map +1 -0
- package/dist/mandate.test.js +222 -0
- package/dist/mandate.test.js.map +1 -0
- package/dist/middleware.d.ts +53 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +227 -0
- package/dist/middleware.js.map +1 -0
- package/dist/rbac.d.ts +22 -0
- package/dist/rbac.d.ts.map +1 -0
- package/dist/rbac.js +50 -0
- package/dist/rbac.js.map +1 -0
- package/dist/rbac.test.d.ts +2 -0
- package/dist/rbac.test.d.ts.map +1 -0
- package/dist/rbac.test.js +97 -0
- package/dist/rbac.test.js.map +1 -0
- package/dist/registry.d.ts +125 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +227 -0
- package/dist/registry.js.map +1 -0
- package/dist/registry.test.d.ts +2 -0
- package/dist/registry.test.d.ts.map +1 -0
- package/dist/registry.test.js +142 -0
- package/dist/registry.test.js.map +1 -0
- package/dist/types.d.ts +19 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/package.json +5 -1
- package/tsconfig.json +0 -12
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aroha Protocol — Human Credential System (Layer 1 extension)
|
|
3
|
+
*
|
|
4
|
+
* Human users are not DID-bearing agents. They authenticate to their Personal
|
|
5
|
+
* Agent (out of band — password, OAuth, passkey, etc.), which then issues them
|
|
6
|
+
* a short-lived signed credential. That credential travels in the body of any
|
|
7
|
+
* Aroha message the user triggers, letting provider agents verify who initiated
|
|
8
|
+
* the request and what they are allowed to do.
|
|
9
|
+
*
|
|
10
|
+
* The credential is signed by the issuing agent's Ed25519 private key using
|
|
11
|
+
* the same canonicalization rules as Aroha envelope proofs.
|
|
12
|
+
*/
|
|
13
|
+
import { type ArohaRole, type HumanCredential } from "./types.js";
|
|
14
|
+
export type { HumanCredential } from "./types.js";
|
|
15
|
+
export interface CredentialVerificationResult {
|
|
16
|
+
valid: boolean;
|
|
17
|
+
credential?: HumanCredential;
|
|
18
|
+
reason?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Issue a signed HumanCredential.
|
|
22
|
+
*
|
|
23
|
+
* @param userId Application user identifier
|
|
24
|
+
* @param roles Roles to grant (e.g. ["human:user"])
|
|
25
|
+
* @param issuerDID DID of the issuing agent (the Personal Agent)
|
|
26
|
+
* @param privateKey Ed25519 private key of the issuing agent (32 bytes)
|
|
27
|
+
* @param ttlMs Time-to-live in milliseconds (default: 1 hour)
|
|
28
|
+
* @param email Optional email for display purposes
|
|
29
|
+
*/
|
|
30
|
+
export declare function issueHumanCredential(userId: string, roles: ArohaRole[], issuerDID: string, privateKey: Uint8Array, ttlMs?: number, email?: string): Promise<HumanCredential>;
|
|
31
|
+
/**
|
|
32
|
+
* Serialize a HumanCredential to a compact base64url token suitable for
|
|
33
|
+
* embedding in a Aroha message body as `credentialToken`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function serializeCredential(cred: HumanCredential): string;
|
|
36
|
+
/**
|
|
37
|
+
* Deserialize a credentialToken back to a HumanCredential object.
|
|
38
|
+
* Returns null if the token is malformed.
|
|
39
|
+
*/
|
|
40
|
+
export declare function deserializeCredential(token: string): HumanCredential | null;
|
|
41
|
+
/**
|
|
42
|
+
* Verify a HumanCredential.
|
|
43
|
+
*
|
|
44
|
+
* Checks:
|
|
45
|
+
* 1. Token is parseable
|
|
46
|
+
* 2. Expiry has not passed
|
|
47
|
+
* 3. Ed25519 signature is valid against the issuer's public key
|
|
48
|
+
*
|
|
49
|
+
* @param token The base64url credentialToken from the message body
|
|
50
|
+
* @param issuerPublicKey Ed25519 public key of the issuer DID (32 bytes)
|
|
51
|
+
*/
|
|
52
|
+
export declare function verifyHumanCredential(token: string, issuerPublicKey: Uint8Array): Promise<CredentialVerificationResult>;
|
|
53
|
+
//# sourceMappingURL=credentials.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAKlE,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,MAAM,WAAW,4BAA4B;IAC3C,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,eAAe,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAID;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,SAAS,EAAE,EAClB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,UAAU,EACtB,KAAK,SAAY,EACjB,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,eAAe,CAAC,CAoB1B;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,CAEjE;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAM3E;AAID;;;;;;;;;;GAUG;AACH,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,MAAM,EACb,eAAe,EAAE,UAAU,GAC1B,OAAO,CAAC,4BAA4B,CAAC,CAsBvC"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aroha Protocol — Human Credential System (Layer 1 extension)
|
|
3
|
+
*
|
|
4
|
+
* Human users are not DID-bearing agents. They authenticate to their Personal
|
|
5
|
+
* Agent (out of band — password, OAuth, passkey, etc.), which then issues them
|
|
6
|
+
* a short-lived signed credential. That credential travels in the body of any
|
|
7
|
+
* Aroha message the user triggers, letting provider agents verify who initiated
|
|
8
|
+
* the request and what they are allowed to do.
|
|
9
|
+
*
|
|
10
|
+
* The credential is signed by the issuing agent's Ed25519 private key using
|
|
11
|
+
* the same canonicalization rules as Aroha envelope proofs.
|
|
12
|
+
*/
|
|
13
|
+
import * as ed from "@noble/ed25519";
|
|
14
|
+
import { sha512 } from "@noble/hashes/sha512";
|
|
15
|
+
import { v4 as uuidv4 } from "uuid";
|
|
16
|
+
ed.etc.sha512Sync = (...m) => sha512(...m);
|
|
17
|
+
// ─── Issue ────────────────────────────────────────────────────────────────────
|
|
18
|
+
/**
|
|
19
|
+
* Issue a signed HumanCredential.
|
|
20
|
+
*
|
|
21
|
+
* @param userId Application user identifier
|
|
22
|
+
* @param roles Roles to grant (e.g. ["human:user"])
|
|
23
|
+
* @param issuerDID DID of the issuing agent (the Personal Agent)
|
|
24
|
+
* @param privateKey Ed25519 private key of the issuing agent (32 bytes)
|
|
25
|
+
* @param ttlMs Time-to-live in milliseconds (default: 1 hour)
|
|
26
|
+
* @param email Optional email for display purposes
|
|
27
|
+
*/
|
|
28
|
+
export async function issueHumanCredential(userId, roles, issuerDID, privateKey, ttlMs = 3_600_000, email) {
|
|
29
|
+
const now = new Date();
|
|
30
|
+
const credential = {
|
|
31
|
+
credentialId: uuidv4(),
|
|
32
|
+
userId,
|
|
33
|
+
roles,
|
|
34
|
+
issuedAt: now.toISOString(),
|
|
35
|
+
expiresAt: new Date(now.getTime() + ttlMs).toISOString(),
|
|
36
|
+
issuerDID,
|
|
37
|
+
...(email ? { email } : {}),
|
|
38
|
+
};
|
|
39
|
+
const canonical = canonicalizeCredential(credential);
|
|
40
|
+
const bytes = new TextEncoder().encode(canonical);
|
|
41
|
+
const sig = await ed.signAsync(bytes, privateKey);
|
|
42
|
+
return {
|
|
43
|
+
...credential,
|
|
44
|
+
signature: Buffer.from(sig).toString("base64url"),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Serialize a HumanCredential to a compact base64url token suitable for
|
|
49
|
+
* embedding in a Aroha message body as `credentialToken`.
|
|
50
|
+
*/
|
|
51
|
+
export function serializeCredential(cred) {
|
|
52
|
+
return Buffer.from(JSON.stringify(cred)).toString("base64url");
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Deserialize a credentialToken back to a HumanCredential object.
|
|
56
|
+
* Returns null if the token is malformed.
|
|
57
|
+
*/
|
|
58
|
+
export function deserializeCredential(token) {
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(Buffer.from(token, "base64url").toString("utf8"));
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// ─── Verify ───────────────────────────────────────────────────────────────────
|
|
67
|
+
/**
|
|
68
|
+
* Verify a HumanCredential.
|
|
69
|
+
*
|
|
70
|
+
* Checks:
|
|
71
|
+
* 1. Token is parseable
|
|
72
|
+
* 2. Expiry has not passed
|
|
73
|
+
* 3. Ed25519 signature is valid against the issuer's public key
|
|
74
|
+
*
|
|
75
|
+
* @param token The base64url credentialToken from the message body
|
|
76
|
+
* @param issuerPublicKey Ed25519 public key of the issuer DID (32 bytes)
|
|
77
|
+
*/
|
|
78
|
+
export async function verifyHumanCredential(token, issuerPublicKey) {
|
|
79
|
+
const cred = deserializeCredential(token);
|
|
80
|
+
if (!cred) {
|
|
81
|
+
return { valid: false, reason: "Credential token is malformed" };
|
|
82
|
+
}
|
|
83
|
+
if (new Date(cred.expiresAt) < new Date()) {
|
|
84
|
+
return { valid: false, reason: `Credential expired at ${cred.expiresAt}` };
|
|
85
|
+
}
|
|
86
|
+
const { signature, ...body } = cred;
|
|
87
|
+
const canonical = canonicalizeCredential(body);
|
|
88
|
+
const bytes = new TextEncoder().encode(canonical);
|
|
89
|
+
try {
|
|
90
|
+
const sig = Buffer.from(signature, "base64url");
|
|
91
|
+
const ok = await ed.verifyAsync(sig, bytes, issuerPublicKey);
|
|
92
|
+
if (!ok)
|
|
93
|
+
return { valid: false, reason: "Credential signature invalid" };
|
|
94
|
+
return { valid: true, credential: cred };
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return { valid: false, reason: "Credential signature verification threw an error" };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// ─── Canonicalization ─────────────────────────────────────────────────────────
|
|
101
|
+
/** Canonical JSON for signing — excludes `signature`, sorts keys. */
|
|
102
|
+
function canonicalizeCredential(cred) {
|
|
103
|
+
return JSON.stringify(sortKeysDeep(cred));
|
|
104
|
+
}
|
|
105
|
+
function sortKeysDeep(value) {
|
|
106
|
+
if (Array.isArray(value))
|
|
107
|
+
return value.map(sortKeysDeep);
|
|
108
|
+
if (value !== null && typeof value === "object") {
|
|
109
|
+
const sorted = {};
|
|
110
|
+
for (const key of Object.keys(value).sort()) {
|
|
111
|
+
sorted[key] = sortKeysDeep(value[key]);
|
|
112
|
+
}
|
|
113
|
+
return sorted;
|
|
114
|
+
}
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=credentials.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials.js","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials.test.d.ts","sourceRoot":"","sources":["../src/credentials.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import * as ed from "@noble/ed25519";
|
|
3
|
+
import { sha512 } from "@noble/hashes/sha512";
|
|
4
|
+
import { issueHumanCredential, verifyHumanCredential, serializeCredential, deserializeCredential, } from "./credentials.js";
|
|
5
|
+
import { ArohaRole } from "./types.js";
|
|
6
|
+
ed.etc.sha512Sync = (...m) => sha512(...m);
|
|
7
|
+
async function makeIssuer() {
|
|
8
|
+
const priv = ed.utils.randomPrivateKey();
|
|
9
|
+
const pub = await ed.getPublicKeyAsync(priv);
|
|
10
|
+
return { priv, pub, did: "did:aroha:issuer" };
|
|
11
|
+
}
|
|
12
|
+
describe("issueHumanCredential", () => {
|
|
13
|
+
it("issues a credential with correct fields", async () => {
|
|
14
|
+
const issuer = await makeIssuer();
|
|
15
|
+
const cred = await issueHumanCredential("user-123", [ArohaRole.HumanUser], issuer.did, issuer.priv, 3_600_000, "user@example.com");
|
|
16
|
+
expect(cred.userId).toBe("user-123");
|
|
17
|
+
expect(cred.roles).toContain(ArohaRole.HumanUser);
|
|
18
|
+
expect(cred.issuerDID).toBe(issuer.did);
|
|
19
|
+
expect(cred.email).toBe("user@example.com");
|
|
20
|
+
expect(cred.signature).toBeTruthy();
|
|
21
|
+
expect(new Date(cred.expiresAt) > new Date()).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
describe("verifyHumanCredential", () => {
|
|
25
|
+
it("verifies a valid credential token", async () => {
|
|
26
|
+
const issuer = await makeIssuer();
|
|
27
|
+
const cred = await issueHumanCredential("user-456", [ArohaRole.HumanAdmin], issuer.did, issuer.priv);
|
|
28
|
+
const token = serializeCredential(cred);
|
|
29
|
+
const result = await verifyHumanCredential(token, issuer.pub);
|
|
30
|
+
expect(result.valid).toBe(true);
|
|
31
|
+
expect(result.credential?.userId).toBe("user-456");
|
|
32
|
+
});
|
|
33
|
+
it("rejects a token signed by a different key", async () => {
|
|
34
|
+
const issuer = await makeIssuer();
|
|
35
|
+
const otherIssuer = await makeIssuer();
|
|
36
|
+
const cred = await issueHumanCredential("user-789", [ArohaRole.HumanUser], issuer.did, issuer.priv);
|
|
37
|
+
const token = serializeCredential(cred);
|
|
38
|
+
const result = await verifyHumanCredential(token, otherIssuer.pub);
|
|
39
|
+
expect(result.valid).toBe(false);
|
|
40
|
+
expect(result.reason).toMatch(/invalid/i);
|
|
41
|
+
});
|
|
42
|
+
it("rejects a malformed token", async () => {
|
|
43
|
+
const issuer = await makeIssuer();
|
|
44
|
+
const result = await verifyHumanCredential("not-valid-base64url!!!", issuer.pub);
|
|
45
|
+
expect(result.valid).toBe(false);
|
|
46
|
+
expect(result.reason).toMatch(/malformed/i);
|
|
47
|
+
});
|
|
48
|
+
it("rejects an expired credential", async () => {
|
|
49
|
+
const issuer = await makeIssuer();
|
|
50
|
+
const cred = await issueHumanCredential("user-exp", [ArohaRole.HumanUser], issuer.did, issuer.priv, -1000);
|
|
51
|
+
const token = serializeCredential(cred);
|
|
52
|
+
const result = await verifyHumanCredential(token, issuer.pub);
|
|
53
|
+
expect(result.valid).toBe(false);
|
|
54
|
+
expect(result.reason).toMatch(/expired/i);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
describe("serializeCredential / deserializeCredential", () => {
|
|
58
|
+
it("round-trips a credential", async () => {
|
|
59
|
+
const issuer = await makeIssuer();
|
|
60
|
+
const cred = await issueHumanCredential("round-trip", [ArohaRole.HumanReadonly], issuer.did, issuer.priv);
|
|
61
|
+
const token = serializeCredential(cred);
|
|
62
|
+
const decoded = deserializeCredential(token);
|
|
63
|
+
expect(decoded).toEqual(cred);
|
|
64
|
+
});
|
|
65
|
+
it("returns null for garbage input", () => {
|
|
66
|
+
expect(deserializeCredential("!!!")).toBeNull();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
//# sourceMappingURL=credentials.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials.test.js","sourceRoot":"","sources":["../src/credentials.test.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @aroha-sdk/credentials — ArohaMandateCredential
|
|
3
|
+
*
|
|
4
|
+
* Issues, attenuates, and verifies ArohaSpendingMandate tokens.
|
|
5
|
+
*
|
|
6
|
+
* The mandate chain enforces monotonic scope narrowing:
|
|
7
|
+
* User → Intent Mandate (max budget)
|
|
8
|
+
* Personal Agent → Cart Mandate (narrowed to specific service)
|
|
9
|
+
* Provider Agent → Payment Mandate (single-transaction authorisation)
|
|
10
|
+
*
|
|
11
|
+
* Each mandate is Ed25519-signed by the grantor. The grantee carries the
|
|
12
|
+
* token in the ArohaSpendingMandate message body — the payment processor
|
|
13
|
+
* verifies the full chain without ever seeing the user's card number.
|
|
14
|
+
*
|
|
15
|
+
* References:
|
|
16
|
+
* AP2 Protocol (Google) Intent/Cart/Payment Mandate pattern.
|
|
17
|
+
* IETF draft-niyikiza-oauth-attenuating-agent-tokens (2024).
|
|
18
|
+
*/
|
|
19
|
+
import { type ArohaSpendingMandateBody, type SpendingConstraints } from "@aroha-sdk/core";
|
|
20
|
+
export interface SignedMandate {
|
|
21
|
+
mandate: ArohaSpendingMandateBody;
|
|
22
|
+
/** base64url Ed25519 signature over the canonical mandate (proof). */
|
|
23
|
+
signature: string;
|
|
24
|
+
/** base64url serialised mandate + signature — embed in message body credentialToken. */
|
|
25
|
+
token: string;
|
|
26
|
+
}
|
|
27
|
+
export interface MandateVerificationResult {
|
|
28
|
+
valid: boolean;
|
|
29
|
+
mandate?: ArohaSpendingMandateBody;
|
|
30
|
+
reason?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Issue a root Intent Mandate from a user to their personal agent.
|
|
34
|
+
*
|
|
35
|
+
* @param grantorDID User's DID (or personal-agent acting on user's behalf)
|
|
36
|
+
* @param granteeDID Personal agent's DID
|
|
37
|
+
* @param constraints Maximum spend envelope — all downstream mandates must be ≤ these
|
|
38
|
+
* @param privateKey Grantor's Ed25519 private key (32 bytes)
|
|
39
|
+
* @param ttlMs Time-to-live in milliseconds (default: 1 hour)
|
|
40
|
+
*/
|
|
41
|
+
export declare function issueIntentMandate(grantorDID: string, granteeDID: string, constraints: SpendingConstraints, privateKey: Uint8Array, ttlMs?: number): Promise<SignedMandate>;
|
|
42
|
+
/**
|
|
43
|
+
* Attenuate a mandate to a narrower Cart Mandate.
|
|
44
|
+
* Validates that the new constraints are a subset of the parent's constraints.
|
|
45
|
+
*/
|
|
46
|
+
export declare function attenuateToCart(parent: SignedMandate, granteeDID: string, narrowedConstraints: SpendingConstraints, grantorPrivateKey: Uint8Array, ttlMs?: number): Promise<SignedMandate>;
|
|
47
|
+
/**
|
|
48
|
+
* Attenuate a Cart Mandate to a single-transaction Payment Mandate.
|
|
49
|
+
*/
|
|
50
|
+
export declare function attenuateToPayment(parent: SignedMandate, granteeDID: string, narrowedConstraints: SpendingConstraints, grantorPrivateKey: Uint8Array, ttlMs?: number): Promise<SignedMandate>;
|
|
51
|
+
/**
|
|
52
|
+
* Verify a mandate token.
|
|
53
|
+
*
|
|
54
|
+
* @param token The base64url mandate token from credentialToken field
|
|
55
|
+
* @param publicKey Grantor's Ed25519 public key (32 bytes)
|
|
56
|
+
* @param expectedGrantee If set, validates the grantee DID matches
|
|
57
|
+
*/
|
|
58
|
+
export declare function verifyMandate(token: string, publicKey: Uint8Array, expectedGrantee?: string): Promise<MandateVerificationResult>;
|
|
59
|
+
//# sourceMappingURL=mandate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mandate.d.ts","sourceRoot":"","sources":["../src/mandate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAKH,OAAO,EAAE,KAAK,wBAAwB,EAAE,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAI1F,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,wBAAwB,CAAC;IAClC,sEAAsE;IACtE,SAAS,EAAE,MAAM,CAAC;IAClB,wFAAwF;IACxF,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,wBAAwB,CAAC;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAID;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,mBAAmB,EAChC,UAAU,EAAE,UAAU,EACtB,KAAK,SAAY,GAChB,OAAO,CAAC,aAAa,CAAC,CAExB;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,aAAa,EACrB,UAAU,EAAE,MAAM,EAClB,mBAAmB,EAAE,mBAAmB,EACxC,iBAAiB,EAAE,UAAU,EAC7B,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,aAAa,CAAC,CAcxB;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,aAAa,EACrB,UAAU,EAAE,MAAM,EAClB,mBAAmB,EAAE,mBAAmB,EACxC,iBAAiB,EAAE,UAAU,EAC7B,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,aAAa,CAAC,CAcxB;AAID;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,UAAU,EACrB,eAAe,CAAC,EAAE,MAAM,GACvB,OAAO,CAAC,yBAAyB,CAAC,CAgCpC"}
|
package/dist/mandate.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @aroha-sdk/credentials — ArohaMandateCredential
|
|
3
|
+
*
|
|
4
|
+
* Issues, attenuates, and verifies ArohaSpendingMandate tokens.
|
|
5
|
+
*
|
|
6
|
+
* The mandate chain enforces monotonic scope narrowing:
|
|
7
|
+
* User → Intent Mandate (max budget)
|
|
8
|
+
* Personal Agent → Cart Mandate (narrowed to specific service)
|
|
9
|
+
* Provider Agent → Payment Mandate (single-transaction authorisation)
|
|
10
|
+
*
|
|
11
|
+
* Each mandate is Ed25519-signed by the grantor. The grantee carries the
|
|
12
|
+
* token in the ArohaSpendingMandate message body — the payment processor
|
|
13
|
+
* verifies the full chain without ever seeing the user's card number.
|
|
14
|
+
*
|
|
15
|
+
* References:
|
|
16
|
+
* AP2 Protocol (Google) Intent/Cart/Payment Mandate pattern.
|
|
17
|
+
* IETF draft-niyikiza-oauth-attenuating-agent-tokens (2024).
|
|
18
|
+
*/
|
|
19
|
+
import * as ed from "@noble/ed25519";
|
|
20
|
+
import { sha512 } from "@noble/hashes/sha512";
|
|
21
|
+
import { v4 as uuidv4 } from "uuid";
|
|
22
|
+
ed.etc.sha512Sync = (...m) => sha512(...m);
|
|
23
|
+
// ─── Issue ────────────────────────────────────────────────────────────────────
|
|
24
|
+
/**
|
|
25
|
+
* Issue a root Intent Mandate from a user to their personal agent.
|
|
26
|
+
*
|
|
27
|
+
* @param grantorDID User's DID (or personal-agent acting on user's behalf)
|
|
28
|
+
* @param granteeDID Personal agent's DID
|
|
29
|
+
* @param constraints Maximum spend envelope — all downstream mandates must be ≤ these
|
|
30
|
+
* @param privateKey Grantor's Ed25519 private key (32 bytes)
|
|
31
|
+
* @param ttlMs Time-to-live in milliseconds (default: 1 hour)
|
|
32
|
+
*/
|
|
33
|
+
export async function issueIntentMandate(grantorDID, granteeDID, constraints, privateKey, ttlMs = 3_600_000) {
|
|
34
|
+
return issueMandateInternal("intent", grantorDID, granteeDID, constraints, null, privateKey, ttlMs);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Attenuate a mandate to a narrower Cart Mandate.
|
|
38
|
+
* Validates that the new constraints are a subset of the parent's constraints.
|
|
39
|
+
*/
|
|
40
|
+
export async function attenuateToCart(parent, granteeDID, narrowedConstraints, grantorPrivateKey, ttlMs) {
|
|
41
|
+
assertNarrowing(parent.mandate.constraints, narrowedConstraints, parent.mandate.mandateTier, "cart");
|
|
42
|
+
const expiry = ttlMs
|
|
43
|
+
? Math.min(new Date(parent.mandate.expiresAt).getTime(), Date.now() + ttlMs)
|
|
44
|
+
: new Date(parent.mandate.expiresAt).getTime();
|
|
45
|
+
return issueMandateInternal("cart", parent.mandate.grantee, granteeDID, narrowedConstraints, parent.mandate.mandateId, grantorPrivateKey, expiry - Date.now());
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Attenuate a Cart Mandate to a single-transaction Payment Mandate.
|
|
49
|
+
*/
|
|
50
|
+
export async function attenuateToPayment(parent, granteeDID, narrowedConstraints, grantorPrivateKey, ttlMs) {
|
|
51
|
+
assertNarrowing(parent.mandate.constraints, narrowedConstraints, parent.mandate.mandateTier, "payment");
|
|
52
|
+
const expiry = ttlMs
|
|
53
|
+
? Math.min(new Date(parent.mandate.expiresAt).getTime(), Date.now() + ttlMs)
|
|
54
|
+
: new Date(parent.mandate.expiresAt).getTime();
|
|
55
|
+
return issueMandateInternal("payment", parent.mandate.grantee, granteeDID, narrowedConstraints, parent.mandate.mandateId, grantorPrivateKey, expiry - Date.now());
|
|
56
|
+
}
|
|
57
|
+
// ─── Verify ───────────────────────────────────────────────────────────────────
|
|
58
|
+
/**
|
|
59
|
+
* Verify a mandate token.
|
|
60
|
+
*
|
|
61
|
+
* @param token The base64url mandate token from credentialToken field
|
|
62
|
+
* @param publicKey Grantor's Ed25519 public key (32 bytes)
|
|
63
|
+
* @param expectedGrantee If set, validates the grantee DID matches
|
|
64
|
+
*/
|
|
65
|
+
export async function verifyMandate(token, publicKey, expectedGrantee) {
|
|
66
|
+
let signed;
|
|
67
|
+
try {
|
|
68
|
+
signed = JSON.parse(Buffer.from(token, "base64url").toString("utf8"));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return { valid: false, reason: "Malformed token" };
|
|
72
|
+
}
|
|
73
|
+
const { mandate, signature } = signed;
|
|
74
|
+
// Expiry check
|
|
75
|
+
if (new Date(mandate.expiresAt) < new Date()) {
|
|
76
|
+
return { valid: false, reason: "Mandate expired" };
|
|
77
|
+
}
|
|
78
|
+
// Grantee check
|
|
79
|
+
if (expectedGrantee && mandate.grantee !== expectedGrantee) {
|
|
80
|
+
return { valid: false, reason: `Mandate grantee mismatch: expected ${expectedGrantee}` };
|
|
81
|
+
}
|
|
82
|
+
// Signature check
|
|
83
|
+
try {
|
|
84
|
+
const canonical = canonicalizeMandate(mandate);
|
|
85
|
+
const bytes = new TextEncoder().encode(canonical);
|
|
86
|
+
const sigBytes = Buffer.from(signature, "base64url");
|
|
87
|
+
const valid = await ed.verifyAsync(sigBytes, bytes, publicKey);
|
|
88
|
+
if (!valid)
|
|
89
|
+
return { valid: false, reason: "Invalid signature" };
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return { valid: false, reason: "Signature verification error" };
|
|
93
|
+
}
|
|
94
|
+
return { valid: true, mandate };
|
|
95
|
+
}
|
|
96
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
97
|
+
async function issueMandateInternal(tier, grantorDID, granteeDID, constraints, parentMandateId, privateKey, ttlMs) {
|
|
98
|
+
const mandate = {
|
|
99
|
+
mandateTier: tier,
|
|
100
|
+
constraints,
|
|
101
|
+
grantee: granteeDID,
|
|
102
|
+
grantor: grantorDID,
|
|
103
|
+
expiresAt: new Date(Date.now() + ttlMs).toISOString(),
|
|
104
|
+
parentMandateId,
|
|
105
|
+
mandateId: uuidv4(),
|
|
106
|
+
};
|
|
107
|
+
const canonical = canonicalizeMandate(mandate);
|
|
108
|
+
const bytes = new TextEncoder().encode(canonical);
|
|
109
|
+
const sig = await ed.signAsync(bytes, privateKey);
|
|
110
|
+
const signature = Buffer.from(sig).toString("base64url");
|
|
111
|
+
const token = Buffer.from(JSON.stringify({ mandate, signature })).toString("base64url");
|
|
112
|
+
return { mandate, signature, token };
|
|
113
|
+
}
|
|
114
|
+
/** Canonical JSON — lexicographically sorted keys, no whitespace. */
|
|
115
|
+
function canonicalizeMandate(mandate) {
|
|
116
|
+
return JSON.stringify(sortKeys(mandate));
|
|
117
|
+
}
|
|
118
|
+
function sortKeys(obj) {
|
|
119
|
+
if (Array.isArray(obj))
|
|
120
|
+
return obj.map(sortKeys);
|
|
121
|
+
if (obj !== null && typeof obj === "object") {
|
|
122
|
+
return Object.keys(obj)
|
|
123
|
+
.sort()
|
|
124
|
+
.reduce((acc, k) => {
|
|
125
|
+
acc[k] = sortKeys(obj[k]);
|
|
126
|
+
return acc;
|
|
127
|
+
}, {});
|
|
128
|
+
}
|
|
129
|
+
return obj;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Validate that a narrowed constraint set is a subset of the parent's.
|
|
133
|
+
* Throws if any narrowed value exceeds the parent's limit.
|
|
134
|
+
*/
|
|
135
|
+
function assertNarrowing(parent, child, parentTier, childTier) {
|
|
136
|
+
const prefix = `Cannot attenuate ${parentTier} → ${childTier}`;
|
|
137
|
+
if (child.spendLimitUsd > parent.spendLimitUsd) {
|
|
138
|
+
throw new Error(`${prefix}: child spendLimitUsd (${child.spendLimitUsd}) exceeds parent (${parent.spendLimitUsd})`);
|
|
139
|
+
}
|
|
140
|
+
if (parent.sessionLimitUsd !== undefined &&
|
|
141
|
+
child.sessionLimitUsd !== undefined &&
|
|
142
|
+
child.sessionLimitUsd > parent.sessionLimitUsd) {
|
|
143
|
+
throw new Error(`${prefix}: child sessionLimitUsd exceeds parent`);
|
|
144
|
+
}
|
|
145
|
+
if (parent.requireHumanApprovalAboveUsd !== undefined &&
|
|
146
|
+
child.requireHumanApprovalAboveUsd !== undefined &&
|
|
147
|
+
child.requireHumanApprovalAboveUsd > parent.requireHumanApprovalAboveUsd) {
|
|
148
|
+
throw new Error(`${prefix}: child approval threshold exceeds parent`);
|
|
149
|
+
}
|
|
150
|
+
if (parent.allowedMerchants != null && parent.allowedMerchants.length > 0) {
|
|
151
|
+
const parentSet = new Set(parent.allowedMerchants);
|
|
152
|
+
if (!child.allowedMerchants || child.allowedMerchants.length === 0) {
|
|
153
|
+
throw new Error(`${prefix}: parent restricts allowedMerchants but child sets none`);
|
|
154
|
+
}
|
|
155
|
+
for (const m of child.allowedMerchants) {
|
|
156
|
+
if (!parentSet.has(m)) {
|
|
157
|
+
throw new Error(`${prefix}: child allowedMerchants contains "${m}" not in parent`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (parent.merchantCategory !== undefined && parent.merchantCategory !== null) {
|
|
162
|
+
if (child.merchantCategory === null || child.merchantCategory === undefined) {
|
|
163
|
+
throw new Error(`${prefix}: child merchantCategory must not widen parent's "${parent.merchantCategory}" restriction`);
|
|
164
|
+
}
|
|
165
|
+
if (child.merchantCategory !== parent.merchantCategory) {
|
|
166
|
+
throw new Error(`${prefix}: child merchantCategory "${child.merchantCategory}" does not match parent "${parent.merchantCategory}"`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (parent.validUntil !== undefined && child.validUntil !== undefined) {
|
|
170
|
+
if (new Date(child.validUntil) > new Date(parent.validUntil)) {
|
|
171
|
+
throw new Error(`${prefix}: child validUntil (${child.validUntil}) extends beyond parent (${parent.validUntil})`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (parent.validFrom !== undefined && child.validFrom !== undefined) {
|
|
175
|
+
if (new Date(child.validFrom) < new Date(parent.validFrom)) {
|
|
176
|
+
throw new Error(`${prefix}: child validFrom (${child.validFrom}) precedes parent (${parent.validFrom})`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=mandate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mandate.js","sourceRoot":"","sources":["../src/mandate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mandate.test.d.ts","sourceRoot":"","sources":["../src/mandate.test.ts"],"names":[],"mappings":""}
|