@oneaddress/setup 1.3.4 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +164 -62
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -856,7 +856,7 @@ var _R = ["\u2588\u2588\u2588\u2588\u2588\u2588 ", "\u2588\u2588 \u2588\u2588"
|
|
|
856
856
|
var _S = [" \u2588\u2588\u2588\u2588\u2588\u2588", "\u2588\u2588 ", "\u2588\u2588 ", " \u2588\u2588\u2588\u2588\u2588 ", " \u2588\u2588", " \u2588\u2588", "\u2588\u2588\u2588\u2588\u2588\u2588 "];
|
|
857
857
|
var ONE_ROWS = Array.from({ length: 7 }, (_3, i) => [_O[i], _N[i], _E[i]].join(" "));
|
|
858
858
|
var ADDR_ROWS = Array.from({ length: 7 }, (_3, i) => [_A[i], _D2[i], _D2[i], _R[i], _E[i], _S[i], _S[i]].join(" "));
|
|
859
|
-
var WIZARD_VERSION = true ? "1.
|
|
859
|
+
var WIZARD_VERSION = true ? "1.4.1" : "?";
|
|
860
860
|
function printCompactHeader() {
|
|
861
861
|
const INNER = 42;
|
|
862
862
|
const TOP = fn("\u250C") + dm("\u2500".repeat(INNER)) + fn("\u2510");
|
|
@@ -1204,6 +1204,7 @@ if (nodeMajor < 22 || (nodeMajor === 22 && nodeMinor < 5)) {
|
|
|
1204
1204
|
|
|
1205
1205
|
import 'dotenv/config';
|
|
1206
1206
|
import express, { Request, Response } from 'express';
|
|
1207
|
+
import { createPrivateKey } from 'node:crypto';
|
|
1207
1208
|
import {
|
|
1208
1209
|
verifySignature,
|
|
1209
1210
|
decryptAddress,
|
|
@@ -1229,10 +1230,39 @@ if (!WEBHOOK_SECRET || !PARTNER_PRIVATE_KEY || !PARTNER_ID) {
|
|
|
1229
1230
|
process.exit(1);
|
|
1230
1231
|
}
|
|
1231
1232
|
|
|
1232
|
-
//
|
|
1233
|
-
//
|
|
1234
|
-
//
|
|
1235
|
-
//
|
|
1233
|
+
// Startup self-check. Two faults were historically INVISIBLE until a live
|
|
1234
|
+
// dispatch, and both read afterwards as "partner key mismatch" deep inside a
|
|
1235
|
+
// try/catch, which is a day of reading webhook logs. Assert them here so the
|
|
1236
|
+
// receiver refuses to start and names the fix instead.
|
|
1237
|
+
// 1. An @oneaddress/partner-sdk too old to export decryptSession. A missing
|
|
1238
|
+
// CJS export is undefined, not a load error, so tsx never complains and it
|
|
1239
|
+
// fails only when the first payload arrives.
|
|
1240
|
+
// 2. A private key that does not parse (empty, truncated, or header stripped).
|
|
1241
|
+
if (typeof decryptSession !== 'function') {
|
|
1242
|
+
console.error('[startup] @oneaddress/partner-sdk does not export decryptSession. Your installed SDK is too old for D5 dispatches.');
|
|
1243
|
+
console.error('[startup] Fix: npm install "@oneaddress/partner-sdk@^1.8.0", then restart.');
|
|
1244
|
+
process.exit(1);
|
|
1245
|
+
}
|
|
1246
|
+
// Only PEM keys are parseable this way; a post-quantum key is a base64 secret,
|
|
1247
|
+
// so skip the check unless the value looks like a PEM.
|
|
1248
|
+
if (PARTNER_PRIVATE_KEY.includes('BEGIN')) {
|
|
1249
|
+
try {
|
|
1250
|
+
createPrivateKey(PARTNER_PRIVATE_KEY);
|
|
1251
|
+
} catch {
|
|
1252
|
+
console.error('[startup] PARTNER_PRIVATE_KEY_PEM does not parse as a private key.');
|
|
1253
|
+
console.error('[startup] Paste the FULL PEM, including the -----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY----- lines.');
|
|
1254
|
+
process.exit(1);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// In-memory dedup cache. Records a dispatch id only once it has been fully
|
|
1259
|
+
// HANDLED, so a dispatch that failed to decrypt is NOT remembered and a later
|
|
1260
|
+
// retry re-runs it rather than being dismissed as a duplicate. Bounded so a
|
|
1261
|
+
// partner running with the default scaffold can't be DoS'd by an attacker
|
|
1262
|
+
// who's already past HMAC verify (i.e. has stolen the webhook secret) by
|
|
1263
|
+
// sending unique dispatch IDs until RAM is exhausted. For production use a
|
|
1264
|
+
// distributed store (Redis), and prefer the completion-claim model the other
|
|
1265
|
+
// language scaffolds use.
|
|
1236
1266
|
const SEEN_DISPATCHES_MAX = 10_000;
|
|
1237
1267
|
const seenDispatches = new Set<string>();
|
|
1238
1268
|
function rememberDispatch(id: string): void {
|
|
@@ -1296,7 +1326,12 @@ app.post('/webhook', async (req: Request, res: Response) => {
|
|
|
1296
1326
|
if (dispatch && seenDispatches.has(dispatch)) {
|
|
1297
1327
|
return res.status(200).json({ ok: true, duplicate: true });
|
|
1298
1328
|
}
|
|
1299
|
-
|
|
1329
|
+
// A dispatch is remembered ONLY after it has been fully handled (see the
|
|
1330
|
+
// success returns below), never here. Remembering before the work would mark
|
|
1331
|
+
// a dispatch that then FAILS to decrypt (422 below) as "seen", so OneAddress's
|
|
1332
|
+
// retry after you fix the key would be dismissed as a duplicate and the update
|
|
1333
|
+
// lost. The connection-verification probe (address.test) is never remembered
|
|
1334
|
+
// at all, so re-running go-live always re-tests instead of short-circuiting.
|
|
1300
1335
|
|
|
1301
1336
|
const event = body.event as string;
|
|
1302
1337
|
|
|
@@ -1387,6 +1422,7 @@ app.post('/webhook', async (req: Request, res: Response) => {
|
|
|
1387
1422
|
if (event === 'address.updated') {
|
|
1388
1423
|
console.log(\`[webhook] address.updated for \${ctx.accountNumber || ctx.name}\`);
|
|
1389
1424
|
await saveAddress(ctx, address);
|
|
1425
|
+
if (dispatch) rememberDispatch(dispatch); // remember only after it is stored
|
|
1390
1426
|
return res.status(200).json({ ok: true });
|
|
1391
1427
|
}
|
|
1392
1428
|
|
|
@@ -1423,6 +1459,7 @@ app.post('/webhook', async (req: Request, res: Response) => {
|
|
|
1423
1459
|
token: callbackToken,
|
|
1424
1460
|
}),
|
|
1425
1461
|
});
|
|
1462
|
+
if (dispatch) rememberDispatch(dispatch); // remember only after the callback posted
|
|
1426
1463
|
return res.status(200).json({ ok: true });
|
|
1427
1464
|
}
|
|
1428
1465
|
|
|
@@ -5372,7 +5409,7 @@ async function scaffold(platform, outputDir, partnerId, webhookSecret, webhookUr
|
|
|
5372
5409
|
|
|
5373
5410
|
// src/register.ts
|
|
5374
5411
|
var import_node_crypto = require("crypto");
|
|
5375
|
-
var PKG_VERSION = true ? "1.
|
|
5412
|
+
var PKG_VERSION = true ? "1.4.1" : "dev";
|
|
5376
5413
|
var REGISTER_URL = "https://partners.oneaddress.io/api/partner/installs";
|
|
5377
5414
|
function hmacSha256(secret, message) {
|
|
5378
5415
|
return (0, import_node_crypto.createHmac)("sha256", secret).update(message).digest("hex");
|
|
@@ -5499,6 +5536,67 @@ async function runConformance(webhookUrl, _partnerId, webhookSecret) {
|
|
|
5499
5536
|
M2.warn(`Conformance: ${passed}/${tests.length} passed. Check your WEBHOOK_SECRET and timestamp enforcement.`);
|
|
5500
5537
|
}
|
|
5501
5538
|
}
|
|
5539
|
+
var VERIFY_DECRYPT_API = "https://partners.oneaddress.io/api/partner/verify-decrypt";
|
|
5540
|
+
async function runDecryptCheck(webhookSecret, partnerId) {
|
|
5541
|
+
async function callOnce() {
|
|
5542
|
+
try {
|
|
5543
|
+
const ts = String(Math.floor(Date.now() / 1e3));
|
|
5544
|
+
const sig = (0, import_node_crypto2.createHmac)("sha256", webhookSecret).update(`${ts}.`).digest("hex");
|
|
5545
|
+
const res2 = await fetch(VERIFY_DECRYPT_API, {
|
|
5546
|
+
method: "POST",
|
|
5547
|
+
headers: {
|
|
5548
|
+
"Content-Type": "application/json",
|
|
5549
|
+
"X-OneAddress-Timestamp": ts,
|
|
5550
|
+
"X-OneAddress-Signature": sig,
|
|
5551
|
+
"X-OneAddress-Partner": partnerId
|
|
5552
|
+
},
|
|
5553
|
+
// The portal-to-receiver probe can take a few seconds; give it room.
|
|
5554
|
+
signal: AbortSignal.timeout(2e4)
|
|
5555
|
+
});
|
|
5556
|
+
return { res: res2 };
|
|
5557
|
+
} catch (err) {
|
|
5558
|
+
return { netErr: err instanceof Error ? err.message : String(err) };
|
|
5559
|
+
}
|
|
5560
|
+
}
|
|
5561
|
+
let { res, netErr } = await callOnce();
|
|
5562
|
+
if (!res) {
|
|
5563
|
+
await new Promise((r2) => setTimeout(r2, 1500));
|
|
5564
|
+
({ res, netErr } = await callOnce());
|
|
5565
|
+
}
|
|
5566
|
+
if (!res) {
|
|
5567
|
+
M2.warn(`Decrypt check skipped: could not reach the portal (${netErr}). Re-run once you are back online.`);
|
|
5568
|
+
return;
|
|
5569
|
+
}
|
|
5570
|
+
if (res.status === 404 || res.status === 405) {
|
|
5571
|
+
M2.info("Decrypt check skipped (the portal does not offer it yet). Your go-live switch will run the same check.");
|
|
5572
|
+
return;
|
|
5573
|
+
}
|
|
5574
|
+
if (res.status === 401 || res.status === 403) {
|
|
5575
|
+
M2.warn("Decrypt check skipped: the portal rejected your signing secret. Copy it again from your profile.");
|
|
5576
|
+
return;
|
|
5577
|
+
}
|
|
5578
|
+
if (!res.ok) {
|
|
5579
|
+
M2.warn(`Decrypt check skipped: the portal returned HTTP ${res.status}.`);
|
|
5580
|
+
return;
|
|
5581
|
+
}
|
|
5582
|
+
let data;
|
|
5583
|
+
try {
|
|
5584
|
+
data = await res.json();
|
|
5585
|
+
} catch {
|
|
5586
|
+
M2.warn("Decrypt check skipped: the portal returned an unexpected response.");
|
|
5587
|
+
return;
|
|
5588
|
+
}
|
|
5589
|
+
if (data.decrypted === true) {
|
|
5590
|
+
M2.success("Decrypt check passed \u2014 your receiver decrypted our encrypted test, so your key is correctly installed. Go-live will not surprise you.");
|
|
5591
|
+
return;
|
|
5592
|
+
}
|
|
5593
|
+
if (data.code) {
|
|
5594
|
+
M2.info(`Decrypt check not run yet: ${data.reason ?? "a setup step is still outstanding."}`);
|
|
5595
|
+
return;
|
|
5596
|
+
}
|
|
5597
|
+
const reason = data.reason ?? "Your receiver answered but could not decrypt our encrypted test, which almost always means the private key in your receiver does not match your registered encryption key.";
|
|
5598
|
+
M2.warn(`Decrypt check FAILED: ${reason}`);
|
|
5599
|
+
}
|
|
5502
5600
|
|
|
5503
5601
|
// src/install.ts
|
|
5504
5602
|
var import_node_child_process = require("child_process");
|
|
@@ -6375,6 +6473,7 @@ async function main() {
|
|
|
6375
6473
|
let webhookReady = true;
|
|
6376
6474
|
let tunnelBase = null;
|
|
6377
6475
|
let existingUrlReason = "";
|
|
6476
|
+
let keepExistingUrl = false;
|
|
6378
6477
|
const existingUrlResult = await getExistingWebhookUrl(pid, secret, (r2) => {
|
|
6379
6478
|
existingUrlReason = r2;
|
|
6380
6479
|
});
|
|
@@ -6390,9 +6489,9 @@ Continuing will overwrite it.`
|
|
|
6390
6489
|
});
|
|
6391
6490
|
assertNotCancelled(continueAnyway);
|
|
6392
6491
|
if (!continueAnyway) {
|
|
6393
|
-
|
|
6394
|
-
|
|
6395
|
-
|
|
6492
|
+
keepExistingUrl = true;
|
|
6493
|
+
webhookUrl = existingUrlResult;
|
|
6494
|
+
M2.info(`Keeping your existing webhook URL: ${webhookUrl}`);
|
|
6396
6495
|
}
|
|
6397
6496
|
} else if (existingUrlResult === void 0) {
|
|
6398
6497
|
M2.warn(
|
|
@@ -6409,70 +6508,72 @@ Continuing will overwrite it.`
|
|
|
6409
6508
|
process.exit(0);
|
|
6410
6509
|
}
|
|
6411
6510
|
}
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
|
|
6416
|
-
assertNotCancelled(hasPublicUrl);
|
|
6417
|
-
if (hasPublicUrl) {
|
|
6418
|
-
const providedUrl = await he({
|
|
6419
|
-
message: "Your public HTTPS webhook URL",
|
|
6420
|
-
placeholder: "https://my-api.example.com/webhook",
|
|
6421
|
-
validate: (v2) => {
|
|
6422
|
-
if (!v2.trim()) return "URL is required";
|
|
6423
|
-
if (!v2.trim().startsWith("https://"))
|
|
6424
|
-
return "Must start with https:// \u2014 OneAddress enforces HTTPS for outbound webhooks";
|
|
6425
|
-
}
|
|
6511
|
+
if (!keepExistingUrl) {
|
|
6512
|
+
const hasPublicUrl = await ye({
|
|
6513
|
+
message: "Do you have a public HTTPS URL where this server is reachable?",
|
|
6514
|
+
initialValue: false
|
|
6426
6515
|
});
|
|
6427
|
-
assertNotCancelled(
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
|
|
6441
|
-
);
|
|
6442
|
-
const
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
stopTunnel();
|
|
6448
|
-
tunnel = await startTunnel(3001);
|
|
6516
|
+
assertNotCancelled(hasPublicUrl);
|
|
6517
|
+
if (hasPublicUrl) {
|
|
6518
|
+
const providedUrl = await he({
|
|
6519
|
+
message: "Your public HTTPS webhook URL",
|
|
6520
|
+
placeholder: "https://my-api.example.com/webhook",
|
|
6521
|
+
validate: (v2) => {
|
|
6522
|
+
if (!v2.trim()) return "URL is required";
|
|
6523
|
+
if (!v2.trim().startsWith("https://"))
|
|
6524
|
+
return "Must start with https:// \u2014 OneAddress enforces HTTPS for outbound webhooks";
|
|
6525
|
+
}
|
|
6526
|
+
});
|
|
6527
|
+
assertNotCancelled(providedUrl);
|
|
6528
|
+
webhookUrl = providedUrl.trim();
|
|
6529
|
+
} else {
|
|
6530
|
+
M2.info("\nSetting up a free HTTPS tunnel via Cloudflare Tunnel\u2026");
|
|
6531
|
+
const s4 = Y2();
|
|
6532
|
+
s4.start("Starting Cloudflare Tunnel");
|
|
6533
|
+
onCleanup(stopTunnel);
|
|
6534
|
+
try {
|
|
6535
|
+
let tunnel = await startTunnel(3001);
|
|
6449
6536
|
webhookUrl = `${tunnel.url}/webhook`;
|
|
6450
6537
|
tunnelBase = tunnel.url;
|
|
6451
|
-
|
|
6452
|
-
s4c.start(`Waiting for the new tunnel to route: ${tunnel.url}`);
|
|
6453
|
-
webhookReady = await waitForTunnelReady(tunnel.url, { budgetMs: 45e3 });
|
|
6454
|
-
s4c.stop(webhookReady ? "Tunnel is routing" : "Tunnel still not routing");
|
|
6455
|
-
} else {
|
|
6456
|
-
s4b.stop("Tunnel is routing");
|
|
6457
|
-
}
|
|
6458
|
-
if (!webhookReady) {
|
|
6538
|
+
s4.stop(`Tunnel active: ${tunnel.url}`);
|
|
6459
6539
|
M2.warn(
|
|
6460
|
-
|
|
6540
|
+
"IMPORTANT: This tunnel URL is temporary. It lives only while this wizard\nkeeps running, and every run gets a new address. Before going to production,\ndeploy to a permanent server and update the webhook URL in the portal."
|
|
6541
|
+
);
|
|
6542
|
+
const s4b = Y2();
|
|
6543
|
+
s4b.start("Waiting for the tunnel to start routing");
|
|
6544
|
+
webhookReady = await waitForTunnelReady(tunnel.url);
|
|
6545
|
+
if (!webhookReady) {
|
|
6546
|
+
s4b.stop("First tunnel is slow to route, trying a fresh one");
|
|
6547
|
+
stopTunnel();
|
|
6548
|
+
tunnel = await startTunnel(3001);
|
|
6549
|
+
webhookUrl = `${tunnel.url}/webhook`;
|
|
6550
|
+
tunnelBase = tunnel.url;
|
|
6551
|
+
const s4c = Y2();
|
|
6552
|
+
s4c.start(`Waiting for the new tunnel to route: ${tunnel.url}`);
|
|
6553
|
+
webhookReady = await waitForTunnelReady(tunnel.url, { budgetMs: 45e3 });
|
|
6554
|
+
s4c.stop(webhookReady ? "Tunnel is routing" : "Tunnel still not routing");
|
|
6555
|
+
} else {
|
|
6556
|
+
s4b.stop("Tunnel is routing");
|
|
6557
|
+
}
|
|
6558
|
+
if (!webhookReady) {
|
|
6559
|
+
M2.warn(
|
|
6560
|
+
`The tunnel edge is still not routing after a retry. This is almost always
|
|
6461
6561
|
one of two things, and neither is your server (it is running on :3001):
|
|
6462
6562
|
1. Open ${tunnel.url}/health in a browser. If it never loads, your network
|
|
6463
6563
|
is blocking cloudflared and no wait will fix it, so try a different network.
|
|
6464
6564
|
2. The tunnel only lives while this wizard runs, so keep this window open.`
|
|
6565
|
+
);
|
|
6566
|
+
}
|
|
6567
|
+
} catch (err) {
|
|
6568
|
+
s4.stop("Tunnel setup failed");
|
|
6569
|
+
M2.warn(`Could not start tunnel: ${err instanceof Error ? err.message : String(err)}`);
|
|
6570
|
+
M2.warn(
|
|
6571
|
+
"To get a public URL manually, try: npx cloudflared tunnel --url http://localhost:3001\nOr deploy to any server and set the URL in partners.oneaddress.io \u2192 My Profile."
|
|
6465
6572
|
);
|
|
6466
6573
|
}
|
|
6467
|
-
} catch (err) {
|
|
6468
|
-
s4.stop("Tunnel setup failed");
|
|
6469
|
-
M2.warn(`Could not start tunnel: ${err instanceof Error ? err.message : String(err)}`);
|
|
6470
|
-
M2.warn(
|
|
6471
|
-
"To get a public URL manually, try: npx cloudflared tunnel --url http://localhost:3001\nOr deploy to any server and set the URL in partners.oneaddress.io \u2192 My Profile."
|
|
6472
|
-
);
|
|
6473
6574
|
}
|
|
6474
6575
|
}
|
|
6475
|
-
if (webhookUrl) {
|
|
6576
|
+
if (webhookUrl && !keepExistingUrl) {
|
|
6476
6577
|
const s5 = Y2();
|
|
6477
6578
|
s5.start("Registering webhook URL in the Partner Portal");
|
|
6478
6579
|
const reg = await registerWebhookUrl(pid, secret, webhookUrl);
|
|
@@ -6516,6 +6617,7 @@ tunnel needing this wizard to stay open. Re-run once that URL loads in a browser
|
|
|
6516
6617
|
await new Promise((r2) => setTimeout(r2, 200));
|
|
6517
6618
|
s6.stop("Conformance checks:");
|
|
6518
6619
|
await runConformance(webhookUrl, pid, secret);
|
|
6620
|
+
await runDecryptCheck(secret, pid);
|
|
6519
6621
|
if (verifiesAccountReference) {
|
|
6520
6622
|
M2.info(
|
|
6521
6623
|
"You have declared that your receiver matches the account reference itself.\nRun `npm test` to include check-14, which sends an account reference that matches\nnothing and fails a receiver that reports it as applied. The three checks above\ncover signing and replay only."
|