open-action-consent 0.0.0-stage → 0.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/gate.js ADDED
@@ -0,0 +1,143 @@
1
+ // A framework-neutral executor gate (twin of gate.py): tier, Preview, Consent hand-over, verification right before
2
+ // the action, run, signed Receipt. A pending Preview is reused for the same person and action until it expires, so
3
+ // a framework that re-runs a step after an interruption gets the Preview the person approved.
4
+ import * as crypto from "node:crypto";
5
+ import * as shapes from "./shapes.js";
6
+ import { OACError } from "./errors.js";
7
+ import { digest, KeySet, tokenDigest, verify } from "./jws.js";
8
+ import { MemoryNonces, verifyConsent } from "./consent.js";
9
+ import { actionDigest, buildPreview } from "./preview.js";
10
+ import { makeReceipt } from "./receipts.js";
11
+ import { tierFor } from "./tiers.js";
12
+ export class NeedsConsent extends Error {
13
+ preview;
14
+ constructor(preview) { super("the person must approve this preview"); this.preview = preview; }
15
+ }
16
+ export class Gate {
17
+ executor;
18
+ key;
19
+ signers;
20
+ nonces;
21
+ policyFloor;
22
+ profiles = {};
23
+ pending = new Map(); // person \0 action digest -> preview
24
+ consents = new Map();
25
+ receipts = new Map();
26
+ clock;
27
+ summarize;
28
+ constructor(o) {
29
+ this.executor = o.executor;
30
+ this.key = o.key;
31
+ this.signers = o.signers;
32
+ this.nonces = o.nonces ?? new MemoryNonces();
33
+ this.policyFloor = o.policyFloor ?? "A";
34
+ this.clock = o.clock ?? (() => Math.floor(Date.now() / 1000));
35
+ this.summarize = o.summarize ?? ((a) => `${a.tool}: ` + Object.keys(a.args).sort().map((k) => `${k}=${JSON.stringify(a.args[k])}`).join(", ").slice(0, 480));
36
+ }
37
+ profile(tool, p) {
38
+ const doc = { oac: "0.1", type: "action-profile", tool, effect: p.effect, undo: { kind: p.undo ?? "none", window_s: p.windowS ?? 0 },
39
+ tier_floor: p.tierFloor ?? "A", binds: { ...(p.binds ?? {}) } };
40
+ if (p.otherArgs?.length)
41
+ doc.other_args = [...p.otherArgs];
42
+ this.profiles[tool] = shapes.profile(doc);
43
+ return doc;
44
+ }
45
+ prof(tool) {
46
+ const p = Object.hasOwn(this.profiles, tool) ? this.profiles[tool] : undefined;
47
+ if (!p)
48
+ throw new OACError("unknown_tool", `${tool} has no OAC profile`);
49
+ return p;
50
+ }
51
+ tier(tool, args, tainted = true) {
52
+ const profile = this.prof(tool);
53
+ shapes.checkAction({ tool, args: { ...args } }, profile);
54
+ return tierFor(profile, { policyFloor: this.policyFloor, tainted });
55
+ }
56
+ preview(tool, args, person, evidence) {
57
+ const action = { tool, args: { ...args } };
58
+ const key = person + "\0" + actionDigest(action);
59
+ const now = this.clock();
60
+ let p = this.pending.get(key);
61
+ if (!p || now >= p.exp) {
62
+ p = buildPreview({ profile: this.prof(tool), action, executor: this.executor, sub: person, summary: this.summarize(action), now,
63
+ previewId: "pv_" + crypto.randomBytes(12).toString("base64url"), evidence });
64
+ this.pending.set(key, p);
65
+ }
66
+ return p;
67
+ }
68
+ previewById(id) { return [...this.pending.values()].find((p) => p.id === id); }
69
+ acceptConsent(token) {
70
+ let c;
71
+ try {
72
+ c = verify(token, this.signers, "oac-consent+jws");
73
+ shapes.consent(c);
74
+ }
75
+ catch {
76
+ return false;
77
+ }
78
+ const pending = new Set([...this.pending.values()].map(digest));
79
+ let kept = false;
80
+ for (const item of c.aud === this.executor ? c.items : []) {
81
+ if (pending.has(item.preview) && !this.consents.has(item.preview)) {
82
+ this.consents.set(item.preview, token);
83
+ kept = true;
84
+ }
85
+ }
86
+ return kept;
87
+ }
88
+ authorize(tool, args, person, tainted) {
89
+ const tier = this.tier(tool, args, tainted);
90
+ if (tier === "D")
91
+ throw new OACError("tier_d", "this action never runs");
92
+ if (tier === "A" || tier === "B")
93
+ return [tier, { kind: "tier" }, null];
94
+ const action = { tool, args: { ...args } };
95
+ const preview = this.preview(tool, args, person);
96
+ const pd = digest(preview);
97
+ const token = this.consents.get(pd);
98
+ if (!token)
99
+ throw new NeedsConsent(preview);
100
+ const c = verifyConsent(token, { keys: this.signers, executor: this.executor, sub: person, previews: [preview], actions: [action],
101
+ profiles: this.profiles, now: this.clock(), nonces: this.nonces, policyFloor: this.policyFloor, tainted });
102
+ this.pending.delete(person + "\0" + actionDigest(action));
103
+ return ["C", { kind: "consent", ref: c.jti }, pd];
104
+ }
105
+ async run(tool, args, fn, o) {
106
+ const [tier, authority, pd] = this.authorize(tool, args, o.person, o.tainted ?? true);
107
+ let result;
108
+ try {
109
+ result = await fn(args);
110
+ }
111
+ catch (e) {
112
+ this.receipt(o.person, tool, args, tier, authority, pd, null, true);
113
+ throw e;
114
+ }
115
+ return [result, this.receipt(o.person, tool, args, tier, authority, pd, result, false)];
116
+ }
117
+ receipt(person, tool, args, tier, authority, pd, result, failed) {
118
+ const profile = this.profiles[tool];
119
+ const chain = this.receipts.get(person) ?? [];
120
+ this.receipts.set(person, chain);
121
+ const now = this.clock();
122
+ const undo = { kind: profile.undo.kind };
123
+ if (undo.kind !== "none" && !failed) {
124
+ undo.until = now + profile.undo.window_s;
125
+ if (undo.kind === "native")
126
+ undo.token = "u_" + crypto.randomBytes(12).toString("base64url");
127
+ }
128
+ let rd;
129
+ try {
130
+ rd = digest(result ?? null);
131
+ }
132
+ catch {
133
+ rd = digest(String(result));
134
+ }
135
+ const payload = { iss: this.executor, sub: person, seq: chain.length + 1, prev: chain.length ? tokenDigest(chain[chain.length - 1]) : null,
136
+ iat: now, tool, tier, authority, action: actionDigest({ tool, args: { ...args } }), outcome: failed ? "failed" : "ok", result: rd, undo };
137
+ if (pd)
138
+ payload.preview = pd;
139
+ const r = makeReceipt(this.key, payload);
140
+ chain.push(r);
141
+ return r;
142
+ }
143
+ }
@@ -0,0 +1,12 @@
1
+ export { CODES, OACError } from "./errors.ts";
2
+ export { canonical, parse } from "./jcs.ts";
3
+ export { digest, generate, KeySet, sign, tokenDigest, verify, type Jwk, type SigningKey } from "./jws.ts";
4
+ export { tierFor } from "./tiers.ts";
5
+ export * as shapes from "./shapes.ts";
6
+ export { actionDigest, buildPreview, checkBinding } from "./preview.ts";
7
+ export { makeConsent, MemoryNonces, verifyConsent, type NonceStore } from "./consent.ts";
8
+ export { checkMandate, loosens, makeMandate } from "./mandate.ts";
9
+ export { makeReceipt, verifyChain } from "./receipts.ts";
10
+ export { checkUndo, makeUndo } from "./undo.ts";
11
+ export { Gate, NeedsConsent, type ProfileSpec } from "./gate.ts";
12
+ export declare const VERSION = "0.2.0";
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ // OAC (Open Action Consent) v0.1 reference SDK for TypeScript. Spec: spec/oac-v0.1.md.
2
+ export { CODES, OACError } from "./errors.js";
3
+ export { canonical, parse } from "./jcs.js";
4
+ export { digest, generate, KeySet, sign, tokenDigest, verify } from "./jws.js";
5
+ export { tierFor } from "./tiers.js";
6
+ export * as shapes from "./shapes.js";
7
+ export { actionDigest, buildPreview, checkBinding } from "./preview.js";
8
+ export { makeConsent, MemoryNonces, verifyConsent } from "./consent.js";
9
+ export { checkMandate, loosens, makeMandate } from "./mandate.js";
10
+ export { makeReceipt, verifyChain } from "./receipts.js";
11
+ export { checkUndo, makeUndo } from "./undo.js";
12
+ export { Gate, NeedsConsent } from "./gate.js";
13
+ export const VERSION = "0.2.0";
package/dist/jcs.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ export type Json = null | boolean | number | string | Json[] | {
2
+ [k: string]: Json;
3
+ };
4
+ export declare const MAX_DEPTH = 64;
5
+ export declare function canonical(v: unknown, depth?: number): string;
6
+ export declare function canonicalBytes(v: unknown): Uint8Array;
7
+ /** Parse JSON strictly: no duplicate keys, no fractions or exponents, no NaN, integers in the safe range. */
8
+ export declare function parse(input: string | Uint8Array): Json;
package/dist/jcs.js ADDED
@@ -0,0 +1,177 @@
1
+ // RFC 8785 (JCS) canonical JSON for the OAC subset, plus a strict parser. OAC carries integers only (money in minor
2
+ // units, time in unix seconds), so JCS reduces to: keys sorted by UTF-16 code units (JavaScript's default sort),
3
+ // no whitespace, strings as JSON.stringify writes them, integers in plain decimal within +-(2^53 - 1).
4
+ import { OACError } from "./errors.js";
5
+ export const MAX_DEPTH = 64; // arrays and objects nested deeper than this are refused
6
+ function str(s) {
7
+ if (!s.isWellFormed())
8
+ throw new OACError("malformed", "string is not valid Unicode");
9
+ return JSON.stringify(s);
10
+ }
11
+ export function canonical(v, depth = 0) {
12
+ if (v === null)
13
+ return "null";
14
+ if (v === true)
15
+ return "true";
16
+ if (v === false)
17
+ return "false";
18
+ if (typeof v === "number") {
19
+ if (!Number.isInteger(v))
20
+ throw new OACError("malformed", "non-integer numbers are not allowed");
21
+ if (!Number.isSafeInteger(v))
22
+ throw new OACError("malformed", "integer out of the safe range");
23
+ return String(v === 0 ? 0 : v);
24
+ }
25
+ if (typeof v === "string")
26
+ return str(v);
27
+ if (typeof v === "object" && depth >= MAX_DEPTH)
28
+ throw new OACError("malformed", "nested too deeply");
29
+ if (Array.isArray(v))
30
+ return "[" + v.map((x) => canonical(x, depth + 1)).join(",") + "]";
31
+ if (typeof v === "object") {
32
+ const o = v;
33
+ const keys = Object.keys(o).map((k) => [k, str(k)]); // Unicode check before sorting
34
+ return "{" + keys.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)).map(([k, s]) => s + ":" + canonical(o[k], depth + 1)).join(",") + "}";
35
+ }
36
+ throw new OACError("malformed", `type ${typeof v} cannot be canonicalized`);
37
+ }
38
+ export function canonicalBytes(v) {
39
+ return new TextEncoder().encode(canonical(v));
40
+ }
41
+ const WS = new Set([" ", "\t", "\n", "\r"]);
42
+ const ESC = { '"': '"', "\\": "\\", "/": "/", b: "\b", f: "\f", n: "\n", r: "\r", t: "\t" };
43
+ /** Parse JSON strictly: no duplicate keys, no fractions or exponents, no NaN, integers in the safe range. */
44
+ export function parse(input) {
45
+ let text;
46
+ try {
47
+ text = typeof input === "string" ? input : new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(input);
48
+ }
49
+ catch {
50
+ throw new OACError("malformed", "not UTF-8");
51
+ }
52
+ let i = 0, depth = 0;
53
+ const bad = (why) => { throw new OACError("malformed", why); };
54
+ const ws = () => { while (i < text.length && WS.has(text[i]))
55
+ i++; };
56
+ const value = () => {
57
+ ws();
58
+ const c = text[i];
59
+ if (c === "{" || c === "[") {
60
+ depth++;
61
+ if (depth > MAX_DEPTH)
62
+ bad("nested too deeply");
63
+ }
64
+ if (c === "{") {
65
+ i++;
66
+ const entries = [];
67
+ const seen = new Set();
68
+ ws();
69
+ if (text[i] === "}") {
70
+ i++;
71
+ depth--;
72
+ return Object.fromEntries(entries);
73
+ }
74
+ for (;;) {
75
+ ws();
76
+ if (text[i] !== '"')
77
+ bad("expected a key");
78
+ const k = string();
79
+ if (seen.has(k))
80
+ bad(`duplicate key ${k}`);
81
+ seen.add(k);
82
+ ws();
83
+ if (text[i++] !== ":")
84
+ bad("expected :");
85
+ entries.push([k, value()]);
86
+ ws();
87
+ if (text[i] === ",") {
88
+ i++;
89
+ continue;
90
+ }
91
+ if (text[i] === "}") {
92
+ i++;
93
+ depth--;
94
+ return Object.fromEntries(entries);
95
+ }
96
+ bad("expected , or }");
97
+ }
98
+ }
99
+ if (c === "[") {
100
+ i++;
101
+ const out = [];
102
+ ws();
103
+ if (text[i] === "]") {
104
+ i++;
105
+ depth--;
106
+ return out;
107
+ }
108
+ for (;;) {
109
+ out.push(value());
110
+ ws();
111
+ if (text[i] === ",") {
112
+ i++;
113
+ continue;
114
+ }
115
+ if (text[i] === "]") {
116
+ i++;
117
+ depth--;
118
+ return out;
119
+ }
120
+ bad("expected , or ]");
121
+ }
122
+ }
123
+ if (c === '"')
124
+ return string();
125
+ for (const [word, val] of [["true", true], ["false", false], ["null", null]]) {
126
+ if (text.startsWith(word, i)) {
127
+ i += word.length;
128
+ return val;
129
+ }
130
+ }
131
+ const m = /^-?(0|[1-9][0-9]*)/.exec(text.slice(i));
132
+ if (!m)
133
+ return bad("not JSON");
134
+ i += m[0].length;
135
+ if (i < text.length && ".eE".includes(text[i]))
136
+ bad("non-integer numbers are not allowed");
137
+ const n = Number(m[0]);
138
+ if (!Number.isSafeInteger(n))
139
+ bad("integer out of the safe range");
140
+ return n;
141
+ };
142
+ const string = () => {
143
+ i++; // opening quote
144
+ let out = "";
145
+ for (;;) {
146
+ if (i >= text.length)
147
+ bad("unterminated string");
148
+ const c = text[i++];
149
+ if (c === '"')
150
+ return out;
151
+ if (c < " ")
152
+ bad("control character in string");
153
+ if (c !== "\\") {
154
+ out += c;
155
+ continue;
156
+ }
157
+ const e = text[i++];
158
+ if (e === "u") {
159
+ const hex = text.slice(i, i + 4);
160
+ if (!/^[0-9a-fA-F]{4}$/.test(hex))
161
+ bad("bad \\u escape");
162
+ out += String.fromCharCode(parseInt(hex, 16));
163
+ i += 4;
164
+ }
165
+ else if (e in ESC)
166
+ out += ESC[e];
167
+ else
168
+ bad("bad escape");
169
+ }
170
+ };
171
+ const v = value();
172
+ ws();
173
+ if (i !== text.length)
174
+ bad("trailing characters");
175
+ canonical(v); // Unicode check
176
+ return v;
177
+ }
package/dist/jws.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ import * as crypto from "node:crypto";
2
+ export declare const ALGS: string[];
3
+ export type Jwk = {
4
+ kty: string;
5
+ crv: string;
6
+ x: string;
7
+ y?: string;
8
+ kid?: string;
9
+ };
10
+ export type SigningKey = {
11
+ alg: string;
12
+ private: crypto.KeyObject;
13
+ jwk: Jwk & {
14
+ kid: string;
15
+ };
16
+ };
17
+ export declare function b64url(data: Uint8Array): string;
18
+ export declare function b64urlDecode(text: string): Uint8Array;
19
+ export declare function digestBytes(data: Uint8Array): string;
20
+ /** The digest of a JSON value: sha-256 over its JCS bytes. */
21
+ export declare function digest(value: unknown): string;
22
+ export declare function thumbprint(jwk: Jwk): string;
23
+ export declare function ed25519FromSeed(seed: Uint8Array): SigningKey;
24
+ export declare function p256FromSeed(seed: Uint8Array): SigningKey;
25
+ export declare function generate(alg?: string): SigningKey;
26
+ /** Public keys a verifier trusts, grouped by issuer: {iss: [jwk, ...]}. A kid maps to one key and one issuer. */
27
+ export declare class KeySet {
28
+ private keys;
29
+ constructor(byIssuer: Record<string, Jwk[]>);
30
+ get(kid: string): (Jwk & {
31
+ kid: string;
32
+ }) | undefined;
33
+ issuer(kid: string): string | undefined;
34
+ }
35
+ export declare function sign(payload: Record<string, unknown>, key: SigningKey, typ: string): string;
36
+ /** Check the envelope and signature; return the payload. Field checks are the caller's. */
37
+ export declare function verify(token: string, keys: KeySet, typ: string): Record<string, any>;
38
+ /** The digest a later object uses to name a signed token (receipt chains, undo requests). */
39
+ export declare function tokenDigest(token: string): string;
package/dist/jws.js ADDED
@@ -0,0 +1,175 @@
1
+ // Keys, digests and the JWS envelope (RFC 7515 compact). Header = exactly {alg, kid, typ}, JCS-canonical; payload =
2
+ // JCS-canonical JSON; a token whose bytes are not the canonical form of what they parse to is refused.
3
+ // Algorithms: EdDSA (Ed25519, RFC 8037) and ES256 (P-256, raw r||s, low-S only). kid = the RFC 7638 JWK thumbprint.
4
+ // Every trusted key belongs to exactly one issuer, and a token's `iss` must be its key's issuer.
5
+ import * as crypto from "node:crypto";
6
+ import { OACError } from "./errors.js";
7
+ import { canonical, canonicalBytes, parse } from "./jcs.js";
8
+ export const ALGS = ["EdDSA", "ES256"];
9
+ const B64URL = /^[A-Za-z0-9_-]*$/;
10
+ const enc = new TextEncoder();
11
+ export function b64url(data) {
12
+ return Buffer.from(data).toString("base64url");
13
+ }
14
+ export function b64urlDecode(text) {
15
+ if (typeof text !== "string" || !B64URL.test(text) || text.length % 4 === 1)
16
+ throw new OACError("malformed", "not base64url");
17
+ const raw = Buffer.from(text, "base64url");
18
+ if (b64url(raw) !== text)
19
+ throw new OACError("malformed", "non-canonical base64url");
20
+ return raw;
21
+ }
22
+ export function digestBytes(data) {
23
+ return "sha-256:" + b64url(crypto.createHash("sha256").update(data).digest());
24
+ }
25
+ /** The digest of a JSON value: sha-256 over its JCS bytes. */
26
+ export function digest(value) {
27
+ return digestBytes(canonicalBytes(value));
28
+ }
29
+ export function thumbprint(jwk) {
30
+ let members;
31
+ if (jwk.kty === "OKP")
32
+ members = { crv: jwk.crv, kty: jwk.kty, x: jwk.x };
33
+ else if (jwk.kty === "EC")
34
+ members = { crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y };
35
+ else
36
+ throw new OACError("malformed", "unsupported key type");
37
+ return b64url(crypto.createHash("sha256").update(canonicalBytes(members)).digest());
38
+ }
39
+ function withKid(jwk) {
40
+ return { ...jwk, kid: thumbprint(jwk) };
41
+ }
42
+ const ED25519_PKCS8 = Buffer.from("302e020100300506032b657004220420", "hex");
43
+ const P256_ORDER = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n;
44
+ export function ed25519FromSeed(seed) {
45
+ const priv = crypto.createPrivateKey({ key: Buffer.concat([ED25519_PKCS8, Buffer.from(seed)]), format: "der", type: "pkcs8" });
46
+ const pub = crypto.createPublicKey(priv).export({ format: "jwk" });
47
+ return { alg: "EdDSA", private: priv, jwk: withKid({ kty: "OKP", crv: "Ed25519", x: pub.x }) };
48
+ }
49
+ export function p256FromSeed(seed) {
50
+ const d = (BigInt("0x" + Buffer.from(seed).toString("hex")) % (P256_ORDER - 1n)) + 1n;
51
+ const dBytes = Buffer.from(d.toString(16).padStart(64, "0"), "hex");
52
+ const ecdh = crypto.createECDH("prime256v1");
53
+ ecdh.setPrivateKey(dBytes);
54
+ const point = ecdh.getPublicKey(); // 04 || x || y
55
+ const jwk = { kty: "EC", crv: "P-256", x: b64url(point.subarray(1, 33)), y: b64url(point.subarray(33, 65)) };
56
+ const priv = crypto.createPrivateKey({ key: { ...jwk, d: b64url(dBytes) }, format: "jwk" });
57
+ return { alg: "ES256", private: priv, jwk: withKid(jwk) };
58
+ }
59
+ export function generate(alg = "EdDSA") {
60
+ if (alg === "EdDSA")
61
+ return ed25519FromSeed(crypto.randomBytes(32));
62
+ if (alg === "ES256") {
63
+ const { privateKey } = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" });
64
+ const pub = crypto.createPublicKey(privateKey).export({ format: "jwk" });
65
+ return { alg, private: privateKey, jwk: withKid({ kty: "EC", crv: "P-256", x: pub.x, y: pub.y }) };
66
+ }
67
+ throw new OACError("bad_alg", alg);
68
+ }
69
+ /** Public keys a verifier trusts, grouped by issuer: {iss: [jwk, ...]}. A kid maps to one key and one issuer. */
70
+ export class KeySet {
71
+ keys = new Map();
72
+ constructor(byIssuer) {
73
+ if (byIssuer === null || typeof byIssuer !== "object" || Array.isArray(byIssuer)) {
74
+ throw new OACError("malformed", "keys are grouped by issuer: {iss: [jwk, ...]}");
75
+ }
76
+ for (const [iss, jwks] of Object.entries(byIssuer)) {
77
+ for (const jwk of jwks) {
78
+ const kid = thumbprint(jwk);
79
+ if (jwk.kid !== undefined && jwk.kid !== kid)
80
+ throw new OACError("malformed", "kid is not the key's thumbprint");
81
+ const prev = this.keys.get(kid);
82
+ if (prev && prev[1] !== iss)
83
+ throw new OACError("malformed", "one key cannot belong to two issuers");
84
+ this.keys.set(kid, [{ ...jwk, kid }, iss]);
85
+ }
86
+ }
87
+ }
88
+ get(kid) {
89
+ return this.keys.get(kid)?.[0];
90
+ }
91
+ issuer(kid) {
92
+ return this.keys.get(kid)?.[1];
93
+ }
94
+ }
95
+ const HALF_N = P256_ORDER / 2n;
96
+ const big = (b) => BigInt("0x" + (Buffer.from(b).toString("hex") || "0"));
97
+ export function sign(payload, key, typ) {
98
+ const header = { alg: key.alg, kid: key.jwk.kid, typ };
99
+ const input = b64url(canonicalBytes(header)) + "." + b64url(canonicalBytes(payload));
100
+ let sig = key.alg === "EdDSA"
101
+ ? crypto.sign(null, enc.encode(input), key.private)
102
+ : crypto.sign("sha256", enc.encode(input), { key: key.private, dsaEncoding: "ieee-p1363" });
103
+ if (key.alg === "ES256" && big(sig.subarray(32)) > HALF_N) { // low-S: the other spelling of the same signature
104
+ const s = (P256_ORDER - big(sig.subarray(32))).toString(16).padStart(64, "0");
105
+ sig = Buffer.concat([sig.subarray(0, 32), Buffer.from(s, "hex")]);
106
+ }
107
+ return input + "." + b64url(sig);
108
+ }
109
+ function checkSig(jwk, alg, data, sig) {
110
+ let ok;
111
+ try {
112
+ if (alg === "EdDSA") {
113
+ if (jwk.kty !== "OKP" || jwk.crv !== "Ed25519")
114
+ throw new OACError("bad_alg", "key is not Ed25519");
115
+ const pub = crypto.createPublicKey({ key: { kty: "OKP", crv: "Ed25519", x: jwk.x }, format: "jwk" });
116
+ ok = crypto.verify(null, data, pub, sig);
117
+ }
118
+ else {
119
+ if (jwk.kty !== "EC" || jwk.crv !== "P-256")
120
+ throw new OACError("bad_alg", "key is not P-256");
121
+ if (sig.length !== 64)
122
+ throw new OACError("bad_signature", "ES256 signature must be 64 bytes");
123
+ if (big(sig.subarray(32)) > HALF_N)
124
+ throw new OACError("bad_signature", "ES256 signature must be low-S");
125
+ const pub = crypto.createPublicKey({ key: { kty: "EC", crv: "P-256", x: jwk.x, y: jwk.y }, format: "jwk" });
126
+ ok = crypto.verify("sha256", data, { key: pub, dsaEncoding: "ieee-p1363" }, sig);
127
+ }
128
+ }
129
+ catch (e) {
130
+ if (e instanceof OACError)
131
+ throw e;
132
+ throw new OACError("bad_signature", "invalid key or signature encoding");
133
+ }
134
+ if (!ok)
135
+ throw new OACError("bad_signature");
136
+ }
137
+ function canonicalPart(part) {
138
+ const raw = b64urlDecode(part);
139
+ const value = parse(raw);
140
+ if (Buffer.compare(Buffer.from(canonicalBytes(value)), Buffer.from(raw)) !== 0)
141
+ throw new OACError("not_canonical");
142
+ return value;
143
+ }
144
+ /** Check the envelope and signature; return the payload. Field checks are the caller's. */
145
+ export function verify(token, keys, typ) {
146
+ if (typeof token !== "string" || !/^[\x00-\x7f]*$/.test(token))
147
+ throw new OACError("malformed", "token must be an ASCII string");
148
+ const parts = token.split(".");
149
+ if (parts.length !== 3)
150
+ throw new OACError("malformed", "a compact JWS has three parts");
151
+ const header = canonicalPart(parts[0]);
152
+ if (header === null || typeof header !== "object" || Array.isArray(header) || canonical(Object.keys(header).sort()) !== '["alg","kid","typ"]') {
153
+ throw new OACError("malformed", "header must be exactly alg, kid, typ");
154
+ }
155
+ if (header.typ !== typ)
156
+ throw new OACError("bad_type", `expected ${typ}`);
157
+ if (!ALGS.includes(header.alg))
158
+ throw new OACError("bad_alg", String(header.alg));
159
+ const jwk = typeof header.kid === "string" ? keys.get(header.kid) : undefined;
160
+ if (jwk === undefined)
161
+ throw new OACError("unknown_key");
162
+ checkSig(jwk, header.alg, enc.encode(parts[0] + "." + parts[1]), b64urlDecode(parts[2]));
163
+ const payload = canonicalPart(parts[1]);
164
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload))
165
+ throw new OACError("malformed", "payload must be an object");
166
+ if (payload.iss !== keys.issuer(header.kid))
167
+ throw new OACError("wrong_issuer", "the token names another issuer than its key's");
168
+ return payload;
169
+ }
170
+ /** The digest a later object uses to name a signed token (receipt chains, undo requests). */
171
+ export function tokenDigest(token) {
172
+ if (typeof token !== "string" || !/^[\x00-\x7f]*$/.test(token))
173
+ throw new OACError("malformed", "token must be an ASCII string");
174
+ return digestBytes(enc.encode(token));
175
+ }
@@ -0,0 +1,24 @@
1
+ import { KeySet, type SigningKey } from "./jws.ts";
2
+ export declare const TYP = "oac-mandate+jws";
3
+ export declare const MAX_S: number;
4
+ export declare function makeMandate(key: SigningKey, payload: Record<string, any>): string;
5
+ export type Usage = {
6
+ calls_today?: number;
7
+ calls_this_run?: number;
8
+ amount_today_minor?: number;
9
+ };
10
+ export declare function checkMandate(token: string, o: {
11
+ keys: KeySet;
12
+ action: Record<string, any>;
13
+ profile: Record<string, any>;
14
+ holder: Record<string, any>;
15
+ sub: string;
16
+ now: number;
17
+ tainted: boolean;
18
+ usage?: Usage | null;
19
+ revoked?: Iterable<string>;
20
+ statusLists?: Record<string, string>;
21
+ policyFloor?: string;
22
+ }): Record<string, any>;
23
+ /** What a replacement mandate allows that the old one did not; non-empty = the signer needs fresh proof (spec 7.4). */
24
+ export declare function loosens(old: Record<string, any>, nw: Record<string, any>): string[];