@orangecheck/sdk 1.16.0 → 1.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/attestation.d.mts +1 -1
- package/dist/attestation.d.ts +1 -1
- package/dist/attestation.js +86 -170
- package/dist/attestation.js.map +1 -1
- package/dist/attestation.mjs +86 -170
- package/dist/attestation.mjs.map +1 -1
- package/dist/canonical.d.mts +1 -1
- package/dist/canonical.d.ts +1 -1
- package/dist/challenge.d.mts +1 -1
- package/dist/challenge.d.ts +1 -1
- package/dist/identity.d.mts +1 -1
- package/dist/identity.d.ts +1 -1
- package/dist/identity.js.map +1 -1
- package/dist/identity.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +106 -173
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +106 -173
- package/dist/index.mjs.map +1 -1
- package/dist/nostr.d.mts +1 -1
- package/dist/nostr.d.ts +1 -1
- package/dist/nostr.js +86 -170
- package/dist/nostr.js.map +1 -1
- package/dist/nostr.mjs +86 -170
- package/dist/nostr.mjs.map +1 -1
- package/dist/{types-BBRMCgMo.d.mts → types-DsrYs3EB.d.mts} +1 -1
- package/dist/{types-BBRMCgMo.d.ts → types-DsrYs3EB.d.ts} +1 -1
- package/dist/verify.d.mts +2 -1
- package/dist/verify.d.ts +2 -1
- package/dist/verify.js +19 -0
- package/dist/verify.js.map +1 -1
- package/dist/verify.mjs +19 -0
- package/dist/verify.mjs.map +1 -1
- package/package.json +1 -1
package/dist/attestation.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/canonical.ts","../src/nostr-pubkey.ts","../src/utils/logger.ts","../src/utils/time.ts","../src/utils/signature.ts","../src/verify.ts","../src/nostr.ts","../src/nostr-crypto.ts","../src/attestation.ts"],"names":["verifyBitcoinSignature","log"],"mappings":";;;;;AA8BA,SAAS,WAAW,KAAA,EAA2B;AAC3C,EAAA,OAAO,MAAM,IAAA,CAAK,KAAK,CAAA,CAClB,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,QAAA,CAAS,EAAE,EAAE,QAAA,CAAS,CAAA,EAAG,GAAG,CAAC,CAAA,CAC1C,KAAK,EAAE,CAAA;AAChB;AAmCA,IAAM,oBAAA,GAAuB,oBAAA;AAC7B,IAAM,wBAAA,GAA2B,SAAA;AAEjC,SAAS,kBAAA,CAAmB,EAAE,QAAA,EAAU,UAAA,EAAW,EAA0B;AACzE,EAAA,IAAI,CAAC,oBAAA,CAAqB,IAAA,CAAK,QAAQ,CAAA,EAAG;AACtC,IAAA,MAAM,IAAI,KAAA;AAAA,MACN,CAAA,2BAAA,EAA8B,IAAA,CAAK,SAAA,CAAU,QAAQ,CAAC,CAAA,uCAAA;AAAA,KAC1D;AAAA,EACJ;AACA,EAAA,IAAI,CAAC,UAAA,IAAc,wBAAA,CAAyB,IAAA,CAAK,UAAU,CAAA,EAAG;AAC1D,IAAA,MAAM,IAAI,KAAA;AAAA,MACN,CAAA,6BAAA,EAAgC,IAAA,CAAK,SAAA,CAAU,UAAU,CAAC,CAAA,2DAAA;AAAA,KAC9D;AAAA,EACJ;AACJ;AA2BO,SAAS,gBAAgB,aAAA,EAA0C;AACtE,EAAA,IAAI,CAAC,aAAA,IAAiB,aAAA,CAAc,IAAA,OAAW,EAAA,EAAI;AAC/C,IAAA,OAAO,EAAC;AAAA,EACZ;AAIA,EAAA,IAAI,yBAAyB,IAAA,CAAK,aAAA,CAAc,QAAQ,IAAA,EAAM,EAAE,CAAC,CAAA,EAAG;AAChE,IAAA,MAAM,IAAI,MAAM,0DAA0D,CAAA;AAAA,EAC9E;AAEA,EAAA,MAAM,WAAW,aAAA,CAAc,KAAA,CAAM,GAAG,CAAA,CAAE,GAAA,CAAI,CAAC,OAAA,KAAY;AACvD,IAAA,MAAM,UAAA,GAAa,OAAA,CAAQ,OAAA,CAAQ,GAAG,CAAA;AACtC,IAAA,IAAI,eAAe,EAAA,EAAI;AACnB,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,iCAAA,EAAoC,OAAO,CAAA,CAAE,CAAA;AAAA,IACjE;AACA,IAAA,OAAO;AAAA,MACH,UAAU,OAAA,CAAQ,SAAA,CAAU,CAAA,EAAG,UAAU,EAAE,IAAA,EAAK;AAAA,MAChD,YAAY,OAAA,CAAQ,SAAA,CAAU,UAAA,GAAa,CAAC,EAAE,IAAA;AAAK,KACvD;AAAA,EACJ,CAAC,CAAA;AAED,EAAA,KAAA,MAAW,KAAK,QAAA,EAAU;AACtB,IAAA,IAAI,CAAC,CAAA,CAAE,QAAA,IAAY,CAAC,EAAE,UAAA,EAAY;AAC9B,MAAA,MAAM,IAAI,MAAM,CAAA,0BAAA,EAA6B,CAAA,CAAE,QAAQ,CAAA,CAAA,EAAI,CAAA,CAAE,UAAU,CAAA,CAAE,CAAA;AAAA,IAC7E;AACA,IAAA,kBAAA,CAAmB,CAAC,CAAA;AAAA,EACxB;AACA,EAAA,OAAO,QAAA;AACX;AA0EA,eAAsB,sBAAsB,OAAA,EAAkC;AAE1E,EAAA,MAAM,OAAA,GAAU,IAAI,WAAA,EAAY;AAChC,EAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,MAAA,CAAO,OAAO,CAAA;AAGnC,EAAA,MAAM,aAAa,MAAM,MAAA,CAAO,MAAA,CAAO,MAAA,CAAO,WAAW,IAAI,CAAA;AAG7D,EAAA,OAAO,UAAA,CAAW,IAAI,UAAA,CAAW,UAAU,CAAC,CAAA;AAChD;AAeA,eAAsB,yBAAA,CAClB,SACA,SAAA,EACA,MAAA,EACA,SACA,UAAA,EACA,UAAA,GAAyB,EAAC,EACE;AAC5B,EAAA,MAAM,cAAA,GAAiB,MAAM,qBAAA,CAAsB,OAAO,CAAA;AAG1D,EAAA,MAAM,aAAA,GAAgB,OAAA,CAAQ,KAAA,CAAM,qBAAqB,CAAA;AACzD,EAAA,MAAM,YAAY,aAAA,GAAgB,CAAC,sBAAK,IAAI,IAAA,IAAO,WAAA,EAAY;AAG/D,EAAA,MAAM,aAAa,UAAA,CAAW,OAAA;AAG9B,EAAA,MAAM,WAAA,GAAc,UAAA,CAAW,WAAA,EAAa,KAAA,CAAM,GAAG,CAAA,CAAE,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,IAAA,EAAM,CAAA;AAG1E,EAAA,MAAM,eAAA,GAAkB,UAAA,CAAW,OAAA,EAAS,KAAA,CAAM,GAAG,CAAA,CAAE,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,IAAA,EAAM,CAAA;AAG1E,EAAA,MAAM,OAAA,GAAU,IAAI,WAAA,EAAY;AAChC,EAAA,MAAM,YAAA,GAAe,OAAA,CAAQ,MAAA,CAAO,OAAO,CAAA;AAC3C,EAAA,MAAM,SAAS,IAAA,CAAK,MAAA,CAAO,YAAA,CAAa,GAAG,YAAY,CAAC,CAAA;AACxD,EAAA,MAAM,cAAA,GAAiB,MAAA,CAAO,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAAE,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAAE,OAAA,CAAQ,IAAA,EAAM,EAAE,CAAA;AAGtF,EAAA,MAAM,gBAAA,GAAmB,iCAAiC,cAAc,CAAA,CAAA;AAExE,EAAA,OAAO;AAAA,IACH,cAAA;AAAA,IACA,MAAA;AAAA,IACA,OAAA;AAAA,IACA,UAAA;AAAA,IACA,OAAA;AAAA,IACA,cAAA;AAAA,IACA,SAAA;AAAA,IACA,SAAA;AAAA,IACA,UAAA;AAAA,IACA,gBAAA;AAAA,IACA,eAAA;AAAA,IACA;AAAA,GACJ;AACJ;AC1RA,IAAM,SAAA,GAAY,gBAAA;AAElB,SAAS,MAAM,KAAA,EAA2B;AACtC,EAAA,IAAI,GAAA,GAAM,EAAA;AACV,EAAA,KAAA,MAAW,CAAA,IAAK,OAAO,GAAA,IAAO,CAAA,CAAE,SAAS,EAAE,CAAA,CAAE,QAAA,CAAS,CAAA,EAAG,GAAG,CAAA;AAC5D,EAAA,OAAO,GAAA;AACX;AAGO,SAAS,iBAAiB,KAAA,EAA8B;AAC3D,EAAA,MAAM,OAAA,GAAU,KAAA,CAAM,IAAA,EAAK,CAAE,WAAA,EAAY;AACzC,EAAA,IAAI,SAAA,CAAU,IAAA,CAAK,OAAO,CAAA,EAAG,OAAO,OAAA;AACpC,EAAA,IAAI,CAAC,OAAA,CAAQ,UAAA,CAAW,OAAO,GAAG,OAAO,IAAA;AACzC,EAAA,IAAI;AACA,IAAA,MAAM,OAAA,GAAU,MAAA,CAAO,MAAA,CAAO,OAAA,EAAkC,IAAI,CAAA;AACpE,IAAA,IAAI,OAAA,CAAQ,MAAA,KAAW,MAAA,EAAQ,OAAO,IAAA;AACtC,IAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,SAAA,CAAU,OAAA,CAAQ,KAAK,CAAA;AAC5C,IAAA,OAAO,KAAA,CAAM,WAAW,EAAA,GAAK,KAAA,CAAM,WAAW,IAAA,CAAK,KAAK,CAAC,CAAA,GAAI,IAAA;AAAA,EACjE,CAAA,CAAA,MAAQ;AACJ,IAAA,OAAO,IAAA;AAAA,EACX;AACJ;AAMO,SAAS,qBAAqB,UAAA,EAA8B;AAC/D,EAAA,MAAM,GAAA,GAAM,iBAAiB,UAAU,CAAA;AACvC,EAAA,IAAI,CAAC,GAAA,EAAK,OAAO,CAAC,UAAU,CAAA;AAC5B,EAAA,MAAM,KAAA,GAAQ,UAAA,CAAW,IAAA,CAAK,GAAA,CAAI,MAAM,KAAK,CAAA,CAAG,GAAA,CAAI,CAAC,CAAA,KAAM,QAAA,CAAS,CAAA,EAAG,EAAE,CAAC,CAAC,CAAA;AAC3E,EAAA,OAAO,CAAC,MAAA,CAAO,MAAA,CAAO,MAAA,EAAQ,MAAA,CAAO,QAAQ,KAAK,CAAA,EAAG,IAAI,CAAA,EAAG,GAAG,CAAA;AACnE;ACcO,SAAS,aAAa,SAAA,EAA2B;AACpD,EAAA,MAAM,SAAA,GAAY,CAAC,KAAA,KAA8C;AAC7D,IAAyB,OAAO,KAAA;AAMJ,EAChC,CAAA;AAEA,EAAA,MAAM,gBAAgB,CAAC,GAAA,KAAwB,CAAA,CAAA,EAAI,SAAS,KAAK,GAAG,CAAA,CAAA;AAEpE,EAAA,OAAO;AAAA,IACH,KAAA,EAAO,CAAC,gBAAA,EAAuC,OAAA,KAA2B;AACtE,MAAA,IAAI,SAAA,CAAiB,CAAA,EAAG;AACpB,QAAA,IAAI,OAAO,qBAAqB,QAAA,EAAU;AACtC,UAAA,OAAA,CAAQ,KAAA,CAAM,aAAA,CAAc,gBAAgB,CAAC,CAAA;AAAA,QACjD,CAAA,MAAO;AACH,UAAA,OAAA,CAAQ,KAAA,CAAM,aAAA,CAAc,OAAA,IAAW,OAAO,GAAG,gBAAgB,CAAA;AAAA,QACrE;AAAA,MACJ;AAAA,IACJ,CAAA;AAAA,IACA,IAAA,EAAM,CAAC,gBAAA,EAAuC,OAAA,KAA2B;AACrE,MAAA,IAAI,SAAA,CAAgB,CAAA,EAAG;AACnB,QAAA,IAAI,OAAO,qBAAqB,QAAA,EAAU;AACtC,UAAA,OAAA,CAAQ,IAAA,CAAK,aAAA,CAAc,gBAAgB,CAAC,CAAA;AAAA,QAChD,CAAA,MAAO;AACH,UAAA,OAAA,CAAQ,IAAA,CAAK,aAAA,CAAc,OAAA,IAAW,MAAM,GAAG,gBAAgB,CAAA;AAAA,QACnE;AAAA,MACJ;AAAA,IACJ,CAAA;AAAA,IACA,IAAA,EAAM,CAAC,gBAAA,EAAuC,OAAA,KAA2B;AACrE,MAAA,IAAI,SAAA,CAAgB,CAAA,EAAG;AACnB,QAAA,IAAI,OAAO,qBAAqB,QAAA,EAAU;AACtC,UAAA,OAAA,CAAQ,IAAA,CAAK,aAAA,CAAc,gBAAgB,CAAC,CAAA;AAAA,QAChD,CAAA,MAAO;AACH,UAAA,OAAA,CAAQ,IAAA,CAAK,aAAA,CAAc,OAAA,IAAW,SAAS,GAAG,gBAAgB,CAAA;AAAA,QACtE;AAAA,MACJ;AAAA,IACJ,CAAA;AAAA,IACA,KAAA,EAAO,CAAC,gBAAA,EAAuC,OAAA,KAA2B;AACtE,MAAA,IAAI,SAAA,CAAiB,CAAA,EAAG;AACpB,QAAA,IAAI,OAAO,qBAAqB,QAAA,EAAU;AACtC,UAAA,OAAA,CAAQ,KAAA,CAAM,aAAA,CAAc,gBAAgB,CAAC,CAAA;AAAA,QACjD,CAAA,MAAO;AACH,UAAA,OAAA,CAAQ,KAAA,CAAM,aAAA,CAAc,OAAA,IAAW,OAAO,GAAG,gBAAgB,CAAA;AAAA,QACrE;AAAA,MACJ;AAAA,IACJ;AAAA,GACJ;AACJ;;;ACnGA,IAAM,OAAA,GAAU,sEAAA;AAGT,SAAS,UAAU,KAAA,EAA0C;AAChE,EAAA,IAAI,CAAC,KAAA,IAAS,CAAC,QAAQ,IAAA,CAAK,KAAK,GAAG,OAAO,IAAA;AAC3C,EAAA,MAAM,EAAA,GAAK,IAAA,CAAK,KAAA,CAAM,KAAK,CAAA;AAC3B,EAAA,OAAO,MAAA,CAAO,KAAA,CAAM,EAAE,CAAA,GAAI,IAAA,GAAO,EAAA;AACrC;ACFO,SAAS,cAAc,GAAA,EAAqB;AAC/C,EAAA,IAAI,iBAAiB,IAAA,CAAK,GAAG,KAAK,GAAA,CAAI,MAAA,GAAS,MAAM,CAAA,EAAG;AACpD,IAAA,OAAO,OAAO,IAAA,CAAK,GAAA,EAAK,KAAK,CAAA,CAAE,SAAS,QAAQ,CAAA;AAAA,EACpD;AACA,EAAA,OAAO,GAAA;AACX;AAQO,SAAS,gBAAgB,SAAA,EAAkC;AAC9D,EAAA,IAAI,YAAA,CAAa,IAAA,CAAK,SAAS,CAAA,EAAG,OAAO,IAAA;AACzC,EAAA,IAAI,SAAA,CAAU,UAAA,CAAW,KAAK,CAAA,EAAG,OAAO,QAAA;AACxC,EAAA,OAAO,OAAO,IAAA,CAAK,SAAA,EAAW,QAAQ,CAAA,CAAE,MAAA,KAAW,KAAK,QAAA,GAAW,QAAA;AACvE;;;ACKA,IAAM,GAAA,GAAM,aAAa,YAAY,CAAA;AAsCrC,eAAe,eAAA,CAAgB,EAAE,IAAA,EAAM,GAAA,EAAK,KAAI,EAA8C;AAC1F,EAAA,MAAM,SAAA,GAAY,cAAc,GAAG,CAAA;AACnC,EAAA,MAAM,MAAA,GAAS,gBAAgB,SAAS,CAAA;AACxC,EAAA,IAAI,CAAC,QAAQ,OAAO,wBAAA;AAEpB,EAAA,MAAM,EAAA,GAAKA,iBAAA,CAAuB,IAAA,EAAM,GAAA,EAAK,SAAS,CAAA;AACtD,EAAA,GAAA,CAAI,IAAA,CAAK,EAAE,IAAA,EAAM,EAAA,EAAI,QAAQ,SAAA,EAAW,GAAA,CAAI,MAAA,EAAO,EAAG,+BAA+B,CAAA;AACrF,EAAA,IAAI,CAAC,IAAI,OAAO,aAAA;AAChB,EAAA,OAAO,MAAA,KAAW,WAAW,eAAA,GAAkB,eAAA;AACnD;AAGA,eAAsB,iBAAiB,KAAA,EAA+C;AAClF,EAAA,IAAI;AACA,IAAA,MAAM,IAAA,GAAO,MAAM,eAAA,CAAgB,KAAK,CAAA;AACxC,IAAA,OAAO,IAAA,KAAS,mBAAmB,IAAA,KAAS,eAAA;AAAA,EAChD,CAAA,CAAA,MAAQ;AACJ,IAAA,OAAO,KAAA;AAAA,EACX;AACJ;AAGO,SAAS,sBAAsB,GAAA,EAAa;AAC/C,EAAA,MAAM,KAAA,GAAQ,GAAA,CAAI,KAAA,CAAM,IAAI,CAAA,CAAE,IAAI,CAAC,CAAA,KAAM,CAAA,CAAE,IAAA,EAAM,CAAA;AAKjD,EAAA,MAAM,MAAA,GAAS,CAAC,MAAA,KAAuC;AACnD,IAAA,MAAM,SAAS,MAAA,GAAS,GAAA;AACxB,IAAA,MAAM,GAAA,GAAM,KAAA,CAAM,IAAA,CAAK,CAAC,CAAA,KAAM,EAAE,WAAA,EAAY,CAAE,UAAA,CAAW,MAAM,CAAC,CAAA;AAChE,IAAA,IAAI,CAAC,KAAK,OAAO,MAAA;AACjB,IAAA,MAAM,GAAA,GAAM,GAAA,CAAI,OAAA,CAAQ,GAAG,CAAA;AAC3B,IAAA,OAAO,GAAA,CAAI,KAAA,CAAM,GAAA,GAAM,CAAC,EAAE,IAAA,EAAK;AAAA,EACnC,CAAA;AAEA,EAAA,MAAM,OAAA,GAAU,MAAA,CAAO,SAAS,CAAA,IAAK,EAAA;AACrC,EAAA,MAAM,aAAA,GAAgB,MAAA,CAAO,YAAY,CAAA,IAAK,EAAA;AAC9C,EAAA,MAAM,SAAA,GAAY,MAAA,CAAO,WAAW,CAAA,IAAK,EAAA;AAEzC,EAAA,MAAM,OAAA,GAAU,OAAO,SAAS,CAAA;AAGhC,EAAA,IAAI,aAAgC,EAAC;AACrC,EAAA,IAAI;AACA,IAAA,UAAA,GAAa,gBAAgB,aAAa,CAAA;AAAA,EAC9C,SAAS,GAAA,EAAK;AACV,IAAA,GAAA,CAAI,KAAK,EAAE,aAAA,EAAe,KAAA,EAAO,GAAA,IAAO,kCAAkC,CAAA;AAAA,EAC9E;AAGA,EAAA,MAAM,QAAA,uBAAe,GAAA,CAAI;AAAA,IACrB,aAAA;AAAA,IACA,YAAA;AAAA,IACA,SAAA;AAAA,IACA,SAAA;AAAA,IACA,OAAA;AAAA,IACA,WAAA;AAAA,IACA;AAAA,GACH,CAAA;AACD,EAAA,MAAM,aAAqC,EAAC;AAC5C,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACtB,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,OAAA,CAAQ,GAAG,CAAA;AAC5B,IAAA,IAAI,QAAQ,EAAA,EAAI;AAChB,IAAA,MAAM,GAAA,GAAM,KAAK,KAAA,CAAM,CAAA,EAAG,GAAG,CAAA,CAAE,IAAA,GAAO,WAAA,EAAY;AAClD,IAAA,MAAM,MAAM,IAAA,CAAK,KAAA,CAAM,GAAA,GAAM,CAAC,EAAE,IAAA,EAAK;AACrC,IAAA,IAAI,CAAC,SAAS,GAAA,CAAI,GAAG,KAAK,GAAA,EAAK,UAAA,CAAW,GAAG,CAAA,GAAI,GAAA;AAAA,EACrD;AAEA,EAAA,MAAM,OAAA,GAAW,UAAA,CAAW,SAAS,CAAA,IAAK,SAAA;AAG1C,EAAA,MAAM,OAAA,GAAU,WAAW,MAAM,CAAA;AACjC,EAAA,MAAM,IAAA,GAAO,OAAA,GAAU,QAAA,CAAS,OAAA,EAAS,EAAE,CAAA,GAAI,MAAA;AAE/C,EAAA,OAAO;AAAA,IACH,IAAA,EAAM,EAAE,OAAA,EAAS,UAAA,EAAY,SAAA,EAAU;AAAA,IACvC,OAAA;AAAA,IACA,OAAA;AAAA,IACA,UAAA;AAAA,IACA,IAAA,EAAM,SAAS,MAAA,IAAa,CAAC,MAAM,IAAI,CAAA,IAAK,IAAA,GAAO,CAAA,GAAI,IAAA,GAAO;AAAA,GAClE;AACJ;;;ACtIA,IAAMC,IAAAA,GAAM,aAAa,WAAW,CAAA;AAK7B,IAAM,sBAAA,GAAyB,KAAA;AAqC/B,IAAM,cAAA,GAA2B;AAAA,EACpC,sBAAA;AAAA,EACA,eAAA;AAAA;AAAA;AAAA,EAGA,sBAAA;AAAA,EACA;AACJ,CAAA;AAyBO,IAAM,kBAAA,GAAqB,GAAA;AAClC,IAAM,gBAAA,GAAmB,GAAA;AAGzB,SAAS,UAAU,UAAA,EAA4B;AAC3C,EAAA,OAAO,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,kBAAkB,UAAA,GAAa,IAAA,CAAK,GAAA,EAAK,CAAC,CAAA;AAC1E;AAYO,SAAS,sBAAA,CACZ,UACA,MAAA,EAC8B;AAQ9B,EAAA,MAAM,IAAA,GAAmB;AAAA,IACrB,CAAC,GAAA,EAAK,QAAA,CAAS,cAAc,CAAA;AAAA;AAAA,IAC7B,CAAC,GAAA,EAAK,QAAA,CAAS,OAAO,CAAA;AAAA;AAAA,IACtB,CAAC,KAAK,WAAW,CAAA;AAAA;AAAA,IACjB,CAAC,SAAA,EAAW,QAAA,CAAS,OAAO,CAAA;AAAA;AAAA,IAC5B,CAAC,QAAA,EAAU,QAAA,CAAS,MAAM,CAAA;AAAA;AAAA,IAC1B,CAAC,WAAA,EAAa,QAAA,CAAS,SAAS;AAAA;AAAA,GACpC;AAGA,EAAA,KAAA,MAAW,QAAA,IAAY,SAAS,UAAA,EAAY;AACxC,IAAA,IAAA,CAAK,IAAA,CAAK,CAAC,GAAA,EAAK,CAAA,EAAG,QAAA,CAAS,QAAQ,CAAA,CAAA,EAAI,QAAA,CAAS,UAAU,CAAA,CAAE,CAAC,CAAA;AAAA,EAClE;AAGA,EAAA,IAAI,SAAS,UAAA,EAAY;AACrB,IAAA,IAAA,CAAK,IAAA,CAAK,CAAC,SAAA,EAAW,QAAA,CAAS,UAAU,CAAC,CAAA;AAAA,EAC9C;AAGA,EAAA,IAAI,QAAA,CAAS,aAAa,MAAA,EAAQ;AAC9B,IAAA,KAAA,MAAW,KAAA,IAAS,SAAS,WAAA,EAAa;AACtC,MAAA,IAAA,CAAK,IAAA,CAAK,CAAC,OAAA,EAAS,KAAK,CAAC,CAAA;AAAA,IAC9B;AAAA,EACJ;AAGA,EAAA,MAAM,OAAA,GAAU,IAAA,CAAK,SAAA,CAAU,QAAA,EAAU,MAAM,CAAC,CAAA;AAEhD,EAAA,OAAO;AAAA,IACH,IAAA,EAAM,sBAAA;AAAA,IACN,IAAA;AAAA,IACA,OAAA;AAAA,IACA,UAAA,EAAY,KAAK,KAAA,CAAM,IAAA,CAAK,MAAM,QAAA,CAAS,SAAS,IAAI,GAAI,CAAA;AAAA,IAC5D;AAAA,GACJ;AACJ;AAoDA,eAAsB,eAAA,CAClB,KAAA,EACA,MAAA,GAAmB,cAAA,EAC6B;AAChD,EAAA,MAAM,UAAoB,EAAC;AAC3B,EAAA,MAAM,SAAmB,EAAC;AAE1B,EAAAA,IAAAA,CAAI,IAAA,CAAK,EAAE,OAAA,EAAS,KAAA,CAAM,IAAI,UAAA,EAAY,MAAA,CAAO,MAAA,EAAO,EAAG,4BAA4B,CAAA;AAIvF,EAAA,MAAM,UAAA,GAAa,IAAA,CAAK,GAAA,EAAI,GAAI,kBAAA;AAEhC,EAAA,MAAM,eAAA,GAAkB,MAAA,CAAO,GAAA,CAAI,OAAO,QAAA,KAAa;AACnD,IAAA,IAAI,EAAA,GAAuB,IAAA;AAU3B,IAAA,IAAI,OAAA,GAAU,KAAA;AACd,IAAA,MAAM,gBAAgB,MAAY;AAC9B,MAAA,IAAI,OAAA,EAAS;AACb,MAAA,OAAA,GAAU,IAAA;AACV,MAAA,OAAA,CAAQ,KAAK,QAAQ,CAAA;AAAA,IACzB,CAAA;AACA,IAAA,MAAM,gBAAgB,MAAY;AAC9B,MAAA,IAAI,OAAA,EAAS;AACb,MAAA,OAAA,GAAU,IAAA;AACV,MAAA,MAAA,CAAO,KAAK,QAAQ,CAAA;AAAA,IACxB,CAAA;AAEA,IAAA,IAAI;AAEA,MAAA,EAAA,GAAK,IAAI,UAAU,QAAQ,CAAA;AAC3B,MAAA,MAAM,KAAA,GAAQ,EAAA;AAEd,MAAA,MAAM,IAAI,OAAA,CAAc,CAAC,OAAA,EAAS,MAAA,KAAW;AAIzC,QAAA,MAAM,QAAA,GAAW,WAAW,MAAM;AAC9B,UAAA,KAAA,CAAM,KAAA,EAAM;AACZ,UAAA,MAAA,CAAO,IAAI,KAAA,CAAM,uBAAuB,CAAC,CAAA;AAAA,QAC7C,CAAA,EAAG,SAAA,CAAU,UAAU,CAAC,CAAA;AAExB,QAAA,MAAM,UAAU,MAAM;AAClB,UAAA,YAAA,CAAa,QAAQ,CAAA;AACrB,UAAA,KAAA,CAAM,KAAA,EAAM;AAAA,QAChB,CAAA;AAEA,QAAA,KAAA,CAAM,SAAS,MAAM;AAGjB,UAAA,KAAA,CAAM,KAAK,IAAA,CAAK,SAAA,CAAU,CAAC,OAAA,EAAS,KAAK,CAAC,CAAC,CAAA;AAAA,QAC/C,CAAA;AAEA,QAAA,KAAA,CAAM,SAAA,GAAY,CAAC,GAAA,KAAQ;AACvB,UAAA,IAAI;AACA,YAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,IAAI,CAAA;AAEhC,YAAA,IAAI,IAAA,CAAK,CAAC,CAAA,KAAM,IAAA,IAAQ,KAAK,CAAC,CAAA,KAAM,MAAM,EAAA,EAAI;AAC1C,cAAA,IAAI,IAAA,CAAK,CAAC,CAAA,KAAM,IAAA,EAAM;AAClB,gBAAA,aAAA,EAAc;AACd,gBAAA,OAAA,EAAQ;AACR,gBAAA,OAAA,EAAQ;AAAA,cACZ,CAAA,MAAO;AACH,gBAAAA,IAAAA,CAAI,IAAA;AAAA,kBACA,EAAE,KAAA,EAAO,QAAA,EAAU,MAAA,EAAQ,IAAA,CAAK,CAAC,CAAA,EAAE;AAAA,kBACnC;AAAA,iBACJ;AACA,gBAAA,aAAA,EAAc;AACd,gBAAA,OAAA,EAAQ;AACR,gBAAA,MAAA,CAAO,IAAI,KAAA,CAAM,IAAA,CAAK,CAAC,CAAA,IAAK,sBAAsB,CAAC,CAAA;AAAA,cACvD;AAAA,YACJ;AAAA,UACJ,SAAS,GAAA,EAAK;AACV,YAAAA,IAAAA,CAAI,KAAA;AAAA,cACA,EAAE,KAAA,EAAO,QAAA,EAAU,KAAA,EAAO,GAAA,EAAI;AAAA,cAC9B;AAAA,aACJ;AACA,YAAA,aAAA,EAAc;AACd,YAAA,OAAA,EAAQ;AACR,YAAA,MAAA,CAAO,GAAG,CAAA;AAAA,UACd;AAAA,QACJ,CAAA;AAEA,QAAA,KAAA,CAAM,OAAA,GAAU,CAAC,GAAA,KAAQ;AACrB,UAAAA,IAAAA,CAAI,MAAM,EAAE,KAAA,EAAO,UAAU,KAAA,EAAO,GAAA,IAAO,iBAAiB,CAAA;AAC5D,UAAA,aAAA,EAAc;AACd,UAAA,OAAA,EAAQ;AACR,UAAA,MAAA,CAAO,GAAG,CAAA;AAAA,QACd,CAAA;AAEA,QAAA,KAAA,CAAM,UAAU,MAAM;AAGlB,UAAA,YAAA,CAAa,QAAQ,CAAA;AAAA,QACzB,CAAA;AAAA,MACJ,CAAC,CAAA;AAAA,IACL,SAAS,GAAA,EAAK;AACV,MAAAA,IAAAA,CAAI,MAAM,EAAE,KAAA,EAAO,UAAU,KAAA,EAAO,GAAA,IAAO,4BAA4B,CAAA;AACvE,MAAA,aAAA,EAAc;AAAA,IAClB,CAAA,SAAE;AAEE,MAAA,IAAI,EAAA,IAAM,EAAA,CAAG,UAAA,KAAe,SAAA,CAAU,MAAA,EAAQ;AAC1C,QAAA,EAAA,CAAG,KAAA,EAAM;AAAA,MACb;AAAA,IACJ;AAAA,EACJ,CAAC,CAAA;AAED,EAAA,MAAM,OAAA,CAAQ,WAAW,eAAe,CAAA;AAExC,EAAAA,IAAAA,CAAI,IAAA,CAAK,EAAE,YAAA,EAAc,OAAA,CAAQ,QAAQ,WAAA,EAAa,MAAA,CAAO,MAAA,EAAO,EAAG,qBAAqB,CAAA;AAE5F,EAAA,OAAO,EAAE,SAAS,MAAA,EAAO;AAC7B;AAKA,eAAsB,oBAAA,CAClB,aAAA,EACA,MAAA,GAAmB,cAAA,EACE;AACrB,EAAA,MAAM,SAAuB,EAAC;AAE9B,EAAAA,IAAAA,CAAI,KAAK,EAAE,aAAA,EAAe,YAAY,MAAA,CAAO,MAAA,IAAU,iCAAiC,CAAA;AAIxF,EAAA,MAAM,UAAA,GAAa,IAAA,CAAK,GAAA,EAAI,GAAI,kBAAA;AAEhC,EAAA,MAAM,aAAA,GAAgB,MAAA,CAAO,GAAA,CAAI,OAAO,QAAA,KAAa;AACjD,IAAA,IAAI,EAAA,GAAuB,IAAA;AAE3B,IAAA,IAAI;AACA,MAAA,EAAA,GAAK,IAAI,UAAU,QAAQ,CAAA;AAC3B,MAAA,MAAM,KAAA,GAAQ,EAAA;AAEd,MAAA,MAAM,IAAI,OAAA,CAAc,CAAC,OAAA,EAAS,MAAA,KAAW;AACzC,QAAA,MAAM,OAAA,GAAU,WAAW,MAAM;AAC7B,UAAA,KAAA,CAAM,KAAA,EAAM;AACZ,UAAA,MAAA,CAAO,IAAI,KAAA,CAAM,eAAe,CAAC,CAAA;AAAA,QACrC,CAAA,EAAG,SAAA,CAAU,UAAU,CAAC,CAAA;AAExB,QAAA,MAAM,UAAU,MAAM;AAClB,UAAA,YAAA,CAAa,OAAO,CAAA;AACpB,UAAA,KAAA,CAAM,KAAA,EAAM;AAAA,QAChB,CAAA;AAEA,QAAA,KAAA,CAAM,SAAS,MAAM;AAEjB,UAAA,MAAM,cAAA,GAAiB,CAAA,KAAA,EAAQ,IAAA,CAAK,GAAA,EAAK,CAAA,CAAA;AACzC,UAAA,KAAA,CAAM,IAAA;AAAA,YACF,KAAK,SAAA,CAAU;AAAA,cACX,KAAA;AAAA,cACA,cAAA;AAAA,cACA;AAAA,gBACI,KAAA,EAAO,CAAC,KAAK,CAAA;AAAA,gBACb,IAAA,EAAM,CAAC,aAAa,CAAA;AAAA,gBACpB,KAAA,EAAO;AAAA;AACX,aACH;AAAA,WACL;AAAA,QACJ,CAAA;AAEA,QAAA,KAAA,CAAM,SAAA,GAAY,CAAC,GAAA,KAAQ;AACvB,UAAA,IAAI;AACA,YAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,IAAI,CAAA;AAChC,YAAA,IAAI,IAAA,CAAK,CAAC,CAAA,KAAM,OAAA,EAAS;AACrB,cAAA,MAAA,CAAO,IAAA,CAAK,IAAA,CAAK,CAAC,CAAC,CAAA;AAAA,YACvB,CAAA,MAAA,IAAW,IAAA,CAAK,CAAC,CAAA,KAAM,MAAA,EAAQ;AAC3B,cAAA,OAAA,EAAQ;AACR,cAAA,OAAA,EAAQ;AAAA,YACZ;AAAA,UACJ,SAAS,GAAA,EAAK;AACV,YAAAA,IAAAA,CAAI,MAAM,EAAE,KAAA,EAAO,UAAU,KAAA,EAAO,GAAA,IAAO,uBAAuB,CAAA;AAAA,UACtE;AAAA,QACJ,CAAA;AAEA,QAAA,KAAA,CAAM,OAAA,GAAU,CAAC,GAAA,KAAQ;AACrB,UAAAA,IAAAA,CAAI,MAAM,EAAE,KAAA,EAAO,UAAU,KAAA,EAAO,GAAA,IAAO,8BAA8B,CAAA;AACzE,UAAA,OAAA,EAAQ;AACR,UAAA,MAAA,CAAO,GAAG,CAAA;AAAA,QACd,CAAA;AAEA,QAAA,KAAA,CAAM,UAAU,MAAM;AAClB,UAAA,YAAA,CAAa,OAAO,CAAA;AAAA,QACxB,CAAA;AAAA,MACJ,CAAC,CAAA;AAAA,IACL,SAAS,GAAA,EAAK;AACV,MAAAA,IAAAA,CAAI,MAAM,EAAE,KAAA,EAAO,UAAU,KAAA,EAAO,GAAA,IAAO,uBAAuB,CAAA;AAAA,IACtE,CAAA,SAAE;AAEE,MAAA,IAAI,EAAA,IAAM,EAAA,CAAG,UAAA,KAAe,SAAA,CAAU,MAAA,EAAQ;AAC1C,QAAA,EAAA,CAAG,KAAA,EAAM;AAAA,MACb;AAAA,IACJ;AAAA,EACJ,CAAC,CAAA;AAED,EAAA,MAAM,OAAA,CAAQ,WAAW,aAAa,CAAA;AAEtC,EAAAA,KAAI,IAAA,CAAK,EAAE,YAAY,MAAA,CAAO,MAAA,IAAU,gBAAgB,CAAA;AAExD,EAAA,OAAO,MAAA;AACX;AAKA,eAAsB,cAAA,CAClB,OAAA,EACA,MAAA,GAAmB,cAAA,EACE;AACrB,EAAA,MAAM,SAAuB,EAAC;AAE9B,EAAAA,IAAAA,CAAI,KAAK,EAAE,OAAA,EAAS,YAAY,MAAA,CAAO,MAAA,IAAU,6BAA6B,CAAA;AAI9E,EAAA,MAAM,UAAA,GAAa,IAAA,CAAK,GAAA,EAAI,GAAI,kBAAA;AAEhC,EAAA,MAAM,aAAA,GAAgB,MAAA,CAAO,GAAA,CAAI,OAAO,QAAA,KAAa;AACjD,IAAA,IAAI;AACA,MAAA,MAAM,EAAA,GAAK,IAAI,SAAA,CAAU,QAAQ,CAAA;AAEjC,MAAA,MAAM,IAAI,OAAA,CAAc,CAAC,OAAA,EAAS,MAAA,KAAW;AACzC,QAAA,MAAM,OAAA,GAAU,WAAW,MAAM;AAC7B,UAAA,EAAA,CAAG,KAAA,EAAM;AACT,UAAA,MAAA,CAAO,IAAI,KAAA,CAAM,eAAe,CAAC,CAAA;AAAA,QACrC,CAAA,EAAG,SAAA,CAAU,UAAU,CAAC,CAAA;AAExB,QAAA,EAAA,CAAG,SAAS,MAAM;AACd,UAAA,MAAM,cAAA,GAAiB,CAAA,UAAA,EAAa,IAAA,CAAK,GAAA,EAAK,CAAA,CAAA;AAC9C,UAAA,EAAA,CAAG,IAAA;AAAA,YACC,KAAK,SAAA,CAAU;AAAA,cACX,KAAA;AAAA,cACA,cAAA;AAAA,cACA;AAAA,gBACI,KAAA,EAAO,CAAC,KAAK,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gBAQb,IAAA,EAAM,CAAC,OAAO,CAAA;AAAA,gBACd,KAAA,EAAO;AAAA;AACX,aACH;AAAA,WACL;AAAA,QACJ,CAAA;AAEA,QAAA,EAAA,CAAG,SAAA,GAAY,CAAC,GAAA,KAAQ;AACpB,UAAA,IAAI;AACA,YAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,IAAI,CAAA;AAChC,YAAA,IAAI,IAAA,CAAK,CAAC,CAAA,KAAM,OAAA,EAAS;AACrB,cAAA,MAAA,CAAO,IAAA,CAAK,IAAA,CAAK,CAAC,CAAC,CAAA;AAAA,YACvB,CAAA,MAAA,IAAW,IAAA,CAAK,CAAC,CAAA,KAAM,MAAA,EAAQ;AAC3B,cAAA,YAAA,CAAa,OAAO,CAAA;AACpB,cAAA,EAAA,CAAG,KAAA,EAAM;AACT,cAAA,OAAA,EAAQ;AAAA,YACZ;AAAA,UACJ,SAAS,GAAA,EAAK;AACV,YAAAA,IAAAA,CAAI,MAAM,EAAE,KAAA,EAAO,UAAU,KAAA,EAAO,GAAA,IAAO,uBAAuB,CAAA;AAAA,UACtE;AAAA,QACJ,CAAA;AAEA,QAAA,EAAA,CAAG,OAAA,GAAU,CAAC,GAAA,KAAQ;AAClB,UAAA,YAAA,CAAa,OAAO,CAAA;AACpB,UAAAA,IAAAA,CAAI,MAAM,EAAE,KAAA,EAAO,UAAU,KAAA,EAAO,GAAA,IAAO,8BAA8B,CAAA;AACzE,UAAA,MAAA,CAAO,GAAG,CAAA;AAAA,QACd,CAAA;AAAA,MACJ,CAAC,CAAA;AAAA,IACL,SAAS,GAAA,EAAK;AACV,MAAAA,IAAAA,CAAI,MAAM,EAAE,KAAA,EAAO,UAAU,KAAA,EAAO,GAAA,IAAO,uBAAuB,CAAA;AAAA,IACtE;AAAA,EACJ,CAAC,CAAA;AAED,EAAA,MAAM,OAAA,CAAQ,WAAW,aAAa,CAAA;AAEtC,EAAAA,KAAI,IAAA,CAAK,EAAE,YAAY,MAAA,CAAO,MAAA,IAAU,gBAAgB,CAAA;AAExD,EAAA,OAAO,MAAA;AACX;AAKA,eAAsB,eAAA,CAClB,QAAA,EACA,UAAA,EACA,MAAA,GAAmB,cAAA,EACE;AACrB,EAAA,MAAM,SAAuB,EAAC;AAC9B,EAAA,MAAM,WAAA,GAAc,CAAA,EAAG,QAAQ,CAAA,CAAA,EAAI,UAAU,CAAA,CAAA;AAC7C,EAAA,MAAM,YAAA,GACF,QAAA,KAAa,OAAA,GACP,oBAAA,CAAqB,UAAU,CAAA,CAAE,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,MAAA,EAAS,CAAC,CAAA,CAAE,CAAA,GACxD,CAAC,WAAW,CAAA;AAEtB,EAAAA,IAAAA,CAAI,KAAK,EAAE,QAAA,EAAU,YAAY,UAAA,EAAY,MAAA,CAAO,MAAA,EAAO,EAAG,8BAA8B,CAAA;AAI5F,EAAA,MAAM,UAAA,GAAa,IAAA,CAAK,GAAA,EAAI,GAAI,kBAAA;AAEhC,EAAA,MAAM,aAAA,GAAgB,MAAA,CAAO,GAAA,CAAI,OAAO,QAAA,KAAa;AACjD,IAAA,IAAI;AACA,MAAA,MAAM,EAAA,GAAK,IAAI,SAAA,CAAU,QAAQ,CAAA;AAEjC,MAAA,MAAM,IAAI,OAAA,CAAc,CAAC,OAAA,EAAS,MAAA,KAAW;AACzC,QAAA,MAAM,OAAA,GAAU,WAAW,MAAM;AAC7B,UAAA,EAAA,CAAG,KAAA,EAAM;AACT,UAAA,MAAA,CAAO,IAAI,KAAA,CAAM,eAAe,CAAC,CAAA;AAAA,QACrC,CAAA,EAAG,SAAA,CAAU,UAAU,CAAC,CAAA;AAExB,QAAA,EAAA,CAAG,SAAS,MAAM;AACd,UAAA,MAAM,cAAA,GAAiB,CAAA,cAAA,EAAiB,IAAA,CAAK,GAAA,EAAK,CAAA,CAAA;AAClD,UAAA,EAAA,CAAG,IAAA;AAAA,YACC,KAAK,SAAA,CAAU;AAAA,cACX,KAAA;AAAA,cACA,cAAA;AAAA,cACA;AAAA,gBACI,KAAA,EAAO,CAAC,KAAK,CAAA;AAAA,gBACb,IAAA,EAAM,YAAA;AAAA,gBACN,KAAA,EAAO;AAAA;AACX,aACH;AAAA,WACL;AAAA,QACJ,CAAA;AAEA,QAAA,EAAA,CAAG,SAAA,GAAY,CAAC,GAAA,KAAQ;AACpB,UAAA,IAAI;AACA,YAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,IAAI,CAAA;AAChC,YAAA,IAAI,IAAA,CAAK,CAAC,CAAA,KAAM,OAAA,EAAS;AACrB,cAAA,MAAA,CAAO,IAAA,CAAK,IAAA,CAAK,CAAC,CAAC,CAAA;AAAA,YACvB,CAAA,MAAA,IAAW,IAAA,CAAK,CAAC,CAAA,KAAM,MAAA,EAAQ;AAC3B,cAAA,YAAA,CAAa,OAAO,CAAA;AACpB,cAAA,EAAA,CAAG,KAAA,EAAM;AACT,cAAA,OAAA,EAAQ;AAAA,YACZ;AAAA,UACJ,SAAS,GAAA,EAAK;AACV,YAAAA,IAAAA,CAAI,MAAM,EAAE,KAAA,EAAO,UAAU,KAAA,EAAO,GAAA,IAAO,uBAAuB,CAAA;AAAA,UACtE;AAAA,QACJ,CAAA;AAEA,QAAA,EAAA,CAAG,OAAA,GAAU,CAAC,GAAA,KAAQ;AAClB,UAAA,YAAA,CAAa,OAAO,CAAA;AACpB,UAAAA,IAAAA,CAAI,MAAM,EAAE,KAAA,EAAO,UAAU,KAAA,EAAO,GAAA,IAAO,8BAA8B,CAAA;AACzE,UAAA,MAAA,CAAO,GAAG,CAAA;AAAA,QACd,CAAA;AAAA,MACJ,CAAC,CAAA;AAAA,IACL,SAAS,GAAA,EAAK;AACV,MAAAA,IAAAA,CAAI,MAAM,EAAE,KAAA,EAAO,UAAU,KAAA,EAAO,GAAA,IAAO,uBAAuB,CAAA;AAAA,IACtE;AAAA,EACJ,CAAC,CAAA;AAED,EAAA,MAAM,OAAA,CAAQ,WAAW,aAAa,CAAA;AAEtC,EAAAA,KAAI,IAAA,CAAK,EAAE,YAAY,MAAA,CAAO,MAAA,IAAU,gBAAgB,CAAA;AAExD,EAAA,OAAO,MAAA;AACX;AAwFA,eAAsB,0BAA0B,QAAA,EAAiD;AAC7F,EAAA,IAAI;AAEA,IAAA,MAAM,OAAA,GAAU,IAAI,WAAA,EAAY;AAChC,IAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,MAAA,CAAO,QAAA,CAAS,OAAO,CAAA;AAC5C,IAAA,MAAM,aAAa,MAAM,MAAA,CAAO,MAAA,CAAO,MAAA,CAAO,WAAW,IAAI,CAAA;AAC7D,IAAA,MAAM,YAAY,KAAA,CAAM,IAAA,CAAK,IAAI,UAAA,CAAW,UAAU,CAAC,CAAA;AACvD,IAAA,MAAM,UAAA,GAAa,SAAA,CAAU,GAAA,CAAI,CAAC,MAAM,CAAA,CAAE,QAAA,CAAS,EAAE,CAAA,CAAE,SAAS,CAAA,EAAG,GAAG,CAAC,CAAA,CAAE,KAAK,EAAE,CAAA;AAEhF,IAAA,OAAO,eAAe,QAAA,CAAS,cAAA;AAAA,EACnC,SAAS,GAAA,EAAK;AACV,IAAAA,KAAI,KAAA,CAAM,EAAE,KAAA,EAAO,GAAA,IAAO,uCAAuC,CAAA;AACjE,IAAA,OAAO,KAAA;AAAA,EACX;AACJ;AAEA,SAAS,UAAU,IAAA,EAAsB;AACrC,EAAA,MAAM,MAAA,GAAS,IAAA,CAAK,MAAA,CAAO,YAAA,CAAa,GAAG,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,IAAI,CAAC,CAAC,CAAA;AAC1E,EAAA,OAAO,MAAA,CAAO,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAAE,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAAE,OAAA,CAAQ,IAAA,EAAM,EAAE,CAAA;AAC1E;AAUO,SAAS,eAAe,QAAA,EAA2D;AACtF,EAAA,IAAI,MAAA;AACJ,EAAA,IAAI;AACA,IAAA,MAAA,GAAS,qBAAA,CAAsB,SAAS,OAAO,CAAA;AAAA,EACnD,CAAA,CAAA,MAAQ;AACJ,IAAA,OAAO,IAAA;AAAA,EACX;AACA,EAAA,IAAI,CAAC,MAAA,CAAO,IAAA,CAAK,OAAA,IAAW,SAAA,CAAU,OAAO,IAAA,CAAK,SAAS,CAAA,KAAM,IAAA,EAAM,OAAO,IAAA;AAC9E,EAAA,MAAM,EAAE,UAAA,EAAY,KAAA,EAAO,GAAG,MAAK,GAAI,QAAA;AACvC,EAAA,OAAO;AAAA,IACH,GAAG,IAAA;AAAA,IACH,OAAA,EAAS,OAAO,IAAA,CAAK,OAAA;AAAA,IACrB,UAAA,EAAY,OAAO,IAAA,CAAK,UAAA;AAAA,IACxB,SAAA,EAAW,OAAO,IAAA,CAAK,SAAA;AAAA,IACvB,GAAI,OAAO,OAAA,KAAY,MAAA,GAAY,EAAE,UAAA,EAAY,MAAA,CAAO,OAAA,EAAQ,GAAI,EAAC;AAAA,IACrE,cAAA,EAAgB,SAAA,CAAU,QAAA,CAAS,OAAO;AAAA,GAC9C;AACJ;AASA,eAAsB,sBAAA,CAClB,MAAA,EACA,MAAA,GAAwE,MAAM,IAAA,EAChD;AAC9B,EAAA,MAAM,MAAA,uBAAa,GAAA,EAAmC;AACtD,EAAA,KAAA,MAAW,SAAS,MAAA,EAAQ;AACxB,IAAA,MAAM,GAAA,GAAM,0BAA0B,KAAK,CAAA;AAC3C,IAAA,IAAI,CAAC,GAAA,IAAO,CAAE,MAAM,yBAAA,CAA0B,GAAG,CAAA,EAAI;AACrD,IAAA,MAAM,QAAA,GAAW,eAAe,GAAG,CAAA;AACnC,IAAA,IAAI,CAAC,QAAA,IAAY,CAAC,MAAA,CAAO,KAAA,EAAO,QAAQ,CAAA,EAAG;AAC3C,IAAA,MAAM,OAAO,MAAA,CAAO,GAAA,CAAI,QAAA,CAAS,cAAc,KAAK,EAAC;AACrD,IAAA,IAAI,CAAC,IAAA,CAAK,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,SAAA,KAAc,QAAA,CAAS,SAAA,IAAa,CAAA,CAAE,MAAA,KAAW,QAAA,CAAS,MAAM,CAAA,EAAG;AACvF,MAAA,IAAA,CAAK,KAAK,QAAQ,CAAA;AAAA,IACtB;AACA,IAAA,MAAA,CAAO,GAAA,CAAI,QAAA,CAAS,cAAA,EAAgB,IAAI,CAAA;AAAA,EAC5C;AACA,EAAA,MAAM,OAA8B,EAAC;AACrC,EAAA,KAAA,MAAW,IAAA,IAAQ,MAAA,CAAO,MAAA,EAAO,EAAG;AAChC,IAAA,IAAI,IAAA,CAAK,WAAW,CAAA,EAAG;AACnB,MAAA,IAAA,CAAK,IAAA,CAAK,IAAA,CAAK,CAAC,CAAE,CAAA;AAClB,MAAA;AAAA,IACJ;AACA,IAAA,IAAI,KAAA;AACJ,IAAA,KAAA,MAAW,KAAK,IAAA,EAAM;AAClB,MAAA,IAAI,MAAM,gBAAA,CAAiB,EAAE,IAAA,EAAM,CAAA,CAAE,SAAS,GAAA,EAAK,CAAA,CAAE,OAAA,EAAS,GAAA,EAAK,EAAE,SAAA,EAAW,MAAA,EAAQ,CAAA,CAAE,MAAA,EAAQ,CAAA,EAAG;AACjG,QAAA,KAAA,GAAQ,CAAA;AACR,QAAA;AAAA,MACJ;AAAA,IACJ;AACA,IAAA,IAAA,CAAK,IAAA,CAAK,KAAA,IAAS,IAAA,CAAK,CAAC,CAAE,CAAA;AAAA,EAC/B;AACA,EAAA,OAAO,IAAA;AACX;AAKO,SAAS,0BAA0B,KAAA,EAA+C;AACrF,EAAA,IAAI;AACA,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,KAAA,CAAM,KAAA,CAAM,OAAO,CAAA;AACzC,IAAA,OAAO,QAAA;AAAA,EACX,SAAS,GAAA,EAAK;AACV,IAAAA,IAAAA,CAAI,MAAM,EAAE,OAAA,EAAS,MAAM,EAAA,EAAI,KAAA,EAAO,GAAA,EAAI,EAAG,wCAAwC,CAAA;AACrF,IAAA,OAAO,IAAA;AAAA,EACX;AACJ;;;ACxuBA,IAAMA,IAAAA,GAAM,aAAa,kBAAkB,CAAA;AA+CpC,SAAS,gBAAA,GAA4B;AACxC,EAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AAC/B,IAAA,OAAO,KAAA;AAAA,EACX;AAEA,EAAA,OAAO,CAAC,CAAE,MAAA,CAAuD,KAAA;AACrE;AA2BA,eAAsB,eAClB,KAAA,EAC0B;AAC1B,EAAA,IAAI,CAAC,kBAAiB,EAAG;AACrB,IAAAA,IAAAA,CAAI,KAAK,gCAAgC,CAAA;AACzC,IAAA,OAAO,IAAA;AAAA,EACX;AAEA,EAAA,IAAI;AACA,IAAA,MAAM,QAAS,MAAA,CAAuD,KAAA;AAGtE,IAAA,MAAM,WAAA,GAAc,MAAM,KAAA,CAAM,SAAA,CAAU,KAAK,CAAA;AAE/C,IAAAA,KAAI,IAAA,CAAK,EAAE,SAAS,WAAA,CAAY,EAAA,IAAM,gCAAgC,CAAA;AAEtE,IAAA,OAAO,WAAA;AAAA,EACX,SAAS,GAAA,EAAK;AACV,IAAAA,KAAI,KAAA,CAAM,EAAE,KAAA,EAAO,GAAA,IAAO,4BAA4B,CAAA;AACtD,IAAA,OAAO,IAAA;AAAA,EACX;AACJ;;;AChGA,IAAMA,IAAAA,GAAM,aAAa,iBAAiB,CAAA;AA2DnC,IAAM,gBAAA,GAAN,cAA+B,KAAA,CAAM;AAAA,EACxC,WAAA,CACI,SACgB,IAAA,EAClB;AACE,IAAA,KAAA,CAAM,OAAO,CAAA;AAFG,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AAGhB,IAAA,IAAA,CAAK,IAAA,GAAO,kBAAA;AAAA,EAChB;AACJ;AAoBA,eAAsB,kBAClB,OAAA,EAC4B;AAC5B,EAAA,MAAM,EAAE,OAAA,EAAS,SAAA,EAAW,MAAA,EAAQ,OAAA,EAAS,UAAA,GAAa,EAAC,EAAG,UAAA,GAAa,EAAC,EAAE,GAAI,OAAA;AAElF,EAAAA,IAAAA,CAAI,KAAK,EAAE,OAAA,EAAS,eAAe,UAAA,CAAW,MAAA,IAAU,sBAAsB,CAAA;AAE9E,EAAA,MAAM,WAAW,MAAM,yBAAA;AAAA,IACnB,OAAA;AAAA,IACA,SAAA;AAAA,IACA,MAAA;AAAA,IACA,OAAA;AAAA,IACA,UAAA;AAAA,IACA;AAAA,GACJ;AAGA,EAAA,MAAM,OAAA,GAAU,MAAM,yBAAA,CAA0B,QAAQ,CAAA;AACxD,EAAA,IAAI,CAAC,OAAA,EAAS;AACV,IAAA,MAAM,IAAI,gBAAA;AAAA,MACN,6CAAA;AAAA,MACA;AAAA,KACJ;AAAA,EACJ;AAEA,EAAAA,KAAI,IAAA,CAAK,EAAE,eAAe,QAAA,CAAS,cAAA,IAAkB,kCAAkC,CAAA;AAEvF,EAAA,OAAO,QAAA;AACX;AAqBA,eAAsB,mBAClB,OAAA,EACsB;AACtB,EAAA,MAAM,EAAE,QAAA,EAAU,IAAA,EAAM,MAAA,GAAS,gBAAe,GAAI,OAAA;AAEpD,EAAAA,IAAAA,CAAI,IAAA;AAAA,IACA,EAAE,aAAA,EAAe,QAAA,CAAS,cAAA,EAAgB,UAAA,EAAY,OAAO,MAAA,EAAO;AAAA,IACpE;AAAA,GACJ;AAGA,EAAA,MAAM,aAAA,GAAgB,sBAAA,CAAuB,QAAA,EAAU,IAAI,CAAA;AAM3D,EAAA,IAAI,CAAC,kBAAiB,EAAG;AACrB,IAAAA,IAAAA,CAAI,KAAK,kDAA6C,CAAA;AACtD,IAAA,MAAM,IAAI,KAAA;AAAA,MACN;AAAA,KACJ;AAAA,EACJ;AAEA,EAAAA,IAAAA,CAAI,KAAK,4BAA4B,CAAA;AACrC,EAAA,MAAM,WAAA,GAAc,MAAM,cAAA,CAAe,aAAa,CAAA;AACtD,EAAA,IAAI,CAAC,WAAA,EAAa;AACd,IAAA,MAAM,IAAI,MAAM,4DAA4D,CAAA;AAAA,EAChF;AAEA,EAAA,MAAM,MAAA,GAAS,MAAM,eAAA,CAAgB,WAAA,EAAa,MAAM,CAAA;AAExD,EAAAA,IAAAA,CAAI,IAAA;AAAA,IACA;AAAA,MACI,eAAe,QAAA,CAAS,cAAA;AAAA,MACxB,YAAA,EAAc,OAAO,OAAA,CAAQ,MAAA;AAAA,MAC7B,WAAA,EAAa,OAAO,MAAA,CAAO;AAAA,KAC/B;AAAA,IACA;AAAA,GACJ;AAEA,EAAA,OAAO,MAAA;AACX;AAOA,SAAS,oBAAA,CAAqB,OAAmB,QAAA,EAAwC;AACrF,EAAA,MAAM,CAAA,GAAI,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,CAAC,CAAA,KAAM,GAAG,CAAA,GAAI,CAAC,CAAA;AAClD,EAAA,MAAM,KAAK,QAAA,CAAS,cAAA;AACpB,EAAA,IAAI,MAAM,EAAA,IAAM,CAAA,KAAM,CAAA,YAAA,EAAe,EAAE,IAAI,OAAO,KAAA;AAClD,EAAA,OAAO,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,CAAC,CAAA,KAAM,GAAA,IAAO,CAAA,CAAE,CAAC,CAAA,KAAM,SAAS,OAAO,CAAA;AAC3E;AAKA,eAAsB,qBAClB,OAAA,EAC8B;AAC9B,EAAA,MAAM,EAAE,aAAA,EAAe,OAAA,EAAS,QAAA,EAAU,MAAA,GAAS,gBAAe,GAAI,OAAA;AAEtE,EAAA,IAAI,SAAuB,EAAC;AAE5B,EAAA,IAAI,aAAA,EAAe;AACf,IAAAA,IAAAA,CAAI,IAAA,CAAK,EAAE,aAAA,IAAiB,+BAA+B,CAAA;AAC3D,IAAA,MAAA,GAAS,MAAM,oBAAA,CAAqB,aAAA,EAAe,MAAM,CAAA;AAAA,EAC7D,WAAW,OAAA,EAAS;AAChB,IAAAA,IAAAA,CAAI,IAAA,CAAK,EAAE,OAAA,IAAW,gCAAgC,CAAA;AACtD,IAAA,MAAA,GAAS,MAAM,cAAA,CAAe,OAAA,EAAS,MAAM,CAAA;AAAA,EACjD,WAAW,QAAA,EAAU;AACjB,IAAAA,IAAAA,CAAI,IAAA,CAAK,EAAE,QAAA,IAAY,yBAAyB,CAAA;AAChD,IAAA,MAAA,GAAS,MAAM,eAAA,CAAgB,QAAA,CAAS,QAAA,EAAU,QAAA,CAAS,YAAY,MAAM,CAAA;AAAA,EACjF,CAAA,MAAO;AACH,IAAA,MAAM,IAAI,MAAM,kDAAkD,CAAA;AAAA,EACtE;AAKA,EAAA,MAAM,YAAY,MAAM,sBAAA,CAAuB,MAAA,EAAQ,CAAC,OAAO,QAAA,KAAa;AACxE,IAAA,IAAI,oBAAA,CAAqB,KAAA,EAAO,QAAQ,CAAA,EAAG,OAAO,IAAA;AAClD,IAAAA,KAAI,IAAA,CAAK,EAAE,SAAS,KAAA,CAAM,EAAA,IAAM,4DAA4D,CAAA;AAC5F,IAAA,OAAO,KAAA;AAAA,EACX,CAAC,CAAA;AAED,EAAAA,IAAAA,CAAI,IAAA,CAAK,EAAE,UAAA,EAAY,MAAA,CAAO,QAAQ,UAAA,EAAY,SAAA,CAAU,MAAA,EAAO,EAAG,oBAAoB,CAAA;AAE1F,EAAA,OAAO,SAAA;AACX;AAKA,eAAsB,qBAAA,CAClB,aAAA,EACA,MAAA,GAAmB,cAAA,EACgB;AACnC,EAAAA,IAAAA,CAAI,IAAA,CAAK,EAAE,aAAA,IAAiB,6BAA6B,CAAA;AAEzD,EAAA,MAAM,YAAY,MAAM,oBAAA,CAAqB,EAAE,aAAA,EAAe,QAAQ,CAAA;AAEtE,EAAA,IAAI,SAAA,CAAU,WAAW,CAAA,EAAG;AACxB,IAAAA,IAAAA,CAAI,IAAA,CAAK,EAAE,aAAA,IAAiB,oCAAoC,CAAA;AAChE,IAAA,OAAO,IAAA;AAAA,EACX;AAEA,EAAA,IAAI,SAAA,CAAU,SAAS,CAAA,EAAG;AACtB,IAAAA,IAAAA,CAAI,IAAA;AAAA,MACA,EAAE,aAAA,EAAe,KAAA,EAAO,SAAA,CAAU,MAAA,EAAO;AAAA,MACzC;AAAA,KACJ;AAAA,EACJ;AAEA,EAAA,OAAO,SAAA,CAAU,CAAC,CAAA,IAAK,IAAA;AAC3B;AAKA,eAAsB,yBAAA,CAClB,OAAA,EACA,MAAA,GAAmB,cAAA,EACW;AAC9B,EAAAA,IAAAA,CAAI,IAAA,CAAK,EAAE,OAAA,IAAW,sCAAsC,CAAA;AAC5D,EAAA,OAAO,oBAAA,CAAqB,EAAE,OAAA,EAAS,MAAA,EAAQ,CAAA;AACnD;AAKA,eAAsB,0BAAA,CAClB,QAAA,EACA,UAAA,EACA,MAAA,GAAmB,cAAA,EACW;AAC9B,EAAAA,KAAI,IAAA,CAAK,EAAE,QAAA,EAAU,UAAA,IAAc,uCAAuC,CAAA;AAC1E,EAAA,OAAO,oBAAA,CAAqB,EAAE,QAAA,EAAU,EAAE,UAAU,UAAA,EAAW,EAAG,QAAQ,CAAA;AAC9E;AAMO,SAAS,2BAA2B,UAAA,EAAuC;AAC9E,EAAA,OAAO,UAAA,CAAW,GAAA,CAAI,CAAC,EAAA,KAAO,CAAA,EAAG,EAAA,CAAG,QAAQ,CAAA,CAAA,EAAI,EAAA,CAAG,UAAU,CAAA,CAAE,CAAA,CAAE,KAAK,IAAI,CAAA;AAC9E;AAOO,SAAS,kBAAA,CACZ,aAAA,EACA,OAAA,GAAU,wBAAA,EACJ;AACN,EAAA,OAAO,CAAA,EAAG,OAAO,CAAA,QAAA,EAAW,aAAa,CAAA,CAAA;AAC7C;AAOO,SAAS,4BAA4B,GAAA,EAA4B;AACpE,EAAA,IAAI;AACA,IAAA,MAAM,CAAA,GAAI,IAAI,GAAA,CAAI,GAAG,CAAA;AAErB,IAAA,MAAM,CAAA,GAAI,CAAA,CAAE,QAAA,CAAS,KAAA,CAAM,gCAAgC,CAAA;AAC3D,IAAA,IAAI,CAAA,EAAG,OAAO,CAAA,CAAE,CAAC,EAAG,WAAA,EAAY;AAEhC,IAAA,MAAM,CAAA,GAAI,CAAA,CAAE,YAAA,CAAa,GAAA,CAAI,IAAI,CAAA;AACjC,IAAA,IAAI,KAAK,iBAAA,CAAkB,IAAA,CAAK,CAAC,CAAA,EAAG,OAAO,EAAE,WAAA,EAAY;AACzD,IAAA,OAAO,IAAA;AAAA,EACX,CAAA,CAAA,MAAQ;AACJ,IAAA,OAAO,IAAA;AAAA,EACX;AACJ","file":"attestation.mjs","sourcesContent":["/**\n * Canonical message builder per SPEC.md §2\n *\n * - LF line endings, exactly one trailing LF\n * - 7 fixed core lines in strict order and wording\n * - Optional extensions appended as key: value with lexicographically sorted lowercase keys\n */\n\nimport type { AttestationEnvelope, IdentityBinding, Scheme } from './types';\n\n/**\n * Input for building a canonical message\n */\nexport interface CanonicalInput {\n /** Bitcoin address */\n address: string;\n /** Optional identity bindings */\n identities?: IdentityBinding[];\n}\n\n/**\n * Extension key-value pairs for canonical message\n */\nexport type Extensions = Record<string, string | undefined>;\n\n/**\n * Convert bytes to lowercase hex string\n * @param bytes - Byte array to convert\n * @returns Lowercase hex string\n */\nfunction toLowerHex(bytes: Uint8Array): string {\n return Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n}\n\n/**\n * Generate a random 16-byte nonce as lowercase hex\n * Uses Web Crypto API when available, falls back to Math.random()\n * @returns 32-character lowercase hex string\n */\nexport function randomNonce16BHexLower(): string {\n const arr = new Uint8Array(16);\n\n try {\n if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {\n crypto.getRandomValues(arr);\n return toLowerHex(arr);\n }\n } catch {\n // Fall through to non-cryptographic fallback\n }\n\n // No silent Math.random fallback: a nonce is a load-bearing security\n // primitive. A deployment without `crypto.getRandomValues` MUST fix its\n // environment rather than sign with weak randomness.\n throw new Error('crypto.getRandomValues unavailable — refusing to mint a weak nonce');\n}\n\n/**\n * Reject characters that would break the line-oriented canonical grammar\n * or the comma-separated identities list. If any of these slipped through,\n * an attacker could smuggle a forged `address:` or extension line into the\n * signed payload: `identities: alice\\naddress: bc1qEVIL…` would parse as a\n * second address line. Catch it at the boundary, not at verification time.\n *\n * Also rejects a colon in the identifier so a bad actor can't turn\n * `github:alice:extra` into a different protocol binding on parse.\n */\nconst IDENTITY_PROTOCOL_RE = /^[a-z][a-z0-9_-]*$/;\nconst IDENTITY_FORBIDDEN_CHARS = /[\\r\\n,]/;\n\nfunction assertSafeIdentity({ protocol, identifier }: IdentityBinding): void {\n if (!IDENTITY_PROTOCOL_RE.test(protocol)) {\n throw new Error(\n `invalid identity protocol: ${JSON.stringify(protocol)} — must match /^[a-z][a-z0-9_-]*$/`\n );\n }\n if (!identifier || IDENTITY_FORBIDDEN_CHARS.test(identifier)) {\n throw new Error(\n `invalid identity identifier: ${JSON.stringify(identifier)} — must be non-empty and contain no newlines or commas`\n );\n }\n}\n\n/**\n * Format identities as comma-separated protocol:identifier pairs, sorted lexicographically\n * @param identities - Array of identity bindings\n * @returns Comma-separated string of protocol:identifier pairs\n */\nexport function formatIdentities(identities: IdentityBinding[] = []): string {\n if (identities.length === 0) {\n return '';\n }\n for (const id of identities) assertSafeIdentity(id);\n\n // Format as protocol:identifier and sort lexicographically\n const formatted = identities\n .map(({ protocol, identifier }) => `${protocol}:${identifier}`)\n .sort();\n\n return formatted.join(',');\n}\n\n/**\n * Parse identities from comma-separated protocol:identifier format\n * @param identitiesStr - Comma-separated identity string\n * @returns Array of identity bindings\n * @throws {Error} If identity format is invalid\n */\nexport function parseIdentities(identitiesStr: string): IdentityBinding[] {\n if (!identitiesStr || identitiesStr.trim() === '') {\n return [];\n }\n // The identities field lives on one line of the canonical message. If it\n // contains a raw LF/CR, it's either corrupt or an attempt to smuggle a\n // fake core line into the parser. Refuse instead of trying to recover.\n if (IDENTITY_FORBIDDEN_CHARS.test(identitiesStr.replace(/,/g, ''))) {\n throw new Error('identities field contains forbidden characters (newline)');\n }\n\n const bindings = identitiesStr.split(',').map((binding) => {\n const colonIndex = binding.indexOf(':');\n if (colonIndex === -1) {\n throw new Error(`Invalid identity binding format: ${binding}`);\n }\n return {\n protocol: binding.substring(0, colonIndex).trim(),\n identifier: binding.substring(colonIndex + 1).trim(),\n };\n });\n\n for (const b of bindings) {\n if (!b.protocol || !b.identifier) {\n throw new Error(`Invalid identity binding: ${b.protocol}:${b.identifier}`);\n }\n assertSafeIdentity(b);\n }\n return bindings;\n}\n\n/**\n * Optional deterministic overrides for message building. Only intended for\n * tests / conformance vectors; production callers should let the builder\n * mint a fresh nonce + timestamp per message.\n */\nexport interface BuildOptions {\n /** 32-char lowercase hex. Default: freshly-generated cryptographic nonce. */\n nonce?: string;\n /** RFC-3339 UTC. Default: `new Date().toISOString()`. */\n issuedAt?: string;\n}\n\n/**\n * Build a canonical message following OrangeCheck Protocol v0 specification\n * @param input - Canonical input with address and identities\n * @param extensions - Optional extension key-value pairs\n * @param options - Optional deterministic overrides (testing only)\n * @returns Canonical message string with LF line endings\n */\nexport function buildCanonicalMessage(\n { address, identities }: CanonicalInput,\n extensions: Extensions = {},\n options: BuildOptions = {}\n): string {\n const nonce = options.nonce ?? randomNonce16BHexLower();\n if (!/^[0-9a-f]{32}$/.test(nonce)) {\n throw new Error(`invalid nonce: must be 32 lowercase hex chars`);\n }\n const issuedAt = options.issuedAt ?? new Date().toISOString();\n\n // Format identities\n const identitiesStr = formatIdentities(identities);\n\n // Core (strict wording + order per SPEC.md §2)\n const core = [\n 'orangecheck',\n `identities: ${identitiesStr}`,\n `address: ${address}`,\n 'purpose: portable reputation attestation (non-custodial)',\n `nonce: ${nonce}`,\n `issued_at: ${issuedAt}`,\n 'ack: I attest control of this address and bind it to my identities.',\n ];\n\n // Normalize and sort extensions\n const normExt: Record<string, string> = {};\n for (const [k, v] of Object.entries(extensions)) {\n if (v == null || v === '') {\n continue;\n }\n const key = k.toLowerCase();\n normExt[key] = String(v);\n }\n\n const extLines = Object.keys(normExt)\n .sort()\n .map((k) => `${k}: ${normExt[k]!}`);\n\n const all = [...core, ...extLines];\n\n // Join with LF and ensure exactly one trailing LF\n return all.join('\\n') + '\\n';\n}\n\n/**\n * Generate attestation ID from canonical message\n *\n * attestation_id = SHA-256(canonical_message)\n *\n * @param message - Canonical message string\n * @returns Lowercase hex SHA-256 hash\n */\nexport async function generateAttestationId(message: string): Promise<string> {\n // Convert message to UTF-8 bytes\n const encoder = new TextEncoder();\n const data = encoder.encode(message);\n\n // Compute SHA-256 hash\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n\n // Convert to lowercase hex\n return toLowerHex(new Uint8Array(hashBuffer));\n}\n\n/**\n * Create an attestation envelope for publishing\n *\n * This wraps the canonical message with metadata for Nostr publishing\n *\n * @param message - Canonical message\n * @param signature - BIP-322 or legacy signature\n * @param scheme - Signature scheme used\n * @param address - Bitcoin address\n * @param identities - Identity bindings\n * @param extensions - Optional extensions\n * @returns Attestation envelope ready for publishing\n */\nexport async function createAttestationEnvelope(\n message: string,\n signature: string,\n scheme: Scheme,\n address: string,\n identities: IdentityBinding[],\n extensions: Extensions = {}\n): Promise<AttestationEnvelope> {\n const attestation_id = await generateAttestationId(message);\n\n // Extract issued_at from message\n const issuedAtMatch = message.match(/issued_at: ([^\\n]+)/);\n const issued_at = issuedAtMatch?.[1] ?? new Date().toISOString();\n\n // Extract expires from extensions if present\n const expires_at = extensions.expires;\n\n // Extract relay hints from extensions if present\n const relay_hints = extensions.relay_hints?.split(',').map((r) => r.trim());\n\n // Extract publish targets from extensions if present\n const publish_targets = extensions.publish?.split(',').map((t) => t.trim());\n\n // Base64url encode the message\n const encoder = new TextEncoder();\n const messageBytes = encoder.encode(message);\n const base64 = btoa(String.fromCharCode(...messageBytes));\n const message_b64url = base64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');\n\n // Construct verification URL\n const verification_url = `https://attest.ochk.io/attest/${attestation_id}`;\n\n return {\n attestation_id,\n scheme,\n address,\n identities,\n message,\n message_b64url,\n signature,\n issued_at,\n expires_at,\n verification_url,\n publish_targets,\n relay_hints,\n };\n}\n","import { bech32 } from '@scure/base';\n\nconst HEX_64_RE = /^[0-9a-f]{64}$/;\n\nfunction toHex(bytes: Uint8Array): string {\n let out = '';\n for (const b of bytes) out += b.toString(16).padStart(2, '0');\n return out;\n}\n\n/** A Nostr public key as lowercase hex, from either `npub1…` or hex. Null if it is neither. */\nexport function nostrPubkeyToHex(value: string): string | null {\n const trimmed = value.trim().toLowerCase();\n if (HEX_64_RE.test(trimmed)) return trimmed;\n if (!trimmed.startsWith('npub1')) return null;\n try {\n const decoded = bech32.decode(trimmed as `${string}1${string}`, 1023);\n if (decoded.prefix !== 'npub') return null;\n const bytes = bech32.fromWords(decoded.words);\n return bytes.length === 32 ? toHex(Uint8Array.from(bytes)) : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Every spelling of one Nostr key an attestation may carry in its `i` tag.\n * SPEC §2 prescribes `npub1…`; the reference site also accepts hex.\n */\nexport function nostrIdentifierForms(identifier: string): string[] {\n const hex = nostrPubkeyToHex(identifier);\n if (!hex) return [identifier];\n const bytes = Uint8Array.from(hex.match(/../g)!.map((b) => parseInt(b, 16)));\n return [bech32.encode('npub', bech32.toWords(bytes), 1023), hex];\n}\n","/**\n * Simple logger for SDK\n * Can be disabled in production builds\n */\n\n/**\n * Log level type\n */\nexport type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';\n\n/**\n * Logger context type\n */\nexport type LogContext = Record<string, unknown>;\n\n/**\n * Logger interface\n */\nexport interface Logger {\n debug: (contextOrMessage: string | LogContext, message?: string) => void;\n info: (contextOrMessage: string | LogContext, message?: string) => void;\n warn: (contextOrMessage: string | LogContext, message?: string) => void;\n error: (contextOrMessage: string | LogContext, message?: string) => void;\n}\n\nlet logLevel: LogLevel = 'none';\n\n/**\n * Set the global log level\n * @param level - Log level to set\n */\nexport function setLogLevel(level: LogLevel): void {\n logLevel = level;\n}\n\n/**\n * Get the current log level\n * @returns Current log level\n */\nexport function getLogLevel(): LogLevel {\n return logLevel;\n}\n\n/**\n * Create a namespaced logger\n * @param namespace - Logger namespace\n * @returns Logger instance\n */\nexport function createLogger(namespace: string): Logger {\n const shouldLog = (level: Exclude<LogLevel, 'none'>): boolean => {\n if (logLevel === 'none') return false;\n\n const levels: Exclude<LogLevel, 'none'>[] = ['debug', 'info', 'warn', 'error'];\n const currentLevelIndex = levels.indexOf(logLevel as Exclude<LogLevel, 'none'>);\n const messageLevelIndex = levels.indexOf(level);\n\n return messageLevelIndex >= currentLevelIndex;\n };\n\n const formatMessage = (msg: string): string => `[${namespace}] ${msg}`;\n\n return {\n debug: (contextOrMessage: string | LogContext, message?: string): void => {\n if (shouldLog('debug')) {\n if (typeof contextOrMessage === 'string') {\n console.debug(formatMessage(contextOrMessage));\n } else {\n console.debug(formatMessage(message ?? 'Debug'), contextOrMessage);\n }\n }\n },\n info: (contextOrMessage: string | LogContext, message?: string): void => {\n if (shouldLog('info')) {\n if (typeof contextOrMessage === 'string') {\n console.info(formatMessage(contextOrMessage));\n } else {\n console.info(formatMessage(message ?? 'Info'), contextOrMessage);\n }\n }\n },\n warn: (contextOrMessage: string | LogContext, message?: string): void => {\n if (shouldLog('warn')) {\n if (typeof contextOrMessage === 'string') {\n console.warn(formatMessage(contextOrMessage));\n } else {\n console.warn(formatMessage(message ?? 'Warning'), contextOrMessage);\n }\n }\n },\n error: (contextOrMessage: string | LogContext, message?: string): void => {\n if (shouldLog('error')) {\n if (typeof contextOrMessage === 'string') {\n console.error(formatMessage(contextOrMessage));\n } else {\n console.error(formatMessage(message ?? 'Error'), contextOrMessage);\n }\n }\n },\n };\n}\n\n/**\n * Log verification error\n * @param protocol - Identity protocol\n * @param identifier - Identity identifier\n * @param error - Error object\n * @param context - Additional context\n */\nexport function logVerificationError(\n protocol: string,\n identifier: string,\n error: Error,\n context?: LogContext\n): void {\n const logger = createLogger('identity-verification');\n logger.error(\n {\n protocol,\n identifier,\n error: error.message,\n stack: error.stack,\n ...context,\n },\n 'Verification failed'\n );\n}\n","const RFC3339 = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:\\d{2})$/;\n\n/** An RFC 3339 date-time as epoch milliseconds, or null when it is not one. */\nexport function rfc3339Ms(value: string | undefined): number | null {\n if (!value || !RFC3339.test(value)) return null;\n const ms = Date.parse(value);\n return Number.isNaN(ms) ? null : ms;\n}\n","import type { Scheme } from '../types';\n\nimport { Buffer } from 'buffer';\n\n/** A signature as base64: hex input is converted, anything else passes through. */\nexport function toBase64Maybe(sig: string): string {\n if (/^[0-9a-fA-F]+$/.test(sig) && sig.length % 2 === 0) {\n return Buffer.from(sig, 'hex').toString('base64');\n }\n return sig;\n}\n\n/**\n * The scheme a signature's bytes say it is, the same rule @orangecheck/bip322\n * verifies by: 65 bytes is a BIP-137 compact signature, whatever the address,\n * and anything else is BIP-322 simple. `null` for a BIP-322 full or\n * proof-of-funds signature, which is not supported.\n */\nexport function signatureScheme(sigBase64: string): Scheme | null {\n if (/^(ful|pof)/.test(sigBase64)) return null;\n if (sigBase64.startsWith('smp')) return 'bip322';\n return Buffer.from(sigBase64, 'base64').length === 65 ? 'legacy' : 'bip322';\n}\n","/**\n * OrangeCheck Protocol Verification\n *\n * Core verification logic for BIP-322 and legacy signatures,\n * UTXO analysis, and reputation metrics computation\n */\n\nimport type { ScoringAlgorithm } from './scoring';\nimport type {\n IdentityBinding,\n Metrics,\n Network,\n Scheme,\n StatusCode,\n VerifyInput,\n VerifyOptions,\n VerifyOutcome,\n} from './types';\n\nimport { verifySignature as verifyBitcoinSignature } from '@orangecheck/bip322';\n\nimport { generateAttestationId, parseIdentities } from './canonical';\nimport { computeAllScores, computeScore } from './scoring';\nimport { createLogger } from './utils/logger';\nimport { signatureScheme, toBase64Maybe } from './utils/signature';\nimport { rfc3339Ms } from './utils/time';\n\nconst log = createLogger('ocp/verify');\n\n/**\n * Scoring function per OCP v0 specification (SPEC.md §6)\n * Formula: score = round( ln(1 + sats_bonded) * (1 + days_unspent / 30), 2 )\n *\n * This produces an unbounded score that combines both sats and time.\n * Typical ranges:\n * - 10k sats, 30 days ≈ 18\n * - 100k sats, 90 days ≈ 46\n * - 1M sats, 365 days ≈ 182\n * - 100M sats, 365 days ≈ 243\n */\n/**\n * Export scoring functions for use in other modules\n */\nexport { computeScore, computeAllScores, type ScoringAlgorithm };\n\n/**\n * Input for signature verification\n */\ninterface SignatureVerifyInput {\n /** Bitcoin address */\n addr: string;\n /** Message to verify */\n msg: string;\n /** Signature (hex or base64) */\n sig: string;\n /** @deprecated Ignored: the signature's own bytes decide the scheme. */\n scheme?: Scheme;\n}\n\n/**\n * Verify a signature and name the scheme that verified it. The scheme comes\n * from the signature's bytes (see `signatureScheme`), never from the address\n * or a caller's hint, so a valid BIP-137 signature verifies on every address\n * it can stand for, whatever its recovery id.\n */\nasync function verifySignature({ addr, msg, sig }: SignatureVerifyInput): Promise<StatusCode> {\n const sigBase64 = toBase64Maybe(sig);\n const scheme = signatureScheme(sigBase64);\n if (!scheme) return 'sig_unsupported_script';\n\n const ok = verifyBitcoinSignature(addr, msg, sigBase64);\n log.info({ addr, ok, scheme, msgLength: msg.length }, 'Signature verification result');\n if (!ok) return 'sig_invalid';\n return scheme === 'legacy' ? 'sig_ok_legacy' : 'sig_ok_bip322';\n}\n\n/** True when `sig` is a valid signature by `addr` over `msg`; no chain lookup. */\nexport async function signatureIsValid(input: SignatureVerifyInput): Promise<boolean> {\n try {\n const code = await verifySignature(input);\n return code === 'sig_ok_bip322' || code === 'sig_ok_legacy';\n } catch {\n return false;\n }\n}\n\n// Canonical message parsing per SPEC + our builder (lowercase keys, extensions)\nexport function parseCanonicalMessage(msg: string) {\n const lines = msg.split('\\n').map((l) => l.trim());\n // `.split(':', 2)[1]` truncates values containing a colon (e.g. URLs in\n // extensions, or npubs in identities). Use the first colon as the\n // separator and keep everything after it — same approach as the\n // extensions loop below.\n const getVal = (prefix: string): string | undefined => {\n const needle = prefix + ':';\n const hit = lines.find((l) => l.toLowerCase().startsWith(needle));\n if (!hit) return undefined;\n const idx = hit.indexOf(':');\n return hit.slice(idx + 1).trim();\n };\n\n const address = getVal('address') || '';\n const identitiesStr = getVal('identities') || '';\n const issued_at = getVal('issued_at') || '';\n // Present but empty is kept as '', so a reader can refuse it rather than skip it.\n const expires = getVal('expires');\n\n // Parse identities\n let identities: IdentityBinding[] = [];\n try {\n identities = parseIdentities(identitiesStr);\n } catch (err) {\n log.warn({ identitiesStr, error: err }, 'Failed to parse identities field');\n }\n\n // Extensions: collect all key: value pairs excluding core known keys\n const coreKeys = new Set([\n 'orangecheck',\n 'identities',\n 'address',\n 'purpose',\n 'nonce',\n 'issued_at',\n 'ack',\n ]);\n const extensions: Record<string, string> = {};\n for (const line of lines) {\n const idx = line.indexOf(':');\n if (idx === -1) continue;\n const key = line.slice(0, idx).trim().toLowerCase();\n const val = line.slice(idx + 1).trim();\n if (!coreKeys.has(key) && val) extensions[key] = val;\n }\n\n const network = (extensions['network'] || 'mainnet') as Network;\n\n // Parse bond extension if present (integer satoshis)\n const bondStr = extensions['bond'];\n const bond = bondStr ? parseInt(bondStr, 10) : undefined;\n\n return {\n core: { address, identities, issued_at },\n expires,\n network,\n extensions,\n bond: bond !== undefined && !isNaN(bond) && bond > 0 ? bond : undefined,\n };\n}\n\n/**\n * UTXO type from Esplora API\n */\ninterface EsploraUtxo {\n /** Transaction ID */\n txid: string;\n /** Output index */\n vout: number;\n /** Value in satoshis */\n value: number;\n /** Confirmation status */\n status: {\n /** Whether UTXO is confirmed */\n confirmed: boolean;\n /** Block height (if confirmed) */\n block_height?: number;\n /** Block timestamp (if confirmed) */\n block_time?: number;\n };\n}\n\n/**\n * Fetch UTXOs for an address from Esplora API\n *\n * Tries multiple endpoints in order with automatic fallback\n *\n * @param address - Bitcoin address\n * @param network - Bitcoin network\n * @param opts - Optional custom Esplora endpoints\n * @returns Array of UTXOs\n * @throws {Error} If all endpoints fail\n */\nasync function getAddressUtxos(\n address: string,\n network: Network,\n opts?: {\n esploraMainnetBase?: string;\n esploraSignetBase?: string;\n esploraTestnetBase?: string;\n }\n): Promise<EsploraUtxo[]> {\n // Define primary and fallback endpoints\n const endpoints =\n network === 'signet'\n ? [\n opts?.esploraSignetBase || 'https://mempool.space/signet/api',\n 'https://blockstream.info/signet/api',\n ]\n : network === 'testnet'\n ? [\n opts?.esploraTestnetBase || 'https://mempool.space/testnet/api',\n 'https://blockstream.info/testnet/api',\n ]\n : [\n opts?.esploraMainnetBase || 'https://mempool.space/api',\n 'https://blockstream.info/api',\n ];\n\n let lastError: Error | null = null;\n\n // Try each endpoint in order. Per-endpoint timeout kept tight (5s) so\n // a slow upstream doesn't stack against the next one; callers who want\n // a hard overall deadline should wrap this call with their own timeout.\n for (const base of endpoints) {\n try {\n const url = `${base}/address/${address}/utxo`;\n log.debug({ url, network }, 'Fetching UTXOs');\n\n const resp = await fetch(url, {\n method: 'GET',\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(5000),\n });\n\n if (!resp.ok) {\n throw new Error(`HTTP ${resp.status}`);\n }\n\n const data = await resp.json();\n\n // Validate response structure\n if (!Array.isArray(data)) {\n throw new Error('Invalid response: expected array');\n }\n\n // Validate each UTXO has required fields\n for (const utxo of data) {\n if (\n typeof utxo.value !== 'number' ||\n typeof utxo.status !== 'object' ||\n typeof utxo.status.confirmed !== 'boolean' ||\n typeof utxo.txid !== 'string' ||\n typeof utxo.vout !== 'number'\n ) {\n throw new Error('Invalid UTXO structure');\n }\n }\n\n log.info({ base, count: data.length, network }, 'UTXOs fetched successfully');\n return data as EsploraUtxo[];\n } catch (err) {\n lastError = err instanceof Error ? err : new Error(String(err));\n log.warn(\n { base, error: lastError.message, network },\n 'Esplora endpoint failed, trying next'\n );\n }\n }\n\n // All endpoints failed\n log.error({ endpoints, error: lastError?.message, network }, 'All Esplora endpoints failed');\n throw new Error(`esplora_all_failed: ${lastError?.message || 'unknown'}`);\n}\n\n/**\n * Enhanced metrics computation per SPEC.md §5.4 and §6.1\n *\n * Supports bond extension with oldest-first greedy UTXO selection\n *\n * @param utxos - Array of UTXOs from Esplora\n * @param bondAmount - Optional bond amount (enables greedy selection)\n * @returns Computed metrics (sats_bonded, days_unspent, confirmed_balance)\n */\nfunction computeMetrics(\n utxos: EsploraUtxo[],\n bondAmount?: number,\n coverSats?: number\n): {\n /** Bonded satoshis (confirmed balance or bond amount) */\n sats_bonded: number;\n /** Days since oldest/youngest UTXO */\n days_unspent: number;\n /** Total confirmed balance */\n confirmed_balance: number;\n} {\n // Filter out unconfirmed UTXOs\n const confirmedUtxos = utxos.filter((u) => u.status.confirmed);\n if (confirmedUtxos.length === 0) {\n return { sats_bonded: 0, days_unspent: 0, confirmed_balance: 0 };\n }\n\n // Compute total confirmed balance\n const confirmed_balance = confirmedUtxos.reduce((sum, u) => sum + u.value, 0);\n\n // If bond extension is present, use oldest-first greedy algorithm\n if (bondAmount !== undefined && bondAmount > 0) {\n // Sort UTXOs by (block_height ASC, txid ASC, vout ASC) - oldest first\n const sorted = [...confirmedUtxos].sort((a, b) => {\n const heightA = a.status.block_height ?? Infinity;\n const heightB = b.status.block_height ?? Infinity;\n if (heightA !== heightB) return heightA - heightB;\n if (a.txid !== b.txid) return a.txid.localeCompare(b.txid);\n return a.vout - b.vout;\n });\n\n // Greedily select UTXOs until sum >= bond\n let sum = 0;\n let youngestTime = 0;\n for (const utxo of sorted) {\n sum += utxo.value;\n youngestTime = Math.max(youngestTime, utxo.status.block_time || 0);\n if (sum >= bondAmount) break;\n }\n\n // Age is computed from the youngest UTXO in the bonded set\n const days_unspent = Math.max(\n 0,\n Math.floor((Date.now() / 1000 - youngestTime) / (24 * 60 * 60))\n );\n\n return {\n sats_bonded: bondAmount, // Use exactly bond amount (ignore surplus)\n days_unspent,\n confirmed_balance,\n };\n }\n\n // Default behavior (no bond extension): sum all confirmed UTXOs\n const sats_bonded = confirmed_balance;\n\n // A checker asking for an amount gets the age of the coins that make it\n // up, oldest first, not the age of whichever coin is oldest.\n if (coverSats !== undefined && coverSats > 0) {\n const sorted = [...confirmedUtxos].sort(\n (a, b) => (a.status.block_height ?? Infinity) - (b.status.block_height ?? Infinity)\n );\n let sum = 0;\n let youngestTime = 0;\n for (const utxo of sorted) {\n sum += utxo.value;\n youngestTime = Math.max(youngestTime, utxo.status.block_time || Date.now() / 1000);\n if (sum >= coverSats) break;\n }\n const days_unspent = Math.max(\n 0,\n Math.floor((Date.now() / 1000 - youngestTime) / (24 * 60 * 60))\n );\n return { sats_bonded, days_unspent, confirmed_balance };\n }\n\n // Find the oldest confirmed UTXO for days_unspent\n const oldestTime = Math.min(\n ...confirmedUtxos.map((u) => u.status.block_time || Date.now() / 1000)\n );\n const days_unspent = Math.max(0, Math.floor((Date.now() / 1000 - oldestTime) / (24 * 60 * 60)));\n\n return { sats_bonded, days_unspent, confirmed_balance };\n}\n\n/**\n * Verify an OrangeCheck attestation\n *\n * This is the main verification function that:\n * 1. Validates the canonical message format\n * 2. Verifies the Bitcoin signature (BIP-322 or legacy)\n * 3. Fetches and analyzes UTXOs\n * 4. Computes reputation metrics\n *\n * @param input - Verification input (message, address, signature)\n * @param opts - Optional verification options (test mode, audience, esplora bases)\n * @returns Verification outcome with status codes and metrics\n *\n * @example\n * ```typescript\n * const result = await verify({\n * msg: canonicalMessage,\n * addr: 'bc1q...',\n * sig: 'AkcwRAIg...',\n * });\n *\n * if (result.ok) {\n * console.log('Score:', result.metrics?.score);\n * }\n * ```\n */\nexport async function verify(input: VerifyInput, opts: VerifyOptions = {}): Promise<VerifyOutcome> {\n const codes: StatusCode[] = [];\n let network: Network = 'mainnet';\n\n try {\n // Check if we have required fields\n if (!input.msg || !input.addr || !input.sig) {\n codes.push('bad_request');\n return { ok: false, codes, network };\n }\n\n // Parse canonical message with extensions\n const parsed = parseCanonicalMessage(input.msg);\n network = parsed.network || 'mainnet';\n\n // Compute attestation ID\n const attestation_id = await generateAttestationId(input.msg);\n\n // Validate address consistency\n if (parsed.core.address !== input.addr) {\n codes.push('bad_request');\n return { ok: false, codes, network, attestation_id };\n }\n\n // Testnet/Signet policy enforcement\n if ((network === 'testnet' || network === 'signet') && !opts.testMode) {\n codes.push('network_testmode');\n return { ok: false, codes, network, attestation_id };\n }\n\n // Optional policy checks for extensions.\n // `aud` binds the attestation to an origin. If the caller passed\n // `expectedAud`, the check must be load-bearing — a mismatch is a\n // phishing signal, not an advisory. Fail hard (spec-aligned).\n if (opts.expectedAud) {\n const aud = parsed.extensions['aud'];\n if (!aud || aud !== opts.expectedAud) {\n codes.push('aud_mismatch');\n return { ok: false, codes, network, attestation_id };\n }\n }\n\n // Check message expiration. An expiry that is not an RFC 3339 time is\n // refused as expired: nothing shows it lies ahead.\n if (parsed.expires !== undefined) {\n const exp = rfc3339Ms(parsed.expires);\n if (exp === null || exp < Date.now()) {\n codes.push('expired');\n return { ok: false, codes, network, attestation_id };\n }\n }\n\n // Enhanced signature verification\n const sigCode = await verifySignature({\n addr: input.addr,\n msg: input.msg,\n sig: input.sig,\n ...(input.scheme && { scheme: input.scheme }),\n });\n codes.push(sigCode);\n\n const sigOk = sigCode === 'sig_ok_bip322' || sigCode === 'sig_ok_legacy';\n if (!sigOk) {\n return {\n ok: false,\n codes,\n network,\n attestation_id,\n identities: parsed.core.identities,\n };\n }\n\n // Enhanced UTXO analysis and metrics computation\n try {\n const utxos = await getAddressUtxos(input.addr, network, {\n esploraMainnetBase: opts.esploraMainnetBase,\n esploraSignetBase: opts.esploraSignetBase,\n });\n\n // Compute metrics with bond extension support (SPEC.md §5.4)\n const { sats_bonded, days_unspent, confirmed_balance } = computeMetrics(\n utxos,\n parsed.bond,\n opts.coverSats\n );\n\n // If bond extension is present, enforce minimum balance (SPEC.md §5.4.c)\n if (parsed.bond !== undefined && parsed.bond > 0) {\n if (confirmed_balance < parsed.bond) {\n codes.push('bond_insufficient');\n log.warn(\n {\n bond: parsed.bond,\n confirmed_balance,\n address: input.addr,\n },\n 'Bond extension: insufficient balance'\n );\n return {\n ok: false,\n codes,\n network,\n attestation_id,\n identities: parsed.core.identities,\n };\n }\n }\n\n // Analyze bond status\n if (sats_bonded <= 0) {\n codes.push('bond_zero');\n // Check for pending UTXOs\n if (utxos.some((u) => !u.status.confirmed)) {\n codes.push('bond_pending');\n }\n return {\n ok: false,\n codes,\n network,\n attestation_id,\n identities: parsed.core.identities,\n };\n } else {\n codes.push('bond_confirmed');\n }\n\n // Compute final metrics with enhanced scoring\n const metrics: Metrics = {\n sats_bonded,\n days_unspent,\n score: (computeScore(sats_bonded, days_unspent, { algorithm: 'v0' }) as number),\n };\n\n return {\n ok: true,\n codes,\n network,\n attestation_id,\n identities: parsed.core.identities,\n metrics,\n };\n } catch (utxoError) {\n log.error({ err: utxoError }, 'UTXO fetch failed');\n codes.push('bad_request');\n return { ok: false, codes, network, attestation_id };\n }\n } catch (e: unknown) {\n const msg = e instanceof Error ? e.message : String(e ?? 'decode_error');\n if (msg === 'decode_error') {\n codes.push('decode_error');\n } else {\n codes.push('bad_request');\n }\n return { ok: false, codes, network };\n }\n}\n","/**\n * Nostr Integration for OrangeCheck Protocol\n *\n * Implements NIP-78 (Application-specific data) for attestation publishing\n */\n\nimport type { AttestationEnvelope, IdentityBinding, NostrEvent } from './types';\n\nimport { nostrIdentifierForms } from './nostr-pubkey';\nimport { createLogger } from './utils/logger';\nimport { rfc3339Ms } from './utils/time';\nimport { parseCanonicalMessage, signatureIsValid } from './verify';\n\nconst log = createLogger('ocp/nostr');\n\n/**\n * NIP-78 event kind for OrangeCheck attestations\n */\nexport const ATTESTATION_EVENT_KIND = 30078;\n\n/**\n * Default Nostr relays for OrangeCheck attestation publishing and lookup.\n *\n * Well-known relays with good uptime, plus the first-party family relay.\n * `relay.ochk.io` was missing here while attest.ochk.io's verifier has always\n * read from it, so the SDK and the reference implementation were looking in\n * different places — the SDK could miss a family attestation that the hosted\n * API found. It runs a kind allowlist (30078-30087, 30110-30114) and canonical OC d-tag\n * prefixes, and is always co-published alongside the public relays, never the\n * only copy. See https://github.com/orangecheck/oc-relay-infra.\n *\n * Order matters only for readability; queries fan out to all of them and\n * merge. Pass your own `relays` to any query to override this entirely — the\n * protocol does not require trusting any particular relay.\n *\n * A NOTE ON `relay.damus.io`, deliberately kept. It is reachable from a browser\n * (measured ~391ms, real events returned) and NOT reachable from at least one\n * serverless runtime — Vercel's, where it fails to connect on every attempt,\n * consistently, over hours. Its NIP-11 declares no `auth_required`,\n * `payment_required` or `restricted_writes`, so this reads as datacenter-IP\n * filtering in front of it rather than policy, and it can change in either\n * direction without notice.\n *\n * It stays for three reasons: it is one of the most widely read relays, so an\n * attestation published there is findable by the most people; the browser is\n * the dominant path for this SDK (wallet signing happens there), and it works\n * there; and since FANOUT_DEADLINE_MS below is a SHARED budget, a relay that\n * fails to connect resolves immediately and costs no latency. Removing a relay\n * that works for most callers, to tidy a status field on one runtime, is the\n * wrong trade.\n *\n * If you run this SDK server-side and want certainty about coverage, pass\n * `relays` explicitly. Silent partial coverage — believing you reached four\n * relays when you reached three — is the real hazard here, not latency.\n */\nexport const DEFAULT_RELAYS: string[] = [\n 'wss://relay.damus.io',\n 'wss://nos.lol',\n // relay.snort.social sat here until 2026-10-21. It now runs in memory: it\n // answered OK true to family events and served none of them back.\n 'wss://nostr.oxtr.dev',\n 'wss://relay.ochk.io',\n];\n\n/**\n * Wall-clock budget for a whole relay fan-out, shared by every relay in it.\n *\n * Queries below use Promise.allSettled, so a fan-out used to take as long as\n * its SLOWEST relay — and with a dead relay in DEFAULT_RELAYS that meant the\n * per-relay 10s timeout, every single call. Measured on 1.4.0:\n *\n * check({ addr }) with the old defaults 10.4s\n * check({ addr }) with the dead relay out 1.0s\n *\n * That is not merely slow, it broke a dependent outright: @orangecheck/gate\n * races check() against a 5s lookupTimeoutMs, so a 10.4s lookup lost the race\n * every time and no address-based gate decision could ever succeed.\n *\n * A shared deadline fixes the class: whichever relay is dead or slow next, a\n * fan-out costs at most this. Healthy relays answer well inside it (~1s for a\n * cold WebSocket plus a query). QUERY_TIMEOUT_MS remains the per-relay ceiling.\n *\n * The value is deliberately BELOW @orangecheck/gate's 5s default\n * lookupTimeoutMs, so a gate decision can still resolve in the worst case\n * rather than always losing its own race. Raising this above 5000 silently\n * breaks that dependent — change both together, or don't.\n */\nexport const FANOUT_DEADLINE_MS = 4000;\nconst QUERY_TIMEOUT_MS = 10000;\n\n/** Remaining budget for a relay, given the fan-out's shared deadline. */\nfunction budgetFor(deadlineAt: number): number {\n return Math.max(0, Math.min(QUERY_TIMEOUT_MS, deadlineAt - Date.now()));\n}\n\n/**\n * Create a NIP-78 event for an OrangeCheck attestation\n *\n * Kind: 30078 (Parameterized Replaceable Event)\n * d-tag: attestation_id (makes it replaceable by attestation ID)\n *\n * @param envelope - Attestation envelope\n * @param pubkey - Nostr public key (hex format)\n * @returns Unsigned Nostr event\n */\nexport function createAttestationEvent(\n envelope: AttestationEnvelope,\n pubkey: string\n): Omit<NostrEvent, 'id' | 'sig'> {\n // NIP-12 relays index SINGLE-LETTER tags only. A multi-letter tag like\n // `address` is stored verbatim and stays readable in the event, but it is\n // never indexed, so no `#address` filter can ever retrieve it. Without the\n // `t` tags below an attestation published by this SDK is invisible to\n // every by-address query on every relay — including attest.ochk.io's own\n // verifier, which queries `#t`. Keep both: `t` is what makes it findable,\n // `address` is human-readable diagnostics and is not load-bearing.\n const tags: string[][] = [\n ['d', envelope.attestation_id], // Parameterized replaceable event identifier (indexed)\n ['t', envelope.address], // Indexed btc-address tag — query with `#t`\n ['t', 'oc-attest'], // Family marker — `#t=[\"oc-attest\"]` for cross-cut discovery\n ['address', envelope.address], // Diagnostic / legacy — relay won't index this\n ['scheme', envelope.scheme], // Signature scheme (bip322 or legacy)\n ['issued_at', envelope.issued_at], // ISO timestamp\n ];\n\n // Add identity tags\n for (const identity of envelope.identities) {\n tags.push(['i', `${identity.protocol}:${identity.identifier}`]);\n }\n\n // Add expiration if present\n if (envelope.expires_at) {\n tags.push(['expires', envelope.expires_at]);\n }\n\n // Add relay hints if present\n if (envelope.relay_hints?.length) {\n for (const relay of envelope.relay_hints) {\n tags.push(['relay', relay]);\n }\n }\n\n // Content is the full attestation envelope as JSON\n const content = JSON.stringify(envelope, null, 2);\n\n return {\n kind: ATTESTATION_EVENT_KIND,\n tags,\n content,\n created_at: Math.floor(Date.parse(envelope.issued_at) / 1000),\n pubkey,\n };\n}\n\n/**\n * Parse identity bindings from Nostr event tags\n *\n * @param event - Nostr event\n * @returns Array of identity bindings\n */\nexport function parseIdentitiesFromEvent(event: NostrEvent): IdentityBinding[] {\n const identities: IdentityBinding[] = [];\n\n for (const tag of event.tags) {\n if (tag[0] === 'i' && tag[1]) {\n const colonIndex = tag[1].indexOf(':');\n if (colonIndex !== -1) {\n identities.push({\n protocol: tag[1].substring(0, colonIndex),\n identifier: tag[1].substring(colonIndex + 1),\n });\n }\n }\n }\n\n return identities;\n}\n\n/**\n * Extract attestation ID from Nostr event d-tag\n *\n * @param event - Nostr event\n * @returns Attestation ID or undefined if not found\n */\nexport function getAttestationIdFromEvent(event: NostrEvent): string | undefined {\n const dTag = event.tags.find((tag) => tag[0] === 'd');\n return dTag?.[1];\n}\n\n/**\n * Extract Bitcoin address from Nostr event tags\n *\n * @param event - Nostr event\n * @returns Bitcoin address or undefined if not found\n */\nexport function getAddressFromEvent(event: NostrEvent): string | undefined {\n const addressTag = event.tags.find((tag) => tag[0] === 'address');\n return addressTag?.[1];\n}\n\n/**\n * Publish a Nostr event to multiple relays\n * Returns array of relay URLs that successfully accepted the event\n */\nexport async function publishToRelays(\n event: NostrEvent,\n relays: string[] = DEFAULT_RELAYS\n): Promise<{ success: string[]; failed: string[] }> {\n const success: string[] = [];\n const failed: string[] = [];\n\n log.info({ eventId: event.id, relayCount: relays.length }, 'Publishing event to relays');\n\n // One shared deadline for the whole fan-out — see FANOUT_DEADLINE_MS.\n\n const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;\n\n const publishPromises = relays.map(async (relayUrl) => {\n let ws: WebSocket | null = null;\n\n // One outcome per relay, first one wins. Without this a relay that\n // ACKs `OK true` and then emits an error or close event during\n // teardown lands in BOTH arrays, and the caller reports \"published\n // to 3 · rejected 3\" over the same three relays. The `failed.includes`\n // check this replaces only ever de-duplicated `failed` against\n // itself, so it could not catch a success/failure split.\n // attest.ochk.io's copy of this file has carried the guard for a\n // while; this is the back-port.\n let settled = false;\n const recordSuccess = (): void => {\n if (settled) return;\n settled = true;\n success.push(relayUrl);\n };\n const recordFailure = (): void => {\n if (settled) return;\n settled = true;\n failed.push(relayUrl);\n };\n\n try {\n // Connect to relay\n ws = new WebSocket(relayUrl);\n const wsRef = ws; // Capture for closures\n\n await new Promise<void>((resolve, reject) => {\n // One outer deadline covers connect + OK round-trip. The old\n // code cleared this on `onopen`, which meant a relay that\n // opened but never sent OK would hang the promise forever.\n const deadline = setTimeout(() => {\n wsRef.close();\n reject(new Error('Relay publish timeout'));\n }, budgetFor(deadlineAt));\n\n const cleanup = () => {\n clearTimeout(deadline);\n wsRef.close();\n };\n\n wsRef.onopen = () => {\n // Do NOT clear the deadline — we still need it in case\n // the relay never responds with OK.\n wsRef.send(JSON.stringify(['EVENT', event]));\n };\n\n wsRef.onmessage = (msg) => {\n try {\n const data = JSON.parse(msg.data);\n // Check for OK response\n if (data[0] === 'OK' && data[1] === event.id) {\n if (data[2] === true) {\n recordSuccess();\n cleanup();\n resolve();\n } else {\n log.warn(\n { relay: relayUrl, reason: data[3] },\n 'Relay rejected event'\n );\n recordFailure();\n cleanup();\n reject(new Error(data[3] || 'Relay rejected event'));\n }\n }\n } catch (err) {\n log.error(\n { relay: relayUrl, error: err },\n 'Failed to parse relay response'\n );\n recordFailure();\n cleanup();\n reject(err);\n }\n };\n\n wsRef.onerror = (err) => {\n log.error({ relay: relayUrl, error: err }, 'WebSocket error');\n recordFailure();\n cleanup();\n reject(err);\n };\n\n wsRef.onclose = () => {\n // Socket closed before we resolved/rejected — counts as\n // failure. clearTimeout here is safe (cleanup handles both).\n clearTimeout(deadline);\n };\n });\n } catch (err) {\n log.error({ relay: relayUrl, error: err }, 'Failed to publish to relay');\n recordFailure();\n } finally {\n // Ensure WebSocket is closed\n if (ws && ws.readyState !== WebSocket.CLOSED) {\n ws.close();\n }\n }\n });\n\n await Promise.allSettled(publishPromises);\n\n log.info({ successCount: success.length, failedCount: failed.length }, 'Publishing complete');\n\n return { success, failed };\n}\n\n/**\n * Query relays for attestations by attestation ID\n */\nexport async function queryByAttestationId(\n attestationId: string,\n relays: string[] = DEFAULT_RELAYS\n): Promise<NostrEvent[]> {\n const events: NostrEvent[] = [];\n\n log.info({ attestationId, relayCount: relays.length }, 'Querying relays for attestation');\n\n // One shared deadline for the whole fan-out — see FANOUT_DEADLINE_MS.\n\n const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;\n\n const queryPromises = relays.map(async (relayUrl) => {\n let ws: WebSocket | null = null;\n\n try {\n ws = new WebSocket(relayUrl);\n const wsRef = ws; // Capture for closures\n\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n wsRef.close();\n reject(new Error('Query timeout'));\n }, budgetFor(deadlineAt));\n\n const cleanup = () => {\n clearTimeout(timeout);\n wsRef.close();\n };\n\n wsRef.onopen = () => {\n // Send REQ message for kind 30078 with d-tag = attestation_id\n const subscriptionId = `ochk_${Date.now()}`;\n wsRef.send(\n JSON.stringify([\n 'REQ',\n subscriptionId,\n {\n kinds: [30078],\n '#d': [attestationId],\n limit: 1,\n },\n ])\n );\n };\n\n wsRef.onmessage = (msg) => {\n try {\n const data = JSON.parse(msg.data);\n if (data[0] === 'EVENT') {\n events.push(data[2]);\n } else if (data[0] === 'EOSE') {\n cleanup();\n resolve();\n }\n } catch (err) {\n log.error({ relay: relayUrl, error: err }, 'Failed to parse event');\n }\n };\n\n wsRef.onerror = (err) => {\n log.error({ relay: relayUrl, error: err }, 'WebSocket error during query');\n cleanup();\n reject(err);\n };\n\n wsRef.onclose = () => {\n clearTimeout(timeout);\n };\n });\n } catch (err) {\n log.error({ relay: relayUrl, error: err }, 'Failed to query relay');\n } finally {\n // Ensure WebSocket is closed\n if (ws && ws.readyState !== WebSocket.CLOSED) {\n ws.close();\n }\n }\n });\n\n await Promise.allSettled(queryPromises);\n\n log.info({ eventCount: events.length }, 'Query complete');\n\n return events;\n}\n\n/**\n * Query relays for attestations by Bitcoin address\n */\nexport async function queryByAddress(\n address: string,\n relays: string[] = DEFAULT_RELAYS\n): Promise<NostrEvent[]> {\n const events: NostrEvent[] = [];\n\n log.info({ address, relayCount: relays.length }, 'Querying relays for address');\n\n // One shared deadline for the whole fan-out — see FANOUT_DEADLINE_MS.\n\n const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;\n\n const queryPromises = relays.map(async (relayUrl) => {\n try {\n const ws = new WebSocket(relayUrl);\n\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n ws.close();\n reject(new Error('Query timeout'));\n }, budgetFor(deadlineAt));\n\n ws.onopen = () => {\n const subscriptionId = `ochk_addr_${Date.now()}`;\n ws.send(\n JSON.stringify([\n 'REQ',\n subscriptionId,\n {\n kinds: [30078],\n // `#t`, NOT `#address`: NIP-12 relays index\n // single-letter tags only, so `#address`\n // matched nothing on any relay and this\n // function returned empty for every real\n // attestation. attest.ochk.io's verifier has\n // queried `#t` since the same fix landed\n // there; this is the back-port.\n '#t': [address],\n limit: 50,\n },\n ])\n );\n };\n\n ws.onmessage = (msg) => {\n try {\n const data = JSON.parse(msg.data);\n if (data[0] === 'EVENT') {\n events.push(data[2]);\n } else if (data[0] === 'EOSE') {\n clearTimeout(timeout);\n ws.close();\n resolve();\n }\n } catch (err) {\n log.error({ relay: relayUrl, error: err }, 'Failed to parse event');\n }\n };\n\n ws.onerror = (err) => {\n clearTimeout(timeout);\n log.error({ relay: relayUrl, error: err }, 'WebSocket error during query');\n reject(err);\n };\n });\n } catch (err) {\n log.error({ relay: relayUrl, error: err }, 'Failed to query relay');\n }\n });\n\n await Promise.allSettled(queryPromises);\n\n log.info({ eventCount: events.length }, 'Query complete');\n\n return events;\n}\n\n/**\n * Query relays for attestations by identity (any protocol)\n */\nexport async function queryByIdentity(\n protocol: string,\n identifier: string,\n relays: string[] = DEFAULT_RELAYS\n): Promise<NostrEvent[]> {\n const events: NostrEvent[] = [];\n const identityTag = `${protocol}:${identifier}`;\n const identityTags =\n protocol === 'nostr'\n ? nostrIdentifierForms(identifier).map((f) => `nostr:${f}`)\n : [identityTag];\n\n log.info({ protocol, identifier, relayCount: relays.length }, 'Querying relays for identity');\n\n // One shared deadline for the whole fan-out — see FANOUT_DEADLINE_MS.\n\n const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;\n\n const queryPromises = relays.map(async (relayUrl) => {\n try {\n const ws = new WebSocket(relayUrl);\n\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n ws.close();\n reject(new Error('Query timeout'));\n }, budgetFor(deadlineAt));\n\n ws.onopen = () => {\n const subscriptionId = `ochk_identity_${Date.now()}`;\n ws.send(\n JSON.stringify([\n 'REQ',\n subscriptionId,\n {\n kinds: [30078],\n '#i': identityTags,\n limit: 10,\n },\n ])\n );\n };\n\n ws.onmessage = (msg) => {\n try {\n const data = JSON.parse(msg.data);\n if (data[0] === 'EVENT') {\n events.push(data[2]);\n } else if (data[0] === 'EOSE') {\n clearTimeout(timeout);\n ws.close();\n resolve();\n }\n } catch (err) {\n log.error({ relay: relayUrl, error: err }, 'Failed to parse event');\n }\n };\n\n ws.onerror = (err) => {\n clearTimeout(timeout);\n log.error({ relay: relayUrl, error: err }, 'WebSocket error during query');\n reject(err);\n };\n });\n } catch (err) {\n log.error({ relay: relayUrl, error: err }, 'Failed to query relay');\n }\n });\n\n await Promise.allSettled(queryPromises);\n\n log.info({ eventCount: events.length }, 'Query complete');\n\n return events;\n}\n\n/**\n * Query relays for the most recent attestations across every publisher.\n *\n * For aggregate views (network stats, a recent-activity feed), not per-subject\n * lookups. Filters on the `#t=[\"oc-attest\"]` family marker because kind 30078\n * is shared by every NIP-78 app: an unfiltered window fills with other apps'\n * data. Returns envelopes whose attestation_id matches their message,\n * deduplicated by attestation_id. Signatures are not checked — call verify()\n * on anything you act on.\n */\nexport async function queryRecent(\n relays: string[] = DEFAULT_RELAYS,\n limit: number = 200\n): Promise<AttestationEnvelope[]> {\n const events: NostrEvent[] = [];\n\n log.info({ relayCount: relays.length, limit }, 'Querying relays for recent attestations');\n\n // One shared deadline for the whole fan-out — see FANOUT_DEADLINE_MS.\n const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;\n\n const queryPromises = relays.map(async (relayUrl) => {\n try {\n const ws = new WebSocket(relayUrl);\n\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n ws.close();\n reject(new Error('Query timeout'));\n }, budgetFor(deadlineAt));\n\n ws.onopen = () => {\n const subscriptionId = `ochk_recent_${Date.now()}`;\n ws.send(\n JSON.stringify([\n 'REQ',\n subscriptionId,\n { kinds: [ATTESTATION_EVENT_KIND], '#t': ['oc-attest'], limit },\n ])\n );\n };\n\n ws.onmessage = (msg) => {\n try {\n const data = JSON.parse(msg.data);\n if (data[0] === 'EVENT') {\n const ev = data[2] as NostrEvent;\n const scheme = ev.tags?.find((t) => t[0] === 'scheme')?.[1];\n const dTag = ev.tags?.find((t) => t[0] === 'd')?.[1];\n if ((scheme === 'bip322' || scheme === 'legacy') && dTag) {\n events.push(ev);\n }\n } else if (data[0] === 'EOSE') {\n clearTimeout(timeout);\n ws.close();\n resolve();\n }\n } catch (err) {\n log.error({ relay: relayUrl, error: err }, 'Failed to parse event');\n }\n };\n\n ws.onerror = (err) => {\n clearTimeout(timeout);\n log.error({ relay: relayUrl, error: err }, 'WebSocket error during query');\n reject(err);\n };\n });\n } catch (err) {\n log.error({ relay: relayUrl, error: err }, 'Failed to query relay');\n }\n });\n\n await Promise.allSettled(queryPromises);\n\n const envelopes = await collectSignedEnvelopes(events);\n\n log.info({ eventCount: events.length, unique: envelopes.length }, 'Recent query complete');\n\n return envelopes;\n}\n\n/**\n * Verify attestation envelope integrity\n * Checks that the attestation ID matches the message hash\n */\nexport async function verifyAttestationEnvelope(envelope: AttestationEnvelope): Promise<boolean> {\n try {\n // Re-compute attestation ID from message\n const encoder = new TextEncoder();\n const data = encoder.encode(envelope.message);\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n const hashArray = Array.from(new Uint8Array(hashBuffer));\n const computedId = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');\n\n return computedId === envelope.attestation_id;\n } catch (err) {\n log.error({ error: err }, 'Failed to verify attestation envelope');\n return false;\n }\n}\n\nfunction base64Url(text: string): string {\n const base64 = btoa(String.fromCharCode(...new TextEncoder().encode(text)));\n return base64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');\n}\n\n/**\n * The envelope with every field the signature covers read from its message.\n *\n * Only `message` and `signature` are signed. `address`, `identities`,\n * `issued_at`, `expires_at` and `message_b64url` are copies beside them, so\n * they are taken from the message here. Null when the message names no address\n * or no RFC 3339 `issued_at`.\n */\nexport function signedEnvelope(envelope: AttestationEnvelope): AttestationEnvelope | null {\n let parsed: ReturnType<typeof parseCanonicalMessage>;\n try {\n parsed = parseCanonicalMessage(envelope.message);\n } catch {\n return null;\n }\n if (!parsed.core.address || rfc3339Ms(parsed.core.issued_at) === null) return null;\n const { expires_at: _copy, ...rest } = envelope;\n return {\n ...rest,\n address: parsed.core.address,\n identities: parsed.core.identities,\n issued_at: parsed.core.issued_at,\n ...(parsed.expires !== undefined ? { expires_at: parsed.expires } : {}),\n message_b64url: base64Url(envelope.message),\n };\n}\n\n/**\n * One envelope per attestation id, its fields read from its signed message.\n *\n * Anyone can publish a copy of an attestation under their own Nostr key, with\n * any signature beside the message. When copies of one id disagree on the\n * signature, the first whose signature is valid is kept.\n */\nexport async function collectSignedEnvelopes(\n events: NostrEvent[],\n accept: (event: NostrEvent, envelope: AttestationEnvelope) => boolean = () => true\n): Promise<AttestationEnvelope[]> {\n const copies = new Map<string, AttestationEnvelope[]>();\n for (const event of events) {\n const raw = parseAttestationFromEvent(event);\n if (!raw || !(await verifyAttestationEnvelope(raw))) continue;\n const envelope = signedEnvelope(raw);\n if (!envelope || !accept(event, envelope)) continue;\n const same = copies.get(envelope.attestation_id) ?? [];\n if (!same.some((e) => e.signature === envelope.signature && e.scheme === envelope.scheme)) {\n same.push(envelope);\n }\n copies.set(envelope.attestation_id, same);\n }\n const kept: AttestationEnvelope[] = [];\n for (const same of copies.values()) {\n if (same.length === 1) {\n kept.push(same[0]!);\n continue;\n }\n let valid: AttestationEnvelope | undefined;\n for (const e of same) {\n if (await signatureIsValid({ addr: e.address, msg: e.message, sig: e.signature, scheme: e.scheme })) {\n valid = e;\n break;\n }\n }\n kept.push(valid ?? same[0]!);\n }\n return kept;\n}\n\n/**\n * Parse attestation envelope from Nostr event content\n */\nexport function parseAttestationFromEvent(event: NostrEvent): AttestationEnvelope | null {\n try {\n const envelope = JSON.parse(event.content) as AttestationEnvelope;\n return envelope;\n } catch (err) {\n log.error({ eventId: event.id, error: err }, 'Failed to parse attestation from event');\n return null;\n }\n}\n","/**\n * Nostr Cryptographic Functions\n * Implements NIP-01 event ID computation and signature verification\n */\n\nimport type { NostrEvent } from './types';\n\nimport { createLogger } from './utils/logger';\n\ntype Nip07Window = {\n nostr?: {\n getPublicKey: () => Promise<string>;\n signEvent: (event: Omit<NostrEvent, 'id' | 'sig'>) => Promise<NostrEvent>;\n _metadata?: { name?: string; version?: string };\n };\n};\n\nconst log = createLogger('ocp/nostr-crypto');\n\n/**\n * Compute Nostr event ID according to NIP-01\n * Event ID is the SHA-256 hash of the serialized event\n * Serialization: [0, pubkey, created_at, kind, tags, content]\n */\nexport async function computeNostrEventId(event: Omit<NostrEvent, 'id' | 'sig'>): Promise<string> {\n // Serialize event according to NIP-01\n const serialized = JSON.stringify([\n 0, // Reserved for future use\n event.pubkey,\n event.created_at,\n event.kind,\n event.tags,\n event.content,\n ]);\n\n log.debug({ serialized }, 'Computing event ID');\n\n // Compute SHA-256 hash\n const encoder = new TextEncoder();\n const data = encoder.encode(serialized);\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n\n // Convert to lowercase hex\n const hashArray = Array.from(new Uint8Array(hashBuffer));\n const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');\n\n log.debug({ eventId: hashHex }, 'Computed event ID');\n\n return hashHex;\n}\n\n/**\n * Verify Nostr event ID\n * Recomputes the ID and checks if it matches\n */\nexport async function verifyNostrEventId(event: NostrEvent): Promise<boolean> {\n const computedId = await computeNostrEventId(event);\n return computedId === event.id;\n}\n\n/**\n * Check if NIP-07 extension is available\n * NIP-07 provides window.nostr for signing events\n */\nexport function isNip07Available(): boolean {\n if (typeof window === 'undefined') {\n return false;\n }\n\n return !!(window as unknown as { nostr?: Nip07Window['nostr'] }).nostr!;\n}\n\n/**\n * Get Nostr public key from NIP-07 extension\n * Returns null if extension not available or user denies\n */\nexport async function getNostrPublicKey(): Promise<string | null> {\n if (!isNip07Available()) {\n log.warn('NIP-07 extension not available');\n return null;\n }\n\n try {\n const nostr = (window as unknown as { nostr?: Nip07Window['nostr'] }).nostr!;\n const pubkey = await nostr.getPublicKey();\n log.info({ pubkey }, 'Got Nostr public key from NIP-07');\n return pubkey;\n } catch (err) {\n log.error({ error: err }, 'Failed to get Nostr public key');\n return null;\n }\n}\n\n/**\n * Sign Nostr event using NIP-07 extension\n * Returns signed event with id and sig fields\n */\nexport async function signNostrEvent(\n event: Omit<NostrEvent, 'id' | 'sig'>\n): Promise<NostrEvent | null> {\n if (!isNip07Available()) {\n log.warn('NIP-07 extension not available');\n return null;\n }\n\n try {\n const nostr = (window as unknown as { nostr?: Nip07Window['nostr'] }).nostr!;\n\n // NIP-07 signEvent expects the event without id and sig\n const signedEvent = await nostr.signEvent(event);\n\n log.info({ eventId: signedEvent.id }, 'Signed Nostr event with NIP-07');\n\n return signedEvent as NostrEvent;\n } catch (err) {\n log.error({ error: err }, 'Failed to sign Nostr event');\n return null;\n }\n}\n\n// Previously this file exported `generateEphemeralKeypair()` and\n// `signEventWithEphemeralKey()` for demo purposes. Both produced non-schnorr\n// values that Nostr relays would silently reject — creating the false\n// impression that publishing had succeeded. They have been removed. All Nostr\n// signing now flows through `signNostrEvent()` / `window.nostr` (NIP-07).\n\n/**\n * Validate Nostr event structure\n */\nexport function validateNostrEvent(event: unknown): event is NostrEvent {\n if (typeof event !== 'object' || event === null) return false;\n const e = event as Record<string, unknown>;\n\n if (typeof e.id !== 'string' || (e.id as string).length !== 64) return false;\n if (typeof e.pubkey !== 'string' || (e.pubkey as string).length !== 64) return false;\n if (typeof e.created_at !== 'number') return false;\n if (typeof e.kind !== 'number') return false;\n if (!Array.isArray(e.tags)) return false;\n if (typeof e.content !== 'string') return false;\n if (typeof e.sig !== 'string' || (e.sig as string).length !== 128) return false;\n\n return true;\n}\n\n/**\n * Get NIP-07 extension info\n */\nexport function getNip07Info(): {\n available: boolean;\n name?: string;\n version?: string;\n} {\n if (!isNip07Available()) {\n return { available: false };\n }\n\n const nostr = (window as unknown as { nostr?: Nip07Window['nostr'] }).nostr!;\n\n return {\n available: true,\n name: nostr._metadata?.name,\n version: nostr._metadata?.version,\n };\n}\n","/**\n * Attestation Manager\n *\n * High-level API for creating, publishing, and discovering OrangeCheck attestations\n */\n\nimport type { AttestationEnvelope, IdentityBinding, NostrEvent, Scheme } from './types';\n\nimport { createAttestationEnvelope } from './canonical';\nimport {\n collectSignedEnvelopes,\n createAttestationEvent,\n DEFAULT_RELAYS,\n publishToRelays,\n queryByAddress,\n queryByAttestationId,\n queryByIdentity,\n verifyAttestationEnvelope,\n} from './nostr';\nimport { isNip07Available, signNostrEvent } from './nostr-crypto';\nimport { createLogger } from './utils/logger';\n\nconst log = createLogger('ocp/attestation');\n\n/**\n * Options for creating an attestation\n */\nexport interface CreateAttestationOptions {\n /** Canonical message */\n message: string;\n /** BIP-322 or legacy signature */\n signature: string;\n /** Signature scheme used */\n scheme: Scheme;\n /** Bitcoin address */\n address: string;\n /** Optional identity bindings */\n identities?: IdentityBinding[];\n /** Optional extension key-value pairs */\n extensions?: Record<string, string | undefined>;\n}\n\n/**\n * Options for publishing an attestation to Nostr\n */\nexport interface PublishAttestationOptions {\n /** Attestation envelope to publish */\n envelope: AttestationEnvelope;\n /** Nostr public key (npub or hex) for signing the event */\n npub: string;\n /** Optional relay URLs (defaults to DEFAULT_RELAYS) */\n relays?: string[];\n}\n\n/**\n * Options for discovering attestations\n */\nexport interface DiscoverAttestationOptions {\n /** Search by attestation ID */\n attestationId?: string;\n /** Search by Bitcoin address */\n address?: string;\n /** Search by identity binding */\n identity?: { protocol: string; identifier: string };\n /** Optional relay URLs to query */\n relays?: string[];\n}\n\n/**\n * Result of publishing an attestation\n */\nexport interface PublishResult {\n /** Relays that successfully received the event */\n success: string[];\n /** Relays that failed to receive the event */\n failed: string[];\n}\n\n/**\n * Custom error for attestation operations\n */\nexport class AttestationError extends Error {\n constructor(\n message: string,\n public readonly code?: string\n ) {\n super(message);\n this.name = 'AttestationError';\n }\n}\n\n/**\n * Create an attestation envelope from a signed message\n *\n * @param options - Attestation creation options\n * @returns Attestation envelope ready for publishing\n * @throws {AttestationError} If envelope integrity check fails\n *\n * @example\n * ```typescript\n * const envelope = await createAttestation({\n * message: canonicalMessage,\n * signature: 'AkcwRAIg...',\n * scheme: 'bip322',\n * address: 'bc1q...',\n * identities: [{ protocol: 'nostr', identifier: 'npub1...' }],\n * });\n * ```\n */\nexport async function createAttestation(\n options: CreateAttestationOptions\n): Promise<AttestationEnvelope> {\n const { message, signature, scheme, address, identities = [], extensions = {} } = options;\n\n log.info({ address, identityCount: identities.length }, 'Creating attestation');\n\n const envelope = await createAttestationEnvelope(\n message,\n signature,\n scheme,\n address,\n identities,\n extensions\n );\n\n // Verify envelope integrity\n const isValid = await verifyAttestationEnvelope(envelope);\n if (!isValid) {\n throw new AttestationError(\n 'Attestation envelope integrity check failed',\n 'INTEGRITY_CHECK_FAILED'\n );\n }\n\n log.info({ attestationId: envelope.attestation_id }, 'Attestation created successfully');\n\n return envelope;\n}\n\n/**\n * Publish an attestation to Nostr relays\n *\n * Uses NIP-07 if available, falls back to ephemeral key for demo\n *\n * @param options - Publishing options\n * @returns Object with successful and failed relay URLs\n * @throws {AttestationError} If signing fails\n *\n * @example\n * ```typescript\n * const result = await publishAttestation({\n * envelope,\n * npub: 'npub1...',\n * relays: ['wss://relay.damus.io'],\n * });\n * console.log('Published to:', result.success);\n * ```\n */\nexport async function publishAttestation(\n options: PublishAttestationOptions\n): Promise<PublishResult> {\n const { envelope, npub, relays = DEFAULT_RELAYS } = options;\n\n log.info(\n { attestationId: envelope.attestation_id, relayCount: relays.length },\n 'Publishing attestation'\n );\n\n // Create unsigned event\n const unsignedEvent = createAttestationEvent(envelope, npub);\n\n // Nostr publishing requires a valid schnorr signature. Only NIP-07\n // (window.nostr) is supported. If no signer is present we refuse to\n // publish rather than emit an invalid-sig event that every relay will\n // silently reject — which would make the caller think it had succeeded.\n if (!isNip07Available()) {\n log.warn('NIP-07 unavailable — skipping Nostr publish');\n throw new Error(\n 'nostr_signer_unavailable: install a NIP-07 extension (e.g. nos2x, Alby) to publish'\n );\n }\n\n log.info('Using NIP-07 to sign event');\n const signedEvent = await signNostrEvent(unsignedEvent);\n if (!signedEvent) {\n throw new Error('nostr_signing_failed: NIP-07 extension rejected or errored');\n }\n\n const result = await publishToRelays(signedEvent, relays);\n\n log.info(\n {\n attestationId: envelope.attestation_id,\n successCount: result.success.length,\n failedCount: result.failed.length,\n },\n 'Publishing complete'\n );\n\n return result;\n}\n\n/**\n * An event's index tags are unsigned, so a relay hit proves nothing until they\n * agree with the envelope. Requiring the `t` address tag also keeps every\n * attestation findable by address, which check()'s shared-stake audit relies on.\n */\nfunction eventIndexesEnvelope(event: NostrEvent, envelope: AttestationEnvelope): boolean {\n const d = event.tags.find((t) => t[0] === 'd')?.[1];\n const id = envelope.attestation_id;\n if (d !== id && d !== `orangecheck:${id}`) return false;\n return event.tags.some((t) => t[0] === 't' && t[1] === envelope.address);\n}\n\n/**\n * Discover attestations from Nostr relays\n */\nexport async function discoverAttestations(\n options: DiscoverAttestationOptions\n): Promise<AttestationEnvelope[]> {\n const { attestationId, address, identity, relays = DEFAULT_RELAYS } = options;\n\n let events: NostrEvent[] = [];\n\n if (attestationId) {\n log.info({ attestationId }, 'Discovering by attestation ID');\n events = await queryByAttestationId(attestationId, relays);\n } else if (address) {\n log.info({ address }, 'Discovering by Bitcoin address');\n events = await queryByAddress(address, relays);\n } else if (identity) {\n log.info({ identity }, 'Discovering by identity');\n events = await queryByIdentity(identity.protocol, identity.identifier, relays);\n } else {\n throw new Error('Must provide attestationId, address, or identity');\n }\n\n // Every relay holding an attestation returns it. One envelope per id, its\n // fields read from the signed message, and only where the relay's index tags\n // agree with it.\n const envelopes = await collectSignedEnvelopes(events, (event, envelope) => {\n if (eventIndexesEnvelope(event, envelope)) return true;\n log.warn({ eventId: event.id }, 'Skipping event whose index tags disagree with its envelope');\n return false;\n });\n\n log.info({ eventCount: events.length, validCount: envelopes.length }, 'Discovery complete');\n\n return envelopes;\n}\n\n/**\n * Verify an attestation by fetching it from Nostr and checking integrity\n */\nexport async function verifyAttestationById(\n attestationId: string,\n relays: string[] = DEFAULT_RELAYS\n): Promise<AttestationEnvelope | null> {\n log.info({ attestationId }, 'Verifying attestation by ID');\n\n const envelopes = await discoverAttestations({ attestationId, relays });\n\n if (envelopes.length === 0) {\n log.warn({ attestationId }, 'Attestation not found on any relay');\n return null;\n }\n\n if (envelopes.length > 1) {\n log.warn(\n { attestationId, count: envelopes.length },\n 'Multiple attestations found with same ID - using first'\n );\n }\n\n return envelopes[0] || null;\n}\n\n/**\n * Get all attestations for a Bitcoin address\n */\nexport async function getAttestationsForAddress(\n address: string,\n relays: string[] = DEFAULT_RELAYS\n): Promise<AttestationEnvelope[]> {\n log.info({ address }, 'Getting all attestations for address');\n return discoverAttestations({ address, relays });\n}\n\n/**\n * Get all attestations for an identity\n */\nexport async function getAttestationsForIdentity(\n protocol: string,\n identifier: string,\n relays: string[] = DEFAULT_RELAYS\n): Promise<AttestationEnvelope[]> {\n log.info({ protocol, identifier }, 'Getting all attestations for identity');\n return discoverAttestations({ identity: { protocol, identifier }, relays });\n}\n\n/**\n * Format identities for display as a comma-separated `protocol:identifier` list.\n * Same wire format the canonical message uses.\n */\nexport function formatIdentitiesForDisplay(identities: IdentityBinding[]): string {\n return identities.map((id) => `${id.protocol}:${id.identifier}`).join(', ');\n}\n\n/**\n * Build the canonical shareable URL for an attestation. Matches the live\n * `/attest/[id]` route on attest.ochk.io. Pass a different `baseUrl` for forks /\n * self-hosted verifiers.\n */\nexport function getVerificationUrl(\n attestationId: string,\n baseUrl = 'https://attest.ochk.io'\n): string {\n return `${baseUrl}/attest/${attestationId}`;\n}\n\n/**\n * Reverse of `getVerificationUrl` — pull an attestation id back out of a\n * `/attest/<id>` or legacy `/verify?id=<id>` URL. Returns `null` if the URL\n * shape doesn't match.\n */\nexport function extractAttestationIdFromUrl(url: string): string | null {\n try {\n const u = new URL(url);\n // New canonical form: /attest/<id>\n const m = u.pathname.match(/^\\/attest\\/([0-9a-f]{64})\\/?$/i);\n if (m) return m[1]!.toLowerCase();\n // Legacy form: /verify?id=<id>\n const q = u.searchParams.get('id');\n if (q && /^[0-9a-f]{64}$/i.test(q)) return q.toLowerCase();\n return null;\n } catch {\n return null;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/canonical.ts","../src/nostr-pubkey.ts","../src/utils/logger.ts","../src/utils/time.ts","../src/utils/signature.ts","../src/verify.ts","../src/nostr.ts","../src/nostr-crypto.ts","../src/attestation.ts"],"names":["verifyBitcoinSignature","log"],"mappings":"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","file":"attestation.mjs","sourcesContent":["/**\n * Canonical message builder per SPEC.md §2\n *\n * - LF line endings, exactly one trailing LF\n * - 7 fixed core lines in strict order and wording\n * - Optional extensions appended as key: value with lexicographically sorted lowercase keys\n */\n\nimport type { AttestationEnvelope, IdentityBinding, Scheme } from './types';\n\n/**\n * Input for building a canonical message\n */\nexport interface CanonicalInput {\n /** Bitcoin address */\n address: string;\n /** Optional identity bindings */\n identities?: IdentityBinding[];\n}\n\n/**\n * Extension key-value pairs for canonical message\n */\nexport type Extensions = Record<string, string | undefined>;\n\n/**\n * Convert bytes to lowercase hex string\n * @param bytes - Byte array to convert\n * @returns Lowercase hex string\n */\nfunction toLowerHex(bytes: Uint8Array): string {\n return Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n}\n\n/**\n * Generate a random 16-byte nonce as lowercase hex\n * Uses Web Crypto API when available, falls back to Math.random()\n * @returns 32-character lowercase hex string\n */\nexport function randomNonce16BHexLower(): string {\n const arr = new Uint8Array(16);\n\n try {\n if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {\n crypto.getRandomValues(arr);\n return toLowerHex(arr);\n }\n } catch {\n // Fall through to non-cryptographic fallback\n }\n\n // No silent Math.random fallback: a nonce is a load-bearing security\n // primitive. A deployment without `crypto.getRandomValues` MUST fix its\n // environment rather than sign with weak randomness.\n throw new Error('crypto.getRandomValues unavailable — refusing to mint a weak nonce');\n}\n\n/**\n * Reject characters that would break the line-oriented canonical grammar\n * or the comma-separated identities list. If any of these slipped through,\n * an attacker could smuggle a forged `address:` or extension line into the\n * signed payload: `identities: alice\\naddress: bc1qEVIL…` would parse as a\n * second address line. Catch it at the boundary, not at verification time.\n *\n * Also rejects a colon in the identifier so a bad actor can't turn\n * `github:alice:extra` into a different protocol binding on parse.\n */\nconst IDENTITY_PROTOCOL_RE = /^[a-z][a-z0-9_-]*$/;\nconst IDENTITY_FORBIDDEN_CHARS = /[\\r\\n,]/;\n\nfunction assertSafeIdentity({ protocol, identifier }: IdentityBinding): void {\n if (!IDENTITY_PROTOCOL_RE.test(protocol)) {\n throw new Error(\n `invalid identity protocol: ${JSON.stringify(protocol)} — must match /^[a-z][a-z0-9_-]*$/`\n );\n }\n if (!identifier || IDENTITY_FORBIDDEN_CHARS.test(identifier)) {\n throw new Error(\n `invalid identity identifier: ${JSON.stringify(identifier)} — must be non-empty and contain no newlines or commas`\n );\n }\n}\n\n/**\n * Format identities as comma-separated protocol:identifier pairs, sorted lexicographically\n * @param identities - Array of identity bindings\n * @returns Comma-separated string of protocol:identifier pairs\n */\nexport function formatIdentities(identities: IdentityBinding[] = []): string {\n if (identities.length === 0) {\n return '';\n }\n for (const id of identities) assertSafeIdentity(id);\n\n // Format as protocol:identifier and sort lexicographically\n const formatted = identities\n .map(({ protocol, identifier }) => `${protocol}:${identifier}`)\n .sort();\n\n return formatted.join(',');\n}\n\n/**\n * Parse identities from comma-separated protocol:identifier format\n * @param identitiesStr - Comma-separated identity string\n * @returns Array of identity bindings\n * @throws {Error} If identity format is invalid\n */\nexport function parseIdentities(identitiesStr: string): IdentityBinding[] {\n if (!identitiesStr || identitiesStr.trim() === '') {\n return [];\n }\n // The identities field lives on one line of the canonical message. If it\n // contains a raw LF/CR, it's either corrupt or an attempt to smuggle a\n // fake core line into the parser. Refuse instead of trying to recover.\n if (IDENTITY_FORBIDDEN_CHARS.test(identitiesStr.replace(/,/g, ''))) {\n throw new Error('identities field contains forbidden characters (newline)');\n }\n\n const bindings = identitiesStr.split(',').map((binding) => {\n const colonIndex = binding.indexOf(':');\n if (colonIndex === -1) {\n throw new Error(`Invalid identity binding format: ${binding}`);\n }\n return {\n protocol: binding.substring(0, colonIndex).trim(),\n identifier: binding.substring(colonIndex + 1).trim(),\n };\n });\n\n for (const b of bindings) {\n if (!b.protocol || !b.identifier) {\n throw new Error(`Invalid identity binding: ${b.protocol}:${b.identifier}`);\n }\n assertSafeIdentity(b);\n }\n return bindings;\n}\n\n/**\n * Optional deterministic overrides for message building. Only intended for\n * tests / conformance vectors; production callers should let the builder\n * mint a fresh nonce + timestamp per message.\n */\nexport interface BuildOptions {\n /** 32-char lowercase hex. Default: freshly-generated cryptographic nonce. */\n nonce?: string;\n /** RFC-3339 UTC. Default: `new Date().toISOString()`. */\n issuedAt?: string;\n}\n\n/**\n * Build a canonical message following OrangeCheck Protocol v0 specification\n * @param input - Canonical input with address and identities\n * @param extensions - Optional extension key-value pairs\n * @param options - Optional deterministic overrides (testing only)\n * @returns Canonical message string with LF line endings\n */\nexport function buildCanonicalMessage(\n { address, identities }: CanonicalInput,\n extensions: Extensions = {},\n options: BuildOptions = {}\n): string {\n const nonce = options.nonce ?? randomNonce16BHexLower();\n if (!/^[0-9a-f]{32}$/.test(nonce)) {\n throw new Error(`invalid nonce: must be 32 lowercase hex chars`);\n }\n const issuedAt = options.issuedAt ?? new Date().toISOString();\n\n // Format identities\n const identitiesStr = formatIdentities(identities);\n\n // Core (strict wording + order per SPEC.md §2)\n const core = [\n 'orangecheck',\n `identities: ${identitiesStr}`,\n `address: ${address}`,\n 'purpose: portable reputation attestation (non-custodial)',\n `nonce: ${nonce}`,\n `issued_at: ${issuedAt}`,\n 'ack: I attest control of this address and bind it to my identities.',\n ];\n\n // Normalize and sort extensions\n const normExt: Record<string, string> = {};\n for (const [k, v] of Object.entries(extensions)) {\n if (v == null || v === '') {\n continue;\n }\n const key = k.toLowerCase();\n normExt[key] = String(v);\n }\n\n const extLines = Object.keys(normExt)\n .sort()\n .map((k) => `${k}: ${normExt[k]!}`);\n\n const all = [...core, ...extLines];\n\n // Join with LF and ensure exactly one trailing LF\n return all.join('\\n') + '\\n';\n}\n\n/**\n * Generate attestation ID from canonical message\n *\n * attestation_id = SHA-256(canonical_message)\n *\n * @param message - Canonical message string\n * @returns Lowercase hex SHA-256 hash\n */\nexport async function generateAttestationId(message: string): Promise<string> {\n // Convert message to UTF-8 bytes\n const encoder = new TextEncoder();\n const data = encoder.encode(message);\n\n // Compute SHA-256 hash\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n\n // Convert to lowercase hex\n return toLowerHex(new Uint8Array(hashBuffer));\n}\n\n/**\n * Create an attestation envelope for publishing\n *\n * This wraps the canonical message with metadata for Nostr publishing\n *\n * @param message - Canonical message\n * @param signature - BIP-322 or legacy signature\n * @param scheme - Signature scheme used\n * @param address - Bitcoin address\n * @param identities - Identity bindings\n * @param extensions - Optional extensions\n * @returns Attestation envelope ready for publishing\n */\nexport async function createAttestationEnvelope(\n message: string,\n signature: string,\n scheme: Scheme,\n address: string,\n identities: IdentityBinding[],\n extensions: Extensions = {}\n): Promise<AttestationEnvelope> {\n const attestation_id = await generateAttestationId(message);\n\n // Extract issued_at from message\n const issuedAtMatch = message.match(/issued_at: ([^\\n]+)/);\n const issued_at = issuedAtMatch?.[1] ?? new Date().toISOString();\n\n // Extract expires from extensions if present\n const expires_at = extensions.expires;\n\n // Extract relay hints from extensions if present\n const relay_hints = extensions.relay_hints?.split(',').map((r) => r.trim());\n\n // Extract publish targets from extensions if present\n const publish_targets = extensions.publish?.split(',').map((t) => t.trim());\n\n // Base64url encode the message\n const encoder = new TextEncoder();\n const messageBytes = encoder.encode(message);\n const base64 = btoa(String.fromCharCode(...messageBytes));\n const message_b64url = base64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');\n\n // Construct verification URL\n const verification_url = `https://attest.ochk.io/attest/${attestation_id}`;\n\n return {\n attestation_id,\n scheme,\n address,\n identities,\n message,\n message_b64url,\n signature,\n issued_at,\n expires_at,\n verification_url,\n publish_targets,\n relay_hints,\n };\n}\n","import { bech32 } from '@scure/base';\n\nconst HEX_64_RE = /^[0-9a-f]{64}$/;\n\nfunction toHex(bytes: Uint8Array): string {\n let out = '';\n for (const b of bytes) out += b.toString(16).padStart(2, '0');\n return out;\n}\n\n/** A Nostr public key as lowercase hex, from either `npub1…` or hex. Null if it is neither. */\nexport function nostrPubkeyToHex(value: string): string | null {\n const trimmed = value.trim().toLowerCase();\n if (HEX_64_RE.test(trimmed)) return trimmed;\n if (!trimmed.startsWith('npub1')) return null;\n try {\n const decoded = bech32.decode(trimmed as `${string}1${string}`, 1023);\n if (decoded.prefix !== 'npub') return null;\n const bytes = bech32.fromWords(decoded.words);\n return bytes.length === 32 ? toHex(Uint8Array.from(bytes)) : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Every spelling of one Nostr key an attestation may carry in its `i` tag.\n * SPEC §2 prescribes `npub1…`; the reference site also accepts hex.\n */\nexport function nostrIdentifierForms(identifier: string): string[] {\n const hex = nostrPubkeyToHex(identifier);\n if (!hex) return [identifier];\n const bytes = Uint8Array.from(hex.match(/../g)!.map((b) => parseInt(b, 16)));\n return [bech32.encode('npub', bech32.toWords(bytes), 1023), hex];\n}\n","/**\n * Simple logger for SDK\n * Can be disabled in production builds\n */\n\n/**\n * Log level type\n */\nexport type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';\n\n/**\n * Logger context type\n */\nexport type LogContext = Record<string, unknown>;\n\n/**\n * Logger interface\n */\nexport interface Logger {\n debug: (contextOrMessage: string | LogContext, message?: string) => void;\n info: (contextOrMessage: string | LogContext, message?: string) => void;\n warn: (contextOrMessage: string | LogContext, message?: string) => void;\n error: (contextOrMessage: string | LogContext, message?: string) => void;\n}\n\nlet logLevel: LogLevel = 'none';\n\n/**\n * Set the global log level\n * @param level - Log level to set\n */\nexport function setLogLevel(level: LogLevel): void {\n logLevel = level;\n}\n\n/**\n * Get the current log level\n * @returns Current log level\n */\nexport function getLogLevel(): LogLevel {\n return logLevel;\n}\n\n/**\n * Create a namespaced logger\n * @param namespace - Logger namespace\n * @returns Logger instance\n */\nexport function createLogger(namespace: string): Logger {\n const shouldLog = (level: Exclude<LogLevel, 'none'>): boolean => {\n if (logLevel === 'none') return false;\n\n const levels: Exclude<LogLevel, 'none'>[] = ['debug', 'info', 'warn', 'error'];\n const currentLevelIndex = levels.indexOf(logLevel as Exclude<LogLevel, 'none'>);\n const messageLevelIndex = levels.indexOf(level);\n\n return messageLevelIndex >= currentLevelIndex;\n };\n\n const formatMessage = (msg: string): string => `[${namespace}] ${msg}`;\n\n return {\n debug: (contextOrMessage: string | LogContext, message?: string): void => {\n if (shouldLog('debug')) {\n if (typeof contextOrMessage === 'string') {\n console.debug(formatMessage(contextOrMessage));\n } else {\n console.debug(formatMessage(message ?? 'Debug'), contextOrMessage);\n }\n }\n },\n info: (contextOrMessage: string | LogContext, message?: string): void => {\n if (shouldLog('info')) {\n if (typeof contextOrMessage === 'string') {\n console.info(formatMessage(contextOrMessage));\n } else {\n console.info(formatMessage(message ?? 'Info'), contextOrMessage);\n }\n }\n },\n warn: (contextOrMessage: string | LogContext, message?: string): void => {\n if (shouldLog('warn')) {\n if (typeof contextOrMessage === 'string') {\n console.warn(formatMessage(contextOrMessage));\n } else {\n console.warn(formatMessage(message ?? 'Warning'), contextOrMessage);\n }\n }\n },\n error: (contextOrMessage: string | LogContext, message?: string): void => {\n if (shouldLog('error')) {\n if (typeof contextOrMessage === 'string') {\n console.error(formatMessage(contextOrMessage));\n } else {\n console.error(formatMessage(message ?? 'Error'), contextOrMessage);\n }\n }\n },\n };\n}\n\n/**\n * Log verification error\n * @param protocol - Identity protocol\n * @param identifier - Identity identifier\n * @param error - Error object\n * @param context - Additional context\n */\nexport function logVerificationError(\n protocol: string,\n identifier: string,\n error: Error,\n context?: LogContext\n): void {\n const logger = createLogger('identity-verification');\n logger.error(\n {\n protocol,\n identifier,\n error: error.message,\n stack: error.stack,\n ...context,\n },\n 'Verification failed'\n );\n}\n","/** How far ahead of the verifier's clock a signed issued_at may be (OC Attest SPEC §7). */\nexport const ISSUED_AHEAD_MS = 120_000;\n\nconst RFC3339 = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:\\d{2})$/;\n\n/** An RFC 3339 date-time as epoch milliseconds, or null when it is not one. */\nexport function rfc3339Ms(value: string | undefined): number | null {\n if (!value || !RFC3339.test(value)) return null;\n const ms = Date.parse(value);\n return Number.isNaN(ms) ? null : ms;\n}\n","import type { Scheme } from '../types';\n\nimport { Buffer } from 'buffer';\n\n/** A signature as base64: hex input is converted, anything else passes through. */\nexport function toBase64Maybe(sig: string): string {\n if (/^[0-9a-fA-F]+$/.test(sig) && sig.length % 2 === 0) {\n return Buffer.from(sig, 'hex').toString('base64');\n }\n return sig;\n}\n\n/**\n * The scheme a signature's bytes say it is, the same rule @orangecheck/bip322\n * verifies by: 65 bytes is a BIP-137 compact signature, whatever the address,\n * and anything else is BIP-322 simple. `null` for a BIP-322 full or\n * proof-of-funds signature, which is not supported.\n */\nexport function signatureScheme(sigBase64: string): Scheme | null {\n if (/^(ful|pof)/.test(sigBase64)) return null;\n if (sigBase64.startsWith('smp')) return 'bip322';\n return Buffer.from(sigBase64, 'base64').length === 65 ? 'legacy' : 'bip322';\n}\n","/**\n * OrangeCheck Protocol Verification\n *\n * Core verification logic for BIP-322 and legacy signatures,\n * UTXO analysis, and reputation metrics computation\n */\n\nimport type { ScoringAlgorithm } from './scoring';\nimport type {\n IdentityBinding,\n Metrics,\n Network,\n Scheme,\n StatusCode,\n VerifyInput,\n VerifyOptions,\n VerifyOutcome,\n} from './types';\n\nimport { verifySignature as verifyBitcoinSignature } from '@orangecheck/bip322';\n\nimport { generateAttestationId, parseIdentities } from './canonical';\nimport { computeAllScores, computeScore } from './scoring';\nimport { createLogger } from './utils/logger';\nimport { signatureScheme, toBase64Maybe } from './utils/signature';\nimport { ISSUED_AHEAD_MS, rfc3339Ms } from './utils/time';\n\nconst log = createLogger('ocp/verify');\n\n/**\n * Scoring function per OCP v0 specification (SPEC.md §6)\n * Formula: score = round( ln(1 + sats_bonded) * (1 + days_unspent / 30), 2 )\n *\n * This produces an unbounded score that combines both sats and time.\n * Typical ranges:\n * - 10k sats, 30 days ≈ 18\n * - 100k sats, 90 days ≈ 46\n * - 1M sats, 365 days ≈ 182\n * - 100M sats, 365 days ≈ 243\n */\n/**\n * Export scoring functions for use in other modules\n */\nexport { computeScore, computeAllScores, type ScoringAlgorithm };\n\n/**\n * Input for signature verification\n */\ninterface SignatureVerifyInput {\n /** Bitcoin address */\n addr: string;\n /** Message to verify */\n msg: string;\n /** Signature (hex or base64) */\n sig: string;\n /** @deprecated Ignored: the signature's own bytes decide the scheme. */\n scheme?: Scheme;\n}\n\n/**\n * Verify a signature and name the scheme that verified it. The scheme comes\n * from the signature's bytes (see `signatureScheme`), never from the address\n * or a caller's hint, so a valid BIP-137 signature verifies on every address\n * it can stand for, whatever its recovery id.\n */\nasync function verifySignature({ addr, msg, sig }: SignatureVerifyInput): Promise<StatusCode> {\n const sigBase64 = toBase64Maybe(sig);\n const scheme = signatureScheme(sigBase64);\n if (!scheme) return 'sig_unsupported_script';\n\n const ok = verifyBitcoinSignature(addr, msg, sigBase64);\n log.info({ addr, ok, scheme, msgLength: msg.length }, 'Signature verification result');\n if (!ok) return 'sig_invalid';\n return scheme === 'legacy' ? 'sig_ok_legacy' : 'sig_ok_bip322';\n}\n\n/** True when `sig` is a valid signature by `addr` over `msg`; no chain lookup. */\nexport async function signatureIsValid(input: SignatureVerifyInput): Promise<boolean> {\n try {\n const code = await verifySignature(input);\n return code === 'sig_ok_bip322' || code === 'sig_ok_legacy';\n } catch {\n return false;\n }\n}\n\n// Canonical message parsing per SPEC + our builder (lowercase keys, extensions)\nexport function parseCanonicalMessage(msg: string) {\n const lines = msg.split('\\n').map((l) => l.trim());\n // `.split(':', 2)[1]` truncates values containing a colon (e.g. URLs in\n // extensions, or npubs in identities). Use the first colon as the\n // separator and keep everything after it — same approach as the\n // extensions loop below.\n const getVal = (prefix: string): string | undefined => {\n const needle = prefix + ':';\n const hit = lines.find((l) => l.toLowerCase().startsWith(needle));\n if (!hit) return undefined;\n const idx = hit.indexOf(':');\n return hit.slice(idx + 1).trim();\n };\n\n const address = getVal('address') || '';\n const identitiesStr = getVal('identities') || '';\n const issued_at = getVal('issued_at') || '';\n // Present but empty is kept as '', so a reader can refuse it rather than skip it.\n const expires = getVal('expires');\n\n // Parse identities\n let identities: IdentityBinding[] = [];\n try {\n identities = parseIdentities(identitiesStr);\n } catch (err) {\n log.warn({ identitiesStr, error: err }, 'Failed to parse identities field');\n }\n\n // Extensions: collect all key: value pairs excluding core known keys\n const coreKeys = new Set([\n 'orangecheck',\n 'identities',\n 'address',\n 'purpose',\n 'nonce',\n 'issued_at',\n 'ack',\n ]);\n const extensions: Record<string, string> = {};\n // A key signed twice has two meanings, and readers could pick either one.\n const keys = new Set<string>();\n const repeated: string[] = [];\n for (const line of lines) {\n const idx = line.indexOf(':');\n if (idx === -1) continue;\n const key = line.slice(0, idx).trim().toLowerCase();\n const val = line.slice(idx + 1).trim();\n if (keys.has(key)) repeated.push(key);\n keys.add(key);\n if (!coreKeys.has(key) && val) extensions[key] = val;\n }\n\n const network = (extensions['network'] || 'mainnet') as Network;\n\n // Parse bond extension if present (integer satoshis)\n const bondStr = extensions['bond'];\n const bond = bondStr ? parseInt(bondStr, 10) : undefined;\n\n return {\n core: { address, identities, issued_at },\n expires,\n repeated,\n network,\n extensions,\n bond: bond !== undefined && !isNaN(bond) && bond > 0 ? bond : undefined,\n };\n}\n\n/**\n * UTXO type from Esplora API\n */\ninterface EsploraUtxo {\n /** Transaction ID */\n txid: string;\n /** Output index */\n vout: number;\n /** Value in satoshis */\n value: number;\n /** Confirmation status */\n status: {\n /** Whether UTXO is confirmed */\n confirmed: boolean;\n /** Block height (if confirmed) */\n block_height?: number;\n /** Block timestamp (if confirmed) */\n block_time?: number;\n };\n}\n\n/**\n * Fetch UTXOs for an address from Esplora API\n *\n * Tries multiple endpoints in order with automatic fallback\n *\n * @param address - Bitcoin address\n * @param network - Bitcoin network\n * @param opts - Optional custom Esplora endpoints\n * @returns Array of UTXOs\n * @throws {Error} If all endpoints fail\n */\nasync function getAddressUtxos(\n address: string,\n network: Network,\n opts?: {\n esploraMainnetBase?: string;\n esploraSignetBase?: string;\n esploraTestnetBase?: string;\n }\n): Promise<EsploraUtxo[]> {\n // Define primary and fallback endpoints\n const endpoints =\n network === 'signet'\n ? [\n opts?.esploraSignetBase || 'https://mempool.space/signet/api',\n 'https://blockstream.info/signet/api',\n ]\n : network === 'testnet'\n ? [\n opts?.esploraTestnetBase || 'https://mempool.space/testnet/api',\n 'https://blockstream.info/testnet/api',\n ]\n : [\n opts?.esploraMainnetBase || 'https://mempool.space/api',\n 'https://blockstream.info/api',\n ];\n\n let lastError: Error | null = null;\n\n // Try each endpoint in order. Per-endpoint timeout kept tight (5s) so\n // a slow upstream doesn't stack against the next one; callers who want\n // a hard overall deadline should wrap this call with their own timeout.\n for (const base of endpoints) {\n try {\n const url = `${base}/address/${address}/utxo`;\n log.debug({ url, network }, 'Fetching UTXOs');\n\n const resp = await fetch(url, {\n method: 'GET',\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(5000),\n });\n\n if (!resp.ok) {\n throw new Error(`HTTP ${resp.status}`);\n }\n\n const data = await resp.json();\n\n // Validate response structure\n if (!Array.isArray(data)) {\n throw new Error('Invalid response: expected array');\n }\n\n // Validate each UTXO has required fields\n for (const utxo of data) {\n if (\n typeof utxo.value !== 'number' ||\n typeof utxo.status !== 'object' ||\n typeof utxo.status.confirmed !== 'boolean' ||\n typeof utxo.txid !== 'string' ||\n typeof utxo.vout !== 'number'\n ) {\n throw new Error('Invalid UTXO structure');\n }\n }\n\n log.info({ base, count: data.length, network }, 'UTXOs fetched successfully');\n return data as EsploraUtxo[];\n } catch (err) {\n lastError = err instanceof Error ? err : new Error(String(err));\n log.warn(\n { base, error: lastError.message, network },\n 'Esplora endpoint failed, trying next'\n );\n }\n }\n\n // All endpoints failed\n log.error({ endpoints, error: lastError?.message, network }, 'All Esplora endpoints failed');\n throw new Error(`esplora_all_failed: ${lastError?.message || 'unknown'}`);\n}\n\n/**\n * Enhanced metrics computation per SPEC.md §5.4 and §6.1\n *\n * Supports bond extension with oldest-first greedy UTXO selection\n *\n * @param utxos - Array of UTXOs from Esplora\n * @param bondAmount - Optional bond amount (enables greedy selection)\n * @returns Computed metrics (sats_bonded, days_unspent, confirmed_balance)\n */\nfunction computeMetrics(\n utxos: EsploraUtxo[],\n bondAmount?: number,\n coverSats?: number\n): {\n /** Bonded satoshis (confirmed balance or bond amount) */\n sats_bonded: number;\n /** Days since oldest/youngest UTXO */\n days_unspent: number;\n /** Total confirmed balance */\n confirmed_balance: number;\n} {\n // Filter out unconfirmed UTXOs\n const confirmedUtxos = utxos.filter((u) => u.status.confirmed);\n if (confirmedUtxos.length === 0) {\n return { sats_bonded: 0, days_unspent: 0, confirmed_balance: 0 };\n }\n\n // Compute total confirmed balance\n const confirmed_balance = confirmedUtxos.reduce((sum, u) => sum + u.value, 0);\n\n // If bond extension is present, use oldest-first greedy algorithm\n if (bondAmount !== undefined && bondAmount > 0) {\n // Sort UTXOs by (block_height ASC, txid ASC, vout ASC) - oldest first\n const sorted = [...confirmedUtxos].sort((a, b) => {\n const heightA = a.status.block_height ?? Infinity;\n const heightB = b.status.block_height ?? Infinity;\n if (heightA !== heightB) return heightA - heightB;\n if (a.txid !== b.txid) return a.txid.localeCompare(b.txid);\n return a.vout - b.vout;\n });\n\n // Greedily select UTXOs until sum >= bond\n let sum = 0;\n let youngestTime = 0;\n for (const utxo of sorted) {\n sum += utxo.value;\n youngestTime = Math.max(youngestTime, utxo.status.block_time || 0);\n if (sum >= bondAmount) break;\n }\n\n // Age is computed from the youngest UTXO in the bonded set\n const days_unspent = Math.max(\n 0,\n Math.floor((Date.now() / 1000 - youngestTime) / (24 * 60 * 60))\n );\n\n return {\n sats_bonded: bondAmount, // Use exactly bond amount (ignore surplus)\n days_unspent,\n confirmed_balance,\n };\n }\n\n // Default behavior (no bond extension): sum all confirmed UTXOs\n const sats_bonded = confirmed_balance;\n\n // A checker asking for an amount gets the age of the coins that make it\n // up, oldest first, not the age of whichever coin is oldest.\n if (coverSats !== undefined && coverSats > 0) {\n const sorted = [...confirmedUtxos].sort(\n (a, b) => (a.status.block_height ?? Infinity) - (b.status.block_height ?? Infinity)\n );\n let sum = 0;\n let youngestTime = 0;\n for (const utxo of sorted) {\n sum += utxo.value;\n youngestTime = Math.max(youngestTime, utxo.status.block_time || Date.now() / 1000);\n if (sum >= coverSats) break;\n }\n const days_unspent = Math.max(\n 0,\n Math.floor((Date.now() / 1000 - youngestTime) / (24 * 60 * 60))\n );\n return { sats_bonded, days_unspent, confirmed_balance };\n }\n\n // Find the oldest confirmed UTXO for days_unspent\n const oldestTime = Math.min(\n ...confirmedUtxos.map((u) => u.status.block_time || Date.now() / 1000)\n );\n const days_unspent = Math.max(0, Math.floor((Date.now() / 1000 - oldestTime) / (24 * 60 * 60)));\n\n return { sats_bonded, days_unspent, confirmed_balance };\n}\n\n/**\n * Verify an OrangeCheck attestation\n *\n * This is the main verification function that:\n * 1. Validates the canonical message format\n * 2. Verifies the Bitcoin signature (BIP-322 or legacy)\n * 3. Fetches and analyzes UTXOs\n * 4. Computes reputation metrics\n *\n * @param input - Verification input (message, address, signature)\n * @param opts - Optional verification options (test mode, audience, esplora bases)\n * @returns Verification outcome with status codes and metrics\n *\n * @example\n * ```typescript\n * const result = await verify({\n * msg: canonicalMessage,\n * addr: 'bc1q...',\n * sig: 'AkcwRAIg...',\n * });\n *\n * if (result.ok) {\n * console.log('Score:', result.metrics?.score);\n * }\n * ```\n */\nexport async function verify(input: VerifyInput, opts: VerifyOptions = {}): Promise<VerifyOutcome> {\n const codes: StatusCode[] = [];\n let network: Network = 'mainnet';\n\n try {\n // Check if we have required fields\n if (!input.msg || !input.addr || !input.sig) {\n codes.push('bad_request');\n return { ok: false, codes, network };\n }\n\n // Parse canonical message with extensions\n const parsed = parseCanonicalMessage(input.msg);\n network = parsed.network || 'mainnet';\n\n // Compute attestation ID\n const attestation_id = await generateAttestationId(input.msg);\n\n // One value per key (SPEC §2.2).\n if (parsed.repeated.length > 0) {\n codes.push('decode_error');\n return { ok: false, codes, network, attestation_id };\n }\n\n // Validate address consistency\n if (parsed.core.address !== input.addr) {\n codes.push('bad_request');\n return { ok: false, codes, network, attestation_id };\n }\n\n // Testnet/Signet policy enforcement\n if ((network === 'testnet' || network === 'signet') && !opts.testMode) {\n codes.push('network_testmode');\n return { ok: false, codes, network, attestation_id };\n }\n\n // Optional policy checks for extensions.\n // `aud` binds the attestation to an origin. If the caller passed\n // `expectedAud`, the check must be load-bearing — a mismatch is a\n // phishing signal, not an advisory. Fail hard (spec-aligned).\n if (opts.expectedAud) {\n const aud = parsed.extensions['aud'];\n if (!aud || aud !== opts.expectedAud) {\n codes.push('aud_mismatch');\n return { ok: false, codes, network, attestation_id };\n }\n }\n\n // The signed issue time is an RFC 3339 time (SPEC §7.1.g) and not ahead\n // of this clock by more than the allowed skew.\n const issued = rfc3339Ms(parsed.core.issued_at);\n if (issued === null) {\n codes.push('decode_error');\n return { ok: false, codes, network, attestation_id };\n }\n if (issued > Date.now() + ISSUED_AHEAD_MS) {\n codes.push('not_yet_valid');\n return { ok: false, codes, network, attestation_id };\n }\n\n // Check message expiration. An expiry that is not an RFC 3339 time is\n // refused as expired: nothing shows it lies ahead.\n if (parsed.expires !== undefined) {\n const exp = rfc3339Ms(parsed.expires);\n if (exp === null || exp < Date.now()) {\n codes.push('expired');\n return { ok: false, codes, network, attestation_id };\n }\n }\n\n // Enhanced signature verification\n const sigCode = await verifySignature({\n addr: input.addr,\n msg: input.msg,\n sig: input.sig,\n ...(input.scheme && { scheme: input.scheme }),\n });\n codes.push(sigCode);\n\n const sigOk = sigCode === 'sig_ok_bip322' || sigCode === 'sig_ok_legacy';\n if (!sigOk) {\n return {\n ok: false,\n codes,\n network,\n attestation_id,\n identities: parsed.core.identities,\n };\n }\n\n // Enhanced UTXO analysis and metrics computation\n try {\n const utxos = await getAddressUtxos(input.addr, network, {\n esploraMainnetBase: opts.esploraMainnetBase,\n esploraSignetBase: opts.esploraSignetBase,\n });\n\n // Compute metrics with bond extension support (SPEC.md §5.4)\n const { sats_bonded, days_unspent, confirmed_balance } = computeMetrics(\n utxos,\n parsed.bond,\n opts.coverSats\n );\n\n // If bond extension is present, enforce minimum balance (SPEC.md §5.4.c)\n if (parsed.bond !== undefined && parsed.bond > 0) {\n if (confirmed_balance < parsed.bond) {\n codes.push('bond_insufficient');\n log.warn(\n {\n bond: parsed.bond,\n confirmed_balance,\n address: input.addr,\n },\n 'Bond extension: insufficient balance'\n );\n return {\n ok: false,\n codes,\n network,\n attestation_id,\n identities: parsed.core.identities,\n };\n }\n }\n\n // Analyze bond status\n if (sats_bonded <= 0) {\n codes.push('bond_zero');\n // Check for pending UTXOs\n if (utxos.some((u) => !u.status.confirmed)) {\n codes.push('bond_pending');\n }\n return {\n ok: false,\n codes,\n network,\n attestation_id,\n identities: parsed.core.identities,\n };\n } else {\n codes.push('bond_confirmed');\n }\n\n // Compute final metrics with enhanced scoring\n const metrics: Metrics = {\n sats_bonded,\n days_unspent,\n score: (computeScore(sats_bonded, days_unspent, { algorithm: 'v0' }) as number),\n };\n\n return {\n ok: true,\n codes,\n network,\n attestation_id,\n identities: parsed.core.identities,\n metrics,\n };\n } catch (utxoError) {\n log.error({ err: utxoError }, 'UTXO fetch failed');\n codes.push('bad_request');\n return { ok: false, codes, network, attestation_id };\n }\n } catch (e: unknown) {\n const msg = e instanceof Error ? e.message : String(e ?? 'decode_error');\n if (msg === 'decode_error') {\n codes.push('decode_error');\n } else {\n codes.push('bad_request');\n }\n return { ok: false, codes, network };\n }\n}\n","/**\n * Nostr Integration for OrangeCheck Protocol\n *\n * Implements NIP-78 (Application-specific data) for attestation publishing\n */\n\nimport type { AttestationEnvelope, IdentityBinding, NostrEvent } from './types';\n\nimport { nostrIdentifierForms } from './nostr-pubkey';\nimport { createLogger } from './utils/logger';\nimport { rfc3339Ms } from './utils/time';\nimport { parseCanonicalMessage, signatureIsValid } from './verify';\n\nconst log = createLogger('ocp/nostr');\n\n/**\n * NIP-78 event kind for OrangeCheck attestations\n */\nexport const ATTESTATION_EVENT_KIND = 30078;\n\n/**\n * Default Nostr relays for OrangeCheck attestation publishing and lookup.\n *\n * Well-known relays with good uptime, plus the first-party family relay.\n * `relay.ochk.io` was missing here while attest.ochk.io's verifier has always\n * read from it, so the SDK and the reference implementation were looking in\n * different places — the SDK could miss a family attestation that the hosted\n * API found. It runs a kind allowlist (30078-30087, 30110-30114) and canonical OC d-tag\n * prefixes, and is always co-published alongside the public relays, never the\n * only copy. See https://github.com/orangecheck/oc-relay-infra.\n *\n * Order matters only for readability; queries fan out to all of them and\n * merge. Pass your own `relays` to any query to override this entirely — the\n * protocol does not require trusting any particular relay.\n *\n * A NOTE ON `relay.damus.io`, deliberately kept. It is reachable from a browser\n * (measured ~391ms, real events returned) and NOT reachable from at least one\n * serverless runtime — Vercel's, where it fails to connect on every attempt,\n * consistently, over hours. Its NIP-11 declares no `auth_required`,\n * `payment_required` or `restricted_writes`, so this reads as datacenter-IP\n * filtering in front of it rather than policy, and it can change in either\n * direction without notice.\n *\n * It stays for three reasons: it is one of the most widely read relays, so an\n * attestation published there is findable by the most people; the browser is\n * the dominant path for this SDK (wallet signing happens there), and it works\n * there; and since FANOUT_DEADLINE_MS below is a SHARED budget, a relay that\n * fails to connect resolves immediately and costs no latency. Removing a relay\n * that works for most callers, to tidy a status field on one runtime, is the\n * wrong trade.\n *\n * If you run this SDK server-side and want certainty about coverage, pass\n * `relays` explicitly. Silent partial coverage — believing you reached four\n * relays when you reached three — is the real hazard here, not latency.\n */\nexport const DEFAULT_RELAYS: string[] = [\n 'wss://relay.damus.io',\n 'wss://nos.lol',\n // relay.snort.social sat here until 2026-10-21. It now runs in memory: it\n // answered OK true to family events and served none of them back.\n 'wss://nostr.oxtr.dev',\n 'wss://relay.ochk.io',\n];\n\n/**\n * Wall-clock budget for a whole relay fan-out, shared by every relay in it.\n *\n * Queries below use Promise.allSettled, so a fan-out used to take as long as\n * its SLOWEST relay — and with a dead relay in DEFAULT_RELAYS that meant the\n * per-relay 10s timeout, every single call. Measured on 1.4.0:\n *\n * check({ addr }) with the old defaults 10.4s\n * check({ addr }) with the dead relay out 1.0s\n *\n * That is not merely slow, it broke a dependent outright: @orangecheck/gate\n * races check() against a 5s lookupTimeoutMs, so a 10.4s lookup lost the race\n * every time and no address-based gate decision could ever succeed.\n *\n * A shared deadline fixes the class: whichever relay is dead or slow next, a\n * fan-out costs at most this. Healthy relays answer well inside it (~1s for a\n * cold WebSocket plus a query). QUERY_TIMEOUT_MS remains the per-relay ceiling.\n *\n * The value is deliberately BELOW @orangecheck/gate's 5s default\n * lookupTimeoutMs, so a gate decision can still resolve in the worst case\n * rather than always losing its own race. Raising this above 5000 silently\n * breaks that dependent — change both together, or don't.\n */\nexport const FANOUT_DEADLINE_MS = 4000;\nconst QUERY_TIMEOUT_MS = 10000;\n\n/** Remaining budget for a relay, given the fan-out's shared deadline. */\nfunction budgetFor(deadlineAt: number): number {\n return Math.max(0, Math.min(QUERY_TIMEOUT_MS, deadlineAt - Date.now()));\n}\n\n/**\n * Create a NIP-78 event for an OrangeCheck attestation\n *\n * Kind: 30078 (Parameterized Replaceable Event)\n * d-tag: attestation_id (makes it replaceable by attestation ID)\n *\n * @param envelope - Attestation envelope\n * @param pubkey - Nostr public key (hex format)\n * @returns Unsigned Nostr event\n */\nexport function createAttestationEvent(\n envelope: AttestationEnvelope,\n pubkey: string\n): Omit<NostrEvent, 'id' | 'sig'> {\n // NIP-12 relays index SINGLE-LETTER tags only. A multi-letter tag like\n // `address` is stored verbatim and stays readable in the event, but it is\n // never indexed, so no `#address` filter can ever retrieve it. Without the\n // `t` tags below an attestation published by this SDK is invisible to\n // every by-address query on every relay — including attest.ochk.io's own\n // verifier, which queries `#t`. Keep both: `t` is what makes it findable,\n // `address` is human-readable diagnostics and is not load-bearing.\n const tags: string[][] = [\n ['d', envelope.attestation_id], // Parameterized replaceable event identifier (indexed)\n ['t', envelope.address], // Indexed btc-address tag — query with `#t`\n ['t', 'oc-attest'], // Family marker — `#t=[\"oc-attest\"]` for cross-cut discovery\n ['address', envelope.address], // Diagnostic / legacy — relay won't index this\n ['scheme', envelope.scheme], // Signature scheme (bip322 or legacy)\n ['issued_at', envelope.issued_at], // ISO timestamp\n ];\n\n // Add identity tags\n for (const identity of envelope.identities) {\n tags.push(['i', `${identity.protocol}:${identity.identifier}`]);\n }\n\n // Add expiration if present\n if (envelope.expires_at) {\n tags.push(['expires', envelope.expires_at]);\n }\n\n // Add relay hints if present\n if (envelope.relay_hints?.length) {\n for (const relay of envelope.relay_hints) {\n tags.push(['relay', relay]);\n }\n }\n\n // Content is the full attestation envelope as JSON\n const content = JSON.stringify(envelope, null, 2);\n\n return {\n kind: ATTESTATION_EVENT_KIND,\n tags,\n content,\n created_at: Math.floor(Date.parse(envelope.issued_at) / 1000),\n pubkey,\n };\n}\n\n/**\n * Parse identity bindings from Nostr event tags\n *\n * @param event - Nostr event\n * @returns Array of identity bindings\n */\nexport function parseIdentitiesFromEvent(event: NostrEvent): IdentityBinding[] {\n const identities: IdentityBinding[] = [];\n\n for (const tag of event.tags) {\n if (tag[0] === 'i' && tag[1]) {\n const colonIndex = tag[1].indexOf(':');\n if (colonIndex !== -1) {\n identities.push({\n protocol: tag[1].substring(0, colonIndex),\n identifier: tag[1].substring(colonIndex + 1),\n });\n }\n }\n }\n\n return identities;\n}\n\n/**\n * Extract attestation ID from Nostr event d-tag\n *\n * @param event - Nostr event\n * @returns Attestation ID or undefined if not found\n */\nexport function getAttestationIdFromEvent(event: NostrEvent): string | undefined {\n const dTag = event.tags.find((tag) => tag[0] === 'd');\n return dTag?.[1];\n}\n\n/**\n * Extract Bitcoin address from Nostr event tags\n *\n * @param event - Nostr event\n * @returns Bitcoin address or undefined if not found\n */\nexport function getAddressFromEvent(event: NostrEvent): string | undefined {\n const addressTag = event.tags.find((tag) => tag[0] === 'address');\n return addressTag?.[1];\n}\n\n/**\n * Publish a Nostr event to multiple relays\n * Returns array of relay URLs that successfully accepted the event\n */\nexport async function publishToRelays(\n event: NostrEvent,\n relays: string[] = DEFAULT_RELAYS\n): Promise<{ success: string[]; failed: string[] }> {\n const success: string[] = [];\n const failed: string[] = [];\n\n log.info({ eventId: event.id, relayCount: relays.length }, 'Publishing event to relays');\n\n // One shared deadline for the whole fan-out — see FANOUT_DEADLINE_MS.\n\n const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;\n\n const publishPromises = relays.map(async (relayUrl) => {\n let ws: WebSocket | null = null;\n\n // One outcome per relay, first one wins. Without this a relay that\n // ACKs `OK true` and then emits an error or close event during\n // teardown lands in BOTH arrays, and the caller reports \"published\n // to 3 · rejected 3\" over the same three relays. The `failed.includes`\n // check this replaces only ever de-duplicated `failed` against\n // itself, so it could not catch a success/failure split.\n // attest.ochk.io's copy of this file has carried the guard for a\n // while; this is the back-port.\n let settled = false;\n const recordSuccess = (): void => {\n if (settled) return;\n settled = true;\n success.push(relayUrl);\n };\n const recordFailure = (): void => {\n if (settled) return;\n settled = true;\n failed.push(relayUrl);\n };\n\n try {\n // Connect to relay\n ws = new WebSocket(relayUrl);\n const wsRef = ws; // Capture for closures\n\n await new Promise<void>((resolve, reject) => {\n // One outer deadline covers connect + OK round-trip. The old\n // code cleared this on `onopen`, which meant a relay that\n // opened but never sent OK would hang the promise forever.\n const deadline = setTimeout(() => {\n wsRef.close();\n reject(new Error('Relay publish timeout'));\n }, budgetFor(deadlineAt));\n\n const cleanup = () => {\n clearTimeout(deadline);\n wsRef.close();\n };\n\n wsRef.onopen = () => {\n // Do NOT clear the deadline — we still need it in case\n // the relay never responds with OK.\n wsRef.send(JSON.stringify(['EVENT', event]));\n };\n\n wsRef.onmessage = (msg) => {\n try {\n const data = JSON.parse(msg.data);\n // Check for OK response\n if (data[0] === 'OK' && data[1] === event.id) {\n if (data[2] === true) {\n recordSuccess();\n cleanup();\n resolve();\n } else {\n log.warn(\n { relay: relayUrl, reason: data[3] },\n 'Relay rejected event'\n );\n recordFailure();\n cleanup();\n reject(new Error(data[3] || 'Relay rejected event'));\n }\n }\n } catch (err) {\n log.error(\n { relay: relayUrl, error: err },\n 'Failed to parse relay response'\n );\n recordFailure();\n cleanup();\n reject(err);\n }\n };\n\n wsRef.onerror = (err) => {\n log.error({ relay: relayUrl, error: err }, 'WebSocket error');\n recordFailure();\n cleanup();\n reject(err);\n };\n\n wsRef.onclose = () => {\n // Socket closed before we resolved/rejected — counts as\n // failure. clearTimeout here is safe (cleanup handles both).\n clearTimeout(deadline);\n };\n });\n } catch (err) {\n log.error({ relay: relayUrl, error: err }, 'Failed to publish to relay');\n recordFailure();\n } finally {\n // Ensure WebSocket is closed\n if (ws && ws.readyState !== WebSocket.CLOSED) {\n ws.close();\n }\n }\n });\n\n await Promise.allSettled(publishPromises);\n\n log.info({ successCount: success.length, failedCount: failed.length }, 'Publishing complete');\n\n return { success, failed };\n}\n\n/** Events asked for per page: relays commonly cap one REQ at 500. */\nconst PAGE_LIMIT = 500;\n/**\n * Pages read per relay for one subject. Anyone can publish events under a\n * subject's tags, so a lookup reads every event a relay holds for it up to\n * PAGE_LIMIT × MAX_PAGES (2,000), newest first, and leaves the choice to the\n * signatures. An older event goes unread only when every queried relay holds\n * more than that many newer events for the same subject.\n */\nconst MAX_PAGES = 4;\n\n/** Every event matching `filter` on each relay, paged by `until`, within the shared deadline. */\nasync function querySubject(\n filter: Record<string, unknown>,\n relays: string[],\n label: string\n): Promise<NostrEvent[]> {\n const events: NostrEvent[] = [];\n const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;\n\n await Promise.allSettled(\n relays.map(async (relayUrl) => {\n let ws: WebSocket | null = null;\n try {\n ws = new WebSocket(relayUrl);\n const wsRef = ws;\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n wsRef.close();\n reject(new Error('Query timeout'));\n }, budgetFor(deadlineAt));\n const seen = new Set<string>();\n let page = 0;\n let inPage = 0;\n let newInPage = 0;\n let oldest = Infinity;\n\n const ask = () => {\n page++;\n inPage = 0;\n newInPage = 0;\n // `until` is inclusive; events already read are skipped by id.\n const until = Number.isFinite(oldest) ? { until: oldest } : {};\n wsRef.send(\n JSON.stringify([\n 'REQ',\n `ochk_${label}_${Date.now()}_${page}`,\n { ...filter, ...until, limit: PAGE_LIMIT },\n ])\n );\n };\n\n wsRef.onopen = ask;\n wsRef.onmessage = (msg) => {\n try {\n const data = JSON.parse(msg.data);\n if (data[0] === 'EVENT') {\n const event = data[2] as NostrEvent;\n const key = event.id ?? JSON.stringify(event);\n inPage++;\n if (!seen.has(key)) {\n seen.add(key);\n newInPage++;\n events.push(event);\n }\n if (typeof event.created_at === 'number') {\n oldest = Math.min(oldest, event.created_at);\n }\n } else if (data[0] === 'EOSE') {\n wsRef.send(JSON.stringify(['CLOSE', data[1]]));\n // A full page that brought something new may have more behind it.\n if (inPage >= PAGE_LIMIT && newInPage > 0 && page < MAX_PAGES) {\n ask();\n } else {\n clearTimeout(timeout);\n wsRef.close();\n resolve();\n }\n }\n } catch (err) {\n log.error({ relay: relayUrl, error: err }, 'Failed to parse event');\n }\n };\n wsRef.onerror = (err) => {\n clearTimeout(timeout);\n log.error({ relay: relayUrl, error: err }, 'WebSocket error during query');\n reject(err);\n };\n });\n } catch (err) {\n log.error({ relay: relayUrl, error: err }, 'Failed to query relay');\n } finally {\n if (ws && ws.readyState !== WebSocket.CLOSED) ws.close();\n }\n })\n );\n\n log.info({ label, eventCount: events.length }, 'Query complete');\n return events;\n}\n\n/**\n * Query relays for every event under an attestation id's `d` tag. Anyone can\n * publish an event with that tag, so the copies are read and the signature\n * decides between them (see collectSignedEnvelopes).\n */\nexport async function queryByAttestationId(\n attestationId: string,\n relays: string[] = DEFAULT_RELAYS\n): Promise<NostrEvent[]> {\n log.info({ attestationId, relayCount: relays.length }, 'Querying relays for attestation');\n return querySubject({ kinds: [30078], '#d': [attestationId] }, relays, 'id');\n}\n\n/**\n * Query relays for attestations by Bitcoin address. Relays index\n * single-letter tags only, so the address is the `t` tag, not `address`.\n */\nexport async function queryByAddress(\n address: string,\n relays: string[] = DEFAULT_RELAYS\n): Promise<NostrEvent[]> {\n log.info({ address, relayCount: relays.length }, 'Querying relays for address');\n return querySubject({ kinds: [30078], '#t': [address] }, relays, 'addr');\n}\n\n/**\n * Query relays for attestations by identity (any protocol)\n */\nexport async function queryByIdentity(\n protocol: string,\n identifier: string,\n relays: string[] = DEFAULT_RELAYS\n): Promise<NostrEvent[]> {\n const identityTags =\n protocol === 'nostr'\n ? nostrIdentifierForms(identifier).map((f) => `nostr:${f}`)\n : [`${protocol}:${identifier}`];\n log.info({ protocol, identifier, relayCount: relays.length }, 'Querying relays for identity');\n return querySubject({ kinds: [30078], '#i': identityTags }, relays, 'identity');\n}\n\n/**\n * Query relays for the most recent attestations across every publisher.\n *\n * For aggregate views (network stats, a recent-activity feed), not per-subject\n * lookups. Filters on the `#t=[\"oc-attest\"]` family marker because kind 30078\n * is shared by every NIP-78 app: an unfiltered window fills with other apps'\n * data. Returns envelopes whose attestation_id matches their message,\n * deduplicated by attestation_id. Signatures are not checked — call verify()\n * on anything you act on.\n */\nexport async function queryRecent(\n relays: string[] = DEFAULT_RELAYS,\n limit: number = 200\n): Promise<AttestationEnvelope[]> {\n const events: NostrEvent[] = [];\n\n log.info({ relayCount: relays.length, limit }, 'Querying relays for recent attestations');\n\n // One shared deadline for the whole fan-out — see FANOUT_DEADLINE_MS.\n const deadlineAt = Date.now() + FANOUT_DEADLINE_MS;\n\n const queryPromises = relays.map(async (relayUrl) => {\n try {\n const ws = new WebSocket(relayUrl);\n\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n ws.close();\n reject(new Error('Query timeout'));\n }, budgetFor(deadlineAt));\n\n ws.onopen = () => {\n const subscriptionId = `ochk_recent_${Date.now()}`;\n ws.send(\n JSON.stringify([\n 'REQ',\n subscriptionId,\n { kinds: [ATTESTATION_EVENT_KIND], '#t': ['oc-attest'], limit },\n ])\n );\n };\n\n ws.onmessage = (msg) => {\n try {\n const data = JSON.parse(msg.data);\n if (data[0] === 'EVENT') {\n const ev = data[2] as NostrEvent;\n const scheme = ev.tags?.find((t) => t[0] === 'scheme')?.[1];\n const dTag = ev.tags?.find((t) => t[0] === 'd')?.[1];\n if ((scheme === 'bip322' || scheme === 'legacy') && dTag) {\n events.push(ev);\n }\n } else if (data[0] === 'EOSE') {\n clearTimeout(timeout);\n ws.close();\n resolve();\n }\n } catch (err) {\n log.error({ relay: relayUrl, error: err }, 'Failed to parse event');\n }\n };\n\n ws.onerror = (err) => {\n clearTimeout(timeout);\n log.error({ relay: relayUrl, error: err }, 'WebSocket error during query');\n reject(err);\n };\n });\n } catch (err) {\n log.error({ relay: relayUrl, error: err }, 'Failed to query relay');\n }\n });\n\n await Promise.allSettled(queryPromises);\n\n const envelopes = await collectSignedEnvelopes(events);\n\n log.info({ eventCount: events.length, unique: envelopes.length }, 'Recent query complete');\n\n return envelopes;\n}\n\n/**\n * Verify attestation envelope integrity\n * Checks that the attestation ID matches the message hash\n */\nexport async function verifyAttestationEnvelope(envelope: AttestationEnvelope): Promise<boolean> {\n try {\n // Re-compute attestation ID from message\n const encoder = new TextEncoder();\n const data = encoder.encode(envelope.message);\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n const hashArray = Array.from(new Uint8Array(hashBuffer));\n const computedId = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');\n\n return computedId === envelope.attestation_id;\n } catch (err) {\n log.error({ error: err }, 'Failed to verify attestation envelope');\n return false;\n }\n}\n\nfunction base64Url(text: string): string {\n const base64 = btoa(String.fromCharCode(...new TextEncoder().encode(text)));\n return base64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');\n}\n\n/**\n * The envelope with every field the signature covers read from its message.\n *\n * Only `message` and `signature` are signed. `address`, `identities`,\n * `issued_at`, `expires_at` and `message_b64url` are copies beside them, so\n * they are taken from the message here. Null when the message names no address\n * or no RFC 3339 `issued_at`, or signs any key twice.\n */\nexport function signedEnvelope(envelope: AttestationEnvelope): AttestationEnvelope | null {\n let parsed: ReturnType<typeof parseCanonicalMessage>;\n try {\n parsed = parseCanonicalMessage(envelope.message);\n } catch {\n return null;\n }\n if (!parsed.core.address || rfc3339Ms(parsed.core.issued_at) === null) return null;\n if (parsed.repeated.length > 0) return null;\n const { expires_at: _copy, ...rest } = envelope;\n return {\n ...rest,\n address: parsed.core.address,\n identities: parsed.core.identities,\n issued_at: parsed.core.issued_at,\n ...(parsed.expires !== undefined ? { expires_at: parsed.expires } : {}),\n message_b64url: base64Url(envelope.message),\n };\n}\n\n/**\n * One envelope per attestation id, its fields read from its signed message.\n *\n * Anyone can publish a copy of an attestation under their own Nostr key, with\n * any signature beside the message. When copies of one id disagree on the\n * signature, the first whose signature is valid is kept.\n */\nexport async function collectSignedEnvelopes(\n events: NostrEvent[],\n accept: (event: NostrEvent, envelope: AttestationEnvelope) => boolean = () => true\n): Promise<AttestationEnvelope[]> {\n const copies = new Map<string, AttestationEnvelope[]>();\n for (const event of events) {\n const raw = parseAttestationFromEvent(event);\n if (!raw || !(await verifyAttestationEnvelope(raw))) continue;\n const envelope = signedEnvelope(raw);\n if (!envelope || !accept(event, envelope)) continue;\n const same = copies.get(envelope.attestation_id) ?? [];\n if (!same.some((e) => e.signature === envelope.signature && e.scheme === envelope.scheme)) {\n same.push(envelope);\n }\n copies.set(envelope.attestation_id, same);\n }\n const kept: AttestationEnvelope[] = [];\n for (const same of copies.values()) {\n if (same.length === 1) {\n kept.push(same[0]!);\n continue;\n }\n let valid: AttestationEnvelope | undefined;\n for (const e of same) {\n if (await signatureIsValid({ addr: e.address, msg: e.message, sig: e.signature, scheme: e.scheme })) {\n valid = e;\n break;\n }\n }\n kept.push(valid ?? same[0]!);\n }\n return kept;\n}\n\n/**\n * Parse attestation envelope from Nostr event content\n */\nexport function parseAttestationFromEvent(event: NostrEvent): AttestationEnvelope | null {\n try {\n const envelope = JSON.parse(event.content) as AttestationEnvelope;\n return envelope;\n } catch (err) {\n log.error({ eventId: event.id, error: err }, 'Failed to parse attestation from event');\n return null;\n }\n}\n","/**\n * Nostr Cryptographic Functions\n * Implements NIP-01 event ID computation and signature verification\n */\n\nimport type { NostrEvent } from './types';\n\nimport { createLogger } from './utils/logger';\n\ntype Nip07Window = {\n nostr?: {\n getPublicKey: () => Promise<string>;\n signEvent: (event: Omit<NostrEvent, 'id' | 'sig'>) => Promise<NostrEvent>;\n _metadata?: { name?: string; version?: string };\n };\n};\n\nconst log = createLogger('ocp/nostr-crypto');\n\n/**\n * Compute Nostr event ID according to NIP-01\n * Event ID is the SHA-256 hash of the serialized event\n * Serialization: [0, pubkey, created_at, kind, tags, content]\n */\nexport async function computeNostrEventId(event: Omit<NostrEvent, 'id' | 'sig'>): Promise<string> {\n // Serialize event according to NIP-01\n const serialized = JSON.stringify([\n 0, // Reserved for future use\n event.pubkey,\n event.created_at,\n event.kind,\n event.tags,\n event.content,\n ]);\n\n log.debug({ serialized }, 'Computing event ID');\n\n // Compute SHA-256 hash\n const encoder = new TextEncoder();\n const data = encoder.encode(serialized);\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n\n // Convert to lowercase hex\n const hashArray = Array.from(new Uint8Array(hashBuffer));\n const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');\n\n log.debug({ eventId: hashHex }, 'Computed event ID');\n\n return hashHex;\n}\n\n/**\n * Verify Nostr event ID\n * Recomputes the ID and checks if it matches\n */\nexport async function verifyNostrEventId(event: NostrEvent): Promise<boolean> {\n const computedId = await computeNostrEventId(event);\n return computedId === event.id;\n}\n\n/**\n * Check if NIP-07 extension is available\n * NIP-07 provides window.nostr for signing events\n */\nexport function isNip07Available(): boolean {\n if (typeof window === 'undefined') {\n return false;\n }\n\n return !!(window as unknown as { nostr?: Nip07Window['nostr'] }).nostr!;\n}\n\n/**\n * Get Nostr public key from NIP-07 extension\n * Returns null if extension not available or user denies\n */\nexport async function getNostrPublicKey(): Promise<string | null> {\n if (!isNip07Available()) {\n log.warn('NIP-07 extension not available');\n return null;\n }\n\n try {\n const nostr = (window as unknown as { nostr?: Nip07Window['nostr'] }).nostr!;\n const pubkey = await nostr.getPublicKey();\n log.info({ pubkey }, 'Got Nostr public key from NIP-07');\n return pubkey;\n } catch (err) {\n log.error({ error: err }, 'Failed to get Nostr public key');\n return null;\n }\n}\n\n/**\n * Sign Nostr event using NIP-07 extension\n * Returns signed event with id and sig fields\n */\nexport async function signNostrEvent(\n event: Omit<NostrEvent, 'id' | 'sig'>\n): Promise<NostrEvent | null> {\n if (!isNip07Available()) {\n log.warn('NIP-07 extension not available');\n return null;\n }\n\n try {\n const nostr = (window as unknown as { nostr?: Nip07Window['nostr'] }).nostr!;\n\n // NIP-07 signEvent expects the event without id and sig\n const signedEvent = await nostr.signEvent(event);\n\n log.info({ eventId: signedEvent.id }, 'Signed Nostr event with NIP-07');\n\n return signedEvent as NostrEvent;\n } catch (err) {\n log.error({ error: err }, 'Failed to sign Nostr event');\n return null;\n }\n}\n\n// Previously this file exported `generateEphemeralKeypair()` and\n// `signEventWithEphemeralKey()` for demo purposes. Both produced non-schnorr\n// values that Nostr relays would silently reject — creating the false\n// impression that publishing had succeeded. They have been removed. All Nostr\n// signing now flows through `signNostrEvent()` / `window.nostr` (NIP-07).\n\n/**\n * Validate Nostr event structure\n */\nexport function validateNostrEvent(event: unknown): event is NostrEvent {\n if (typeof event !== 'object' || event === null) return false;\n const e = event as Record<string, unknown>;\n\n if (typeof e.id !== 'string' || (e.id as string).length !== 64) return false;\n if (typeof e.pubkey !== 'string' || (e.pubkey as string).length !== 64) return false;\n if (typeof e.created_at !== 'number') return false;\n if (typeof e.kind !== 'number') return false;\n if (!Array.isArray(e.tags)) return false;\n if (typeof e.content !== 'string') return false;\n if (typeof e.sig !== 'string' || (e.sig as string).length !== 128) return false;\n\n return true;\n}\n\n/**\n * Get NIP-07 extension info\n */\nexport function getNip07Info(): {\n available: boolean;\n name?: string;\n version?: string;\n} {\n if (!isNip07Available()) {\n return { available: false };\n }\n\n const nostr = (window as unknown as { nostr?: Nip07Window['nostr'] }).nostr!;\n\n return {\n available: true,\n name: nostr._metadata?.name,\n version: nostr._metadata?.version,\n };\n}\n","/**\n * Attestation Manager\n *\n * High-level API for creating, publishing, and discovering OrangeCheck attestations\n */\n\nimport type { AttestationEnvelope, IdentityBinding, NostrEvent, Scheme } from './types';\n\nimport { createAttestationEnvelope } from './canonical';\nimport {\n collectSignedEnvelopes,\n createAttestationEvent,\n DEFAULT_RELAYS,\n publishToRelays,\n queryByAddress,\n queryByAttestationId,\n queryByIdentity,\n verifyAttestationEnvelope,\n} from './nostr';\nimport { isNip07Available, signNostrEvent } from './nostr-crypto';\nimport { createLogger } from './utils/logger';\n\nconst log = createLogger('ocp/attestation');\n\n/**\n * Options for creating an attestation\n */\nexport interface CreateAttestationOptions {\n /** Canonical message */\n message: string;\n /** BIP-322 or legacy signature */\n signature: string;\n /** Signature scheme used */\n scheme: Scheme;\n /** Bitcoin address */\n address: string;\n /** Optional identity bindings */\n identities?: IdentityBinding[];\n /** Optional extension key-value pairs */\n extensions?: Record<string, string | undefined>;\n}\n\n/**\n * Options for publishing an attestation to Nostr\n */\nexport interface PublishAttestationOptions {\n /** Attestation envelope to publish */\n envelope: AttestationEnvelope;\n /** Nostr public key (npub or hex) for signing the event */\n npub: string;\n /** Optional relay URLs (defaults to DEFAULT_RELAYS) */\n relays?: string[];\n}\n\n/**\n * Options for discovering attestations\n */\nexport interface DiscoverAttestationOptions {\n /** Search by attestation ID */\n attestationId?: string;\n /** Search by Bitcoin address */\n address?: string;\n /** Search by identity binding */\n identity?: { protocol: string; identifier: string };\n /** Optional relay URLs to query */\n relays?: string[];\n}\n\n/**\n * Result of publishing an attestation\n */\nexport interface PublishResult {\n /** Relays that successfully received the event */\n success: string[];\n /** Relays that failed to receive the event */\n failed: string[];\n}\n\n/**\n * Custom error for attestation operations\n */\nexport class AttestationError extends Error {\n constructor(\n message: string,\n public readonly code?: string\n ) {\n super(message);\n this.name = 'AttestationError';\n }\n}\n\n/**\n * Create an attestation envelope from a signed message\n *\n * @param options - Attestation creation options\n * @returns Attestation envelope ready for publishing\n * @throws {AttestationError} If envelope integrity check fails\n *\n * @example\n * ```typescript\n * const envelope = await createAttestation({\n * message: canonicalMessage,\n * signature: 'AkcwRAIg...',\n * scheme: 'bip322',\n * address: 'bc1q...',\n * identities: [{ protocol: 'nostr', identifier: 'npub1...' }],\n * });\n * ```\n */\nexport async function createAttestation(\n options: CreateAttestationOptions\n): Promise<AttestationEnvelope> {\n const { message, signature, scheme, address, identities = [], extensions = {} } = options;\n\n log.info({ address, identityCount: identities.length }, 'Creating attestation');\n\n const envelope = await createAttestationEnvelope(\n message,\n signature,\n scheme,\n address,\n identities,\n extensions\n );\n\n // Verify envelope integrity\n const isValid = await verifyAttestationEnvelope(envelope);\n if (!isValid) {\n throw new AttestationError(\n 'Attestation envelope integrity check failed',\n 'INTEGRITY_CHECK_FAILED'\n );\n }\n\n log.info({ attestationId: envelope.attestation_id }, 'Attestation created successfully');\n\n return envelope;\n}\n\n/**\n * Publish an attestation to Nostr relays\n *\n * Uses NIP-07 if available, falls back to ephemeral key for demo\n *\n * @param options - Publishing options\n * @returns Object with successful and failed relay URLs\n * @throws {AttestationError} If signing fails\n *\n * @example\n * ```typescript\n * const result = await publishAttestation({\n * envelope,\n * npub: 'npub1...',\n * relays: ['wss://relay.damus.io'],\n * });\n * console.log('Published to:', result.success);\n * ```\n */\nexport async function publishAttestation(\n options: PublishAttestationOptions\n): Promise<PublishResult> {\n const { envelope, npub, relays = DEFAULT_RELAYS } = options;\n\n log.info(\n { attestationId: envelope.attestation_id, relayCount: relays.length },\n 'Publishing attestation'\n );\n\n // Create unsigned event\n const unsignedEvent = createAttestationEvent(envelope, npub);\n\n // Nostr publishing requires a valid schnorr signature. Only NIP-07\n // (window.nostr) is supported. If no signer is present we refuse to\n // publish rather than emit an invalid-sig event that every relay will\n // silently reject — which would make the caller think it had succeeded.\n if (!isNip07Available()) {\n log.warn('NIP-07 unavailable — skipping Nostr publish');\n throw new Error(\n 'nostr_signer_unavailable: install a NIP-07 extension (e.g. nos2x, Alby) to publish'\n );\n }\n\n log.info('Using NIP-07 to sign event');\n const signedEvent = await signNostrEvent(unsignedEvent);\n if (!signedEvent) {\n throw new Error('nostr_signing_failed: NIP-07 extension rejected or errored');\n }\n\n const result = await publishToRelays(signedEvent, relays);\n\n log.info(\n {\n attestationId: envelope.attestation_id,\n successCount: result.success.length,\n failedCount: result.failed.length,\n },\n 'Publishing complete'\n );\n\n return result;\n}\n\n/**\n * An event's index tags are unsigned, so a relay hit proves nothing until they\n * agree with the envelope. Requiring the `t` address tag also keeps every\n * attestation findable by address, which check()'s shared-stake audit relies on.\n */\nfunction eventIndexesEnvelope(event: NostrEvent, envelope: AttestationEnvelope): boolean {\n const d = event.tags.find((t) => t[0] === 'd')?.[1];\n const id = envelope.attestation_id;\n if (d !== id && d !== `orangecheck:${id}`) return false;\n return event.tags.some((t) => t[0] === 't' && t[1] === envelope.address);\n}\n\n/**\n * Discover attestations from Nostr relays\n */\nexport async function discoverAttestations(\n options: DiscoverAttestationOptions\n): Promise<AttestationEnvelope[]> {\n const { attestationId, address, identity, relays = DEFAULT_RELAYS } = options;\n\n let events: NostrEvent[] = [];\n\n if (attestationId) {\n log.info({ attestationId }, 'Discovering by attestation ID');\n events = await queryByAttestationId(attestationId, relays);\n } else if (address) {\n log.info({ address }, 'Discovering by Bitcoin address');\n events = await queryByAddress(address, relays);\n } else if (identity) {\n log.info({ identity }, 'Discovering by identity');\n events = await queryByIdentity(identity.protocol, identity.identifier, relays);\n } else {\n throw new Error('Must provide attestationId, address, or identity');\n }\n\n // Every relay holding an attestation returns it. One envelope per id, its\n // fields read from the signed message, and only where the relay's index tags\n // agree with it.\n const envelopes = await collectSignedEnvelopes(events, (event, envelope) => {\n if (eventIndexesEnvelope(event, envelope)) return true;\n log.warn({ eventId: event.id }, 'Skipping event whose index tags disagree with its envelope');\n return false;\n });\n\n log.info({ eventCount: events.length, validCount: envelopes.length }, 'Discovery complete');\n\n return envelopes;\n}\n\n/**\n * Verify an attestation by fetching it from Nostr and checking integrity\n */\nexport async function verifyAttestationById(\n attestationId: string,\n relays: string[] = DEFAULT_RELAYS\n): Promise<AttestationEnvelope | null> {\n log.info({ attestationId }, 'Verifying attestation by ID');\n\n const envelopes = await discoverAttestations({ attestationId, relays });\n\n if (envelopes.length === 0) {\n log.warn({ attestationId }, 'Attestation not found on any relay');\n return null;\n }\n\n if (envelopes.length > 1) {\n log.warn(\n { attestationId, count: envelopes.length },\n 'Multiple attestations found with same ID - using first'\n );\n }\n\n return envelopes[0] || null;\n}\n\n/**\n * Get all attestations for a Bitcoin address\n */\nexport async function getAttestationsForAddress(\n address: string,\n relays: string[] = DEFAULT_RELAYS\n): Promise<AttestationEnvelope[]> {\n log.info({ address }, 'Getting all attestations for address');\n return discoverAttestations({ address, relays });\n}\n\n/**\n * Get all attestations for an identity\n */\nexport async function getAttestationsForIdentity(\n protocol: string,\n identifier: string,\n relays: string[] = DEFAULT_RELAYS\n): Promise<AttestationEnvelope[]> {\n log.info({ protocol, identifier }, 'Getting all attestations for identity');\n return discoverAttestations({ identity: { protocol, identifier }, relays });\n}\n\n/**\n * Format identities for display as a comma-separated `protocol:identifier` list.\n * Same wire format the canonical message uses.\n */\nexport function formatIdentitiesForDisplay(identities: IdentityBinding[]): string {\n return identities.map((id) => `${id.protocol}:${id.identifier}`).join(', ');\n}\n\n/**\n * Build the canonical shareable URL for an attestation. Matches the live\n * `/attest/[id]` route on attest.ochk.io. Pass a different `baseUrl` for forks /\n * self-hosted verifiers.\n */\nexport function getVerificationUrl(\n attestationId: string,\n baseUrl = 'https://attest.ochk.io'\n): string {\n return `${baseUrl}/attest/${attestationId}`;\n}\n\n/**\n * Reverse of `getVerificationUrl` — pull an attestation id back out of a\n * `/attest/<id>` or legacy `/verify?id=<id>` URL. Returns `null` if the URL\n * shape doesn't match.\n */\nexport function extractAttestationIdFromUrl(url: string): string | null {\n try {\n const u = new URL(url);\n // New canonical form: /attest/<id>\n const m = u.pathname.match(/^\\/attest\\/([0-9a-f]{64})\\/?$/i);\n if (m) return m[1]!.toLowerCase();\n // Legacy form: /verify?id=<id>\n const q = u.searchParams.get('id');\n if (q && /^[0-9a-f]{64}$/i.test(q)) return q.toLowerCase();\n return null;\n } catch {\n return null;\n }\n}\n"]}
|
package/dist/canonical.d.mts
CHANGED
package/dist/canonical.d.ts
CHANGED
package/dist/challenge.d.mts
CHANGED
package/dist/challenge.d.ts
CHANGED
package/dist/identity.d.mts
CHANGED
package/dist/identity.d.ts
CHANGED