@orangecheck/sdk 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/index.d.mts +45 -1
- package/dist/index.d.ts +45 -1
- package/dist/index.js +191 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +189 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -1155,6 +1155,189 @@ async function check(params) {
|
|
|
1155
1155
|
...reasons.length ? { reasons } : {}
|
|
1156
1156
|
};
|
|
1157
1157
|
}
|
|
1158
|
+
var HEADER = "orangecheck-binding";
|
|
1159
|
+
var ACK = "I attest the keys named in this message are one principal.";
|
|
1160
|
+
var BINDING_KIND = 30079;
|
|
1161
|
+
function toHex(bytes) {
|
|
1162
|
+
let out = "";
|
|
1163
|
+
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
1164
|
+
return out;
|
|
1165
|
+
}
|
|
1166
|
+
function fromHex(hex) {
|
|
1167
|
+
const clean = hex.toLowerCase();
|
|
1168
|
+
if (clean.length % 2 !== 0 || /[^0-9a-f]/.test(clean)) {
|
|
1169
|
+
throw new Error("invalid hex");
|
|
1170
|
+
}
|
|
1171
|
+
const out = new Uint8Array(clean.length / 2);
|
|
1172
|
+
for (let i = 0; i < out.length; i++) out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
1173
|
+
return out;
|
|
1174
|
+
}
|
|
1175
|
+
function assertPrintableAscii(field, value) {
|
|
1176
|
+
for (let i = 0; i < value.length; i++) {
|
|
1177
|
+
const c = value.charCodeAt(i);
|
|
1178
|
+
if (c < 32 || c > 126) {
|
|
1179
|
+
throw new Error(
|
|
1180
|
+
`binding ${field} contains an unsafe character (0x${c.toString(16)}) \u2014 line-smuggling defense (SPEC-BINDING \xA73.5)`
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
function buildBindingMessage(input) {
|
|
1186
|
+
assertPrintableAscii("principal", input.principal);
|
|
1187
|
+
assertPrintableAscii("btc", input.btc);
|
|
1188
|
+
assertPrintableAscii("nostr", input.nostr);
|
|
1189
|
+
assertPrintableAscii("nonce", input.nonce);
|
|
1190
|
+
assertPrintableAscii("issued_at", input.issued_at);
|
|
1191
|
+
const core = [
|
|
1192
|
+
HEADER,
|
|
1193
|
+
"v: 1",
|
|
1194
|
+
`principal: ${input.principal}`,
|
|
1195
|
+
`btc: ${input.btc}`,
|
|
1196
|
+
`nostr: ${input.nostr}`,
|
|
1197
|
+
`nonce: ${input.nonce}`,
|
|
1198
|
+
`issued_at: ${input.issued_at}`,
|
|
1199
|
+
`ack: ${ACK}`
|
|
1200
|
+
];
|
|
1201
|
+
const ext = [];
|
|
1202
|
+
if (input.extensions) {
|
|
1203
|
+
for (const key of Object.keys(input.extensions).sort()) {
|
|
1204
|
+
const value = input.extensions[key];
|
|
1205
|
+
if (value == null) continue;
|
|
1206
|
+
if (!/^[a-z]+$/.test(key)) {
|
|
1207
|
+
throw new Error(`binding extension key "${key}" must be lowercase ASCII`);
|
|
1208
|
+
}
|
|
1209
|
+
assertPrintableAscii(`extension ${key}`, value);
|
|
1210
|
+
ext.push(`${key}: ${value}`);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
return [...core, ...ext].join("\n") + "\n";
|
|
1214
|
+
}
|
|
1215
|
+
function bindingId(message) {
|
|
1216
|
+
return toHex(sha256(new TextEncoder().encode(message)));
|
|
1217
|
+
}
|
|
1218
|
+
function linePrefix(line, prefix) {
|
|
1219
|
+
if (typeof line !== "string" || !line.startsWith(prefix)) return null;
|
|
1220
|
+
return line.slice(prefix.length);
|
|
1221
|
+
}
|
|
1222
|
+
function nostrEventId(ev) {
|
|
1223
|
+
const serialized = JSON.stringify([
|
|
1224
|
+
0,
|
|
1225
|
+
ev.pubkey,
|
|
1226
|
+
ev.created_at,
|
|
1227
|
+
ev.kind,
|
|
1228
|
+
ev.tags,
|
|
1229
|
+
ev.content
|
|
1230
|
+
]);
|
|
1231
|
+
return toHex(sha256(new TextEncoder().encode(serialized)));
|
|
1232
|
+
}
|
|
1233
|
+
function npubToXOnlyHex(npub) {
|
|
1234
|
+
const decoded = bech32.decode(npub, 90);
|
|
1235
|
+
if (decoded.prefix !== "npub") throw new Error("not an npub");
|
|
1236
|
+
const bytes = Uint8Array.from(bech32.fromWords(decoded.words));
|
|
1237
|
+
if (bytes.length !== 32) throw new Error("npub payload is not 32 bytes");
|
|
1238
|
+
return toHex(bytes);
|
|
1239
|
+
}
|
|
1240
|
+
function verifyBinding(envelope) {
|
|
1241
|
+
const fail = (status) => ({
|
|
1242
|
+
valid: false,
|
|
1243
|
+
status
|
|
1244
|
+
});
|
|
1245
|
+
try {
|
|
1246
|
+
const msg = envelope.message;
|
|
1247
|
+
if (typeof msg !== "string" || msg.length === 0) return fail("malformed");
|
|
1248
|
+
const lines = msg.split("\n");
|
|
1249
|
+
if ((lines[0] ?? "") !== HEADER) return fail("bad_header");
|
|
1250
|
+
if (lines.length < 9 || lines[lines.length - 1] !== "") return fail("malformed");
|
|
1251
|
+
const body = lines.slice(0, -1);
|
|
1252
|
+
if (body.length < 8) return fail("malformed");
|
|
1253
|
+
if (body[1] !== "v: 1") return fail("malformed");
|
|
1254
|
+
if (body[7] !== `ack: ${ACK}`) return fail("bad_ack");
|
|
1255
|
+
const principal = linePrefix(body[2], "principal: ");
|
|
1256
|
+
const btc = linePrefix(body[3], "btc: ");
|
|
1257
|
+
const nostr = linePrefix(body[4], "nostr: ");
|
|
1258
|
+
const nonce = linePrefix(body[5], "nonce: ");
|
|
1259
|
+
const issuedAt = linePrefix(body[6], "issued_at: ");
|
|
1260
|
+
if (principal == null || btc == null || nostr == null || nonce == null || issuedAt == null) {
|
|
1261
|
+
return fail("malformed");
|
|
1262
|
+
}
|
|
1263
|
+
for (const value of [principal, btc, nostr, nonce, issuedAt]) {
|
|
1264
|
+
for (let i = 0; i < value.length; i++) {
|
|
1265
|
+
const c = value.charCodeAt(i);
|
|
1266
|
+
if (c < 32 || c > 126) return fail("malformed");
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
if (!/^did:oc:[0-9a-f]{32}$/.test(principal)) return fail("bad_principal");
|
|
1270
|
+
if (!/^[0-9a-f]{32}$/.test(nonce)) return fail("malformed");
|
|
1271
|
+
const ext = {};
|
|
1272
|
+
let prevKey = "";
|
|
1273
|
+
for (const line of body.slice(8)) {
|
|
1274
|
+
const idx = line.indexOf(": ");
|
|
1275
|
+
if (idx <= 0) return fail("malformed");
|
|
1276
|
+
const key = line.slice(0, idx);
|
|
1277
|
+
const value = line.slice(idx + 2);
|
|
1278
|
+
if (!/^[a-z]+$/.test(key)) return fail("malformed");
|
|
1279
|
+
if (key <= prevKey) return fail("ext_unsorted");
|
|
1280
|
+
prevKey = key;
|
|
1281
|
+
ext[key] = value;
|
|
1282
|
+
}
|
|
1283
|
+
const bid = bindingId(msg);
|
|
1284
|
+
if (envelope.binding_id && envelope.binding_id.toLowerCase() !== bid) {
|
|
1285
|
+
return fail("id_mismatch");
|
|
1286
|
+
}
|
|
1287
|
+
if (envelope.btc && envelope.btc !== btc) return fail("field_mismatch");
|
|
1288
|
+
if (envelope.nostr && envelope.nostr !== nostr) return fail("field_mismatch");
|
|
1289
|
+
if (envelope.principal && envelope.principal !== principal) return fail("field_mismatch");
|
|
1290
|
+
if (ext.expires) {
|
|
1291
|
+
const t = Date.parse(ext.expires);
|
|
1292
|
+
if (!Number.isNaN(t) && Date.now() > t) return fail("expired");
|
|
1293
|
+
}
|
|
1294
|
+
if (typeof envelope.btc_signature !== "string" || !envelope.btc_signature) {
|
|
1295
|
+
return fail("btc_sig_invalid");
|
|
1296
|
+
}
|
|
1297
|
+
const scheme = envelope.btc_scheme ?? "bip322";
|
|
1298
|
+
let btcOk = false;
|
|
1299
|
+
try {
|
|
1300
|
+
if (scheme === "bip322") {
|
|
1301
|
+
btcOk = Verifier.verifySignature(btc, msg, envelope.btc_signature);
|
|
1302
|
+
} else if (scheme === "legacy") {
|
|
1303
|
+
btcOk = bitcoinMessage.verify(msg, btc, envelope.btc_signature);
|
|
1304
|
+
} else {
|
|
1305
|
+
return fail("bad_scheme");
|
|
1306
|
+
}
|
|
1307
|
+
} catch {
|
|
1308
|
+
btcOk = false;
|
|
1309
|
+
}
|
|
1310
|
+
if (!btcOk) return fail("btc_sig_invalid");
|
|
1311
|
+
const ev = envelope.nostr_event;
|
|
1312
|
+
if (!ev || ev.kind !== BINDING_KIND || typeof ev.id !== "string") return fail("malformed");
|
|
1313
|
+
if (nostrEventId(ev) !== ev.id.toLowerCase()) return fail("nostr_id_invalid");
|
|
1314
|
+
let nostrSigOk = false;
|
|
1315
|
+
try {
|
|
1316
|
+
nostrSigOk = schnorr.verify(fromHex(ev.sig), fromHex(ev.id), fromHex(ev.pubkey));
|
|
1317
|
+
} catch {
|
|
1318
|
+
nostrSigOk = false;
|
|
1319
|
+
}
|
|
1320
|
+
if (!nostrSigOk) return fail("nostr_sig_invalid");
|
|
1321
|
+
let nostrXOnly;
|
|
1322
|
+
try {
|
|
1323
|
+
nostrXOnly = npubToXOnlyHex(nostr);
|
|
1324
|
+
} catch {
|
|
1325
|
+
return fail("nostr_key_mismatch");
|
|
1326
|
+
}
|
|
1327
|
+
if (nostrXOnly !== ev.pubkey.toLowerCase()) return fail("nostr_key_mismatch");
|
|
1328
|
+
let content;
|
|
1329
|
+
try {
|
|
1330
|
+
content = JSON.parse(ev.content);
|
|
1331
|
+
} catch {
|
|
1332
|
+
return fail("message_mismatch");
|
|
1333
|
+
}
|
|
1334
|
+
if (content.message !== msg) return fail("message_mismatch");
|
|
1335
|
+
if (content.btc_signature !== envelope.btc_signature) return fail("message_mismatch");
|
|
1336
|
+
return { valid: true, status: "binding_ok", binding_id: bid, principal, btc, nostr };
|
|
1337
|
+
} catch {
|
|
1338
|
+
return fail("malformed");
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1158
1341
|
var AUTH_HEADER = "orangecheck-auth";
|
|
1159
1342
|
var AUTH_ACK = "I authorize this session under the OrangeCheck auth challenge.";
|
|
1160
1343
|
function randomNonce() {
|
|
@@ -1610,12 +1793,12 @@ function getGistCreationUrl() {
|
|
|
1610
1793
|
}
|
|
1611
1794
|
var log7 = createLogger("identity-verification/nostr");
|
|
1612
1795
|
var HEX_64_RE = /^[0-9a-f]{64}$/;
|
|
1613
|
-
function
|
|
1796
|
+
function toHex2(bytes) {
|
|
1614
1797
|
let out = "";
|
|
1615
1798
|
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
1616
1799
|
return out;
|
|
1617
1800
|
}
|
|
1618
|
-
function
|
|
1801
|
+
function fromHex2(hex) {
|
|
1619
1802
|
if (hex.length % 2 !== 0) throw new Error("odd-length hex");
|
|
1620
1803
|
const out = new Uint8Array(hex.length / 2);
|
|
1621
1804
|
for (let i = 0; i < out.length; i++) {
|
|
@@ -1639,7 +1822,7 @@ function npubToHex(npub) {
|
|
|
1639
1822
|
if (bytes.length !== 32) {
|
|
1640
1823
|
throw new Error(`invalid npub payload length: ${bytes.length}`);
|
|
1641
1824
|
}
|
|
1642
|
-
return
|
|
1825
|
+
return toHex2(Uint8Array.from(bytes));
|
|
1643
1826
|
}
|
|
1644
1827
|
async function queryNostrEvents(options) {
|
|
1645
1828
|
const { authors, kinds, search, relays = DEFAULT_RELAYS, limit = 100 } = options;
|
|
@@ -1722,7 +1905,7 @@ async function verifyNostrEventSignature(event) {
|
|
|
1722
1905
|
event.tags ?? [],
|
|
1723
1906
|
event.content ?? ""
|
|
1724
1907
|
]);
|
|
1725
|
-
const computedId =
|
|
1908
|
+
const computedId = toHex2(sha256(new TextEncoder().encode(preimage)));
|
|
1726
1909
|
if (computedId !== event.id.toLowerCase()) {
|
|
1727
1910
|
log7.warn(
|
|
1728
1911
|
{ eventId: event.id, computedId },
|
|
@@ -1730,7 +1913,7 @@ async function verifyNostrEventSignature(event) {
|
|
|
1730
1913
|
);
|
|
1731
1914
|
return false;
|
|
1732
1915
|
}
|
|
1733
|
-
return schnorr.verify(
|
|
1916
|
+
return schnorr.verify(fromHex2(event.sig), fromHex2(event.id), fromHex2(event.pubkey));
|
|
1734
1917
|
} catch (err) {
|
|
1735
1918
|
log7.warn({ err }, "schnorr verify threw");
|
|
1736
1919
|
return false;
|
|
@@ -2090,6 +2273,6 @@ var STATUS_META = {
|
|
|
2090
2273
|
}
|
|
2091
2274
|
};
|
|
2092
2275
|
|
|
2093
|
-
export { DEFAULT_RELAYS, STATUS_META, buildCanonicalMessage, check, computeAllScores, computeScore, createAttestation, createAttestationEnvelope, createAttestationEvent, discoverAttestations, extractAttestationIdFromUrl, formatIdentities, formatIdentitiesForDisplay, generateAttestationId, generateGistTemplate, generateTweetText, generateWellKnownContent, getAttestationsForAddress, getAttestationsForIdentity, getDnsVerificationInstructions, getGistCreationUrl, getGitHubVerificationInstructions, getManualVerificationInstructions, getNip07Info, getNostrPublicKey, getNostrVerificationInstructions, getTweetUrl, getTwitterComposeUrl, getTwitterVerificationInstructions, getVerificationInstructions, getVerificationSummary, getVerificationUrl, getWellKnownUrl, isDnsIdentityVerified, isGitHubIdentityVerified, isNip07Available, isNostrIdentityVerified, isValidDomain, isValidTweetUrl, issueChallenge, parseAttestationFromEvent, parseIdentities, publishAttestation, publishToRelays, queryByAddress, queryByAttestationId, queryByIdentity, requiresManualVerification, signNostrEvent, supportsAutomaticVerification, verify2 as verify, verifyAllIdentities, verifyAttestationById, verifyChallenge, verifyDnsIdentity, verifyGitHubIdentity, verifyIdentity, verifyNostrIdentity, verifyTwitterIdentity };
|
|
2276
|
+
export { DEFAULT_RELAYS, STATUS_META, bindingId, buildBindingMessage, buildCanonicalMessage, check, computeAllScores, computeScore, createAttestation, createAttestationEnvelope, createAttestationEvent, discoverAttestations, extractAttestationIdFromUrl, formatIdentities, formatIdentitiesForDisplay, generateAttestationId, generateGistTemplate, generateTweetText, generateWellKnownContent, getAttestationsForAddress, getAttestationsForIdentity, getDnsVerificationInstructions, getGistCreationUrl, getGitHubVerificationInstructions, getManualVerificationInstructions, getNip07Info, getNostrPublicKey, getNostrVerificationInstructions, getTweetUrl, getTwitterComposeUrl, getTwitterVerificationInstructions, getVerificationInstructions, getVerificationSummary, getVerificationUrl, getWellKnownUrl, isDnsIdentityVerified, isGitHubIdentityVerified, isNip07Available, isNostrIdentityVerified, isValidDomain, isValidTweetUrl, issueChallenge, parseAttestationFromEvent, parseIdentities, publishAttestation, publishToRelays, queryByAddress, queryByAttestationId, queryByIdentity, requiresManualVerification, signNostrEvent, supportsAutomaticVerification, verify2 as verify, verifyAllIdentities, verifyAttestationById, verifyBinding, verifyChallenge, verifyDnsIdentity, verifyGitHubIdentity, verifyIdentity, verifyNostrIdentity, verifyTwitterIdentity };
|
|
2094
2277
|
//# sourceMappingURL=index.mjs.map
|
|
2095
2278
|
//# sourceMappingURL=index.mjs.map
|