@meantjs/consent 0.3.0 → 0.5.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/CHANGELOG.md +31 -0
- package/README.md +60 -7
- package/package.json +7 -3
- package/src/consent.mjs +12 -4
- package/src/erc7730.d.mts +2 -0
- package/src/erc7730.mjs +17 -1
- package/src/errors.mjs +1 -0
- package/src/evm.d.mts +9 -1
- package/src/evm.mjs +38 -3
- package/src/grant.mjs +140 -0
- package/src/index.d.mts +28 -0
- package/src/render.mjs +1 -1
- package/src/sha256.mjs +60 -0
- package/src/terms.d.mts +103 -0
- package/src/terms.mjs +500 -0
package/src/sha256.mjs
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// SHA-256 in plain JavaScript, synchronous, so that the id of confirmed terms (terms.mjs) needs no node:crypto and the
|
|
2
|
+
// ./terms subpath bundles for a browser (WebCrypto is asynchronous). The same code as the resolver's src/sha256.mjs;
|
|
3
|
+
// test/terms.test.mjs compares it with node:crypto.
|
|
4
|
+
|
|
5
|
+
const K = new Uint32Array([
|
|
6
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
7
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
8
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
9
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
const rotr = (x, n) => (x >>> n) | (x << (32 - n));
|
|
13
|
+
|
|
14
|
+
/** SHA-256 of a byte array, as 32 bytes. */
|
|
15
|
+
export function sha256(bytes) {
|
|
16
|
+
const h = new Uint32Array([0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]);
|
|
17
|
+
const bitLength = bytes.length * 8;
|
|
18
|
+
const padded = new Uint8Array(((bytes.length + 9 + 63) >> 6) << 6);
|
|
19
|
+
padded.set(bytes);
|
|
20
|
+
padded[bytes.length] = 0x80;
|
|
21
|
+
const view = new DataView(padded.buffer);
|
|
22
|
+
view.setUint32(padded.length - 8, Math.floor(bitLength / 2 ** 32), false);
|
|
23
|
+
view.setUint32(padded.length - 4, bitLength >>> 0, false);
|
|
24
|
+
const w = new Uint32Array(64);
|
|
25
|
+
for (let off = 0; off < padded.length; off += 64) {
|
|
26
|
+
for (let i = 0; i < 16; i++) w[i] = view.getUint32(off + 4 * i, false);
|
|
27
|
+
for (let i = 16; i < 64; i++) {
|
|
28
|
+
const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >>> 3);
|
|
29
|
+
const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >>> 10);
|
|
30
|
+
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) | 0;
|
|
31
|
+
}
|
|
32
|
+
let [a, b, c, d, e, f, g, hh] = h;
|
|
33
|
+
for (let i = 0; i < 64; i++) {
|
|
34
|
+
const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
35
|
+
const ch = (e & f) ^ (~e & g);
|
|
36
|
+
const t1 = (hh + S1 + ch + K[i] + w[i]) | 0;
|
|
37
|
+
const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
38
|
+
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
39
|
+
const t2 = (S0 + maj) | 0;
|
|
40
|
+
hh = g;
|
|
41
|
+
g = f;
|
|
42
|
+
f = e;
|
|
43
|
+
e = (d + t1) | 0;
|
|
44
|
+
d = c;
|
|
45
|
+
c = b;
|
|
46
|
+
b = a;
|
|
47
|
+
a = (t1 + t2) | 0;
|
|
48
|
+
}
|
|
49
|
+
h[0] += a; h[1] += b; h[2] += c; h[3] += d; h[4] += e; h[5] += f; h[6] += g; h[7] += hh;
|
|
50
|
+
}
|
|
51
|
+
const out = new Uint8Array(32);
|
|
52
|
+
const ov = new DataView(out.buffer);
|
|
53
|
+
h.forEach((x, i) => ov.setUint32(4 * i, x, false));
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Lowercase hex SHA-256 of a string's UTF-8 bytes. */
|
|
58
|
+
export function sha256Hex(text) {
|
|
59
|
+
return Array.from(sha256(new TextEncoder().encode(text)), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
60
|
+
}
|
package/src/terms.d.mts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Types for @meantjs/consent/terms: terms the user confirms for one task, and the check of a request's grant against
|
|
2
|
+
// them (spec section 5.3, "Confirmed terms", D-056). Hand-written; the source is src/terms.mjs. scripts/types.test.mjs
|
|
3
|
+
// keeps the declared names equal to the runtime exports.
|
|
4
|
+
import type { Grant } from "./index.mjs";
|
|
5
|
+
|
|
6
|
+
/** A term the agent wrote that cannot be read, or a step out of order: `message` is written for the agent. */
|
|
7
|
+
export declare class TermsError extends Error {
|
|
8
|
+
constructor(code: string, message: string);
|
|
9
|
+
readonly code: TermsCode;
|
|
10
|
+
}
|
|
11
|
+
export declare const TERMS_CODES: {
|
|
12
|
+
/** A field is missing or empty, or both spender and to are given. */
|
|
13
|
+
readonly MISSING: "E_TERMS_MISSING";
|
|
14
|
+
/** A token the wallet does not know. */
|
|
15
|
+
readonly TOKEN: "E_TERMS_TOKEN";
|
|
16
|
+
/** An app the wallet does not know, or an address or account that is not one. */
|
|
17
|
+
readonly PARTY: "E_TERMS_PARTY";
|
|
18
|
+
/** An amount that cannot be read, or has more decimals than its token: never rounded. */
|
|
19
|
+
readonly AMOUNT: "E_TERMS_AMOUNT";
|
|
20
|
+
/** An end that cannot be read, or a date before today. */
|
|
21
|
+
readonly END: "E_TERMS_END";
|
|
22
|
+
/** termsGate: a step out of order. */
|
|
23
|
+
readonly ORDER: "E_TERMS_ORDER";
|
|
24
|
+
};
|
|
25
|
+
export type TermsCode = (typeof TERMS_CODES)[keyof typeof TERMS_CODES];
|
|
26
|
+
|
|
27
|
+
/** What the terms functions read from the wallet. Pure: the wallet's clock and zone come in here. */
|
|
28
|
+
export interface Wallet {
|
|
29
|
+
/** The tokens it knows, by symbol: an EVM token's address or a ledger's canister id, and its decimals. */
|
|
30
|
+
tokens: Record<string, { address: string; decimals: number } | { canister: string; decimals: number }>;
|
|
31
|
+
/** The apps and people it knows by name: an address, or an account in its ICRC-1 text. */
|
|
32
|
+
apps?: Record<string, string>;
|
|
33
|
+
/** Nanoseconds since the Unix epoch. */
|
|
34
|
+
now: bigint;
|
|
35
|
+
/** The user's zone (default 0, UTC): a date allows its whole day there. */
|
|
36
|
+
utcOffsetMinutes?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The terms as the agent writes them, a tool's arguments: `spender` or `to`, exactly one. */
|
|
40
|
+
export interface TermsFields {
|
|
41
|
+
/** The token, by the symbol the wallet knows. */
|
|
42
|
+
token: string;
|
|
43
|
+
/** Who may spend the tokens: an app's name as the wallet knows it, or an address or account the user gave. */
|
|
44
|
+
spender?: string;
|
|
45
|
+
/** Who is paid directly: a name the wallet knows, or an address or account the user gave. */
|
|
46
|
+
to?: string;
|
|
47
|
+
/** The most, in tokens ("100", "0.5", "100 USDC"), or "unlimited". */
|
|
48
|
+
max_amount: string;
|
|
49
|
+
/** YYYY-MM-DD (the whole day counts), a length from today ("30 days", "2 weeks", "1 month", "1 year"), hours from now ("2 hours", an exact time), or "never". */
|
|
50
|
+
expires_by: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** What the user allows for one task, in the wallet's own values: `spender` or `to`, exactly one. */
|
|
54
|
+
export interface Terms {
|
|
55
|
+
/** The token: an EIP-55 address, or a canister id in its canonical text. */
|
|
56
|
+
token: string;
|
|
57
|
+
symbol: string;
|
|
58
|
+
decimals: number;
|
|
59
|
+
spender?: string;
|
|
60
|
+
to?: string;
|
|
61
|
+
/** The app's name in the wallet's list, or null. */
|
|
62
|
+
name: string | null;
|
|
63
|
+
/** The most, in base units as a decimal string; `null` when unlimited. */
|
|
64
|
+
amount: { units: string; unlimited: false } | { units: null; unlimited: true };
|
|
65
|
+
/** `at`: the latest end, exact seconds since the Unix epoch as a decimal string; `day`: the local date whose whole day counts. */
|
|
66
|
+
ends: { at: string; day?: string } | { never: true };
|
|
67
|
+
utcOffsetMinutes: number;
|
|
68
|
+
/** The fields as the agent wrote them, trimmed. */
|
|
69
|
+
written: Record<string, string>;
|
|
70
|
+
}
|
|
71
|
+
/** Terms the user confirmed: `id` is the SHA-256 of their canonical JSON (RFC 8785), lowercase hex. */
|
|
72
|
+
export type ConfirmedTerms = Readonly<Terms & { id: string }>;
|
|
73
|
+
|
|
74
|
+
/** Reads the agent's terms. Throws TermsError when a term cannot be read; it never guesses a token, an app, decimals or a date. */
|
|
75
|
+
export function readTerms(fields: TermsFields | Record<string, unknown>, wallet: Wallet): Terms;
|
|
76
|
+
/** The terms in words, exactly, for the user to confirm on the wallet's own screen. */
|
|
77
|
+
export function termsText(terms: Terms | ConfirmedTerms): string;
|
|
78
|
+
/** The terms once the user has confirmed exactly `termsText(terms)`: frozen, with their `id`. */
|
|
79
|
+
export function confirmTerms(terms: Terms): ConfirmedTerms;
|
|
80
|
+
/** Whether a request's grant fits confirmed terms, and if not, why, in words. A null grant is not covered by terms. */
|
|
81
|
+
export function checkGrant(confirmed: ConfirmedTerms, grant: Grant | null, wallet: Wallet): { fits: boolean; reasons: string[] };
|
|
82
|
+
|
|
83
|
+
/** The order of one task: propose, confirm on the wallet's screen, read, check. Steps out of order throw TermsError E_TERMS_ORDER. */
|
|
84
|
+
export interface TermsGate {
|
|
85
|
+
readonly state: "open" | "proposed" | "confirmed" | "read";
|
|
86
|
+
readonly proposed: Readonly<Terms> | null;
|
|
87
|
+
readonly confirmed: ConfirmedTerms | null;
|
|
88
|
+
/** The agent's draft; replaces an earlier one and its confirmation. Returns the text to show the user. Refused once the request was read. */
|
|
89
|
+
propose(terms: Terms): string;
|
|
90
|
+
/** The user confirmed `shown`, which must be exactly the draft's text. */
|
|
91
|
+
confirm(shown: string): ConfirmedTerms;
|
|
92
|
+
/** The user said no: the draft is dropped. */
|
|
93
|
+
reject(): void;
|
|
94
|
+
/** Before the agent reads the request: refused until the user has confirmed terms. */
|
|
95
|
+
read(): void;
|
|
96
|
+
/**
|
|
97
|
+
* The grant of the request read. `requestId` names the request the wallet will sign or send: eip712Consent's `digest`,
|
|
98
|
+
* or for a call the wallet's own id of it. The terms cover one request, the first checked: another is refused, even
|
|
99
|
+
* one that grants the same.
|
|
100
|
+
*/
|
|
101
|
+
check(grant: Grant | null, wallet: Wallet, requestId: string): { fits: boolean; reasons: string[] };
|
|
102
|
+
}
|
|
103
|
+
export function termsGate(): TermsGate;
|