@orangecheck/lock-device 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,7 +25,8 @@ This package is WebCrypto-free: it works in Node and in the browser.
25
25
 
26
26
  - `generateDeviceKey()` — new `(device_sk, device_pk, device_id, created_at)`.
27
27
  - `buildBindingStatement({ address, device_pk, device_id, created_at, nostr_pk? })` — exact bytes per SPEC §3.2 (v3 when `nostr_pk` is given).
28
- - `authorizedDevices(records)` — the verified records whose Nostr pubkey may act for their address (SPEC §3.4).
28
+ - `liveDevices(records, { now?, maxSkewMs? })` — the verified bind records no revocation retires (SPEC §3.5). Seal to these, one per `device_id`. `pendingDevices` lists binds dated ahead of the clock; `revokedAtFor` dates a revocation.
29
+ - `authorizedDevices(records, { now? })` — the records whose Nostr pubkey is the device's inbox (SPEC §3.4): every v3 record, and before `V2_INBOX_SUNSET` (2027-01-21T00:00:00Z) a v2 record carried by one key only. For Nostr delivery, never for choosing recipients.
29
30
  - `buildRevocationStatement({ address, device_id, revoked_at })` — for explicit revocation.
30
31
  - `deriveNostrKey(deviceSk)` — deterministic `nostr_sk` from `device_sk` (HKDF).
31
32
  - `finalizeDeviceEvent({ ... bindingSigBase64, ... })` — returns a fully signed kind-30078 `NostrEvent`.
package/dist/index.d.mts CHANGED
@@ -1,4 +1,8 @@
1
- declare function authorizedDevices(records: readonly ParsedDeviceEvent[]): ParsedDeviceEvent[];
1
+ declare const V2_INBOX_SUNSET = "2027-01-21T00:00:00Z";
2
+ interface AuthorizeOptions {
3
+ now?: Date;
4
+ }
5
+ declare function authorizedDevices(records: readonly ParsedDeviceEvent[], opts?: AuthorizeOptions): ParsedDeviceEvent[];
2
6
 
3
7
  declare function signedTime(statement: string, field: 'created_at' | 'revoked_at'): number | null;
4
8
  declare const DEFAULT_BIND_SKEW_MS: number;
@@ -80,4 +84,4 @@ interface ParsedDeviceEvent {
80
84
  }
81
85
  declare function parseDeviceEvent(event: NostrEvent): ParsedDeviceEvent;
82
86
 
83
- export { BINDING_VERSION, BINDING_VERSION_V3, DEFAULT_BIND_SKEW_MS, DEVICE_KIND, type DeviceKeyPair, type LiveOptions, type NostrEvent, type ParsedDeviceEvent, authorizedDevices, buildBindingStatement, buildDeviceEvent, buildRevocationStatement, computeNostrEventId, deriveNostrKey, finalizeDeviceEvent, generateDeviceKey, liveDevices, parseDeviceEvent, pendingDevices, revokedAtFor, signNostrEvent, signedTime };
87
+ export { type AuthorizeOptions, BINDING_VERSION, BINDING_VERSION_V3, DEFAULT_BIND_SKEW_MS, DEVICE_KIND, type DeviceKeyPair, type LiveOptions, type NostrEvent, type ParsedDeviceEvent, V2_INBOX_SUNSET, authorizedDevices, buildBindingStatement, buildDeviceEvent, buildRevocationStatement, computeNostrEventId, deriveNostrKey, finalizeDeviceEvent, generateDeviceKey, liveDevices, parseDeviceEvent, pendingDevices, revokedAtFor, signNostrEvent, signedTime };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,8 @@
1
- declare function authorizedDevices(records: readonly ParsedDeviceEvent[]): ParsedDeviceEvent[];
1
+ declare const V2_INBOX_SUNSET = "2027-01-21T00:00:00Z";
2
+ interface AuthorizeOptions {
3
+ now?: Date;
4
+ }
5
+ declare function authorizedDevices(records: readonly ParsedDeviceEvent[], opts?: AuthorizeOptions): ParsedDeviceEvent[];
2
6
 
3
7
  declare function signedTime(statement: string, field: 'created_at' | 'revoked_at'): number | null;
4
8
  declare const DEFAULT_BIND_SKEW_MS: number;
@@ -80,4 +84,4 @@ interface ParsedDeviceEvent {
80
84
  }
81
85
  declare function parseDeviceEvent(event: NostrEvent): ParsedDeviceEvent;
82
86
 
83
- export { BINDING_VERSION, BINDING_VERSION_V3, DEFAULT_BIND_SKEW_MS, DEVICE_KIND, type DeviceKeyPair, type LiveOptions, type NostrEvent, type ParsedDeviceEvent, authorizedDevices, buildBindingStatement, buildDeviceEvent, buildRevocationStatement, computeNostrEventId, deriveNostrKey, finalizeDeviceEvent, generateDeviceKey, liveDevices, parseDeviceEvent, pendingDevices, revokedAtFor, signNostrEvent, signedTime };
87
+ export { type AuthorizeOptions, BINDING_VERSION, BINDING_VERSION_V3, DEFAULT_BIND_SKEW_MS, DEVICE_KIND, type DeviceKeyPair, type LiveOptions, type NostrEvent, type ParsedDeviceEvent, V2_INBOX_SUNSET, authorizedDevices, buildBindingStatement, buildDeviceEvent, buildRevocationStatement, computeNostrEventId, deriveNostrKey, finalizeDeviceEvent, generateDeviceKey, liveDevices, parseDeviceEvent, pendingDevices, revokedAtFor, signNostrEvent, signedTime };
package/dist/index.js CHANGED
@@ -6,7 +6,12 @@ var lockCrypto = require('@orangecheck/lock-crypto');
6
6
  // src/index.ts
7
7
 
8
8
  // src/authorize.ts
