@orangecheck/sdk 1.17.1 → 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 +8 -2
- package/dist/attestation.js.map +1 -1
- package/dist/attestation.mjs +8 -2
- package/dist/attestation.mjs.map +1 -1
- package/dist/canonical.d.mts +3 -2
- package/dist/canonical.d.ts +3 -2
- package/dist/canonical.js +9 -0
- package/dist/canonical.js.map +1 -1
- package/dist/canonical.mjs +9 -1
- 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 +27 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +27 -3
- 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 +8 -2
- package/dist/nostr.js.map +1 -1
- package/dist/nostr.mjs +8 -2
- 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 +18 -2
- package/dist/verify.js.map +1 -1
- package/dist/verify.mjs +18 -2
- package/dist/verify.mjs.map +1 -1
- package/package.json +1 -1
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 * 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"]}
|
|
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]}`);
|
|
@@ -127,6 +135,6 @@ async function createAttestationEnvelope(message, signature, scheme, address, id
|
|
|
127
135
|
};
|
|
128
136
|
}
|
|
129
137
|
|
|
130
|
-
export { buildCanonicalMessage, createAttestationEnvelope, formatIdentities, generateAttestationId, parseIdentities, randomNonce16BHexLower, readMessageValue };
|
|
138
|
+
export { buildCanonicalMessage, createAttestationEnvelope, formatIdentities, generateAttestationId, parseIdentities, randomNonce16BHexLower, readBondExtension, readMessageValue };
|
|
131
139
|
//# sourceMappingURL=canonical.mjs.map
|
|
132
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;AAMO,SAAS,gBAAA,CAAiB,SAAiB,GAAA,EAAiC;AAC/E,EAAA,MAAM,SAAS,GAAA,GAAM,GAAA;AACrB,EAAA,MAAM,GAAA,GAAM,QACP,KAAA,CAAM,IAAI,EACV,GAAA,CAAI,CAAC,MAAM,CAAA,CAAE,IAAA,EAAM,CAAA,CACnB,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,aAAY,CAAE,UAAA,CAAW,MAAM,CAAC,CAAA;AACnD,EAAA,IAAI,CAAC,KAAK,OAAO,MAAA;AACjB,EAAA,OAAO,GAAA,CAAI,MAAM,GAAA,CAAI,OAAA,CAAQ,GAAG,CAAA,GAAI,CAAC,EAAE,IAAA,EAAK;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,SAAA,GAAY,iBAAiB,OAAA,EAAS,WAAW,sBAAK,IAAI,IAAA,IAAO,WAAA,EAAY;AAGnF,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 * 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"]}
|
|
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]}`);
|
|
@@ -358,14 +366,15 @@ function parseCanonicalMessage(msg) {
|
|
|
358
366
|
}
|
|
359
367
|
const network = extensions["network"] || "mainnet";
|
|
360
368
|
const bondStr = extensions["bond"];
|
|
361
|
-
const bond = bondStr ?
|
|
369
|
+
const bond = bondStr ? readBondExtension(bondStr) : void 0;
|
|
362
370
|
return {
|
|
363
371
|
core: { address, identities, issued_at },
|
|
364
372
|
expires,
|
|
365
373
|
repeated,
|
|
366
374
|
network,
|
|
367
375
|
extensions,
|
|
368
|
-
bond: bond !== void 0 &&
|
|
376
|
+
bond: bond !== void 0 && bond !== null && bond > 0 ? bond : void 0,
|
|
377
|
+
bondMalformed: bond === null
|
|
369
378
|
};
|
|
370
379
|
}
|
|
371
380
|
async function getAddressUtxos(address, network, opts) {
|
|
@@ -534,6 +543,16 @@ async function verify(input, opts = {}) {
|
|
|
534
543
|
identities: parsed.core.identities
|
|
535
544
|
};
|
|
536
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
|
+
}
|
|
537
556
|
try {
|
|
538
557
|
const utxos = await getAddressUtxos(input.addr, network, {
|
|
539
558
|
esploraMainnetBase: opts.esploraMainnetBase,
|
|
@@ -2318,6 +2337,11 @@ var STATUS_META = {
|
|
|
2318
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.",
|
|
2319
2338
|
severity: "error"
|
|
2320
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
|
+
},
|
|
2321
2345
|
aud_mismatch: {
|
|
2322
2346
|
label: "Audience mismatch",
|
|
2323
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.",
|
|
@@ -2423,6 +2447,7 @@ exports.queryByAddress = queryByAddress;
|
|
|
2423
2447
|
exports.queryByAttestationId = queryByAttestationId;
|
|
2424
2448
|
exports.queryByIdentity = queryByIdentity;
|
|
2425
2449
|
exports.queryRecent = queryRecent;
|
|
2450
|
+
exports.readBondExtension = readBondExtension;
|
|
2426
2451
|
exports.requiresManualVerification = requiresManualVerification;
|
|
2427
2452
|
exports.signNostrEvent = signNostrEvent;
|
|
2428
2453
|
exports.signatureIsValid = signatureIsValid;
|