@oneaddress/setup 1.3.0 → 1.3.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.
Files changed (2) hide show
  1. package/dist/index.js +138 -32
  2. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -856,8 +856,31 @@ 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.0" : "?";
859
+ var WIZARD_VERSION = true ? "1.3.2" : "?";
860
+ function printCompactHeader() {
861
+ const INNER = 42;
862
+ const TOP = fn("\u250C") + dm("\u2500".repeat(INNER)) + fn("\u2510");
863
+ const BOT = fn("\u2514") + dm("\u2500".repeat(INNER)) + fn("\u2518");
864
+ const S3 = dm("\u2502");
865
+ const blank = ` ${S3}${" ".repeat(INNER)}${S3}`;
866
+ const row = (content, visibleLen) => ` ${S3} ${content}${" ".repeat(Math.max(0, INNER - 2 - visibleLen))}${S3}`;
867
+ const ver = `v${WIZARD_VERSION} \xB7 partners.oneaddress.io`;
868
+ console.log("");
869
+ console.log(" " + TOP);
870
+ console.log(blank);
871
+ console.log(row(wb("One ") + ab("Address"), "One Address".length));
872
+ console.log(row(mi("Partner Setup Wizard"), "Partner Setup Wizard".length));
873
+ console.log(row(fn(ver), ver.length));
874
+ console.log(blank);
875
+ console.log(" " + BOT);
876
+ console.log("");
877
+ }
860
878
  function printHeader() {
879
+ const cols = typeof process !== "undefined" && process.stdout && process.stdout.columns ? process.stdout.columns : 80;
880
+ if (cols < 88) {
881
+ printCompactHeader();
882
+ return;
883
+ }
861
884
  const BAR_LEN = 85;
862
885
  const TOP_BAR = fn("\u250C") + dm("\u2500".repeat(BAR_LEN)) + fn("\u2510");
863
886
  const BOT_BAR = fn("\u2514") + dm("\u2500".repeat(BAR_LEN)) + fn("\u2518");
@@ -5201,7 +5224,7 @@ async function scaffold(platform, outputDir, partnerId, webhookSecret, webhookUr
5201
5224
 
5202
5225
  // src/register.ts
5203
5226
  var import_node_crypto = require("crypto");
5204
- var PKG_VERSION = true ? "1.3.0" : "dev";
5227
+ var PKG_VERSION = true ? "1.3.2" : "dev";
5205
5228
  var REGISTER_URL = "https://partners.oneaddress.io/api/partner/installs";
5206
5229
  function hmacSha256(secret, message) {
5207
5230
  return (0, import_node_crypto.createHmac)("sha256", secret).update(message).digest("hex");
@@ -5625,7 +5648,7 @@ async function resolveBinary() {
5625
5648
  return downloadCloudflared();
5626
5649
  }
5627
5650
  async function waitForTunnelReady(url, opts = {}) {
5628
- const budgetMs = opts.budgetMs ?? 3e4;
5651
+ const budgetMs = opts.budgetMs ?? 6e4;
5629
5652
  const intervalMs = opts.intervalMs ?? 1500;
5630
5653
  const attemptTimeoutMs = opts.attemptTimeoutMs ?? 4e3;
5631
5654
  const doFetch = opts.fetchImpl ?? fetch;
@@ -5686,7 +5709,13 @@ async function startTunnel(port) {
5686
5709
  // src/register-url.ts
5687
5710
  var import_node_crypto4 = require("crypto");
5688
5711
  var PORTAL_API = "https://partners.oneaddress.io/api/partner/webhook-url";
5689
- async function getExistingWebhookUrl(partnerId, webhookSecret) {
5712
+ function describeReadFailure(status) {
5713
+ if (status === 401) return "the portal rejected the signature (HTTP 401), which means your Webhook signing secret does not match the one on your profile. Copy it again from partners.oneaddress.io \u2192 Webhook \u2192 Webhook signing secret.";
5714
+ if (status === 404) return "the portal has no active partner for this account (HTTP 404), which usually means it is not approved yet.";
5715
+ if (status === 400) return "the portal rejected the request (HTTP 400).";
5716
+ return `the portal returned HTTP ${status}.`;
5717
+ }
5718
+ async function getExistingWebhookUrl(partnerId, webhookSecret, onError) {
5690
5719
  try {
5691
5720
  const ts = String(Math.floor(Date.now() / 1e3));
5692
5721
  const sig = (0, import_node_crypto4.createHmac)("sha256", webhookSecret).update(`${ts}.`).digest("hex");
@@ -5699,14 +5728,18 @@ async function getExistingWebhookUrl(partnerId, webhookSecret) {
5699
5728
  },
5700
5729
  signal: AbortSignal.timeout(8e3)
5701
5730
  });
5702
- if (!res.ok) return void 0;
5731
+ if (!res.ok) {
5732
+ onError?.(describeReadFailure(res.status));
5733
+ return void 0;
5734
+ }
5703
5735
  const data = await res.json();
5704
5736
  return data.webhook_url ?? null;
5705
- } catch {
5737
+ } catch (err) {
5738
+ onError?.(`could not reach the portal (${err instanceof Error ? err.message : "network error"})`);
5706
5739
  return void 0;
5707
5740
  }
5708
5741
  }
5709
- async function getVerifiesAccountReference(partnerId, webhookSecret) {
5742
+ async function getVerifiesAccountReference(partnerId, webhookSecret, onError) {
5710
5743
  try {
5711
5744
  const ts = String(Math.floor(Date.now() / 1e3));
5712
5745
  const sig = (0, import_node_crypto4.createHmac)("sha256", webhookSecret).update(`${ts}.`).digest("hex");
@@ -5719,16 +5752,40 @@ async function getVerifiesAccountReference(partnerId, webhookSecret) {
5719
5752
  },
5720
5753
  signal: AbortSignal.timeout(8e3)
5721
5754
  });
5722
- if (!res.ok) return void 0;
5755
+ if (!res.ok) {
5756
+ onError?.(describeReadFailure(res.status));
5757
+ return void 0;
5758
+ }
5723
5759
  const data = await res.json();
5724
5760
  const v2 = data.verifies_account_reference;
5725
5761
  if (v2 === true || v2 === false) return v2;
5726
5762
  if (v2 === null) return null;
5727
5763
  return void 0;
5728
- } catch {
5764
+ } catch (err) {
5765
+ onError?.(`could not reach the portal (${err instanceof Error ? err.message : "network error"})`);
5729
5766
  return void 0;
5730
5767
  }
5731
5768
  }
5769
+ async function verifyWebhookSecret(partnerId, webhookSecret) {
5770
+ try {
5771
+ const ts = String(Math.floor(Date.now() / 1e3));
5772
+ const sig = (0, import_node_crypto4.createHmac)("sha256", webhookSecret).update(`${ts}.`).digest("hex");
5773
+ const res = await fetch(PORTAL_API, {
5774
+ method: "GET",
5775
+ headers: {
5776
+ "X-OneAddress-Timestamp": ts,
5777
+ "X-OneAddress-Signature": sig,
5778
+ "X-OneAddress-Partner": partnerId
5779
+ },
5780
+ signal: AbortSignal.timeout(8e3)
5781
+ });
5782
+ if (res.ok) return "ok";
5783
+ if (res.status === 401 || res.status === 403) return "bad_secret";
5784
+ return "unreachable";
5785
+ } catch {
5786
+ return "unreachable";
5787
+ }
5788
+ }
5732
5789
  async function registerWebhookUrl(partnerId, webhookSecret, webhookUrl) {
5733
5790
  try {
5734
5791
  const body = JSON.stringify({ webhook_url: webhookUrl });
@@ -5996,14 +6053,42 @@ async function main() {
5996
6053
  M2.info(" 2. Scaffold a working webhook server for your platform");
5997
6054
  M2.info(" 3. Get it live with a passing conformance check \u2014 in under 5 minutes");
5998
6055
  M2.info("\nOpen partners.oneaddress.io \u2192 My Profile to find the two values below.\n");
5999
- const webhookSecret = await ge({
6000
- message: "Your Webhook Secret (min 20 chars)",
6001
- validate: (v2) => {
6002
- if (!v2.trim()) return "Webhook Secret is required";
6003
- if (v2.length < 20) return "That looks too short \u2014 paste the full secret from My Profile";
6056
+ let secret;
6057
+ for (let attempt = 1; ; attempt++) {
6058
+ const webhookSecret = await ge({
6059
+ message: attempt === 1 ? "Your Webhook signing secret (min 20 chars)" : `Your Webhook signing secret (attempt ${attempt})`,
6060
+ validate: (v2) => {
6061
+ if (!v2.trim()) return "Webhook signing secret is required";
6062
+ if (v2.length < 20) return "That looks too short \u2014 paste the full secret from My Profile";
6063
+ }
6064
+ });
6065
+ assertNotCancelled(webhookSecret);
6066
+ secret = webhookSecret.trim();
6067
+ const sv = Y2();
6068
+ sv.start("Checking your Webhook signing secret");
6069
+ const check = await verifyWebhookSecret(pid, secret);
6070
+ if (check === "ok") {
6071
+ sv.stop("Webhook signing secret verified");
6072
+ break;
6004
6073
  }
6005
- });
6006
- assertNotCancelled(webhookSecret);
6074
+ if (check === "unreachable") {
6075
+ sv.stop("Could not verify the secret against the portal, continuing");
6076
+ M2.warn(
6077
+ "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."
6078
+ );
6079
+ break;
6080
+ }
6081
+ sv.stop("That Webhook signing secret was rejected");
6082
+ if (attempt >= 3) {
6083
+ xe(
6084
+ "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."
6085
+ );
6086
+ process.exit(1);
6087
+ }
6088
+ M2.error(
6089
+ "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."
6090
+ );
6091
+ }
6007
6092
  const privateKeyRaw = await he({
6008
6093
  message: "Your ECDH Private Key",
6009
6094
  placeholder: "Paste the base64 key body from My Profile, or enter a path to the .pem file",
@@ -6015,7 +6100,6 @@ async function main() {
6015
6100
  }
6016
6101
  });
6017
6102
  assertNotCancelled(privateKeyRaw);
6018
- const secret = webhookSecret.trim();
6019
6103
  const normalised = normalisePrivateKey(privateKeyRaw.trim());
6020
6104
  if (normalised.error) {
6021
6105
  xe(`Private key parse failed unexpectedly: ${normalised.error}`);
@@ -6059,11 +6143,14 @@ async function main() {
6059
6143
  }
6060
6144
  }
6061
6145
  }
6062
- const accountRefDeclaration = await getVerifiesAccountReference(pid, secret);
6146
+ let accountRefReason = "";
6147
+ const accountRefDeclaration = await getVerifiesAccountReference(pid, secret, (r2) => {
6148
+ accountRefReason = r2;
6149
+ });
6063
6150
  const verifiesAccountReference = accountRefDeclaration === true;
6064
6151
  if (accountRefDeclaration === void 0) {
6065
6152
  M2.warn(
6066
- "Could not read your account-reference declaration from the portal.\nConformance check-14 has been left out of the generated `npm test`, so a\npassing run does NOT mean your receiver matches account references correctly.\nSet it at partners.oneaddress.io \u2192 My Profile, then re-run this wizard."
6153
+ "Could not read your account-reference declaration from the portal.\n" + (accountRefReason ? "Reason: " + accountRefReason + "\n" : "") + "Conformance check-14 has been left out of the generated `npm test`, so a\npassing run does NOT mean your receiver matches account references correctly.\nSet it at partners.oneaddress.io \u2192 My Profile, then re-run this wizard."
6067
6154
  );
6068
6155
  }
6069
6156
  const s1 = Y2();
@@ -6137,7 +6224,12 @@ async function main() {
6137
6224
  if (start.output) M2.warn(start.output.slice(0, 600));
6138
6225
  }
6139
6226
  let webhookUrl = "";
6140
- const existingUrlResult = await getExistingWebhookUrl(pid, secret);
6227
+ let webhookReady = true;
6228
+ let tunnelBase = null;
6229
+ let existingUrlReason = "";
6230
+ const existingUrlResult = await getExistingWebhookUrl(pid, secret, (r2) => {
6231
+ existingUrlReason = r2;
6232
+ });
6141
6233
  if (typeof existingUrlResult === "string") {
6142
6234
  M2.warn(
6143
6235
  `A webhook URL is already registered for this partner:
@@ -6156,7 +6248,7 @@ Continuing will overwrite it.`
6156
6248
  }
6157
6249
  } else if (existingUrlResult === void 0) {
6158
6250
  M2.warn(
6159
- "Could not check whether a webhook URL is already registered (portal API unreachable). Any URL you register now will overwrite an existing one without warning."
6251
+ "Could not check whether a webhook URL is already registered" + (existingUrlReason ? ": " + existingUrlReason : " (portal API unreachable).") + "\nAny URL you register now will overwrite an existing one without warning."
6160
6252
  );
6161
6253
  const continueAnyway = await ye({
6162
6254
  message: "Continue anyway?",
@@ -6194,6 +6286,7 @@ Continuing will overwrite it.`
6194
6286
  try {
6195
6287
  const tunnel = await startTunnel(3001);
6196
6288
  webhookUrl = `${tunnel.url}/webhook`;
6289
+ tunnelBase = tunnel.url;
6197
6290
  s4.stop(`Tunnel active: ${tunnel.url}`);
6198
6291
  M2.warn(
6199
6292
  "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."
@@ -6201,10 +6294,11 @@ Continuing will overwrite it.`
6201
6294
  const s4b = Y2();
6202
6295
  s4b.start("Waiting for the tunnel to start routing");
6203
6296
  const ready = await waitForTunnelReady(tunnel.url);
6204
- s4b.stop(ready ? "Tunnel is routing" : "Tunnel is slow to come online \u2014 continuing anyway");
6297
+ webhookReady = ready;
6298
+ s4b.stop(ready ? "Tunnel is routing" : "Tunnel is slow to come online");
6205
6299
  if (!ready) {
6206
6300
  M2.warn(
6207
- "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."
6301
+ "The tunnel edge has not answered yet. This is a Cloudflare warmup delay,\nnot your server, which is running on :3001. The wizard will wait once more\nbefore the conformance checks so a cold tunnel does not read as a failure."
6208
6302
  );
6209
6303
  }
6210
6304
  } catch (err) {
@@ -6239,16 +6333,28 @@ Continuing will overwrite it.`
6239
6333
  });
6240
6334
  }
6241
6335
  if (webhookUrl) {
6242
- M2.info("");
6243
- const s6 = Y2();
6244
- s6.start("Running conformance checks");
6245
- await new Promise((r2) => setTimeout(r2, 200));
6246
- s6.stop("Conformance checks:");
6247
- await runConformance(webhookUrl, pid, secret);
6248
- if (verifiesAccountReference) {
6249
- M2.info(
6250
- "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."
6336
+ if (!webhookReady && tunnelBase) {
6337
+ const sr = Y2();
6338
+ sr.start("Waiting for the tunnel to start routing");
6339
+ webhookReady = await waitForTunnelReady(tunnelBase, { budgetMs: 2e4 });
6340
+ sr.stop(webhookReady ? "Tunnel is routing" : "Tunnel still not routing");
6341
+ }
6342
+ if (!webhookReady) {
6343
+ M2.warn(
6344
+ "Skipping the conformance checks: the Cloudflare tunnel edge has not come online.\nThis is a tunnel warmup delay, not your server, which is running on :3001.\nRe-run the wizard, or run `npm test` once the tunnel URL responds in a browser."
6251
6345
  );
6346
+ } else {
6347
+ M2.info("");
6348
+ const s6 = Y2();
6349
+ s6.start("Running conformance checks");
6350
+ await new Promise((r2) => setTimeout(r2, 200));
6351
+ s6.stop("Conformance checks:");
6352
+ await runConformance(webhookUrl, pid, secret);
6353
+ if (verifiesAccountReference) {
6354
+ M2.info(
6355
+ "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."
6356
+ );
6357
+ }
6252
6358
  }
6253
6359
  }
6254
6360
  const summary = [
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@oneaddress/setup",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "Interactive setup wizard for OneAddress partner webhook integrations",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "oneaddress-setup": "dist/index.js"
8
8
  },
9
9
  "files": [
10
- "LICENSE", "dist",
10
+ "LICENSE",
11
+ "dist",
11
12
  "README.md"
12
13
  ],
13
14
  "scripts": {