@orangecheck/sdk 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -8
- package/dist/attestation.d.mts +1 -2
- package/dist/attestation.d.ts +1 -2
- package/dist/attestation.js +12 -28
- package/dist/attestation.js.map +1 -1
- package/dist/attestation.mjs +13 -28
- package/dist/attestation.mjs.map +1 -1
- package/dist/canonical.js +27 -5
- package/dist/canonical.js.map +1 -1
- package/dist/canonical.mjs +27 -5
- package/dist/canonical.mjs.map +1 -1
- package/dist/challenge.js +10 -6
- package/dist/challenge.js.map +1 -1
- package/dist/challenge.mjs +10 -6
- package/dist/challenge.mjs.map +1 -1
- package/dist/identity.js +130 -44
- package/dist/identity.js.map +1 -1
- package/dist/identity.mjs +130 -44
- package/dist/identity.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +210 -74
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +208 -75
- package/dist/index.mjs.map +1 -1
- package/dist/nostr.js +5 -6
- package/dist/nostr.js.map +1 -1
- package/dist/nostr.mjs +5 -6
- package/dist/nostr.mjs.map +1 -1
- package/dist/verify.js +44 -13
- package/dist/verify.js.map +1 -1
- package/dist/verify.mjs +44 -13
- package/dist/verify.mjs.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -205,12 +205,11 @@ await publishAttestation({ envelope, npub: userNpub });
|
|
|
205
205
|
```ts
|
|
206
206
|
import { verifyIdentity } from '@orangecheck/sdk';
|
|
207
207
|
|
|
208
|
-
const result = await verifyIdentity(
|
|
209
|
-
|
|
210
|
-
identifier: 'alice'
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
});
|
|
208
|
+
const result = await verifyIdentity(
|
|
209
|
+
envelope.attestation_id,
|
|
210
|
+
{ protocol: 'github', identifier: 'alice' }
|
|
211
|
+
// Optional: pass { relays } for 'nostr', { tweetUrl } for 'twitter'.
|
|
212
|
+
);
|
|
214
213
|
|
|
215
214
|
if (result.verified) {
|
|
216
215
|
// the GitHub gist contains the attestation ID → handle ownership proven
|
|
@@ -226,7 +225,7 @@ Identity bindings are self-asserted inside the signed message. They are **claims
|
|
|
226
225
|
The reference algorithm is intentionally simple:
|
|
227
226
|
|
|
228
227
|
```
|
|
229
|
-
score_v0 = round( ln(1 + sats_bonded)
|
|
228
|
+
score_v0 = round( ln(1 + sats_bonded) * (1 + days_unspent / 30), 2 )
|
|
230
229
|
```
|
|
231
230
|
|
|
232
231
|
This is **advisory**. Relying parties should compare `sats_bonded` and `days_unspent` against their own thresholds rather than trusting a displayed score. `check()` does exactly that.
|
|
@@ -250,7 +249,7 @@ buildCanonicalMessage(...)
|
|
|
250
249
|
generateAttestationId(msg: string): Promise<string>
|
|
251
250
|
publishAttestation({ envelope, npub, relays? })
|
|
252
251
|
discoverAttestations({ attestationId | address | identity, relays? })
|
|
253
|
-
verifyIdentity({ protocol, identifier
|
|
252
|
+
verifyIdentity(attestationId, { protocol, identifier }, options?)
|
|
254
253
|
```
|
|
255
254
|
|
|
256
255
|
Full type definitions ship with the package.
|
package/dist/attestation.d.mts
CHANGED
|
@@ -36,9 +36,8 @@ declare function discoverAttestations(options: DiscoverAttestationOptions): Prom
|
|
|
36
36
|
declare function verifyAttestationById(attestationId: string, relays?: string[]): Promise<AttestationEnvelope | null>;
|
|
37
37
|
declare function getAttestationsForAddress(address: string, relays?: string[]): Promise<AttestationEnvelope[]>;
|
|
38
38
|
declare function getAttestationsForIdentity(protocol: string, identifier: string, relays?: string[]): Promise<AttestationEnvelope[]>;
|
|
39
|
-
declare function parseIdentitiesFromString(identitiesStr: string): IdentityBinding[];
|
|
40
39
|
declare function formatIdentitiesForDisplay(identities: IdentityBinding[]): string;
|
|
41
40
|
declare function getVerificationUrl(attestationId: string, baseUrl?: string): string;
|
|
42
41
|
declare function extractAttestationIdFromUrl(url: string): string | null;
|
|
43
42
|
|
|
44
|
-
export { AttestationError, type CreateAttestationOptions, type DiscoverAttestationOptions, type PublishAttestationOptions, type PublishResult, createAttestation, discoverAttestations, extractAttestationIdFromUrl, formatIdentitiesForDisplay, getAttestationsForAddress, getAttestationsForIdentity, getVerificationUrl,
|
|
43
|
+
export { AttestationError, type CreateAttestationOptions, type DiscoverAttestationOptions, type PublishAttestationOptions, type PublishResult, createAttestation, discoverAttestations, extractAttestationIdFromUrl, formatIdentitiesForDisplay, getAttestationsForAddress, getAttestationsForIdentity, getVerificationUrl, publishAttestation, verifyAttestationById };
|
package/dist/attestation.d.ts
CHANGED
|
@@ -36,9 +36,8 @@ declare function discoverAttestations(options: DiscoverAttestationOptions): Prom
|
|
|
36
36
|
declare function verifyAttestationById(attestationId: string, relays?: string[]): Promise<AttestationEnvelope | null>;
|
|
37
37
|
declare function getAttestationsForAddress(address: string, relays?: string[]): Promise<AttestationEnvelope[]>;
|
|
38
38
|
declare function getAttestationsForIdentity(protocol: string, identifier: string, relays?: string[]): Promise<AttestationEnvelope[]>;
|
|
39
|
-
declare function parseIdentitiesFromString(identitiesStr: string): IdentityBinding[];
|
|
40
39
|
declare function formatIdentitiesForDisplay(identities: IdentityBinding[]): string;
|
|
41
40
|
declare function getVerificationUrl(attestationId: string, baseUrl?: string): string;
|
|
42
41
|
declare function extractAttestationIdFromUrl(url: string): string | null;
|
|
43
42
|
|
|
44
|
-
export { AttestationError, type CreateAttestationOptions, type DiscoverAttestationOptions, type PublishAttestationOptions, type PublishResult, createAttestation, discoverAttestations, extractAttestationIdFromUrl, formatIdentitiesForDisplay, getAttestationsForAddress, getAttestationsForIdentity, getVerificationUrl,
|
|
43
|
+
export { AttestationError, type CreateAttestationOptions, type DiscoverAttestationOptions, type PublishAttestationOptions, type PublishResult, createAttestation, discoverAttestations, extractAttestationIdFromUrl, formatIdentitiesForDisplay, getAttestationsForAddress, getAttestationsForIdentity, getVerificationUrl, publishAttestation, verifyAttestationById };
|
package/dist/attestation.js
CHANGED
|
@@ -4,21 +4,6 @@
|
|
|
4
4
|
function toLowerHex(bytes) {
|
|
5
5
|
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
6
6
|
}
|
|
7
|
-
function parseIdentities(identitiesStr) {
|
|
8
|
-
if (!identitiesStr || identitiesStr.trim() === "") {
|
|
9
|
-
return [];
|
|
10
|
-
}
|
|
11
|
-
return identitiesStr.split(",").map((binding) => {
|
|
12
|
-
const colonIndex = binding.indexOf(":");
|
|
13
|
-
if (colonIndex === -1) {
|
|
14
|
-
throw new Error(`Invalid identity binding format: ${binding}`);
|
|
15
|
-
}
|
|
16
|
-
return {
|
|
17
|
-
protocol: binding.substring(0, colonIndex).trim(),
|
|
18
|
-
identifier: binding.substring(colonIndex + 1).trim()
|
|
19
|
-
};
|
|
20
|
-
}).filter((binding) => binding.protocol && binding.identifier);
|
|
21
|
-
}
|
|
22
7
|
async function generateAttestationId(message) {
|
|
23
8
|
const encoder = new TextEncoder();
|
|
24
9
|
const data = encoder.encode(message);
|
|
@@ -147,16 +132,15 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
|
|
|
147
132
|
ws = new WebSocket(relayUrl);
|
|
148
133
|
const wsRef = ws;
|
|
149
134
|
await new Promise((resolve, reject) => {
|
|
150
|
-
const
|
|
135
|
+
const deadline = setTimeout(() => {
|
|
151
136
|
wsRef.close();
|
|
152
|
-
reject(new Error("
|
|
153
|
-
},
|
|
137
|
+
reject(new Error("Relay publish timeout"));
|
|
138
|
+
}, 1e4);
|
|
154
139
|
const cleanup = () => {
|
|
155
|
-
clearTimeout(
|
|
140
|
+
clearTimeout(deadline);
|
|
156
141
|
wsRef.close();
|
|
157
142
|
};
|
|
158
143
|
wsRef.onopen = () => {
|
|
159
|
-
clearTimeout(timeout);
|
|
160
144
|
wsRef.send(JSON.stringify(["EVENT", event]));
|
|
161
145
|
};
|
|
162
146
|
wsRef.onmessage = (msg) => {
|
|
@@ -194,7 +178,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
|
|
|
194
178
|
reject(err);
|
|
195
179
|
};
|
|
196
180
|
wsRef.onclose = () => {
|
|
197
|
-
clearTimeout(
|
|
181
|
+
clearTimeout(deadline);
|
|
198
182
|
};
|
|
199
183
|
});
|
|
200
184
|
} catch (err) {
|
|
@@ -546,19 +530,20 @@ async function getAttestationsForIdentity(protocol, identifier, relays = DEFAULT
|
|
|
546
530
|
log3.info({ protocol, identifier }, "Getting all attestations for identity");
|
|
547
531
|
return discoverAttestations({ identity: { protocol, identifier }, relays });
|
|
548
532
|
}
|
|
549
|
-
function parseIdentitiesFromString(identitiesStr) {
|
|
550
|
-
return parseIdentities(identitiesStr);
|
|
551
|
-
}
|
|
552
533
|
function formatIdentitiesForDisplay(identities) {
|
|
553
534
|
return identities.map((id) => `${id.protocol}:${id.identifier}`).join(", ");
|
|
554
535
|
}
|
|
555
536
|
function getVerificationUrl(attestationId, baseUrl = "https://ochk.io") {
|
|
556
|
-
return `${baseUrl}/
|
|
537
|
+
return `${baseUrl}/attest/${attestationId}`;
|
|
557
538
|
}
|
|
558
539
|
function extractAttestationIdFromUrl(url) {
|
|
559
540
|
try {
|
|
560
|
-
const
|
|
561
|
-
|
|
541
|
+
const u = new URL(url);
|
|
542
|
+
const m = u.pathname.match(/^\/attest\/([0-9a-f]{64})\/?$/i);
|
|
543
|
+
if (m) return m[1].toLowerCase();
|
|
544
|
+
const q = u.searchParams.get("id");
|
|
545
|
+
if (q && /^[0-9a-f]{64}$/i.test(q)) return q.toLowerCase();
|
|
546
|
+
return null;
|
|
562
547
|
} catch {
|
|
563
548
|
return null;
|
|
564
549
|
}
|
|
@@ -572,7 +557,6 @@ exports.formatIdentitiesForDisplay = formatIdentitiesForDisplay;
|
|
|
572
557
|
exports.getAttestationsForAddress = getAttestationsForAddress;
|
|
573
558
|
exports.getAttestationsForIdentity = getAttestationsForIdentity;
|
|
574
559
|
exports.getVerificationUrl = getVerificationUrl;
|
|
575
|
-
exports.parseIdentitiesFromString = parseIdentitiesFromString;
|
|
576
560
|
exports.publishAttestation = publishAttestation;
|
|
577
561
|
exports.verifyAttestationById = verifyAttestationById;
|
|
578
562
|
//# sourceMappingURL=attestation.js.map
|
package/dist/attestation.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/canonical.ts","../src/utils/logger.ts","../src/nostr.ts","../src/nostr-crypto.ts","../src/attestation.ts"],"names":["log"],"mappings":"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","file":"attestation.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 // Fallback (non-cryptographic) for environments without Web Crypto\n for (let i = 0; i < arr.length; i++) {\n arr[i] = Math.floor(Math.random() * 256);\n }\n return toLowerHex(arr);\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\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\n return identitiesStr\n .split(',')\n .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 .filter((binding) => binding.protocol && binding.identifier);\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 * @returns Canonical message string with LF line endings\n */\nexport function buildCanonicalMessage(\n { address, identities }: CanonicalInput,\n extensions: Extensions = {}\n): string {\n const nonce = randomNonce16BHexLower();\n const issuedAt = new Date().toISOString();\n\n // Format identities\n const identitiesStr = formatIdentities(identities);\n\n // Core (strict wording + order per SPEC.md §2)\n const core = [\n 'orangecheck',\n `identities: ${identitiesStr}`,\n `address: ${address}`,\n 'purpose: portable reputation attestation (non-custodial)',\n `nonce: ${nonce}`,\n `issued_at: ${issuedAt}`,\n 'ack: I attest control of this address and bind it to my identities.',\n ];\n\n // Normalize and sort extensions\n const normExt: Record<string, string> = {};\n for (const [k, v] of Object.entries(extensions)) {\n if (v == null || v === '') {\n continue;\n }\n const key = k.toLowerCase();\n normExt[key] = String(v);\n }\n\n const extLines = Object.keys(normExt)\n .sort()\n .map((k) => `${k}: ${normExt[k]!}`);\n\n const all = [...core, ...extLines];\n\n // Join with LF and ensure exactly one trailing LF\n return all.join('\\n') + '\\n';\n}\n\n/**\n * Generate attestation ID from canonical message\n *\n * attestation_id = SHA-256(canonical_message)\n *\n * @param message - Canonical message string\n * @returns Lowercase hex SHA-256 hash\n */\nexport async function generateAttestationId(message: string): Promise<string> {\n // Convert message to UTF-8 bytes\n const encoder = new TextEncoder();\n const data = encoder.encode(message);\n\n // Compute SHA-256 hash\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n\n // Convert to lowercase hex\n return toLowerHex(new Uint8Array(hashBuffer));\n}\n\n/**\n * Create an attestation envelope for publishing\n *\n * This wraps the canonical message with metadata for Nostr publishing\n *\n * @param message - Canonical message\n * @param signature - BIP-322 or legacy signature\n * @param scheme - Signature scheme used\n * @param address - Bitcoin address\n * @param identities - Identity bindings\n * @param extensions - Optional extensions\n * @returns Attestation envelope ready for publishing\n */\nexport async function createAttestationEnvelope(\n message: string,\n signature: string,\n scheme: Scheme,\n address: string,\n identities: IdentityBinding[],\n extensions: Extensions = {}\n): Promise<AttestationEnvelope> {\n const attestation_id = await generateAttestationId(message);\n\n // Extract issued_at from message\n const issuedAtMatch = message.match(/issued_at: ([^\\n]+)/);\n const issued_at = issuedAtMatch?.[1] ?? new Date().toISOString();\n\n // Extract expires from extensions if present\n const expires_at = extensions.expires;\n\n // Extract relay hints from extensions if present\n const relay_hints = extensions.relay_hints?.split(',').map((r) => r.trim());\n\n // Extract publish targets from extensions if present\n const publish_targets = extensions.publish?.split(',').map((t) => t.trim());\n\n // Base64url encode the message\n const encoder = new TextEncoder();\n const messageBytes = encoder.encode(message);\n const base64 = btoa(String.fromCharCode(...messageBytes));\n const message_b64url = base64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');\n\n // Construct verification URL\n const verification_url = `https://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","/**\n * Simple logger for SDK\n * Can be disabled in production builds\n */\n\n/**\n * Log level type\n */\nexport type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';\n\n/**\n * Logger context type\n */\nexport type LogContext = Record<string, unknown>;\n\n/**\n * Logger interface\n */\nexport interface Logger {\n debug: (contextOrMessage: string | LogContext, message?: string) => void;\n info: (contextOrMessage: string | LogContext, message?: string) => void;\n warn: (contextOrMessage: string | LogContext, message?: string) => void;\n error: (contextOrMessage: string | LogContext, message?: string) => void;\n}\n\nlet logLevel: LogLevel = 'none';\n\n/**\n * Set the global log level\n * @param level - Log level to set\n */\nexport function setLogLevel(level: LogLevel): void {\n logLevel = level;\n}\n\n/**\n * Get the current log level\n * @returns Current log level\n */\nexport function getLogLevel(): LogLevel {\n return logLevel;\n}\n\n/**\n * Create a namespaced logger\n * @param namespace - Logger namespace\n * @returns Logger instance\n */\nexport function createLogger(namespace: string): Logger {\n const shouldLog = (level: Exclude<LogLevel, 'none'>): boolean => {\n if (logLevel === 'none') return false;\n\n const levels: Exclude<LogLevel, 'none'>[] = ['debug', 'info', 'warn', 'error'];\n const currentLevelIndex = levels.indexOf(logLevel as Exclude<LogLevel, 'none'>);\n const messageLevelIndex = levels.indexOf(level);\n\n return messageLevelIndex >= currentLevelIndex;\n };\n\n const formatMessage = (msg: string): string => `[${namespace}] ${msg}`;\n\n return {\n debug: (contextOrMessage: string | LogContext, message?: string): void => {\n if (shouldLog('debug')) {\n if (typeof contextOrMessage === 'string') {\n console.debug(formatMessage(contextOrMessage));\n } else {\n console.debug(formatMessage(message ?? 'Debug'), contextOrMessage);\n }\n }\n },\n info: (contextOrMessage: string | LogContext, message?: string): void => {\n if (shouldLog('info')) {\n if (typeof contextOrMessage === 'string') {\n console.info(formatMessage(contextOrMessage));\n } else {\n console.info(formatMessage(message ?? 'Info'), contextOrMessage);\n }\n }\n },\n warn: (contextOrMessage: string | LogContext, message?: string): void => {\n if (shouldLog('warn')) {\n if (typeof contextOrMessage === 'string') {\n console.warn(formatMessage(contextOrMessage));\n } else {\n console.warn(formatMessage(message ?? 'Warning'), contextOrMessage);\n }\n }\n },\n error: (contextOrMessage: string | LogContext, message?: string): void => {\n if (shouldLog('error')) {\n if (typeof contextOrMessage === 'string') {\n console.error(formatMessage(contextOrMessage));\n } else {\n console.error(formatMessage(message ?? 'Error'), contextOrMessage);\n }\n }\n },\n };\n}\n\n/**\n * Log verification error\n * @param protocol - Identity protocol\n * @param identifier - Identity identifier\n * @param error - Error object\n * @param context - Additional context\n */\nexport function logVerificationError(\n protocol: string,\n identifier: string,\n error: Error,\n context?: LogContext\n): void {\n const logger = createLogger('identity-verification');\n logger.error(\n {\n protocol,\n identifier,\n error: error.message,\n stack: error.stack,\n ...context,\n },\n 'Verification failed'\n );\n}\n","/**\n * Nostr Integration for OrangeCheck Protocol\n *\n * Implements NIP-78 (Application-specific data) for attestation publishing\n */\n\nimport type { AttestationEnvelope, IdentityBinding, NostrEvent } from './types';\n\nimport { createLogger } from './utils/logger';\n\nconst log = createLogger('ocp/nostr');\n\n/**\n * NIP-78 event kind for OrangeCheck attestations\n */\nexport const ATTESTATION_EVENT_KIND = 30078;\n\n/**\n * Default Nostr relays for OrangeCheck attestation publishing\n *\n * These are well-known, reliable relays with good uptime\n */\nexport const DEFAULT_RELAYS: string[] = [\n 'wss://relay.damus.io',\n 'wss://relay.nostr.band',\n 'wss://nos.lol',\n 'wss://relay.snort.social',\n];\n\n/**\n * Create a NIP-78 event for an OrangeCheck attestation\n *\n * Kind: 30078 (Parameterized Replaceable Event)\n * d-tag: attestation_id (makes it replaceable by attestation ID)\n *\n * @param envelope - Attestation envelope\n * @param pubkey - Nostr public key (hex format)\n * @returns Unsigned Nostr event\n */\nexport function createAttestationEvent(\n envelope: AttestationEnvelope,\n pubkey: string\n): Omit<NostrEvent, 'id' | 'sig'> {\n const tags: string[][] = [\n ['d', envelope.attestation_id], // Parameterized replaceable event identifier\n ['address', envelope.address], // Bitcoin address\n ['scheme', envelope.scheme], // Signature scheme (bip322 or legacy)\n ['issued_at', envelope.issued_at], // ISO timestamp\n ];\n\n // Add identity tags\n for (const identity of envelope.identities) {\n tags.push(['i', `${identity.protocol}:${identity.identifier}`]);\n }\n\n // Add expiration if present\n if (envelope.expires_at) {\n tags.push(['expires', envelope.expires_at]);\n }\n\n // Add relay hints if present\n if (envelope.relay_hints?.length) {\n for (const relay of envelope.relay_hints) {\n tags.push(['relay', relay]);\n }\n }\n\n // Content is the full attestation envelope as JSON\n const content = JSON.stringify(envelope, null, 2);\n\n return {\n kind: ATTESTATION_EVENT_KIND,\n tags,\n content,\n created_at: Math.floor(Date.parse(envelope.issued_at) / 1000),\n pubkey,\n };\n}\n\n/**\n * Parse identity bindings from Nostr event tags\n *\n * @param event - Nostr event\n * @returns Array of identity bindings\n */\nexport function parseIdentitiesFromEvent(event: NostrEvent): IdentityBinding[] {\n const identities: IdentityBinding[] = [];\n\n for (const tag of event.tags) {\n if (tag[0] === 'i' && tag[1]) {\n const colonIndex = tag[1].indexOf(':');\n if (colonIndex !== -1) {\n identities.push({\n protocol: tag[1].substring(0, colonIndex),\n identifier: tag[1].substring(colonIndex + 1),\n });\n }\n }\n }\n\n return identities;\n}\n\n/**\n * Extract attestation ID from Nostr event d-tag\n *\n * @param event - Nostr event\n * @returns Attestation ID or undefined if not found\n */\nexport function getAttestationIdFromEvent(event: NostrEvent): string | undefined {\n const dTag = event.tags.find((tag) => tag[0] === 'd');\n return dTag?.[1];\n}\n\n/**\n * Extract Bitcoin address from Nostr event tags\n *\n * @param event - Nostr event\n * @returns Bitcoin address or undefined if not found\n */\nexport function getAddressFromEvent(event: NostrEvent): string | undefined {\n const addressTag = event.tags.find((tag) => tag[0] === 'address');\n return addressTag?.[1];\n}\n\n/**\n * Publish a Nostr event to multiple relays\n * Returns array of relay URLs that successfully accepted the event\n */\nexport async function publishToRelays(\n event: NostrEvent,\n relays: string[] = DEFAULT_RELAYS\n): Promise<{ success: string[]; failed: string[] }> {\n const success: string[] = [];\n const failed: string[] = [];\n\n log.info({ eventId: event.id, relayCount: relays.length }, 'Publishing event to relays');\n\n const publishPromises = relays.map(async (relayUrl) => {\n let ws: WebSocket | null = null;\n\n try {\n // Connect to relay\n ws = new WebSocket(relayUrl);\n const wsRef = ws; // Capture for closures\n\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n wsRef.close();\n reject(new Error('Connection timeout'));\n }, 5000);\n\n const cleanup = () => {\n clearTimeout(timeout);\n wsRef.close();\n };\n\n wsRef.onopen = () => {\n clearTimeout(timeout);\n // Send EVENT message\n wsRef.send(JSON.stringify(['EVENT', event]));\n };\n\n wsRef.onmessage = (msg) => {\n try {\n const data = JSON.parse(msg.data);\n // Check for OK response\n if (data[0] === 'OK' && data[1] === event.id) {\n if (data[2] === true) {\n success.push(relayUrl);\n cleanup();\n resolve();\n } else {\n log.warn(\n { relay: relayUrl, reason: data[3] },\n 'Relay rejected event'\n );\n failed.push(relayUrl);\n cleanup();\n reject(new Error(data[3] || 'Relay rejected event'));\n }\n }\n } catch (err) {\n log.error(\n { relay: relayUrl, error: err },\n 'Failed to parse relay response'\n );\n failed.push(relayUrl);\n cleanup();\n reject(err);\n }\n };\n\n wsRef.onerror = (err) => {\n log.error({ relay: relayUrl, error: err }, 'WebSocket error');\n failed.push(relayUrl);\n cleanup();\n reject(err);\n };\n\n wsRef.onclose = () => {\n clearTimeout(timeout);\n };\n });\n } catch (err) {\n log.error({ relay: relayUrl, error: err }, 'Failed to publish to relay');\n if (!failed.includes(relayUrl)) {\n failed.push(relayUrl);\n }\n } finally {\n // Ensure WebSocket is closed\n if (ws && ws.readyState !== WebSocket.CLOSED) {\n ws.close();\n }\n }\n });\n\n await Promise.allSettled(publishPromises);\n\n log.info({ successCount: success.length, failedCount: failed.length }, 'Publishing complete');\n\n return { success, failed };\n}\n\n/**\n * Query relays for attestations by attestation ID\n */\nexport async function queryByAttestationId(\n attestationId: string,\n relays: string[] = DEFAULT_RELAYS\n): Promise<NostrEvent[]> {\n const events: NostrEvent[] = [];\n\n log.info({ attestationId, relayCount: relays.length }, 'Querying relays for attestation');\n\n const queryPromises = relays.map(async (relayUrl) => {\n let ws: WebSocket | null = null;\n\n try {\n ws = new WebSocket(relayUrl);\n const wsRef = ws; // Capture for closures\n\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n wsRef.close();\n reject(new Error('Query timeout'));\n }, 10000);\n\n const cleanup = () => {\n clearTimeout(timeout);\n wsRef.close();\n };\n\n wsRef.onopen = () => {\n // Send REQ message for kind 30078 with d-tag = attestation_id\n const subscriptionId = `ochk_${Date.now()}`;\n wsRef.send(\n JSON.stringify([\n 'REQ',\n subscriptionId,\n {\n kinds: [30078],\n '#d': [attestationId],\n limit: 1,\n },\n ])\n );\n };\n\n wsRef.onmessage = (msg) => {\n try {\n const data = JSON.parse(msg.data);\n if (data[0] === 'EVENT') {\n events.push(data[2]);\n } else if (data[0] === 'EOSE') {\n cleanup();\n resolve();\n }\n } catch (err) {\n log.error({ relay: relayUrl, error: err }, 'Failed to parse event');\n }\n };\n\n wsRef.onerror = (err) => {\n log.error({ relay: relayUrl, error: err }, 'WebSocket error during query');\n cleanup();\n reject(err);\n };\n\n wsRef.onclose = () => {\n clearTimeout(timeout);\n };\n });\n } catch (err) {\n log.error({ relay: relayUrl, error: err }, 'Failed to query relay');\n } finally {\n // Ensure WebSocket is closed\n if (ws && ws.readyState !== WebSocket.CLOSED) {\n ws.close();\n }\n }\n });\n\n await Promise.allSettled(queryPromises);\n\n log.info({ eventCount: events.length }, 'Query complete');\n\n return events;\n}\n\n/**\n * Query relays for attestations by Bitcoin address\n */\nexport async function queryByAddress(\n address: string,\n relays: string[] = DEFAULT_RELAYS\n): Promise<NostrEvent[]> {\n const events: NostrEvent[] = [];\n\n log.info({ address, relayCount: relays.length }, 'Querying relays for address');\n\n const queryPromises = relays.map(async (relayUrl) => {\n try {\n const ws = new WebSocket(relayUrl);\n\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n ws.close();\n reject(new Error('Query timeout'));\n }, 10000);\n\n ws.onopen = () => {\n const subscriptionId = `ochk_addr_${Date.now()}`;\n ws.send(\n JSON.stringify([\n 'REQ',\n subscriptionId,\n {\n kinds: [30078],\n '#address': [address],\n limit: 10,\n },\n ])\n );\n };\n\n ws.onmessage = (msg) => {\n try {\n const data = JSON.parse(msg.data);\n if (data[0] === 'EVENT') {\n events.push(data[2]);\n } else if (data[0] === 'EOSE') {\n clearTimeout(timeout);\n ws.close();\n resolve();\n }\n } catch (err) {\n log.error({ relay: relayUrl, error: err }, 'Failed to parse event');\n }\n };\n\n ws.onerror = (err) => {\n clearTimeout(timeout);\n log.error({ relay: relayUrl, error: err }, 'WebSocket error during query');\n reject(err);\n };\n });\n } catch (err) {\n log.error({ relay: relayUrl, error: err }, 'Failed to query relay');\n }\n });\n\n await Promise.allSettled(queryPromises);\n\n log.info({ eventCount: events.length }, 'Query complete');\n\n return events;\n}\n\n/**\n * Query relays for attestations by identity (any protocol)\n */\nexport async function queryByIdentity(\n protocol: string,\n identifier: string,\n relays: string[] = DEFAULT_RELAYS\n): Promise<NostrEvent[]> {\n const events: NostrEvent[] = [];\n const identityTag = `${protocol}:${identifier}`;\n\n log.info({ protocol, identifier, relayCount: relays.length }, 'Querying relays for identity');\n\n const queryPromises = relays.map(async (relayUrl) => {\n try {\n const ws = new WebSocket(relayUrl);\n\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n ws.close();\n reject(new Error('Query timeout'));\n }, 10000);\n\n ws.onopen = () => {\n const subscriptionId = `ochk_identity_${Date.now()}`;\n ws.send(\n JSON.stringify([\n 'REQ',\n subscriptionId,\n {\n kinds: [30078],\n '#i': [identityTag],\n limit: 10,\n },\n ])\n );\n };\n\n ws.onmessage = (msg) => {\n try {\n const data = JSON.parse(msg.data);\n if (data[0] === 'EVENT') {\n events.push(data[2]);\n } else if (data[0] === 'EOSE') {\n clearTimeout(timeout);\n ws.close();\n resolve();\n }\n } catch (err) {\n log.error({ relay: relayUrl, error: err }, 'Failed to parse event');\n }\n };\n\n ws.onerror = (err) => {\n clearTimeout(timeout);\n log.error({ relay: relayUrl, error: err }, 'WebSocket error during query');\n reject(err);\n };\n });\n } catch (err) {\n log.error({ relay: relayUrl, error: err }, 'Failed to query relay');\n }\n });\n\n await Promise.allSettled(queryPromises);\n\n log.info({ eventCount: events.length }, 'Query complete');\n\n return events;\n}\n\n/**\n * Verify attestation envelope integrity\n * Checks that the attestation ID matches the message hash\n */\nexport async function verifyAttestationEnvelope(envelope: AttestationEnvelope): Promise<boolean> {\n try {\n // Re-compute attestation ID from message\n const encoder = new TextEncoder();\n const data = encoder.encode(envelope.message);\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n const hashArray = Array.from(new Uint8Array(hashBuffer));\n const computedId = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');\n\n return computedId === envelope.attestation_id;\n } catch (err) {\n log.error({ error: err }, 'Failed to verify attestation envelope');\n return false;\n }\n}\n\n/**\n * Parse attestation envelope from Nostr event content\n */\nexport function parseAttestationFromEvent(event: NostrEvent): AttestationEnvelope | null {\n try {\n const envelope = JSON.parse(event.content) as AttestationEnvelope;\n return envelope;\n } catch (err) {\n log.error({ eventId: event.id, error: err }, 'Failed to parse attestation from event');\n return null;\n }\n}\n","/**\n * Nostr Cryptographic Functions\n * Implements NIP-01 event ID computation and signature verification\n */\n\nimport type { NostrEvent } from './types';\n\nimport { createLogger } from './utils/logger';\n\ntype Nip07Window = {\n nostr?: {\n getPublicKey: () => Promise<string>;\n signEvent: (event: Omit<NostrEvent, 'id' | 'sig'>) => Promise<NostrEvent>;\n _metadata?: { name?: string; version?: string };\n };\n};\n\nconst log = createLogger('ocp/nostr-crypto');\n\n/**\n * Compute Nostr event ID according to NIP-01\n * Event ID is the SHA-256 hash of the serialized event\n * Serialization: [0, pubkey, created_at, kind, tags, content]\n */\nexport async function computeNostrEventId(event: Omit<NostrEvent, 'id' | 'sig'>): Promise<string> {\n // Serialize event according to NIP-01\n const serialized = JSON.stringify([\n 0, // Reserved for future use\n event.pubkey,\n event.created_at,\n event.kind,\n event.tags,\n event.content,\n ]);\n\n log.debug({ serialized }, 'Computing event ID');\n\n // Compute SHA-256 hash\n const encoder = new TextEncoder();\n const data = encoder.encode(serialized);\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n\n // Convert to lowercase hex\n const hashArray = Array.from(new Uint8Array(hashBuffer));\n const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');\n\n log.debug({ eventId: hashHex }, 'Computed event ID');\n\n return hashHex;\n}\n\n/**\n * Verify Nostr event ID\n * Recomputes the ID and checks if it matches\n */\nexport async function verifyNostrEventId(event: NostrEvent): Promise<boolean> {\n const computedId = await computeNostrEventId(event);\n return computedId === event.id;\n}\n\n/**\n * Check if NIP-07 extension is available\n * NIP-07 provides window.nostr for signing events\n */\nexport function isNip07Available(): boolean {\n if (typeof window === 'undefined') {\n return false;\n }\n\n return !!(window as unknown as { nostr?: Nip07Window['nostr'] }).nostr!;\n}\n\n/**\n * Get Nostr public key from NIP-07 extension\n * Returns null if extension not available or user denies\n */\nexport async function getNostrPublicKey(): Promise<string | null> {\n if (!isNip07Available()) {\n log.warn('NIP-07 extension not available');\n return null;\n }\n\n try {\n const nostr = (window as unknown as { nostr?: Nip07Window['nostr'] }).nostr!;\n const pubkey = await nostr.getPublicKey();\n log.info({ pubkey }, 'Got Nostr public key from NIP-07');\n return pubkey;\n } catch (err) {\n log.error({ error: err }, 'Failed to get Nostr public key');\n return null;\n }\n}\n\n/**\n * Sign Nostr event using NIP-07 extension\n * Returns signed event with id and sig fields\n */\nexport async function signNostrEvent(\n event: Omit<NostrEvent, 'id' | 'sig'>\n): Promise<NostrEvent | null> {\n if (!isNip07Available()) {\n log.warn('NIP-07 extension not available');\n return null;\n }\n\n try {\n const nostr = (window as unknown as { nostr?: Nip07Window['nostr'] }).nostr!;\n\n // NIP-07 signEvent expects the event without id and sig\n const signedEvent = await nostr.signEvent(event);\n\n log.info({ eventId: signedEvent.id }, 'Signed Nostr event with NIP-07');\n\n return signedEvent as NostrEvent;\n } catch (err) {\n log.error({ error: err }, 'Failed to sign Nostr event');\n return null;\n }\n}\n\n// Previously this file exported `generateEphemeralKeypair()` and\n// `signEventWithEphemeralKey()` for demo purposes. Both produced non-schnorr\n// values that Nostr relays would silently reject — creating the false\n// impression that publishing had succeeded. They have been removed. All Nostr\n// signing now flows through `signNostrEvent()` / `window.nostr` (NIP-07).\n\n/**\n * Validate Nostr event structure\n */\nexport function validateNostrEvent(event: unknown): event is NostrEvent {\n if (typeof event !== 'object' || event === null) return false;\n const e = event as Record<string, unknown>;\n\n if (typeof e.id !== 'string' || (e.id as string).length !== 64) return false;\n if (typeof e.pubkey !== 'string' || (e.pubkey as string).length !== 64) return false;\n if (typeof e.created_at !== 'number') return false;\n if (typeof e.kind !== 'number') return false;\n if (!Array.isArray(e.tags)) return false;\n if (typeof e.content !== 'string') return false;\n if (typeof e.sig !== 'string' || (e.sig as string).length !== 128) return false;\n\n return true;\n}\n\n/**\n * Get NIP-07 extension info\n */\nexport function getNip07Info(): {\n available: boolean;\n name?: string;\n version?: string;\n} {\n if (!isNip07Available()) {\n return { available: false };\n }\n\n const nostr = (window as unknown as { nostr?: Nip07Window['nostr'] }).nostr!;\n\n return {\n available: true,\n name: nostr._metadata?.name,\n version: nostr._metadata?.version,\n };\n}\n","/**\n * Attestation Manager\n *\n * High-level API for creating, publishing, and discovering OrangeCheck attestations\n */\n\nimport type { AttestationEnvelope, IdentityBinding, NostrEvent, Scheme } from './types';\n\nimport { createAttestationEnvelope, parseIdentities } from './canonical';\nimport {\n createAttestationEvent,\n DEFAULT_RELAYS,\n parseAttestationFromEvent,\n publishToRelays,\n queryByAddress,\n queryByAttestationId,\n queryByIdentity,\n verifyAttestationEnvelope,\n} from './nostr';\nimport { isNip07Available, signNostrEvent } from './nostr-crypto';\nimport { createLogger } from './utils/logger';\n\nconst log = createLogger('ocp/attestation');\n\n/**\n * Options for creating an attestation\n */\nexport interface CreateAttestationOptions {\n /** Canonical message */\n message: string;\n /** BIP-322 or legacy signature */\n signature: string;\n /** Signature scheme used */\n scheme: Scheme;\n /** Bitcoin address */\n address: string;\n /** Optional identity bindings */\n identities?: IdentityBinding[];\n /** Optional extension key-value pairs */\n extensions?: Record<string, string | undefined>;\n}\n\n/**\n * Options for publishing an attestation to Nostr\n */\nexport interface PublishAttestationOptions {\n /** Attestation envelope to publish */\n envelope: AttestationEnvelope;\n /** Nostr public key (npub or hex) for signing the event */\n npub: string;\n /** Optional relay URLs (defaults to DEFAULT_RELAYS) */\n relays?: string[];\n}\n\n/**\n * Options for discovering attestations\n */\nexport interface DiscoverAttestationOptions {\n /** Search by attestation ID */\n attestationId?: string;\n /** Search by Bitcoin address */\n address?: string;\n /** Search by identity binding */\n identity?: { protocol: string; identifier: string };\n /** Optional relay URLs to query */\n relays?: string[];\n}\n\n/**\n * Result of publishing an attestation\n */\nexport interface PublishResult {\n /** Relays that successfully received the event */\n success: string[];\n /** Relays that failed to receive the event */\n failed: string[];\n}\n\n/**\n * Custom error for attestation operations\n */\nexport class AttestationError extends Error {\n constructor(\n message: string,\n public readonly code?: string\n ) {\n super(message);\n this.name = 'AttestationError';\n }\n}\n\n/**\n * Create an attestation envelope from a signed message\n *\n * @param options - Attestation creation options\n * @returns Attestation envelope ready for publishing\n * @throws {AttestationError} If envelope integrity check fails\n *\n * @example\n * ```typescript\n * const envelope = await createAttestation({\n * message: canonicalMessage,\n * signature: 'AkcwRAIg...',\n * scheme: 'bip322',\n * address: 'bc1q...',\n * identities: [{ protocol: 'nostr', identifier: 'npub1...' }],\n * });\n * ```\n */\nexport async function createAttestation(\n options: CreateAttestationOptions\n): Promise<AttestationEnvelope> {\n const { message, signature, scheme, address, identities = [], extensions = {} } = options;\n\n log.info({ address, identityCount: identities.length }, 'Creating attestation');\n\n const envelope = await createAttestationEnvelope(\n message,\n signature,\n scheme,\n address,\n identities,\n extensions\n );\n\n // Verify envelope integrity\n const isValid = await verifyAttestationEnvelope(envelope);\n if (!isValid) {\n throw new AttestationError(\n 'Attestation envelope integrity check failed',\n 'INTEGRITY_CHECK_FAILED'\n );\n }\n\n log.info({ attestationId: envelope.attestation_id }, 'Attestation created successfully');\n\n return envelope;\n}\n\n/**\n * Publish an attestation to Nostr relays\n *\n * Uses NIP-07 if available, falls back to ephemeral key for demo\n *\n * @param options - Publishing options\n * @returns Object with successful and failed relay URLs\n * @throws {AttestationError} If signing fails\n *\n * @example\n * ```typescript\n * const result = await publishAttestation({\n * envelope,\n * npub: 'npub1...',\n * relays: ['wss://relay.damus.io'],\n * });\n * console.log('Published to:', result.success);\n * ```\n */\nexport async function publishAttestation(\n options: PublishAttestationOptions\n): Promise<PublishResult> {\n const { envelope, npub, relays = DEFAULT_RELAYS } = options;\n\n log.info(\n { attestationId: envelope.attestation_id, relayCount: relays.length },\n 'Publishing attestation'\n );\n\n // Create unsigned event\n const unsignedEvent = createAttestationEvent(envelope, npub);\n\n // Nostr publishing requires a valid schnorr signature. Only NIP-07\n // (window.nostr) is supported. If no signer is present we refuse to\n // publish rather than emit an invalid-sig event that every relay will\n // silently reject — which would make the caller think it had succeeded.\n if (!isNip07Available()) {\n log.warn('NIP-07 unavailable — skipping Nostr publish');\n throw new Error(\n 'nostr_signer_unavailable: install a NIP-07 extension (e.g. nos2x, Alby) to publish'\n );\n }\n\n log.info('Using NIP-07 to sign event');\n const signedEvent = await signNostrEvent(unsignedEvent);\n if (!signedEvent) {\n throw new Error('nostr_signing_failed: NIP-07 extension rejected or errored');\n }\n\n const result = await publishToRelays(signedEvent, relays);\n\n log.info(\n {\n attestationId: envelope.attestation_id,\n successCount: result.success.length,\n failedCount: result.failed.length,\n },\n 'Publishing complete'\n );\n\n return result;\n}\n\n/**\n * Discover attestations from Nostr relays\n */\nexport async function discoverAttestations(\n options: DiscoverAttestationOptions\n): Promise<AttestationEnvelope[]> {\n const { attestationId, address, identity, relays = DEFAULT_RELAYS } = options;\n\n let events: NostrEvent[] = [];\n\n if (attestationId) {\n log.info({ attestationId }, 'Discovering by attestation ID');\n events = await queryByAttestationId(attestationId, relays);\n } else if (address) {\n log.info({ address }, 'Discovering by Bitcoin address');\n events = await queryByAddress(address, relays);\n } else if (identity) {\n log.info({ identity }, 'Discovering by identity');\n events = await queryByIdentity(identity.protocol, identity.identifier, relays);\n } else {\n throw new Error('Must provide attestationId, address, or identity');\n }\n\n // Parse and verify envelopes\n const envelopes: AttestationEnvelope[] = [];\n for (const event of events) {\n const envelope = parseAttestationFromEvent(event);\n if (envelope) {\n // Verify envelope integrity\n const isValid = await verifyAttestationEnvelope(envelope);\n if (isValid) {\n envelopes.push(envelope);\n } else {\n log.warn(\n { attestationId: envelope.attestation_id },\n 'Skipping invalid attestation envelope'\n );\n }\n }\n }\n\n log.info({ eventCount: events.length, validCount: envelopes.length }, 'Discovery complete');\n\n return envelopes;\n}\n\n/**\n * Verify an attestation by fetching it from Nostr and checking integrity\n */\nexport async function verifyAttestationById(\n attestationId: string,\n relays: string[] = DEFAULT_RELAYS\n): Promise<AttestationEnvelope | null> {\n log.info({ attestationId }, 'Verifying attestation by ID');\n\n const envelopes = await discoverAttestations({ attestationId, relays });\n\n if (envelopes.length === 0) {\n log.warn({ attestationId }, 'Attestation not found on any relay');\n return null;\n }\n\n if (envelopes.length > 1) {\n log.warn(\n { attestationId, count: envelopes.length },\n 'Multiple attestations found with same ID - using first'\n );\n }\n\n return envelopes[0] || null;\n}\n\n/**\n * Get all attestations for a Bitcoin address\n */\nexport async function getAttestationsForAddress(\n address: string,\n relays: string[] = DEFAULT_RELAYS\n): Promise<AttestationEnvelope[]> {\n log.info({ address }, 'Getting all attestations for address');\n return discoverAttestations({ address, relays });\n}\n\n/**\n * Get all attestations for an identity\n */\nexport async function getAttestationsForIdentity(\n protocol: string,\n identifier: string,\n relays: string[] = DEFAULT_RELAYS\n): Promise<AttestationEnvelope[]> {\n log.info({ protocol, identifier }, 'Getting all attestations for identity');\n return discoverAttestations({ identity: { protocol, identifier }, relays });\n}\n\n/**\n * Parse identities from a comma-separated string\n * Helper for UI integration\n */\nexport function parseIdentitiesFromString(identitiesStr: string): IdentityBinding[] {\n return parseIdentities(identitiesStr);\n}\n\n/**\n * Format identities for display\n */\nexport function formatIdentitiesForDisplay(identities: IdentityBinding[]): string {\n return identities.map((id) => `${id.protocol}:${id.identifier}`).join(', ');\n}\n\n/**\n * Get verification URL for an attestation\n */\nexport function getVerificationUrl(attestationId: string, baseUrl = 'https://ochk.io'): string {\n return `${baseUrl}/verify?id=${attestationId}`;\n}\n\n/**\n * Extract attestation ID from verification URL\n */\nexport function extractAttestationIdFromUrl(url: string): string | null {\n try {\n const urlObj = new URL(url);\n return urlObj.searchParams.get('id');\n } catch {\n return null;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/canonical.ts","../src/utils/logger.ts","../src/nostr.ts","../src/nostr-crypto.ts","../src/attestation.ts"],"names":["log"],"mappings":"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","file":"attestation.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 * 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 * @returns Canonical message string with LF line endings\n */\nexport function buildCanonicalMessage(\n { address, identities }: CanonicalInput,\n extensions: Extensions = {}\n): string {\n const nonce = randomNonce16BHexLower();\n const issuedAt = new Date().toISOString();\n\n // Format identities\n const identitiesStr = formatIdentities(identities);\n\n // Core (strict wording + order per SPEC.md §2)\n const core = [\n 'orangecheck',\n `identities: ${identitiesStr}`,\n `address: ${address}`,\n 'purpose: portable reputation attestation (non-custodial)',\n `nonce: ${nonce}`,\n `issued_at: ${issuedAt}`,\n 'ack: I attest control of this address and bind it to my identities.',\n ];\n\n // Normalize and sort extensions\n const normExt: Record<string, string> = {};\n for (const [k, v] of Object.entries(extensions)) {\n if (v == null || v === '') {\n continue;\n }\n const key = k.toLowerCase();\n normExt[key] = String(v);\n }\n\n const extLines = Object.keys(normExt)\n .sort()\n .map((k) => `${k}: ${normExt[k]!}`);\n\n const all = [...core, ...extLines];\n\n // Join with LF and ensure exactly one trailing LF\n return all.join('\\n') + '\\n';\n}\n\n/**\n * Generate attestation ID from canonical message\n *\n * attestation_id = SHA-256(canonical_message)\n *\n * @param message - Canonical message string\n * @returns Lowercase hex SHA-256 hash\n */\nexport async function generateAttestationId(message: string): Promise<string> {\n // Convert message to UTF-8 bytes\n const encoder = new TextEncoder();\n const data = encoder.encode(message);\n\n // Compute SHA-256 hash\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n\n // Convert to lowercase hex\n return toLowerHex(new Uint8Array(hashBuffer));\n}\n\n/**\n * Create an attestation envelope for publishing\n *\n * This wraps the canonical message with metadata for Nostr publishing\n *\n * @param message - Canonical message\n * @param signature - BIP-322 or legacy signature\n * @param scheme - Signature scheme used\n * @param address - Bitcoin address\n * @param identities - Identity bindings\n * @param extensions - Optional extensions\n * @returns Attestation envelope ready for publishing\n */\nexport async function createAttestationEnvelope(\n message: string,\n signature: string,\n scheme: Scheme,\n address: string,\n identities: IdentityBinding[],\n extensions: Extensions = {}\n): Promise<AttestationEnvelope> {\n const attestation_id = await generateAttestationId(message);\n\n // Extract issued_at from message\n const issuedAtMatch = message.match(/issued_at: ([^\\n]+)/);\n const issued_at = issuedAtMatch?.[1] ?? new Date().toISOString();\n\n // Extract expires from extensions if present\n const expires_at = extensions.expires;\n\n // Extract relay hints from extensions if present\n const relay_hints = extensions.relay_hints?.split(',').map((r) => r.trim());\n\n // Extract publish targets from extensions if present\n const publish_targets = extensions.publish?.split(',').map((t) => t.trim());\n\n // Base64url encode the message\n const encoder = new TextEncoder();\n const messageBytes = encoder.encode(message);\n const base64 = btoa(String.fromCharCode(...messageBytes));\n const message_b64url = base64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');\n\n // Construct verification URL\n const verification_url = `https://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","/**\n * Simple logger for SDK\n * Can be disabled in production builds\n */\n\n/**\n * Log level type\n */\nexport type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';\n\n/**\n * Logger context type\n */\nexport type LogContext = Record<string, unknown>;\n\n/**\n * Logger interface\n */\nexport interface Logger {\n debug: (contextOrMessage: string | LogContext, message?: string) => void;\n info: (contextOrMessage: string | LogContext, message?: string) => void;\n warn: (contextOrMessage: string | LogContext, message?: string) => void;\n error: (contextOrMessage: string | LogContext, message?: string) => void;\n}\n\nlet logLevel: LogLevel = 'none';\n\n/**\n * Set the global log level\n * @param level - Log level to set\n */\nexport function setLogLevel(level: LogLevel): void {\n logLevel = level;\n}\n\n/**\n * Get the current log level\n * @returns Current log level\n */\nexport function getLogLevel(): LogLevel {\n return logLevel;\n}\n\n/**\n * Create a namespaced logger\n * @param namespace - Logger namespace\n * @returns Logger instance\n */\nexport function createLogger(namespace: string): Logger {\n const shouldLog = (level: Exclude<LogLevel, 'none'>): boolean => {\n if (logLevel === 'none') return false;\n\n const levels: Exclude<LogLevel, 'none'>[] = ['debug', 'info', 'warn', 'error'];\n const currentLevelIndex = levels.indexOf(logLevel as Exclude<LogLevel, 'none'>);\n const messageLevelIndex = levels.indexOf(level);\n\n return messageLevelIndex >= currentLevelIndex;\n };\n\n const formatMessage = (msg: string): string => `[${namespace}] ${msg}`;\n\n return {\n debug: (contextOrMessage: string | LogContext, message?: string): void => {\n if (shouldLog('debug')) {\n if (typeof contextOrMessage === 'string') {\n console.debug(formatMessage(contextOrMessage));\n } else {\n console.debug(formatMessage(message ?? 'Debug'), contextOrMessage);\n }\n }\n },\n info: (contextOrMessage: string | LogContext, message?: string): void => {\n if (shouldLog('info')) {\n if (typeof contextOrMessage === 'string') {\n console.info(formatMessage(contextOrMessage));\n } else {\n console.info(formatMessage(message ?? 'Info'), contextOrMessage);\n }\n }\n },\n warn: (contextOrMessage: string | LogContext, message?: string): void => {\n if (shouldLog('warn')) {\n if (typeof contextOrMessage === 'string') {\n console.warn(formatMessage(contextOrMessage));\n } else {\n console.warn(formatMessage(message ?? 'Warning'), contextOrMessage);\n }\n }\n },\n error: (contextOrMessage: string | LogContext, message?: string): void => {\n if (shouldLog('error')) {\n if (typeof contextOrMessage === 'string') {\n console.error(formatMessage(contextOrMessage));\n } else {\n console.error(formatMessage(message ?? 'Error'), contextOrMessage);\n }\n }\n },\n };\n}\n\n/**\n * Log verification error\n * @param protocol - Identity protocol\n * @param identifier - Identity identifier\n * @param error - Error object\n * @param context - Additional context\n */\nexport function logVerificationError(\n protocol: string,\n identifier: string,\n error: Error,\n context?: LogContext\n): void {\n const logger = createLogger('identity-verification');\n logger.error(\n {\n protocol,\n identifier,\n error: error.message,\n stack: error.stack,\n ...context,\n },\n 'Verification failed'\n );\n}\n","/**\n * Nostr Integration for OrangeCheck Protocol\n *\n * Implements NIP-78 (Application-specific data) for attestation publishing\n */\n\nimport type { AttestationEnvelope, IdentityBinding, NostrEvent } from './types';\n\nimport { createLogger } from './utils/logger';\n\nconst log = createLogger('ocp/nostr');\n\n/**\n * NIP-78 event kind for OrangeCheck attestations\n */\nexport const ATTESTATION_EVENT_KIND = 30078;\n\n/**\n * Default Nostr relays for OrangeCheck attestation publishing\n *\n * These are well-known, reliable relays with good uptime\n */\nexport const DEFAULT_RELAYS: string[] = [\n 'wss://relay.damus.io',\n 'wss://relay.nostr.band',\n 'wss://nos.lol',\n 'wss://relay.snort.social',\n];\n\n/**\n * Create a NIP-78 event for an OrangeCheck attestation\n *\n * Kind: 30078 (Parameterized Replaceable Event)\n * d-tag: attestation_id (makes it replaceable by attestation ID)\n *\n * @param envelope - Attestation envelope\n * @param pubkey - Nostr public key (hex format)\n * @returns Unsigned Nostr event\n */\nexport function createAttestationEvent(\n envelope: AttestationEnvelope,\n pubkey: string\n): Omit<NostrEvent, 'id' | 'sig'> {\n const tags: string[][] = [\n ['d', envelope.attestation_id], // Parameterized replaceable event identifier\n ['address', envelope.address], // Bitcoin address\n ['scheme', envelope.scheme], // Signature scheme (bip322 or legacy)\n ['issued_at', envelope.issued_at], // ISO timestamp\n ];\n\n // Add identity tags\n for (const identity of envelope.identities) {\n tags.push(['i', `${identity.protocol}:${identity.identifier}`]);\n }\n\n // Add expiration if present\n if (envelope.expires_at) {\n tags.push(['expires', envelope.expires_at]);\n }\n\n // Add relay hints if present\n if (envelope.relay_hints?.length) {\n for (const relay of envelope.relay_hints) {\n tags.push(['relay', relay]);\n }\n }\n\n // Content is the full attestation envelope as JSON\n const content = JSON.stringify(envelope, null, 2);\n\n return {\n kind: ATTESTATION_EVENT_KIND,\n tags,\n content,\n created_at: Math.floor(Date.parse(envelope.issued_at) / 1000),\n pubkey,\n };\n}\n\n/**\n * Parse identity bindings from Nostr event tags\n *\n * @param event - Nostr event\n * @returns Array of identity bindings\n */\nexport function parseIdentitiesFromEvent(event: NostrEvent): IdentityBinding[] {\n const identities: IdentityBinding[] = [];\n\n for (const tag of event.tags) {\n if (tag[0] === 'i' && tag[1]) {\n const colonIndex = tag[1].indexOf(':');\n if (colonIndex !== -1) {\n identities.push({\n protocol: tag[1].substring(0, colonIndex),\n identifier: tag[1].substring(colonIndex + 1),\n });\n }\n }\n }\n\n return identities;\n}\n\n/**\n * Extract attestation ID from Nostr event d-tag\n *\n * @param event - Nostr event\n * @returns Attestation ID or undefined if not found\n */\nexport function getAttestationIdFromEvent(event: NostrEvent): string | undefined {\n const dTag = event.tags.find((tag) => tag[0] === 'd');\n return dTag?.[1];\n}\n\n/**\n * Extract Bitcoin address from Nostr event tags\n *\n * @param event - Nostr event\n * @returns Bitcoin address or undefined if not found\n */\nexport function getAddressFromEvent(event: NostrEvent): string | undefined {\n const addressTag = event.tags.find((tag) => tag[0] === 'address');\n return addressTag?.[1];\n}\n\n/**\n * Publish a Nostr event to multiple relays\n * Returns array of relay URLs that successfully accepted the event\n */\nexport async function publishToRelays(\n event: NostrEvent,\n relays: string[] = DEFAULT_RELAYS\n): Promise<{ success: string[]; failed: string[] }> {\n const success: string[] = [];\n const failed: string[] = [];\n\n log.info({ eventId: event.id, relayCount: relays.length }, 'Publishing event to relays');\n\n const publishPromises = relays.map(async (relayUrl) => {\n let ws: WebSocket | null = null;\n\n try {\n // Connect to relay\n ws = new WebSocket(relayUrl);\n const wsRef = ws; // Capture for closures\n\n await new Promise<void>((resolve, reject) => {\n // One outer deadline covers connect + OK round-trip. The old\n // code cleared this on `onopen`, which meant a relay that\n // opened but never sent OK would hang the promise forever.\n const deadline = setTimeout(() => {\n wsRef.close();\n reject(new Error('Relay publish timeout'));\n }, 10_000);\n\n const cleanup = () => {\n clearTimeout(deadline);\n wsRef.close();\n };\n\n wsRef.onopen = () => {\n // Do NOT clear the deadline — we still need it in case\n // the relay never responds with OK.\n wsRef.send(JSON.stringify(['EVENT', event]));\n };\n\n wsRef.onmessage = (msg) => {\n try {\n const data = JSON.parse(msg.data);\n // Check for OK response\n if (data[0] === 'OK' && data[1] === event.id) {\n if (data[2] === true) {\n success.push(relayUrl);\n cleanup();\n resolve();\n } else {\n log.warn(\n { relay: relayUrl, reason: data[3] },\n 'Relay rejected event'\n );\n failed.push(relayUrl);\n cleanup();\n reject(new Error(data[3] || 'Relay rejected event'));\n }\n }\n } catch (err) {\n log.error(\n { relay: relayUrl, error: err },\n 'Failed to parse relay response'\n );\n failed.push(relayUrl);\n cleanup();\n reject(err);\n }\n };\n\n wsRef.onerror = (err) => {\n log.error({ relay: relayUrl, error: err }, 'WebSocket error');\n failed.push(relayUrl);\n cleanup();\n reject(err);\n };\n\n wsRef.onclose = () => {\n // Socket closed before we resolved/rejected — counts as\n // failure. clearTimeout here is safe (cleanup handles both).\n clearTimeout(deadline);\n };\n });\n } catch (err) {\n log.error({ relay: relayUrl, error: err }, 'Failed to publish to relay');\n if (!failed.includes(relayUrl)) {\n failed.push(relayUrl);\n }\n } finally {\n // Ensure WebSocket is closed\n if (ws && ws.readyState !== WebSocket.CLOSED) {\n ws.close();\n }\n }\n });\n\n await Promise.allSettled(publishPromises);\n\n log.info({ successCount: success.length, failedCount: failed.length }, 'Publishing complete');\n\n return { success, failed };\n}\n\n/**\n * Query relays for attestations by attestation ID\n */\nexport async function queryByAttestationId(\n attestationId: string,\n relays: string[] = DEFAULT_RELAYS\n): Promise<NostrEvent[]> {\n const events: NostrEvent[] = [];\n\n log.info({ attestationId, relayCount: relays.length }, 'Querying relays for attestation');\n\n const queryPromises = relays.map(async (relayUrl) => {\n let ws: WebSocket | null = null;\n\n try {\n ws = new WebSocket(relayUrl);\n const wsRef = ws; // Capture for closures\n\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n wsRef.close();\n reject(new Error('Query timeout'));\n }, 10000);\n\n const cleanup = () => {\n clearTimeout(timeout);\n wsRef.close();\n };\n\n wsRef.onopen = () => {\n // Send REQ message for kind 30078 with d-tag = attestation_id\n const subscriptionId = `ochk_${Date.now()}`;\n wsRef.send(\n JSON.stringify([\n 'REQ',\n subscriptionId,\n {\n kinds: [30078],\n '#d': [attestationId],\n limit: 1,\n },\n ])\n );\n };\n\n wsRef.onmessage = (msg) => {\n try {\n const data = JSON.parse(msg.data);\n if (data[0] === 'EVENT') {\n events.push(data[2]);\n } else if (data[0] === 'EOSE') {\n cleanup();\n resolve();\n }\n } catch (err) {\n log.error({ relay: relayUrl, error: err }, 'Failed to parse event');\n }\n };\n\n wsRef.onerror = (err) => {\n log.error({ relay: relayUrl, error: err }, 'WebSocket error during query');\n cleanup();\n reject(err);\n };\n\n wsRef.onclose = () => {\n clearTimeout(timeout);\n };\n });\n } catch (err) {\n log.error({ relay: relayUrl, error: err }, 'Failed to query relay');\n } finally {\n // Ensure WebSocket is closed\n if (ws && ws.readyState !== WebSocket.CLOSED) {\n ws.close();\n }\n }\n });\n\n await Promise.allSettled(queryPromises);\n\n log.info({ eventCount: events.length }, 'Query complete');\n\n return events;\n}\n\n/**\n * Query relays for attestations by Bitcoin address\n */\nexport async function queryByAddress(\n address: string,\n relays: string[] = DEFAULT_RELAYS\n): Promise<NostrEvent[]> {\n const events: NostrEvent[] = [];\n\n log.info({ address, relayCount: relays.length }, 'Querying relays for address');\n\n const queryPromises = relays.map(async (relayUrl) => {\n try {\n const ws = new WebSocket(relayUrl);\n\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n ws.close();\n reject(new Error('Query timeout'));\n }, 10000);\n\n ws.onopen = () => {\n const subscriptionId = `ochk_addr_${Date.now()}`;\n ws.send(\n JSON.stringify([\n 'REQ',\n subscriptionId,\n {\n kinds: [30078],\n '#address': [address],\n limit: 10,\n },\n ])\n );\n };\n\n ws.onmessage = (msg) => {\n try {\n const data = JSON.parse(msg.data);\n if (data[0] === 'EVENT') {\n events.push(data[2]);\n } else if (data[0] === 'EOSE') {\n clearTimeout(timeout);\n ws.close();\n resolve();\n }\n } catch (err) {\n log.error({ relay: relayUrl, error: err }, 'Failed to parse event');\n }\n };\n\n ws.onerror = (err) => {\n clearTimeout(timeout);\n log.error({ relay: relayUrl, error: err }, 'WebSocket error during query');\n reject(err);\n };\n });\n } catch (err) {\n log.error({ relay: relayUrl, error: err }, 'Failed to query relay');\n }\n });\n\n await Promise.allSettled(queryPromises);\n\n log.info({ eventCount: events.length }, 'Query complete');\n\n return events;\n}\n\n/**\n * Query relays for attestations by identity (any protocol)\n */\nexport async function queryByIdentity(\n protocol: string,\n identifier: string,\n relays: string[] = DEFAULT_RELAYS\n): Promise<NostrEvent[]> {\n const events: NostrEvent[] = [];\n const identityTag = `${protocol}:${identifier}`;\n\n log.info({ protocol, identifier, relayCount: relays.length }, 'Querying relays for identity');\n\n const queryPromises = relays.map(async (relayUrl) => {\n try {\n const ws = new WebSocket(relayUrl);\n\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n ws.close();\n reject(new Error('Query timeout'));\n }, 10000);\n\n ws.onopen = () => {\n const subscriptionId = `ochk_identity_${Date.now()}`;\n ws.send(\n JSON.stringify([\n 'REQ',\n subscriptionId,\n {\n kinds: [30078],\n '#i': [identityTag],\n limit: 10,\n },\n ])\n );\n };\n\n ws.onmessage = (msg) => {\n try {\n const data = JSON.parse(msg.data);\n if (data[0] === 'EVENT') {\n events.push(data[2]);\n } else if (data[0] === 'EOSE') {\n clearTimeout(timeout);\n ws.close();\n resolve();\n }\n } catch (err) {\n log.error({ relay: relayUrl, error: err }, 'Failed to parse event');\n }\n };\n\n ws.onerror = (err) => {\n clearTimeout(timeout);\n log.error({ relay: relayUrl, error: err }, 'WebSocket error during query');\n reject(err);\n };\n });\n } catch (err) {\n log.error({ relay: relayUrl, error: err }, 'Failed to query relay');\n }\n });\n\n await Promise.allSettled(queryPromises);\n\n log.info({ eventCount: events.length }, 'Query complete');\n\n return events;\n}\n\n/**\n * Verify attestation envelope integrity\n * Checks that the attestation ID matches the message hash\n */\nexport async function verifyAttestationEnvelope(envelope: AttestationEnvelope): Promise<boolean> {\n try {\n // Re-compute attestation ID from message\n const encoder = new TextEncoder();\n const data = encoder.encode(envelope.message);\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n const hashArray = Array.from(new Uint8Array(hashBuffer));\n const computedId = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');\n\n return computedId === envelope.attestation_id;\n } catch (err) {\n log.error({ error: err }, 'Failed to verify attestation envelope');\n return false;\n }\n}\n\n/**\n * Parse attestation envelope from Nostr event content\n */\nexport function parseAttestationFromEvent(event: NostrEvent): AttestationEnvelope | null {\n try {\n const envelope = JSON.parse(event.content) as AttestationEnvelope;\n return envelope;\n } catch (err) {\n log.error({ eventId: event.id, error: err }, 'Failed to parse attestation from event');\n return null;\n }\n}\n","/**\n * Nostr Cryptographic Functions\n * Implements NIP-01 event ID computation and signature verification\n */\n\nimport type { NostrEvent } from './types';\n\nimport { createLogger } from './utils/logger';\n\ntype Nip07Window = {\n nostr?: {\n getPublicKey: () => Promise<string>;\n signEvent: (event: Omit<NostrEvent, 'id' | 'sig'>) => Promise<NostrEvent>;\n _metadata?: { name?: string; version?: string };\n };\n};\n\nconst log = createLogger('ocp/nostr-crypto');\n\n/**\n * Compute Nostr event ID according to NIP-01\n * Event ID is the SHA-256 hash of the serialized event\n * Serialization: [0, pubkey, created_at, kind, tags, content]\n */\nexport async function computeNostrEventId(event: Omit<NostrEvent, 'id' | 'sig'>): Promise<string> {\n // Serialize event according to NIP-01\n const serialized = JSON.stringify([\n 0, // Reserved for future use\n event.pubkey,\n event.created_at,\n event.kind,\n event.tags,\n event.content,\n ]);\n\n log.debug({ serialized }, 'Computing event ID');\n\n // Compute SHA-256 hash\n const encoder = new TextEncoder();\n const data = encoder.encode(serialized);\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n\n // Convert to lowercase hex\n const hashArray = Array.from(new Uint8Array(hashBuffer));\n const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');\n\n log.debug({ eventId: hashHex }, 'Computed event ID');\n\n return hashHex;\n}\n\n/**\n * Verify Nostr event ID\n * Recomputes the ID and checks if it matches\n */\nexport async function verifyNostrEventId(event: NostrEvent): Promise<boolean> {\n const computedId = await computeNostrEventId(event);\n return computedId === event.id;\n}\n\n/**\n * Check if NIP-07 extension is available\n * NIP-07 provides window.nostr for signing events\n */\nexport function isNip07Available(): boolean {\n if (typeof window === 'undefined') {\n return false;\n }\n\n return !!(window as unknown as { nostr?: Nip07Window['nostr'] }).nostr!;\n}\n\n/**\n * Get Nostr public key from NIP-07 extension\n * Returns null if extension not available or user denies\n */\nexport async function getNostrPublicKey(): Promise<string | null> {\n if (!isNip07Available()) {\n log.warn('NIP-07 extension not available');\n return null;\n }\n\n try {\n const nostr = (window as unknown as { nostr?: Nip07Window['nostr'] }).nostr!;\n const pubkey = await nostr.getPublicKey();\n log.info({ pubkey }, 'Got Nostr public key from NIP-07');\n return pubkey;\n } catch (err) {\n log.error({ error: err }, 'Failed to get Nostr public key');\n return null;\n }\n}\n\n/**\n * Sign Nostr event using NIP-07 extension\n * Returns signed event with id and sig fields\n */\nexport async function signNostrEvent(\n event: Omit<NostrEvent, 'id' | 'sig'>\n): Promise<NostrEvent | null> {\n if (!isNip07Available()) {\n log.warn('NIP-07 extension not available');\n return null;\n }\n\n try {\n const nostr = (window as unknown as { nostr?: Nip07Window['nostr'] }).nostr!;\n\n // NIP-07 signEvent expects the event without id and sig\n const signedEvent = await nostr.signEvent(event);\n\n log.info({ eventId: signedEvent.id }, 'Signed Nostr event with NIP-07');\n\n return signedEvent as NostrEvent;\n } catch (err) {\n log.error({ error: err }, 'Failed to sign Nostr event');\n return null;\n }\n}\n\n// Previously this file exported `generateEphemeralKeypair()` and\n// `signEventWithEphemeralKey()` for demo purposes. Both produced non-schnorr\n// values that Nostr relays would silently reject — creating the false\n// impression that publishing had succeeded. They have been removed. All Nostr\n// signing now flows through `signNostrEvent()` / `window.nostr` (NIP-07).\n\n/**\n * Validate Nostr event structure\n */\nexport function validateNostrEvent(event: unknown): event is NostrEvent {\n if (typeof event !== 'object' || event === null) return false;\n const e = event as Record<string, unknown>;\n\n if (typeof e.id !== 'string' || (e.id as string).length !== 64) return false;\n if (typeof e.pubkey !== 'string' || (e.pubkey as string).length !== 64) return false;\n if (typeof e.created_at !== 'number') return false;\n if (typeof e.kind !== 'number') return false;\n if (!Array.isArray(e.tags)) return false;\n if (typeof e.content !== 'string') return false;\n if (typeof e.sig !== 'string' || (e.sig as string).length !== 128) return false;\n\n return true;\n}\n\n/**\n * Get NIP-07 extension info\n */\nexport function getNip07Info(): {\n available: boolean;\n name?: string;\n version?: string;\n} {\n if (!isNip07Available()) {\n return { available: false };\n }\n\n const nostr = (window as unknown as { nostr?: Nip07Window['nostr'] }).nostr!;\n\n return {\n available: true,\n name: nostr._metadata?.name,\n version: nostr._metadata?.version,\n };\n}\n","/**\n * Attestation Manager\n *\n * High-level API for creating, publishing, and discovering OrangeCheck attestations\n */\n\nimport type { AttestationEnvelope, IdentityBinding, NostrEvent, Scheme } from './types';\n\nimport { createAttestationEnvelope } from './canonical';\nimport {\n createAttestationEvent,\n DEFAULT_RELAYS,\n parseAttestationFromEvent,\n publishToRelays,\n queryByAddress,\n queryByAttestationId,\n queryByIdentity,\n verifyAttestationEnvelope,\n} from './nostr';\nimport { isNip07Available, signNostrEvent } from './nostr-crypto';\nimport { createLogger } from './utils/logger';\n\nconst log = createLogger('ocp/attestation');\n\n/**\n * Options for creating an attestation\n */\nexport interface CreateAttestationOptions {\n /** Canonical message */\n message: string;\n /** BIP-322 or legacy signature */\n signature: string;\n /** Signature scheme used */\n scheme: Scheme;\n /** Bitcoin address */\n address: string;\n /** Optional identity bindings */\n identities?: IdentityBinding[];\n /** Optional extension key-value pairs */\n extensions?: Record<string, string | undefined>;\n}\n\n/**\n * Options for publishing an attestation to Nostr\n */\nexport interface PublishAttestationOptions {\n /** Attestation envelope to publish */\n envelope: AttestationEnvelope;\n /** Nostr public key (npub or hex) for signing the event */\n npub: string;\n /** Optional relay URLs (defaults to DEFAULT_RELAYS) */\n relays?: string[];\n}\n\n/**\n * Options for discovering attestations\n */\nexport interface DiscoverAttestationOptions {\n /** Search by attestation ID */\n attestationId?: string;\n /** Search by Bitcoin address */\n address?: string;\n /** Search by identity binding */\n identity?: { protocol: string; identifier: string };\n /** Optional relay URLs to query */\n relays?: string[];\n}\n\n/**\n * Result of publishing an attestation\n */\nexport interface PublishResult {\n /** Relays that successfully received the event */\n success: string[];\n /** Relays that failed to receive the event */\n failed: string[];\n}\n\n/**\n * Custom error for attestation operations\n */\nexport class AttestationError extends Error {\n constructor(\n message: string,\n public readonly code?: string\n ) {\n super(message);\n this.name = 'AttestationError';\n }\n}\n\n/**\n * Create an attestation envelope from a signed message\n *\n * @param options - Attestation creation options\n * @returns Attestation envelope ready for publishing\n * @throws {AttestationError} If envelope integrity check fails\n *\n * @example\n * ```typescript\n * const envelope = await createAttestation({\n * message: canonicalMessage,\n * signature: 'AkcwRAIg...',\n * scheme: 'bip322',\n * address: 'bc1q...',\n * identities: [{ protocol: 'nostr', identifier: 'npub1...' }],\n * });\n * ```\n */\nexport async function createAttestation(\n options: CreateAttestationOptions\n): Promise<AttestationEnvelope> {\n const { message, signature, scheme, address, identities = [], extensions = {} } = options;\n\n log.info({ address, identityCount: identities.length }, 'Creating attestation');\n\n const envelope = await createAttestationEnvelope(\n message,\n signature,\n scheme,\n address,\n identities,\n extensions\n );\n\n // Verify envelope integrity\n const isValid = await verifyAttestationEnvelope(envelope);\n if (!isValid) {\n throw new AttestationError(\n 'Attestation envelope integrity check failed',\n 'INTEGRITY_CHECK_FAILED'\n );\n }\n\n log.info({ attestationId: envelope.attestation_id }, 'Attestation created successfully');\n\n return envelope;\n}\n\n/**\n * Publish an attestation to Nostr relays\n *\n * Uses NIP-07 if available, falls back to ephemeral key for demo\n *\n * @param options - Publishing options\n * @returns Object with successful and failed relay URLs\n * @throws {AttestationError} If signing fails\n *\n * @example\n * ```typescript\n * const result = await publishAttestation({\n * envelope,\n * npub: 'npub1...',\n * relays: ['wss://relay.damus.io'],\n * });\n * console.log('Published to:', result.success);\n * ```\n */\nexport async function publishAttestation(\n options: PublishAttestationOptions\n): Promise<PublishResult> {\n const { envelope, npub, relays = DEFAULT_RELAYS } = options;\n\n log.info(\n { attestationId: envelope.attestation_id, relayCount: relays.length },\n 'Publishing attestation'\n );\n\n // Create unsigned event\n const unsignedEvent = createAttestationEvent(envelope, npub);\n\n // Nostr publishing requires a valid schnorr signature. Only NIP-07\n // (window.nostr) is supported. If no signer is present we refuse to\n // publish rather than emit an invalid-sig event that every relay will\n // silently reject — which would make the caller think it had succeeded.\n if (!isNip07Available()) {\n log.warn('NIP-07 unavailable — skipping Nostr publish');\n throw new Error(\n 'nostr_signer_unavailable: install a NIP-07 extension (e.g. nos2x, Alby) to publish'\n );\n }\n\n log.info('Using NIP-07 to sign event');\n const signedEvent = await signNostrEvent(unsignedEvent);\n if (!signedEvent) {\n throw new Error('nostr_signing_failed: NIP-07 extension rejected or errored');\n }\n\n const result = await publishToRelays(signedEvent, relays);\n\n log.info(\n {\n attestationId: envelope.attestation_id,\n successCount: result.success.length,\n failedCount: result.failed.length,\n },\n 'Publishing complete'\n );\n\n return result;\n}\n\n/**\n * Discover attestations from Nostr relays\n */\nexport async function discoverAttestations(\n options: DiscoverAttestationOptions\n): Promise<AttestationEnvelope[]> {\n const { attestationId, address, identity, relays = DEFAULT_RELAYS } = options;\n\n let events: NostrEvent[] = [];\n\n if (attestationId) {\n log.info({ attestationId }, 'Discovering by attestation ID');\n events = await queryByAttestationId(attestationId, relays);\n } else if (address) {\n log.info({ address }, 'Discovering by Bitcoin address');\n events = await queryByAddress(address, relays);\n } else if (identity) {\n log.info({ identity }, 'Discovering by identity');\n events = await queryByIdentity(identity.protocol, identity.identifier, relays);\n } else {\n throw new Error('Must provide attestationId, address, or identity');\n }\n\n // Parse and verify envelopes\n const envelopes: AttestationEnvelope[] = [];\n for (const event of events) {\n const envelope = parseAttestationFromEvent(event);\n if (envelope) {\n // Verify envelope integrity\n const isValid = await verifyAttestationEnvelope(envelope);\n if (isValid) {\n envelopes.push(envelope);\n } else {\n log.warn(\n { attestationId: envelope.attestation_id },\n 'Skipping invalid attestation envelope'\n );\n }\n }\n }\n\n log.info({ eventCount: events.length, validCount: envelopes.length }, 'Discovery complete');\n\n return envelopes;\n}\n\n/**\n * Verify an attestation by fetching it from Nostr and checking integrity\n */\nexport async function verifyAttestationById(\n attestationId: string,\n relays: string[] = DEFAULT_RELAYS\n): Promise<AttestationEnvelope | null> {\n log.info({ attestationId }, 'Verifying attestation by ID');\n\n const envelopes = await discoverAttestations({ attestationId, relays });\n\n if (envelopes.length === 0) {\n log.warn({ attestationId }, 'Attestation not found on any relay');\n return null;\n }\n\n if (envelopes.length > 1) {\n log.warn(\n { attestationId, count: envelopes.length },\n 'Multiple attestations found with same ID - using first'\n );\n }\n\n return envelopes[0] || null;\n}\n\n/**\n * Get all attestations for a Bitcoin address\n */\nexport async function getAttestationsForAddress(\n address: string,\n relays: string[] = DEFAULT_RELAYS\n): Promise<AttestationEnvelope[]> {\n log.info({ address }, 'Getting all attestations for address');\n return discoverAttestations({ address, relays });\n}\n\n/**\n * Get all attestations for an identity\n */\nexport async function getAttestationsForIdentity(\n protocol: string,\n identifier: string,\n relays: string[] = DEFAULT_RELAYS\n): Promise<AttestationEnvelope[]> {\n log.info({ protocol, identifier }, 'Getting all attestations for identity');\n return discoverAttestations({ identity: { protocol, identifier }, relays });\n}\n\n/**\n * Format identities for display as a comma-separated `protocol:identifier` list.\n * Same wire format the canonical message uses.\n */\nexport function formatIdentitiesForDisplay(identities: IdentityBinding[]): string {\n return identities.map((id) => `${id.protocol}:${id.identifier}`).join(', ');\n}\n\n/**\n * Build the canonical shareable URL for an attestation. Matches the live\n * `/attest/[id]` route on ochk.io. Pass a different `baseUrl` for forks /\n * self-hosted verifiers.\n */\nexport function getVerificationUrl(attestationId: string, baseUrl = 'https://ochk.io'): string {\n return `${baseUrl}/attest/${attestationId}`;\n}\n\n/**\n * Reverse of `getVerificationUrl` — pull an attestation id back out of a\n * `/attest/<id>` or legacy `/verify?id=<id>` URL. Returns `null` if the URL\n * shape doesn't match.\n */\nexport function extractAttestationIdFromUrl(url: string): string | null {\n try {\n const u = new URL(url);\n // New canonical form: /attest/<id>\n const m = u.pathname.match(/^\\/attest\\/([0-9a-f]{64})\\/?$/i);\n if (m) return m[1]!.toLowerCase();\n // Legacy form: /verify?id=<id>\n const q = u.searchParams.get('id');\n if (q && /^[0-9a-f]{64}$/i.test(q)) return q.toLowerCase();\n return null;\n } catch {\n return null;\n }\n}\n"]}
|
package/dist/attestation.mjs
CHANGED
|
@@ -2,21 +2,6 @@
|
|
|
2
2
|
function toLowerHex(bytes) {
|
|
3
3
|
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
4
4
|
}
|
|
5
|
-
function parseIdentities(identitiesStr) {
|
|
6
|
-
if (!identitiesStr || identitiesStr.trim() === "") {
|
|
7
|
-
return [];
|
|
8
|
-
}
|
|
9
|
-
return identitiesStr.split(",").map((binding) => {
|
|
10
|
-
const colonIndex = binding.indexOf(":");
|
|
11
|
-
if (colonIndex === -1) {
|
|
12
|
-
throw new Error(`Invalid identity binding format: ${binding}`);
|
|
13
|
-
}
|
|
14
|
-
return {
|
|
15
|
-
protocol: binding.substring(0, colonIndex).trim(),
|
|
16
|
-
identifier: binding.substring(colonIndex + 1).trim()
|
|
17
|
-
};
|
|
18
|
-
}).filter((binding) => binding.protocol && binding.identifier);
|
|
19
|
-
}
|
|
20
5
|
async function generateAttestationId(message) {
|
|
21
6
|
const encoder = new TextEncoder();
|
|
22
7
|
const data = encoder.encode(message);
|
|
@@ -145,16 +130,15 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
|
|
|
145
130
|
ws = new WebSocket(relayUrl);
|
|
146
131
|
const wsRef = ws;
|
|
147
132
|
await new Promise((resolve, reject) => {
|
|
148
|
-
const
|
|
133
|
+
const deadline = setTimeout(() => {
|
|
149
134
|
wsRef.close();
|
|
150
|
-
reject(new Error("
|
|
151
|
-
},
|
|
135
|
+
reject(new Error("Relay publish timeout"));
|
|
136
|
+
}, 1e4);
|
|
152
137
|
const cleanup = () => {
|
|
153
|
-
clearTimeout(
|
|
138
|
+
clearTimeout(deadline);
|
|
154
139
|
wsRef.close();
|
|
155
140
|
};
|
|
156
141
|
wsRef.onopen = () => {
|
|
157
|
-
clearTimeout(timeout);
|
|
158
142
|
wsRef.send(JSON.stringify(["EVENT", event]));
|
|
159
143
|
};
|
|
160
144
|
wsRef.onmessage = (msg) => {
|
|
@@ -192,7 +176,7 @@ async function publishToRelays(event, relays = DEFAULT_RELAYS) {
|
|
|
192
176
|
reject(err);
|
|
193
177
|
};
|
|
194
178
|
wsRef.onclose = () => {
|
|
195
|
-
clearTimeout(
|
|
179
|
+
clearTimeout(deadline);
|
|
196
180
|
};
|
|
197
181
|
});
|
|
198
182
|
} catch (err) {
|
|
@@ -544,24 +528,25 @@ async function getAttestationsForIdentity(protocol, identifier, relays = DEFAULT
|
|
|
544
528
|
log3.info({ protocol, identifier }, "Getting all attestations for identity");
|
|
545
529
|
return discoverAttestations({ identity: { protocol, identifier }, relays });
|
|
546
530
|
}
|
|
547
|
-
function parseIdentitiesFromString(identitiesStr) {
|
|
548
|
-
return parseIdentities(identitiesStr);
|
|
549
|
-
}
|
|
550
531
|
function formatIdentitiesForDisplay(identities) {
|
|
551
532
|
return identities.map((id) => `${id.protocol}:${id.identifier}`).join(", ");
|
|
552
533
|
}
|
|
553
534
|
function getVerificationUrl(attestationId, baseUrl = "https://ochk.io") {
|
|
554
|
-
return `${baseUrl}/
|
|
535
|
+
return `${baseUrl}/attest/${attestationId}`;
|
|
555
536
|
}
|
|
556
537
|
function extractAttestationIdFromUrl(url) {
|
|
557
538
|
try {
|
|
558
|
-
const
|
|
559
|
-
|
|
539
|
+
const u = new URL(url);
|
|
540
|
+
const m = u.pathname.match(/^\/attest\/([0-9a-f]{64})\/?$/i);
|
|
541
|
+
if (m) return m[1].toLowerCase();
|
|
542
|
+
const q = u.searchParams.get("id");
|
|
543
|
+
if (q && /^[0-9a-f]{64}$/i.test(q)) return q.toLowerCase();
|
|
544
|
+
return null;
|
|
560
545
|
} catch {
|
|
561
546
|
return null;
|
|
562
547
|
}
|
|
563
548
|
}
|
|
564
549
|
|
|
565
|
-
export { AttestationError, createAttestation, discoverAttestations, extractAttestationIdFromUrl, formatIdentitiesForDisplay, getAttestationsForAddress, getAttestationsForIdentity, getVerificationUrl,
|
|
550
|
+
export { AttestationError, createAttestation, discoverAttestations, extractAttestationIdFromUrl, formatIdentitiesForDisplay, getAttestationsForAddress, getAttestationsForIdentity, getVerificationUrl, publishAttestation, verifyAttestationById };
|
|
566
551
|
//# sourceMappingURL=attestation.mjs.map
|
|
567
552
|
//# sourceMappingURL=attestation.mjs.map
|