@oneaddress/setup 2.3.0 → 2.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 +386 -136
  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 ? "2.3.0" : "?";
859
+ var WIZARD_VERSION = true ? "2.4.0" : "?";
860
860
  function printCompactHeader() {
861
861
  const INNER = 42;
862
862
  const TOP = fn("\u250C") + dm("\u2500".repeat(INNER)) + fn("\u2510");
@@ -2136,6 +2136,11 @@ export function noteChange(facts: ChangeFacts): void {
2136
2136
  * \`--headless\` skips both: no prompt (a service has nobody to ask), no screen.
2137
2137
  */
2138
2138
  import { printBanner } from './brand.js';
2139
+ // Config only - NOT the server, the store or the database. Those derive keys on
2140
+ // import, which is the whole reason this file loads them dynamically below,
2141
+ // after the passphrase is resolved. \`config.js\` reads a JSON file and the
2142
+ // environment and touches neither.
2143
+ import { config } from './config.js';
2139
2144
  import { PassphraseRequiredError, WrongPassphraseError } from './vault.js';
2140
2145
 
2141
2146
  /**
@@ -2310,29 +2315,71 @@ async function resolvePassphrase(): Promise<string | null> {
2310
2315
  // Reported by exactly that partner: "there was no opportunity to set a
2311
2316
  // passphrase". Without this line the only options are guess or search the
2312
2317
  // internet, and the answer is one command.
2313
- process.stdout.write('\\n This customer database is encrypted.\\n');
2314
- process.stdout.write(' If you do not know the passphrase, delete data.db and start\\n');
2315
- process.stdout.write(' again: it holds your demo roster and test dispatches, nothing\\n');
2316
- process.stdout.write(' OneAddress needs.\\n\\n');
2317
- const answer = await ask(' Passphrase to unlock: ');
2318
+ process.stdout.write('\\n This customer database is encrypted and needs its password.\\n\\n');
2319
+ process.stdout.write(' It is the one you chose on this receiver. It is NOT your Webhook\\n');
2320
+ process.stdout.write(' signing secret, NOT your ECDH private key, and NOT your\\n');
2321
+ process.stdout.write(' OneAddress sign-in.\\n\\n');
2322
+ process.stdout.write(' If you do not have it, delete data.db and start again: it holds\\n');
2323
+ process.stdout.write(' your demo roster and test dispatches, nothing OneAddress needs.\\n\\n');
2324
+ const answer = await ask(' Password to unlock: ');
2318
2325
  return answer || null;
2319
2326
  }
2320
2327
 
2321
- process.stdout.write('\\n SET A PASSPHRASE to encrypt your customer records at rest.\\n');
2322
- process.stdout.write(' It is YOURS. It is not the OneAddress private key, and OneAddress\\n');
2323
- process.stdout.write(' never sees it and CANNOT RECOVER IT. Lose it and the records in\\n');
2324
- process.stdout.write(' data.db cannot be read again.\\n');
2325
- process.stdout.write(' Press Enter to skip and store records unencrypted.\\n\\n');
2328
+ // SAYS WHAT IS BEING ASKED BEFORE IT WARNS ABOUT IT. The first version opened
2329
+ // with SET A PASSPHRASE and four lines of consequence, which reads as an
2330
+ // instruction to produce a passphrase you are assumed to already have - and
2331
+ // this prompt arrives moments after the partner has pasted two real
2332
+ // credentials, so assuming exactly that is the natural reading. Reported as
2333
+ // confusing, and the confusion was ours.
2334
+ //
2335
+ // It therefore leads with the decision, says plainly that the answer is
2336
+ // INVENTED HERE, and names the three secrets it is not, because those are the
2337
+ // three things a partner at this point in setup might reach for.
2338
+ // NOT ASKED IN INBOX MODE, because the honest answer to "protect your customer
2339
+ // records" is that there are none here: the connector holds them. The
2340
+ // database in this mode carries dispatch ciphertext - already encrypted to a
2341
+ // key this process does not have - and a queue of dispatch ids. Asking anyway
2342
+ // is the confusing-prompt problem twice over: a question about data that is
2343
+ // absent, put to somebody who has just been told this receiver cannot read
2344
+ // anything.
2345
+ //
2346
+ // The UNLOCK branch above is deliberately NOT skipped. A database encrypted
2347
+ // while the receiver ran write-through is still encrypted after a switch to
2348
+ // inbox, and skipping the question there would refuse to open a file whose
2349
+ // owner has the password.
2350
+ if (config.mode === 'inbox') {
2351
+ process.stdout.write('\\n No password needed: in inbox mode this receiver holds no customer records.\\n');
2352
+ process.stdout.write(' Your connector holds them, and the key.\\n\\n');
2353
+ return null;
2354
+ }
2355
+
2356
+ process.stdout.write('\\n \u2500\u2500 Protect this receiver\\'s customer database? \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\\n\\n');
2357
+ process.stdout.write(' MAKE UP A NEW PASSWORD now. You are NOT being asked for anything\\n');
2358
+ process.stdout.write(' you already have: not your Webhook signing secret, not your ECDH\\n');
2359
+ process.stdout.write(' private key, not your OneAddress sign-in. This one is invented\\n');
2360
+ process.stdout.write(' here and used only on this machine.\\n\\n');
2361
+ process.stdout.write(' It encrypts the customer records this receiver keeps in data.db.\\n');
2362
+ process.stdout.write(' OneAddress never sees it and cannot reset it, so if you lose it\\n');
2363
+ process.stdout.write(' those records cannot be read again.\\n\\n');
2364
+ process.stdout.write(' OPTIONAL. Press Enter to skip and leave the database readable.\\n');
2365
+ // ACCURATE ABOUT "LATER", because the obvious reading is wrong. Setting a
2366
+ // password afterwards works and the receiver reports itself encrypted, but
2367
+ // rows written before it stay in the clear until something rewrites them -
2368
+ // confirmed by finding a seeded customer name in the database file after
2369
+ // exactly that sequence.
2370
+ process.stdout.write(' You can set one later with ONEADDRESS_DB_PASSPHRASE, but records\\n');
2371
+ process.stdout.write(' written before then stay readable until they change.\\n\\n');
2326
2372
 
2327
2373
  for (;;) {
2328
- const first = await ask(' New passphrase: ');
2374
+ const first = await ask(' New password (or Enter to skip): ');
2329
2375
  if (!first) {
2330
- process.stdout.write(' Continuing WITHOUT encryption.\\n\\n');
2376
+ process.stdout.write(' Continuing WITHOUT encryption: anyone who can read data.db can\\n');
2377
+ process.stdout.write(' read your customer records.\\n\\n');
2331
2378
  return null;
2332
2379
  }
2333
- const again = await ask(' Confirm passphrase: ');
2380
+ const again = await ask(' Type it again: ');
2334
2381
  if (first === again) {
2335
- process.stdout.write(' Passphrase set. Keep it somewhere you will still have it.\\n\\n');
2382
+ process.stdout.write(' Password set. Keep it somewhere you will still have it.\\n\\n');
2336
2383
  return first;
2337
2384
  }
2338
2385
  process.stdout.write(' Those do not match. Try again.\\n\\n');
@@ -2471,6 +2518,22 @@ import type {
2471
2518
  * if anything awaited between the set and the read, which is why they are
2472
2519
  * adjacent and why this comment exists.
2473
2520
  */
2521
+ /**
2522
+ * The connector could not be reached, so the question was never answered.
2523
+ *
2524
+ * A THROW RATHER THAN A VERDICT, because every value this function can return
2525
+ * is a statement about the customer's record and none of them means "we could
2526
+ * not ask". The handler turns this into a non-2xx, which the relay already
2527
+ * classifies as \`unreachable\` and shows the consumer as a transient network
2528
+ * problem rather than as a missing account.
2529
+ */
2530
+ export class ConnectorUnreachableError extends Error {
2531
+ constructor(public readonly kind: string) {
2532
+ super(\`the connector could not be reached to answer \${kind}\`);
2533
+ this.name = 'ConnectorUnreachableError';
2534
+ }
2535
+ }
2536
+
2474
2537
  let currentRawBody = '';
2475
2538
 
2476
2539
  export function setCurrentRawBody(raw: string): void {
@@ -2496,14 +2559,26 @@ export const connectorStore = {
2496
2559
  ): Promise<AccountVerdict> {
2497
2560
  const answer = await askConnector('account.verify', currentRawBody);
2498
2561
  if (!answer.reached) {
2499
- // NEVER GUESSED. A fabricated match authorises a stranger's address onto
2500
- // a customer's account. \`no_account\` is the safe answer and stops the
2501
- // consumer before they pay, which is the trade this design accepted.
2562
+ // NEVER GUESSED: a fabricated match authorises a stranger's address onto
2563
+ // a customer's account.
2564
+ //
2565
+ // BUT NOT \`no_account\` EITHER, which is what this returned. That is a
2566
+ // POSITIVE CLAIM - the consumer is shown "\u2717 No account found / This
2567
+ // service couldn't find an account with these details" and goes off to
2568
+ // re-check an account number that was correct all along, or concludes
2569
+ // they have no account with a provider they do.
2570
+ //
2571
+ // The trade was accepted for its safety, and the safety is real. What was
2572
+ // missed is that it is available WITHOUT the false claim: the relay
2573
+ // already classifies a failed check as \`unreachable\` and tells the
2574
+ // consumer "we couldn't reach this service just now". That stops them
2575
+ // before they pay AND is true. Throwing gets us there, because a
2576
+ // non-2xx is exactly what the relay reads as unreachable.
2502
2577
  report.warn(
2503
2578
  \`[connector] account check for \${accountNumber ?? '(none)'} could not be answered; \` +
2504
- 'refusing rather than guessing',
2579
+ 'reporting the service as unreachable rather than claiming no account exists',
2505
2580
  );
2506
- return 'no_account';
2581
+ throw new ConnectorUnreachableError('account.verify');
2507
2582
  }
2508
2583
  const status = verdictOf(answer.body, 'status');
2509
2584
  if (status === 'match' || status === 'no_match' || status === 'no_account') return status;
@@ -2513,7 +2588,9 @@ export const connectorStore = {
2513
2588
 
2514
2589
  async verifyAddress(_customer: Customer, _incoming: Address): Promise<VerifyResult> {
2515
2590
  const answer = await askConnector('address.verify', currentRawBody);
2516
- if (!answer.reached) return 'not_found';
2591
+ // Same reasoning as \`verifyAccount\`: \`not_found\` is a claim about the
2592
+ // customer's record, and "we could not ask" is not that claim.
2593
+ if (!answer.reached) throw new ConnectorUnreachableError('address.verify');
2517
2594
  const result = verdictOf(answer.body, 'result');
2518
2595
  if (result === 'match' || result === 'mismatch' || result === 'not_found') return result;
2519
2596
  report.warn(\`[connector] address.verify answered "\${result ?? '(nothing)'}", which is not a result\`);
@@ -4177,6 +4254,7 @@ export interface CustomerStore {
4177
4254
  import { readFileSync } from 'node:fs';
4178
4255
  import { join } from 'node:path';
4179
4256
  import { report } from './report.js';
4257
+ import { config } from './config.js';
4180
4258
  import db, { accountKey, dec, enc, encrypted, ensureColumn, isEncrypted, once } from './db.js';
4181
4259
  import type {
4182
4260
  AccountVerdict,
@@ -4356,8 +4434,30 @@ function loadRoster(): RosterEntry[] {
4356
4434
  }
4357
4435
  }
4358
4436
 
4359
- const ROSTER = loadRoster();
4360
- {
4437
+ /**
4438
+ * IN INBOX MODE NOTHING IS SEEDED, and that is a privacy fix rather than a
4439
+ * saving.
4440
+ *
4441
+ * This block runs on IMPORT, and \`server.ts\` imports this module in both modes
4442
+ * because it picks the store at runtime. So an inbox receiver - the process that
4443
+ * exists specifically so it cannot read what it holds - was writing a full
4444
+ * customer roster to its own database on startup: names, account numbers and
4445
+ * the address on file for each, in the clear.
4446
+ *
4447
+ * The dispatches were never the leak. Those are stored as the ciphertext they
4448
+ * arrived as, and an end-to-end run confirms the new address appears nowhere in
4449
+ * the receiver's database file. The ROSTER was, and it was there before a
4450
+ * single dispatch arrived. Found by searching the receiver's database bytes for
4451
+ * a customer name after that run, which is the only check that would have found
4452
+ * it: every other one asks the receiver what it thinks it is holding.
4453
+ *
4454
+ * In inbox mode the connector owns the roster and answers every account
4455
+ * question over the loopback channel, so the receiver needs none of it.
4456
+ */
4457
+ const ROSTER = config.mode === 'inbox' ? [] : loadRoster();
4458
+ if (config.mode === 'inbox') {
4459
+ report.info('[store] no roster held: in inbox mode your connector owns customer records');
4460
+ } else {
4361
4461
  const upsert = db.prepare(\`
4362
4462
  INSERT INTO customers (account_key, account_number, name, address)
4363
4463
  VALUES ($account_key, $account_number, $name, $address)
@@ -4623,7 +4723,7 @@ import {
4623
4723
  // exporting a \`CustomerStore\` (see src/customer-store.ts) and nothing else in
4624
4724
  // the protocol layer changes.
4625
4725
  import { store as writeThroughStore } from './store.js';
4626
- import { connectorStore, setCurrentRawBody } from './connector-store.js';
4726
+ import { ConnectorUnreachableError, connectorStore, setCurrentRawBody } from './connector-store.js';
4627
4727
  import { noteChange } from './tui.js';
4628
4728
  import { config } from './config.js';
4629
4729
  import { safeOneAddressCallbackUrl } from './callback-url.js';
@@ -4761,12 +4861,22 @@ report.info(
4761
4861
  // lock out every install that has never set a passphrase - but choosing it by
4762
4862
  // not being asked is not a choice, so the absence is stated as loudly as the
4763
4863
  // presence.
4864
+ //
4865
+ // AND IN INBOX MODE IT SAYS SOMETHING ELSE, for the same reason the keys line
4866
+ // above does. There are no customer records in this mode - the connector holds
4867
+ // them - so "Customer records are readable by anyone who can read the file" was
4868
+ // alarming about data that is not here, and sent the partner to set a passphrase
4869
+ // protecting nothing. \`connectorStore.encrypted\` was already hardcoded false
4870
+ // with a comment saying exactly that; this line had simply never read it.
4764
4871
  report.info(
4765
- store.encrypted
4766
- ? \`[startup] store: \${store.name} (encrypted at rest)\`
4767
- : \`[startup] store: \${store.name} \u2014 NOT ENCRYPTED AT REST. \` +
4768
- 'Customer records are readable by anyone who can read the file. ' +
4769
- 'Set ONEADDRESS_DB_PASSPHRASE, or run \`npm start\` in a terminal to be asked.',
4872
+ config.mode === 'inbox'
4873
+ ? \`[startup] store: \${store.name} \u2014 no customer records here by design. This database holds \` +
4874
+ 'dispatch ciphertext your connector decrypts, and the confirm queue.'
4875
+ : store.encrypted
4876
+ ? \`[startup] store: \${store.name} (encrypted at rest)\`
4877
+ : \`[startup] store: \${store.name} \u2014 NOT ENCRYPTED AT REST. \` +
4878
+ 'Customer records are readable by anyone who can read the file. ' +
4879
+ 'Set ONEADDRESS_DB_PASSPHRASE, or run \`npm start\` in a terminal to be asked.',
4770
4880
  );
4771
4881
 
4772
4882
  // In-memory dedup cache. Records a dispatch id only once it has been fully
@@ -4806,11 +4916,17 @@ async function confirmToOneAddress(dispatchId: number, status: ConfirmStatus): P
4806
4916
  // and it drops non-numeric dispatch ids, so by the time a row is drained it
4807
4917
  // is a real dispatch. Keeping a second copy of that rule would mean a probe
4808
4918
  // could be queued forever and silently skipped on every drain.
4919
+ // REPORTED, not "Applied". The note is fixed text sent with EVERY confirm,
4920
+ // including \`failed\` ones - so a dispatch the connector could not open, or
4921
+ // one refused because the account reference matched nobody, arrived at
4922
+ // OneAddress reading "Applied by the OneAddress webhook receiver". That is
4923
+ // read by whoever is working out what happened, and it said the opposite of
4924
+ // \`status\` sitting beside it.
4809
4925
  const bodyStr = JSON.stringify({
4810
4926
  dispatch_id: dispatchId,
4811
4927
  partner_id: PARTNER_ID,
4812
4928
  status,
4813
- note: 'Applied by the OneAddress webhook receiver',
4929
+ note: 'Reported by the OneAddress webhook receiver',
4814
4930
  });
4815
4931
  const ts = String(Math.floor(Date.now() / 1000));
4816
4932
  const sig = createHmac('sha256', CONFIRM_SECRET).update(\`\${ts}.\${bodyStr}\`).digest('hex');
@@ -4961,7 +5077,19 @@ app.post('/webhook', async (req: Request, res: Response) => {
4961
5077
  // and the customer records, so it answers. \`connectorStore\` sends the raw
4962
5078
  // body; the arguments below are the contract's shape, not its source.
4963
5079
  if (config.mode === 'inbox') {
4964
- const status = await store.verifyAccount(null, '', []);
5080
+ let status: Awaited<ReturnType<typeof store.verifyAccount>>;
5081
+ try {
5082
+ status = await store.verifyAccount(null, '', []);
5083
+ } catch (err) {
5084
+ // 503 RATHER THAN A VERDICT. Every verdict is a claim about the
5085
+ // customer's record; "the connector is down" is not one of them. A
5086
+ // non-2xx is what OneAddress reads as \`unreachable\`, which the consumer
5087
+ // sees as a transient network problem instead of "no account found".
5088
+ if (err instanceof ConnectorUnreachableError) {
5089
+ return res.status(503).json({ ok: false, error: 'connector_unreachable' });
5090
+ }
5091
+ throw err;
5092
+ }
4965
5093
  report.info(\`[webhook] account.verify \u2192 \${status} (answered by the connector)\`);
4966
5094
  return res.status(200).json({ status });
4967
5095
  }
@@ -5090,7 +5218,17 @@ app.post('/webhook', async (req: Request, res: Response) => {
5090
5218
  if (config.mode === 'inbox' && event === 'address.verify') {
5091
5219
  // The contract's arguments, not its source: \`connectorStore\` sends the raw
5092
5220
  // body it was parked with and ignores these.
5093
- const result = await store.verifyAddress({ email: null, name: '' }, {});
5221
+ let result: Awaited<ReturnType<typeof store.verifyAddress>>;
5222
+ try {
5223
+ result = await store.verifyAddress({ email: null, name: '' }, {});
5224
+ } catch (err) {
5225
+ // As above, and the callback is deliberately NOT posted: a verdict posted
5226
+ // here would be a claim about a record nobody read.
5227
+ if (err instanceof ConnectorUnreachableError) {
5228
+ return res.status(503).json({ ok: false, error: 'connector_unreachable' });
5229
+ }
5230
+ throw err;
5231
+ }
5094
5232
  report.info(\`[webhook] address.verify \u2192 \${result} (answered by the connector)\`);
5095
5233
  const refused = await postVerifyVerdict(result);
5096
5234
  if (refused) return refused;
@@ -5554,6 +5692,7 @@ export const PARTNER_NAME = process.env.PARTNER_NAME?.trim() || 'Your receiver';
5554
5692
  import 'dotenv/config';
5555
5693
  import { createPrivateKey, createPublicKey } from 'node:crypto';
5556
5694
  import { spawnSync } from 'node:child_process';
5695
+ import { readFileSync } from 'node:fs';
5557
5696
 
5558
5697
  const secret = process.env.WEBHOOK_SECRET ?? '';
5559
5698
  const partnerId = process.env.PARTNER_ID ?? '%%PARTNER_ID%%';
@@ -5561,21 +5700,73 @@ const keyPem = (process.env.PARTNER_PRIVATE_KEY_PEM ?? '').replace(/\\\\n/g,
5561
5700
  const port = process.env.PORT ?? '3001';
5562
5701
  const targetUrl = process.argv[2] ?? \`http://localhost:\${port}/webhook\`;
5563
5702
 
5564
- if (!secret || !partnerId || !keyPem) {
5565
- console.error('[test] Missing required env vars. Check your .env file.');
5703
+ /**
5704
+ * Which shape of receiver is this?
5705
+ *
5706
+ * Read from the same config file the server reads, so the two cannot disagree.
5707
+ * An inbox receiver holds NO private key - that is the property it exists for -
5708
+ * so this runner used to die on its own env gate with "Missing required env
5709
+ * vars. Check your .env file", telling the partner to fix a file that was
5710
+ * exactly as the wizard wrote it. It was never run in that mode.
5711
+ */
5712
+ let receiverMode = 'write-through';
5713
+ try {
5714
+ const cfg = JSON.parse(readFileSync(new URL('../oneaddress.config.json', import.meta.url), 'utf8'));
5715
+ if (cfg && typeof cfg.mode === 'string') receiverMode = cfg.mode;
5716
+ } catch {
5717
+ // No config, or unreadable: treat it as the default, which is what every
5718
+ // scaffold before the mode existed was.
5719
+ }
5720
+ const isInbox = receiverMode === 'inbox';
5721
+
5722
+ if (!secret || !partnerId) {
5723
+ console.error('[test] Missing WEBHOOK_SECRET or PARTNER_ID. Check your .env file.');
5566
5724
  process.exit(1);
5567
5725
  }
5568
5726
 
5569
- let publicKeyB64: string;
5570
- try {
5571
- const priv = createPrivateKey({ key: keyPem, format: 'pem' });
5572
- const pub = createPublicKey(priv);
5573
- publicKeyB64 = pub.export({ type: 'spki', format: 'der' }).toString('base64');
5574
- } catch (err) {
5575
- console.error('[test] Failed to derive public key from PARTNER_PRIVATE_KEY_PEM:', err);
5727
+ /**
5728
+ * The public key the conformance suite encrypts to.
5729
+ *
5730
+ * Write-through derives it from the private key it already holds. Inbox mode
5731
+ * has no private key to derive from, so it takes the PUBLIC half directly:
5732
+ * a public key is not a secret, and keeping one here does not give this process
5733
+ * the ability to read anything.
5734
+ *
5735
+ * Copy it from partners.oneaddress.io -> My Profile.
5736
+ */
5737
+ const publicKeyFromEnv = (process.env.PARTNER_PUBLIC_KEY_B64 ?? '').trim();
5738
+
5739
+ if (isInbox && !publicKeyFromEnv) {
5740
+ console.log('[test] Inbox mode: this receiver holds no private key, so the encrypted checks');
5741
+ console.log('[test] cannot be built here. Two ways to run them:');
5742
+ console.log('[test]');
5743
+ console.log('[test] 1. Set PARTNER_PUBLIC_KEY_B64 in .env (copy it from My Profile in the');
5744
+ console.log('[test] portal). It is a public value and safe to keep on this machine.');
5745
+ console.log('[test] 2. Or run conformance from your connector host, which has the key.');
5746
+ console.log('[test]');
5747
+ console.log('[test] Signing and replay are covered by the wizard\\'s own checks either way.');
5748
+ process.exit(0);
5749
+ }
5750
+
5751
+ if (!isInbox && !keyPem) {
5752
+ console.error('[test] Missing PARTNER_PRIVATE_KEY_PEM. Check your .env file.');
5576
5753
  process.exit(1);
5577
5754
  }
5578
5755
 
5756
+ let publicKeyB64: string;
5757
+ if (publicKeyFromEnv) {
5758
+ publicKeyB64 = publicKeyFromEnv;
5759
+ } else {
5760
+ try {
5761
+ const priv = createPrivateKey({ key: keyPem, format: 'pem' });
5762
+ const pub = createPublicKey(priv);
5763
+ publicKeyB64 = pub.export({ type: 'spki', format: 'der' }).toString('base64');
5764
+ } catch (err) {
5765
+ console.error('[test] Failed to derive public key from PARTNER_PRIVATE_KEY_PEM:', err);
5766
+ process.exit(1);
5767
+ }
5768
+ }
5769
+
5579
5770
  console.log(\`[test] Running conformance against \${targetUrl}\\n\`);
5580
5771
 
5581
5772
  /**
@@ -5596,9 +5787,14 @@ console.log(\`[test] Running conformance against \${targetUrl}\\n\`);
5596
5787
  * the check would be asking a question your code is not the one answering.
5597
5788
  */
5598
5789
  const verifiesAccountReference =
5599
- process.env.OA_VERIFIES_ACCOUNT_REFERENCE === '1' ||
5600
- process.env.OA_VERIFIES_ACCOUNT_REFERENCE === 'true' ||
5601
- %%VERIFIES_ACCOUNT_REFERENCE%%;
5790
+ // NEVER in inbox mode. The receiver cannot read an account reference there,
5791
+ // so check-14 asks it a question only the connector can answer, and a
5792
+ // correctly built system fails a check it was never the subject of.
5793
+ !isInbox && (
5794
+ process.env.OA_VERIFIES_ACCOUNT_REFERENCE === '1' ||
5795
+ process.env.OA_VERIFIES_ACCOUNT_REFERENCE === 'true' ||
5796
+ %%VERIFIES_ACCOUNT_REFERENCE%%
5797
+ );
5602
5798
 
5603
5799
  const result = spawnSync(
5604
5800
  'npx',
@@ -6310,7 +6506,7 @@ async def _confirm_to_oneaddress(dispatch: str, status: str) -> None:
6310
6506
  "dispatch_id": dispatch_id,
6311
6507
  "partner_id": PARTNER_ID,
6312
6508
  "status": status,
6313
- "note": "Applied by the OneAddress webhook receiver",
6509
+ "note": "Reported by the OneAddress webhook receiver",
6314
6510
  })
6315
6511
  ts = str(int(time.time()))
6316
6512
  sig = hmac_lib.new(CONFIRM_SECRET.encode(), f"{ts}.{body_str}".encode(), hashlib.sha256).hexdigest()
@@ -7325,7 +7521,7 @@ public class OneAddressWebhookController {
7325
7521
  confirmBody.put("dispatch_id", dispatchId);
7326
7522
  confirmBody.put("partner_id", partnerId);
7327
7523
  confirmBody.put("status", status);
7328
- confirmBody.put("note", "Applied by the OneAddress webhook receiver");
7524
+ confirmBody.put("note", "Reported by the OneAddress webhook receiver");
7329
7525
  String bodyStr = MAPPER.writeValueAsString(confirmBody);
7330
7526
  String ts = String.valueOf(System.currentTimeMillis() / 1000L);
7331
7527
  String sig = OneAddressVerifier.sign(ts + "." + bodyStr, confirmSecret);
@@ -8371,7 +8567,7 @@ async Task ConfirmToOneAddress(string oneAddressApi, string confirmSecret, strin
8371
8567
  dispatch_id = dispatchId,
8372
8568
  partner_id = pid,
8373
8569
  status,
8374
- note = "Applied by the OneAddress webhook receiver",
8570
+ note = "Reported by the OneAddress webhook receiver",
8375
8571
  });
8376
8572
  var ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
8377
8573
  var sig = Convert.ToHexString(
@@ -9473,7 +9669,7 @@ func confirmToOneAddress(oneAddressAPI, confirmSecret, partnerID, dispatch, stat
9473
9669
  "dispatch_id": dispatchID,
9474
9670
  "partner_id": partnerID,
9475
9671
  "status": status,
9476
- "note": "Applied by the OneAddress webhook receiver",
9672
+ "note": "Reported by the OneAddress webhook receiver",
9477
9673
  })
9478
9674
  bodyStr := string(bodyBytes)
9479
9675
  ts := strconv.FormatInt(time.Now().Unix(), 10)
@@ -10257,7 +10453,7 @@ function oaConfirmToOneAddress(string $oneAddressApi, string $confirmSecret, str
10257
10453
  'dispatch_id' => (int) $dispatch,
10258
10454
  'partner_id' => $partnerId,
10259
10455
  'status' => $status,
10260
- 'note' => 'Applied by the OneAddress webhook receiver',
10456
+ 'note' => 'Reported by the OneAddress webhook receiver',
10261
10457
  ]);
10262
10458
  $ts = (string) time();
10263
10459
  $sig = hash_hmac('sha256', $ts . '.' . $bodyStr, $confirmSecret);
@@ -10735,10 +10931,11 @@ npx @oneaddress/conformance test %%WEBHOOK_URL%%
10735
10931
  };
10736
10932
 
10737
10933
  // src/scaffold.ts
10738
- function applyTokens(content, partnerId, webhookSecret, webhookUrl, privateKey, verifiesAccountReference, oneAddressApi, mode, connectorToken) {
10934
+ function applyTokens(content, partnerId, webhookSecret, webhookUrl, privateKey, verifiesAccountReference, oneAddressApi, mode, connectorToken, port) {
10739
10935
  const filled = content.replaceAll("%%PARTNER_ID%%", partnerId).replaceAll("%%WEBHOOK_SECRET%%", webhookSecret).replaceAll("%%WEBHOOK_URL%%", webhookUrl || "<your-webhook-url>").replaceAll("%%PRIVATE_KEY%%", privateKey || "<paste your PKCS8 PEM private key here>").replaceAll("%%ONEADDRESS_API%%", oneAddressApi || "https://oneaddress.io").replaceAll("%%VERIFIES_ACCOUNT_REFERENCE%%", verifiesAccountReference ? "true" : "false").replaceAll("%%RECEIVER_MODE%%", mode === "inbox" ? "inbox" : "write-through");
10740
- if (mode !== "inbox") return filled;
10741
- const withReadme = filled.replace(
10936
+ const withPort = applyPort(filled, port);
10937
+ if (mode !== "inbox") return withPort;
10938
+ const withReadme = withPort.replace(
10742
10939
  "## Architecture",
10743
10940
  `## Inbox mode
10744
10941
 
@@ -10773,11 +10970,17 @@ hand somebody your customers' addresses.
10773
10970
  CONNECTOR_TOKEN=${connectorToken || generateConnectorToken()}`
10774
10971
  );
10775
10972
  }
10973
+ function applyPort(content, port) {
10974
+ if (!Number.isInteger(port) || port === DEFAULT_PORT) return content;
10975
+ return content.replace(/^PORT=\d+$/m, `PORT=${port}`);
10976
+ }
10977
+ var DEFAULT_PORT = 3001;
10978
+ var RESERVED_PORTS = [3002, 3003];
10776
10979
  function generateConnectorToken() {
10777
10980
  return (0, import_node_crypto.randomBytes)(24).toString("hex");
10778
10981
  }
10779
10982
  var SENSITIVE_FILES = /* @__PURE__ */ new Set([".env"]);
10780
- async function scaffold(platform, outputDir, partnerId, webhookSecret, webhookUrl, privateKey = "", verifiesAccountReference = false, oneAddressApi = "https://oneaddress.io", mode = "write-through", connectorToken = "") {
10983
+ async function scaffold(platform, outputDir, partnerId, webhookSecret, webhookUrl, privateKey = "", verifiesAccountReference = false, oneAddressApi = "https://oneaddress.io", mode = "write-through", connectorToken = "", port = DEFAULT_PORT) {
10781
10984
  const templates = TEMPLATES[platform];
10782
10985
  if (!templates) throw new Error(`Unknown platform: ${platform}`);
10783
10986
  const written = [];
@@ -10787,7 +10990,7 @@ async function scaffold(platform, outputDir, partnerId, webhookSecret, webhookUr
10787
10990
  if (!(0, import_node_fs.existsSync)(dir)) {
10788
10991
  await (0, import_promises.mkdir)(dir, { recursive: true });
10789
10992
  }
10790
- const filled = applyTokens(content, partnerId, webhookSecret, webhookUrl, privateKey, verifiesAccountReference, oneAddressApi, mode, connectorToken);
10993
+ const filled = applyTokens(content, partnerId, webhookSecret, webhookUrl, privateKey, verifiesAccountReference, oneAddressApi, mode, connectorToken, port);
10791
10994
  const isSensitive = SENSITIVE_FILES.has((0, import_node_path.basename)(name));
10792
10995
  await (0, import_promises.writeFile)(dest, filled, { encoding: "utf8", mode: isSensitive ? 384 : 420 });
10793
10996
  if (isSensitive && process.platform !== "win32") {
@@ -10803,7 +11006,7 @@ async function scaffold(platform, outputDir, partnerId, webhookSecret, webhookUr
10803
11006
 
10804
11007
  // src/register.ts
10805
11008
  var import_node_crypto2 = require("crypto");
10806
- var PKG_VERSION = true ? "2.3.0" : "dev";
11009
+ var PKG_VERSION = true ? "2.4.0" : "dev";
10807
11010
  var REGISTER_URL = "https://partners.oneaddress.io/api/partner/installs";
10808
11011
  function hmacSha256(secret, message) {
10809
11012
  return (0, import_node_crypto2.createHmac)("sha256", secret).update(message).digest("hex");
@@ -11054,15 +11257,17 @@ Timed out after 5 minutes.`.trim(),
11054
11257
 
11055
11258
  // src/autostart.ts
11056
11259
  var import_node_child_process2 = require("child_process");
11057
- var COMMANDS2 = {
11058
- "ts-node": { cmd: "npx", args: ["tsx", "src/server.ts"] },
11059
- "python": { cmd: "uvicorn", args: ["app:app", "--port", "3001"] },
11060
- "go-http": { cmd: "go", args: ["run", "."] },
11061
- "php-laravel": { cmd: "php", args: ["artisan", "serve", "--port=3001"] },
11062
- "csharp-aspnet": { cmd: "dotnet", args: ["run"] },
11063
- "java-spring": { cmd: "mvn", args: ["spring-boot:run"] }
11064
- // no mvnw wrapper is scaffolded
11065
- };
11260
+ function commandsFor(port) {
11261
+ return {
11262
+ "ts-node": { cmd: "npx", args: ["tsx", "src/server.ts"] },
11263
+ "python": { cmd: "uvicorn", args: ["app:app", "--port", String(port)] },
11264
+ "go-http": { cmd: "go", args: ["run", "."] },
11265
+ "php-laravel": { cmd: "php", args: ["artisan", "serve", `--port=${port}`] },
11266
+ "csharp-aspnet": { cmd: "dotnet", args: ["run"] },
11267
+ "java-spring": { cmd: "mvn", args: ["spring-boot:run"] }
11268
+ // no mvnw wrapper is scaffolded
11269
+ };
11270
+ }
11066
11271
  var DASHBOARD_COMMANDS = {
11067
11272
  "ts-node": { cmd: "npx", args: ["tsx", "src/index.ts"] }
11068
11273
  };
@@ -11164,13 +11369,13 @@ async function pollHealth(port, timeoutMs) {
11164
11369
  return false;
11165
11370
  }
11166
11371
  async function startServer(platform, outputDir, port = 3001, secrets = {}) {
11167
- const spec = COMMANDS2[platform];
11372
+ const spec = commandsFor(port)[platform];
11168
11373
  if (!spec) {
11169
11374
  return { ok: false, output: `No start command defined for platform: ${platform}`, manualCommand: "" };
11170
11375
  }
11171
11376
  serverOutput = "";
11172
11377
  const manualCommand = `cd ${outputDir} && ${spec.cmd} ${spec.args.join(" ")}`;
11173
- serverProcess = spawnReceiver(spec, outputDir, ["ignore", "pipe", "pipe"], secrets);
11378
+ serverProcess = spawnReceiver(spec, outputDir, ["ignore", "pipe", "pipe"], { ...secrets, PORT: String(port) });
11174
11379
  serverProcess.stdout?.on("data", (d3) => {
11175
11380
  serverOutput += d3.toString();
11176
11381
  });
@@ -11217,7 +11422,7 @@ async function handOverTerminal(platform, outputDir, port, secrets = {}) {
11217
11422
  }
11218
11423
  }
11219
11424
  process.stdin.pause();
11220
- const child = spawnReceiver(spec, outputDir, "inherit", secrets);
11425
+ const child = spawnReceiver(spec, outputDir, "inherit", { ...secrets, PORT: String(port) });
11221
11426
  serverProcess = child;
11222
11427
  return new Promise((resolve2) => {
11223
11428
  child.once("error", (err) => {
@@ -11747,6 +11952,25 @@ function validatePrivateKey(pem) {
11747
11952
  function stripControlChars(line) {
11748
11953
  return line.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/g, "");
11749
11954
  }
11955
+ function validatePortChoice(v2) {
11956
+ const raw = (v2 ?? "").trim();
11957
+ if (!raw) return void 0;
11958
+ if (!/^\d+$/.test(raw)) return `Enter a port number, or leave blank for ${DEFAULT_PORT}`;
11959
+ const n = Number(raw);
11960
+ if (n < 1024 || n > 65535) return "Choose a port between 1024 and 65535";
11961
+ if (RESERVED_PORTS.includes(n)) {
11962
+ return `${n} is used by the connector channel. Choose another port.`;
11963
+ }
11964
+ return void 0;
11965
+ }
11966
+ function childEnvForMode(mode, v2) {
11967
+ return {
11968
+ WEBHOOK_SECRET: v2.secret,
11969
+ PARTNER_ID: v2.partnerId,
11970
+ VERIFIES_ACCOUNT_REFERENCE: String(v2.verifiesAccountReference),
11971
+ ...mode === "inbox" ? { CONNECTOR_TOKEN: v2.connectorToken } : { PARTNER_PRIVATE_KEY_PEM: v2.privateKey }
11972
+ };
11973
+ }
11750
11974
  async function main() {
11751
11975
  printHeader();
11752
11976
  Ie("OneAddress Partner Setup");
@@ -11796,23 +12020,6 @@ async function main() {
11796
12020
  "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."
11797
12021
  );
11798
12022
  }
11799
- const privateKeyRaw = await he({
11800
- message: "Your ECDH Private Key",
11801
- placeholder: "Paste the base64 key body from My Profile, or enter a path to the .pem file",
11802
- validate: (v2) => {
11803
- if (!v2.trim()) return "Private key is required";
11804
- const { pem, error } = normalisePrivateKey(v2);
11805
- if (error) return error;
11806
- return validatePrivateKey(pem);
11807
- }
11808
- });
11809
- assertNotCancelled(privateKeyRaw);
11810
- const normalised = normalisePrivateKey(privateKeyRaw.trim());
11811
- if (normalised.error) {
11812
- xe(`Private key parse failed unexpectedly: ${normalised.error}`);
11813
- process.exit(1);
11814
- }
11815
- const privateKey = normalised.pem;
11816
12023
  const platform = await ve({
11817
12024
  message: "Which platform?",
11818
12025
  options: [
@@ -11829,29 +12036,6 @@ async function main() {
11829
12036
  ]
11830
12037
  });
11831
12038
  assertNotCancelled(platform);
11832
- let outDir;
11833
- for (; ; ) {
11834
- const outputDir = await he({
11835
- message: "Where to write the files?",
11836
- placeholder: "./oneaddress-webhook",
11837
- defaultValue: "./oneaddress-webhook"
11838
- });
11839
- assertNotCancelled(outputDir);
11840
- outDir = outputDir.trim() || "./oneaddress-webhook";
11841
- if (!(0, import_node_fs3.existsSync)(outDir) || (0, import_node_fs3.readdirSync)(outDir).length === 0) break;
11842
- const overwrite = await ye({
11843
- message: `${outDir} already has files \u2014 overwrite?`,
11844
- initialValue: false
11845
- });
11846
- assertNotCancelled(overwrite);
11847
- if (overwrite) break;
11848
- M2.info(`Keeping the files in ${outDir}. Enter a different directory to write into.`);
11849
- }
11850
- let accountRefReason = "";
11851
- const accountRefDeclaration = await getVerifiesAccountReference(pid, secret, (r2) => {
11852
- accountRefReason = r2;
11853
- });
11854
- const verifiesAccountReference = accountRefDeclaration === true;
11855
12039
  const modeChoice = platform !== "ts-node" ? "write-through" : await ve({
11856
12040
  message: "How should this receiver handle updates?",
11857
12041
  options: [
@@ -11885,6 +12069,69 @@ not your webhook secret and not your confirm secret, because those are shared wi
11885
12069
  OneAddress and a leak of either must not also hand somebody your customers' addresses.`
11886
12070
  );
11887
12071
  }
12072
+ let privateKey = "";
12073
+ if (receiverMode === "inbox") {
12074
+ M2.info(
12075
+ "No private key is collected for an inbox receiver. It belongs in your connector,\nwhich is the only process that decrypts. This one is meant never to hold one, and\nit refuses to start if it finds one in its environment."
12076
+ );
12077
+ } else {
12078
+ const privateKeyRaw = await he({
12079
+ message: "Your ECDH Private Key",
12080
+ placeholder: "Paste the base64 key body from My Profile, or enter a path to the .pem file",
12081
+ validate: (v2) => {
12082
+ if (!v2.trim()) return "Private key is required";
12083
+ const { pem, error } = normalisePrivateKey(v2);
12084
+ if (error) return error;
12085
+ return validatePrivateKey(pem);
12086
+ }
12087
+ });
12088
+ assertNotCancelled(privateKeyRaw);
12089
+ const normalised = normalisePrivateKey(privateKeyRaw.trim());
12090
+ if (normalised.error) {
12091
+ xe(`Private key parse failed unexpectedly: ${normalised.error}`);
12092
+ process.exit(1);
12093
+ }
12094
+ privateKey = normalised.pem;
12095
+ }
12096
+ const portAnswer = await he({
12097
+ message: "Which port should the receiver listen on?",
12098
+ placeholder: String(DEFAULT_PORT),
12099
+ defaultValue: String(DEFAULT_PORT),
12100
+ initialValue: String(DEFAULT_PORT),
12101
+ validate: validatePortChoice
12102
+ });
12103
+ assertNotCancelled(portAnswer);
12104
+ const chosenPort = Number(String(portAnswer).trim() || DEFAULT_PORT);
12105
+ if (!await isPortFree(chosenPort)) {
12106
+ M2.warn(
12107
+ `Port ${chosenPort} looks busy. Free it (lsof -i :${chosenPort} on macOS or Linux,
12108
+ netstat -ano | findstr :${chosenPort} on Windows), or change PORT in the generated
12109
+ .env afterwards. Setup will carry on either way.`
12110
+ );
12111
+ }
12112
+ let outDir;
12113
+ for (; ; ) {
12114
+ const outputDir = await he({
12115
+ message: "Where to write the files?",
12116
+ placeholder: "./oneaddress-webhook",
12117
+ defaultValue: "./oneaddress-webhook"
12118
+ });
12119
+ assertNotCancelled(outputDir);
12120
+ outDir = outputDir.trim() || "./oneaddress-webhook";
12121
+ if (!(0, import_node_fs3.existsSync)(outDir) || (0, import_node_fs3.readdirSync)(outDir).length === 0) break;
12122
+ const overwrite = await ye({
12123
+ message: `${outDir} already has files \u2014 overwrite?`,
12124
+ initialValue: false
12125
+ });
12126
+ assertNotCancelled(overwrite);
12127
+ if (overwrite) break;
12128
+ M2.info(`Keeping the files in ${outDir}. Enter a different directory to write into.`);
12129
+ }
12130
+ let accountRefReason = "";
12131
+ const accountRefDeclaration = await getVerifiesAccountReference(pid, secret, (r2) => {
12132
+ accountRefReason = r2;
12133
+ });
12134
+ const verifiesAccountReference = accountRefDeclaration === true;
11888
12135
  if (accountRefDeclaration === void 0) {
11889
12136
  M2.warn(
11890
12137
  "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."
@@ -11905,7 +12152,8 @@ OneAddress and a leak of either must not also hand somebody your customers' addr
11905
12152
  verifiesAccountReference,
11906
12153
  void 0,
11907
12154
  receiverMode,
11908
- connectorToken
12155
+ connectorToken,
12156
+ chosenPort
11909
12157
  );
11910
12158
  s1.stop(`Scaffolded ${written.length} files in ${outDir}`);
11911
12159
  for (const f of written) M2.success(` ${f}`);
@@ -11942,22 +12190,18 @@ OneAddress and a leak of either must not also hand somebody your customers' addr
11942
12190
  }
11943
12191
  if (choice === "skip") break;
11944
12192
  }
11945
- const SERVER_PORT = 3001;
11946
- const portFree = await isPortFree(SERVER_PORT);
11947
- if (!portFree) {
11948
- M2.warn(
11949
- `Port ${SERVER_PORT} is in use. Free it (lsof -i :${SERVER_PORT} on macOS/Linux, netstat -ano | findstr :${SERVER_PORT} on Windows) and re-run the wizard, or change the PORT in ${outDir}/.env after setup completes.`
11950
- );
11951
- }
12193
+ const childSecrets = childEnvForMode(receiverMode, {
12194
+ secret,
12195
+ partnerId: pid,
12196
+ verifiesAccountReference,
12197
+ privateKey,
12198
+ connectorToken
12199
+ });
12200
+ const SERVER_PORT = chosenPort;
11952
12201
  const s3 = Y2();
11953
12202
  s3.start(`Starting server (waiting up to 15 s for /health on :${SERVER_PORT})`);
11954
12203
  onCleanup(stopServer);
11955
- const start = await startServer(platform, outDir, SERVER_PORT, {
11956
- WEBHOOK_SECRET: secret,
11957
- PARTNER_ID: pid,
11958
- PARTNER_PRIVATE_KEY_PEM: privateKey,
11959
- VERIFIES_ACCOUNT_REFERENCE: String(verifiesAccountReference)
11960
- });
12204
+ const start = await startServer(platform, outDir, SERVER_PORT, childSecrets);
11961
12205
  let serverRunning = false;
11962
12206
  if (start.ok) {
11963
12207
  s3.stop(`Server is healthy on port ${SERVER_PORT}`);
@@ -12031,7 +12275,7 @@ Continuing will overwrite it.`
12031
12275
  s4.start("Starting Cloudflare Tunnel");
12032
12276
  onCleanup(stopTunnel);
12033
12277
  try {
12034
- let tunnel = await startTunnel(3001);
12278
+ let tunnel = await startTunnel(SERVER_PORT);
12035
12279
  webhookUrl = `${tunnel.url}/webhook`;
12036
12280
  tunnelBase = tunnel.url;
12037
12281
  s4.stop(`Tunnel active: ${tunnel.url}`);
@@ -12044,7 +12288,7 @@ Continuing will overwrite it.`
12044
12288
  if (!webhookReady) {
12045
12289
  s4b.stop("First tunnel is slow to route, trying a fresh one");
12046
12290
  stopTunnel();
12047
- tunnel = await startTunnel(3001);
12291
+ tunnel = await startTunnel(SERVER_PORT);
12048
12292
  webhookUrl = `${tunnel.url}/webhook`;
12049
12293
  tunnelBase = tunnel.url;
12050
12294
  const s4c = Y2();
@@ -12057,7 +12301,7 @@ Continuing will overwrite it.`
12057
12301
  if (!webhookReady) {
12058
12302
  M2.warn(
12059
12303
  `The tunnel edge is still not routing after a retry. This is almost always
12060
- one of two things, and neither is your server (it is running on :3001):
12304
+ one of two things, and neither is your server (it is running on :${SERVER_PORT}):
12061
12305
  1. Open ${tunnel.url}/health in a browser. If it never loads, your network
12062
12306
  is blocking cloudflared and no wait will fix it, so try a different network.
12063
12307
  2. The tunnel only lives while this wizard runs, so keep this window open.`
@@ -12067,7 +12311,8 @@ one of two things, and neither is your server (it is running on :3001):
12067
12311
  s4.stop("Tunnel setup failed");
12068
12312
  M2.warn(`Could not start tunnel: ${err instanceof Error ? err.message : String(err)}`);
12069
12313
  M2.warn(
12070
- "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."
12314
+ `To get a public URL manually, try: npx cloudflared tunnel --url http://localhost:${SERVER_PORT}
12315
+ Or deploy to any server and set the URL in partners.oneaddress.io \u2192 My Profile.`
12071
12316
  );
12072
12317
  }
12073
12318
  }
@@ -12105,7 +12350,7 @@ one of two things, and neither is your server (it is running on :3001):
12105
12350
  if (!webhookReady) {
12106
12351
  M2.warn(
12107
12352
  `Skipping the conformance checks: the tunnel edge is not routing.
12108
- Your server is fine, it is running on :3001. This is either your network
12353
+ Your server is fine, it is running on :${SERVER_PORT}. This is either your network
12109
12354
  blocking cloudflared (open ${tunnelBase}/health in a browser to check) or the
12110
12355
  tunnel needing this wizard to stay open. Re-run once that URL loads in a browser.`
12111
12356
  );
@@ -12116,18 +12361,28 @@ tunnel needing this wizard to stay open. Re-run once that URL loads in a browser
12116
12361
  await new Promise((r2) => setTimeout(r2, 200));
12117
12362
  s6.stop("Conformance checks:");
12118
12363
  await runConformance(webhookUrl, pid, secret);
12119
- await runDecryptCheck(secret, pid);
12120
- if (verifiesAccountReference) {
12364
+ if (receiverMode === "inbox") {
12365
+ M2.info(
12366
+ "Decrypt check skipped: this receiver holds no private key, which is what inbox\nmode is for. Your connector is the process that decrypts, so prove the key there:\nstart it against this receiver and confirm it draws and applies a dispatch."
12367
+ );
12368
+ } else {
12369
+ await runDecryptCheck(secret, pid);
12370
+ }
12371
+ if (verifiesAccountReference && receiverMode !== "inbox") {
12121
12372
  M2.info(
12122
12373
  "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."
12123
12374
  );
12375
+ } else if (verifiesAccountReference) {
12376
+ M2.info(
12377
+ "You have declared that your receiver matches the account reference itself.\nIn inbox mode your CONNECTOR does that, before it applies anything, and it\nrefuses a dispatch whose reference matches none of your records. The three checks\nabove cover signing and replay only."
12378
+ );
12124
12379
  }
12125
12380
  }
12126
12381
  }
12127
12382
  const summary = [
12128
12383
  `Partner ID: ${pid}`,
12129
12384
  `Webhook URL: ${webhookUrl || "(not set \u2014 update in portal)"}`,
12130
- `Server port: 3001`,
12385
+ `Server port: ${SERVER_PORT}`,
12131
12386
  `Files in: ${outDir}`
12132
12387
  ].join("\n");
12133
12388
  const nextSteps = [
@@ -12142,12 +12397,7 @@ tunnel needing this wizard to stay open. Re-run once that URL loads in a browser
12142
12397
  if (serverRunning) {
12143
12398
  const openDashboard = platformHasDashboard(platform) ? await ye({ message: "Open the receiver dashboard now?", initialValue: true }) : false;
12144
12399
  if (openDashboard === true) {
12145
- const handover = await handOverTerminal(platform, outDir, SERVER_PORT, {
12146
- WEBHOOK_SECRET: secret,
12147
- PARTNER_ID: pid,
12148
- PARTNER_PRIVATE_KEY_PEM: privateKey,
12149
- VERIFIES_ACCOUNT_REFERENCE: String(verifiesAccountReference)
12150
- });
12400
+ const handover = await handOverTerminal(platform, outDir, SERVER_PORT, childSecrets);
12151
12401
  if (!handover.ok) {
12152
12402
  M2.warn(
12153
12403
  `${handover.reason ?? "The dashboard could not start."}
@@ -12161,7 +12411,7 @@ Your receiver is set up and working. Start it yourself with:
12161
12411
  console.log("");
12162
12412
  console.log(` ${DIM2}\u250C${"\u2500".repeat(BOX_WIDTH)}\u2510${R3}`);
12163
12413
  for (const inner of [
12164
- ` ${GRN}\u25C8${R3} ${CRM2}Server live${R3} ${MID2}on http://localhost:3001/webhook${R3}`,
12414
+ ` ${GRN}\u25C8${R3} ${CRM2}Server live${R3} ${MID2}on http://localhost:${SERVER_PORT}/webhook${R3}`,
12165
12415
  ` ${AMB2}\u25C8${R3} ${MID2}Edit ${CRM2}src/store.ts${R3} ${MID2}to wire your database${R3}`,
12166
12416
  ` ${MID2}Press Ctrl+C to stop the server and exit.${R3}`,
12167
12417
  ...platformHasDashboard(platform) ? [` ${MID2}Run ${CRM2}npm start${R3} ${MID2}here any time for the dashboard.${R3}`] : []
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oneaddress/setup",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Interactive setup wizard for OneAddress partner webhook integrations",
5
5
  "main": "dist/index.js",
6
6
  "bin": {