@orangecheck/sdk 1.17.0 → 1.18.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/attestation.d.mts +1 -1
- package/dist/attestation.d.ts +1 -1
- package/dist/attestation.js +16 -11
- package/dist/attestation.js.map +1 -1
- package/dist/attestation.mjs +16 -11
- package/dist/attestation.mjs.map +1 -1
- package/dist/canonical.d.mts +4 -2
- package/dist/canonical.d.ts +4 -2
- package/dist/canonical.js +17 -2
- package/dist/canonical.js.map +1 -1
- package/dist/canonical.mjs +16 -3
- package/dist/canonical.mjs.map +1 -1
- package/dist/challenge.d.mts +1 -1
- package/dist/challenge.d.ts +1 -1
- package/dist/identity.d.mts +1 -1
- package/dist/identity.d.ts +1 -1
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +35 -11
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +35 -12
- package/dist/index.mjs.map +1 -1
- package/dist/nostr.d.mts +1 -1
- package/dist/nostr.d.ts +1 -1
- package/dist/nostr.js +15 -9
- package/dist/nostr.js.map +1 -1
- package/dist/nostr.mjs +15 -9
- package/dist/nostr.mjs.map +1 -1
- package/dist/{types-DsrYs3EB.d.mts → types-BdJoNWgs.d.mts} +1 -1
- package/dist/{types-DsrYs3EB.d.ts → types-BdJoNWgs.d.ts} +1 -1
- package/dist/verify.d.mts +2 -1
- package/dist/verify.d.ts +2 -1
- package/dist/verify.js +25 -9
- package/dist/verify.js.map +1 -1
- package/dist/verify.mjs +25 -9
- package/dist/verify.mjs.map +1 -1
- package/package.json +1 -1
package/dist/canonical.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { I as IdentityBinding, S as Scheme, A as AttestationEnvelope } from './types-
|
|
1
|
+
import { I as IdentityBinding, S as Scheme, A as AttestationEnvelope } from './types-BdJoNWgs.mjs';
|
|
2
2
|
|
|
3
3
|
interface CanonicalInput {
|
|
4
4
|
address: string;
|
|
@@ -12,8 +12,10 @@ interface BuildOptions {
|
|
|
12
12
|
nonce?: string;
|
|
13
13
|
issuedAt?: string;
|
|
14
14
|
}
|
|
15
|
+
declare function readBondExtension(value: string): number | null;
|
|
15
16
|
declare function buildCanonicalMessage({ address, identities }: CanonicalInput, extensions?: Extensions, options?: BuildOptions): string;
|
|
16
17
|
declare function generateAttestationId(message: string): Promise<string>;
|
|
18
|
+
declare function readMessageValue(message: string, key: string): string | undefined;
|
|
17
19
|
declare function createAttestationEnvelope(message: string, signature: string, scheme: Scheme, address: string, identities: IdentityBinding[], extensions?: Extensions): Promise<AttestationEnvelope>;
|
|
18
20
|
|
|
19
|
-
export { type BuildOptions, type CanonicalInput, type Extensions, buildCanonicalMessage, createAttestationEnvelope, formatIdentities, generateAttestationId, parseIdentities, randomNonce16BHexLower };
|
|
21
|
+
export { type BuildOptions, type CanonicalInput, type Extensions, buildCanonicalMessage, createAttestationEnvelope, formatIdentities, generateAttestationId, parseIdentities, randomNonce16BHexLower, readBondExtension, readMessageValue };
|
package/dist/canonical.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { I as IdentityBinding, S as Scheme, A as AttestationEnvelope } from './types-
|
|
1
|
+
import { I as IdentityBinding, S as Scheme, A as AttestationEnvelope } from './types-BdJoNWgs.js';
|
|
2
2
|
|
|
3
3
|
interface CanonicalInput {
|
|
4
4
|
address: string;
|
|
@@ -12,8 +12,10 @@ interface BuildOptions {
|
|
|
12
12
|
nonce?: string;
|
|
13
13
|
issuedAt?: string;
|
|
14
14
|
}
|
|
15
|
+
declare function readBondExtension(value: string): number | null;
|
|
15
16
|
declare function buildCanonicalMessage({ address, identities }: CanonicalInput, extensions?: Extensions, options?: BuildOptions): string;
|
|
16
17
|
declare function generateAttestationId(message: string): Promise<string>;
|
|
18
|
+
declare function readMessageValue(message: string, key: string): string | undefined;
|
|
17
19
|
declare function createAttestationEnvelope(message: string, signature: string, scheme: Scheme, address: string, identities: IdentityBinding[], extensions?: Extensions): Promise<AttestationEnvelope>;
|
|
18
20
|
|
|
19
|
-
export { type BuildOptions, type CanonicalInput, type Extensions, buildCanonicalMessage, createAttestationEnvelope, formatIdentities, generateAttestationId, parseIdentities, randomNonce16BHexLower };
|
|
21
|
+
export { type BuildOptions, type CanonicalInput, type Extensions, buildCanonicalMessage, createAttestationEnvelope, formatIdentities, generateAttestationId, parseIdentities, randomNonce16BHexLower, readBondExtension, readMessageValue };
|
package/dist/canonical.js
CHANGED
|
@@ -62,6 +62,11 @@ function parseIdentities(identitiesStr) {
|
|
|
62
62
|
}
|
|
63
63
|
return bindings;
|
|
64
64
|
}
|
|
65
|
+
function readBondExtension(value) {
|
|
66
|
+
if (!/^[0-9]+$/.test(value)) return null;
|
|
67
|
+
const n = Number(value);
|
|
68
|
+
return Number.isSafeInteger(n) ? n : null;
|
|
69
|
+
}
|
|
65
70
|
function buildCanonicalMessage({ address, identities }, extensions = {}, options = {}) {
|
|
66
71
|
const nonce = options.nonce ?? randomNonce16BHexLower();
|
|
67
72
|
if (!/^[0-9a-f]{32}$/.test(nonce)) {
|
|
@@ -84,6 +89,9 @@ function buildCanonicalMessage({ address, identities }, extensions = {}, options
|
|
|
84
89
|
continue;
|
|
85
90
|
}
|
|
86
91
|
const key = k.toLowerCase();
|
|
92
|
+
if (key === "bond" && readBondExtension(String(v)) === null) {
|
|
93
|
+
throw new Error(`invalid bond: ${String(v)} is not a whole number of sats up to 2^53 - 1`);
|
|
94
|
+
}
|
|
87
95
|
normExt[key] = String(v);
|
|
88
96
|
}
|
|
89
97
|
const extLines = Object.keys(normExt).sort().map((k) => `${k}: ${normExt[k]}`);
|
|
@@ -96,10 +104,15 @@ async function generateAttestationId(message) {
|
|
|
96
104
|
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
97
105
|
return toLowerHex(new Uint8Array(hashBuffer));
|
|
98
106
|
}
|
|
107
|
+
function readMessageValue(message, key) {
|
|
108
|
+
const needle = key + ":";
|
|
109
|
+
const hit = message.split("\n").map((l) => l.trim()).find((l) => l.toLowerCase().startsWith(needle));
|
|
110
|
+
if (!hit) return void 0;
|
|
111
|
+
return hit.slice(hit.indexOf(":") + 1).trim();
|
|
112
|
+
}
|
|
99
113
|
async function createAttestationEnvelope(message, signature, scheme, address, identities, extensions = {}) {
|
|
100
114
|
const attestation_id = await generateAttestationId(message);
|
|
101
|
-
const
|
|
102
|
-
const issued_at = issuedAtMatch?.[1] ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
115
|
+
const issued_at = readMessageValue(message, "issued_at") ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
103
116
|
const expires_at = extensions.expires;
|
|
104
117
|
const relay_hints = extensions.relay_hints?.split(",").map((r) => r.trim());
|
|
105
118
|
const publish_targets = extensions.publish?.split(",").map((t) => t.trim());
|
|
@@ -130,5 +143,7 @@ exports.formatIdentities = formatIdentities;
|
|
|
130
143
|
exports.generateAttestationId = generateAttestationId;
|
|
131
144
|
exports.parseIdentities = parseIdentities;
|
|
132
145
|
exports.randomNonce16BHexLower = randomNonce16BHexLower;
|
|
146
|
+
exports.readBondExtension = readBondExtension;
|
|
147
|
+
exports.readMessageValue = readMessageValue;
|
|
133
148
|
//# sourceMappingURL=canonical.js.map
|
|
134
149
|
//# sourceMappingURL=canonical.js.map
|
package/dist/canonical.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/canonical.ts"],"names":[],"mappings":"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","file":"canonical.js","sourcesContent":["/**\n * Canonical message builder per SPEC.md §2\n *\n * - LF line endings, exactly one trailing LF\n * - 7 fixed core lines in strict order and wording\n * - Optional extensions appended as key: value with lexicographically sorted lowercase keys\n */\n\nimport type { AttestationEnvelope, IdentityBinding, Scheme } from './types';\n\n/**\n * Input for building a canonical message\n */\nexport interface CanonicalInput {\n /** Bitcoin address */\n address: string;\n /** Optional identity bindings */\n identities?: IdentityBinding[];\n}\n\n/**\n * Extension key-value pairs for canonical message\n */\nexport type Extensions = Record<string, string | undefined>;\n\n/**\n * Convert bytes to lowercase hex string\n * @param bytes - Byte array to convert\n * @returns Lowercase hex string\n */\nfunction toLowerHex(bytes: Uint8Array): string {\n return Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n}\n\n/**\n * Generate a random 16-byte nonce as lowercase hex\n * Uses Web Crypto API when available, falls back to Math.random()\n * @returns 32-character lowercase hex string\n */\nexport function randomNonce16BHexLower(): string {\n const arr = new Uint8Array(16);\n\n try {\n if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {\n crypto.getRandomValues(arr);\n return toLowerHex(arr);\n }\n } catch {\n // Fall through to non-cryptographic fallback\n }\n\n // No silent Math.random fallback: a nonce is a load-bearing security\n // primitive. A deployment without `crypto.getRandomValues` MUST fix its\n // environment rather than sign with weak randomness.\n throw new Error('crypto.getRandomValues unavailable — refusing to mint a weak nonce');\n}\n\n/**\n * Reject characters that would break the line-oriented canonical grammar\n * or the comma-separated identities list. If any of these slipped through,\n * an attacker could smuggle a forged `address:` or extension line into the\n * signed payload: `identities: alice\\naddress: bc1qEVIL…` would parse as a\n * second address line. Catch it at the boundary, not at verification time.\n *\n * Also rejects a colon in the identifier so a bad actor can't turn\n * `github:alice:extra` into a different protocol binding on parse.\n */\nconst IDENTITY_PROTOCOL_RE = /^[a-z][a-z0-9_-]*$/;\nconst IDENTITY_FORBIDDEN_CHARS = /[\\r\\n,]/;\n\nfunction assertSafeIdentity({ protocol, identifier }: IdentityBinding): void {\n if (!IDENTITY_PROTOCOL_RE.test(protocol)) {\n throw new Error(\n `invalid identity protocol: ${JSON.stringify(protocol)} — must match /^[a-z][a-z0-9_-]*$/`\n );\n }\n if (!identifier || IDENTITY_FORBIDDEN_CHARS.test(identifier)) {\n throw new Error(\n `invalid identity identifier: ${JSON.stringify(identifier)} — must be non-empty and contain no newlines or commas`\n );\n }\n}\n\n/**\n * Format identities as comma-separated protocol:identifier pairs, sorted lexicographically\n * @param identities - Array of identity bindings\n * @returns Comma-separated string of protocol:identifier pairs\n */\nexport function formatIdentities(identities: IdentityBinding[] = []): string {\n if (identities.length === 0) {\n return '';\n }\n for (const id of identities) assertSafeIdentity(id);\n\n // Format as protocol:identifier and sort lexicographically\n const formatted = identities\n .map(({ protocol, identifier }) => `${protocol}:${identifier}`)\n .sort();\n\n return formatted.join(',');\n}\n\n/**\n * Parse identities from comma-separated protocol:identifier format\n * @param identitiesStr - Comma-separated identity string\n * @returns Array of identity bindings\n * @throws {Error} If identity format is invalid\n */\nexport function parseIdentities(identitiesStr: string): IdentityBinding[] {\n if (!identitiesStr || identitiesStr.trim() === '') {\n return [];\n }\n // The identities field lives on one line of the canonical message. If it\n // contains a raw LF/CR, it's either corrupt or an attempt to smuggle a\n // fake core line into the parser. Refuse instead of trying to recover.\n if (IDENTITY_FORBIDDEN_CHARS.test(identitiesStr.replace(/,/g, ''))) {\n throw new Error('identities field contains forbidden characters (newline)');\n }\n\n const bindings = identitiesStr.split(',').map((binding) => {\n const colonIndex = binding.indexOf(':');\n if (colonIndex === -1) {\n throw new Error(`Invalid identity binding format: ${binding}`);\n }\n return {\n protocol: binding.substring(0, colonIndex).trim(),\n identifier: binding.substring(colonIndex + 1).trim(),\n };\n });\n\n for (const b of bindings) {\n if (!b.protocol || !b.identifier) {\n throw new Error(`Invalid identity binding: ${b.protocol}:${b.identifier}`);\n }\n assertSafeIdentity(b);\n }\n return bindings;\n}\n\n/**\n * Optional deterministic overrides for message building. Only intended for\n * tests / conformance vectors; production callers should let the builder\n * mint a fresh nonce + timestamp per message.\n */\nexport interface BuildOptions {\n /** 32-char lowercase hex. Default: freshly-generated cryptographic nonce. */\n nonce?: string;\n /** RFC-3339 UTC. Default: `new Date().toISOString()`. */\n issuedAt?: string;\n}\n\n/**\n * Build a canonical message following OrangeCheck Protocol v0 specification\n * @param input - Canonical input with address and identities\n * @param extensions - Optional extension key-value pairs\n * @param options - Optional deterministic overrides (testing only)\n * @returns Canonical message string with LF line endings\n */\nexport function buildCanonicalMessage(\n { address, identities }: CanonicalInput,\n extensions: Extensions = {},\n options: BuildOptions = {}\n): string {\n const nonce = options.nonce ?? randomNonce16BHexLower();\n if (!/^[0-9a-f]{32}$/.test(nonce)) {\n throw new Error(`invalid nonce: must be 32 lowercase hex chars`);\n }\n const issuedAt = options.issuedAt ?? new Date().toISOString();\n\n // Format identities\n const identitiesStr = formatIdentities(identities);\n\n // Core (strict wording + order per SPEC.md §2)\n const core = [\n 'orangecheck',\n `identities: ${identitiesStr}`,\n `address: ${address}`,\n 'purpose: portable reputation attestation (non-custodial)',\n `nonce: ${nonce}`,\n `issued_at: ${issuedAt}`,\n 'ack: I attest control of this address and bind it to my identities.',\n ];\n\n // Normalize and sort extensions\n const normExt: Record<string, string> = {};\n for (const [k, v] of Object.entries(extensions)) {\n if (v == null || v === '') {\n continue;\n }\n const key = k.toLowerCase();\n normExt[key] = String(v);\n }\n\n const extLines = Object.keys(normExt)\n .sort()\n .map((k) => `${k}: ${normExt[k]!}`);\n\n const all = [...core, ...extLines];\n\n // Join with LF and ensure exactly one trailing LF\n return all.join('\\n') + '\\n';\n}\n\n/**\n * Generate attestation ID from canonical message\n *\n * attestation_id = SHA-256(canonical_message)\n *\n * @param message - Canonical message string\n * @returns Lowercase hex SHA-256 hash\n */\nexport async function generateAttestationId(message: string): Promise<string> {\n // Convert message to UTF-8 bytes\n const encoder = new TextEncoder();\n const data = encoder.encode(message);\n\n // Compute SHA-256 hash\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n\n // Convert to lowercase hex\n return toLowerHex(new Uint8Array(hashBuffer));\n}\n\n/**\n * Create an attestation envelope for publishing\n *\n * This wraps the canonical message with metadata for Nostr publishing\n *\n * @param message - Canonical message\n * @param signature - BIP-322 or legacy signature\n * @param scheme - Signature scheme used\n * @param address - Bitcoin address\n * @param identities - Identity bindings\n * @param extensions - Optional extensions\n * @returns Attestation envelope ready for publishing\n */\nexport async function createAttestationEnvelope(\n message: string,\n signature: string,\n scheme: Scheme,\n address: string,\n identities: IdentityBinding[],\n extensions: Extensions = {}\n): Promise<AttestationEnvelope> {\n const attestation_id = await generateAttestationId(message);\n\n // Extract issued_at from message\n const issuedAtMatch = message.match(/issued_at: ([^\\n]+)/);\n const issued_at = issuedAtMatch?.[1] ?? new Date().toISOString();\n\n // Extract expires from extensions if present\n const expires_at = extensions.expires;\n\n // Extract relay hints from extensions if present\n const relay_hints = extensions.relay_hints?.split(',').map((r) => r.trim());\n\n // Extract publish targets from extensions if present\n const publish_targets = extensions.publish?.split(',').map((t) => t.trim());\n\n // Base64url encode the message\n const encoder = new TextEncoder();\n const messageBytes = encoder.encode(message);\n const base64 = btoa(String.fromCharCode(...messageBytes));\n const message_b64url = base64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');\n\n // Construct verification URL\n const verification_url = `https://attest.ochk.io/attest/${attestation_id}`;\n\n return {\n attestation_id,\n scheme,\n address,\n identities,\n message,\n message_b64url,\n signature,\n issued_at,\n expires_at,\n verification_url,\n publish_targets,\n relay_hints,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/canonical.ts"],"names":[],"mappings":"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","file":"canonical.js","sourcesContent":["/**\n * Canonical message builder per SPEC.md §2\n *\n * - LF line endings, exactly one trailing LF\n * - 7 fixed core lines in strict order and wording\n * - Optional extensions appended as key: value with lexicographically sorted lowercase keys\n */\n\nimport type { AttestationEnvelope, IdentityBinding, Scheme } from './types';\n\n/**\n * Input for building a canonical message\n */\nexport interface CanonicalInput {\n /** Bitcoin address */\n address: string;\n /** Optional identity bindings */\n identities?: IdentityBinding[];\n}\n\n/**\n * Extension key-value pairs for canonical message\n */\nexport type Extensions = Record<string, string | undefined>;\n\n/**\n * Convert bytes to lowercase hex string\n * @param bytes - Byte array to convert\n * @returns Lowercase hex string\n */\nfunction toLowerHex(bytes: Uint8Array): string {\n return Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n}\n\n/**\n * Generate a random 16-byte nonce as lowercase hex\n * Uses Web Crypto API when available, falls back to Math.random()\n * @returns 32-character lowercase hex string\n */\nexport function randomNonce16BHexLower(): string {\n const arr = new Uint8Array(16);\n\n try {\n if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {\n crypto.getRandomValues(arr);\n return toLowerHex(arr);\n }\n } catch {\n // Fall through to non-cryptographic fallback\n }\n\n // No silent Math.random fallback: a nonce is a load-bearing security\n // primitive. A deployment without `crypto.getRandomValues` MUST fix its\n // environment rather than sign with weak randomness.\n throw new Error('crypto.getRandomValues unavailable — refusing to mint a weak nonce');\n}\n\n/**\n * Reject characters that would break the line-oriented canonical grammar\n * or the comma-separated identities list. If any of these slipped through,\n * an attacker could smuggle a forged `address:` or extension line into the\n * signed payload: `identities: alice\\naddress: bc1qEVIL…` would parse as a\n * second address line. Catch it at the boundary, not at verification time.\n *\n * Also rejects a colon in the identifier so a bad actor can't turn\n * `github:alice:extra` into a different protocol binding on parse.\n */\nconst IDENTITY_PROTOCOL_RE = /^[a-z][a-z0-9_-]*$/;\nconst IDENTITY_FORBIDDEN_CHARS = /[\\r\\n,]/;\n\nfunction assertSafeIdentity({ protocol, identifier }: IdentityBinding): void {\n if (!IDENTITY_PROTOCOL_RE.test(protocol)) {\n throw new Error(\n `invalid identity protocol: ${JSON.stringify(protocol)} — must match /^[a-z][a-z0-9_-]*$/`\n );\n }\n if (!identifier || IDENTITY_FORBIDDEN_CHARS.test(identifier)) {\n throw new Error(\n `invalid identity identifier: ${JSON.stringify(identifier)} — must be non-empty and contain no newlines or commas`\n );\n }\n}\n\n/**\n * Format identities as comma-separated protocol:identifier pairs, sorted lexicographically\n * @param identities - Array of identity bindings\n * @returns Comma-separated string of protocol:identifier pairs\n */\nexport function formatIdentities(identities: IdentityBinding[] = []): string {\n if (identities.length === 0) {\n return '';\n }\n for (const id of identities) assertSafeIdentity(id);\n\n // Format as protocol:identifier and sort lexicographically\n const formatted = identities\n .map(({ protocol, identifier }) => `${protocol}:${identifier}`)\n .sort();\n\n return formatted.join(',');\n}\n\n/**\n * Parse identities from comma-separated protocol:identifier format\n * @param identitiesStr - Comma-separated identity string\n * @returns Array of identity bindings\n * @throws {Error} If identity format is invalid\n */\nexport function parseIdentities(identitiesStr: string): IdentityBinding[] {\n if (!identitiesStr || identitiesStr.trim() === '') {\n return [];\n }\n // The identities field lives on one line of the canonical message. If it\n // contains a raw LF/CR, it's either corrupt or an attempt to smuggle a\n // fake core line into the parser. Refuse instead of trying to recover.\n if (IDENTITY_FORBIDDEN_CHARS.test(identitiesStr.replace(/,/g, ''))) {\n throw new Error('identities field contains forbidden characters (newline)');\n }\n\n const bindings = identitiesStr.split(',').map((binding) => {\n const colonIndex = binding.indexOf(':');\n if (colonIndex === -1) {\n throw new Error(`Invalid identity binding format: ${binding}`);\n }\n return {\n protocol: binding.substring(0, colonIndex).trim(),\n identifier: binding.substring(colonIndex + 1).trim(),\n };\n });\n\n for (const b of bindings) {\n if (!b.protocol || !b.identifier) {\n throw new Error(`Invalid identity binding: ${b.protocol}:${b.identifier}`);\n }\n assertSafeIdentity(b);\n }\n return bindings;\n}\n\n/**\n * Optional deterministic overrides for message building. Only intended for\n * tests / conformance vectors; production callers should let the builder\n * mint a fresh nonce + timestamp per message.\n */\nexport interface BuildOptions {\n /** 32-char lowercase hex. Default: freshly-generated cryptographic nonce. */\n nonce?: string;\n /** RFC-3339 UTC. Default: `new Date().toISOString()`. */\n issuedAt?: string;\n}\n\n/**\n * The amount a `bond:` value gives, or null when SPEC §2.2 does not allow it:\n * base-10 ASCII digits only, at most 2^53 - 1. parseInt would read \"5000abc\"\n * as 5000 and \"1e9\" as 1.\n */\nexport function readBondExtension(value: string): number | null {\n if (!/^[0-9]+$/.test(value)) return null;\n const n = Number(value);\n return Number.isSafeInteger(n) ? n : null;\n}\n\n/**\n * Build a canonical message following OrangeCheck Protocol v0 specification\n * @param input - Canonical input with address and identities\n * @param extensions - Optional extension key-value pairs\n * @param options - Optional deterministic overrides (testing only)\n * @returns Canonical message string with LF line endings\n */\nexport function buildCanonicalMessage(\n { address, identities }: CanonicalInput,\n extensions: Extensions = {},\n options: BuildOptions = {}\n): string {\n const nonce = options.nonce ?? randomNonce16BHexLower();\n if (!/^[0-9a-f]{32}$/.test(nonce)) {\n throw new Error(`invalid nonce: must be 32 lowercase hex chars`);\n }\n const issuedAt = options.issuedAt ?? new Date().toISOString();\n\n // Format identities\n const identitiesStr = formatIdentities(identities);\n\n // Core (strict wording + order per SPEC.md §2)\n const core = [\n 'orangecheck',\n `identities: ${identitiesStr}`,\n `address: ${address}`,\n 'purpose: portable reputation attestation (non-custodial)',\n `nonce: ${nonce}`,\n `issued_at: ${issuedAt}`,\n 'ack: I attest control of this address and bind it to my identities.',\n ];\n\n // Normalize and sort extensions\n const normExt: Record<string, string> = {};\n for (const [k, v] of Object.entries(extensions)) {\n if (v == null || v === '') {\n continue;\n }\n const key = k.toLowerCase();\n // A bond is written as SPEC §2.2 reads it, so no verifier refuses it as bond_malformed.\n if (key === 'bond' && readBondExtension(String(v)) === null) {\n throw new Error(`invalid bond: ${String(v)} is not a whole number of sats up to 2^53 - 1`);\n }\n normExt[key] = String(v);\n }\n\n const extLines = Object.keys(normExt)\n .sort()\n .map((k) => `${k}: ${normExt[k]!}`);\n\n const all = [...core, ...extLines];\n\n // Join with LF and ensure exactly one trailing LF\n return all.join('\\n') + '\\n';\n}\n\n/**\n * Generate attestation ID from canonical message\n *\n * attestation_id = SHA-256(canonical_message)\n *\n * @param message - Canonical message string\n * @returns Lowercase hex SHA-256 hash\n */\nexport async function generateAttestationId(message: string): Promise<string> {\n // Convert message to UTF-8 bytes\n const encoder = new TextEncoder();\n const data = encoder.encode(message);\n\n // Compute SHA-256 hash\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n\n // Convert to lowercase hex\n return toLowerHex(new Uint8Array(hashBuffer));\n}\n\n/**\n * A signed line's value as verify() reads it: the first trimmed line whose key,\n * in any case, is `key`, split at its first colon. Not exported from the package.\n */\nexport function readMessageValue(message: string, key: string): string | undefined {\n const needle = key + ':';\n const hit = message\n .split('\\n')\n .map((l) => l.trim())\n .find((l) => l.toLowerCase().startsWith(needle));\n if (!hit) return undefined;\n return hit.slice(hit.indexOf(':') + 1).trim();\n}\n\n/**\n * Create an attestation envelope for publishing\n *\n * This wraps the canonical message with metadata for Nostr publishing\n *\n * @param message - Canonical message\n * @param signature - BIP-322 or legacy signature\n * @param scheme - Signature scheme used\n * @param address - Bitcoin address\n * @param identities - Identity bindings\n * @param extensions - Optional extensions\n * @returns Attestation envelope ready for publishing\n */\nexport async function createAttestationEnvelope(\n message: string,\n signature: string,\n scheme: Scheme,\n address: string,\n identities: IdentityBinding[],\n extensions: Extensions = {}\n): Promise<AttestationEnvelope> {\n const attestation_id = await generateAttestationId(message);\n\n // The signed line verify() reads, not the first \"issued_at: \" anywhere in the text.\n const issued_at = readMessageValue(message, 'issued_at') ?? new Date().toISOString();\n\n // Extract expires from extensions if present\n const expires_at = extensions.expires;\n\n // Extract relay hints from extensions if present\n const relay_hints = extensions.relay_hints?.split(',').map((r) => r.trim());\n\n // Extract publish targets from extensions if present\n const publish_targets = extensions.publish?.split(',').map((t) => t.trim());\n\n // Base64url encode the message\n const encoder = new TextEncoder();\n const messageBytes = encoder.encode(message);\n const base64 = btoa(String.fromCharCode(...messageBytes));\n const message_b64url = base64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');\n\n // Construct verification URL\n const verification_url = `https://attest.ochk.io/attest/${attestation_id}`;\n\n return {\n attestation_id,\n scheme,\n address,\n identities,\n message,\n message_b64url,\n signature,\n issued_at,\n expires_at,\n verification_url,\n publish_targets,\n relay_hints,\n };\n}\n"]}
|
package/dist/canonical.mjs
CHANGED
|
@@ -60,6 +60,11 @@ function parseIdentities(identitiesStr) {
|
|
|
60
60
|
}
|
|
61
61
|
return bindings;
|
|
62
62
|
}
|
|
63
|
+
function readBondExtension(value) {
|
|
64
|
+
if (!/^[0-9]+$/.test(value)) return null;
|
|
65
|
+
const n = Number(value);
|
|
66
|
+
return Number.isSafeInteger(n) ? n : null;
|
|
67
|
+
}
|
|
63
68
|
function buildCanonicalMessage({ address, identities }, extensions = {}, options = {}) {
|
|
64
69
|
const nonce = options.nonce ?? randomNonce16BHexLower();
|
|
65
70
|
if (!/^[0-9a-f]{32}$/.test(nonce)) {
|
|
@@ -82,6 +87,9 @@ function buildCanonicalMessage({ address, identities }, extensions = {}, options
|
|
|
82
87
|
continue;
|
|
83
88
|
}
|
|
84
89
|
const key = k.toLowerCase();
|
|
90
|
+
if (key === "bond" && readBondExtension(String(v)) === null) {
|
|
91
|
+
throw new Error(`invalid bond: ${String(v)} is not a whole number of sats up to 2^53 - 1`);
|
|
92
|
+
}
|
|
85
93
|
normExt[key] = String(v);
|
|
86
94
|
}
|
|
87
95
|
const extLines = Object.keys(normExt).sort().map((k) => `${k}: ${normExt[k]}`);
|
|
@@ -94,10 +102,15 @@ async function generateAttestationId(message) {
|
|
|
94
102
|
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
95
103
|
return toLowerHex(new Uint8Array(hashBuffer));
|
|
96
104
|
}
|
|
105
|
+
function readMessageValue(message, key) {
|
|
106
|
+
const needle = key + ":";
|
|
107
|
+
const hit = message.split("\n").map((l) => l.trim()).find((l) => l.toLowerCase().startsWith(needle));
|
|
108
|
+
if (!hit) return void 0;
|
|
109
|
+
return hit.slice(hit.indexOf(":") + 1).trim();
|
|
110
|
+
}
|
|
97
111
|
async function createAttestationEnvelope(message, signature, scheme, address, identities, extensions = {}) {
|
|
98
112
|
const attestation_id = await generateAttestationId(message);
|
|
99
|
-
const
|
|
100
|
-
const issued_at = issuedAtMatch?.[1] ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
113
|
+
const issued_at = readMessageValue(message, "issued_at") ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
101
114
|
const expires_at = extensions.expires;
|
|
102
115
|
const relay_hints = extensions.relay_hints?.split(",").map((r) => r.trim());
|
|
103
116
|
const publish_targets = extensions.publish?.split(",").map((t) => t.trim());
|
|
@@ -122,6 +135,6 @@ async function createAttestationEnvelope(message, signature, scheme, address, id
|
|
|
122
135
|
};
|
|
123
136
|
}
|
|
124
137
|
|
|
125
|
-
export { buildCanonicalMessage, createAttestationEnvelope, formatIdentities, generateAttestationId, parseIdentities, randomNonce16BHexLower };
|
|
138
|
+
export { buildCanonicalMessage, createAttestationEnvelope, formatIdentities, generateAttestationId, parseIdentities, randomNonce16BHexLower, readBondExtension, readMessageValue };
|
|
126
139
|
//# sourceMappingURL=canonical.mjs.map
|
|
127
140
|
//# sourceMappingURL=canonical.mjs.map
|
package/dist/canonical.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/canonical.ts"],"names":[],"mappings":";AA8BA,SAAS,WAAW,KAAA,EAA2B;AAC3C,EAAA,OAAO,MAAM,IAAA,CAAK,KAAK,CAAA,CAClB,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,QAAA,CAAS,EAAE,EAAE,QAAA,CAAS,CAAA,EAAG,GAAG,CAAC,CAAA,CAC1C,KAAK,EAAE,CAAA;AAChB;AAOO,SAAS,sBAAA,GAAiC;AAC7C,EAAA,MAAM,GAAA,GAAM,IAAI,UAAA,CAAW,EAAE,CAAA;AAE7B,EAAA,IAAI;AACA,IAAA,IAAI,OAAO,MAAA,KAAW,WAAA,IAAe,OAAO,MAAA,CAAO,oBAAoB,UAAA,EAAY;AAC/E,MAAA,MAAA,CAAO,gBAAgB,GAAG,CAAA;AAC1B,MAAA,OAAO,WAAW,GAAG,CAAA;AAAA,IACzB;AAAA,EACJ,CAAA,CAAA,MAAQ;AAAA,EAER;AAKA,EAAA,MAAM,IAAI,MAAM,yEAAoE,CAAA;AACxF;AAYA,IAAM,oBAAA,GAAuB,oBAAA;AAC7B,IAAM,wBAAA,GAA2B,SAAA;AAEjC,SAAS,kBAAA,CAAmB,EAAE,QAAA,EAAU,UAAA,EAAW,EAA0B;AACzE,EAAA,IAAI,CAAC,oBAAA,CAAqB,IAAA,CAAK,QAAQ,CAAA,EAAG;AACtC,IAAA,MAAM,IAAI,KAAA;AAAA,MACN,CAAA,2BAAA,EAA8B,IAAA,CAAK,SAAA,CAAU,QAAQ,CAAC,CAAA,uCAAA;AAAA,KAC1D;AAAA,EACJ;AACA,EAAA,IAAI,CAAC,UAAA,IAAc,wBAAA,CAAyB,IAAA,CAAK,UAAU,CAAA,EAAG;AAC1D,IAAA,MAAM,IAAI,KAAA;AAAA,MACN,CAAA,6BAAA,EAAgC,IAAA,CAAK,SAAA,CAAU,UAAU,CAAC,CAAA,2DAAA;AAAA,KAC9D;AAAA,EACJ;AACJ;AAOO,SAAS,gBAAA,CAAiB,UAAA,GAAgC,EAAC,EAAW;AACzE,EAAA,IAAI,UAAA,CAAW,WAAW,CAAA,EAAG;AACzB,IAAA,OAAO,EAAA;AAAA,EACX;AACA,EAAA,KAAA,MAAW,EAAA,IAAM,UAAA,EAAY,kBAAA,CAAmB,EAAE,CAAA;AAGlD,EAAA,MAAM,SAAA,GAAY,UAAA,CACb,GAAA,CAAI,CAAC,EAAE,QAAA,EAAU,UAAA,EAAW,KAAM,CAAA,EAAG,QAAQ,CAAA,CAAA,EAAI,UAAU,CAAA,CAAE,EAC7D,IAAA,EAAK;AAEV,EAAA,OAAO,SAAA,CAAU,KAAK,GAAG,CAAA;AAC7B;AAQO,SAAS,gBAAgB,aAAA,EAA0C;AACtE,EAAA,IAAI,CAAC,aAAA,IAAiB,aAAA,CAAc,IAAA,OAAW,EAAA,EAAI;AAC/C,IAAA,OAAO,EAAC;AAAA,EACZ;AAIA,EAAA,IAAI,yBAAyB,IAAA,CAAK,aAAA,CAAc,QAAQ,IAAA,EAAM,EAAE,CAAC,CAAA,EAAG;AAChE,IAAA,MAAM,IAAI,MAAM,0DAA0D,CAAA;AAAA,EAC9E;AAEA,EAAA,MAAM,WAAW,aAAA,CAAc,KAAA,CAAM,GAAG,CAAA,CAAE,GAAA,CAAI,CAAC,OAAA,KAAY;AACvD,IAAA,MAAM,UAAA,GAAa,OAAA,CAAQ,OAAA,CAAQ,GAAG,CAAA;AACtC,IAAA,IAAI,eAAe,EAAA,EAAI;AACnB,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,iCAAA,EAAoC,OAAO,CAAA,CAAE,CAAA;AAAA,IACjE;AACA,IAAA,OAAO;AAAA,MACH,UAAU,OAAA,CAAQ,SAAA,CAAU,CAAA,EAAG,UAAU,EAAE,IAAA,EAAK;AAAA,MAChD,YAAY,OAAA,CAAQ,SAAA,CAAU,UAAA,GAAa,CAAC,EAAE,IAAA;AAAK,KACvD;AAAA,EACJ,CAAC,CAAA;AAED,EAAA,KAAA,MAAW,KAAK,QAAA,EAAU;AACtB,IAAA,IAAI,CAAC,CAAA,CAAE,QAAA,IAAY,CAAC,EAAE,UAAA,EAAY;AAC9B,MAAA,MAAM,IAAI,MAAM,CAAA,0BAAA,EAA6B,CAAA,CAAE,QAAQ,CAAA,CAAA,EAAI,CAAA,CAAE,UAAU,CAAA,CAAE,CAAA;AAAA,IAC7E;AACA,IAAA,kBAAA,CAAmB,CAAC,CAAA;AAAA,EACxB;AACA,EAAA,OAAO,QAAA;AACX;AAqBO,SAAS,qBAAA,CACZ,EAAE,OAAA,EAAS,UAAA,EAAW,EACtB,aAAyB,EAAC,EAC1B,OAAA,GAAwB,EAAC,EACnB;AACN,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,KAAA,IAAS,sBAAA,EAAuB;AACtD,EAAA,IAAI,CAAC,gBAAA,CAAiB,IAAA,CAAK,KAAK,CAAA,EAAG;AAC/B,IAAA,MAAM,IAAI,MAAM,CAAA,6CAAA,CAA+C,CAAA;AAAA,EACnE;AACA,EAAA,MAAM,WAAW,OAAA,CAAQ,QAAA,IAAA,iBAAY,IAAI,IAAA,IAAO,WAAA,EAAY;AAG5D,EAAA,MAAM,aAAA,GAAgB,iBAAiB,UAAU,CAAA;AAGjD,EAAA,MAAM,IAAA,GAAO;AAAA,IACT,aAAA;AAAA,IACA,eAAe,aAAa,CAAA,CAAA;AAAA,IAC5B,YAAY,OAAO,CAAA,CAAA;AAAA,IACnB,0DAAA;AAAA,IACA,UAAU,KAAK,CAAA,CAAA;AAAA,IACf,cAAc,QAAQ,CAAA,CAAA;AAAA,IACtB;AAAA,GACJ;AAGA,EAAA,MAAM,UAAkC,EAAC;AACzC,EAAA,KAAA,MAAW,CAAC,CAAA,EAAG,CAAC,KAAK,MAAA,CAAO,OAAA,CAAQ,UAAU,CAAA,EAAG;AAC7C,IAAA,IAAI,CAAA,IAAK,IAAA,IAAQ,CAAA,KAAM,EAAA,EAAI;AACvB,MAAA;AAAA,IACJ;AACA,IAAA,MAAM,GAAA,GAAM,EAAE,WAAA,EAAY;AAC1B,IAAA,OAAA,CAAQ,GAAG,CAAA,GAAI,MAAA,CAAO,CAAC,CAAA;AAAA,EAC3B;AAEA,EAAA,MAAM,WAAW,MAAA,CAAO,IAAA,CAAK,OAAO,CAAA,CAC/B,MAAK,CACL,GAAA,CAAI,CAAC,CAAA,KAAM,GAAG,CAAC,CAAA,EAAA,EAAK,OAAA,CAAQ,CAAC,CAAE,CAAA,CAAE,CAAA;AAEtC,EAAA,MAAM,GAAA,GAAM,CAAC,GAAG,IAAA,EAAM,GAAG,QAAQ,CAAA;AAGjC,EAAA,OAAO,GAAA,CAAI,IAAA,CAAK,IAAI,CAAA,GAAI,IAAA;AAC5B;AAUA,eAAsB,sBAAsB,OAAA,EAAkC;AAE1E,EAAA,MAAM,OAAA,GAAU,IAAI,WAAA,EAAY;AAChC,EAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,MAAA,CAAO,OAAO,CAAA;AAGnC,EAAA,MAAM,aAAa,MAAM,MAAA,CAAO,MAAA,CAAO,MAAA,CAAO,WAAW,IAAI,CAAA;AAG7D,EAAA,OAAO,UAAA,CAAW,IAAI,UAAA,CAAW,UAAU,CAAC,CAAA;AAChD;AAeA,eAAsB,yBAAA,CAClB,SACA,SAAA,EACA,MAAA,EACA,SACA,UAAA,EACA,UAAA,GAAyB,EAAC,EACE;AAC5B,EAAA,MAAM,cAAA,GAAiB,MAAM,qBAAA,CAAsB,OAAO,CAAA;AAG1D,EAAA,MAAM,aAAA,GAAgB,OAAA,CAAQ,KAAA,CAAM,qBAAqB,CAAA;AACzD,EAAA,MAAM,YAAY,aAAA,GAAgB,CAAC,sBAAK,IAAI,IAAA,IAAO,WAAA,EAAY;AAG/D,EAAA,MAAM,aAAa,UAAA,CAAW,OAAA;AAG9B,EAAA,MAAM,WAAA,GAAc,UAAA,CAAW,WAAA,EAAa,KAAA,CAAM,GAAG,CAAA,CAAE,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,IAAA,EAAM,CAAA;AAG1E,EAAA,MAAM,eAAA,GAAkB,UAAA,CAAW,OAAA,EAAS,KAAA,CAAM,GAAG,CAAA,CAAE,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,IAAA,EAAM,CAAA;AAG1E,EAAA,MAAM,OAAA,GAAU,IAAI,WAAA,EAAY;AAChC,EAAA,MAAM,YAAA,GAAe,OAAA,CAAQ,MAAA,CAAO,OAAO,CAAA;AAC3C,EAAA,MAAM,SAAS,IAAA,CAAK,MAAA,CAAO,YAAA,CAAa,GAAG,YAAY,CAAC,CAAA;AACxD,EAAA,MAAM,cAAA,GAAiB,MAAA,CAAO,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAAE,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAAE,OAAA,CAAQ,IAAA,EAAM,EAAE,CAAA;AAGtF,EAAA,MAAM,gBAAA,GAAmB,iCAAiC,cAAc,CAAA,CAAA;AAExE,EAAA,OAAO;AAAA,IACH,cAAA;AAAA,IACA,MAAA;AAAA,IACA,OAAA;AAAA,IACA,UAAA;AAAA,IACA,OAAA;AAAA,IACA,cAAA;AAAA,IACA,SAAA;AAAA,IACA,SAAA;AAAA,IACA,UAAA;AAAA,IACA,gBAAA;AAAA,IACA,eAAA;AAAA,IACA;AAAA,GACJ;AACJ","file":"canonical.mjs","sourcesContent":["/**\n * Canonical message builder per SPEC.md §2\n *\n * - LF line endings, exactly one trailing LF\n * - 7 fixed core lines in strict order and wording\n * - Optional extensions appended as key: value with lexicographically sorted lowercase keys\n */\n\nimport type { AttestationEnvelope, IdentityBinding, Scheme } from './types';\n\n/**\n * Input for building a canonical message\n */\nexport interface CanonicalInput {\n /** Bitcoin address */\n address: string;\n /** Optional identity bindings */\n identities?: IdentityBinding[];\n}\n\n/**\n * Extension key-value pairs for canonical message\n */\nexport type Extensions = Record<string, string | undefined>;\n\n/**\n * Convert bytes to lowercase hex string\n * @param bytes - Byte array to convert\n * @returns Lowercase hex string\n */\nfunction toLowerHex(bytes: Uint8Array): string {\n return Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n}\n\n/**\n * Generate a random 16-byte nonce as lowercase hex\n * Uses Web Crypto API when available, falls back to Math.random()\n * @returns 32-character lowercase hex string\n */\nexport function randomNonce16BHexLower(): string {\n const arr = new Uint8Array(16);\n\n try {\n if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {\n crypto.getRandomValues(arr);\n return toLowerHex(arr);\n }\n } catch {\n // Fall through to non-cryptographic fallback\n }\n\n // No silent Math.random fallback: a nonce is a load-bearing security\n // primitive. A deployment without `crypto.getRandomValues` MUST fix its\n // environment rather than sign with weak randomness.\n throw new Error('crypto.getRandomValues unavailable — refusing to mint a weak nonce');\n}\n\n/**\n * Reject characters that would break the line-oriented canonical grammar\n * or the comma-separated identities list. If any of these slipped through,\n * an attacker could smuggle a forged `address:` or extension line into the\n * signed payload: `identities: alice\\naddress: bc1qEVIL…` would parse as a\n * second address line. Catch it at the boundary, not at verification time.\n *\n * Also rejects a colon in the identifier so a bad actor can't turn\n * `github:alice:extra` into a different protocol binding on parse.\n */\nconst IDENTITY_PROTOCOL_RE = /^[a-z][a-z0-9_-]*$/;\nconst IDENTITY_FORBIDDEN_CHARS = /[\\r\\n,]/;\n\nfunction assertSafeIdentity({ protocol, identifier }: IdentityBinding): void {\n if (!IDENTITY_PROTOCOL_RE.test(protocol)) {\n throw new Error(\n `invalid identity protocol: ${JSON.stringify(protocol)} — must match /^[a-z][a-z0-9_-]*$/`\n );\n }\n if (!identifier || IDENTITY_FORBIDDEN_CHARS.test(identifier)) {\n throw new Error(\n `invalid identity identifier: ${JSON.stringify(identifier)} — must be non-empty and contain no newlines or commas`\n );\n }\n}\n\n/**\n * Format identities as comma-separated protocol:identifier pairs, sorted lexicographically\n * @param identities - Array of identity bindings\n * @returns Comma-separated string of protocol:identifier pairs\n */\nexport function formatIdentities(identities: IdentityBinding[] = []): string {\n if (identities.length === 0) {\n return '';\n }\n for (const id of identities) assertSafeIdentity(id);\n\n // Format as protocol:identifier and sort lexicographically\n const formatted = identities\n .map(({ protocol, identifier }) => `${protocol}:${identifier}`)\n .sort();\n\n return formatted.join(',');\n}\n\n/**\n * Parse identities from comma-separated protocol:identifier format\n * @param identitiesStr - Comma-separated identity string\n * @returns Array of identity bindings\n * @throws {Error} If identity format is invalid\n */\nexport function parseIdentities(identitiesStr: string): IdentityBinding[] {\n if (!identitiesStr || identitiesStr.trim() === '') {\n return [];\n }\n // The identities field lives on one line of the canonical message. If it\n // contains a raw LF/CR, it's either corrupt or an attempt to smuggle a\n // fake core line into the parser. Refuse instead of trying to recover.\n if (IDENTITY_FORBIDDEN_CHARS.test(identitiesStr.replace(/,/g, ''))) {\n throw new Error('identities field contains forbidden characters (newline)');\n }\n\n const bindings = identitiesStr.split(',').map((binding) => {\n const colonIndex = binding.indexOf(':');\n if (colonIndex === -1) {\n throw new Error(`Invalid identity binding format: ${binding}`);\n }\n return {\n protocol: binding.substring(0, colonIndex).trim(),\n identifier: binding.substring(colonIndex + 1).trim(),\n };\n });\n\n for (const b of bindings) {\n if (!b.protocol || !b.identifier) {\n throw new Error(`Invalid identity binding: ${b.protocol}:${b.identifier}`);\n }\n assertSafeIdentity(b);\n }\n return bindings;\n}\n\n/**\n * Optional deterministic overrides for message building. Only intended for\n * tests / conformance vectors; production callers should let the builder\n * mint a fresh nonce + timestamp per message.\n */\nexport interface BuildOptions {\n /** 32-char lowercase hex. Default: freshly-generated cryptographic nonce. */\n nonce?: string;\n /** RFC-3339 UTC. Default: `new Date().toISOString()`. */\n issuedAt?: string;\n}\n\n/**\n * Build a canonical message following OrangeCheck Protocol v0 specification\n * @param input - Canonical input with address and identities\n * @param extensions - Optional extension key-value pairs\n * @param options - Optional deterministic overrides (testing only)\n * @returns Canonical message string with LF line endings\n */\nexport function buildCanonicalMessage(\n { address, identities }: CanonicalInput,\n extensions: Extensions = {},\n options: BuildOptions = {}\n): string {\n const nonce = options.nonce ?? randomNonce16BHexLower();\n if (!/^[0-9a-f]{32}$/.test(nonce)) {\n throw new Error(`invalid nonce: must be 32 lowercase hex chars`);\n }\n const issuedAt = options.issuedAt ?? new Date().toISOString();\n\n // Format identities\n const identitiesStr = formatIdentities(identities);\n\n // Core (strict wording + order per SPEC.md §2)\n const core = [\n 'orangecheck',\n `identities: ${identitiesStr}`,\n `address: ${address}`,\n 'purpose: portable reputation attestation (non-custodial)',\n `nonce: ${nonce}`,\n `issued_at: ${issuedAt}`,\n 'ack: I attest control of this address and bind it to my identities.',\n ];\n\n // Normalize and sort extensions\n const normExt: Record<string, string> = {};\n for (const [k, v] of Object.entries(extensions)) {\n if (v == null || v === '') {\n continue;\n }\n const key = k.toLowerCase();\n normExt[key] = String(v);\n }\n\n const extLines = Object.keys(normExt)\n .sort()\n .map((k) => `${k}: ${normExt[k]!}`);\n\n const all = [...core, ...extLines];\n\n // Join with LF and ensure exactly one trailing LF\n return all.join('\\n') + '\\n';\n}\n\n/**\n * Generate attestation ID from canonical message\n *\n * attestation_id = SHA-256(canonical_message)\n *\n * @param message - Canonical message string\n * @returns Lowercase hex SHA-256 hash\n */\nexport async function generateAttestationId(message: string): Promise<string> {\n // Convert message to UTF-8 bytes\n const encoder = new TextEncoder();\n const data = encoder.encode(message);\n\n // Compute SHA-256 hash\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n\n // Convert to lowercase hex\n return toLowerHex(new Uint8Array(hashBuffer));\n}\n\n/**\n * Create an attestation envelope for publishing\n *\n * This wraps the canonical message with metadata for Nostr publishing\n *\n * @param message - Canonical message\n * @param signature - BIP-322 or legacy signature\n * @param scheme - Signature scheme used\n * @param address - Bitcoin address\n * @param identities - Identity bindings\n * @param extensions - Optional extensions\n * @returns Attestation envelope ready for publishing\n */\nexport async function createAttestationEnvelope(\n message: string,\n signature: string,\n scheme: Scheme,\n address: string,\n identities: IdentityBinding[],\n extensions: Extensions = {}\n): Promise<AttestationEnvelope> {\n const attestation_id = await generateAttestationId(message);\n\n // Extract issued_at from message\n const issuedAtMatch = message.match(/issued_at: ([^\\n]+)/);\n const issued_at = issuedAtMatch?.[1] ?? new Date().toISOString();\n\n // Extract expires from extensions if present\n const expires_at = extensions.expires;\n\n // Extract relay hints from extensions if present\n const relay_hints = extensions.relay_hints?.split(',').map((r) => r.trim());\n\n // Extract publish targets from extensions if present\n const publish_targets = extensions.publish?.split(',').map((t) => t.trim());\n\n // Base64url encode the message\n const encoder = new TextEncoder();\n const messageBytes = encoder.encode(message);\n const base64 = btoa(String.fromCharCode(...messageBytes));\n const message_b64url = base64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');\n\n // Construct verification URL\n const verification_url = `https://attest.ochk.io/attest/${attestation_id}`;\n\n return {\n attestation_id,\n scheme,\n address,\n identities,\n message,\n message_b64url,\n signature,\n issued_at,\n expires_at,\n verification_url,\n publish_targets,\n relay_hints,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/canonical.ts"],"names":[],"mappings":"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","file":"canonical.mjs","sourcesContent":["/**\n * Canonical message builder per SPEC.md §2\n *\n * - LF line endings, exactly one trailing LF\n * - 7 fixed core lines in strict order and wording\n * - Optional extensions appended as key: value with lexicographically sorted lowercase keys\n */\n\nimport type { AttestationEnvelope, IdentityBinding, Scheme } from './types';\n\n/**\n * Input for building a canonical message\n */\nexport interface CanonicalInput {\n /** Bitcoin address */\n address: string;\n /** Optional identity bindings */\n identities?: IdentityBinding[];\n}\n\n/**\n * Extension key-value pairs for canonical message\n */\nexport type Extensions = Record<string, string | undefined>;\n\n/**\n * Convert bytes to lowercase hex string\n * @param bytes - Byte array to convert\n * @returns Lowercase hex string\n */\nfunction toLowerHex(bytes: Uint8Array): string {\n return Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n}\n\n/**\n * Generate a random 16-byte nonce as lowercase hex\n * Uses Web Crypto API when available, falls back to Math.random()\n * @returns 32-character lowercase hex string\n */\nexport function randomNonce16BHexLower(): string {\n const arr = new Uint8Array(16);\n\n try {\n if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {\n crypto.getRandomValues(arr);\n return toLowerHex(arr);\n }\n } catch {\n // Fall through to non-cryptographic fallback\n }\n\n // No silent Math.random fallback: a nonce is a load-bearing security\n // primitive. A deployment without `crypto.getRandomValues` MUST fix its\n // environment rather than sign with weak randomness.\n throw new Error('crypto.getRandomValues unavailable — refusing to mint a weak nonce');\n}\n\n/**\n * Reject characters that would break the line-oriented canonical grammar\n * or the comma-separated identities list. If any of these slipped through,\n * an attacker could smuggle a forged `address:` or extension line into the\n * signed payload: `identities: alice\\naddress: bc1qEVIL…` would parse as a\n * second address line. Catch it at the boundary, not at verification time.\n *\n * Also rejects a colon in the identifier so a bad actor can't turn\n * `github:alice:extra` into a different protocol binding on parse.\n */\nconst IDENTITY_PROTOCOL_RE = /^[a-z][a-z0-9_-]*$/;\nconst IDENTITY_FORBIDDEN_CHARS = /[\\r\\n,]/;\n\nfunction assertSafeIdentity({ protocol, identifier }: IdentityBinding): void {\n if (!IDENTITY_PROTOCOL_RE.test(protocol)) {\n throw new Error(\n `invalid identity protocol: ${JSON.stringify(protocol)} — must match /^[a-z][a-z0-9_-]*$/`\n );\n }\n if (!identifier || IDENTITY_FORBIDDEN_CHARS.test(identifier)) {\n throw new Error(\n `invalid identity identifier: ${JSON.stringify(identifier)} — must be non-empty and contain no newlines or commas`\n );\n }\n}\n\n/**\n * Format identities as comma-separated protocol:identifier pairs, sorted lexicographically\n * @param identities - Array of identity bindings\n * @returns Comma-separated string of protocol:identifier pairs\n */\nexport function formatIdentities(identities: IdentityBinding[] = []): string {\n if (identities.length === 0) {\n return '';\n }\n for (const id of identities) assertSafeIdentity(id);\n\n // Format as protocol:identifier and sort lexicographically\n const formatted = identities\n .map(({ protocol, identifier }) => `${protocol}:${identifier}`)\n .sort();\n\n return formatted.join(',');\n}\n\n/**\n * Parse identities from comma-separated protocol:identifier format\n * @param identitiesStr - Comma-separated identity string\n * @returns Array of identity bindings\n * @throws {Error} If identity format is invalid\n */\nexport function parseIdentities(identitiesStr: string): IdentityBinding[] {\n if (!identitiesStr || identitiesStr.trim() === '') {\n return [];\n }\n // The identities field lives on one line of the canonical message. If it\n // contains a raw LF/CR, it's either corrupt or an attempt to smuggle a\n // fake core line into the parser. Refuse instead of trying to recover.\n if (IDENTITY_FORBIDDEN_CHARS.test(identitiesStr.replace(/,/g, ''))) {\n throw new Error('identities field contains forbidden characters (newline)');\n }\n\n const bindings = identitiesStr.split(',').map((binding) => {\n const colonIndex = binding.indexOf(':');\n if (colonIndex === -1) {\n throw new Error(`Invalid identity binding format: ${binding}`);\n }\n return {\n protocol: binding.substring(0, colonIndex).trim(),\n identifier: binding.substring(colonIndex + 1).trim(),\n };\n });\n\n for (const b of bindings) {\n if (!b.protocol || !b.identifier) {\n throw new Error(`Invalid identity binding: ${b.protocol}:${b.identifier}`);\n }\n assertSafeIdentity(b);\n }\n return bindings;\n}\n\n/**\n * Optional deterministic overrides for message building. Only intended for\n * tests / conformance vectors; production callers should let the builder\n * mint a fresh nonce + timestamp per message.\n */\nexport interface BuildOptions {\n /** 32-char lowercase hex. Default: freshly-generated cryptographic nonce. */\n nonce?: string;\n /** RFC-3339 UTC. Default: `new Date().toISOString()`. */\n issuedAt?: string;\n}\n\n/**\n * The amount a `bond:` value gives, or null when SPEC §2.2 does not allow it:\n * base-10 ASCII digits only, at most 2^53 - 1. parseInt would read \"5000abc\"\n * as 5000 and \"1e9\" as 1.\n */\nexport function readBondExtension(value: string): number | null {\n if (!/^[0-9]+$/.test(value)) return null;\n const n = Number(value);\n return Number.isSafeInteger(n) ? n : null;\n}\n\n/**\n * Build a canonical message following OrangeCheck Protocol v0 specification\n * @param input - Canonical input with address and identities\n * @param extensions - Optional extension key-value pairs\n * @param options - Optional deterministic overrides (testing only)\n * @returns Canonical message string with LF line endings\n */\nexport function buildCanonicalMessage(\n { address, identities }: CanonicalInput,\n extensions: Extensions = {},\n options: BuildOptions = {}\n): string {\n const nonce = options.nonce ?? randomNonce16BHexLower();\n if (!/^[0-9a-f]{32}$/.test(nonce)) {\n throw new Error(`invalid nonce: must be 32 lowercase hex chars`);\n }\n const issuedAt = options.issuedAt ?? new Date().toISOString();\n\n // Format identities\n const identitiesStr = formatIdentities(identities);\n\n // Core (strict wording + order per SPEC.md §2)\n const core = [\n 'orangecheck',\n `identities: ${identitiesStr}`,\n `address: ${address}`,\n 'purpose: portable reputation attestation (non-custodial)',\n `nonce: ${nonce}`,\n `issued_at: ${issuedAt}`,\n 'ack: I attest control of this address and bind it to my identities.',\n ];\n\n // Normalize and sort extensions\n const normExt: Record<string, string> = {};\n for (const [k, v] of Object.entries(extensions)) {\n if (v == null || v === '') {\n continue;\n }\n const key = k.toLowerCase();\n // A bond is written as SPEC §2.2 reads it, so no verifier refuses it as bond_malformed.\n if (key === 'bond' && readBondExtension(String(v)) === null) {\n throw new Error(`invalid bond: ${String(v)} is not a whole number of sats up to 2^53 - 1`);\n }\n normExt[key] = String(v);\n }\n\n const extLines = Object.keys(normExt)\n .sort()\n .map((k) => `${k}: ${normExt[k]!}`);\n\n const all = [...core, ...extLines];\n\n // Join with LF and ensure exactly one trailing LF\n return all.join('\\n') + '\\n';\n}\n\n/**\n * Generate attestation ID from canonical message\n *\n * attestation_id = SHA-256(canonical_message)\n *\n * @param message - Canonical message string\n * @returns Lowercase hex SHA-256 hash\n */\nexport async function generateAttestationId(message: string): Promise<string> {\n // Convert message to UTF-8 bytes\n const encoder = new TextEncoder();\n const data = encoder.encode(message);\n\n // Compute SHA-256 hash\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n\n // Convert to lowercase hex\n return toLowerHex(new Uint8Array(hashBuffer));\n}\n\n/**\n * A signed line's value as verify() reads it: the first trimmed line whose key,\n * in any case, is `key`, split at its first colon. Not exported from the package.\n */\nexport function readMessageValue(message: string, key: string): string | undefined {\n const needle = key + ':';\n const hit = message\n .split('\\n')\n .map((l) => l.trim())\n .find((l) => l.toLowerCase().startsWith(needle));\n if (!hit) return undefined;\n return hit.slice(hit.indexOf(':') + 1).trim();\n}\n\n/**\n * Create an attestation envelope for publishing\n *\n * This wraps the canonical message with metadata for Nostr publishing\n *\n * @param message - Canonical message\n * @param signature - BIP-322 or legacy signature\n * @param scheme - Signature scheme used\n * @param address - Bitcoin address\n * @param identities - Identity bindings\n * @param extensions - Optional extensions\n * @returns Attestation envelope ready for publishing\n */\nexport async function createAttestationEnvelope(\n message: string,\n signature: string,\n scheme: Scheme,\n address: string,\n identities: IdentityBinding[],\n extensions: Extensions = {}\n): Promise<AttestationEnvelope> {\n const attestation_id = await generateAttestationId(message);\n\n // The signed line verify() reads, not the first \"issued_at: \" anywhere in the text.\n const issued_at = readMessageValue(message, 'issued_at') ?? new Date().toISOString();\n\n // Extract expires from extensions if present\n const expires_at = extensions.expires;\n\n // Extract relay hints from extensions if present\n const relay_hints = extensions.relay_hints?.split(',').map((r) => r.trim());\n\n // Extract publish targets from extensions if present\n const publish_targets = extensions.publish?.split(',').map((t) => t.trim());\n\n // Base64url encode the message\n const encoder = new TextEncoder();\n const messageBytes = encoder.encode(message);\n const base64 = btoa(String.fromCharCode(...messageBytes));\n const message_b64url = base64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');\n\n // Construct verification URL\n const verification_url = `https://attest.ochk.io/attest/${attestation_id}`;\n\n return {\n attestation_id,\n scheme,\n address,\n identities,\n message,\n message_b64url,\n signature,\n issued_at,\n expires_at,\n verification_url,\n publish_targets,\n relay_hints,\n };\n}\n"]}
|
package/dist/challenge.d.mts
CHANGED
package/dist/challenge.d.ts
CHANGED
package/dist/identity.d.mts
CHANGED
package/dist/identity.d.ts
CHANGED
package/dist/index.d.mts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { a as VerifyOptions, I as IdentityBinding, N as Network, c as NostrEvent, d as StatusCode } from './types-
|
|
2
|
-
export { A as AttestationEnvelope, e as IdentityProtocol, M as Metrics, S as Scheme, V as VerifyInput, b as VerifyOutcome } from './types-
|
|
1
|
+
import { a as VerifyOptions, I as IdentityBinding, N as Network, c as NostrEvent, d as StatusCode } from './types-BdJoNWgs.mjs';
|
|
2
|
+
export { A as AttestationEnvelope, e as IdentityProtocol, M as Metrics, S as Scheme, V as VerifyInput, b as VerifyOutcome } from './types-BdJoNWgs.mjs';
|
|
3
3
|
export { signatureIsValid, verify } from './verify.mjs';
|
|
4
4
|
export { createAttestation, discoverAttestations, extractAttestationIdFromUrl, formatIdentitiesForDisplay, getAttestationsForAddress, getAttestationsForIdentity, getVerificationUrl, publishAttestation, verifyAttestationById } from './attestation.mjs';
|
|
5
5
|
export { Challenge, IssueChallengeOptions, VerifyChallengeOptions, VerifyChallengeReason, VerifyChallengeResult, issueChallenge, verifyChallenge } from './challenge.mjs';
|
|
6
|
-
export { BuildOptions, Extensions, buildCanonicalMessage, createAttestationEnvelope, formatIdentities, generateAttestationId, parseIdentities } from './canonical.mjs';
|
|
6
|
+
export { BuildOptions, Extensions, buildCanonicalMessage, createAttestationEnvelope, formatIdentities, generateAttestationId, parseIdentities, readBondExtension } from './canonical.mjs';
|
|
7
7
|
export { DEFAULT_RELAYS, FANOUT_DEADLINE_MS, createAttestationEvent, parseAttestationFromEvent, publishToRelays, queryByAddress, queryByAttestationId, queryByIdentity, queryRecent, signedEnvelope } from './nostr.mjs';
|
|
8
8
|
export { DnsVerificationResult, GitHubVerificationResult, NostrVerificationResult, TwitterVerificationResult, VerificationResult, VerifyIdentityOptions, generateGistTemplate, generateTweetText, generateWellKnownContent, getDnsVerificationInstructions, getGistCreationUrl, getGitHubVerificationInstructions, getManualVerificationInstructions, getNostrVerificationInstructions, getTweetUrl, getTwitterComposeUrl, getTwitterVerificationInstructions, getVerificationInstructions, getVerificationSummary, getWellKnownUrl, isDnsIdentityVerified, isGitHubIdentityVerified, isNostrIdentityVerified, isValidDomain, isValidTweetUrl, requiresManualVerification, supportsAutomaticVerification, verifyAllIdentities, verifyDnsIdentity, verifyGitHubIdentity, verifyIdentity, verifyNostrIdentity, verifyTwitterIdentity } from './identity.mjs';
|
|
9
9
|
export { ScoringAlgorithm, computeAllScores, computeScore } from './scoring.mjs';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { a as VerifyOptions, I as IdentityBinding, N as Network, c as NostrEvent, d as StatusCode } from './types-
|
|
2
|
-
export { A as AttestationEnvelope, e as IdentityProtocol, M as Metrics, S as Scheme, V as VerifyInput, b as VerifyOutcome } from './types-
|
|
1
|
+
import { a as VerifyOptions, I as IdentityBinding, N as Network, c as NostrEvent, d as StatusCode } from './types-BdJoNWgs.js';
|
|
2
|
+
export { A as AttestationEnvelope, e as IdentityProtocol, M as Metrics, S as Scheme, V as VerifyInput, b as VerifyOutcome } from './types-BdJoNWgs.js';
|
|
3
3
|
export { signatureIsValid, verify } from './verify.js';
|
|
4
4
|
export { createAttestation, discoverAttestations, extractAttestationIdFromUrl, formatIdentitiesForDisplay, getAttestationsForAddress, getAttestationsForIdentity, getVerificationUrl, publishAttestation, verifyAttestationById } from './attestation.js';
|
|
5
5
|
export { Challenge, IssueChallengeOptions, VerifyChallengeOptions, VerifyChallengeReason, VerifyChallengeResult, issueChallenge, verifyChallenge } from './challenge.js';
|
|
6
|
-
export { BuildOptions, Extensions, buildCanonicalMessage, createAttestationEnvelope, formatIdentities, generateAttestationId, parseIdentities } from './canonical.js';
|
|
6
|
+
export { BuildOptions, Extensions, buildCanonicalMessage, createAttestationEnvelope, formatIdentities, generateAttestationId, parseIdentities, readBondExtension } from './canonical.js';
|
|
7
7
|
export { DEFAULT_RELAYS, FANOUT_DEADLINE_MS, createAttestationEvent, parseAttestationFromEvent, publishToRelays, queryByAddress, queryByAttestationId, queryByIdentity, queryRecent, signedEnvelope } from './nostr.js';
|
|
8
8
|
export { DnsVerificationResult, GitHubVerificationResult, NostrVerificationResult, TwitterVerificationResult, VerificationResult, VerifyIdentityOptions, generateGistTemplate, generateTweetText, generateWellKnownContent, getDnsVerificationInstructions, getGistCreationUrl, getGitHubVerificationInstructions, getManualVerificationInstructions, getNostrVerificationInstructions, getTweetUrl, getTwitterComposeUrl, getTwitterVerificationInstructions, getVerificationInstructions, getVerificationSummary, getWellKnownUrl, isDnsIdentityVerified, isGitHubIdentityVerified, isNostrIdentityVerified, isValidDomain, isValidTweetUrl, requiresManualVerification, supportsAutomaticVerification, verifyAllIdentities, verifyDnsIdentity, verifyGitHubIdentity, verifyIdentity, verifyNostrIdentity, verifyTwitterIdentity } from './identity.js';
|
|
9
9
|
export { ScoringAlgorithm, computeAllScores, computeScore } from './scoring.js';
|
package/dist/index.js
CHANGED
|
@@ -70,6 +70,11 @@ function parseIdentities(identitiesStr) {
|
|
|
70
70
|
}
|
|
71
71
|
return bindings;
|
|
72
72
|
}
|
|
73
|
+
function readBondExtension(value) {
|
|
74
|
+
if (!/^[0-9]+$/.test(value)) return null;
|
|
75
|
+
const n = Number(value);
|
|
76
|
+
return Number.isSafeInteger(n) ? n : null;
|
|
77
|
+
}
|
|
73
78
|
function buildCanonicalMessage({ address, identities }, extensions = {}, options = {}) {
|
|
74
79
|
const nonce = options.nonce ?? randomNonce16BHexLower();
|
|
75
80
|
if (!/^[0-9a-f]{32}$/.test(nonce)) {
|
|
@@ -92,6 +97,9 @@ function buildCanonicalMessage({ address, identities }, extensions = {}, options
|
|
|
92
97
|
continue;
|
|
93
98
|
}
|
|
94
99
|
const key = k.toLowerCase();
|
|
100
|
+
if (key === "bond" && readBondExtension(String(v)) === null) {
|
|
101
|
+
throw new Error(`invalid bond: ${String(v)} is not a whole number of sats up to 2^53 - 1`);
|
|
102
|
+
}
|
|
95
103
|
normExt[key] = String(v);
|
|
96
104
|
}
|
|
97
105
|
const extLines = Object.keys(normExt).sort().map((k) => `${k}: ${normExt[k]}`);
|
|
@@ -104,10 +112,15 @@ async function generateAttestationId(message) {
|
|
|
104
112
|
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
105
113
|
return toLowerHex(new Uint8Array(hashBuffer));
|
|
106
114
|
}
|
|
115
|
+
function readMessageValue(message, key) {
|
|
116
|
+
const needle = key + ":";
|
|
117
|
+
const hit = message.split("\n").map((l) => l.trim()).find((l) => l.toLowerCase().startsWith(needle));
|
|
118
|
+
if (!hit) return void 0;
|
|
119
|
+
return hit.slice(hit.indexOf(":") + 1).trim();
|
|
120
|
+
}
|
|
107
121
|
async function createAttestationEnvelope(message, signature, scheme, address, identities, extensions = {}) {
|
|
108
122
|
const attestation_id = await generateAttestationId(message);
|
|
109
|
-
const
|
|
110
|
-
const issued_at = issuedAtMatch?.[1] ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
123
|
+
const issued_at = readMessageValue(message, "issued_at") ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
111
124
|
const expires_at = extensions.expires;
|
|
112
125
|
const relay_hints = extensions.relay_hints?.split(",").map((r) => r.trim());
|
|
113
126
|
const publish_targets = extensions.publish?.split(",").map((t) => t.trim());
|
|
@@ -319,13 +332,7 @@ async function signatureIsValid(input) {
|
|
|
319
332
|
}
|
|
320
333
|
function parseCanonicalMessage(msg) {
|
|
321
334
|
const lines = msg.split("\n").map((l) => l.trim());
|
|
322
|
-
const getVal = (
|
|
323
|
-
const needle = prefix + ":";
|
|
324
|
-
const hit = lines.find((l) => l.toLowerCase().startsWith(needle));
|
|
325
|
-
if (!hit) return void 0;
|
|
326
|
-
const idx = hit.indexOf(":");
|
|
327
|
-
return hit.slice(idx + 1).trim();
|
|
328
|
-
};
|
|
335
|
+
const getVal = (key) => readMessageValue(msg, key);
|
|
329
336
|
const address = getVal("address") || "";
|
|
330
337
|
const identitiesStr = getVal("identities") || "";
|
|
331
338
|
const issued_at = getVal("issued_at") || "";
|
|
@@ -359,14 +366,15 @@ function parseCanonicalMessage(msg) {
|
|
|
359
366
|
}
|
|
360
367
|
const network = extensions["network"] || "mainnet";
|
|
361
368
|
const bondStr = extensions["bond"];
|
|
362
|
-
const bond = bondStr ?
|
|
369
|
+
const bond = bondStr ? readBondExtension(bondStr) : void 0;
|
|
363
370
|
return {
|
|
364
371
|
core: { address, identities, issued_at },
|
|
365
372
|
expires,
|
|
366
373
|
repeated,
|
|
367
374
|
network,
|
|
368
375
|
extensions,
|
|
369
|
-
bond: bond !== void 0 &&
|
|
376
|
+
bond: bond !== void 0 && bond !== null && bond > 0 ? bond : void 0,
|
|
377
|
+
bondMalformed: bond === null
|
|
370
378
|
};
|
|
371
379
|
}
|
|
372
380
|
async function getAddressUtxos(address, network, opts) {
|
|
@@ -535,6 +543,16 @@ async function verify(input, opts = {}) {
|
|
|
535
543
|
identities: parsed.core.identities
|
|
536
544
|
};
|
|
537
545
|
}
|
|
546
|
+
if (parsed.bondMalformed) {
|
|
547
|
+
codes.push("bond_malformed");
|
|
548
|
+
return {
|
|
549
|
+
ok: false,
|
|
550
|
+
codes,
|
|
551
|
+
network,
|
|
552
|
+
attestation_id,
|
|
553
|
+
identities: parsed.core.identities
|
|
554
|
+
};
|
|
555
|
+
}
|
|
538
556
|
try {
|
|
539
557
|
const utxos = await getAddressUtxos(input.addr, network, {
|
|
540
558
|
esploraMainnetBase: opts.esploraMainnetBase,
|
|
@@ -2319,6 +2337,11 @@ var STATUS_META = {
|
|
|
2319
2337
|
detail: "The confirmed balance is less than the declared bond amount. The bond extension requires the address to hold at least the specified amount in confirmed UTXOs.",
|
|
2320
2338
|
severity: "error"
|
|
2321
2339
|
},
|
|
2340
|
+
bond_malformed: {
|
|
2341
|
+
label: "Bond amount not readable",
|
|
2342
|
+
detail: "The signed bond line is not a whole number of sats written in plain digits, so the attestation does not say how much it bonds.",
|
|
2343
|
+
severity: "error"
|
|
2344
|
+
},
|
|
2322
2345
|
aud_mismatch: {
|
|
2323
2346
|
label: "Audience mismatch",
|
|
2324
2347
|
detail: "The message was signed for a different audience than expected. This may be intentional or indicate the signature is being used in the wrong context.",
|
|
@@ -2424,6 +2447,7 @@ exports.queryByAddress = queryByAddress;
|
|
|
2424
2447
|
exports.queryByAttestationId = queryByAttestationId;
|
|
2425
2448
|
exports.queryByIdentity = queryByIdentity;
|
|
2426
2449
|
exports.queryRecent = queryRecent;
|
|
2450
|
+
exports.readBondExtension = readBondExtension;
|
|
2427
2451
|
exports.requiresManualVerification = requiresManualVerification;
|
|
2428
2452
|
exports.signNostrEvent = signNostrEvent;
|
|
2429
2453
|
exports.signatureIsValid = signatureIsValid;
|