@jterrazz/attestation 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -0
- package/bin/attestation +2 -0
- package/dist/audit.cjs +57 -0
- package/dist/audit.cjs.map +1 -0
- package/dist/audit.js +52 -0
- package/dist/audit.js.map +1 -0
- package/dist/browser.cjs +183 -0
- package/dist/browser.cjs.map +1 -0
- package/dist/browser.d.cts +111 -0
- package/dist/browser.d.ts +111 -0
- package/dist/browser.js +168 -0
- package/dist/browser.js.map +1 -0
- package/dist/cli.cjs +284 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +285 -0
- package/dist/cli.js.map +1 -0
- package/dist/eip712-schema.cjs +96 -0
- package/dist/eip712-schema.cjs.map +1 -0
- package/dist/eip712-schema.d.cts +75 -0
- package/dist/eip712-schema.d.ts +75 -0
- package/dist/eip712-schema.js +73 -0
- package/dist/eip712-schema.js.map +1 -0
- package/dist/index.cjs +21 -0
- package/dist/index.d.cts +24 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +4 -0
- package/dist/node.cjs +279 -0
- package/dist/node.cjs.map +1 -0
- package/dist/node.d.cts +87 -0
- package/dist/node.d.ts +87 -0
- package/dist/node.js +274 -0
- package/dist/node.js.map +1 -0
- package/dist/sign-flow.cjs +323 -0
- package/dist/sign-flow.cjs.map +1 -0
- package/dist/sign-flow.js +277 -0
- package/dist/sign-flow.js.map +1 -0
- package/dist/verify.cjs +276 -0
- package/dist/verify.cjs.map +1 -0
- package/dist/verify.d.cts +105 -0
- package/dist/verify.d.ts +105 -0
- package/dist/verify.js +199 -0
- package/dist/verify.js.map +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { o as AttestationMessage } from "./eip712-schema.cjs";
|
|
2
|
+
import { LocalAccount } from "viem";
|
|
3
|
+
//#region src/core/canonicalize.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Canonicalize an article body for attestation v1.
|
|
6
|
+
*
|
|
7
|
+
* FROZEN CONTRACT — v1 rules below must NEVER change. Any modification requires
|
|
8
|
+
* publishing a v2 alongside (and v1 verifiers stay alive forever).
|
|
9
|
+
*
|
|
10
|
+
* Rules applied in order:
|
|
11
|
+
* 1. Reject unpaired UTF-16 surrogates (corrupt input).
|
|
12
|
+
* 2. Strip a leading BOM (U+FEFF) if present.
|
|
13
|
+
* 3. Apply Unicode NFC normalization.
|
|
14
|
+
* 4. Convert CRLF and stray CR to LF.
|
|
15
|
+
* 5. Trim trailing whitespace and append exactly one LF.
|
|
16
|
+
* 6. Encode as UTF-8 bytes.
|
|
17
|
+
*
|
|
18
|
+
* No markdown parsing. No per-line whitespace trim (would break " \n" soft breaks).
|
|
19
|
+
* No tab → space substitution (would break code blocks).
|
|
20
|
+
* No interior whitespace collapsing.
|
|
21
|
+
*/
|
|
22
|
+
declare function canonicalize(input: string): Uint8Array;
|
|
23
|
+
declare class InvalidContentError extends Error {
|
|
24
|
+
name: string;
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/attestation/types.d.ts
|
|
28
|
+
type SignedAttestation = AttestationMessage & {
|
|
29
|
+
signature: `0x${string}`;
|
|
30
|
+
signerAddress: `0x${string}`;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* On-disk shape (JSON-safe). `publishedAt` is stringified because JSON has no
|
|
34
|
+
* native bigint. Parse / stringify helpers in serialize.ts.
|
|
35
|
+
*/
|
|
36
|
+
type StoredAttestation = {
|
|
37
|
+
schemaVersion: number;
|
|
38
|
+
subject: {
|
|
39
|
+
title: string;
|
|
40
|
+
contentDigest: `0x${string}`;
|
|
41
|
+
locale: string;
|
|
42
|
+
};
|
|
43
|
+
claims: {
|
|
44
|
+
slug: string;
|
|
45
|
+
publishedAt: string;
|
|
46
|
+
revision: number;
|
|
47
|
+
priorAttestation: `0x${string}`;
|
|
48
|
+
};
|
|
49
|
+
signature: `0x${string}`;
|
|
50
|
+
signerAddress: `0x${string}`;
|
|
51
|
+
};
|
|
52
|
+
type VerifyOk = {
|
|
53
|
+
ok: true;
|
|
54
|
+
signerAddress: `0x${string}`;
|
|
55
|
+
};
|
|
56
|
+
type VerifyError = {
|
|
57
|
+
kind: 'content-mismatch';
|
|
58
|
+
expectedDigest: `0x${string}`;
|
|
59
|
+
actualDigest: `0x${string}`;
|
|
60
|
+
} | {
|
|
61
|
+
kind: 'invalid-signature';
|
|
62
|
+
reason: string;
|
|
63
|
+
} | {
|
|
64
|
+
kind: 'schema-version-unsupported';
|
|
65
|
+
version: number;
|
|
66
|
+
} | {
|
|
67
|
+
kind: 'signer-mismatch';
|
|
68
|
+
recovered: `0x${string}`;
|
|
69
|
+
declared: `0x${string}`;
|
|
70
|
+
};
|
|
71
|
+
type VerifyFail = {
|
|
72
|
+
ok: false;
|
|
73
|
+
error: VerifyError;
|
|
74
|
+
};
|
|
75
|
+
type VerifyResult = VerifyFail | VerifyOk;
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/attestation/create.d.ts
|
|
78
|
+
type CreateAttestationInput = {
|
|
79
|
+
content: string;
|
|
80
|
+
title: string;
|
|
81
|
+
slug: string;
|
|
82
|
+
locale: string;
|
|
83
|
+
publishedAt: bigint | Date | number;
|
|
84
|
+
revision?: number;
|
|
85
|
+
priorAttestation?: `0x${string}`;
|
|
86
|
+
};
|
|
87
|
+
declare function buildAttestationMessage(input: CreateAttestationInput): AttestationMessage;
|
|
88
|
+
declare function signAttestation(message: AttestationMessage, account: LocalAccount): Promise<SignedAttestation>;
|
|
89
|
+
declare function createAttestation(input: CreateAttestationInput, account: LocalAccount): Promise<SignedAttestation>;
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/attestation/serialize.d.ts
|
|
92
|
+
declare function toStored(att: SignedAttestation): StoredAttestation;
|
|
93
|
+
declare function fromStored(stored: StoredAttestation): SignedAttestation;
|
|
94
|
+
declare function stringify(att: SignedAttestation): string;
|
|
95
|
+
declare function parse(json: string): SignedAttestation;
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/attestation/verify.d.ts
|
|
98
|
+
type VerifyInput = {
|
|
99
|
+
content: string;
|
|
100
|
+
attestation: SignedAttestation;
|
|
101
|
+
};
|
|
102
|
+
declare function verifyAttestation(input: VerifyInput): Promise<VerifyResult>;
|
|
103
|
+
//#endregion
|
|
104
|
+
export { InvalidContentError as _, stringify as a, buildAttestationMessage as c, SignedAttestation as d, StoredAttestation as f, VerifyResult as g, VerifyOk as h, parse as i, createAttestation as l, VerifyFail as m, verifyAttestation as n, toStored as o, VerifyError as p, fromStored as r, CreateAttestationInput as s, VerifyInput as t, signAttestation as u, canonicalize as v };
|
|
105
|
+
//# sourceMappingURL=verify.d.cts.map
|
package/dist/verify.d.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { o as AttestationMessage } from "./eip712-schema.js";
|
|
2
|
+
import { LocalAccount } from "viem";
|
|
3
|
+
//#region src/core/canonicalize.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Canonicalize an article body for attestation v1.
|
|
6
|
+
*
|
|
7
|
+
* FROZEN CONTRACT — v1 rules below must NEVER change. Any modification requires
|
|
8
|
+
* publishing a v2 alongside (and v1 verifiers stay alive forever).
|
|
9
|
+
*
|
|
10
|
+
* Rules applied in order:
|
|
11
|
+
* 1. Reject unpaired UTF-16 surrogates (corrupt input).
|
|
12
|
+
* 2. Strip a leading BOM (U+FEFF) if present.
|
|
13
|
+
* 3. Apply Unicode NFC normalization.
|
|
14
|
+
* 4. Convert CRLF and stray CR to LF.
|
|
15
|
+
* 5. Trim trailing whitespace and append exactly one LF.
|
|
16
|
+
* 6. Encode as UTF-8 bytes.
|
|
17
|
+
*
|
|
18
|
+
* No markdown parsing. No per-line whitespace trim (would break " \n" soft breaks).
|
|
19
|
+
* No tab → space substitution (would break code blocks).
|
|
20
|
+
* No interior whitespace collapsing.
|
|
21
|
+
*/
|
|
22
|
+
declare function canonicalize(input: string): Uint8Array;
|
|
23
|
+
declare class InvalidContentError extends Error {
|
|
24
|
+
name: string;
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/attestation/types.d.ts
|
|
28
|
+
type SignedAttestation = AttestationMessage & {
|
|
29
|
+
signature: `0x${string}`;
|
|
30
|
+
signerAddress: `0x${string}`;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* On-disk shape (JSON-safe). `publishedAt` is stringified because JSON has no
|
|
34
|
+
* native bigint. Parse / stringify helpers in serialize.ts.
|
|
35
|
+
*/
|
|
36
|
+
type StoredAttestation = {
|
|
37
|
+
schemaVersion: number;
|
|
38
|
+
subject: {
|
|
39
|
+
title: string;
|
|
40
|
+
contentDigest: `0x${string}`;
|
|
41
|
+
locale: string;
|
|
42
|
+
};
|
|
43
|
+
claims: {
|
|
44
|
+
slug: string;
|
|
45
|
+
publishedAt: string;
|
|
46
|
+
revision: number;
|
|
47
|
+
priorAttestation: `0x${string}`;
|
|
48
|
+
};
|
|
49
|
+
signature: `0x${string}`;
|
|
50
|
+
signerAddress: `0x${string}`;
|
|
51
|
+
};
|
|
52
|
+
type VerifyOk = {
|
|
53
|
+
ok: true;
|
|
54
|
+
signerAddress: `0x${string}`;
|
|
55
|
+
};
|
|
56
|
+
type VerifyError = {
|
|
57
|
+
kind: 'content-mismatch';
|
|
58
|
+
expectedDigest: `0x${string}`;
|
|
59
|
+
actualDigest: `0x${string}`;
|
|
60
|
+
} | {
|
|
61
|
+
kind: 'invalid-signature';
|
|
62
|
+
reason: string;
|
|
63
|
+
} | {
|
|
64
|
+
kind: 'schema-version-unsupported';
|
|
65
|
+
version: number;
|
|
66
|
+
} | {
|
|
67
|
+
kind: 'signer-mismatch';
|
|
68
|
+
recovered: `0x${string}`;
|
|
69
|
+
declared: `0x${string}`;
|
|
70
|
+
};
|
|
71
|
+
type VerifyFail = {
|
|
72
|
+
ok: false;
|
|
73
|
+
error: VerifyError;
|
|
74
|
+
};
|
|
75
|
+
type VerifyResult = VerifyFail | VerifyOk;
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/attestation/create.d.ts
|
|
78
|
+
type CreateAttestationInput = {
|
|
79
|
+
content: string;
|
|
80
|
+
title: string;
|
|
81
|
+
slug: string;
|
|
82
|
+
locale: string;
|
|
83
|
+
publishedAt: bigint | Date | number;
|
|
84
|
+
revision?: number;
|
|
85
|
+
priorAttestation?: `0x${string}`;
|
|
86
|
+
};
|
|
87
|
+
declare function buildAttestationMessage(input: CreateAttestationInput): AttestationMessage;
|
|
88
|
+
declare function signAttestation(message: AttestationMessage, account: LocalAccount): Promise<SignedAttestation>;
|
|
89
|
+
declare function createAttestation(input: CreateAttestationInput, account: LocalAccount): Promise<SignedAttestation>;
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/attestation/serialize.d.ts
|
|
92
|
+
declare function toStored(att: SignedAttestation): StoredAttestation;
|
|
93
|
+
declare function fromStored(stored: StoredAttestation): SignedAttestation;
|
|
94
|
+
declare function stringify(att: SignedAttestation): string;
|
|
95
|
+
declare function parse(json: string): SignedAttestation;
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/attestation/verify.d.ts
|
|
98
|
+
type VerifyInput = {
|
|
99
|
+
content: string;
|
|
100
|
+
attestation: SignedAttestation;
|
|
101
|
+
};
|
|
102
|
+
declare function verifyAttestation(input: VerifyInput): Promise<VerifyResult>;
|
|
103
|
+
//#endregion
|
|
104
|
+
export { InvalidContentError as _, stringify as a, buildAttestationMessage as c, SignedAttestation as d, StoredAttestation as f, VerifyResult as g, VerifyOk as h, parse as i, createAttestation as l, VerifyFail as m, verifyAttestation as n, toStored as o, VerifyError as p, fromStored as r, CreateAttestationInput as s, VerifyInput as t, signAttestation as u, canonicalize as v };
|
|
105
|
+
//# sourceMappingURL=verify.d.ts.map
|
package/dist/verify.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { n as ATTESTATION_PRIMARY_TYPE, r as ATTESTATION_TYPES_V1, t as ATTESTATION_DOMAIN_V1 } from "./eip712-schema.js";
|
|
2
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
3
|
+
import { recoverTypedDataAddress } from "viem";
|
|
4
|
+
//#region src/version.ts
|
|
5
|
+
const SCHEMA_VERSION = 1;
|
|
6
|
+
const CANONICAL_VERSION = 1;
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region src/core/canonicalize.ts
|
|
9
|
+
/**
|
|
10
|
+
* Canonicalize an article body for attestation v1.
|
|
11
|
+
*
|
|
12
|
+
* FROZEN CONTRACT — v1 rules below must NEVER change. Any modification requires
|
|
13
|
+
* publishing a v2 alongside (and v1 verifiers stay alive forever).
|
|
14
|
+
*
|
|
15
|
+
* Rules applied in order:
|
|
16
|
+
* 1. Reject unpaired UTF-16 surrogates (corrupt input).
|
|
17
|
+
* 2. Strip a leading BOM (U+FEFF) if present.
|
|
18
|
+
* 3. Apply Unicode NFC normalization.
|
|
19
|
+
* 4. Convert CRLF and stray CR to LF.
|
|
20
|
+
* 5. Trim trailing whitespace and append exactly one LF.
|
|
21
|
+
* 6. Encode as UTF-8 bytes.
|
|
22
|
+
*
|
|
23
|
+
* No markdown parsing. No per-line whitespace trim (would break " \n" soft breaks).
|
|
24
|
+
* No tab → space substitution (would break code blocks).
|
|
25
|
+
* No interior whitespace collapsing.
|
|
26
|
+
*/
|
|
27
|
+
function canonicalize(input) {
|
|
28
|
+
assertValidUtf16(input);
|
|
29
|
+
let s = input;
|
|
30
|
+
if (s.charCodeAt(0) === 65279) s = s.slice(1);
|
|
31
|
+
s = s.normalize("NFC");
|
|
32
|
+
s = s.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
33
|
+
s = `${s.replace(/\s+$/u, "")}\n`;
|
|
34
|
+
return new TextEncoder().encode(s);
|
|
35
|
+
}
|
|
36
|
+
var InvalidContentError = class extends Error {
|
|
37
|
+
name = "InvalidContentError";
|
|
38
|
+
};
|
|
39
|
+
function assertValidUtf16(s) {
|
|
40
|
+
for (let i = 0; i < s.length; i++) {
|
|
41
|
+
const code = s.charCodeAt(i);
|
|
42
|
+
if (code >= 55296 && code <= 56319) {
|
|
43
|
+
const next = s.charCodeAt(i + 1);
|
|
44
|
+
if (!(next >= 56320 && next <= 57343)) throw new InvalidContentError(`Unpaired high surrogate at index ${i}`);
|
|
45
|
+
i++;
|
|
46
|
+
} else if (code >= 56320 && code <= 57343) throw new InvalidContentError(`Unpaired low surrogate at index ${i}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/core/sha256.ts
|
|
51
|
+
/**
|
|
52
|
+
* SHA-256 of bytes, returned as bare hex (no 0x prefix).
|
|
53
|
+
*
|
|
54
|
+
* Uses @noble/hashes — pure JS, sync, audited, runs identically in Node, Bun,
|
|
55
|
+
* Deno, and every modern browser. This is the only hash primitive used by the
|
|
56
|
+
* package, so swapping the implementation here is the single point of change.
|
|
57
|
+
*/
|
|
58
|
+
function sha256Hex(bytes) {
|
|
59
|
+
const out = sha256(bytes);
|
|
60
|
+
let hex = "";
|
|
61
|
+
for (const byte of out) hex += byte.toString(16).padStart(2, "0");
|
|
62
|
+
return hex;
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/attestation/create.ts
|
|
66
|
+
function buildAttestationMessage(input) {
|
|
67
|
+
const digest = sha256Hex(canonicalize(input.content));
|
|
68
|
+
return {
|
|
69
|
+
claims: {
|
|
70
|
+
priorAttestation: input.priorAttestation ?? "0x0000000000000000000000000000000000000000000000000000000000000000",
|
|
71
|
+
publishedAt: toUnixSeconds(input.publishedAt),
|
|
72
|
+
revision: input.revision ?? 1,
|
|
73
|
+
slug: input.slug
|
|
74
|
+
},
|
|
75
|
+
schemaVersion: 1,
|
|
76
|
+
subject: {
|
|
77
|
+
contentDigest: `0x${digest}`,
|
|
78
|
+
locale: input.locale,
|
|
79
|
+
title: input.title
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
async function signAttestation(message, account) {
|
|
84
|
+
const signature = await account.signTypedData({
|
|
85
|
+
domain: ATTESTATION_DOMAIN_V1,
|
|
86
|
+
message,
|
|
87
|
+
primaryType: ATTESTATION_PRIMARY_TYPE,
|
|
88
|
+
types: ATTESTATION_TYPES_V1
|
|
89
|
+
});
|
|
90
|
+
return {
|
|
91
|
+
...message,
|
|
92
|
+
signature,
|
|
93
|
+
signerAddress: account.address
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
async function createAttestation(input, account) {
|
|
97
|
+
return signAttestation(buildAttestationMessage(input), account);
|
|
98
|
+
}
|
|
99
|
+
function toUnixSeconds(value) {
|
|
100
|
+
if (typeof value === "bigint") return value;
|
|
101
|
+
if (typeof value === "number") return BigInt(Math.floor(value));
|
|
102
|
+
return BigInt(Math.floor(value.getTime() / 1e3));
|
|
103
|
+
}
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/attestation/serialize.ts
|
|
106
|
+
function toStored(att) {
|
|
107
|
+
return {
|
|
108
|
+
claims: {
|
|
109
|
+
priorAttestation: att.claims.priorAttestation,
|
|
110
|
+
publishedAt: att.claims.publishedAt.toString(),
|
|
111
|
+
revision: att.claims.revision,
|
|
112
|
+
slug: att.claims.slug
|
|
113
|
+
},
|
|
114
|
+
schemaVersion: att.schemaVersion,
|
|
115
|
+
signature: att.signature,
|
|
116
|
+
signerAddress: att.signerAddress,
|
|
117
|
+
subject: att.subject
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function fromStored(stored) {
|
|
121
|
+
return {
|
|
122
|
+
claims: {
|
|
123
|
+
priorAttestation: stored.claims.priorAttestation,
|
|
124
|
+
publishedAt: BigInt(stored.claims.publishedAt),
|
|
125
|
+
revision: stored.claims.revision,
|
|
126
|
+
slug: stored.claims.slug
|
|
127
|
+
},
|
|
128
|
+
schemaVersion: stored.schemaVersion,
|
|
129
|
+
signature: stored.signature,
|
|
130
|
+
signerAddress: stored.signerAddress,
|
|
131
|
+
subject: stored.subject
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function stringify(att) {
|
|
135
|
+
return `${JSON.stringify(toStored(att), null, 2)}\n`;
|
|
136
|
+
}
|
|
137
|
+
function parse(json) {
|
|
138
|
+
return fromStored(JSON.parse(json));
|
|
139
|
+
}
|
|
140
|
+
//#endregion
|
|
141
|
+
//#region src/attestation/verify.ts
|
|
142
|
+
async function verifyAttestation(input) {
|
|
143
|
+
const att = input.attestation;
|
|
144
|
+
if (att.schemaVersion !== 1) return {
|
|
145
|
+
error: {
|
|
146
|
+
kind: "schema-version-unsupported",
|
|
147
|
+
version: att.schemaVersion
|
|
148
|
+
},
|
|
149
|
+
ok: false
|
|
150
|
+
};
|
|
151
|
+
const expected = att.subject.contentDigest;
|
|
152
|
+
const actual = `0x${sha256Hex(canonicalize(input.content))}`;
|
|
153
|
+
if (actual !== expected) return {
|
|
154
|
+
error: {
|
|
155
|
+
actualDigest: actual,
|
|
156
|
+
expectedDigest: expected,
|
|
157
|
+
kind: "content-mismatch"
|
|
158
|
+
},
|
|
159
|
+
ok: false
|
|
160
|
+
};
|
|
161
|
+
let recovered;
|
|
162
|
+
try {
|
|
163
|
+
recovered = await recoverTypedDataAddress({
|
|
164
|
+
domain: ATTESTATION_DOMAIN_V1,
|
|
165
|
+
message: {
|
|
166
|
+
claims: att.claims,
|
|
167
|
+
schemaVersion: att.schemaVersion,
|
|
168
|
+
subject: att.subject
|
|
169
|
+
},
|
|
170
|
+
primaryType: ATTESTATION_PRIMARY_TYPE,
|
|
171
|
+
signature: att.signature,
|
|
172
|
+
types: ATTESTATION_TYPES_V1
|
|
173
|
+
});
|
|
174
|
+
} catch (error) {
|
|
175
|
+
return {
|
|
176
|
+
error: {
|
|
177
|
+
kind: "invalid-signature",
|
|
178
|
+
reason: error.message
|
|
179
|
+
},
|
|
180
|
+
ok: false
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
if (recovered.toLowerCase() !== att.signerAddress.toLowerCase()) return {
|
|
184
|
+
error: {
|
|
185
|
+
declared: att.signerAddress,
|
|
186
|
+
kind: "signer-mismatch",
|
|
187
|
+
recovered
|
|
188
|
+
},
|
|
189
|
+
ok: false
|
|
190
|
+
};
|
|
191
|
+
return {
|
|
192
|
+
ok: true,
|
|
193
|
+
signerAddress: recovered
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
//#endregion
|
|
197
|
+
export { toStored as a, signAttestation as c, canonicalize as d, CANONICAL_VERSION as f, stringify as i, sha256Hex as l, fromStored as n, buildAttestationMessage as o, SCHEMA_VERSION as p, parse as r, createAttestation as s, verifyAttestation as t, InvalidContentError as u };
|
|
198
|
+
|
|
199
|
+
//# sourceMappingURL=verify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.js","names":[],"sources":["../src/version.ts","../src/core/canonicalize.ts","../src/core/sha256.ts","../src/attestation/create.ts","../src/attestation/serialize.ts","../src/attestation/verify.ts"],"sourcesContent":["// Frozen on first published attestation. Bump only by adding v2 alongside v1.\nexport const SCHEMA_VERSION = 1 as const;\nexport const CANONICAL_VERSION = 1 as const;\n","/**\n * Canonicalize an article body for attestation v1.\n *\n * FROZEN CONTRACT — v1 rules below must NEVER change. Any modification requires\n * publishing a v2 alongside (and v1 verifiers stay alive forever).\n *\n * Rules applied in order:\n * 1. Reject unpaired UTF-16 surrogates (corrupt input).\n * 2. Strip a leading BOM (U+FEFF) if present.\n * 3. Apply Unicode NFC normalization.\n * 4. Convert CRLF and stray CR to LF.\n * 5. Trim trailing whitespace and append exactly one LF.\n * 6. Encode as UTF-8 bytes.\n *\n * No markdown parsing. No per-line whitespace trim (would break \" \\n\" soft breaks).\n * No tab → space substitution (would break code blocks).\n * No interior whitespace collapsing.\n */\nexport function canonicalize(input: string): Uint8Array {\n assertValidUtf16(input);\n\n let s = input;\n if (s.charCodeAt(0) === 0xfeff) {\n s = s.slice(1);\n }\n s = s.normalize('NFC');\n s = s.replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');\n // \\s with /u flag covers all ECMAScript whitespace, including U+00A0 and U+FEFF.\n s = `${s.replace(/\\s+$/u, '')}\\n`;\n\n return new TextEncoder().encode(s);\n}\n\n/**\n * Version of the canonicalization algorithm that produced bytes.\n * Embedded into every attestation so future verifiers can dispatch correctly.\n */\nexport { CANONICAL_VERSION as canonicalVersion } from '../version.js';\n\nexport class InvalidContentError extends Error {\n override name = 'InvalidContentError';\n}\n\nfunction assertValidUtf16(s: string): void {\n for (let i = 0; i < s.length; i++) {\n const code = s.charCodeAt(i);\n if (code >= 0xd800 && code <= 0xdbff) {\n const next = s.charCodeAt(i + 1);\n if (!(next >= 0xdc00 && next <= 0xdfff)) {\n throw new InvalidContentError(`Unpaired high surrogate at index ${i}`);\n }\n i++;\n } else if (code >= 0xdc00 && code <= 0xdfff) {\n throw new InvalidContentError(`Unpaired low surrogate at index ${i}`);\n }\n }\n}\n","import { sha256 } from '@noble/hashes/sha2.js';\n\n/**\n * SHA-256 of bytes, returned as bare hex (no 0x prefix).\n *\n * Uses @noble/hashes — pure JS, sync, audited, runs identically in Node, Bun,\n * Deno, and every modern browser. This is the only hash primitive used by the\n * package, so swapping the implementation here is the single point of change.\n */\nexport function sha256Hex(bytes: Uint8Array): string {\n const out = sha256(bytes);\n let hex = '';\n for (const byte of out) {\n hex += byte.toString(16).padStart(2, '0');\n }\n return hex;\n}\n","import { type LocalAccount } from 'viem';\n\nimport { canonicalize } from '../core/canonicalize.js';\nimport {\n ATTESTATION_DOMAIN_V1,\n ATTESTATION_PRIMARY_TYPE,\n ATTESTATION_TYPES_V1,\n type AttestationMessage,\n NO_PRIOR_ATTESTATION,\n} from '../core/eip712-schema.js';\nimport { sha256Hex } from '../core/sha256.js';\nimport { SCHEMA_VERSION } from '../version.js';\nimport { type SignedAttestation } from './types.js';\n\nexport type CreateAttestationInput = {\n content: string;\n title: string;\n slug: string;\n locale: string;\n publishedAt: bigint | Date | number;\n revision?: number;\n priorAttestation?: `0x${string}`;\n};\n\nexport function buildAttestationMessage(input: CreateAttestationInput): AttestationMessage {\n const bytes = canonicalize(input.content);\n const digest = sha256Hex(bytes);\n\n return {\n claims: {\n priorAttestation: input.priorAttestation ?? NO_PRIOR_ATTESTATION,\n publishedAt: toUnixSeconds(input.publishedAt),\n revision: input.revision ?? 1,\n slug: input.slug,\n },\n schemaVersion: SCHEMA_VERSION,\n subject: {\n contentDigest: `0x${digest}` as const,\n locale: input.locale,\n title: input.title,\n },\n };\n}\n\nexport async function signAttestation(\n message: AttestationMessage,\n account: LocalAccount,\n): Promise<SignedAttestation> {\n const signature = await account.signTypedData({\n domain: ATTESTATION_DOMAIN_V1,\n message,\n primaryType: ATTESTATION_PRIMARY_TYPE,\n types: ATTESTATION_TYPES_V1,\n });\n\n return {\n ...message,\n signature,\n signerAddress: account.address,\n };\n}\n\nexport async function createAttestation(\n input: CreateAttestationInput,\n account: LocalAccount,\n): Promise<SignedAttestation> {\n return signAttestation(buildAttestationMessage(input), account);\n}\n\nfunction toUnixSeconds(value: bigint | Date | number): bigint {\n if (typeof value === 'bigint') {\n return value;\n }\n if (typeof value === 'number') {\n return BigInt(Math.floor(value));\n }\n return BigInt(Math.floor(value.getTime() / 1000));\n}\n","import { type SignedAttestation, type StoredAttestation } from './types.js';\n\nexport function toStored(att: SignedAttestation): StoredAttestation {\n return {\n claims: {\n priorAttestation: att.claims.priorAttestation,\n publishedAt: att.claims.publishedAt.toString(),\n revision: att.claims.revision,\n slug: att.claims.slug,\n },\n schemaVersion: att.schemaVersion,\n signature: att.signature,\n signerAddress: att.signerAddress,\n subject: att.subject,\n };\n}\n\nexport function fromStored(stored: StoredAttestation): SignedAttestation {\n return {\n claims: {\n priorAttestation: stored.claims.priorAttestation,\n publishedAt: BigInt(stored.claims.publishedAt),\n revision: stored.claims.revision,\n slug: stored.claims.slug,\n },\n schemaVersion: stored.schemaVersion,\n signature: stored.signature,\n signerAddress: stored.signerAddress,\n subject: stored.subject,\n };\n}\n\nexport function stringify(att: SignedAttestation): string {\n return `${JSON.stringify(toStored(att), null, 2)}\\n`;\n}\n\nexport function parse(json: string): SignedAttestation {\n const parsed = JSON.parse(json) as StoredAttestation;\n return fromStored(parsed);\n}\n","import { recoverTypedDataAddress } from 'viem';\n\nimport { canonicalize } from '../core/canonicalize.js';\nimport {\n ATTESTATION_DOMAIN_V1,\n ATTESTATION_PRIMARY_TYPE,\n ATTESTATION_TYPES_V1,\n} from '../core/eip712-schema.js';\nimport { sha256Hex } from '../core/sha256.js';\nimport { SCHEMA_VERSION } from '../version.js';\nimport { type SignedAttestation, type VerifyResult } from './types.js';\n\nexport type VerifyInput = {\n content: string;\n attestation: SignedAttestation;\n};\n\nexport async function verifyAttestation(input: VerifyInput): Promise<VerifyResult> {\n const att = input.attestation;\n\n if (att.schemaVersion !== SCHEMA_VERSION) {\n return {\n error: { kind: 'schema-version-unsupported', version: att.schemaVersion },\n ok: false,\n };\n }\n\n const expected = att.subject.contentDigest;\n const actual = `0x${sha256Hex(canonicalize(input.content))}` as const;\n if (actual !== expected) {\n return {\n error: { actualDigest: actual, expectedDigest: expected, kind: 'content-mismatch' },\n ok: false,\n };\n }\n\n let recovered: `0x${string}`;\n try {\n recovered = await recoverTypedDataAddress({\n domain: ATTESTATION_DOMAIN_V1,\n message: { claims: att.claims, schemaVersion: att.schemaVersion, subject: att.subject },\n primaryType: ATTESTATION_PRIMARY_TYPE,\n signature: att.signature,\n types: ATTESTATION_TYPES_V1,\n });\n } catch (error) {\n return {\n error: { kind: 'invalid-signature', reason: (error as Error).message },\n ok: false,\n };\n }\n\n if (recovered.toLowerCase() !== att.signerAddress.toLowerCase()) {\n return {\n error: { declared: att.signerAddress, kind: 'signer-mismatch', recovered },\n ok: false,\n };\n }\n\n return { ok: true, signerAddress: recovered };\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jterrazz/attestation",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Cryptographic attestation for articles — EIP-712 signature anchored to Bitcoin via OpenTimestamps.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Jean-Baptiste Terrazzoni <contact@jterrazz.com>",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/jterrazz/package-attestation.git"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"attestation": "bin/attestation"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"bin"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"require": "./dist/index.cjs",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./browser": {
|
|
25
|
+
"require": "./dist/browser.cjs",
|
|
26
|
+
"import": "./dist/browser.js"
|
|
27
|
+
},
|
|
28
|
+
"./node": {
|
|
29
|
+
"require": "./dist/node.cjs",
|
|
30
|
+
"import": "./dist/node.js"
|
|
31
|
+
},
|
|
32
|
+
"./cli": {
|
|
33
|
+
"require": "./dist/cli.cjs",
|
|
34
|
+
"import": "./dist/cli.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsdown --config tsdown.config.ts",
|
|
39
|
+
"prepare": "tsdown --config tsdown.config.ts",
|
|
40
|
+
"lint": "typescript check",
|
|
41
|
+
"lint:fix": "typescript fix",
|
|
42
|
+
"test": "vitest --run",
|
|
43
|
+
"test:network": "ATTEST_E2E_NETWORK=1 vitest --run --project e2e-network"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@noble/hashes": "^2.2.0",
|
|
47
|
+
"javascript-opentimestamps": "^0.4.5",
|
|
48
|
+
"open": "^11.0.0",
|
|
49
|
+
"viem": "^2.54.1"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@jterrazz/test": "^8.0.0",
|
|
53
|
+
"@jterrazz/typescript": "^6.1.0",
|
|
54
|
+
"@types/node": "^24.13.2",
|
|
55
|
+
"tsdown": "^0.22.3",
|
|
56
|
+
"vitest": "^4.1.9"
|
|
57
|
+
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=24"
|
|
60
|
+
},
|
|
61
|
+
"knip": {
|
|
62
|
+
"ignoreBinaries": [
|
|
63
|
+
"tsdown",
|
|
64
|
+
"vitest"
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
}
|