@orangecheck/sdk 1.17.1 → 1.18.1
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/challenge.js +29 -0
- package/dist/challenge.js.map +1 -1
- package/dist/challenge.mjs +29 -0
- package/dist/challenge.mjs.map +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 +48 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +48 -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/scoring.js +22 -0
- package/dist/scoring.js.map +1 -1
- package/dist/scoring.mjs +22 -0
- package/dist/scoring.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 +27 -2
- package/dist/verify.js.map +1 -1
- package/dist/verify.mjs +27 -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":"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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 * 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/challenge.js
CHANGED
|
@@ -4,6 +4,34 @@ var bip322 = require('@orangecheck/bip322');
|
|
|
4
4
|
var buffer = require('buffer');
|
|
5
5
|
|
|
6
6
|
// src/challenge.ts
|
|
7
|
+
|
|
8
|
+
// src/options.ts
|
|
9
|
+
function refuseUnknown(where, opts, known) {
|
|
10
|
+
if (opts === null || typeof opts !== "object") {
|
|
11
|
+
throw new TypeError(`@orangecheck/sdk: ${where} must be an object`);
|
|
12
|
+
}
|
|
13
|
+
const unknown = Object.keys(opts).filter((k) => !known.has(k));
|
|
14
|
+
if (unknown.length > 0) {
|
|
15
|
+
throw new TypeError(
|
|
16
|
+
`@orangecheck/sdk: ${where}: unknown option ${unknown.join(", ")}. It reads only ${[...known].join(", ")}.`
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
const inherited = [...known].filter((k) => k in opts && !Object.prototype.hasOwnProperty.call(opts, k));
|
|
20
|
+
if (inherited.length > 0) {
|
|
21
|
+
throw new TypeError(`@orangecheck/sdk: ${where}: option ${inherited.join(", ")} is inherited, not set by the caller`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
var CHALLENGE_OPTIONS = /* @__PURE__ */ new Set([
|
|
25
|
+
"address",
|
|
26
|
+
"ttlSeconds",
|
|
27
|
+
"nonce",
|
|
28
|
+
"audience",
|
|
29
|
+
"purpose",
|
|
30
|
+
"now"
|
|
31
|
+
]);
|
|
32
|
+
function checkChallengeOptions(opts) {
|
|
33
|
+
refuseUnknown("issueChallenge()", opts, CHALLENGE_OPTIONS);
|
|
34
|
+
}
|
|
7
35
|
function toBase64Maybe(sig) {
|
|
8
36
|
if (/^[0-9a-fA-F]+$/.test(sig) && sig.length % 2 === 0) {
|
|
9
37
|
return buffer.Buffer.from(sig, "hex").toString("base64");
|
|
@@ -32,6 +60,7 @@ function iso(date) {
|
|
|
32
60
|
return date.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
33
61
|
}
|
|
34
62
|
function issueChallenge(opts) {
|
|
63
|
+
checkChallengeOptions(opts);
|
|
35
64
|
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
36
65
|
const ttl = opts.ttlSeconds ?? 300;
|
|
37
66
|
const expiresAt = new Date(now.getTime() + ttl * 1e3);
|
package/dist/challenge.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils/signature.ts","../src/challenge.ts"],"names":["Buffer","verifyBitcoinSignature"],"mappings":"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","file":"challenge.js","sourcesContent":["import type { Scheme } from '../types';\n\nimport { Buffer } from 'buffer';\n\n/** A signature as base64: hex input is converted, anything else passes through. */\nexport function toBase64Maybe(sig: string): string {\n if (/^[0-9a-fA-F]+$/.test(sig) && sig.length % 2 === 0) {\n return Buffer.from(sig, 'hex').toString('base64');\n }\n return sig;\n}\n\n/**\n * The scheme a signature's bytes say it is, the same rule @orangecheck/bip322\n * verifies by: 65 bytes is a BIP-137 compact signature, whatever the address,\n * and anything else is BIP-322 simple. `null` for a BIP-322 full or\n * proof-of-funds signature, which is not supported.\n */\nexport function signatureScheme(sigBase64: string): Scheme | null {\n if (/^(ful|pof)/.test(sigBase64)) return null;\n if (sigBase64.startsWith('smp')) return 'bip322';\n return Buffer.from(sigBase64, 'base64').length === 65 ? 'legacy' : 'bip322';\n}\n","/**\n * Signed-challenge auth\n *\n * When a gate can't trust the address source (public header, query string,\n * unsigned cookie), run a one-shot BIP-322 challenge first to cryptographically\n * prove the caller controls the address. Bind the proven address to a session\n * cookie or JWT, and the gate reads from there.\n *\n * This is a different wire format from the main OCP attestation — the header\n * literal is `orangecheck-auth`, not `orangecheck`, so a signed auth challenge\n * can never be confused with a reputation attestation.\n *\n * // Server, on the \"connect wallet\" flow:\n * const challenge = issueChallenge({ address, ttlSeconds: 300 });\n * // → send challenge.message to the client\n *\n * // Client signs challenge.message with BIP-322 via the user's wallet,\n * // then posts { message, signature } back.\n *\n * // Server, on callback:\n * const result = await verifyChallenge({ message, signature });\n * if (result.ok) {\n * // result.address is now cryptographically proven; attach to session.\n * }\n */\n\nimport type { Scheme } from './types';\n\nimport { verifySignature as verifyBitcoinSignature } from '@orangecheck/bip322';\n\nimport { signatureScheme, toBase64Maybe } from './utils/signature';\n\nconst AUTH_HEADER = 'orangecheck-auth';\nconst AUTH_ACK = 'I authorize this session under the OrangeCheck auth challenge.';\n\nfunction randomNonce(): string {\n // 16 bytes → 32 lowercase hex chars. Matches OCP core nonce format.\n const bytes = new Uint8Array(16);\n // Runtimes: Node 20+ (webcrypto), browsers.\n if (typeof globalThis.crypto?.getRandomValues === 'function') {\n globalThis.crypto.getRandomValues(bytes);\n } else {\n // Fallback: Math.random is *not* cryptographic — acceptable only for\n // environments without webcrypto (old Node). Flagged explicitly.\n for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);\n }\n return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');\n}\n\nfunction iso(date: Date): string {\n return date.toISOString().replace(/\\.\\d{3}Z$/, 'Z');\n}\n\nexport interface IssueChallengeOptions {\n /** Bitcoin address the caller claims to control. */\n address: string;\n /** How long the challenge is valid for. Default 300 (5 minutes). */\n ttlSeconds?: number;\n /** Explicit nonce. Rare; prefer auto-generated. */\n nonce?: string;\n /** Origin-binding extension. If set, verify will require a match. */\n audience?: string;\n /** Human-readable label baked into the signed message. */\n purpose?: string;\n /** Freeze `now` for tests. */\n now?: Date;\n}\n\nexport interface Challenge {\n /** The canonical text the client MUST sign. */\n message: string;\n /** The generated nonce (kept for correlation / server-side storage). */\n nonce: string;\n /** Unix ms epoch the challenge expires at. */\n expiresAt: number;\n /** ISO string of the same. */\n expiresAtIso: string;\n}\n\n/**\n * Build a new signed-challenge message. The server MUST remember the nonce\n * it issued against the session (Redis, signed cookie, DB) so it can detect\n * replay or a different nonce coming back on verify.\n */\nexport function issueChallenge(opts: IssueChallengeOptions): Challenge {\n const now = opts.now ?? new Date();\n const ttl = opts.ttlSeconds ?? 300;\n const expiresAt = new Date(now.getTime() + ttl * 1000);\n const nonce = opts.nonce ?? randomNonce();\n\n if (!/^[0-9a-f]{32}$/.test(nonce)) {\n throw new Error('nonce must be 32 lowercase hex characters');\n }\n\n const coreLines = [\n AUTH_HEADER,\n `address: ${opts.address}`,\n `nonce: ${nonce}`,\n `issued_at: ${iso(now)}`,\n `expires_at: ${iso(expiresAt)}`,\n `ack: ${AUTH_ACK}`,\n ];\n\n const extensions: Array<[string, string]> = [];\n if (opts.audience) extensions.push(['audience', opts.audience]);\n if (opts.purpose) extensions.push(['purpose', opts.purpose]);\n // Lexicographic sort by key — matches OCP canonicalization rules.\n extensions.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));\n\n const extLines = extensions.map(([k, v]) => `${k}: ${v}`);\n const message = [...coreLines, ...extLines].join('\\n') + '\\n';\n\n return {\n message,\n nonce,\n expiresAt: expiresAt.getTime(),\n expiresAtIso: iso(expiresAt),\n };\n}\n\ninterface ParsedChallenge {\n address?: string;\n nonce?: string;\n issuedAt?: Date;\n expiresAt?: Date;\n ack?: string;\n audience?: string;\n purpose?: string;\n extensions: Array<[string, string]>;\n}\n\nfunction parseChallenge(message: string): ParsedChallenge {\n const lines = message.endsWith('\\n') ? message.slice(0, -1).split('\\n') : message.split('\\n');\n\n if (lines[0] !== AUTH_HEADER) {\n throw new Error('not an orangecheck-auth challenge');\n }\n\n const out: ParsedChallenge = { extensions: [] };\n const coreKeys = ['address', 'nonce', 'issued_at', 'expires_at', 'ack'] as const;\n const coreKeySet = new Set<string>(coreKeys);\n // Duplicate detection: a challenge with two `address:` lines would\n // previously let the second overwrite the first. A signer who saw both\n // lines would still sign the whole text — which means the verifier and\n // the signer disagree about what the message \"said\". Refuse instead.\n const seenCore = new Set<string>();\n\n for (let i = 1; i < lines.length; i++) {\n const line = lines[i] ?? '';\n const sep = line.indexOf(': ');\n if (sep === -1) continue;\n const key = line.slice(0, sep);\n const val = line.slice(sep + 2);\n\n if (coreKeySet.has(key)) {\n if (seenCore.has(key)) {\n throw new Error(`duplicate core key in challenge: ${key}`);\n }\n seenCore.add(key);\n if (key === 'address') out.address = val;\n else if (key === 'nonce') out.nonce = val;\n else if (key === 'issued_at') out.issuedAt = new Date(val);\n else if (key === 'expires_at') out.expiresAt = new Date(val);\n else if (key === 'ack') out.ack = val;\n } else {\n out.extensions.push([key, val]);\n if (key === 'audience') out.audience = val;\n if (key === 'purpose') out.purpose = val;\n }\n }\n\n if (seenCore.size !== coreKeys.length) {\n throw new Error(`challenge missing core lines (${seenCore.size}/${coreKeys.length})`);\n }\n return out;\n}\n\nexport interface VerifyChallengeOptions {\n /** The signed canonical challenge text, exactly as issued. */\n message: string;\n /** BIP-322 or legacy signature, base64 or hex. */\n signature: string;\n /** @deprecated Ignored: the signature's own bytes decide the scheme. */\n scheme?: Scheme;\n\n /** If set, require the challenge's `audience:` extension to match. */\n expectedAudience?: string;\n /** If set, require the challenge's `purpose:` extension to match. */\n expectedPurpose?: string;\n /**\n * If set, require the challenge's `nonce:` to match. Use this to defeat\n * replay — pair with server-side nonce storage (Redis / signed cookie).\n */\n expectedNonce?: string;\n\n /** Tolerance on expiry check in ms. Default 0. */\n clockSkewMs?: number;\n /** Freeze `now` for tests. */\n now?: Date;\n}\n\nexport type VerifyChallengeReason =\n | 'ok'\n | 'malformed'\n | 'expired'\n | 'not_yet_valid'\n | 'sig_invalid'\n | 'sig_unsupported_scheme'\n | 'audience_mismatch'\n | 'purpose_mismatch'\n | 'nonce_mismatch';\n\nexport interface VerifyChallengeResult {\n ok: boolean;\n reason: VerifyChallengeReason;\n /** Present on success — the cryptographically proven Bitcoin address. */\n address?: string;\n /** Present on success — the scheme that verified the signature. */\n scheme?: Scheme;\n /** Present on success — when the challenge expires (unix ms). */\n expiresAt?: number;\n /** Present on success — the nonce from the challenge. */\n nonce?: string;\n /** Present on success — the audience extension, if any. */\n audience?: string;\n /** Present on success — the purpose extension, if any. */\n purpose?: string;\n}\n\n/**\n * Verify a signed challenge. Returns the proven address on success.\n *\n * The caller is responsible for nonce replay prevention — pass `expectedNonce`\n * with the value you stashed at issue time.\n */\nexport async function verifyChallenge(\n opts: VerifyChallengeOptions\n): Promise<VerifyChallengeResult> {\n let parsed: ParsedChallenge;\n try {\n parsed = parseChallenge(opts.message);\n } catch {\n return { ok: false, reason: 'malformed' };\n }\n\n const { address, nonce, issuedAt, expiresAt, ack, audience, purpose } = parsed;\n if (!address || !nonce || !issuedAt || !expiresAt || !ack) {\n return { ok: false, reason: 'malformed' };\n }\n if (ack !== AUTH_ACK) {\n return { ok: false, reason: 'malformed' };\n }\n if (!/^[0-9a-f]{32}$/.test(nonce)) {\n return { ok: false, reason: 'malformed' };\n }\n\n const now = opts.now ?? new Date();\n const skew = opts.clockSkewMs ?? 0;\n\n if (now.getTime() - skew > expiresAt.getTime()) {\n return { ok: false, reason: 'expired' };\n }\n if (now.getTime() + skew < issuedAt.getTime()) {\n return { ok: false, reason: 'not_yet_valid' };\n }\n\n if (opts.expectedNonce && nonce !== opts.expectedNonce) {\n return { ok: false, reason: 'nonce_mismatch' };\n }\n if (opts.expectedAudience && audience !== opts.expectedAudience) {\n return { ok: false, reason: 'audience_mismatch' };\n }\n if (opts.expectedPurpose && purpose !== opts.expectedPurpose) {\n return { ok: false, reason: 'purpose_mismatch' };\n }\n\n const sigBase64 = toBase64Maybe(opts.signature);\n const scheme = signatureScheme(sigBase64);\n if (!scheme) return { ok: false, reason: 'sig_unsupported_scheme' };\n if (!verifyBitcoinSignature(address, opts.message, sigBase64)) {\n return { ok: false, reason: 'sig_invalid' };\n }\n\n return {\n ok: true,\n reason: 'ok',\n address,\n scheme,\n expiresAt: expiresAt.getTime(),\n nonce,\n ...(audience !== undefined ? { audience } : {}),\n ...(purpose !== undefined ? { purpose } : {}),\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/options.ts","../src/utils/signature.ts","../src/challenge.ts"],"names":["Buffer","verifyBitcoinSignature"],"mappings":"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","file":"challenge.js","sourcesContent":["import type { IssueChallengeOptions } from './challenge';\nimport type { CheckParams } from './check';\nimport type { ScoringConfig } from './scoring';\nimport type { VerifyOptions } from './types';\n\n/** The options `check()` reads. */\nconst CHECK_OPTIONS: ReadonlySet<string> = new Set<keyof CheckParams>([\n 'addr',\n 'id',\n 'identity',\n 'minSats',\n 'minDays',\n 'relays',\n 'verifyOptions',\n]);\n\n/** The options `verify()` reads. */\nconst VERIFY_OPTIONS: ReadonlySet<string> = new Set<keyof VerifyOptions>([\n 'testMode',\n 'demoMode',\n 'expectedAud',\n 'esploraMainnetBase',\n 'esploraSignetBase',\n 'esploraTestnetBase',\n 'coverSats',\n]);\n\n/**\n * Throw on a key the SDK does not read. Ignored, a misspelt `minSats` would\n * leave the threshold at 0, and a misspelt `expectedAud` would skip the\n * audience check.\n */\nfunction refuseUnknown(where: string, opts: unknown, known: ReadonlySet<string>): void {\n if (opts === null || typeof opts !== 'object') {\n throw new TypeError(`@orangecheck/sdk: ${where} must be an object`);\n }\n const unknown = Object.keys(opts).filter((k) => !known.has(k));\n if (unknown.length > 0) {\n throw new TypeError(\n `@orangecheck/sdk: ${where}: unknown option ${unknown.join(', ')}. ` +\n `It reads only ${[...known].join(', ')}.`\n );\n }\n // Readers read opts.key, which follows the prototype: a known key must be the caller's own.\n const inherited = [...known].filter((k) => k in opts && !Object.prototype.hasOwnProperty.call(opts, k));\n if (inherited.length > 0) {\n throw new TypeError(`@orangecheck/sdk: ${where}: option ${inherited.join(', ')} is inherited, not set by the caller`);\n }\n}\n\nexport function checkCheckParams(params: CheckParams): void {\n refuseUnknown('check()', params, CHECK_OPTIONS);\n if (params.verifyOptions !== undefined) checkVerifyOptions(params.verifyOptions, 'verifyOptions');\n}\n\nexport function checkVerifyOptions(opts: VerifyOptions, where = 'verify()'): void {\n refuseUnknown(where, opts, VERIFY_OPTIONS);\n}\n\n/** The options `issueChallenge()` reads. */\nconst CHALLENGE_OPTIONS: ReadonlySet<string> = new Set<keyof IssueChallengeOptions>([\n 'address',\n 'ttlSeconds',\n 'nonce',\n 'audience',\n 'purpose',\n 'now',\n]);\n\nexport function checkChallengeOptions(opts: IssueChallengeOptions): void {\n refuseUnknown('issueChallenge()', opts, CHALLENGE_OPTIONS);\n}\n\n/** The keys `computeScore()` reads from its config. */\nconst SCORING_OPTIONS: ReadonlySet<string> = new Set<keyof ScoringConfig>(['algorithm']);\n\nexport function checkScoringConfig(config: ScoringConfig): void {\n refuseUnknown('computeScore()', config, SCORING_OPTIONS);\n}\n","import type { Scheme } from '../types';\n\nimport { Buffer } from 'buffer';\n\n/** A signature as base64: hex input is converted, anything else passes through. */\nexport function toBase64Maybe(sig: string): string {\n if (/^[0-9a-fA-F]+$/.test(sig) && sig.length % 2 === 0) {\n return Buffer.from(sig, 'hex').toString('base64');\n }\n return sig;\n}\n\n/**\n * The scheme a signature's bytes say it is, the same rule @orangecheck/bip322\n * verifies by: 65 bytes is a BIP-137 compact signature, whatever the address,\n * and anything else is BIP-322 simple. `null` for a BIP-322 full or\n * proof-of-funds signature, which is not supported.\n */\nexport function signatureScheme(sigBase64: string): Scheme | null {\n if (/^(ful|pof)/.test(sigBase64)) return null;\n if (sigBase64.startsWith('smp')) return 'bip322';\n return Buffer.from(sigBase64, 'base64').length === 65 ? 'legacy' : 'bip322';\n}\n","/**\n * Signed-challenge auth\n *\n * When a gate can't trust the address source (public header, query string,\n * unsigned cookie), run a one-shot BIP-322 challenge first to cryptographically\n * prove the caller controls the address. Bind the proven address to a session\n * cookie or JWT, and the gate reads from there.\n *\n * This is a different wire format from the main OCP attestation — the header\n * literal is `orangecheck-auth`, not `orangecheck`, so a signed auth challenge\n * can never be confused with a reputation attestation.\n *\n * // Server, on the \"connect wallet\" flow:\n * const challenge = issueChallenge({ address, ttlSeconds: 300 });\n * // → send challenge.message to the client\n *\n * // Client signs challenge.message with BIP-322 via the user's wallet,\n * // then posts { message, signature } back.\n *\n * // Server, on callback:\n * const result = await verifyChallenge({ message, signature });\n * if (result.ok) {\n * // result.address is now cryptographically proven; attach to session.\n * }\n */\n\nimport type { Scheme } from './types';\n\nimport { verifySignature as verifyBitcoinSignature } from '@orangecheck/bip322';\n\nimport { checkChallengeOptions } from './options';\nimport { signatureScheme, toBase64Maybe } from './utils/signature';\n\nconst AUTH_HEADER = 'orangecheck-auth';\nconst AUTH_ACK = 'I authorize this session under the OrangeCheck auth challenge.';\n\nfunction randomNonce(): string {\n // 16 bytes → 32 lowercase hex chars. Matches OCP core nonce format.\n const bytes = new Uint8Array(16);\n // Runtimes: Node 20+ (webcrypto), browsers.\n if (typeof globalThis.crypto?.getRandomValues === 'function') {\n globalThis.crypto.getRandomValues(bytes);\n } else {\n // Fallback: Math.random is *not* cryptographic — acceptable only for\n // environments without webcrypto (old Node). Flagged explicitly.\n for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);\n }\n return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');\n}\n\nfunction iso(date: Date): string {\n return date.toISOString().replace(/\\.\\d{3}Z$/, 'Z');\n}\n\nexport interface IssueChallengeOptions {\n /** Bitcoin address the caller claims to control. */\n address: string;\n /** How long the challenge is valid for. Default 300 (5 minutes). */\n ttlSeconds?: number;\n /** Explicit nonce. Rare; prefer auto-generated. */\n nonce?: string;\n /** Origin-binding extension. If set, verify will require a match. */\n audience?: string;\n /** Human-readable label baked into the signed message. */\n purpose?: string;\n /** Freeze `now` for tests. */\n now?: Date;\n}\n\nexport interface Challenge {\n /** The canonical text the client MUST sign. */\n message: string;\n /** The generated nonce (kept for correlation / server-side storage). */\n nonce: string;\n /** Unix ms epoch the challenge expires at. */\n expiresAt: number;\n /** ISO string of the same. */\n expiresAtIso: string;\n}\n\n/**\n * Build a new signed-challenge message. The server MUST remember the nonce\n * it issued against the session (Redis, signed cookie, DB) so it can detect\n * replay or a different nonce coming back on verify.\n */\nexport function issueChallenge(opts: IssueChallengeOptions): Challenge {\n checkChallengeOptions(opts);\n const now = opts.now ?? new Date();\n const ttl = opts.ttlSeconds ?? 300;\n const expiresAt = new Date(now.getTime() + ttl * 1000);\n const nonce = opts.nonce ?? randomNonce();\n\n if (!/^[0-9a-f]{32}$/.test(nonce)) {\n throw new Error('nonce must be 32 lowercase hex characters');\n }\n\n const coreLines = [\n AUTH_HEADER,\n `address: ${opts.address}`,\n `nonce: ${nonce}`,\n `issued_at: ${iso(now)}`,\n `expires_at: ${iso(expiresAt)}`,\n `ack: ${AUTH_ACK}`,\n ];\n\n const extensions: Array<[string, string]> = [];\n if (opts.audience) extensions.push(['audience', opts.audience]);\n if (opts.purpose) extensions.push(['purpose', opts.purpose]);\n // Lexicographic sort by key — matches OCP canonicalization rules.\n extensions.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));\n\n const extLines = extensions.map(([k, v]) => `${k}: ${v}`);\n const message = [...coreLines, ...extLines].join('\\n') + '\\n';\n\n return {\n message,\n nonce,\n expiresAt: expiresAt.getTime(),\n expiresAtIso: iso(expiresAt),\n };\n}\n\ninterface ParsedChallenge {\n address?: string;\n nonce?: string;\n issuedAt?: Date;\n expiresAt?: Date;\n ack?: string;\n audience?: string;\n purpose?: string;\n extensions: Array<[string, string]>;\n}\n\nfunction parseChallenge(message: string): ParsedChallenge {\n const lines = message.endsWith('\\n') ? message.slice(0, -1).split('\\n') : message.split('\\n');\n\n if (lines[0] !== AUTH_HEADER) {\n throw new Error('not an orangecheck-auth challenge');\n }\n\n const out: ParsedChallenge = { extensions: [] };\n const coreKeys = ['address', 'nonce', 'issued_at', 'expires_at', 'ack'] as const;\n const coreKeySet = new Set<string>(coreKeys);\n // Duplicate detection: a challenge with two `address:` lines would\n // previously let the second overwrite the first. A signer who saw both\n // lines would still sign the whole text — which means the verifier and\n // the signer disagree about what the message \"said\". Refuse instead.\n const seenCore = new Set<string>();\n\n for (let i = 1; i < lines.length; i++) {\n const line = lines[i] ?? '';\n const sep = line.indexOf(': ');\n if (sep === -1) continue;\n const key = line.slice(0, sep);\n const val = line.slice(sep + 2);\n\n if (coreKeySet.has(key)) {\n if (seenCore.has(key)) {\n throw new Error(`duplicate core key in challenge: ${key}`);\n }\n seenCore.add(key);\n if (key === 'address') out.address = val;\n else if (key === 'nonce') out.nonce = val;\n else if (key === 'issued_at') out.issuedAt = new Date(val);\n else if (key === 'expires_at') out.expiresAt = new Date(val);\n else if (key === 'ack') out.ack = val;\n } else {\n out.extensions.push([key, val]);\n if (key === 'audience') out.audience = val;\n if (key === 'purpose') out.purpose = val;\n }\n }\n\n if (seenCore.size !== coreKeys.length) {\n throw new Error(`challenge missing core lines (${seenCore.size}/${coreKeys.length})`);\n }\n return out;\n}\n\nexport interface VerifyChallengeOptions {\n /** The signed canonical challenge text, exactly as issued. */\n message: string;\n /** BIP-322 or legacy signature, base64 or hex. */\n signature: string;\n /** @deprecated Ignored: the signature's own bytes decide the scheme. */\n scheme?: Scheme;\n\n /** If set, require the challenge's `audience:` extension to match. */\n expectedAudience?: string;\n /** If set, require the challenge's `purpose:` extension to match. */\n expectedPurpose?: string;\n /**\n * If set, require the challenge's `nonce:` to match. Use this to defeat\n * replay — pair with server-side nonce storage (Redis / signed cookie).\n */\n expectedNonce?: string;\n\n /** Tolerance on expiry check in ms. Default 0. */\n clockSkewMs?: number;\n /** Freeze `now` for tests. */\n now?: Date;\n}\n\nexport type VerifyChallengeReason =\n | 'ok'\n | 'malformed'\n | 'expired'\n | 'not_yet_valid'\n | 'sig_invalid'\n | 'sig_unsupported_scheme'\n | 'audience_mismatch'\n | 'purpose_mismatch'\n | 'nonce_mismatch';\n\nexport interface VerifyChallengeResult {\n ok: boolean;\n reason: VerifyChallengeReason;\n /** Present on success — the cryptographically proven Bitcoin address. */\n address?: string;\n /** Present on success — the scheme that verified the signature. */\n scheme?: Scheme;\n /** Present on success — when the challenge expires (unix ms). */\n expiresAt?: number;\n /** Present on success — the nonce from the challenge. */\n nonce?: string;\n /** Present on success — the audience extension, if any. */\n audience?: string;\n /** Present on success — the purpose extension, if any. */\n purpose?: string;\n}\n\n/**\n * Verify a signed challenge. Returns the proven address on success.\n *\n * The caller is responsible for nonce replay prevention — pass `expectedNonce`\n * with the value you stashed at issue time.\n */\nexport async function verifyChallenge(\n opts: VerifyChallengeOptions\n): Promise<VerifyChallengeResult> {\n let parsed: ParsedChallenge;\n try {\n parsed = parseChallenge(opts.message);\n } catch {\n return { ok: false, reason: 'malformed' };\n }\n\n const { address, nonce, issuedAt, expiresAt, ack, audience, purpose } = parsed;\n if (!address || !nonce || !issuedAt || !expiresAt || !ack) {\n return { ok: false, reason: 'malformed' };\n }\n if (ack !== AUTH_ACK) {\n return { ok: false, reason: 'malformed' };\n }\n if (!/^[0-9a-f]{32}$/.test(nonce)) {\n return { ok: false, reason: 'malformed' };\n }\n\n const now = opts.now ?? new Date();\n const skew = opts.clockSkewMs ?? 0;\n\n if (now.getTime() - skew > expiresAt.getTime()) {\n return { ok: false, reason: 'expired' };\n }\n if (now.getTime() + skew < issuedAt.getTime()) {\n return { ok: false, reason: 'not_yet_valid' };\n }\n\n if (opts.expectedNonce && nonce !== opts.expectedNonce) {\n return { ok: false, reason: 'nonce_mismatch' };\n }\n if (opts.expectedAudience && audience !== opts.expectedAudience) {\n return { ok: false, reason: 'audience_mismatch' };\n }\n if (opts.expectedPurpose && purpose !== opts.expectedPurpose) {\n return { ok: false, reason: 'purpose_mismatch' };\n }\n\n const sigBase64 = toBase64Maybe(opts.signature);\n const scheme = signatureScheme(sigBase64);\n if (!scheme) return { ok: false, reason: 'sig_unsupported_scheme' };\n if (!verifyBitcoinSignature(address, opts.message, sigBase64)) {\n return { ok: false, reason: 'sig_invalid' };\n }\n\n return {\n ok: true,\n reason: 'ok',\n address,\n scheme,\n expiresAt: expiresAt.getTime(),\n nonce,\n ...(audience !== undefined ? { audience } : {}),\n ...(purpose !== undefined ? { purpose } : {}),\n };\n}\n"]}
|
package/dist/challenge.mjs
CHANGED
|
@@ -2,6 +2,34 @@ import { verifySignature } from '@orangecheck/bip322';
|
|
|
2
2
|
import { Buffer } from 'buffer';
|
|
3
3
|
|
|
4
4
|
// src/challenge.ts
|
|
5
|
+
|
|
6
|
+
// src/options.ts
|
|
7
|
+
function refuseUnknown(where, opts, known) {
|
|
8
|
+
if (opts === null || typeof opts !== "object") {
|
|
9
|
+
throw new TypeError(`@orangecheck/sdk: ${where} must be an object`);
|
|
10
|
+
}
|
|
11
|
+
const unknown = Object.keys(opts).filter((k) => !known.has(k));
|
|
12
|
+
if (unknown.length > 0) {
|
|
13
|
+
throw new TypeError(
|
|
14
|
+
`@orangecheck/sdk: ${where}: unknown option ${unknown.join(", ")}. It reads only ${[...known].join(", ")}.`
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
const inherited = [...known].filter((k) => k in opts && !Object.prototype.hasOwnProperty.call(opts, k));
|
|
18
|
+
if (inherited.length > 0) {
|
|
19
|
+
throw new TypeError(`@orangecheck/sdk: ${where}: option ${inherited.join(", ")} is inherited, not set by the caller`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
var CHALLENGE_OPTIONS = /* @__PURE__ */ new Set([
|
|
23
|
+
"address",
|
|
24
|
+
"ttlSeconds",
|
|
25
|
+
"nonce",
|
|
26
|
+
"audience",
|
|
27
|
+
"purpose",
|
|
28
|
+
"now"
|
|
29
|
+
]);
|
|
30
|
+
function checkChallengeOptions(opts) {
|
|
31
|
+
refuseUnknown("issueChallenge()", opts, CHALLENGE_OPTIONS);
|
|
32
|
+
}
|
|
5
33
|
function toBase64Maybe(sig) {
|
|
6
34
|
if (/^[0-9a-fA-F]+$/.test(sig) && sig.length % 2 === 0) {
|
|
7
35
|
return Buffer.from(sig, "hex").toString("base64");
|
|
@@ -30,6 +58,7 @@ function iso(date) {
|
|
|
30
58
|
return date.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
31
59
|
}
|
|
32
60
|
function issueChallenge(opts) {
|
|
61
|
+
checkChallengeOptions(opts);
|
|
33
62
|
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
34
63
|
const ttl = opts.ttlSeconds ?? 300;
|
|
35
64
|
const expiresAt = new Date(now.getTime() + ttl * 1e3);
|
package/dist/challenge.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils/signature.ts","../src/challenge.ts"],"names":["verifyBitcoinSignature"],"mappings":"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","file":"challenge.mjs","sourcesContent":["import type { Scheme } from '../types';\n\nimport { Buffer } from 'buffer';\n\n/** A signature as base64: hex input is converted, anything else passes through. */\nexport function toBase64Maybe(sig: string): string {\n if (/^[0-9a-fA-F]+$/.test(sig) && sig.length % 2 === 0) {\n return Buffer.from(sig, 'hex').toString('base64');\n }\n return sig;\n}\n\n/**\n * The scheme a signature's bytes say it is, the same rule @orangecheck/bip322\n * verifies by: 65 bytes is a BIP-137 compact signature, whatever the address,\n * and anything else is BIP-322 simple. `null` for a BIP-322 full or\n * proof-of-funds signature, which is not supported.\n */\nexport function signatureScheme(sigBase64: string): Scheme | null {\n if (/^(ful|pof)/.test(sigBase64)) return null;\n if (sigBase64.startsWith('smp')) return 'bip322';\n return Buffer.from(sigBase64, 'base64').length === 65 ? 'legacy' : 'bip322';\n}\n","/**\n * Signed-challenge auth\n *\n * When a gate can't trust the address source (public header, query string,\n * unsigned cookie), run a one-shot BIP-322 challenge first to cryptographically\n * prove the caller controls the address. Bind the proven address to a session\n * cookie or JWT, and the gate reads from there.\n *\n * This is a different wire format from the main OCP attestation — the header\n * literal is `orangecheck-auth`, not `orangecheck`, so a signed auth challenge\n * can never be confused with a reputation attestation.\n *\n * // Server, on the \"connect wallet\" flow:\n * const challenge = issueChallenge({ address, ttlSeconds: 300 });\n * // → send challenge.message to the client\n *\n * // Client signs challenge.message with BIP-322 via the user's wallet,\n * // then posts { message, signature } back.\n *\n * // Server, on callback:\n * const result = await verifyChallenge({ message, signature });\n * if (result.ok) {\n * // result.address is now cryptographically proven; attach to session.\n * }\n */\n\nimport type { Scheme } from './types';\n\nimport { verifySignature as verifyBitcoinSignature } from '@orangecheck/bip322';\n\nimport { signatureScheme, toBase64Maybe } from './utils/signature';\n\nconst AUTH_HEADER = 'orangecheck-auth';\nconst AUTH_ACK = 'I authorize this session under the OrangeCheck auth challenge.';\n\nfunction randomNonce(): string {\n // 16 bytes → 32 lowercase hex chars. Matches OCP core nonce format.\n const bytes = new Uint8Array(16);\n // Runtimes: Node 20+ (webcrypto), browsers.\n if (typeof globalThis.crypto?.getRandomValues === 'function') {\n globalThis.crypto.getRandomValues(bytes);\n } else {\n // Fallback: Math.random is *not* cryptographic — acceptable only for\n // environments without webcrypto (old Node). Flagged explicitly.\n for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);\n }\n return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');\n}\n\nfunction iso(date: Date): string {\n return date.toISOString().replace(/\\.\\d{3}Z$/, 'Z');\n}\n\nexport interface IssueChallengeOptions {\n /** Bitcoin address the caller claims to control. */\n address: string;\n /** How long the challenge is valid for. Default 300 (5 minutes). */\n ttlSeconds?: number;\n /** Explicit nonce. Rare; prefer auto-generated. */\n nonce?: string;\n /** Origin-binding extension. If set, verify will require a match. */\n audience?: string;\n /** Human-readable label baked into the signed message. */\n purpose?: string;\n /** Freeze `now` for tests. */\n now?: Date;\n}\n\nexport interface Challenge {\n /** The canonical text the client MUST sign. */\n message: string;\n /** The generated nonce (kept for correlation / server-side storage). */\n nonce: string;\n /** Unix ms epoch the challenge expires at. */\n expiresAt: number;\n /** ISO string of the same. */\n expiresAtIso: string;\n}\n\n/**\n * Build a new signed-challenge message. The server MUST remember the nonce\n * it issued against the session (Redis, signed cookie, DB) so it can detect\n * replay or a different nonce coming back on verify.\n */\nexport function issueChallenge(opts: IssueChallengeOptions): Challenge {\n const now = opts.now ?? new Date();\n const ttl = opts.ttlSeconds ?? 300;\n const expiresAt = new Date(now.getTime() + ttl * 1000);\n const nonce = opts.nonce ?? randomNonce();\n\n if (!/^[0-9a-f]{32}$/.test(nonce)) {\n throw new Error('nonce must be 32 lowercase hex characters');\n }\n\n const coreLines = [\n AUTH_HEADER,\n `address: ${opts.address}`,\n `nonce: ${nonce}`,\n `issued_at: ${iso(now)}`,\n `expires_at: ${iso(expiresAt)}`,\n `ack: ${AUTH_ACK}`,\n ];\n\n const extensions: Array<[string, string]> = [];\n if (opts.audience) extensions.push(['audience', opts.audience]);\n if (opts.purpose) extensions.push(['purpose', opts.purpose]);\n // Lexicographic sort by key — matches OCP canonicalization rules.\n extensions.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));\n\n const extLines = extensions.map(([k, v]) => `${k}: ${v}`);\n const message = [...coreLines, ...extLines].join('\\n') + '\\n';\n\n return {\n message,\n nonce,\n expiresAt: expiresAt.getTime(),\n expiresAtIso: iso(expiresAt),\n };\n}\n\ninterface ParsedChallenge {\n address?: string;\n nonce?: string;\n issuedAt?: Date;\n expiresAt?: Date;\n ack?: string;\n audience?: string;\n purpose?: string;\n extensions: Array<[string, string]>;\n}\n\nfunction parseChallenge(message: string): ParsedChallenge {\n const lines = message.endsWith('\\n') ? message.slice(0, -1).split('\\n') : message.split('\\n');\n\n if (lines[0] !== AUTH_HEADER) {\n throw new Error('not an orangecheck-auth challenge');\n }\n\n const out: ParsedChallenge = { extensions: [] };\n const coreKeys = ['address', 'nonce', 'issued_at', 'expires_at', 'ack'] as const;\n const coreKeySet = new Set<string>(coreKeys);\n // Duplicate detection: a challenge with two `address:` lines would\n // previously let the second overwrite the first. A signer who saw both\n // lines would still sign the whole text — which means the verifier and\n // the signer disagree about what the message \"said\". Refuse instead.\n const seenCore = new Set<string>();\n\n for (let i = 1; i < lines.length; i++) {\n const line = lines[i] ?? '';\n const sep = line.indexOf(': ');\n if (sep === -1) continue;\n const key = line.slice(0, sep);\n const val = line.slice(sep + 2);\n\n if (coreKeySet.has(key)) {\n if (seenCore.has(key)) {\n throw new Error(`duplicate core key in challenge: ${key}`);\n }\n seenCore.add(key);\n if (key === 'address') out.address = val;\n else if (key === 'nonce') out.nonce = val;\n else if (key === 'issued_at') out.issuedAt = new Date(val);\n else if (key === 'expires_at') out.expiresAt = new Date(val);\n else if (key === 'ack') out.ack = val;\n } else {\n out.extensions.push([key, val]);\n if (key === 'audience') out.audience = val;\n if (key === 'purpose') out.purpose = val;\n }\n }\n\n if (seenCore.size !== coreKeys.length) {\n throw new Error(`challenge missing core lines (${seenCore.size}/${coreKeys.length})`);\n }\n return out;\n}\n\nexport interface VerifyChallengeOptions {\n /** The signed canonical challenge text, exactly as issued. */\n message: string;\n /** BIP-322 or legacy signature, base64 or hex. */\n signature: string;\n /** @deprecated Ignored: the signature's own bytes decide the scheme. */\n scheme?: Scheme;\n\n /** If set, require the challenge's `audience:` extension to match. */\n expectedAudience?: string;\n /** If set, require the challenge's `purpose:` extension to match. */\n expectedPurpose?: string;\n /**\n * If set, require the challenge's `nonce:` to match. Use this to defeat\n * replay — pair with server-side nonce storage (Redis / signed cookie).\n */\n expectedNonce?: string;\n\n /** Tolerance on expiry check in ms. Default 0. */\n clockSkewMs?: number;\n /** Freeze `now` for tests. */\n now?: Date;\n}\n\nexport type VerifyChallengeReason =\n | 'ok'\n | 'malformed'\n | 'expired'\n | 'not_yet_valid'\n | 'sig_invalid'\n | 'sig_unsupported_scheme'\n | 'audience_mismatch'\n | 'purpose_mismatch'\n | 'nonce_mismatch';\n\nexport interface VerifyChallengeResult {\n ok: boolean;\n reason: VerifyChallengeReason;\n /** Present on success — the cryptographically proven Bitcoin address. */\n address?: string;\n /** Present on success — the scheme that verified the signature. */\n scheme?: Scheme;\n /** Present on success — when the challenge expires (unix ms). */\n expiresAt?: number;\n /** Present on success — the nonce from the challenge. */\n nonce?: string;\n /** Present on success — the audience extension, if any. */\n audience?: string;\n /** Present on success — the purpose extension, if any. */\n purpose?: string;\n}\n\n/**\n * Verify a signed challenge. Returns the proven address on success.\n *\n * The caller is responsible for nonce replay prevention — pass `expectedNonce`\n * with the value you stashed at issue time.\n */\nexport async function verifyChallenge(\n opts: VerifyChallengeOptions\n): Promise<VerifyChallengeResult> {\n let parsed: ParsedChallenge;\n try {\n parsed = parseChallenge(opts.message);\n } catch {\n return { ok: false, reason: 'malformed' };\n }\n\n const { address, nonce, issuedAt, expiresAt, ack, audience, purpose } = parsed;\n if (!address || !nonce || !issuedAt || !expiresAt || !ack) {\n return { ok: false, reason: 'malformed' };\n }\n if (ack !== AUTH_ACK) {\n return { ok: false, reason: 'malformed' };\n }\n if (!/^[0-9a-f]{32}$/.test(nonce)) {\n return { ok: false, reason: 'malformed' };\n }\n\n const now = opts.now ?? new Date();\n const skew = opts.clockSkewMs ?? 0;\n\n if (now.getTime() - skew > expiresAt.getTime()) {\n return { ok: false, reason: 'expired' };\n }\n if (now.getTime() + skew < issuedAt.getTime()) {\n return { ok: false, reason: 'not_yet_valid' };\n }\n\n if (opts.expectedNonce && nonce !== opts.expectedNonce) {\n return { ok: false, reason: 'nonce_mismatch' };\n }\n if (opts.expectedAudience && audience !== opts.expectedAudience) {\n return { ok: false, reason: 'audience_mismatch' };\n }\n if (opts.expectedPurpose && purpose !== opts.expectedPurpose) {\n return { ok: false, reason: 'purpose_mismatch' };\n }\n\n const sigBase64 = toBase64Maybe(opts.signature);\n const scheme = signatureScheme(sigBase64);\n if (!scheme) return { ok: false, reason: 'sig_unsupported_scheme' };\n if (!verifyBitcoinSignature(address, opts.message, sigBase64)) {\n return { ok: false, reason: 'sig_invalid' };\n }\n\n return {\n ok: true,\n reason: 'ok',\n address,\n scheme,\n expiresAt: expiresAt.getTime(),\n nonce,\n ...(audience !== undefined ? { audience } : {}),\n ...(purpose !== undefined ? { purpose } : {}),\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/options.ts","../src/utils/signature.ts","../src/challenge.ts"],"names":["verifyBitcoinSignature"],"mappings":"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","file":"challenge.mjs","sourcesContent":["import type { IssueChallengeOptions } from './challenge';\nimport type { CheckParams } from './check';\nimport type { ScoringConfig } from './scoring';\nimport type { VerifyOptions } from './types';\n\n/** The options `check()` reads. */\nconst CHECK_OPTIONS: ReadonlySet<string> = new Set<keyof CheckParams>([\n 'addr',\n 'id',\n 'identity',\n 'minSats',\n 'minDays',\n 'relays',\n 'verifyOptions',\n]);\n\n/** The options `verify()` reads. */\nconst VERIFY_OPTIONS: ReadonlySet<string> = new Set<keyof VerifyOptions>([\n 'testMode',\n 'demoMode',\n 'expectedAud',\n 'esploraMainnetBase',\n 'esploraSignetBase',\n 'esploraTestnetBase',\n 'coverSats',\n]);\n\n/**\n * Throw on a key the SDK does not read. Ignored, a misspelt `minSats` would\n * leave the threshold at 0, and a misspelt `expectedAud` would skip the\n * audience check.\n */\nfunction refuseUnknown(where: string, opts: unknown, known: ReadonlySet<string>): void {\n if (opts === null || typeof opts !== 'object') {\n throw new TypeError(`@orangecheck/sdk: ${where} must be an object`);\n }\n const unknown = Object.keys(opts).filter((k) => !known.has(k));\n if (unknown.length > 0) {\n throw new TypeError(\n `@orangecheck/sdk: ${where}: unknown option ${unknown.join(', ')}. ` +\n `It reads only ${[...known].join(', ')}.`\n );\n }\n // Readers read opts.key, which follows the prototype: a known key must be the caller's own.\n const inherited = [...known].filter((k) => k in opts && !Object.prototype.hasOwnProperty.call(opts, k));\n if (inherited.length > 0) {\n throw new TypeError(`@orangecheck/sdk: ${where}: option ${inherited.join(', ')} is inherited, not set by the caller`);\n }\n}\n\nexport function checkCheckParams(params: CheckParams): void {\n refuseUnknown('check()', params, CHECK_OPTIONS);\n if (params.verifyOptions !== undefined) checkVerifyOptions(params.verifyOptions, 'verifyOptions');\n}\n\nexport function checkVerifyOptions(opts: VerifyOptions, where = 'verify()'): void {\n refuseUnknown(where, opts, VERIFY_OPTIONS);\n}\n\n/** The options `issueChallenge()` reads. */\nconst CHALLENGE_OPTIONS: ReadonlySet<string> = new Set<keyof IssueChallengeOptions>([\n 'address',\n 'ttlSeconds',\n 'nonce',\n 'audience',\n 'purpose',\n 'now',\n]);\n\nexport function checkChallengeOptions(opts: IssueChallengeOptions): void {\n refuseUnknown('issueChallenge()', opts, CHALLENGE_OPTIONS);\n}\n\n/** The keys `computeScore()` reads from its config. */\nconst SCORING_OPTIONS: ReadonlySet<string> = new Set<keyof ScoringConfig>(['algorithm']);\n\nexport function checkScoringConfig(config: ScoringConfig): void {\n refuseUnknown('computeScore()', config, SCORING_OPTIONS);\n}\n","import type { Scheme } from '../types';\n\nimport { Buffer } from 'buffer';\n\n/** A signature as base64: hex input is converted, anything else passes through. */\nexport function toBase64Maybe(sig: string): string {\n if (/^[0-9a-fA-F]+$/.test(sig) && sig.length % 2 === 0) {\n return Buffer.from(sig, 'hex').toString('base64');\n }\n return sig;\n}\n\n/**\n * The scheme a signature's bytes say it is, the same rule @orangecheck/bip322\n * verifies by: 65 bytes is a BIP-137 compact signature, whatever the address,\n * and anything else is BIP-322 simple. `null` for a BIP-322 full or\n * proof-of-funds signature, which is not supported.\n */\nexport function signatureScheme(sigBase64: string): Scheme | null {\n if (/^(ful|pof)/.test(sigBase64)) return null;\n if (sigBase64.startsWith('smp')) return 'bip322';\n return Buffer.from(sigBase64, 'base64').length === 65 ? 'legacy' : 'bip322';\n}\n","/**\n * Signed-challenge auth\n *\n * When a gate can't trust the address source (public header, query string,\n * unsigned cookie), run a one-shot BIP-322 challenge first to cryptographically\n * prove the caller controls the address. Bind the proven address to a session\n * cookie or JWT, and the gate reads from there.\n *\n * This is a different wire format from the main OCP attestation — the header\n * literal is `orangecheck-auth`, not `orangecheck`, so a signed auth challenge\n * can never be confused with a reputation attestation.\n *\n * // Server, on the \"connect wallet\" flow:\n * const challenge = issueChallenge({ address, ttlSeconds: 300 });\n * // → send challenge.message to the client\n *\n * // Client signs challenge.message with BIP-322 via the user's wallet,\n * // then posts { message, signature } back.\n *\n * // Server, on callback:\n * const result = await verifyChallenge({ message, signature });\n * if (result.ok) {\n * // result.address is now cryptographically proven; attach to session.\n * }\n */\n\nimport type { Scheme } from './types';\n\nimport { verifySignature as verifyBitcoinSignature } from '@orangecheck/bip322';\n\nimport { checkChallengeOptions } from './options';\nimport { signatureScheme, toBase64Maybe } from './utils/signature';\n\nconst AUTH_HEADER = 'orangecheck-auth';\nconst AUTH_ACK = 'I authorize this session under the OrangeCheck auth challenge.';\n\nfunction randomNonce(): string {\n // 16 bytes → 32 lowercase hex chars. Matches OCP core nonce format.\n const bytes = new Uint8Array(16);\n // Runtimes: Node 20+ (webcrypto), browsers.\n if (typeof globalThis.crypto?.getRandomValues === 'function') {\n globalThis.crypto.getRandomValues(bytes);\n } else {\n // Fallback: Math.random is *not* cryptographic — acceptable only for\n // environments without webcrypto (old Node). Flagged explicitly.\n for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);\n }\n return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');\n}\n\nfunction iso(date: Date): string {\n return date.toISOString().replace(/\\.\\d{3}Z$/, 'Z');\n}\n\nexport interface IssueChallengeOptions {\n /** Bitcoin address the caller claims to control. */\n address: string;\n /** How long the challenge is valid for. Default 300 (5 minutes). */\n ttlSeconds?: number;\n /** Explicit nonce. Rare; prefer auto-generated. */\n nonce?: string;\n /** Origin-binding extension. If set, verify will require a match. */\n audience?: string;\n /** Human-readable label baked into the signed message. */\n purpose?: string;\n /** Freeze `now` for tests. */\n now?: Date;\n}\n\nexport interface Challenge {\n /** The canonical text the client MUST sign. */\n message: string;\n /** The generated nonce (kept for correlation / server-side storage). */\n nonce: string;\n /** Unix ms epoch the challenge expires at. */\n expiresAt: number;\n /** ISO string of the same. */\n expiresAtIso: string;\n}\n\n/**\n * Build a new signed-challenge message. The server MUST remember the nonce\n * it issued against the session (Redis, signed cookie, DB) so it can detect\n * replay or a different nonce coming back on verify.\n */\nexport function issueChallenge(opts: IssueChallengeOptions): Challenge {\n checkChallengeOptions(opts);\n const now = opts.now ?? new Date();\n const ttl = opts.ttlSeconds ?? 300;\n const expiresAt = new Date(now.getTime() + ttl * 1000);\n const nonce = opts.nonce ?? randomNonce();\n\n if (!/^[0-9a-f]{32}$/.test(nonce)) {\n throw new Error('nonce must be 32 lowercase hex characters');\n }\n\n const coreLines = [\n AUTH_HEADER,\n `address: ${opts.address}`,\n `nonce: ${nonce}`,\n `issued_at: ${iso(now)}`,\n `expires_at: ${iso(expiresAt)}`,\n `ack: ${AUTH_ACK}`,\n ];\n\n const extensions: Array<[string, string]> = [];\n if (opts.audience) extensions.push(['audience', opts.audience]);\n if (opts.purpose) extensions.push(['purpose', opts.purpose]);\n // Lexicographic sort by key — matches OCP canonicalization rules.\n extensions.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));\n\n const extLines = extensions.map(([k, v]) => `${k}: ${v}`);\n const message = [...coreLines, ...extLines].join('\\n') + '\\n';\n\n return {\n message,\n nonce,\n expiresAt: expiresAt.getTime(),\n expiresAtIso: iso(expiresAt),\n };\n}\n\ninterface ParsedChallenge {\n address?: string;\n nonce?: string;\n issuedAt?: Date;\n expiresAt?: Date;\n ack?: string;\n audience?: string;\n purpose?: string;\n extensions: Array<[string, string]>;\n}\n\nfunction parseChallenge(message: string): ParsedChallenge {\n const lines = message.endsWith('\\n') ? message.slice(0, -1).split('\\n') : message.split('\\n');\n\n if (lines[0] !== AUTH_HEADER) {\n throw new Error('not an orangecheck-auth challenge');\n }\n\n const out: ParsedChallenge = { extensions: [] };\n const coreKeys = ['address', 'nonce', 'issued_at', 'expires_at', 'ack'] as const;\n const coreKeySet = new Set<string>(coreKeys);\n // Duplicate detection: a challenge with two `address:` lines would\n // previously let the second overwrite the first. A signer who saw both\n // lines would still sign the whole text — which means the verifier and\n // the signer disagree about what the message \"said\". Refuse instead.\n const seenCore = new Set<string>();\n\n for (let i = 1; i < lines.length; i++) {\n const line = lines[i] ?? '';\n const sep = line.indexOf(': ');\n if (sep === -1) continue;\n const key = line.slice(0, sep);\n const val = line.slice(sep + 2);\n\n if (coreKeySet.has(key)) {\n if (seenCore.has(key)) {\n throw new Error(`duplicate core key in challenge: ${key}`);\n }\n seenCore.add(key);\n if (key === 'address') out.address = val;\n else if (key === 'nonce') out.nonce = val;\n else if (key === 'issued_at') out.issuedAt = new Date(val);\n else if (key === 'expires_at') out.expiresAt = new Date(val);\n else if (key === 'ack') out.ack = val;\n } else {\n out.extensions.push([key, val]);\n if (key === 'audience') out.audience = val;\n if (key === 'purpose') out.purpose = val;\n }\n }\n\n if (seenCore.size !== coreKeys.length) {\n throw new Error(`challenge missing core lines (${seenCore.size}/${coreKeys.length})`);\n }\n return out;\n}\n\nexport interface VerifyChallengeOptions {\n /** The signed canonical challenge text, exactly as issued. */\n message: string;\n /** BIP-322 or legacy signature, base64 or hex. */\n signature: string;\n /** @deprecated Ignored: the signature's own bytes decide the scheme. */\n scheme?: Scheme;\n\n /** If set, require the challenge's `audience:` extension to match. */\n expectedAudience?: string;\n /** If set, require the challenge's `purpose:` extension to match. */\n expectedPurpose?: string;\n /**\n * If set, require the challenge's `nonce:` to match. Use this to defeat\n * replay — pair with server-side nonce storage (Redis / signed cookie).\n */\n expectedNonce?: string;\n\n /** Tolerance on expiry check in ms. Default 0. */\n clockSkewMs?: number;\n /** Freeze `now` for tests. */\n now?: Date;\n}\n\nexport type VerifyChallengeReason =\n | 'ok'\n | 'malformed'\n | 'expired'\n | 'not_yet_valid'\n | 'sig_invalid'\n | 'sig_unsupported_scheme'\n | 'audience_mismatch'\n | 'purpose_mismatch'\n | 'nonce_mismatch';\n\nexport interface VerifyChallengeResult {\n ok: boolean;\n reason: VerifyChallengeReason;\n /** Present on success — the cryptographically proven Bitcoin address. */\n address?: string;\n /** Present on success — the scheme that verified the signature. */\n scheme?: Scheme;\n /** Present on success — when the challenge expires (unix ms). */\n expiresAt?: number;\n /** Present on success — the nonce from the challenge. */\n nonce?: string;\n /** Present on success — the audience extension, if any. */\n audience?: string;\n /** Present on success — the purpose extension, if any. */\n purpose?: string;\n}\n\n/**\n * Verify a signed challenge. Returns the proven address on success.\n *\n * The caller is responsible for nonce replay prevention — pass `expectedNonce`\n * with the value you stashed at issue time.\n */\nexport async function verifyChallenge(\n opts: VerifyChallengeOptions\n): Promise<VerifyChallengeResult> {\n let parsed: ParsedChallenge;\n try {\n parsed = parseChallenge(opts.message);\n } catch {\n return { ok: false, reason: 'malformed' };\n }\n\n const { address, nonce, issuedAt, expiresAt, ack, audience, purpose } = parsed;\n if (!address || !nonce || !issuedAt || !expiresAt || !ack) {\n return { ok: false, reason: 'malformed' };\n }\n if (ack !== AUTH_ACK) {\n return { ok: false, reason: 'malformed' };\n }\n if (!/^[0-9a-f]{32}$/.test(nonce)) {\n return { ok: false, reason: 'malformed' };\n }\n\n const now = opts.now ?? new Date();\n const skew = opts.clockSkewMs ?? 0;\n\n if (now.getTime() - skew > expiresAt.getTime()) {\n return { ok: false, reason: 'expired' };\n }\n if (now.getTime() + skew < issuedAt.getTime()) {\n return { ok: false, reason: 'not_yet_valid' };\n }\n\n if (opts.expectedNonce && nonce !== opts.expectedNonce) {\n return { ok: false, reason: 'nonce_mismatch' };\n }\n if (opts.expectedAudience && audience !== opts.expectedAudience) {\n return { ok: false, reason: 'audience_mismatch' };\n }\n if (opts.expectedPurpose && purpose !== opts.expectedPurpose) {\n return { ok: false, reason: 'purpose_mismatch' };\n }\n\n const sigBase64 = toBase64Maybe(opts.signature);\n const scheme = signatureScheme(sigBase64);\n if (!scheme) return { ok: false, reason: 'sig_unsupported_scheme' };\n if (!verifyBitcoinSignature(address, opts.message, sigBase64)) {\n return { ok: false, reason: 'sig_invalid' };\n }\n\n return {\n ok: true,\n reason: 'ok',\n address,\n scheme,\n expiresAt: expiresAt.getTime(),\n nonce,\n ...(audience !== undefined ? { audience } : {}),\n ...(purpose !== undefined ? { purpose } : {}),\n };\n}\n"]}
|
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';
|