9
- function authorizedDevices(records) {
9
+ var V2_INBOX_SUNSET = "2027-01-21T00:00:00Z";
10
+ function authorizedDevices(records, opts = {}) {
11
+ for (const k of Object.keys(opts)) {
12
+ if (k !== "now") throw new TypeError(`authorizedDevices does not read option "${k}"`);
13
+ }
14
+ const v2Counts = (opts.now ?? /* @__PURE__ */ new Date()).getTime() < Date.parse(V2_INBOX_SUNSET);
10
15
  const v3Ids = /* @__PURE__ */ new Set();
11
16
  const v2Pubkeys = /* @__PURE__ */ new Map();
12
17
  for (const r of records) {
@@ -22,7 +27,7 @@ function authorizedDevices(records) {
22
27
  return records.filter((r) => {
23
28
  if (r.revoked) return false;
24
29
  if (r.bindingVersion === "v3") return true;
25
- if (v3Ids.has(r.device_id)) return false;
30
+ if (!v2Counts || v3Ids.has(r.device_id)) return false;
26
31
  return v2Pubkeys.get(r.device_id)?.size === 1;
27
32
  });
28
33
  }
@@ -273,6 +278,7 @@ exports.BINDING_VERSION = BINDING_VERSION;
273
278
  exports.BINDING_VERSION_V3 = BINDING_VERSION_V3;
274
279
  exports.DEFAULT_BIND_SKEW_MS = DEFAULT_BIND_SKEW_MS;
275
280
  exports.DEVICE_KIND = DEVICE_KIND;
281
+ exports.V2_INBOX_SUNSET = V2_INBOX_SUNSET;
276
282
  exports.authorizedDevices = authorizedDevices;
277
283
  exports.buildBindingStatement = buildBindingStatement;
278
284
  exports.buildDeviceEvent = buildDeviceEvent;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/authorize.ts","../src/live.ts","../src/index.ts"],"names":["generateX25519KeyPair","randomBytesN","hexEncode","hkdfSha256","utf8Encode","schnorr","hexDecode","sha256Bytes"],"mappings":"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","file":"index.js","sourcesContent":["// Which Nostr pubkeys a set of device records authorizes. See SPEC §3.4.\n\nimport type { ParsedDeviceEvent } from './index.js';\n\n/**\n * Select the device records whose `nostrPubkey` may act for their address.\n *\n * `records` MUST already be BIP-322-verified, non-revoked bind records for one\n * address. A v3 record signs its Nostr pubkey, so it is always authorized. A v2\n * record does not, so its pubkey is only the event author's claim: it is\n * authorized only while its `device_id` has no v3 record and appears under\n * exactly one Nostr pubkey. Two pubkeys carrying one v2 device authorize neither.\n */\nexport function authorizedDevices(records: readonly ParsedDeviceEvent[]): ParsedDeviceEvent[] {\n const v3Ids = new Set<string>();\n const v2Pubkeys = new Map<string, Set<string>>();\n for (const r of records) {\n if (r.revoked) continue;\n if (r.bindingVersion === 'v3') {\n v3Ids.add(r.device_id);\n } else {\n const set = v2Pubkeys.get(r.device_id) ?? new Set<string>();\n set.add(r.nostrPubkey);\n v2Pubkeys.set(r.device_id, set);\n }\n }\n return records.filter((r) => {\n if (r.revoked) return false;\n if (r.bindingVersion === 'v3') return true;\n if (v3Ids.has(r.device_id)) return false;\n return v2Pubkeys.get(r.device_id)?.size === 1;\n });\n}\n","// Which device records are still live once revocations and the reader's clock\n// are applied. See OC Lock SPEC §3.5 and LIFECYCLE §1.1.\n\nimport type { ParsedDeviceEvent } from './index.js';\n\n/** A timestamp line from a signed device statement, as epoch ms, or null. */\nexport function signedTime(statement: string, field: 'created_at' | 'revoked_at'): number | null {\n const line = statement.split('\\n').find((l) => l.startsWith(`${field}: `));\n if (!line) return null;\n const t = Date.parse(line.slice(field.length + 2));\n return Number.isFinite(t) ? t : null;\n}\n\n/** How far a bind's signed `created_at` may run ahead of the reader's clock and still count. */\nexport const DEFAULT_BIND_SKEW_MS = 10 * 60_000;\n\nexport interface LiveOptions {\n /** The reader's clock. Default: the current time. */\n now?: Date;\n /** Default DEFAULT_BIND_SKEW_MS. */\n maxSkewMs?: number;\n}\n\n/** Binds that no revocation retires, before the reader's clock is applied. */\nfunction standing(records: readonly ParsedDeviceEvent[]): ParsedDeviceEvent[] {\n const key = (r: ParsedDeviceEvent) => `${r.address}\\n${r.device_id}`;\n const revokedAt = new Map<string, number>();\n for (const r of records) {\n if (!r.revoked) continue;\n const at = signedTime(r.bindingStatement, 'revoked_at');\n if (at !== null) revokedAt.set(key(r), Math.max(revokedAt.get(key(r)) ?? at, at));\n }\n return records.filter((r) => {\n if (r.revoked) return false;\n const bound = signedTime(r.bindingStatement, 'created_at');\n if (bound === null) return false;\n const cut = revokedAt.get(key(r));\n return cut === undefined || bound > cut;\n });\n}\n\nconst limitOf = (opts: LiveOptions) =>\n (opts.now ?? new Date()).getTime() + (opts.maxSkewMs ?? DEFAULT_BIND_SKEW_MS);\n\n/**\n * The bind records still live after revocations.\n *\n * `records` MUST already be BIP-322-verified, both binds and revocations: a\n * revocation's authority is the address's signature, so one that was not\n * verified must not be passed in. A revocation retires every bind of its\n * `(address, device_id)` whose signed `created_at` is not later than its signed\n * `revoked_at`, whichever Nostr key carried either record and whichever relay\n * served it. A bind signed after the revocation stays live. A bind whose signed\n * `created_at` is later than the reader's clock plus `maxSkewMs` is not live yet\n * (see pendingDevices).\n */\nexport function liveDevices(\n records: readonly ParsedDeviceEvent[],\n opts: LiveOptions = {}\n): ParsedDeviceEvent[] {\n const limit = limitOf(opts);\n return standing(records).filter((r) => signedTime(r.bindingStatement, 'created_at')! <= limit);\n}\n\n/**\n * Binds that no revocation retires but that are dated later than the reader's\n * clock allows: nobody seals to them yet, and a device list shows them so they\n * can be revoked before their date.\n */\nexport function pendingDevices(\n records: readonly ParsedDeviceEvent[],\n opts: LiveOptions = {}\n): ParsedDeviceEvent[] {\n const limit = limitOf(opts);\n return standing(records).filter((r) => signedTime(r.bindingStatement, 'created_at')! > limit);\n}\n\n/**\n * The `revoked_at` to sign when revoking `(address, deviceId)`: the current\n * time, or the latest signed `created_at` among that device's binds in\n * `records` if later, so the revocation also retires a bind dated ahead.\n */\nexport function revokedAtFor(\n records: readonly ParsedDeviceEvent[],\n address: string,\n deviceId: string,\n now: Date = new Date()\n): string {\n let at = now.getTime();\n for (const r of records) {\n if (r.revoked || r.address !== address || r.device_id !== deviceId) continue;\n const t = signedTime(r.bindingStatement, 'created_at');\n if (t !== null && t > at) at = t;\n }\n return new Date(at).toISOString();\n}\n","// Device key binding and Nostr directory publication.\n// See SPEC.md §3.\n\nimport { schnorr } from '@noble/curves/secp256k1';\nimport {\n generateX25519KeyPair,\n hexDecode,\n hexEncode,\n hkdfSha256,\n randomBytesN,\n sha256Bytes,\n utf8Encode,\n} from '@orangecheck/lock-crypto';\n\nexport const DEVICE_KIND = 30078;\nexport const BINDING_VERSION = 'v2';\n/** Bind statement version that also signs the device's Nostr inbox pubkey. */\nexport const BINDING_VERSION_V3 = 'v3';\n\nexport interface DeviceKeyPair {\n device_id: string;\n device_pk: string; // 32-byte hex\n device_sk: Uint8Array; // 32 bytes\n created_at: string; // iso8601\n}\n\nexport function generateDeviceKey(): DeviceKeyPair {\n const kp = generateX25519KeyPair();\n const deviceIdBytes = randomBytesN(16);\n return {\n device_id: hexEncode(deviceIdBytes),\n device_pk: hexEncode(kp.public),\n device_sk: kp.secret,\n created_at: new Date().toISOString(),\n };\n}\n\nexport function buildBindingStatement(params: {\n address: string;\n device_pk: string;\n device_id: string;\n created_at: string;\n /** The device's Nostr pubkey (deriveNostrKey). Present ⇒ a v3 statement. */\n nostr_pk?: string;\n}): string {\n // Exact byte layout per SPEC §3.2. Each line ends with LF; no trailing LF\n // beyond the last field's. Order is fixed.\n const lines = params.nostr_pk\n ? [\n `oc-lock:device-bind:${BINDING_VERSION_V3}`,\n `address: ${params.address}`,\n `device_pk: ${params.device_pk}`,\n `nostr_pk: ${params.nostr_pk}`,\n `device_id: ${params.device_id}`,\n `created_at: ${params.created_at}`,\n ]\n : [\n `oc-lock:device-bind:${BINDING_VERSION}`,\n `address: ${params.address}`,\n `device_pk: ${params.device_pk}`,\n `device_id: ${params.device_id}`,\n `created_at: ${params.created_at}`,\n ];\n return lines.join('\\n') + '\\n';\n}\n\nexport function buildRevocationStatement(params: {\n address: string;\n device_id: string;\n revoked_at: string;\n}): string {\n const lines = [\n `oc-lock:device-revoke:${BINDING_VERSION}`,\n `address: ${params.address}`,\n `device_id: ${params.device_id}`,\n `revoked_at: ${params.revoked_at}`,\n ];\n return lines.join('\\n') + '\\n';\n}\n\n// ---------------------------------------------------------------------------\n// Nostr event helpers\n// ---------------------------------------------------------------------------\n\nexport interface NostrEvent {\n id: string;\n pubkey: string;\n created_at: number;\n kind: number;\n tags: string[][];\n content: string;\n sig: string;\n}\n\n/**\n * Derive an ephemeral Nostr keypair from a device secret per SPEC §3.4.\n * Returns a 32-byte private key (hex).\n */\nexport function deriveNostrKey(deviceSk: Uint8Array): { nostrSk: string; nostrPk: string } {\n const sk = hkdfSha256(\n deviceSk,\n utf8Encode('oc-lock/v2/nostr-key'),\n utf8Encode('nostr-sk'),\n 32\n );\n const pk = schnorr.getPublicKey(sk);\n return { nostrSk: hexEncode(sk), nostrPk: hexEncode(pk) };\n}\n\n/**\n * Build a kind-30078 addressable event announcing a device binding.\n * The caller must supply the BIP-322 signature produced by the user's Bitcoin\n * wallet (we cannot sign BIP-322 in this package — that requires a wallet).\n */\nexport function buildDeviceEvent(params: {\n address: string;\n device_id: string;\n device_pk: string;\n bindingStatement: string;\n bindingSigBase64: string;\n createdAtUnix?: number;\n}): Omit<NostrEvent, 'id' | 'sig' | 'pubkey'> & { pubkey?: undefined } {\n const tags: string[][] = [\n ['d', `oc-lock:device:${params.address}`],\n ['addr', params.address],\n ['device_id', params.device_id],\n ['device_pk', params.device_pk],\n ['alg', 'x25519'],\n ['binding_sig', params.bindingSigBase64],\n ];\n const created_at = params.createdAtUnix ?? Math.floor(Date.now() / 1000);\n return {\n kind: DEVICE_KIND,\n tags,\n content: params.bindingStatement,\n created_at,\n } as unknown as Omit<NostrEvent, 'id' | 'sig' | 'pubkey'> & { pubkey?: undefined };\n}\n\n/**\n * Sign a Nostr event with a Schnorr key (derived via deriveNostrKey). Produces\n * the full NostrEvent with id and sig.\n */\nexport function signNostrEvent(\n event: Omit<NostrEvent, 'id' | 'sig'>,\n nostrSkHex: string\n): NostrEvent {\n const id = computeNostrEventId(event);\n const sk = hexDecode(nostrSkHex);\n const sig = schnorr.sign(hexDecode(id), sk);\n return {\n ...event,\n id,\n sig: hexEncode(sig),\n };\n}\n\nexport function computeNostrEventId(event: Omit<NostrEvent, 'id' | 'sig'>): string {\n // Per NIP-01: id = sha256(utf8(JSON.stringify([0, pubkey, created_at, kind, tags, content]))).\n // The array must be serialized in *exactly* this form with no whitespace.\n const serialized = JSON.stringify([\n 0,\n event.pubkey,\n event.created_at,\n event.kind,\n event.tags,\n event.content,\n ]);\n return hexEncode(sha256Bytes(utf8Encode(serialized)));\n}\n\n/**\n * Finalize a device event: derive Nostr key from device_sk, attach pubkey,\n * compute id, sign. Returns a fully-signed NIP-01 event ready to publish.\n */\nexport function finalizeDeviceEvent(params: {\n deviceSk: Uint8Array;\n address: string;\n device_id: string;\n device_pk: string;\n bindingStatement: string;\n bindingSigBase64: string;\n createdAtUnix?: number;\n}): NostrEvent {\n const { nostrSk, nostrPk } = deriveNostrKey(params.deviceSk);\n const base = buildDeviceEvent({\n address: params.address,\n device_id: params.device_id,\n device_pk: params.device_pk,\n bindingStatement: params.bindingStatement,\n bindingSigBase64: params.bindingSigBase64,\n createdAtUnix: params.createdAtUnix,\n });\n const withPubkey = { ...base, pubkey: nostrPk } as Omit<NostrEvent, 'id' | 'sig'>;\n return signNostrEvent(withPubkey, nostrSk);\n}\n\n// ---------------------------------------------------------------------------\n// Parsing\n// ---------------------------------------------------------------------------\n\nexport interface ParsedDeviceEvent {\n address: string;\n device_id: string;\n device_pk: string;\n bindingStatement: string;\n bindingSigBase64: string;\n revoked: boolean;\n createdAtUnix: number;\n nostrPubkey: string;\n eventId: string;\n /** 'v3' binds signed the Nostr pubkey; 'v2' binds and revocations did not. */\n bindingVersion: 'v2' | 'v3';\n}\n\nexport function parseDeviceEvent(event: NostrEvent): ParsedDeviceEvent {\n if (event.kind !== DEVICE_KIND) {\n throw new Error('not a device event (kind mismatch)');\n }\n const tag = (name: string): string | null => {\n for (const t of event.tags) {\n if (t[0] === name && t.length > 1) return t[1] ?? null;\n }\n return null;\n };\n const address = tag('addr');\n const device_id = tag('device_id');\n const device_pk = tag('device_pk');\n const binding_sig = tag('binding_sig');\n if (!address || !device_id || !device_pk || !binding_sig) {\n throw new Error('device event missing required tags');\n }\n\n // The BIP-322 signature covers `event.content` — the statement — and nothing\n // else. Tags are an index the relay lets anyone write, so a value read from a\n // tag has been authenticated by nobody.\n //\n // Skipping this check was a total confidentiality break, and it needed no key\n // material: republish a victim's device event with their real content and\n // real binding_sig, change only the `device_pk` TAG to your own X25519 key.\n // verifyBip322(content, sig, addr) passes — every signed byte is genuine —\n // and the sender then seals the content key to the attacker's key, because\n // the recipient record was built from the tag. Proven against the published\n // package before this existed.\n //\n // So the statement is the source of truth and the tags must agree with it.\n const signed = parseBindingStatement(event.content);\n if (!signed) {\n throw new Error('device event content is not a v2/v3 bind or revoke statement');\n }\n if (signed.address !== address) {\n throw new Error(\n `device event addr tag (${address}) does not match the signed statement (${signed.address})`,\n );\n }\n // SPEC §3.3 / §3.6: the record lives at `oc-lock:device:<addr>` or the\n // per-device `oc-lock:device:<addr>:<device_id>`. Lookups filter on `d`, so a\n // record for one address filed under another's slot must not parse.\n const d = tag('d');\n if (\n d !== `oc-lock:device:${signed.address}` &&\n d !== `oc-lock:device:${signed.address}:${signed.device_id}`\n ) {\n throw new Error(\n `device event d tag (${d}) is not the slot for the signed address (${signed.address})`,\n );\n }\n if (signed.device_id !== device_id) {\n throw new Error(\n `device event device_id tag (${device_id}) does not match the signed statement (${signed.device_id})`,\n );\n }\n if (signed.kind === 'bind') {\n if (signed.device_pk !== device_pk) {\n throw new Error(\n `device event device_pk tag does not match the signed statement — ` +\n `tag says ${device_pk}, the signature covers ${signed.device_pk}`,\n );\n }\n // A v3 statement names the one Nostr key allowed to carry it.\n if (signed.nostr_pk !== null && signed.nostr_pk !== event.pubkey) {\n throw new Error(\n `device event pubkey (${event.pubkey}) does not match the signed nostr_pk (${signed.nostr_pk})`,\n );\n }\n } else if (device_pk !== 'revoked') {\n throw new Error(\n `device event carries a revocation statement but its device_pk tag is ${device_pk}, not \"revoked\"`,\n );\n }\n\n return {\n address: signed.address,\n device_id: signed.device_id,\n // Take the key from the SIGNED bytes, never the tag.\n device_pk: signed.kind === 'bind' ? signed.device_pk : 'revoked',\n bindingStatement: event.content,\n bindingSigBase64: binding_sig,\n revoked: signed.kind === 'revoke',\n createdAtUnix: event.created_at,\n nostrPubkey: event.pubkey,\n eventId: event.id,\n bindingVersion: signed.kind === 'bind' && signed.nostr_pk !== null ? 'v3' : 'v2',\n };\n}\n\n/**\n * Read back the fields the signature actually commits to. Returns null for\n * anything that is not a well-formed v2/v3 bind or v2 revoke statement, which the caller\n * treats as an unusable record.\n */\nfunction parseBindingStatement(\n content: string,\n):\n | {\n kind: 'bind';\n address: string;\n device_pk: string;\n device_id: string;\n nostr_pk: string | null;\n }\n | { kind: 'revoke'; address: string; device_id: string }\n | null {\n const lines = content.split('\\n');\n const header = lines[0];\n const field = (name: string): string | null => {\n const prefix = `${name}: `;\n for (const l of lines.slice(1)) {\n if (l.startsWith(prefix)) return l.slice(prefix.length);\n }\n return null;\n };\n const address = field('address');\n const device_id = field('device_id');\n if (!address || !device_id) return null;\n\n if (header === `oc-lock:device-bind:${BINDING_VERSION}`) {\n const device_pk = field('device_pk');\n if (!device_pk) return null;\n return { kind: 'bind', address, device_pk, device_id, nostr_pk: null };\n }\n if (header === `oc-lock:device-bind:${BINDING_VERSION_V3}`) {\n const device_pk = field('device_pk');\n const nostr_pk = field('nostr_pk');\n if (!device_pk || !nostr_pk || !/^[0-9a-f]{64}$/.test(nostr_pk)) return null;\n return { kind: 'bind', address, device_pk, device_id, nostr_pk };\n }\n if (header === `oc-lock:device-revoke:${BINDING_VERSION}`) {\n return { kind: 'revoke', address, device_id };\n }\n return null;\n}\n\nexport { authorizedDevices } from './authorize.js';\nexport {\n DEFAULT_BIND_SKEW_MS,\n liveDevices,\n pendingDevices,\n revokedAtFor,\n signedTime,\n} from './live.js';\nexport type { LiveOptions } from './live.js';\n"]}
1
+ {"version":3,"sources":["../src/authorize.ts","../src/live.ts","../src/index.ts"],"names":["generateX25519KeyPair","randomBytesN","hexEncode","hkdfSha256","utf8Encode","schnorr","hexDecode","sha256Bytes"],"mappings":"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","file":"index.js","sourcesContent":["// Which Nostr pubkeys a set of device records authorizes. See SPEC §3.4.\n\nimport type { ParsedDeviceEvent } from './index.js';\n\n/** From this instant, by the reader's clock, a v2 record authorizes no Nostr key (SPEC §3.4). */\nexport const V2_INBOX_SUNSET = '2027-01-21T00:00:00Z';\n\nexport interface AuthorizeOptions {\n /** The reader's clock. Default: the current time. */\n now?: Date;\n}\n\n/**\n * Select the device records whose `nostrPubkey` may act for their address.\n *\n * `records` MUST already be BIP-322-verified, non-revoked bind records for one\n * address. A v3 record signs its Nostr pubkey, so it is always authorized. A v2\n * record does not, so its pubkey is only the event author's claim. Before\n * `V2_INBOX_SUNSET` it is authorized only while its `device_id` has no v3 record\n * and appears under exactly one Nostr pubkey; from then on it authorizes no key.\n * A v2 bind stays live for sealing to its `device_pk` either way (liveDevices).\n */\nexport function authorizedDevices(\n records: readonly ParsedDeviceEvent[],\n opts: AuthorizeOptions = {}\n): ParsedDeviceEvent[] {\n for (const k of Object.keys(opts)) {\n if (k !== 'now') throw new TypeError(`authorizedDevices does not read option \"${k}\"`);\n }\n const v2Counts = (opts.now ?? new Date()).getTime() < Date.parse(V2_INBOX_SUNSET);\n const v3Ids = new Set<string>();\n const v2Pubkeys = new Map<string, Set<string>>();\n for (const r of records) {\n if (r.revoked) continue;\n if (r.bindingVersion === 'v3') {\n v3Ids.add(r.device_id);\n } else {\n const set = v2Pubkeys.get(r.device_id) ?? new Set<string>();\n set.add(r.nostrPubkey);\n v2Pubkeys.set(r.device_id, set);\n }\n }\n return records.filter((r) => {\n if (r.revoked) return false;\n if (r.bindingVersion === 'v3') return true;\n if (!v2Counts || v3Ids.has(r.device_id)) return false;\n return v2Pubkeys.get(r.device_id)?.size === 1;\n });\n}\n","// Which device records are still live once revocations and the reader's clock\n// are applied. See OC Lock SPEC §3.5 and LIFECYCLE §1.1.\n\nimport type { ParsedDeviceEvent } from './index.js';\n\n/** A timestamp line from a signed device statement, as epoch ms, or null. */\nexport function signedTime(statement: string, field: 'created_at' | 'revoked_at'): number | null {\n const line = statement.split('\\n').find((l) => l.startsWith(`${field}: `));\n if (!line) return null;\n const t = Date.parse(line.slice(field.length + 2));\n return Number.isFinite(t) ? t : null;\n}\n\n/** How far a bind's signed `created_at` may run ahead of the reader's clock and still count. */\nexport const DEFAULT_BIND_SKEW_MS = 10 * 60_000;\n\nexport interface LiveOptions {\n /** The reader's clock. Default: the current time. */\n now?: Date;\n /** Default DEFAULT_BIND_SKEW_MS. */\n maxSkewMs?: number;\n}\n\n/** Binds that no revocation retires, before the reader's clock is applied. */\nfunction standing(records: readonly ParsedDeviceEvent[]): ParsedDeviceEvent[] {\n const key = (r: ParsedDeviceEvent) => `${r.address}\\n${r.device_id}`;\n const revokedAt = new Map<string, number>();\n for (const r of records) {\n if (!r.revoked) continue;\n const at = signedTime(r.bindingStatement, 'revoked_at');\n if (at !== null) revokedAt.set(key(r), Math.max(revokedAt.get(key(r)) ?? at, at));\n }\n return records.filter((r) => {\n if (r.revoked) return false;\n const bound = signedTime(r.bindingStatement, 'created_at');\n if (bound === null) return false;\n const cut = revokedAt.get(key(r));\n return cut === undefined || bound > cut;\n });\n}\n\nconst limitOf = (opts: LiveOptions) =>\n (opts.now ?? new Date()).getTime() + (opts.maxSkewMs ?? DEFAULT_BIND_SKEW_MS);\n\n/**\n * The bind records still live after revocations.\n *\n * `records` MUST already be BIP-322-verified, both binds and revocations: a\n * revocation's authority is the address's signature, so one that was not\n * verified must not be passed in. A revocation retires every bind of its\n * `(address, device_id)` whose signed `created_at` is not later than its signed\n * `revoked_at`, whichever Nostr key carried either record and whichever relay\n * served it. A bind signed after the revocation stays live. A bind whose signed\n * `created_at` is later than the reader's clock plus `maxSkewMs` is not live yet\n * (see pendingDevices).\n */\nexport function liveDevices(\n records: readonly ParsedDeviceEvent[],\n opts: LiveOptions = {}\n): ParsedDeviceEvent[] {\n const limit = limitOf(opts);\n return standing(records).filter((r) => signedTime(r.bindingStatement, 'created_at')! <= limit);\n}\n\n/**\n * Binds that no revocation retires but that are dated later than the reader's\n * clock allows: nobody seals to them yet, and a device list shows them so they\n * can be revoked before their date.\n */\nexport function pendingDevices(\n records: readonly ParsedDeviceEvent[],\n opts: LiveOptions = {}\n): ParsedDeviceEvent[] {\n const limit = limitOf(opts);\n return standing(records).filter((r) => signedTime(r.bindingStatement, 'created_at')! > limit);\n}\n\n/**\n * The `revoked_at` to sign when revoking `(address, deviceId)`: the current\n * time, or the latest signed `created_at` among that device's binds in\n * `records` if later, so the revocation also retires a bind dated ahead.\n */\nexport function revokedAtFor(\n records: readonly ParsedDeviceEvent[],\n address: string,\n deviceId: string,\n now: Date = new Date()\n): string {\n let at = now.getTime();\n for (const r of records) {\n if (r.revoked || r.address !== address || r.device_id !== deviceId) continue;\n const t = signedTime(r.bindingStatement, 'created_at');\n if (t !== null && t > at) at = t;\n }\n return new Date(at).toISOString();\n}\n","// Device key binding and Nostr directory publication.\n// See SPEC.md §3.\n\nimport { schnorr } from '@noble/curves/secp256k1';\nimport {\n generateX25519KeyPair,\n hexDecode,\n hexEncode,\n hkdfSha256,\n randomBytesN,\n sha256Bytes,\n utf8Encode,\n} from '@orangecheck/lock-crypto';\n\nexport const DEVICE_KIND = 30078;\nexport const BINDING_VERSION = 'v2';\n/** Bind statement version that also signs the device's Nostr inbox pubkey. */\nexport const BINDING_VERSION_V3 = 'v3';\n\nexport interface DeviceKeyPair {\n device_id: string;\n device_pk: string; // 32-byte hex\n device_sk: Uint8Array; // 32 bytes\n created_at: string; // iso8601\n}\n\nexport function generateDeviceKey(): DeviceKeyPair {\n const kp = generateX25519KeyPair();\n const deviceIdBytes = randomBytesN(16);\n return {\n device_id: hexEncode(deviceIdBytes),\n device_pk: hexEncode(kp.public),\n device_sk: kp.secret,\n created_at: new Date().toISOString(),\n };\n}\n\nexport function buildBindingStatement(params: {\n address: string;\n device_pk: string;\n device_id: string;\n created_at: string;\n /** The device's Nostr pubkey (deriveNostrKey). Present ⇒ a v3 statement. */\n nostr_pk?: string;\n}): string {\n // Exact byte layout per SPEC §3.2. Each line ends with LF; no trailing LF\n // beyond the last field's. Order is fixed.\n const lines = params.nostr_pk\n ? [\n `oc-lock:device-bind:${BINDING_VERSION_V3}`,\n `address: ${params.address}`,\n `device_pk: ${params.device_pk}`,\n `nostr_pk: ${params.nostr_pk}`,\n `device_id: ${params.device_id}`,\n `created_at: ${params.created_at}`,\n ]\n : [\n `oc-lock:device-bind:${BINDING_VERSION}`,\n `address: ${params.address}`,\n `device_pk: ${params.device_pk}`,\n `device_id: ${params.device_id}`,\n `created_at: ${params.created_at}`,\n ];\n return lines.join('\\n') + '\\n';\n}\n\nexport function buildRevocationStatement(params: {\n address: string;\n device_id: string;\n revoked_at: string;\n}): string {\n const lines = [\n `oc-lock:device-revoke:${BINDING_VERSION}`,\n `address: ${params.address}`,\n `device_id: ${params.device_id}`,\n `revoked_at: ${params.revoked_at}`,\n ];\n return lines.join('\\n') + '\\n';\n}\n\n// ---------------------------------------------------------------------------\n// Nostr event helpers\n// ---------------------------------------------------------------------------\n\nexport interface NostrEvent {\n id: string;\n pubkey: string;\n created_at: number;\n kind: number;\n tags: string[][];\n content: string;\n sig: string;\n}\n\n/**\n * Derive an ephemeral Nostr keypair from a device secret per SPEC §3.4.\n * Returns a 32-byte private key (hex).\n */\nexport function deriveNostrKey(deviceSk: Uint8Array): { nostrSk: string; nostrPk: string } {\n const sk = hkdfSha256(\n deviceSk,\n utf8Encode('oc-lock/v2/nostr-key'),\n utf8Encode('nostr-sk'),\n 32\n );\n const pk = schnorr.getPublicKey(sk);\n return { nostrSk: hexEncode(sk), nostrPk: hexEncode(pk) };\n}\n\n/**\n * Build a kind-30078 addressable event announcing a device binding.\n * The caller must supply the BIP-322 signature produced by the user's Bitcoin\n * wallet (we cannot sign BIP-322 in this package — that requires a wallet).\n */\nexport function buildDeviceEvent(params: {\n address: string;\n device_id: string;\n device_pk: string;\n bindingStatement: string;\n bindingSigBase64: string;\n createdAtUnix?: number;\n}): Omit<NostrEvent, 'id' | 'sig' | 'pubkey'> & { pubkey?: undefined } {\n const tags: string[][] = [\n ['d', `oc-lock:device:${params.address}`],\n ['addr', params.address],\n ['device_id', params.device_id],\n ['device_pk', params.device_pk],\n ['alg', 'x25519'],\n ['binding_sig', params.bindingSigBase64],\n ];\n const created_at = params.createdAtUnix ?? Math.floor(Date.now() / 1000);\n return {\n kind: DEVICE_KIND,\n tags,\n content: params.bindingStatement,\n created_at,\n } as unknown as Omit<NostrEvent, 'id' | 'sig' | 'pubkey'> & { pubkey?: undefined };\n}\n\n/**\n * Sign a Nostr event with a Schnorr key (derived via deriveNostrKey). Produces\n * the full NostrEvent with id and sig.\n */\nexport function signNostrEvent(\n event: Omit<NostrEvent, 'id' | 'sig'>,\n nostrSkHex: string\n): NostrEvent {\n const id = computeNostrEventId(event);\n const sk = hexDecode(nostrSkHex);\n const sig = schnorr.sign(hexDecode(id), sk);\n return {\n ...event,\n id,\n sig: hexEncode(sig),\n };\n}\n\nexport function computeNostrEventId(event: Omit<NostrEvent, 'id' | 'sig'>): string {\n // Per NIP-01: id = sha256(utf8(JSON.stringify([0, pubkey, created_at, kind, tags, content]))).\n // The array must be serialized in *exactly* this form with no whitespace.\n const serialized = JSON.stringify([\n 0,\n event.pubkey,\n event.created_at,\n event.kind,\n event.tags,\n event.content,\n ]);\n return hexEncode(sha256Bytes(utf8Encode(serialized)));\n}\n\n/**\n * Finalize a device event: derive Nostr key from device_sk, attach pubkey,\n * compute id, sign. Returns a fully-signed NIP-01 event ready to publish.\n */\nexport function finalizeDeviceEvent(params: {\n deviceSk: Uint8Array;\n address: string;\n device_id: string;\n device_pk: string;\n bindingStatement: string;\n bindingSigBase64: string;\n createdAtUnix?: number;\n}): NostrEvent {\n const { nostrSk, nostrPk } = deriveNostrKey(params.deviceSk);\n const base = buildDeviceEvent({\n address: params.address,\n device_id: params.device_id,\n device_pk: params.device_pk,\n bindingStatement: params.bindingStatement,\n bindingSigBase64: params.bindingSigBase64,\n createdAtUnix: params.createdAtUnix,\n });\n const withPubkey = { ...base, pubkey: nostrPk } as Omit<NostrEvent, 'id' | 'sig'>;\n return signNostrEvent(withPubkey, nostrSk);\n}\n\n// ---------------------------------------------------------------------------\n// Parsing\n// ---------------------------------------------------------------------------\n\nexport interface ParsedDeviceEvent {\n address: string;\n device_id: string;\n device_pk: string;\n bindingStatement: string;\n bindingSigBase64: string;\n revoked: boolean;\n createdAtUnix: number;\n nostrPubkey: string;\n eventId: string;\n /** 'v3' binds signed the Nostr pubkey; 'v2' binds and revocations did not. */\n bindingVersion: 'v2' | 'v3';\n}\n\nexport function parseDeviceEvent(event: NostrEvent): ParsedDeviceEvent {\n if (event.kind !== DEVICE_KIND) {\n throw new Error('not a device event (kind mismatch)');\n }\n const tag = (name: string): string | null => {\n for (const t of event.tags) {\n if (t[0] === name && t.length > 1) return t[1] ?? null;\n }\n return null;\n };\n const address = tag('addr');\n const device_id = tag('device_id');\n const device_pk = tag('device_pk');\n const binding_sig = tag('binding_sig');\n if (!address || !device_id || !device_pk || !binding_sig) {\n throw new Error('device event missing required tags');\n }\n\n // The BIP-322 signature covers `event.content` — the statement — and nothing\n // else. Tags are an index the relay lets anyone write, so a value read from a\n // tag has been authenticated by nobody.\n //\n // Skipping this check was a total confidentiality break, and it needed no key\n // material: republish a victim's device event with their real content and\n // real binding_sig, change only the `device_pk` TAG to your own X25519 key.\n // verifyBip322(content, sig, addr) passes — every signed byte is genuine —\n // and the sender then seals the content key to the attacker's key, because\n // the recipient record was built from the tag. Proven against the published\n // package before this existed.\n //\n // So the statement is the source of truth and the tags must agree with it.\n const signed = parseBindingStatement(event.content);\n if (!signed) {\n throw new Error('device event content is not a v2/v3 bind or revoke statement');\n }\n if (signed.address !== address) {\n throw new Error(\n `device event addr tag (${address}) does not match the signed statement (${signed.address})`,\n );\n }\n // SPEC §3.3 / §3.6: the record lives at `oc-lock:device:<addr>` or the\n // per-device `oc-lock:device:<addr>:<device_id>`. Lookups filter on `d`, so a\n // record for one address filed under another's slot must not parse.\n const d = tag('d');\n if (\n d !== `oc-lock:device:${signed.address}` &&\n d !== `oc-lock:device:${signed.address}:${signed.device_id}`\n ) {\n throw new Error(\n `device event d tag (${d}) is not the slot for the signed address (${signed.address})`,\n );\n }\n if (signed.device_id !== device_id) {\n throw new Error(\n `device event device_id tag (${device_id}) does not match the signed statement (${signed.device_id})`,\n );\n }\n if (signed.kind === 'bind') {\n if (signed.device_pk !== device_pk) {\n throw new Error(\n `device event device_pk tag does not match the signed statement — ` +\n `tag says ${device_pk}, the signature covers ${signed.device_pk}`,\n );\n }\n // A v3 statement names the one Nostr key allowed to carry it.\n if (signed.nostr_pk !== null && signed.nostr_pk !== event.pubkey) {\n throw new Error(\n `device event pubkey (${event.pubkey}) does not match the signed nostr_pk (${signed.nostr_pk})`,\n );\n }\n } else if (device_pk !== 'revoked') {\n throw new Error(\n `device event carries a revocation statement but its device_pk tag is ${device_pk}, not \"revoked\"`,\n );\n }\n\n return {\n address: signed.address,\n device_id: signed.device_id,\n // Take the key from the SIGNED bytes, never the tag.\n device_pk: signed.kind === 'bind' ? signed.device_pk : 'revoked',\n bindingStatement: event.content,\n bindingSigBase64: binding_sig,\n revoked: signed.kind === 'revoke',\n createdAtUnix: event.created_at,\n nostrPubkey: event.pubkey,\n eventId: event.id,\n bindingVersion: signed.kind === 'bind' && signed.nostr_pk !== null ? 'v3' : 'v2',\n };\n}\n\n/**\n * Read back the fields the signature actually commits to. Returns null for\n * anything that is not a well-formed v2/v3 bind or v2 revoke statement, which the caller\n * treats as an unusable record.\n */\nfunction parseBindingStatement(\n content: string,\n):\n | {\n kind: 'bind';\n address: string;\n device_pk: string;\n device_id: string;\n nostr_pk: string | null;\n }\n | { kind: 'revoke'; address: string; device_id: string }\n | null {\n const lines = content.split('\\n');\n const header = lines[0];\n const field = (name: string): string | null => {\n const prefix = `${name}: `;\n for (const l of lines.slice(1)) {\n if (l.startsWith(prefix)) return l.slice(prefix.length);\n }\n return null;\n };\n const address = field('address');\n const device_id = field('device_id');\n if (!address || !device_id) return null;\n\n if (header === `oc-lock:device-bind:${BINDING_VERSION}`) {\n const device_pk = field('device_pk');\n if (!device_pk) return null;\n return { kind: 'bind', address, device_pk, device_id, nostr_pk: null };\n }\n if (header === `oc-lock:device-bind:${BINDING_VERSION_V3}`) {\n const device_pk = field('device_pk');\n const nostr_pk = field('nostr_pk');\n if (!device_pk || !nostr_pk || !/^[0-9a-f]{64}$/.test(nostr_pk)) return null;\n return { kind: 'bind', address, device_pk, device_id, nostr_pk };\n }\n if (header === `oc-lock:device-revoke:${BINDING_VERSION}`) {\n return { kind: 'revoke', address, device_id };\n }\n return null;\n}\n\nexport { authorizedDevices, V2_INBOX_SUNSET } from './authorize.js';\nexport type { AuthorizeOptions } from './authorize.js';\nexport {\n DEFAULT_BIND_SKEW_MS,\n liveDevices,\n pendingDevices,\n revokedAtFor,\n signedTime,\n} from './live.js';\nexport type { LiveOptions } from './live.js';\n"]}
package/dist/index.mjs CHANGED
@@ -4,7 +4,12 @@ import { generateX25519KeyPair, randomBytesN, hexEncode, hkdfSha256, utf8Encode,
4
4
  // src/index.ts
5
5
 
6
6
  // src/authorize.ts
7
- function authorizedDevices(records) {
7
+ var V2_INBOX_SUNSET = "2027-01-21T00:00:00Z";
8
+ function authorizedDevices(records, opts = {}) {
9
+ for (const k of Object.keys(opts)) {
10
+ if (k !== "now") throw new TypeError(`authorizedDevices does not read option "${k}"`);
11
+ }
12
+ const v2Counts = (opts.now ?? /* @__PURE__ */ new Date()).getTime() < Date.parse(V2_INBOX_SUNSET);
8
13
  const v3Ids = /* @__PURE__ */ new Set();
9
14
  const v2Pubkeys = /* @__PURE__ */ new Map();
10
15
  for (const r of records) {
@@ -20,7 +25,7 @@ function authorizedDevices(records) {
20
25
  return records.filter((r) => {
21
26
  if (r.revoked) return false;
22
27
  if (r.bindingVersion === "v3") return true;
23
- if (v3Ids.has(r.device_id)) return false;
28
+ if (!v2Counts || v3Ids.has(r.device_id)) return false;
24
29
  return v2Pubkeys.get(r.device_id)?.size === 1;
25
30
  });
26
31
  }
@@ -267,6 +272,6 @@ function parseBindingStatement(content) {
267
272
  return null;
268
273
  }
269
274
 
270
- export { BINDING_VERSION, BINDING_VERSION_V3, DEFAULT_BIND_SKEW_MS, DEVICE_KIND, authorizedDevices, buildBindingStatement, buildDeviceEvent, buildRevocationStatement, computeNostrEventId, deriveNostrKey, finalizeDeviceEvent, generateDeviceKey, liveDevices, parseDeviceEvent, pendingDevices, revokedAtFor, signNostrEvent, signedTime };
275
+ export { BINDING_VERSION, BINDING_VERSION_V3, DEFAULT_BIND_SKEW_MS, DEVICE_KIND, V2_INBOX_SUNSET, authorizedDevices, buildBindingStatement, buildDeviceEvent, buildRevocationStatement, computeNostrEventId, deriveNostrKey, finalizeDeviceEvent, generateDeviceKey, liveDevices, parseDeviceEvent, pendingDevices, revokedAtFor, signNostrEvent, signedTime };
271
276
  //# sourceMappingURL=index.mjs.map
272
277
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/authorize.ts","../src/live.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["// Which Nostr pubkeys a set of device records authorizes. See SPEC §3.4.\n\nimport type { ParsedDeviceEvent } from './index.js';\n\n/**\n * Select the device records whose `nostrPubkey` may act for their address.\n *\n * `records` MUST already be BIP-322-verified, non-revoked bind records for one\n * address. A v3 record signs its Nostr pubkey, so it is always authorized. A v2\n * record does not, so its pubkey is only the event author's claim: it is\n * authorized only while its `device_id` has no v3 record and appears under\n * exactly one Nostr pubkey. Two pubkeys carrying one v2 device authorize neither.\n */\nexport function authorizedDevices(records: readonly ParsedDeviceEvent[]): ParsedDeviceEvent[] {\n const v3Ids = new Set<string>();\n const v2Pubkeys = new Map<string, Set<string>>();\n for (const r of records) {\n if (r.revoked) continue;\n if (r.bindingVersion === 'v3') {\n v3Ids.add(r.device_id);\n } else {\n const set = v2Pubkeys.get(r.device_id) ?? new Set<string>();\n set.add(r.nostrPubkey);\n v2Pubkeys.set(r.device_id, set);\n }\n }\n return records.filter((r) => {\n if (r.revoked) return false;\n if (r.bindingVersion === 'v3') return true;\n if (v3Ids.has(r.device_id)) return false;\n return v2Pubkeys.get(r.device_id)?.size === 1;\n });\n}\n","// Which device records are still live once revocations and the reader's clock\n// are applied. See OC Lock SPEC §3.5 and LIFECYCLE §1.1.\n\nimport type { ParsedDeviceEvent } from './index.js';\n\n/** A timestamp line from a signed device statement, as epoch ms, or null. */\nexport function signedTime(statement: string, field: 'created_at' | 'revoked_at'): number | null {\n const line = statement.split('\\n').find((l) => l.startsWith(`${field}: `));\n if (!line) return null;\n const t = Date.parse(line.slice(field.length + 2));\n return Number.isFinite(t) ? t : null;\n}\n\n/** How far a bind's signed `created_at` may run ahead of the reader's clock and still count. */\nexport const DEFAULT_BIND_SKEW_MS = 10 * 60_000;\n\nexport interface LiveOptions {\n /** The reader's clock. Default: the current time. */\n now?: Date;\n /** Default DEFAULT_BIND_SKEW_MS. */\n maxSkewMs?: number;\n}\n\n/** Binds that no revocation retires, before the reader's clock is applied. */\nfunction standing(records: readonly ParsedDeviceEvent[]): ParsedDeviceEvent[] {\n const key = (r: ParsedDeviceEvent) => `${r.address}\\n${r.device_id}`;\n const revokedAt = new Map<string, number>();\n for (const r of records) {\n if (!r.revoked) continue;\n const at = signedTime(r.bindingStatement, 'revoked_at');\n if (at !== null) revokedAt.set(key(r), Math.max(revokedAt.get(key(r)) ?? at, at));\n }\n return records.filter((r) => {\n if (r.revoked) return false;\n const bound = signedTime(r.bindingStatement, 'created_at');\n if (bound === null) return false;\n const cut = revokedAt.get(key(r));\n return cut === undefined || bound > cut;\n });\n}\n\nconst limitOf = (opts: LiveOptions) =>\n (opts.now ?? new Date()).getTime() + (opts.maxSkewMs ?? DEFAULT_BIND_SKEW_MS);\n\n/**\n * The bind records still live after revocations.\n *\n * `records` MUST already be BIP-322-verified, both binds and revocations: a\n * revocation's authority is the address's signature, so one that was not\n * verified must not be passed in. A revocation retires every bind of its\n * `(address, device_id)` whose signed `created_at` is not later than its signed\n * `revoked_at`, whichever Nostr key carried either record and whichever relay\n * served it. A bind signed after the revocation stays live. A bind whose signed\n * `created_at` is later than the reader's clock plus `maxSkewMs` is not live yet\n * (see pendingDevices).\n */\nexport function liveDevices(\n records: readonly ParsedDeviceEvent[],\n opts: LiveOptions = {}\n): ParsedDeviceEvent[] {\n const limit = limitOf(opts);\n return standing(records).filter((r) => signedTime(r.bindingStatement, 'created_at')! <= limit);\n}\n\n/**\n * Binds that no revocation retires but that are dated later than the reader's\n * clock allows: nobody seals to them yet, and a device list shows them so they\n * can be revoked before their date.\n */\nexport function pendingDevices(\n records: readonly ParsedDeviceEvent[],\n opts: LiveOptions = {}\n): ParsedDeviceEvent[] {\n const limit = limitOf(opts);\n return standing(records).filter((r) => signedTime(r.bindingStatement, 'created_at')! > limit);\n}\n\n/**\n * The `revoked_at` to sign when revoking `(address, deviceId)`: the current\n * time, or the latest signed `created_at` among that device's binds in\n * `records` if later, so the revocation also retires a bind dated ahead.\n */\nexport function revokedAtFor(\n records: readonly ParsedDeviceEvent[],\n address: string,\n deviceId: string,\n now: Date = new Date()\n): string {\n let at = now.getTime();\n for (const r of records) {\n if (r.revoked || r.address !== address || r.device_id !== deviceId) continue;\n const t = signedTime(r.bindingStatement, 'created_at');\n if (t !== null && t > at) at = t;\n }\n return new Date(at).toISOString();\n}\n","// Device key binding and Nostr directory publication.\n// See SPEC.md §3.\n\nimport { schnorr } from '@noble/curves/secp256k1';\nimport {\n generateX25519KeyPair,\n hexDecode,\n hexEncode,\n hkdfSha256,\n randomBytesN,\n sha256Bytes,\n utf8Encode,\n} from '@orangecheck/lock-crypto';\n\nexport const DEVICE_KIND = 30078;\nexport const BINDING_VERSION = 'v2';\n/** Bind statement version that also signs the device's Nostr inbox pubkey. */\nexport const BINDING_VERSION_V3 = 'v3';\n\nexport interface DeviceKeyPair {\n device_id: string;\n device_pk: string; // 32-byte hex\n device_sk: Uint8Array; // 32 bytes\n created_at: string; // iso8601\n}\n\nexport function generateDeviceKey(): DeviceKeyPair {\n const kp = generateX25519KeyPair();\n const deviceIdBytes = randomBytesN(16);\n return {\n device_id: hexEncode(deviceIdBytes),\n device_pk: hexEncode(kp.public),\n device_sk: kp.secret,\n created_at: new Date().toISOString(),\n };\n}\n\nexport function buildBindingStatement(params: {\n address: string;\n device_pk: string;\n device_id: string;\n created_at: string;\n /** The device's Nostr pubkey (deriveNostrKey). Present ⇒ a v3 statement. */\n nostr_pk?: string;\n}): string {\n // Exact byte layout per SPEC §3.2. Each line ends with LF; no trailing LF\n // beyond the last field's. Order is fixed.\n const lines = params.nostr_pk\n ? [\n `oc-lock:device-bind:${BINDING_VERSION_V3}`,\n `address: ${params.address}`,\n `device_pk: ${params.device_pk}`,\n `nostr_pk: ${params.nostr_pk}`,\n `device_id: ${params.device_id}`,\n `created_at: ${params.created_at}`,\n ]\n : [\n `oc-lock:device-bind:${BINDING_VERSION}`,\n `address: ${params.address}`,\n `device_pk: ${params.device_pk}`,\n `device_id: ${params.device_id}`,\n `created_at: ${params.created_at}`,\n ];\n return lines.join('\\n') + '\\n';\n}\n\nexport function buildRevocationStatement(params: {\n address: string;\n device_id: string;\n revoked_at: string;\n}): string {\n const lines = [\n `oc-lock:device-revoke:${BINDING_VERSION}`,\n `address: ${params.address}`,\n `device_id: ${params.device_id}`,\n `revoked_at: ${params.revoked_at}`,\n ];\n return lines.join('\\n') + '\\n';\n}\n\n// ---------------------------------------------------------------------------\n// Nostr event helpers\n// ---------------------------------------------------------------------------\n\nexport interface NostrEvent {\n id: string;\n pubkey: string;\n created_at: number;\n kind: number;\n tags: string[][];\n content: string;\n sig: string;\n}\n\n/**\n * Derive an ephemeral Nostr keypair from a device secret per SPEC §3.4.\n * Returns a 32-byte private key (hex).\n */\nexport function deriveNostrKey(deviceSk: Uint8Array): { nostrSk: string; nostrPk: string } {\n const sk = hkdfSha256(\n deviceSk,\n utf8Encode('oc-lock/v2/nostr-key'),\n utf8Encode('nostr-sk'),\n 32\n );\n const pk = schnorr.getPublicKey(sk);\n return { nostrSk: hexEncode(sk), nostrPk: hexEncode(pk) };\n}\n\n/**\n * Build a kind-30078 addressable event announcing a device binding.\n * The caller must supply the BIP-322 signature produced by the user's Bitcoin\n * wallet (we cannot sign BIP-322 in this package — that requires a wallet).\n */\nexport function buildDeviceEvent(params: {\n address: string;\n device_id: string;\n device_pk: string;\n bindingStatement: string;\n bindingSigBase64: string;\n createdAtUnix?: number;\n}): Omit<NostrEvent, 'id' | 'sig' | 'pubkey'> & { pubkey?: undefined } {\n const tags: string[][] = [\n ['d', `oc-lock:device:${params.address}`],\n ['addr', params.address],\n ['device_id', params.device_id],\n ['device_pk', params.device_pk],\n ['alg', 'x25519'],\n ['binding_sig', params.bindingSigBase64],\n ];\n const created_at = params.createdAtUnix ?? Math.floor(Date.now() / 1000);\n return {\n kind: DEVICE_KIND,\n tags,\n content: params.bindingStatement,\n created_at,\n } as unknown as Omit<NostrEvent, 'id' | 'sig' | 'pubkey'> & { pubkey?: undefined };\n}\n\n/**\n * Sign a Nostr event with a Schnorr key (derived via deriveNostrKey). Produces\n * the full NostrEvent with id and sig.\n */\nexport function signNostrEvent(\n event: Omit<NostrEvent, 'id' | 'sig'>,\n nostrSkHex: string\n): NostrEvent {\n const id = computeNostrEventId(event);\n const sk = hexDecode(nostrSkHex);\n const sig = schnorr.sign(hexDecode(id), sk);\n return {\n ...event,\n id,\n sig: hexEncode(sig),\n };\n}\n\nexport function computeNostrEventId(event: Omit<NostrEvent, 'id' | 'sig'>): string {\n // Per NIP-01: id = sha256(utf8(JSON.stringify([0, pubkey, created_at, kind, tags, content]))).\n // The array must be serialized in *exactly* this form with no whitespace.\n const serialized = JSON.stringify([\n 0,\n event.pubkey,\n event.created_at,\n event.kind,\n event.tags,\n event.content,\n ]);\n return hexEncode(sha256Bytes(utf8Encode(serialized)));\n}\n\n/**\n * Finalize a device event: derive Nostr key from device_sk, attach pubkey,\n * compute id, sign. Returns a fully-signed NIP-01 event ready to publish.\n */\nexport function finalizeDeviceEvent(params: {\n deviceSk: Uint8Array;\n address: string;\n device_id: string;\n device_pk: string;\n bindingStatement: string;\n bindingSigBase64: string;\n createdAtUnix?: number;\n}): NostrEvent {\n const { nostrSk, nostrPk } = deriveNostrKey(params.deviceSk);\n const base = buildDeviceEvent({\n address: params.address,\n device_id: params.device_id,\n device_pk: params.device_pk,\n bindingStatement: params.bindingStatement,\n bindingSigBase64: params.bindingSigBase64,\n createdAtUnix: params.createdAtUnix,\n });\n const withPubkey = { ...base, pubkey: nostrPk } as Omit<NostrEvent, 'id' | 'sig'>;\n return signNostrEvent(withPubkey, nostrSk);\n}\n\n// ---------------------------------------------------------------------------\n// Parsing\n// ---------------------------------------------------------------------------\n\nexport interface ParsedDeviceEvent {\n address: string;\n device_id: string;\n device_pk: string;\n bindingStatement: string;\n bindingSigBase64: string;\n revoked: boolean;\n createdAtUnix: number;\n nostrPubkey: string;\n eventId: string;\n /** 'v3' binds signed the Nostr pubkey; 'v2' binds and revocations did not. */\n bindingVersion: 'v2' | 'v3';\n}\n\nexport function parseDeviceEvent(event: NostrEvent): ParsedDeviceEvent {\n if (event.kind !== DEVICE_KIND) {\n throw new Error('not a device event (kind mismatch)');\n }\n const tag = (name: string): string | null => {\n for (const t of event.tags) {\n if (t[0] === name && t.length > 1) return t[1] ?? null;\n }\n return null;\n };\n const address = tag('addr');\n const device_id = tag('device_id');\n const device_pk = tag('device_pk');\n const binding_sig = tag('binding_sig');\n if (!address || !device_id || !device_pk || !binding_sig) {\n throw new Error('device event missing required tags');\n }\n\n // The BIP-322 signature covers `event.content` — the statement — and nothing\n // else. Tags are an index the relay lets anyone write, so a value read from a\n // tag has been authenticated by nobody.\n //\n // Skipping this check was a total confidentiality break, and it needed no key\n // material: republish a victim's device event with their real content and\n // real binding_sig, change only the `device_pk` TAG to your own X25519 key.\n // verifyBip322(content, sig, addr) passes — every signed byte is genuine —\n // and the sender then seals the content key to the attacker's key, because\n // the recipient record was built from the tag. Proven against the published\n // package before this existed.\n //\n // So the statement is the source of truth and the tags must agree with it.\n const signed = parseBindingStatement(event.content);\n if (!signed) {\n throw new Error('device event content is not a v2/v3 bind or revoke statement');\n }\n if (signed.address !== address) {\n throw new Error(\n `device event addr tag (${address}) does not match the signed statement (${signed.address})`,\n );\n }\n // SPEC §3.3 / §3.6: the record lives at `oc-lock:device:<addr>` or the\n // per-device `oc-lock:device:<addr>:<device_id>`. Lookups filter on `d`, so a\n // record for one address filed under another's slot must not parse.\n const d = tag('d');\n if (\n d !== `oc-lock:device:${signed.address}` &&\n d !== `oc-lock:device:${signed.address}:${signed.device_id}`\n ) {\n throw new Error(\n `device event d tag (${d}) is not the slot for the signed address (${signed.address})`,\n );\n }\n if (signed.device_id !== device_id) {\n throw new Error(\n `device event device_id tag (${device_id}) does not match the signed statement (${signed.device_id})`,\n );\n }\n if (signed.kind === 'bind') {\n if (signed.device_pk !== device_pk) {\n throw new Error(\n `device event device_pk tag does not match the signed statement — ` +\n `tag says ${device_pk}, the signature covers ${signed.device_pk}`,\n );\n }\n // A v3 statement names the one Nostr key allowed to carry it.\n if (signed.nostr_pk !== null && signed.nostr_pk !== event.pubkey) {\n throw new Error(\n `device event pubkey (${event.pubkey}) does not match the signed nostr_pk (${signed.nostr_pk})`,\n );\n }\n } else if (device_pk !== 'revoked') {\n throw new Error(\n `device event carries a revocation statement but its device_pk tag is ${device_pk}, not \"revoked\"`,\n );\n }\n\n return {\n address: signed.address,\n device_id: signed.device_id,\n // Take the key from the SIGNED bytes, never the tag.\n device_pk: signed.kind === 'bind' ? signed.device_pk : 'revoked',\n bindingStatement: event.content,\n bindingSigBase64: binding_sig,\n revoked: signed.kind === 'revoke',\n createdAtUnix: event.created_at,\n nostrPubkey: event.pubkey,\n eventId: event.id,\n bindingVersion: signed.kind === 'bind' && signed.nostr_pk !== null ? 'v3' : 'v2',\n };\n}\n\n/**\n * Read back the fields the signature actually commits to. Returns null for\n * anything that is not a well-formed v2/v3 bind or v2 revoke statement, which the caller\n * treats as an unusable record.\n */\nfunction parseBindingStatement(\n content: string,\n):\n | {\n kind: 'bind';\n address: string;\n device_pk: string;\n device_id: string;\n nostr_pk: string | null;\n }\n | { kind: 'revoke'; address: string; device_id: string }\n | null {\n const lines = content.split('\\n');\n const header = lines[0];\n const field = (name: string): string | null => {\n const prefix = `${name}: `;\n for (const l of lines.slice(1)) {\n if (l.startsWith(prefix)) return l.slice(prefix.length);\n }\n return null;\n };\n const address = field('address');\n const device_id = field('device_id');\n if (!address || !device_id) return null;\n\n if (header === `oc-lock:device-bind:${BINDING_VERSION}`) {\n const device_pk = field('device_pk');\n if (!device_pk) return null;\n return { kind: 'bind', address, device_pk, device_id, nostr_pk: null };\n }\n if (header === `oc-lock:device-bind:${BINDING_VERSION_V3}`) {\n const device_pk = field('device_pk');\n const nostr_pk = field('nostr_pk');\n if (!device_pk || !nostr_pk || !/^[0-9a-f]{64}$/.test(nostr_pk)) return null;\n return { kind: 'bind', address, device_pk, device_id, nostr_pk };\n }\n if (header === `oc-lock:device-revoke:${BINDING_VERSION}`) {\n return { kind: 'revoke', address, device_id };\n }\n return null;\n}\n\nexport { authorizedDevices } from './authorize.js';\nexport {\n DEFAULT_BIND_SKEW_MS,\n liveDevices,\n pendingDevices,\n revokedAtFor,\n signedTime,\n} from './live.js';\nexport type { LiveOptions } from './live.js';\n"]}
1
+ {"version":3,"sources":["../src/authorize.ts","../src/live.ts","../src/index.ts"],"names":[],"mappings":";;;;;;AAKO,IAAM,eAAA,GAAkB;AAiBxB,SAAS,iBAAA,CACZ,OAAA,EACA,IAAA,GAAyB,EAAC,EACP;AACnB,EAAA,KAAA,MAAW,CAAA,IAAK,MAAA,CAAO,IAAA,CAAK,IAAI,CAAA,EAAG;AAC/B,IAAA,IAAI,MAAM,KAAA,EAAO,MAAM,IAAI,SAAA,CAAU,CAAA,wCAAA,EAA2C,CAAC,CAAA,CAAA,CAAG,CAAA;AAAA,EACxF;AACA,EAAA,MAAM,QAAA,GAAA,CAAY,IAAA,CAAK,GAAA,oBAAO,IAAI,IAAA,IAAQ,OAAA,EAAQ,GAAI,IAAA,CAAK,KAAA,CAAM,eAAe,CAAA;AAChF,EAAA,MAAM,KAAA,uBAAY,GAAA,EAAY;AAC9B,EAAA,MAAM,SAAA,uBAAgB,GAAA,EAAyB;AAC/C,EAAA,KAAA,MAAW,KAAK,OAAA,EAAS;AACrB,IAAA,IAAI,EAAE,OAAA,EAAS;AACf,IAAA,IAAI,CAAA,CAAE,mBAAmB,IAAA,EAAM;AAC3B,MAAA,KAAA,CAAM,GAAA,CAAI,EAAE,SAAS,CAAA;AAAA,IACzB,CAAA,MAAO;AACH,MAAA,MAAM,MAAM,SAAA,CAAU,GAAA,CAAI,EAAE,SAAS,CAAA,wBAAS,GAAA,EAAY;AAC1D,MAAA,GAAA,CAAI,GAAA,CAAI,EAAE,WAAW,CAAA;AACrB,MAAA,SAAA,CAAU,GAAA,CAAI,CAAA,CAAE,SAAA,EAAW,GAAG,CAAA;AAAA,IAClC;AAAA,EACJ;AACA,EAAA,OAAO,OAAA,CAAQ,MAAA,CAAO,CAAC,CAAA,KAAM;AACzB,IAAA,IAAI,CAAA,CAAE,SAAS,OAAO,KAAA;AACtB,IAAA,IAAI,CAAA,CAAE,cAAA,KAAmB,IAAA,EAAM,OAAO,IAAA;AACtC,IAAA,IAAI,CAAC,QAAA,IAAY,KAAA,CAAM,IAAI,CAAA,CAAE,SAAS,GAAG,OAAO,KAAA;AAChD,IAAA,OAAO,SAAA,CAAU,GAAA,CAAI,CAAA,CAAE,SAAS,GAAG,IAAA,KAAS,CAAA;AAAA,EAChD,CAAC,CAAA;AACL;;;AC1CO,SAAS,UAAA,CAAW,WAAmB,KAAA,EAAmD;AAC7F,EAAA,MAAM,IAAA,GAAO,SAAA,CAAU,KAAA,CAAM,IAAI,CAAA,CAAE,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,UAAA,CAAW,CAAA,EAAG,KAAK,IAAI,CAAC,CAAA;AACzE,EAAA,IAAI,CAAC,MAAM,OAAO,IAAA;AAClB,EAAA,MAAM,CAAA,GAAI,KAAK,KAAA,CAAM,IAAA,CAAK,MAAM,KAAA,CAAM,MAAA,GAAS,CAAC,CAAC,CAAA;AACjD,EAAA,OAAO,MAAA,CAAO,QAAA,CAAS,CAAC,CAAA,GAAI,CAAA,GAAI,IAAA;AACpC;AAGO,IAAM,uBAAuB,EAAA,GAAK;AAUzC,SAAS,SAAS,OAAA,EAA4D;AAC1E,EAAA,MAAM,GAAA,GAAM,CAAC,CAAA,KAAyB,CAAA,EAAG,EAAE,OAAO;AAAA,EAAK,EAAE,SAAS,CAAA,CAAA;AAClE,EAAA,MAAM,SAAA,uBAAgB,GAAA,EAAoB;AAC1C,EAAA,KAAA,MAAW,KAAK,OAAA,EAAS;AACrB,IAAA,IAAI,CAAC,EAAE,OAAA,EAAS;AAChB,IAAA,MAAM,EAAA,GAAK,UAAA,CAAW,CAAA,CAAE,gBAAA,EAAkB,YAAY,CAAA;AACtD,IAAA,IAAI,OAAO,IAAA,EAAM,SAAA,CAAU,GAAA,CAAI,GAAA,CAAI,CAAC,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,SAAA,CAAU,IAAI,GAAA,CAAI,CAAC,CAAC,CAAA,IAAK,EAAA,EAAI,EAAE,CAAC,CAAA;AAAA,EACpF;AACA,EAAA,OAAO,OAAA,CAAQ,MAAA,CAAO,CAAC,CAAA,KAAM;AACzB,IAAA,IAAI,CAAA,CAAE,SAAS,OAAO,KAAA;AACtB,IAAA,MAAM,KAAA,GAAQ,UAAA,CAAW,CAAA,CAAE,gBAAA,EAAkB,YAAY,CAAA;AACzD,IAAA,IAAI,KAAA,KAAU,MAAM,OAAO,KAAA;AAC3B,IAAA,MAAM,GAAA,GAAM,SAAA,CAAU,GAAA,CAAI,GAAA,CAAI,CAAC,CAAC,CAAA;AAChC,IAAA,OAAO,GAAA,KAAQ,UAAa,KAAA,GAAQ,GAAA;AAAA,EACxC,CAAC,CAAA;AACL;AAEA,IAAM,OAAA,GAAU,CAAC,IAAA,KAAA,CACZ,IAAA,CAAK,GAAA,oBAAO,IAAI,IAAA,EAAK,EAAG,OAAA,EAAQ,IAAK,IAAA,CAAK,SAAA,IAAa,oBAAA,CAAA;AAcrD,SAAS,WAAA,CACZ,OAAA,EACA,IAAA,GAAoB,EAAC,EACF;AACnB,EAAA,MAAM,KAAA,GAAQ,QAAQ,IAAI,CAAA;AAC1B,EAAA,OAAO,QAAA,CAAS,OAAO,CAAA,CAAE,MAAA,CAAO,CAAC,CAAA,KAAM,UAAA,CAAW,CAAA,CAAE,gBAAA,EAAkB,YAAY,CAAA,IAAM,KAAK,CAAA;AACjG;AAOO,SAAS,cAAA,CACZ,OAAA,EACA,IAAA,GAAoB,EAAC,EACF;AACnB,EAAA,MAAM,KAAA,GAAQ,QAAQ,IAAI,CAAA;AAC1B,EAAA,OAAO,QAAA,CAAS,OAAO,CAAA,CAAE,MAAA,CAAO,CAAC,CAAA,KAAM,UAAA,CAAW,CAAA,CAAE,gBAAA,EAAkB,YAAY,CAAA,GAAK,KAAK,CAAA;AAChG;AAOO,SAAS,aACZ,OAAA,EACA,OAAA,EACA,UACA,GAAA,mBAAY,IAAI,MAAK,EACf;AACN,EAAA,IAAI,EAAA,GAAK,IAAI,OAAA,EAAQ;AACrB,EAAA,KAAA,MAAW,KAAK,OAAA,EAAS;AACrB,IAAA,IAAI,EAAE,OAAA,IAAW,CAAA,CAAE,YAAY,OAAA,IAAW,CAAA,CAAE,cAAc,QAAA,EAAU;AACpE,IAAA,MAAM,CAAA,GAAI,UAAA,CAAW,CAAA,CAAE,gBAAA,EAAkB,YAAY,CAAA;AACrD,IAAA,IAAI,CAAA,KAAM,IAAA,IAAQ,CAAA,GAAI,EAAA,EAAI,EAAA,GAAK,CAAA;AAAA,EACnC;AACA,EAAA,OAAO,IAAI,IAAA,CAAK,EAAE,CAAA,CAAE,WAAA,EAAY;AACpC;;;ACjFO,IAAM,WAAA,GAAc;AACpB,IAAM,eAAA,GAAkB;AAExB,IAAM,kBAAA,GAAqB;AAS3B,SAAS,iBAAA,GAAmC;AAC/C,EAAA,MAAM,KAAK,qBAAA,EAAsB;AACjC,EAAA,MAAM,aAAA,GAAgB,aAAa,EAAE,CAAA;AACrC,EAAA,OAAO;AAAA,IACH,SAAA,EAAW,UAAU,aAAa,CAAA;AAAA,IAClC,SAAA,EAAW,SAAA,CAAU,EAAA,CAAG,MAAM,CAAA;AAAA,IAC9B,WAAW,EAAA,CAAG,MAAA;AAAA,IACd,UAAA,EAAA,iBAAY,IAAI,IAAA,EAAK,EAAE,WAAA;AAAY,GACvC;AACJ;AAEO,SAAS,sBAAsB,MAAA,EAO3B;AAGP,EAAA,MAAM,KAAA,GAAQ,OAAO,QAAA,GACf;AAAA,IACI,uBAAuB,kBAAkB,CAAA,CAAA;AAAA,IACzC,CAAA,SAAA,EAAY,OAAO,OAAO,CAAA,CAAA;AAAA,IAC1B,CAAA,WAAA,EAAc,OAAO,SAAS,CAAA,CAAA;AAAA,IAC9B,CAAA,UAAA,EAAa,OAAO,QAAQ,CAAA,CAAA;AAAA,IAC5B,CAAA,WAAA,EAAc,OAAO,SAAS,CAAA,CAAA;AAAA,IAC9B,CAAA,YAAA,EAAe,OAAO,UAAU,CAAA;AAAA,GACpC,GACA;AAAA,IACI,uBAAuB,eAAe,CAAA,CAAA;AAAA,IACtC,CAAA,SAAA,EAAY,OAAO,OAAO,CAAA,CAAA;AAAA,IAC1B,CAAA,WAAA,EAAc,OAAO,SAAS,CAAA,CAAA;AAAA,IAC9B,CAAA,WAAA,EAAc,OAAO,SAAS,CAAA,CAAA;AAAA,IAC9B,CAAA,YAAA,EAAe,OAAO,UAAU,CAAA;AAAA,GACpC;AACN,EAAA,OAAO,KAAA,CAAM,IAAA,CAAK,IAAI,CAAA,GAAI,IAAA;AAC9B;AAEO,SAAS,yBAAyB,MAAA,EAI9B;AACP,EAAA,MAAM,KAAA,GAAQ;AAAA,IACV,yBAAyB,eAAe,CAAA,CAAA;AAAA,IACxC,CAAA,SAAA,EAAY,OAAO,OAAO,CAAA,CAAA;AAAA,IAC1B,CAAA,WAAA,EAAc,OAAO,SAAS,CAAA,CAAA;AAAA,IAC9B,CAAA,YAAA,EAAe,OAAO,UAAU,CAAA;AAAA,GACpC;AACA,EAAA,OAAO,KAAA,CAAM,IAAA,CAAK,IAAI,CAAA,GAAI,IAAA;AAC9B;AAoBO,SAAS,eAAe,QAAA,EAA4D;AACvF,EAAA,MAAM,EAAA,GAAK,UAAA;AAAA,IACP,QAAA;AAAA,IACA,WAAW,sBAAsB,CAAA;AAAA,IACjC,WAAW,UAAU,CAAA;AAAA,IACrB;AAAA,GACJ;AACA,EAAA,MAAM,EAAA,GAAK,OAAA,CAAQ,YAAA,CAAa,EAAE,CAAA;AAClC,EAAA,OAAO,EAAE,SAAS,SAAA,CAAU,EAAE,GAAG,OAAA,EAAS,SAAA,CAAU,EAAE,CAAA,EAAE;AAC5D;AAOO,SAAS,iBAAiB,MAAA,EAOsC;AACnE,EAAA,MAAM,IAAA,GAAmB;AAAA,IACrB,CAAC,GAAA,EAAK,CAAA,eAAA,EAAkB,MAAA,CAAO,OAAO,CAAA,CAAE,CAAA;AAAA,IACxC,CAAC,MAAA,EAAQ,MAAA,CAAO,OAAO,CAAA;AAAA,IACvB,CAAC,WAAA,EAAa,MAAA,CAAO,SAAS,CAAA;AAAA,IAC9B,CAAC,WAAA,EAAa,MAAA,CAAO,SAAS,CAAA;AAAA,IAC9B,CAAC,OAAO,QAAQ,CAAA;AAAA,IAChB,CAAC,aAAA,EAAe,MAAA,CAAO,gBAAgB;AAAA,GAC3C;AACA,EAAA,MAAM,UAAA,GAAa,OAAO,aAAA,IAAiB,IAAA,CAAK,MAAM,IAAA,CAAK,GAAA,KAAQ,GAAI,CAAA;AACvE,EAAA,OAAO;AAAA,IACH,IAAA,EAAM,WAAA;AAAA,IACN,IAAA;AAAA,IACA,SAAS,MAAA,CAAO,gBAAA;AAAA,IAChB;AAAA,GACJ;AACJ;AAMO,SAAS,cAAA,CACZ,OACA,UAAA,EACU;AACV,EAAA,MAAM,EAAA,GAAK,oBAAoB,KAAK,CAAA;AACpC,EAAA,MAAM,EAAA,GAAK,UAAU,UAAU,CAAA;AAC/B,EAAA,MAAM,MAAM,OAAA,CAAQ,IAAA,CAAK,SAAA,CAAU,EAAE,GAAG,EAAE,CAAA;AAC1C,EAAA,OAAO;AAAA,IACH,GAAG,KAAA;AAAA,IACH,EAAA;AAAA,IACA,GAAA,EAAK,UAAU,GAAG;AAAA,GACtB;AACJ;AAEO,SAAS,oBAAoB,KAAA,EAA+C;AAG/E,EAAA,MAAM,UAAA,GAAa,KAAK,SAAA,CAAU;AAAA,IAC9B,CAAA;AAAA,IACA,KAAA,CAAM,MAAA;AAAA,IACN,KAAA,CAAM,UAAA;AAAA,IACN,KAAA,CAAM,IAAA;AAAA,IACN,KAAA,CAAM,IAAA;AAAA,IACN,KAAA,CAAM;AAAA,GACT,CAAA;AACD,EAAA,OAAO,SAAA,CAAU,WAAA,CAAY,UAAA,CAAW,UAAU,CAAC,CAAC,CAAA;AACxD;AAMO,SAAS,oBAAoB,MAAA,EAQrB;AACX,EAAA,MAAM,EAAE,OAAA,EAAS,OAAA,EAAQ,GAAI,cAAA,CAAe,OAAO,QAAQ,CAAA;AAC3D,EAAA,MAAM,OAAO,gBAAA,CAAiB;AAAA,IAC1B,SAAS,MAAA,CAAO,OAAA;AAAA,IAChB,WAAW,MAAA,CAAO,SAAA;AAAA,IAClB,WAAW,MAAA,CAAO,SAAA;AAAA,IAClB,kBAAkB,MAAA,CAAO,gBAAA;AAAA,IACzB,kBAAkB,MAAA,CAAO,gBAAA;AAAA,IACzB,eAAe,MAAA,CAAO;AAAA,GACzB,CAAA;AACD,EAAA,MAAM,UAAA,GAAa,EAAE,GAAG,IAAA,EAAM,QAAQ,OAAA,EAAQ;AAC9C,EAAA,OAAO,cAAA,CAAe,YAAY,OAAO,CAAA;AAC7C;AAoBO,SAAS,iBAAiB,KAAA,EAAsC;AACnE,EAAA,IAAI,KAAA,CAAM,SAAS,WAAA,EAAa;AAC5B,IAAA,MAAM,IAAI,MAAM,oCAAoC,CAAA;AAAA,EACxD;AACA,EAAA,MAAM,GAAA,GAAM,CAAC,IAAA,KAAgC;AACzC,IAAA,KAAA,MAAW,CAAA,IAAK,MAAM,IAAA,EAAM;AACxB,MAAA,IAAI,CAAA,CAAE,CAAC,CAAA,KAAM,IAAA,IAAQ,CAAA,CAAE,SAAS,CAAA,EAAG,OAAO,CAAA,CAAE,CAAC,CAAA,IAAK,IAAA;AAAA,IACtD;AACA,IAAA,OAAO,IAAA;AAAA,EACX,CAAA;AACA,EAAA,MAAM,OAAA,GAAU,IAAI,MAAM,CAAA;AAC1B,EAAA,MAAM,SAAA,GAAY,IAAI,WAAW,CAAA;AACjC,EAAA,MAAM,SAAA,GAAY,IAAI,WAAW,CAAA;AACjC,EAAA,MAAM,WAAA,GAAc,IAAI,aAAa,CAAA;AACrC,EAAA,IAAI,CAAC,OAAA,IAAW,CAAC,aAAa,CAAC,SAAA,IAAa,CAAC,WAAA,EAAa;AACtD,IAAA,MAAM,IAAI,MAAM,oCAAoC,CAAA;AAAA,EACxD;AAeA,EAAA,MAAM,MAAA,GAAS,qBAAA,CAAsB,KAAA,CAAM,OAAO,CAAA;AAClD,EAAA,IAAI,CAAC,MAAA,EAAQ;AACT,IAAA,MAAM,IAAI,MAAM,8DAA8D,CAAA;AAAA,EAClF;AACA,EAAA,IAAI,MAAA,CAAO,YAAY,OAAA,EAAS;AAC5B,IAAA,MAAM,IAAI,KAAA;AAAA,MACN,CAAA,uBAAA,EAA0B,OAAO,CAAA,uCAAA,EAA0C,MAAA,CAAO,OAAO,CAAA,CAAA;AAAA,KAC7F;AAAA,EACJ;AAIA,EAAA,MAAM,CAAA,GAAI,IAAI,GAAG,CAAA;AACjB,EAAA,IACI,CAAA,KAAM,CAAA,eAAA,EAAkB,MAAA,CAAO,OAAO,CAAA,CAAA,IACtC,CAAA,KAAM,CAAA,eAAA,EAAkB,MAAA,CAAO,OAAO,CAAA,CAAA,EAAI,MAAA,CAAO,SAAS,CAAA,CAAA,EAC5D;AACE,IAAA,MAAM,IAAI,KAAA;AAAA,MACN,CAAA,oBAAA,EAAuB,CAAC,CAAA,0CAAA,EAA6C,MAAA,CAAO,OAAO,CAAA,CAAA;AAAA,KACvF;AAAA,EACJ;AACA,EAAA,IAAI,MAAA,CAAO,cAAc,SAAA,EAAW;AAChC,IAAA,MAAM,IAAI,KAAA;AAAA,MACN,CAAA,4BAAA,EAA+B,SAAS,CAAA,uCAAA,EAA0C,MAAA,CAAO,SAAS,CAAA,CAAA;AAAA,KACtG;AAAA,EACJ;AACA,EAAA,IAAI,MAAA,CAAO,SAAS,MAAA,EAAQ;AACxB,IAAA,IAAI,MAAA,CAAO,cAAc,SAAA,EAAW;AAChC,MAAA,MAAM,IAAI,KAAA;AAAA,QACN,CAAA,+EAAA,EACgB,SAAS,CAAA,uBAAA,EAA0B,MAAA,CAAO,SAAS,CAAA;AAAA,OACvE;AAAA,IACJ;AAEA,IAAA,IAAI,OAAO,QAAA,KAAa,IAAA,IAAQ,MAAA,CAAO,QAAA,KAAa,MAAM,MAAA,EAAQ;AAC9D,MAAA,MAAM,IAAI,KAAA;AAAA,QACN,CAAA,qBAAA,EAAwB,KAAA,CAAM,MAAM,CAAA,sCAAA,EAAyC,OAAO,QAAQ,CAAA,CAAA;AAAA,OAChG;AAAA,IACJ;AAAA,EACJ,CAAA,MAAA,IAAW,cAAc,SAAA,EAAW;AAChC,IAAA,MAAM,IAAI,KAAA;AAAA,MACN,wEAAwE,SAAS,CAAA,eAAA;AAAA,KACrF;AAAA,EACJ;AAEA,EAAA,OAAO;AAAA,IACH,SAAS,MAAA,CAAO,OAAA;AAAA,IAChB,WAAW,MAAA,CAAO,SAAA;AAAA;AAAA,IAElB,SAAA,EAAW,MAAA,CAAO,IAAA,KAAS,MAAA,GAAS,OAAO,SAAA,GAAY,SAAA;AAAA,IACvD,kBAAkB,KAAA,CAAM,OAAA;AAAA,IACxB,gBAAA,EAAkB,WAAA;AAAA,IAClB,OAAA,EAAS,OAAO,IAAA,KAAS,QAAA;AAAA,IACzB,eAAe,KAAA,CAAM,UAAA;AAAA,IACrB,aAAa,KAAA,CAAM,MAAA;AAAA,IACnB,SAAS,KAAA,CAAM,EAAA;AAAA,IACf,gBAAgB,MAAA,CAAO,IAAA,KAAS,UAAU,MAAA,CAAO,QAAA,KAAa,OAAO,IAAA,GAAO;AAAA,GAChF;AACJ;AAOA,SAAS,sBACL,OAAA,EAUO;AACP,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,KAAA,CAAM,IAAI,CAAA;AAChC,EAAA,MAAM,MAAA,GAAS,MAAM,CAAC,CAAA;AACtB,EAAA,MAAM,KAAA,GAAQ,CAAC,IAAA,KAAgC;AAC3C,IAAA,MAAM,MAAA,GAAS,GAAG,IAAI,CAAA,EAAA,CAAA;AACtB,IAAA,KAAA,MAAW,CAAA,IAAK,KAAA,CAAM,KAAA,CAAM,CAAC,CAAA,EAAG;AAC5B,MAAA,IAAI,CAAA,CAAE,WAAW,MAAM,CAAA,SAAU,CAAA,CAAE,KAAA,CAAM,OAAO,MAAM,CAAA;AAAA,IAC1D;AACA,IAAA,OAAO,IAAA;AAAA,EACX,CAAA;AACA,EAAA,MAAM,OAAA,GAAU,MAAM,SAAS,CAAA;AAC/B,EAAA,MAAM,SAAA,GAAY,MAAM,WAAW,CAAA;AACnC,EAAA,IAAI,CAAC,OAAA,IAAW,CAAC,SAAA,EAAW,OAAO,IAAA;AAEnC,EAAA,IAAI,MAAA,KAAW,CAAA,oBAAA,EAAuB,eAAe,CAAA,CAAA,EAAI;AACrD,IAAA,MAAM,SAAA,GAAY,MAAM,WAAW,CAAA;AACnC,IAAA,IAAI,CAAC,WAAW,OAAO,IAAA;AACvB,IAAA,OAAO,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,SAAA,EAAW,SAAA,EAAW,UAAU,IAAA,EAAK;AAAA,EACzE;AACA,EAAA,IAAI,MAAA,KAAW,CAAA,oBAAA,EAAuB,kBAAkB,CAAA,CAAA,EAAI;AACxD,IAAA,MAAM,SAAA,GAAY,MAAM,WAAW,CAAA;AACnC,IAAA,MAAM,QAAA,GAAW,MAAM,UAAU,CAAA;AACjC,IAAA,IAAI,CAAC,aAAa,CAAC,QAAA,IAAY,CAAC,gBAAA,CAAiB,IAAA,CAAK,QAAQ,CAAA,EAAG,OAAO,IAAA;AACxE,IAAA,OAAO,EAAE,IAAA,EAAM,MAAA,EAAQ,OAAA,EAAS,SAAA,EAAW,WAAW,QAAA,EAAS;AAAA,EACnE;AACA,EAAA,IAAI,MAAA,KAAW,CAAA,sBAAA,EAAyB,eAAe,CAAA,CAAA,EAAI;AACvD,IAAA,OAAO,EAAE,IAAA,EAAM,QAAA,EAAU,OAAA,EAAS,SAAA,EAAU;AAAA,EAChD;AACA,EAAA,OAAO,IAAA;AACX","file":"index.mjs","sourcesContent":["// Which Nostr pubkeys a set of device records authorizes. See SPEC §3.4.\n\nimport type { ParsedDeviceEvent } from './index.js';\n\n/** From this instant, by the reader's clock, a v2 record authorizes no Nostr key (SPEC §3.4). */\nexport const V2_INBOX_SUNSET = '2027-01-21T00:00:00Z';\n\nexport interface AuthorizeOptions {\n /** The reader's clock. Default: the current time. */\n now?: Date;\n}\n\n/**\n * Select the device records whose `nostrPubkey` may act for their address.\n *\n * `records` MUST already be BIP-322-verified, non-revoked bind records for one\n * address. A v3 record signs its Nostr pubkey, so it is always authorized. A v2\n * record does not, so its pubkey is only the event author's claim. Before\n * `V2_INBOX_SUNSET` it is authorized only while its `device_id` has no v3 record\n * and appears under exactly one Nostr pubkey; from then on it authorizes no key.\n * A v2 bind stays live for sealing to its `device_pk` either way (liveDevices).\n */\nexport function authorizedDevices(\n records: readonly ParsedDeviceEvent[],\n opts: AuthorizeOptions = {}\n): ParsedDeviceEvent[] {\n for (const k of Object.keys(opts)) {\n if (k !== 'now') throw new TypeError(`authorizedDevices does not read option \"${k}\"`);\n }\n const v2Counts = (opts.now ?? new Date()).getTime() < Date.parse(V2_INBOX_SUNSET);\n const v3Ids = new Set<string>();\n const v2Pubkeys = new Map<string, Set<string>>();\n for (const r of records) {\n if (r.revoked) continue;\n if (r.bindingVersion === 'v3') {\n v3Ids.add(r.device_id);\n } else {\n const set = v2Pubkeys.get(r.device_id) ?? new Set<string>();\n set.add(r.nostrPubkey);\n v2Pubkeys.set(r.device_id, set);\n }\n }\n return records.filter((r) => {\n if (r.revoked) return false;\n if (r.bindingVersion === 'v3') return true;\n if (!v2Counts || v3Ids.has(r.device_id)) return false;\n return v2Pubkeys.get(r.device_id)?.size === 1;\n });\n}\n","// Which device records are still live once revocations and the reader's clock\n// are applied. See OC Lock SPEC §3.5 and LIFECYCLE §1.1.\n\nimport type { ParsedDeviceEvent } from './index.js';\n\n/** A timestamp line from a signed device statement, as epoch ms, or null. */\nexport function signedTime(statement: string, field: 'created_at' | 'revoked_at'): number | null {\n const line = statement.split('\\n').find((l) => l.startsWith(`${field}: `));\n if (!line) return null;\n const t = Date.parse(line.slice(field.length + 2));\n return Number.isFinite(t) ? t : null;\n}\n\n/** How far a bind's signed `created_at` may run ahead of the reader's clock and still count. */\nexport const DEFAULT_BIND_SKEW_MS = 10 * 60_000;\n\nexport interface LiveOptions {\n /** The reader's clock. Default: the current time. */\n now?: Date;\n /** Default DEFAULT_BIND_SKEW_MS. */\n maxSkewMs?: number;\n}\n\n/** Binds that no revocation retires, before the reader's clock is applied. */\nfunction standing(records: readonly ParsedDeviceEvent[]): ParsedDeviceEvent[] {\n const key = (r: ParsedDeviceEvent) => `${r.address}\\n${r.device_id}`;\n const revokedAt = new Map<string, number>();\n for (const r of records) {\n if (!r.revoked) continue;\n const at = signedTime(r.bindingStatement, 'revoked_at');\n if (at !== null) revokedAt.set(key(r), Math.max(revokedAt.get(key(r)) ?? at, at));\n }\n return records.filter((r) => {\n if (r.revoked) return false;\n const bound = signedTime(r.bindingStatement, 'created_at');\n if (bound === null) return false;\n const cut = revokedAt.get(key(r));\n return cut === undefined || bound > cut;\n });\n}\n\nconst limitOf = (opts: LiveOptions) =>\n (opts.now ?? new Date()).getTime() + (opts.maxSkewMs ?? DEFAULT_BIND_SKEW_MS);\n\n/**\n * The bind records still live after revocations.\n *\n * `records` MUST already be BIP-322-verified, both binds and revocations: a\n * revocation's authority is the address's signature, so one that was not\n * verified must not be passed in. A revocation retires every bind of its\n * `(address, device_id)` whose signed `created_at` is not later than its signed\n * `revoked_at`, whichever Nostr key carried either record and whichever relay\n * served it. A bind signed after the revocation stays live. A bind whose signed\n * `created_at` is later than the reader's clock plus `maxSkewMs` is not live yet\n * (see pendingDevices).\n */\nexport function liveDevices(\n records: readonly ParsedDeviceEvent[],\n opts: LiveOptions = {}\n): ParsedDeviceEvent[] {\n const limit = limitOf(opts);\n return standing(records).filter((r) => signedTime(r.bindingStatement, 'created_at')! <= limit);\n}\n\n/**\n * Binds that no revocation retires but that are dated later than the reader's\n * clock allows: nobody seals to them yet, and a device list shows them so they\n * can be revoked before their date.\n */\nexport function pendingDevices(\n records: readonly ParsedDeviceEvent[],\n opts: LiveOptions = {}\n): ParsedDeviceEvent[] {\n const limit = limitOf(opts);\n return standing(records).filter((r) => signedTime(r.bindingStatement, 'created_at')! > limit);\n}\n\n/**\n * The `revoked_at` to sign when revoking `(address, deviceId)`: the current\n * time, or the latest signed `created_at` among that device's binds in\n * `records` if later, so the revocation also retires a bind dated ahead.\n */\nexport function revokedAtFor(\n records: readonly ParsedDeviceEvent[],\n address: string,\n deviceId: string,\n now: Date = new Date()\n): string {\n let at = now.getTime();\n for (const r of records) {\n if (r.revoked || r.address !== address || r.device_id !== deviceId) continue;\n const t = signedTime(r.bindingStatement, 'created_at');\n if (t !== null && t > at) at = t;\n }\n return new Date(at).toISOString();\n}\n","// Device key binding and Nostr directory publication.\n// See SPEC.md §3.\n\nimport { schnorr } from '@noble/curves/secp256k1';\nimport {\n generateX25519KeyPair,\n hexDecode,\n hexEncode,\n hkdfSha256,\n randomBytesN,\n sha256Bytes,\n utf8Encode,\n} from '@orangecheck/lock-crypto';\n\nexport const DEVICE_KIND = 30078;\nexport const BINDING_VERSION = 'v2';\n/** Bind statement version that also signs the device's Nostr inbox pubkey. */\nexport const BINDING_VERSION_V3 = 'v3';\n\nexport interface DeviceKeyPair {\n device_id: string;\n device_pk: string; // 32-byte hex\n device_sk: Uint8Array; // 32 bytes\n created_at: string; // iso8601\n}\n\nexport function generateDeviceKey(): DeviceKeyPair {\n const kp = generateX25519KeyPair();\n const deviceIdBytes = randomBytesN(16);\n return {\n device_id: hexEncode(deviceIdBytes),\n device_pk: hexEncode(kp.public),\n device_sk: kp.secret,\n created_at: new Date().toISOString(),\n };\n}\n\nexport function buildBindingStatement(params: {\n address: string;\n device_pk: string;\n device_id: string;\n created_at: string;\n /** The device's Nostr pubkey (deriveNostrKey). Present ⇒ a v3 statement. */\n nostr_pk?: string;\n}): string {\n // Exact byte layout per SPEC §3.2. Each line ends with LF; no trailing LF\n // beyond the last field's. Order is fixed.\n const lines = params.nostr_pk\n ? [\n `oc-lock:device-bind:${BINDING_VERSION_V3}`,\n `address: ${params.address}`,\n `device_pk: ${params.device_pk}`,\n `nostr_pk: ${params.nostr_pk}`,\n `device_id: ${params.device_id}`,\n `created_at: ${params.created_at}`,\n ]\n : [\n `oc-lock:device-bind:${BINDING_VERSION}`,\n `address: ${params.address}`,\n `device_pk: ${params.device_pk}`,\n `device_id: ${params.device_id}`,\n `created_at: ${params.created_at}`,\n ];\n return lines.join('\\n') + '\\n';\n}\n\nexport function buildRevocationStatement(params: {\n address: string;\n device_id: string;\n revoked_at: string;\n}): string {\n const lines = [\n `oc-lock:device-revoke:${BINDING_VERSION}`,\n `address: ${params.address}`,\n `device_id: ${params.device_id}`,\n `revoked_at: ${params.revoked_at}`,\n ];\n return lines.join('\\n') + '\\n';\n}\n\n// ---------------------------------------------------------------------------\n// Nostr event helpers\n// ---------------------------------------------------------------------------\n\nexport interface NostrEvent {\n id: string;\n pubkey: string;\n created_at: number;\n kind: number;\n tags: string[][];\n content: string;\n sig: string;\n}\n\n/**\n * Derive an ephemeral Nostr keypair from a device secret per SPEC §3.4.\n * Returns a 32-byte private key (hex).\n */\nexport function deriveNostrKey(deviceSk: Uint8Array): { nostrSk: string; nostrPk: string } {\n const sk = hkdfSha256(\n deviceSk,\n utf8Encode('oc-lock/v2/nostr-key'),\n utf8Encode('nostr-sk'),\n 32\n );\n const pk = schnorr.getPublicKey(sk);\n return { nostrSk: hexEncode(sk), nostrPk: hexEncode(pk) };\n}\n\n/**\n * Build a kind-30078 addressable event announcing a device binding.\n * The caller must supply the BIP-322 signature produced by the user's Bitcoin\n * wallet (we cannot sign BIP-322 in this package — that requires a wallet).\n */\nexport function buildDeviceEvent(params: {\n address: string;\n device_id: string;\n device_pk: string;\n bindingStatement: string;\n bindingSigBase64: string;\n createdAtUnix?: number;\n}): Omit<NostrEvent, 'id' | 'sig' | 'pubkey'> & { pubkey?: undefined } {\n const tags: string[][] = [\n ['d', `oc-lock:device:${params.address}`],\n ['addr', params.address],\n ['device_id', params.device_id],\n ['device_pk', params.device_pk],\n ['alg', 'x25519'],\n ['binding_sig', params.bindingSigBase64],\n ];\n const created_at = params.createdAtUnix ?? Math.floor(Date.now() / 1000);\n return {\n kind: DEVICE_KIND,\n tags,\n content: params.bindingStatement,\n created_at,\n } as unknown as Omit<NostrEvent, 'id' | 'sig' | 'pubkey'> & { pubkey?: undefined };\n}\n\n/**\n * Sign a Nostr event with a Schnorr key (derived via deriveNostrKey). Produces\n * the full NostrEvent with id and sig.\n */\nexport function signNostrEvent(\n event: Omit<NostrEvent, 'id' | 'sig'>,\n nostrSkHex: string\n): NostrEvent {\n const id = computeNostrEventId(event);\n const sk = hexDecode(nostrSkHex);\n const sig = schnorr.sign(hexDecode(id), sk);\n return {\n ...event,\n id,\n sig: hexEncode(sig),\n };\n}\n\nexport function computeNostrEventId(event: Omit<NostrEvent, 'id' | 'sig'>): string {\n // Per NIP-01: id = sha256(utf8(JSON.stringify([0, pubkey, created_at, kind, tags, content]))).\n // The array must be serialized in *exactly* this form with no whitespace.\n const serialized = JSON.stringify([\n 0,\n event.pubkey,\n event.created_at,\n event.kind,\n event.tags,\n event.content,\n ]);\n return hexEncode(sha256Bytes(utf8Encode(serialized)));\n}\n\n/**\n * Finalize a device event: derive Nostr key from device_sk, attach pubkey,\n * compute id, sign. Returns a fully-signed NIP-01 event ready to publish.\n */\nexport function finalizeDeviceEvent(params: {\n deviceSk: Uint8Array;\n address: string;\n device_id: string;\n device_pk: string;\n bindingStatement: string;\n bindingSigBase64: string;\n createdAtUnix?: number;\n}): NostrEvent {\n const { nostrSk, nostrPk } = deriveNostrKey(params.deviceSk);\n const base = buildDeviceEvent({\n address: params.address,\n device_id: params.device_id,\n device_pk: params.device_pk,\n bindingStatement: params.bindingStatement,\n bindingSigBase64: params.bindingSigBase64,\n createdAtUnix: params.createdAtUnix,\n });\n const withPubkey = { ...base, pubkey: nostrPk } as Omit<NostrEvent, 'id' | 'sig'>;\n return signNostrEvent(withPubkey, nostrSk);\n}\n\n// ---------------------------------------------------------------------------\n// Parsing\n// ---------------------------------------------------------------------------\n\nexport interface ParsedDeviceEvent {\n address: string;\n device_id: string;\n device_pk: string;\n bindingStatement: string;\n bindingSigBase64: string;\n revoked: boolean;\n createdAtUnix: number;\n nostrPubkey: string;\n eventId: string;\n /** 'v3' binds signed the Nostr pubkey; 'v2' binds and revocations did not. */\n bindingVersion: 'v2' | 'v3';\n}\n\nexport function parseDeviceEvent(event: NostrEvent): ParsedDeviceEvent {\n if (event.kind !== DEVICE_KIND) {\n throw new Error('not a device event (kind mismatch)');\n }\n const tag = (name: string): string | null => {\n for (const t of event.tags) {\n if (t[0] === name && t.length > 1) return t[1] ?? null;\n }\n return null;\n };\n const address = tag('addr');\n const device_id = tag('device_id');\n const device_pk = tag('device_pk');\n const binding_sig = tag('binding_sig');\n if (!address || !device_id || !device_pk || !binding_sig) {\n throw new Error('device event missing required tags');\n }\n\n // The BIP-322 signature covers `event.content` — the statement — and nothing\n // else. Tags are an index the relay lets anyone write, so a value read from a\n // tag has been authenticated by nobody.\n //\n // Skipping this check was a total confidentiality break, and it needed no key\n // material: republish a victim's device event with their real content and\n // real binding_sig, change only the `device_pk` TAG to your own X25519 key.\n // verifyBip322(content, sig, addr) passes — every signed byte is genuine —\n // and the sender then seals the content key to the attacker's key, because\n // the recipient record was built from the tag. Proven against the published\n // package before this existed.\n //\n // So the statement is the source of truth and the tags must agree with it.\n const signed = parseBindingStatement(event.content);\n if (!signed) {\n throw new Error('device event content is not a v2/v3 bind or revoke statement');\n }\n if (signed.address !== address) {\n throw new Error(\n `device event addr tag (${address}) does not match the signed statement (${signed.address})`,\n );\n }\n // SPEC §3.3 / §3.6: the record lives at `oc-lock:device:<addr>` or the\n // per-device `oc-lock:device:<addr>:<device_id>`. Lookups filter on `d`, so a\n // record for one address filed under another's slot must not parse.\n const d = tag('d');\n if (\n d !== `oc-lock:device:${signed.address}` &&\n d !== `oc-lock:device:${signed.address}:${signed.device_id}`\n ) {\n throw new Error(\n `device event d tag (${d}) is not the slot for the signed address (${signed.address})`,\n );\n }\n if (signed.device_id !== device_id) {\n throw new Error(\n `device event device_id tag (${device_id}) does not match the signed statement (${signed.device_id})`,\n );\n }\n if (signed.kind === 'bind') {\n if (signed.device_pk !== device_pk) {\n throw new Error(\n `device event device_pk tag does not match the signed statement — ` +\n `tag says ${device_pk}, the signature covers ${signed.device_pk}`,\n );\n }\n // A v3 statement names the one Nostr key allowed to carry it.\n if (signed.nostr_pk !== null && signed.nostr_pk !== event.pubkey) {\n throw new Error(\n `device event pubkey (${event.pubkey}) does not match the signed nostr_pk (${signed.nostr_pk})`,\n );\n }\n } else if (device_pk !== 'revoked') {\n throw new Error(\n `device event carries a revocation statement but its device_pk tag is ${device_pk}, not \"revoked\"`,\n );\n }\n\n return {\n address: signed.address,\n device_id: signed.device_id,\n // Take the key from the SIGNED bytes, never the tag.\n device_pk: signed.kind === 'bind' ? signed.device_pk : 'revoked',\n bindingStatement: event.content,\n bindingSigBase64: binding_sig,\n revoked: signed.kind === 'revoke',\n createdAtUnix: event.created_at,\n nostrPubkey: event.pubkey,\n eventId: event.id,\n bindingVersion: signed.kind === 'bind' && signed.nostr_pk !== null ? 'v3' : 'v2',\n };\n}\n\n/**\n * Read back the fields the signature actually commits to. Returns null for\n * anything that is not a well-formed v2/v3 bind or v2 revoke statement, which the caller\n * treats as an unusable record.\n */\nfunction parseBindingStatement(\n content: string,\n):\n | {\n kind: 'bind';\n address: string;\n device_pk: string;\n device_id: string;\n nostr_pk: string | null;\n }\n | { kind: 'revoke'; address: string; device_id: string }\n | null {\n const lines = content.split('\\n');\n const header = lines[0];\n const field = (name: string): string | null => {\n const prefix = `${name}: `;\n for (const l of lines.slice(1)) {\n if (l.startsWith(prefix)) return l.slice(prefix.length);\n }\n return null;\n };\n const address = field('address');\n const device_id = field('device_id');\n if (!address || !device_id) return null;\n\n if (header === `oc-lock:device-bind:${BINDING_VERSION}`) {\n const device_pk = field('device_pk');\n if (!device_pk) return null;\n return { kind: 'bind', address, device_pk, device_id, nostr_pk: null };\n }\n if (header === `oc-lock:device-bind:${BINDING_VERSION_V3}`) {\n const device_pk = field('device_pk');\n const nostr_pk = field('nostr_pk');\n if (!device_pk || !nostr_pk || !/^[0-9a-f]{64}$/.test(nostr_pk)) return null;\n return { kind: 'bind', address, device_pk, device_id, nostr_pk };\n }\n if (header === `oc-lock:device-revoke:${BINDING_VERSION}`) {\n return { kind: 'revoke', address, device_id };\n }\n return null;\n}\n\nexport { authorizedDevices, V2_INBOX_SUNSET } from './authorize.js';\nexport type { AuthorizeOptions } from './authorize.js';\nexport {\n DEFAULT_BIND_SKEW_MS,\n liveDevices,\n pendingDevices,\n revokedAtFor,\n signedTime,\n} from './live.js';\nexport type { LiveOptions } from './live.js';\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orangecheck/lock-device",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Device-key binding and Nostr kind-30078 directory publication for OC Lock.",
5
5
  "keywords": [
6
6
  "bitcoin",
@@ -2,7 +2,13 @@
2
2
  // so a v2 record's event pubkey is only its author's claim.
3
3
  import { describe, expect, it } from 'vitest';
4
4
 
5
- import { authorizedDevices, buildBindingStatement, parseDeviceEvent } from './index.js';
5
+ import {
6
+ authorizedDevices,
7
+ buildBindingStatement,
8
+ liveDevices,
9
+ parseDeviceEvent,
10
+ V2_INBOX_SUNSET,
11
+ } from './index.js';
6
12
 
7
13
  const ADDR = 'bc1qowner00000000000000000000000000000000';
8
14
  const DEVICE_PK = 'aa'.repeat(32);
@@ -72,33 +78,70 @@ describe('v3 bind statement', () => {
72
78
  });
73
79
  });
74
80
 
75
- describe('authorizedDevices', () => {
81
+ const SUNSET = new Date(V2_INBOX_SUNSET);
82
+ const BEFORE = { now: new Date(SUNSET.getTime() - 1000) };
83
+ const AT = { now: SUNSET };
84
+
85
+ describe('authorizedDevices before the v2 sunset', () => {
76
86
  const v2Owner = parseDeviceEvent(event(OWNER_NOSTR, statement()));
77
87
  const v2Other = parseDeviceEvent(event(OTHER_NOSTR, statement()));
78
88
  const v3Owner = parseDeviceEvent(event(OWNER_NOSTR, statement(OWNER_NOSTR)));
79
89
 
80
90
  it('authorizes a v2 device seen under exactly one pubkey', () => {
81
- expect(authorizedDevices([v2Owner]).map((d) => d.nostrPubkey)).toEqual([OWNER_NOSTR]);
91
+ expect(authorizedDevices([v2Owner], BEFORE).map((d) => d.nostrPubkey)).toEqual([
92
+ OWNER_NOSTR,
93
+ ]);
82
94
  });
83
95
 
84
96
  it('authorizes neither pubkey when one v2 device appears under two', () => {
85
- expect(authorizedDevices([v2Owner, v2Other])).toEqual([]);
97
+ expect(authorizedDevices([v2Owner, v2Other], BEFORE)).toEqual([]);
86
98
  });
87
99
 
88
100
  it('a v3 record for the device supersedes every v2 copy of it', () => {
89
- expect(authorizedDevices([v2Other, v3Owner, v2Owner]).map((d) => d.nostrPubkey)).toEqual(
90
- [OWNER_NOSTR]
91
- );
92
- expect(authorizedDevices([v2Other, v3Owner])).toEqual([v3Owner]);
101
+ expect(
102
+ authorizedDevices([v2Other, v3Owner, v2Owner], BEFORE).map((d) => d.nostrPubkey)
103
+ ).toEqual([OWNER_NOSTR]);
104
+ expect(authorizedDevices([v2Other, v3Owner], BEFORE)).toEqual([v3Owner]);
93
105
  });
94
106
 
95
107
  it('scopes the v2 rule per device_id', () => {
96
108
  const second = parseDeviceEvent(
97
109
  event(OTHER_NOSTR, statement(undefined, 'owner-phone'), 'owner-phone')
98
110
  );
99
- expect(authorizedDevices([v2Owner, second]).map((d) => d.device_id)).toEqual([
111
+ expect(authorizedDevices([v2Owner, second], BEFORE).map((d) => d.device_id)).toEqual([
100
112
  DEVICE_ID,
101
113
  'owner-phone',
102
114
  ]);
103
115
  });
104
116
  });
117
+
118
+ describe('authorizedDevices from the v2 sunset (SPEC §3.4)', () => {
119
+ const v2Owner = parseDeviceEvent(event(OWNER_NOSTR, statement()));
120
+ const v2Other = parseDeviceEvent(event(OTHER_NOSTR, statement()));
121
+ const v3Owner = parseDeviceEvent(event(OWNER_NOSTR, statement(OWNER_NOSTR)));
122
+
123
+ it('a v2 record authorizes no key, however many carry it', () => {
124
+ expect(authorizedDevices([v2Owner], AT)).toEqual([]);
125
+ expect(authorizedDevices([v2Other], AT)).toEqual([]);
126
+ expect(authorizedDevices([v2Owner, v2Other], AT)).toEqual([]);
127
+ });
128
+
129
+ it('a v3 record still authorizes its signed key', () => {
130
+ expect(authorizedDevices([v2Other, v3Owner, v2Owner], AT)).toEqual([v3Owner]);
131
+ });
132
+
133
+ it('a v2 bind stays live for sealing to its device_pk', () => {
134
+ expect(liveDevices([v2Owner], AT)).toEqual([v2Owner]);
135
+ });
136
+
137
+ it("reads the reader's clock when no now is given", () => {
138
+ const v2Counts = Date.now() < SUNSET.getTime();
139
+ expect(authorizedDevices([v2Owner]).length).toBe(v2Counts ? 1 : 0);
140
+ });
141
+
142
+ it('refuses an option it does not read', () => {
143
+ expect(() =>
144
+ authorizedDevices([v2Owner], { at: SUNSET } as unknown as { now?: Date })
145
+ ).toThrow(/does not read option "at"/);
146
+ });
147
+ });
package/src/authorize.ts CHANGED
@@ -2,16 +2,32 @@
2
2
 
3
3
  import type { ParsedDeviceEvent } from './index.js';
4
4
 
5
+ /** From this instant, by the reader's clock, a v2 record authorizes no Nostr key (SPEC §3.4). */
6
+ export const V2_INBOX_SUNSET = '2027-01-21T00:00:00Z';
7
+
8
+ export interface AuthorizeOptions {
9
+ /** The reader's clock. Default: the current time. */
10
+ now?: Date;
11
+ }
12
+
5
13
  /**
6
14
  * Select the device records whose `nostrPubkey` may act for their address.
7
15
  *
8
16
  * `records` MUST already be BIP-322-verified, non-revoked bind records for one
9
17
  * address. A v3 record signs its Nostr pubkey, so it is always authorized. A v2
10
- * record does not, so its pubkey is only the event author's claim: it is
11
- * authorized only while its `device_id` has no v3 record and appears under
12
- * exactly one Nostr pubkey. Two pubkeys carrying one v2 device authorize neither.
18
+ * record does not, so its pubkey is only the event author's claim. Before
19
+ * `V2_INBOX_SUNSET` it is authorized only while its `device_id` has no v3 record
20
+ * and appears under exactly one Nostr pubkey; from then on it authorizes no key.
21
+ * A v2 bind stays live for sealing to its `device_pk` either way (liveDevices).
13
22
  */
14
- export function authorizedDevices(records: readonly ParsedDeviceEvent[]): ParsedDeviceEvent[] {
23
+ export function authorizedDevices(
24
+ records: readonly ParsedDeviceEvent[],
25
+ opts: AuthorizeOptions = {}
26
+ ): ParsedDeviceEvent[] {
27
+ for (const k of Object.keys(opts)) {
28
+ if (k !== 'now') throw new TypeError(`authorizedDevices does not read option "${k}"`);
29
+ }
30
+ const v2Counts = (opts.now ?? new Date()).getTime() < Date.parse(V2_INBOX_SUNSET);
15
31
  const v3Ids = new Set<string>();
16
32
  const v2Pubkeys = new Map<string, Set<string>>();
17
33
  for (const r of records) {
@@ -27,7 +43,7 @@ export function authorizedDevices(records: readonly ParsedDeviceEvent[]): Parsed
27
43
  return records.filter((r) => {
28
44
  if (r.revoked) return false;
29
45
  if (r.bindingVersion === 'v3') return true;
30
- if (v3Ids.has(r.device_id)) return false;
46
+ if (!v2Counts || v3Ids.has(r.device_id)) return false;
31
47
  return v2Pubkeys.get(r.device_id)?.size === 1;
32
48
  });
33
49
  }
package/src/index.ts CHANGED
@@ -351,7 +351,8 @@ function parseBindingStatement(
351
351
  return null;
352
352
  }
353
353
 
354
- export { authorizedDevices } from './authorize.js';
354
+ export { authorizedDevices, V2_INBOX_SUNSET } from './authorize.js';
355
+ export type { AuthorizeOptions } from './authorize.js';
355
356
  export {
356
357
  DEFAULT_BIND_SKEW_MS,
357
358
  liveDevices,
@@ -0,0 +1,94 @@
1
+ // OC Lock test vector a01 (oc-lock-protocol test-vectors/device-binding): the v2 inbox
2
+ // sunset and the v3 re-bind of a v2 device, checked byte for byte and case by case.
3
+ import { readFileSync } from 'node:fs';
4
+
5
+ import { x25519 } from '@noble/curves/ed25519';
6
+ import { describe, expect, it } from 'vitest';
7
+
8
+ import {
9
+ authorizedDevices,
10
+ buildBindingStatement,
11
+ buildRevocationStatement,
12
+ computeNostrEventId,
13
+ deriveNostrKey,
14
+ liveDevices,
15
+ parseDeviceEvent,
16
+ V2_INBOX_SUNSET,
17
+ type NostrEvent,
18
+ } from './index.js';
19
+
20
+ interface Case {
21
+ records: string[];
22
+ reader_time: string;
23
+ live: string[];
24
+ authorized: { record: string; pubkey: string }[];
25
+ }
26
+
27
+ const v = JSON.parse(
28
+ readFileSync(new URL('../test-vectors/a01-v2-sunset.json', import.meta.url), 'utf8')
29
+ );
30
+ const hex = (u: Uint8Array) => Buffer.from(u).toString('hex');
31
+ const bytes = (h: string) => Uint8Array.from(Buffer.from(h, 'hex'));
32
+ const NAMES = ['v2', 'v2_copy', 'v3_rebind', 'revocation', 'v3_rebind_later'];
33
+ const named: Record<string, NostrEvent> = Object.fromEntries(
34
+ NAMES.map((n) => [n, v.expected[n].event])
35
+ );
36
+ const parsed = (name: string) =>
37
+ Object.assign(
38
+ parseDeviceEvent({ ...named[name]!, id: '0'.repeat(64), sig: '0'.repeat(128) }),
39
+ { name }
40
+ );
41
+
42
+ describe('test vector a01: the v2 inbox sunset', () => {
43
+ it('is judged at the sunset this package applies', () => {
44
+ expect(v.inputs.sunset).toBe(V2_INBOX_SUNSET);
45
+ });
46
+
47
+ it('derives the keys and builds both statements byte for byte', () => {
48
+ const sk = bytes(v.inputs.device_sk);
49
+ expect(hex(x25519.getPublicKey(sk))).toBe(v.expected.device_pk);
50
+ expect(deriveNostrKey(sk)).toEqual({
51
+ nostrSk: v.expected.nostr_sk,
52
+ nostrPk: v.expected.nostr_pk,
53
+ });
54
+ const base = {
55
+ address: v.inputs.address,
56
+ device_pk: v.expected.device_pk,
57
+ device_id: v.inputs.device_id,
58
+ created_at: v.inputs.created_at,
59
+ };
60
+ expect(buildBindingStatement(base)).toBe(v.expected.v2.statement);
61
+ expect(buildBindingStatement({ ...base, nostr_pk: v.expected.nostr_pk })).toBe(
62
+ v.expected.v3_rebind.statement
63
+ );
64
+ expect(
65
+ buildBindingStatement({
66
+ ...base,
67
+ nostr_pk: v.expected.nostr_pk,
68
+ created_at: v.inputs.rebind_later_created_at,
69
+ })
70
+ ).toBe(v.expected.v3_rebind_later.statement);
71
+ expect(
72
+ buildRevocationStatement({
73
+ address: v.inputs.address,
74
+ device_id: v.inputs.device_id,
75
+ revoked_at: v.inputs.revoked_at,
76
+ })
77
+ ).toBe(v.expected.revocation.statement);
78
+ });
79
+
80
+ it('computes every event id', () => {
81
+ for (const n of NAMES) expect(computeNostrEventId(named[n]!)).toBe(v.expected[n].event_id);
82
+ });
83
+
84
+ it.each(v.expected.cases.cases as Case[])('$records at $reader_time', (c) => {
85
+ const now = new Date(c.reader_time);
86
+ const live = liveDevices(c.records.map(parsed), { now });
87
+ expect(live.map((r) => r.name).sort()).toEqual(c.live);
88
+ const authorized = authorizedDevices(live, { now }).map((r) => ({
89
+ record: r.name,
90
+ pubkey: r.nostrPubkey,
91
+ }));
92
+ expect(authorized).toEqual(c.authorized);
93
+ });
94
+ });