@oneaddress/setup 2.8.0 → 2.10.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 +279 -15
  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.8.0" : "?";
859
+ var WIZARD_VERSION = true ? "2.10.0" : "?";
860
860
  function printCompactHeader() {
861
861
  const INNER = 42;
862
862
  const TOP = fn("\u250C") + dm("\u2500".repeat(INNER)) + fn("\u2510");
@@ -984,6 +984,7 @@ data.db-shm
984
984
  "build": "tsup src/index.ts --format esm --no-dts --outDir dist",
985
985
  "type-check": "tsc --noEmit",
986
986
  "show": "tsx scripts/show.ts",
987
+ "audit": "tsx scripts/audit.ts",
987
988
  "test": "tsx scripts/test.ts"
988
989
  },
989
990
  "dependencies": {
@@ -1022,6 +1023,7 @@ data.db-shm
1022
1023
  name: "customers.json",
1023
1024
  content: `{
1024
1025
  "//": "YOUR CUSTOMER ROSTER \u2014 edit this file to your customers (account number, name, and the address you hold on file today). src/store.ts seeds from here on startup; point loadRoster at your real customer database when you outgrow the file. Override the path with the ONEADDRESS_CUSTOMERS env var.",
1026
+ "//plaintext": "THIS FILE IS PLAINTEXT AND IS NOT ENCRYPTED. It is a seed you author and control, so treat it like any other file of customer data in your repo: keep it out of version control if it holds real people. The receiver's ENCRYPTED/UNENCRYPTED banner refers to its own database (records at rest, under ONEADDRESS_DB_PASSPHRASE), not to this file. Addresses that ARRIVE from OneAddress land in that database, never here.",
1025
1027
  "customers": [
1026
1028
  { "account_number": "AUR-583920", "name": "Tim Hooper", "address": { "street": "12 Old Mill Road", "suburb": "Parramatta", "state": "NSW", "postcode": "2150" } },
1027
1029
  { "account_number": "AUR-104772", "name": "Olivia Robinson", "address": { "street": "5 Rosewood Lane", "suburb": "Brisbane", "state": "QLD", "postcode": "4000" } },
@@ -1906,7 +1908,14 @@ export function startDashboard({ partnerName, port, onQuit, onReplay, stats }: T
1906
1908
  status.setContent(
1907
1909
  \`{\${AMBER}-fg}{bold}\${esc(partnerName)}{/} \` +
1908
1910
  \`{\${DIM}-fg}listening{/} {\${CREAM}-fg}:\${port}/webhook{/} \` +
1909
- \`{\${DIM}-fg}customer file{/} \${vault} \` +
1911
+ // "customer file" WAS THE WRONG WORDS FOR THE RIGHT VALUE, and it
1912
+ // misread exactly as you would expect. store.encrypted is about the
1913
+ // personal columns of THIS RECEIVER'S DATABASE. The only file a partner
1914
+ // has been told to open is customers.json, which is a plaintext SEED
1915
+ // that src/store.ts inserts from on startup - so a header reading
1916
+ // "customer file ENCRYPTED" above a cleartext customers.json looked
1917
+ // like a false claim about encryption. It was never measuring that file.
1918
+ \`{\${DIM}-fg}records at rest{/} \${vault} \` +
1910
1919
  // A DASH RATHER THAN A ZERO when the store declines to count. Zero is a
1911
1920
  // claim ("you have no customers") and would be a lie on a receiver
1912
1921
  // pointed at a real customer table, where counting every row forty times
@@ -4924,6 +4933,24 @@ db.exec(\`
4924
4933
  -- rather than only what it became.
4925
4934
  prev_address TEXT NOT NULL DEFAULT '{}',
4926
4935
  address TEXT NOT NULL,
4936
+ -- ON WHOSE AUTHORITY. The reference of the signed authorisation that
4937
+ -- produced this change, and the dispatch it arrived under.
4938
+ --
4939
+ -- WHY THEY ARE HERE AND NOT ONLY ON THE DASHBOARD. Both were computed on
4940
+ -- arrival and shown on the LAST CHANGE panel, and nowhere else: a second
4941
+ -- dispatch overwrote the panel and a restart cleared it. So "on whose
4942
+ -- authority did you change this customer's address on the 15th" could not
4943
+ -- be answered from this receiver at all, which is the first question an
4944
+ -- inspection asks about an applied change.
4945
+ --
4946
+ -- NOT ENCRYPTED, deliberately, unlike the addresses beside them. Neither is
4947
+ -- personal information: the reference is a digest of an authorisation and
4948
+ -- the dispatch id is an integer. Encrypting them would make the audit trail
4949
+ -- unreadable without the password in exactly the situation it exists for,
4950
+ -- and would buy nothing, since an attacker who can read this file can read
4951
+ -- the addresses too once they have what they came for.
4952
+ loa_ref TEXT,
4953
+ dispatch_id TEXT,
4927
4954
  recorded_at TEXT NOT NULL DEFAULT (datetime('now'))
4928
4955
  );
4929
4956
  CREATE INDEX IF NOT EXISTS idx_history_account ON address_history(account_key, id DESC);
@@ -4939,6 +4966,11 @@ ensureColumn('customers', 'updated_at', 'TEXT'); // nullable on migrate; set on
4939
4966
  ensureColumn('customers', 'account_key', 'TEXT');
4940
4967
  ensureColumn('address_history', 'account_key', 'TEXT');
4941
4968
  ensureColumn('address_history', 'prev_address', "TEXT NOT NULL DEFAULT '{}'");
4969
+ // NULLABLE WITH NO DEFAULT, because a row written before this column existed
4970
+ // genuinely has no authority recorded and must not claim one. Null reads as
4971
+ // "not recorded", which is the truth; a default would read as a value.
4972
+ ensureColumn('address_history', 'loa_ref', 'TEXT');
4973
+ ensureColumn('address_history', 'dispatch_id', 'TEXT');
4942
4974
 
4943
4975
  // A database written before at-rest encryption existed holds plaintext rows and
4944
4976
  // no account_key. Backfill the key and encrypt in place.
@@ -5187,6 +5219,10 @@ export interface AddressChange {
5187
5219
  name: string;
5188
5220
  prev_address: Address | null;
5189
5221
  address: Address;
5222
+ /** The authorisation this change was made under. Null on an older row. */
5223
+ loa_ref: string | null;
5224
+ /** The dispatch it arrived under. Null on an older row. */
5225
+ dispatch_id: string | null;
5190
5226
  recorded_at: string;
5191
5227
  }
5192
5228
 
@@ -5213,6 +5249,8 @@ export function addressHistory(limit = 20): AddressChange[] {
5213
5249
  const rows = db.prepare(
5214
5250
  \`SELECT h.prev_address AS prev_address,
5215
5251
  h.address AS address,
5252
+ h.loa_ref AS loa_ref,
5253
+ h.dispatch_id AS dispatch_id,
5216
5254
  h.recorded_at AS recorded_at,
5217
5255
  c.account_number AS account_number,
5218
5256
  c.name AS name
@@ -5236,11 +5274,62 @@ export function addressHistory(limit = 20): AddressChange[] {
5236
5274
  name: dec('name', r.name) ?? '(unknown)',
5237
5275
  prev_address: prev,
5238
5276
  address: JSON.parse(dec('address', r.address) ?? '{}') as Address,
5277
+ // NOT decrypted, because they were never encrypted. See the schema.
5278
+ loa_ref: r.loa_ref ?? null,
5279
+ dispatch_id: r.dispatch_id ?? null,
5239
5280
  recorded_at: r.recorded_at ?? '',
5240
5281
  };
5241
5282
  });
5242
5283
  }
5243
5284
 
5285
+ /**
5286
+ * Drop applied-change history older than \`days\`. Returns how many rows went.
5287
+ *
5288
+ * ## Why this exists, and why it is NOT wired to a timer
5289
+ *
5290
+ * The quarantine and the confirm queue both age out on their own, because both
5291
+ * hold work in progress and an unbounded hold is a retention decision made on
5292
+ * a consumer's behalf by nobody. \`address_history\` is different: it is the
5293
+ * AUDIT TRAIL, and the same argument runs the other way. Deleting the record
5294
+ * of a change you applied is not hygiene, it is losing the answer to "on whose
5295
+ * authority", which is the first thing an inspection asks.
5296
+ *
5297
+ * So the default is to keep it, and that default is now a DECISION rather than
5298
+ * an omission - which is the only thing that changed here. Before this
5299
+ * function existed, keeping it forever was simply what happened, and a partner
5300
+ * whose own retention policy required deletion had no lever to pull at all.
5301
+ *
5302
+ * ## If you do call it
5303
+ *
5304
+ * Your retention obligation and your evidentiary one point in opposite
5305
+ * directions and only you know which governs your business. Australian
5306
+ * financial-services records commonly run seven years; APP 11.2 says destroy
5307
+ * or de-identify personal information you no longer need. Those are compatible
5308
+ * only once someone decides what "no longer need" means here, and that someone
5309
+ * is not this file.
5310
+ *
5311
+ * Nothing calls this by default. Wire it to your own schedule, or run it by
5312
+ * hand, deliberately.
5313
+ */
5314
+ export function purgeAddressHistory(days: number): number {
5315
+ if (!Number.isFinite(days) || days <= 0) return 0;
5316
+ // THE COMPARISON HAPPENS INSIDE SQLITE, in SQLite's own datetime format,
5317
+ // for the same reason \`purgeQuarantine\` does it: \`recorded_at\` is written by
5318
+ // the column default \`datetime('now')\`, and comparing that to a JavaScript
5319
+ // ISO string works right up until it does not.
5320
+ const info = db.prepare(
5321
+ \`DELETE FROM address_history WHERE recorded_at < datetime('now', ?)\`,
5322
+ ).run(\`-\${Math.floor(days)} days\`);
5323
+ const n = Number(info.changes ?? 0);
5324
+ if (n > 0) {
5325
+ report.warn(
5326
+ \`[store] purged \${n} applied-change record(s) older than \${Math.floor(days)}d. \` +
5327
+ 'That is audit history and it is gone.',
5328
+ );
5329
+ }
5330
+ return n;
5331
+ }
5332
+
5244
5333
  /** Is the file on disk protected? Surfaced in the dashboard, in both states. */
5245
5334
  export const storeEncrypted = encrypted;
5246
5335
 
@@ -5301,7 +5390,18 @@ export async function verifyAddress(customer: Customer, incoming: Address): Prom
5301
5390
  * ours \u2014 but be aware two customers who share a name share a row. If that is
5302
5391
  * possible in your data, give this a key of your own instead.
5303
5392
  */
5304
- export async function saveAddress(customer: Customer, incoming: Address): Promise<Address> {
5393
+ export async function saveAddress(
5394
+ customer: Customer,
5395
+ incoming: Address,
5396
+ /**
5397
+ * On whose authority, recorded ALONGSIDE the change rather than beside it.
5398
+ *
5399
+ * Optional so an existing caller compiles, and the absence is honest: a row
5400
+ * written without it says "not recorded" rather than claiming an authority
5401
+ * it never had.
5402
+ */
5403
+ provenance?: { loaRef?: string | null; dispatchId?: string | null },
5404
+ ): Promise<Address> {
5305
5405
  const acct = (customer.accountNumber ?? '').trim() || customer.name.trim();
5306
5406
  const addressJson = JSON.stringify(incoming);
5307
5407
  const key = accountKey(acct);
@@ -5329,12 +5429,14 @@ export async function saveAddress(customer: Customer, incoming: Address): Promis
5329
5429
  });
5330
5430
 
5331
5431
  db.prepare(\`
5332
- INSERT INTO address_history (account_key, prev_address, address)
5333
- VALUES ($account_key, $prev_address, $address)
5432
+ INSERT INTO address_history (account_key, prev_address, address, loa_ref, dispatch_id)
5433
+ VALUES ($account_key, $prev_address, $address, $loa_ref, $dispatch_id)
5334
5434
  \`).run({
5335
5435
  account_key: key,
5336
5436
  prev_address: enc('address', JSON.stringify(previous)),
5337
5437
  address: enc('address', addressJson),
5438
+ loa_ref: provenance?.loaRef ?? null,
5439
+ dispatch_id: provenance?.dispatchId ?? null,
5338
5440
  });
5339
5441
 
5340
5442
  // Metadata only \u2014 the address itself is personal information, so the key is
@@ -5386,8 +5488,8 @@ export const store = {
5386
5488
  verifyAddress,
5387
5489
  saveAddress,
5388
5490
  find: findByAccount,
5389
- // Three rows, so a count is free. A store over a real customer table should
5390
- // return null here; the contract file says why.
5491
+ // A demo roster, so a count is free. A store over a real customer table
5492
+ // should return null here; the contract file says why.
5391
5493
  count: customerCount,
5392
5494
  } satisfies CustomerStore;
5393
5495
  `
@@ -6186,7 +6288,10 @@ app.post('/webhook', async (req: Request, res: Response) => {
6186
6288
  // than marking test dispatches as safe to display: a marker is something an
6187
6289
  // attacker can try to forge onto a real dispatch, and a panel that never
6188
6290
  // renders an address has nothing to forge it into.
6189
- const replaced = await store.saveAddress(ctx, address);
6291
+ // PROVENANCE GOES IN WITH THE CHANGE. Both values were already computed
6292
+ // here and went only to the dashboard panel, which one more dispatch or one
6293
+ // restart wiped.
6294
+ const replaced = await store.saveAddress(ctx, address, { loaRef, dispatchId: dispatch || null });
6190
6295
  noteChange({
6191
6296
  accountNumber: ctx.accountNumber || '(no account reference)',
6192
6297
  accountChecked: config.verifiesAccountReference,
@@ -6574,6 +6679,135 @@ export async function ask(prompt: string): Promise<string> {
6574
6679
  stdin.on('data', onData);
6575
6680
  });
6576
6681
  }
6682
+ `
6683
+ },
6684
+ {
6685
+ name: "scripts/audit.ts",
6686
+ content: `/**
6687
+ * Export the applied-change record: what changed, when, and on whose authority.
6688
+ *
6689
+ * Usage:
6690
+ * npm run audit \u2014 every applied change, as CSV
6691
+ * npm run audit -- --json \u2014 the same as JSON
6692
+ * npm run audit -- --from 2026-07-01 --to 2026-09-30
6693
+ * npm run audit -- --out C:\\reports\\q3.csv
6694
+ *
6695
+ * ## Why this exists
6696
+ *
6697
+ * \`npm run show\` answers "did it land", for a person, now. This answers "show
6698
+ * me every address change you applied last quarter and what authorised each
6699
+ * one", for somebody who is not you and is not at your desk. Those are
6700
+ * different questions and the second one is the one an inspection asks.
6701
+ *
6702
+ * ## What is in it, and what deliberately is not
6703
+ *
6704
+ * The account reference, the name, both sides of the change, the timestamp, the
6705
+ * LOA reference and the dispatch id. That is the full record of a change this
6706
+ * receiver applied.
6707
+ *
6708
+ * It is NOT redacted, unlike \`npm run show\`, and that is the point: this file
6709
+ * is the thing you hand to an auditor, and a redacted audit trail is not one.
6710
+ * It therefore contains personal information in cleartext the moment it is
6711
+ * written, which is why it writes where you tell it, says where it went, and
6712
+ * says so out loud.
6713
+ *
6714
+ * ## The one thing it cannot do
6715
+ *
6716
+ * It cannot prove the authority is genuine. \`loa_ref\` is recomputed by THIS
6717
+ * receiver from the LOA it decrypted, so it is evidence this receiver saw a
6718
+ * valid authorisation, not a signature anybody else can re-verify from the CSV.
6719
+ * Verifying it independently needs OneAddress's LOA public key, which is a
6720
+ * different exercise and is not what this file claims to be.
6721
+ */
6722
+ import 'dotenv/config';
6723
+ import { writeFileSync } from 'node:fs';
6724
+ import { resolve } from 'node:path';
6725
+ import { config } from '../src/config.js';
6726
+ import { ask, databaseIsLocked } from '../src/unlock.js';
6727
+
6728
+ function arg(name: string): string | null {
6729
+ const i = process.argv.indexOf(\`--\${name}\`);
6730
+ return i >= 0 && process.argv[i + 1] ? String(process.argv[i + 1]) : null;
6731
+ }
6732
+
6733
+ /** CSV with the quoting rules that stop a comma in a street name shifting every column. */
6734
+ function csvCell(v: unknown): string {
6735
+ const s = String(v ?? '');
6736
+ return /[",\\n]/.test(s) ? \`"\${s.replace(/"/g, '""')}"\` : s;
6737
+ }
6738
+
6739
+ function fmtAddress(a: Record<string, string> | null): string {
6740
+ if (!a) return '';
6741
+ return [a.street, a.suburb, a.state, a.postcode, a.country].filter(Boolean).join(', ');
6742
+ }
6743
+
6744
+ async function askPassword(): Promise<void> {
6745
+ if (process.env.ONEADDRESS_DB_PASSPHRASE?.trim()) return;
6746
+ if (!process.stdin.isTTY) return;
6747
+ if (!(await databaseIsLocked())) return;
6748
+ const answer = await ask(' Password to unlock: ');
6749
+ if (answer) process.env.ONEADDRESS_DB_PASSPHRASE = answer;
6750
+ }
6751
+
6752
+ async function main(): Promise<void> {
6753
+ if (config.mode === 'inbox') {
6754
+ console.log('\\n This receiver is in inbox mode: your connector applies changes and holds the record.');
6755
+ console.log(' Export from there instead.\\n');
6756
+ return;
6757
+ }
6758
+
6759
+ await askPassword();
6760
+ const store = await import('../src/store.js');
6761
+
6762
+ const from = arg('from');
6763
+ const to = arg('to');
6764
+ // EVERY row, then filtered here. The store's reader takes a limit rather than
6765
+ // a date range, and a range that silently truncated at 20 would be worse than
6766
+ // useless in an audit file.
6767
+ const all = store.addressHistory(Number.MAX_SAFE_INTEGER);
6768
+ const rows = all.filter((h) => {
6769
+ const at = String(h.recorded_at ?? '');
6770
+ if (from && at < from) return false;
6771
+ // Inclusive of the whole end day: a --to of 2026-09-30 must include changes
6772
+ // applied at 14:00 that day, and 'YYYY-MM-DD HH:MM:SS' > 'YYYY-MM-DD'.
6773
+ if (to && at > \`\${to} 99\`) return false;
6774
+ return true;
6775
+ });
6776
+
6777
+ const asJson = process.argv.includes('--json');
6778
+ const body = asJson
6779
+ ? JSON.stringify({
6780
+ exported_at: new Date().toISOString(),
6781
+ range: { from: from ?? null, to: to ?? null },
6782
+ note: 'Applied address changes recorded by this receiver. loa_ref is recomputed by this receiver from the authorisation it decrypted; verifying it independently needs the OneAddress LOA public key.',
6783
+ changes: rows,
6784
+ }, null, 2)
6785
+ : [
6786
+ ['recorded_at', 'account_number', 'name', 'previous_address', 'new_address', 'loa_ref', 'dispatch_id']
6787
+ .join(','),
6788
+ ...rows.map((h) => [
6789
+ h.recorded_at, h.account_number, h.name,
6790
+ fmtAddress(h.prev_address as unknown as Record<string, string> | null),
6791
+ fmtAddress(h.address as unknown as Record<string, string>),
6792
+ h.loa_ref ?? '', h.dispatch_id ?? '',
6793
+ ].map(csvCell).join(',')),
6794
+ ].join('\\n');
6795
+
6796
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
6797
+ const target = resolve(arg('out') ?? config.exportDir ?? '', arg('out')
6798
+ ? ''
6799
+ : \`oneaddress-audit-\${stamp}.\${asJson ? 'json' : 'csv'}\`);
6800
+ writeFileSync(target, body);
6801
+
6802
+ console.log(\`\\n Exported \${rows.length} applied change(s) to\\n \${target}\\n\`);
6803
+ console.log(' THIS FILE CONTAINS CUSTOMER ADDRESSES IN CLEARTEXT. It is the audit record,');
6804
+ console.log(' so it is deliberately not redacted. Store and send it accordingly.\\n');
6805
+ }
6806
+
6807
+ void main().catch((err: unknown) => {
6808
+ console.error(err instanceof Error ? err.message : String(err));
6809
+ process.exit(1);
6810
+ });
6577
6811
  `
6578
6812
  },
6579
6813
  {
@@ -6582,9 +6816,11 @@ export async function ask(prompt: string): Promise<string> {
6582
6816
  * Show what this receiver holds: the roster, and every change it has applied.
6583
6817
  *
6584
6818
  * Usage:
6585
- * npm run show \u2014 a LIVE branded view, refreshing as changes land
6586
- * npm run show 100 \u2014 keep 100 changes on screen instead of 20
6587
- * npm run show > f.txt \u2014 plain one-shot text, for a file or a pipe
6819
+ * npm run show \u2014 a LIVE branded view, refreshing as changes land
6820
+ * npm run show 100 \u2014 keep 100 changes on screen instead of 20
6821
+ * npm run show -- --reveal \u2014 show street lines (hidden by default)
6822
+ * npm run show > f.txt \u2014 plain one-shot text, for a file or a pipe
6823
+ * npm run audit \u2014 export the applied-change record (see scripts/audit.ts)
6588
6824
  *
6589
6825
  * ## Why this script exists
6590
6826
  *
@@ -6634,10 +6870,32 @@ const DIM = '#8a7f6a';
6634
6870
  /** Escape blessed's tag syntax so a customer's name can never inject markup. */
6635
6871
  const esc = (s: unknown): string => String(s ?? '').replace(/[{}]/g, '');
6636
6872
 
6873
+ /**
6874
+ * REDACTED BY DEFAULT, revealed on purpose.
6875
+ *
6876
+ * This script's whole job is to show what the receiver holds, so refusing to
6877
+ * print addresses would defeat it. But the default run prints EVERY customer's
6878
+ * address into a terminal, where it lands in scrollback, in a \`| tee\` file and
6879
+ * in any screen recording, and the commonest reason to run it is to check one
6880
+ * record or prove one dispatch landed - neither of which needs the other 23.
6881
+ *
6882
+ * So the default shows enough to recognise a record (suburb, state, postcode)
6883
+ * and withholds the street line, and \`--reveal\` prints the lot. The suburb is
6884
+ * deliberately NOT withheld: an audit row you cannot tell apart from the next
6885
+ * one is not an audit row, and the suburb is what makes a change legible as a
6886
+ * change.
6887
+ */
6888
+ const REVEAL = process.argv.includes('--reveal') || process.env.ONEADDRESS_SHOW_REVEAL === '1';
6889
+
6637
6890
  function fmt(a: Record<string, string> | null): string {
6638
6891
  if (!a) return '(nothing on file)';
6639
- const parts = [a.street, a.suburb, a.state, a.postcode, a.country].filter(Boolean);
6640
- return parts.join(', ') || '(empty)';
6892
+ const rest = [a.suburb, a.state, a.postcode, a.country].filter(Boolean).join(', ');
6893
+ if (REVEAL) {
6894
+ const parts = [a.street, a.suburb, a.state, a.postcode, a.country].filter(Boolean);
6895
+ return parts.join(', ') || '(empty)';
6896
+ }
6897
+ if (!a.street) return rest || '(empty)';
6898
+ return \`[street hidden], \${rest}\` || '(empty)';
6641
6899
  }
6642
6900
 
6643
6901
  async function askPassword(): Promise<void> {
@@ -6684,6 +6942,12 @@ function changeRows(store: Store, limit: number, query: string): string[] {
6684
6942
  out.push(\` {\${DIM}-fg}\${esc(h.recorded_at)}{/} {\${CREAM}-fg}\${esc(h.account_number)}{/} \${esc(h.name)}\`);
6685
6943
  out.push(\` {\${DIM}-fg}was:{/} \${esc(fmt(prev))}\`);
6686
6944
  out.push(\` {\${AMBER}-fg}now:{/} \${esc(fmt(now))}\`);
6945
+ // ON WHOSE AUTHORITY, on its own line. Null means the row predates the
6946
+ // column, which is not the same as a change nobody authorised, so it says
6947
+ // so rather than printing an empty field.
6948
+ out.push(h.loa_ref
6949
+ ? \` {\${DIM}-fg}auth:{/} \${esc(h.loa_ref)}\${h.dispatch_id ? \` {\${DIM}-fg}dispatch{/} \${esc(h.dispatch_id)}\` : ''}\`
6950
+ : \` {\${DIM}-fg}auth: not recorded (applied before this receiver kept it){/}\`);
6687
6951
  }
6688
6952
  return out;
6689
6953
  }
@@ -6781,7 +7045,7 @@ function stream(store: Store, limit: number): void {
6781
7045
  const drawFooter = (): void => {
6782
7046
  footer.setContent(query
6783
7047
  ? \`{\${AMBER}-fg}filter: \${esc(query)}{/} {\${CREAM}-fg}[/]{/} change {\${CREAM}-fg}[esc]{/} clear {\${CREAM}-fg}[q]{/} quit\`
6784
- : \`{\${DIM}-fg}live \u2014 updates as dispatches land{/} {\${CREAM}-fg}[/]{/} search {\${CREAM}-fg}[q]{/} quit\`);
7048
+ : \`{\${DIM}-fg}live\${REVEAL ? '' : ' \u2014 streets hidden, --reveal to show'}{/} {\${CREAM}-fg}[/]{/} search {\${CREAM}-fg}[q]{/} quit\`);
6785
7049
  };
6786
7050
 
6787
7051
  const tick = (force = false): void => {
@@ -12246,7 +12510,7 @@ async function scaffold(platform, outputDir, partnerId, webhookSecret, webhookUr
12246
12510
 
12247
12511
  // src/register.ts
12248
12512
  var import_node_crypto2 = require("crypto");
12249
- var PKG_VERSION = true ? "2.8.0" : "dev";
12513
+ var PKG_VERSION = true ? "2.10.0" : "dev";
12250
12514
  var REGISTER_URL = "https://partners.oneaddress.io/api/partner/installs";
12251
12515
  function hmacSha256(secret, message) {
12252
12516
  return (0, import_node_crypto2.createHmac)("sha256", secret).update(message).digest("hex");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oneaddress/setup",
3
- "version": "2.8.0",
3
+ "version": "2.10.0",
4
4
  "description": "Interactive setup wizard for OneAddress partner webhook integrations",
5
5
  "main": "dist/index.js",
6
6
  "bin": {