@oneaddress/setup 1.3.1 → 1.3.3

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 +181 -28
  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.1" : "?";
859
+ var WIZARD_VERSION = true ? "1.3.3" : "?";
860
860
  function printCompactHeader() {
861
861
  const INNER = 42;
862
862
  const TOP = fn("\u250C") + dm("\u2500".repeat(INNER)) + fn("\u2510");
@@ -1426,6 +1426,19 @@ app.post('/webhook', async (req: Request, res: Response) => {
1426
1426
  return res.status(200).json({ ok: true });
1427
1427
  }
1428
1428
 
1429
+ // \u2500\u2500 address.test / address.test-dispatch: OneAddress connection-verification
1430
+ // A fabricated test record wrapped to your key. Reaching this line proves
1431
+ // PARTNER_PRIVATE_KEY_PEM decrypted it: a wrong key would have failed the D5
1432
+ // decrypt above and returned 422, never here. Answer { verification: true },
1433
+ // which is what the receiver-verification and go-live checks look for. Return
1434
+ // anything else (including the forward-compat skip below) and the check reads
1435
+ // as "answered but could not decrypt", which is not what happened.
1436
+ if (event === 'address.test' || event === 'address.test-dispatch') {
1437
+ const index = typeof body.index === 'number' ? body.index : undefined;
1438
+ console.log(\`[webhook] \${event} verification probe decrypted OK\`);
1439
+ return res.status(200).json({ verification: true, index });
1440
+ }
1441
+
1429
1442
  // Unknown event \u2014 acknowledge (forward compatibility)
1430
1443
  console.log(\`[webhook] Unknown event "\${event}" \u2014 acknowledged\`);
1431
1444
  return res.status(200).json({ ok: true, skipped: true });
@@ -1960,6 +1973,15 @@ async def webhook(request: Request) -> Response:
1960
1973
  except Exception as e:
1961
1974
  print(f"[webhook] Callback POST failed: {e}")
1962
1975
 
1976
+ elif event in ("address.test", "address.test-dispatch"):
1977
+ # OneAddress connection-verification probe. Reaching here means the D5
1978
+ # decrypt above succeeded (a wrong key returned 422), which is exactly
1979
+ # what the check proves, so answer verification=true. Anything else reads
1980
+ # as "answered but could not decrypt", which is not what happened.
1981
+ print(f"[webhook] {event} verification probe decrypted OK")
1982
+ return Response(content=json.dumps({"verification": True, "index": body.get("index")}),
1983
+ media_type="application/json")
1984
+
1963
1985
  else:
1964
1986
  print(f"[webhook] Unknown event '{event}' \u2014 acknowledged")
1965
1987
 
@@ -2517,6 +2539,14 @@ public class OneAddressWebhookController {
2517
2539
  } else if ("address.verify".equals(event)) {
2518
2540
  handleAddressVerify(body, address, accountNumber, verifiedName, knownNames);
2519
2541
  if (dispatchId != null && !dispatchId.isBlank()) store.markProcessed(dispatchId, "verify");
2542
+ } else if ("address.test".equals(event) || "address.test-dispatch".equals(event)) {
2543
+ // OneAddress connection-verification probe. Reaching here means the
2544
+ // D5 decrypt above succeeded (a wrong key returned 422), which is
2545
+ // what the check proves, so answer verification=true.
2546
+ if (dispatchId != null && !dispatchId.isBlank()) store.markProcessed(dispatchId, "verification:ok");
2547
+ Object idx = body.get("index");
2548
+ log.info("[OneAddress] {} verification probe decrypted OK", event);
2549
+ return ResponseEntity.ok("{\\"verification\\":true,\\"index\\":" + (idx == null ? "null" : idx.toString()) + "}");
2520
2550
  } else {
2521
2551
  log.info("[OneAddress] skipping unknown event: {}", event);
2522
2552
  if (dispatchId != null && !dispatchId.isBlank()) store.markProcessed(dispatchId, "ignored:unknown-event");
@@ -3285,7 +3315,11 @@ app.MapPost("/webhooks/oneaddress", async (HttpRequest request) =>
3285
3315
 
3286
3316
  var eventType = body.TryGetProperty("event", out var evtEl) ? evtEl.GetString() : null;
3287
3317
 
3288
- if (eventType != "address.updated" && eventType != "address.verify")
3318
+ // address.test / address.test-dispatch (the connection-verification probe)
3319
+ // must pass this guard so it reaches the decrypt below; a wrong key then
3320
+ // returns 422 and only a real decrypt reaches the verification answer.
3321
+ if (eventType != "address.updated" && eventType != "address.verify"
3322
+ && eventType != "address.test" && eventType != "address.test-dispatch")
3289
3323
  {
3290
3324
  app.Logger.LogInformation("[OneAddress] skipping unknown event: {EventType}", eventType);
3291
3325
  return Results.Json(new { ok = true, skipped = true });
@@ -3366,6 +3400,17 @@ app.MapPost("/webhooks/oneaddress", async (HttpRequest request) =>
3366
3400
  app.Logger.LogWarning("[OneAddress] dispatch {DispatchId} received before but never completed \u2014 reprocessing", dispatchId);
3367
3401
  }
3368
3402
 
3403
+ if (eventType == "address.test" || eventType == "address.test-dispatch")
3404
+ {
3405
+ // OneAddress connection-verification probe. Reaching here means the D5
3406
+ // decrypt above succeeded (a wrong key returned 422), which is what the
3407
+ // check proves, so answer verification=true.
3408
+ int? index = body.TryGetProperty("index", out var idxEl) && idxEl.ValueKind == JsonValueKind.Number
3409
+ ? idxEl.GetInt32() : (int?)null;
3410
+ app.Logger.LogInformation("[OneAddress] {EventType} verification probe decrypted OK", eventType);
3411
+ return Results.Json(new { verification = true, index });
3412
+ }
3413
+
3369
3414
  if (eventType == "address.updated")
3370
3415
  {
3371
3416
  // Work FIRST, respond after. Acknowledging before the write commits
@@ -4203,6 +4248,21 @@ func main() {
4203
4248
  _ = store.MarkProcessed(dispatch, "verify:"+result)
4204
4249
  }
4205
4250
 
4251
+ case "address.test", "address.test-dispatch":
4252
+ // OneAddress connection-verification probe. Reaching this case means the
4253
+ // decrypt above succeeded (a wrong key returned 422), which is what the
4254
+ // check proves, so answer verification=true rather than the ok:true below.
4255
+ log.Printf("[webhook] %s verification probe decrypted OK", event)
4256
+ if dispatch != "" {
4257
+ _ = store.MarkProcessed(dispatch, "verification:ok")
4258
+ }
4259
+ var idx any = nil
4260
+ if v, ok := parsed["index"]; ok {
4261
+ idx = v
4262
+ }
4263
+ jsonResp(w, 200, map[string]any{"verification": true, "index": idx})
4264
+ return
4265
+
4206
4266
  default:
4207
4267
  log.Printf("[webhook] Unknown event %q \u2014 acknowledged", event)
4208
4268
  if dispatch != "" {
@@ -4869,7 +4929,11 @@ Route::post('/webhook', function (Request $request): Response {
4869
4929
 
4870
4930
  $eventType = $body['event'] ?? '';
4871
4931
 
4872
- if ($eventType !== 'address.updated' && $eventType !== 'address.verify') {
4932
+ // address.test / address.test-dispatch (the connection-verification probe)
4933
+ // must pass this guard so it reaches the decrypt below; a wrong key then
4934
+ // returns 422 and only a real decrypt reaches the verification answer.
4935
+ if ($eventType !== 'address.updated' && $eventType !== 'address.verify'
4936
+ && $eventType !== 'address.test' && $eventType !== 'address.test-dispatch') {
4873
4937
  return response()->json(['ok' => true, 'skipped' => true]);
4874
4938
  }
4875
4939
 
@@ -5038,6 +5102,14 @@ Route::post('/webhook', function (Request $request): Response {
5038
5102
  }
5039
5103
  }
5040
5104
 
5105
+ if ($eventType === 'address.test' || $eventType === 'address.test-dispatch') {
5106
+ // OneAddress connection-verification probe. Reaching here means the D5
5107
+ // decrypt above succeeded (a wrong key returned 422), which is what the
5108
+ // check proves, so answer verification=true.
5109
+ \\Log::info('[OneAddress] verification probe decrypted OK', ['event' => $eventType]);
5110
+ return response()->json(['verification' => true, 'index' => $body['index'] ?? null]);
5111
+ }
5112
+
5041
5113
  if ($eventType === 'address.updated') {
5042
5114
  // Work FIRST, respond after. Acknowledging before the write commits
5043
5115
  // marks the dispatch delivered on a failure that is never retried.
@@ -5224,7 +5296,7 @@ async function scaffold(platform, outputDir, partnerId, webhookSecret, webhookUr
5224
5296
 
5225
5297
  // src/register.ts
5226
5298
  var import_node_crypto = require("crypto");
5227
- var PKG_VERSION = true ? "1.3.1" : "dev";
5299
+ var PKG_VERSION = true ? "1.3.3" : "dev";
5228
5300
  var REGISTER_URL = "https://partners.oneaddress.io/api/partner/installs";
5229
5301
  function hmacSha256(secret, message) {
5230
5302
  return (0, import_node_crypto.createHmac)("sha256", secret).update(message).digest("hex");
@@ -5648,7 +5720,7 @@ async function resolveBinary() {
5648
5720
  return downloadCloudflared();
5649
5721
  }
5650
5722
  async function waitForTunnelReady(url, opts = {}) {
5651
- const budgetMs = opts.budgetMs ?? 3e4;
5723
+ const budgetMs = opts.budgetMs ?? 6e4;
5652
5724
  const intervalMs = opts.intervalMs ?? 1500;
5653
5725
  const attemptTimeoutMs = opts.attemptTimeoutMs ?? 4e3;
5654
5726
  const doFetch = opts.fetchImpl ?? fetch;
@@ -5766,6 +5838,26 @@ async function getVerifiesAccountReference(partnerId, webhookSecret, onError) {
5766
5838
  return void 0;
5767
5839
  }
5768
5840
  }
5841
+ async function verifyWebhookSecret(partnerId, webhookSecret) {
5842
+ try {
5843
+ const ts = String(Math.floor(Date.now() / 1e3));
5844
+ const sig = (0, import_node_crypto4.createHmac)("sha256", webhookSecret).update(`${ts}.`).digest("hex");
5845
+ const res = await fetch(PORTAL_API, {
5846
+ method: "GET",
5847
+ headers: {
5848
+ "X-OneAddress-Timestamp": ts,
5849
+ "X-OneAddress-Signature": sig,
5850
+ "X-OneAddress-Partner": partnerId
5851
+ },
5852
+ signal: AbortSignal.timeout(8e3)
5853
+ });
5854
+ if (res.ok) return "ok";
5855
+ if (res.status === 401 || res.status === 403) return "bad_secret";
5856
+ return "unreachable";
5857
+ } catch {
5858
+ return "unreachable";
5859
+ }
5860
+ }
5769
5861
  async function registerWebhookUrl(partnerId, webhookSecret, webhookUrl) {
5770
5862
  try {
5771
5863
  const body = JSON.stringify({ webhook_url: webhookUrl });
@@ -6033,14 +6125,42 @@ async function main() {
6033
6125
  M2.info(" 2. Scaffold a working webhook server for your platform");
6034
6126
  M2.info(" 3. Get it live with a passing conformance check \u2014 in under 5 minutes");
6035
6127
  M2.info("\nOpen partners.oneaddress.io \u2192 My Profile to find the two values below.\n");
6036
- const webhookSecret = await ge({
6037
- message: "Your Webhook signing secret (min 20 chars)",
6038
- validate: (v2) => {
6039
- if (!v2.trim()) return "Webhook signing secret is required";
6040
- if (v2.length < 20) return "That looks too short \u2014 paste the full secret from My Profile";
6128
+ let secret;
6129
+ for (let attempt = 1; ; attempt++) {
6130
+ const webhookSecret = await ge({
6131
+ message: attempt === 1 ? "Your Webhook signing secret (min 20 chars)" : `Your Webhook signing secret (attempt ${attempt})`,
6132
+ validate: (v2) => {
6133
+ if (!v2.trim()) return "Webhook signing secret is required";
6134
+ if (v2.length < 20) return "That looks too short \u2014 paste the full secret from My Profile";
6135
+ }
6136
+ });
6137
+ assertNotCancelled(webhookSecret);
6138
+ secret = webhookSecret.trim();
6139
+ const sv = Y2();
6140
+ sv.start("Checking your Webhook signing secret");
6141
+ const check = await verifyWebhookSecret(pid, secret);
6142
+ if (check === "ok") {
6143
+ sv.stop("Webhook signing secret verified");
6144
+ break;
6041
6145
  }
6042
- });
6043
- assertNotCancelled(webhookSecret);
6146
+ if (check === "unreachable") {
6147
+ sv.stop("Could not verify the secret against the portal, continuing");
6148
+ M2.warn(
6149
+ "Could not reach the portal to check your Webhook signing secret. Continuing.\nIf live dispatches later fail signature verification, re-check it at\npartners.oneaddress.io -> Webhook -> Webhook signing secret."
6150
+ );
6151
+ break;
6152
+ }
6153
+ sv.stop("That Webhook signing secret was rejected");
6154
+ if (attempt >= 3) {
6155
+ xe(
6156
+ "The Webhook signing secret did not match after three tries.\nRegenerate it at partners.oneaddress.io -> Webhook -> Webhook signing secret\n(use Revoke and regenerate, not Rotate), then run the wizard again."
6157
+ );
6158
+ process.exit(1);
6159
+ }
6160
+ M2.error(
6161
+ "The portal rejected that secret (HTTP 401): it does not match the one on your profile.\nRegenerate it at partners.oneaddress.io -> Webhook -> Webhook signing secret\n(use Revoke and regenerate, not Rotate), copy the value shown once, and paste it here."
6162
+ );
6163
+ }
6044
6164
  const privateKeyRaw = await he({
6045
6165
  message: "Your ECDH Private Key",
6046
6166
  placeholder: "Paste the base64 key body from My Profile, or enter a path to the .pem file",
@@ -6052,7 +6172,6 @@ async function main() {
6052
6172
  }
6053
6173
  });
6054
6174
  assertNotCancelled(privateKeyRaw);
6055
- const secret = webhookSecret.trim();
6056
6175
  const normalised = normalisePrivateKey(privateKeyRaw.trim());
6057
6176
  if (normalised.error) {
6058
6177
  xe(`Private key parse failed unexpectedly: ${normalised.error}`);
@@ -6177,6 +6296,8 @@ async function main() {
6177
6296
  if (start.output) M2.warn(start.output.slice(0, 600));
6178
6297
  }
6179
6298
  let webhookUrl = "";
6299
+ let webhookReady = true;
6300
+ let tunnelBase = null;
6180
6301
  let existingUrlReason = "";
6181
6302
  const existingUrlResult = await getExistingWebhookUrl(pid, secret, (r2) => {
6182
6303
  existingUrlReason = r2;
@@ -6235,19 +6356,36 @@ Continuing will overwrite it.`
6235
6356
  s4.start("Starting Cloudflare Tunnel");
6236
6357
  onCleanup(stopTunnel);
6237
6358
  try {
6238
- const tunnel = await startTunnel(3001);
6359
+ let tunnel = await startTunnel(3001);
6239
6360
  webhookUrl = `${tunnel.url}/webhook`;
6361
+ tunnelBase = tunnel.url;
6240
6362
  s4.stop(`Tunnel active: ${tunnel.url}`);
6241
6363
  M2.warn(
6242
- "IMPORTANT: This tunnel URL is temporary \u2014 it expires when this process stops.\nBefore going to production, deploy to a permanent server and update the webhook URL in the portal."
6364
+ "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."
6243
6365
  );
6244
6366
  const s4b = Y2();
6245
6367
  s4b.start("Waiting for the tunnel to start routing");
6246
- const ready = await waitForTunnelReady(tunnel.url);
6247
- s4b.stop(ready ? "Tunnel is routing" : "Tunnel is slow to come online \u2014 continuing anyway");
6248
- if (!ready) {
6368
+ webhookReady = await waitForTunnelReady(tunnel.url);
6369
+ if (!webhookReady) {
6370
+ s4b.stop("First tunnel is slow to route, trying a fresh one");
6371
+ stopTunnel();
6372
+ tunnel = await startTunnel(3001);
6373
+ webhookUrl = `${tunnel.url}/webhook`;
6374
+ tunnelBase = tunnel.url;
6375
+ const s4c = Y2();
6376
+ s4c.start(`Waiting for the new tunnel to route: ${tunnel.url}`);
6377
+ webhookReady = await waitForTunnelReady(tunnel.url, { budgetMs: 45e3 });
6378
+ s4c.stop(webhookReady ? "Tunnel is routing" : "Tunnel still not routing");
6379
+ } else {
6380
+ s4b.stop("Tunnel is routing");
6381
+ }
6382
+ if (!webhookReady) {
6249
6383
  M2.warn(
6250
- "The tunnel edge has not answered yet. If the checks below time out it is almost\ncertainly still warming up, not your server \u2014 wait a few seconds and re-run the wizard."
6384
+ `The tunnel edge is still not routing after a retry. This is almost always
6385
+ one of two things, and neither is your server (it is running on :3001):
6386
+ 1. Open ${tunnel.url}/health in a browser. If it never loads, your network
6387
+ is blocking cloudflared and no wait will fix it, so try a different network.
6388
+ 2. The tunnel only lives while this wizard runs, so keep this window open.`
6251
6389
  );
6252
6390
  }
6253
6391
  } catch (err) {
@@ -6282,16 +6420,31 @@ Continuing will overwrite it.`
6282
6420
  });
6283
6421
  }
6284
6422
  if (webhookUrl) {
6285
- M2.info("");
6286
- const s6 = Y2();
6287
- s6.start("Running conformance checks");
6288
- await new Promise((r2) => setTimeout(r2, 200));
6289
- s6.stop("Conformance checks:");
6290
- await runConformance(webhookUrl, pid, secret);
6291
- if (verifiesAccountReference) {
6292
- M2.info(
6293
- "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."
6423
+ if (!webhookReady && tunnelBase) {
6424
+ const sr = Y2();
6425
+ sr.start("Waiting for the tunnel to start routing");
6426
+ webhookReady = await waitForTunnelReady(tunnelBase, { budgetMs: 2e4 });
6427
+ sr.stop(webhookReady ? "Tunnel is routing" : "Tunnel still not routing");
6428
+ }
6429
+ if (!webhookReady) {
6430
+ M2.warn(
6431
+ `Skipping the conformance checks: the tunnel edge is not routing.
6432
+ Your server is fine, it is running on :3001. This is either your network
6433
+ blocking cloudflared (open ${tunnelBase}/health in a browser to check) or the
6434
+ tunnel needing this wizard to stay open. Re-run once that URL loads in a browser.`
6294
6435
  );
6436
+ } else {
6437
+ M2.info("");
6438
+ const s6 = Y2();
6439
+ s6.start("Running conformance checks");
6440
+ await new Promise((r2) => setTimeout(r2, 200));
6441
+ s6.stop("Conformance checks:");
6442
+ await runConformance(webhookUrl, pid, secret);
6443
+ if (verifiesAccountReference) {
6444
+ M2.info(
6445
+ "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."
6446
+ );
6447
+ }
6295
6448
  }
6296
6449
  }
6297
6450
  const summary = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oneaddress/setup",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "Interactive setup wizard for OneAddress partner webhook integrations",
5
5
  "main": "dist/index.js",
6
6
  "bin": {