@oneaddress/setup 1.3.4 → 1.4.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.
Files changed (2) hide show
  1. package/dist/index.js +102 -62
  2. 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.3.4" : "?";
859
+ var WIZARD_VERSION = true ? "1.4.0" : "?";
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
- // In-memory dedup cache \u2014 bounded so a partner running with the default
1233
- // scaffold can't be DoS'd by an attacker who's already past HMAC verify
1234
- // (i.e. has stolen the webhook secret) by sending unique dispatch IDs
1235
- // until RAM is exhausted. For production use a distributed store (Redis).
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
- if (dispatch) rememberDispatch(dispatch);
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.3.4" : "dev";
5412
+ var PKG_VERSION = true ? "1.4.0" : "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");
@@ -6375,6 +6412,7 @@ async function main() {
6375
6412
  let webhookReady = true;
6376
6413
  let tunnelBase = null;
6377
6414
  let existingUrlReason = "";
6415
+ let keepExistingUrl = false;
6378
6416
  const existingUrlResult = await getExistingWebhookUrl(pid, secret, (r2) => {
6379
6417
  existingUrlReason = r2;
6380
6418
  });
@@ -6390,9 +6428,9 @@ Continuing will overwrite it.`
6390
6428
  });
6391
6429
  assertNotCancelled(continueAnyway);
6392
6430
  if (!continueAnyway) {
6393
- runCleanup();
6394
- Se("Setup cancelled \u2014 your existing webhook URL was not changed.");
6395
- process.exit(0);
6431
+ keepExistingUrl = true;
6432
+ webhookUrl = existingUrlResult;
6433
+ M2.info(`Keeping your existing webhook URL: ${webhookUrl}`);
6396
6434
  }
6397
6435
  } else if (existingUrlResult === void 0) {
6398
6436
  M2.warn(
@@ -6409,70 +6447,72 @@ Continuing will overwrite it.`
6409
6447
  process.exit(0);
6410
6448
  }
6411
6449
  }
6412
- const hasPublicUrl = await ye({
6413
- message: "Do you have a public HTTPS URL where this server is reachable?",
6414
- initialValue: false
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
- }
6450
+ if (!keepExistingUrl) {
6451
+ const hasPublicUrl = await ye({
6452
+ message: "Do you have a public HTTPS URL where this server is reachable?",
6453
+ initialValue: false
6426
6454
  });
6427
- assertNotCancelled(providedUrl);
6428
- webhookUrl = providedUrl.trim();
6429
- } else {
6430
- M2.info("\nSetting up a free HTTPS tunnel via Cloudflare Tunnel\u2026");
6431
- const s4 = Y2();
6432
- s4.start("Starting Cloudflare Tunnel");
6433
- onCleanup(stopTunnel);
6434
- try {
6435
- let tunnel = await startTunnel(3001);
6436
- webhookUrl = `${tunnel.url}/webhook`;
6437
- tunnelBase = tunnel.url;
6438
- s4.stop(`Tunnel active: ${tunnel.url}`);
6439
- M2.warn(
6440
- "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."
6441
- );
6442
- const s4b = Y2();
6443
- s4b.start("Waiting for the tunnel to start routing");
6444
- webhookReady = await waitForTunnelReady(tunnel.url);
6445
- if (!webhookReady) {
6446
- s4b.stop("First tunnel is slow to route, trying a fresh one");
6447
- stopTunnel();
6448
- tunnel = await startTunnel(3001);
6455
+ assertNotCancelled(hasPublicUrl);
6456
+ if (hasPublicUrl) {
6457
+ const providedUrl = await he({
6458
+ message: "Your public HTTPS webhook URL",
6459
+ placeholder: "https://my-api.example.com/webhook",
6460
+ validate: (v2) => {
6461
+ if (!v2.trim()) return "URL is required";
6462
+ if (!v2.trim().startsWith("https://"))
6463
+ return "Must start with https:// \u2014 OneAddress enforces HTTPS for outbound webhooks";
6464
+ }
6465
+ });
6466
+ assertNotCancelled(providedUrl);
6467
+ webhookUrl = providedUrl.trim();
6468
+ } else {
6469
+ M2.info("\nSetting up a free HTTPS tunnel via Cloudflare Tunnel\u2026");
6470
+ const s4 = Y2();
6471
+ s4.start("Starting Cloudflare Tunnel");
6472
+ onCleanup(stopTunnel);
6473
+ try {
6474
+ let tunnel = await startTunnel(3001);
6449
6475
  webhookUrl = `${tunnel.url}/webhook`;
6450
6476
  tunnelBase = tunnel.url;
6451
- const s4c = Y2();
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) {
6477
+ s4.stop(`Tunnel active: ${tunnel.url}`);
6459
6478
  M2.warn(
6460
- `The tunnel edge is still not routing after a retry. This is almost always
6479
+ "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."
6480
+ );
6481
+ const s4b = Y2();
6482
+ s4b.start("Waiting for the tunnel to start routing");
6483
+ webhookReady = await waitForTunnelReady(tunnel.url);
6484
+ if (!webhookReady) {
6485
+ s4b.stop("First tunnel is slow to route, trying a fresh one");
6486
+ stopTunnel();
6487
+ tunnel = await startTunnel(3001);
6488
+ webhookUrl = `${tunnel.url}/webhook`;
6489
+ tunnelBase = tunnel.url;
6490
+ const s4c = Y2();
6491
+ s4c.start(`Waiting for the new tunnel to route: ${tunnel.url}`);
6492
+ webhookReady = await waitForTunnelReady(tunnel.url, { budgetMs: 45e3 });
6493
+ s4c.stop(webhookReady ? "Tunnel is routing" : "Tunnel still not routing");
6494
+ } else {
6495
+ s4b.stop("Tunnel is routing");
6496
+ }
6497
+ if (!webhookReady) {
6498
+ M2.warn(
6499
+ `The tunnel edge is still not routing after a retry. This is almost always
6461
6500
  one of two things, and neither is your server (it is running on :3001):
6462
6501
  1. Open ${tunnel.url}/health in a browser. If it never loads, your network
6463
6502
  is blocking cloudflared and no wait will fix it, so try a different network.
6464
6503
  2. The tunnel only lives while this wizard runs, so keep this window open.`
6504
+ );
6505
+ }
6506
+ } catch (err) {
6507
+ s4.stop("Tunnel setup failed");
6508
+ M2.warn(`Could not start tunnel: ${err instanceof Error ? err.message : String(err)}`);
6509
+ M2.warn(
6510
+ "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
6511
  );
6466
6512
  }
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
6513
  }
6474
6514
  }
6475
- if (webhookUrl) {
6515
+ if (webhookUrl && !keepExistingUrl) {
6476
6516
  const s5 = Y2();
6477
6517
  s5.start("Registering webhook URL in the Partner Portal");
6478
6518
  const reg = await registerWebhookUrl(pid, secret, webhookUrl);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oneaddress/setup",
3
- "version": "1.3.4",
3
+ "version": "1.4.0",
4
4
  "description": "Interactive setup wizard for OneAddress partner webhook integrations",
5
5
  "main": "dist/index.js",
6
6
  "bin": {