@oneaddress/setup 2.8.0 → 2.9.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 +270 -14
  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.9.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": {
@@ -4924,6 +4925,24 @@ db.exec(\`
4924
4925
  -- rather than only what it became.
4925
4926
  prev_address TEXT NOT NULL DEFAULT '{}',
4926
4927
  address TEXT NOT NULL,
4928
+ -- ON WHOSE AUTHORITY. The reference of the signed authorisation that
4929
+ -- produced this change, and the dispatch it arrived under.
4930
+ --
4931
+ -- WHY THEY ARE HERE AND NOT ONLY ON THE DASHBOARD. Both were computed on
4932
+ -- arrival and shown on the LAST CHANGE panel, and nowhere else: a second
4933
+ -- dispatch overwrote the panel and a restart cleared it. So "on whose
4934
+ -- authority did you change this customer's address on the 15th" could not
4935
+ -- be answered from this receiver at all, which is the first question an
4936
+ -- inspection asks about an applied change.
4937
+ --
4938
+ -- NOT ENCRYPTED, deliberately, unlike the addresses beside them. Neither is
4939
+ -- personal information: the reference is a digest of an authorisation and
4940
+ -- the dispatch id is an integer. Encrypting them would make the audit trail
4941
+ -- unreadable without the password in exactly the situation it exists for,
4942
+ -- and would buy nothing, since an attacker who can read this file can read
4943
+ -- the addresses too once they have what they came for.
4944
+ loa_ref TEXT,
4945
+ dispatch_id TEXT,
4927
4946
  recorded_at TEXT NOT NULL DEFAULT (datetime('now'))
4928
4947
  );
4929
4948
  CREATE INDEX IF NOT EXISTS idx_history_account ON address_history(account_key, id DESC);
@@ -4939,6 +4958,11 @@ ensureColumn('customers', 'updated_at', 'TEXT'); // nullable on migrate; set on
4939
4958
  ensureColumn('customers', 'account_key', 'TEXT');
4940
4959
  ensureColumn('address_history', 'account_key', 'TEXT');
4941
4960
  ensureColumn('address_history', 'prev_address', "TEXT NOT NULL DEFAULT '{}'");
4961
+ // NULLABLE WITH NO DEFAULT, because a row written before this column existed
4962
+ // genuinely has no authority recorded and must not claim one. Null reads as
4963
+ // "not recorded", which is the truth; a default would read as a value.
4964
+ ensureColumn('address_history', 'loa_ref', 'TEXT');
4965
+ ensureColumn('address_history', 'dispatch_id', 'TEXT');
4942
4966
 
4943
4967
  // A database written before at-rest encryption existed holds plaintext rows and
4944
4968
  // no account_key. Backfill the key and encrypt in place.
@@ -5187,6 +5211,10 @@ export interface AddressChange {
5187
5211
  name: string;
5188
5212
  prev_address: Address | null;
5189
5213
  address: Address;
5214
+ /** The authorisation this change was made under. Null on an older row. */
5215
+ loa_ref: string | null;
5216
+ /** The dispatch it arrived under. Null on an older row. */
5217
+ dispatch_id: string | null;
5190
5218
  recorded_at: string;
5191
5219
  }
5192
5220
 
@@ -5213,6 +5241,8 @@ export function addressHistory(limit = 20): AddressChange[] {
5213
5241
  const rows = db.prepare(
5214
5242
  \`SELECT h.prev_address AS prev_address,
5215
5243
  h.address AS address,
5244
+ h.loa_ref AS loa_ref,
5245
+ h.dispatch_id AS dispatch_id,
5216
5246
  h.recorded_at AS recorded_at,
5217
5247
  c.account_number AS account_number,
5218
5248
  c.name AS name
@@ -5236,11 +5266,62 @@ export function addressHistory(limit = 20): AddressChange[] {
5236
5266
  name: dec('name', r.name) ?? '(unknown)',
5237
5267
  prev_address: prev,
5238
5268
  address: JSON.parse(dec('address', r.address) ?? '{}') as Address,
5269
+ // NOT decrypted, because they were never encrypted. See the schema.
5270
+ loa_ref: r.loa_ref ?? null,
5271
+ dispatch_id: r.dispatch_id ?? null,
5239
5272
  recorded_at: r.recorded_at ?? '',
5240
5273
  };
5241
5274
  });
5242
5275
  }
5243
5276
 
5277
+ /**
5278
+ * Drop applied-change history older than \`days\`. Returns how many rows went.
5279
+ *
5280
+ * ## Why this exists, and why it is NOT wired to a timer
5281
+ *
5282
+ * The quarantine and the confirm queue both age out on their own, because both
5283
+ * hold work in progress and an unbounded hold is a retention decision made on
5284
+ * a consumer's behalf by nobody. \`address_history\` is different: it is the
5285
+ * AUDIT TRAIL, and the same argument runs the other way. Deleting the record
5286
+ * of a change you applied is not hygiene, it is losing the answer to "on whose
5287
+ * authority", which is the first thing an inspection asks.
5288
+ *
5289
+ * So the default is to keep it, and that default is now a DECISION rather than
5290
+ * an omission - which is the only thing that changed here. Before this
5291
+ * function existed, keeping it forever was simply what happened, and a partner
5292
+ * whose own retention policy required deletion had no lever to pull at all.
5293
+ *
5294
+ * ## If you do call it
5295
+ *
5296
+ * Your retention obligation and your evidentiary one point in opposite
5297
+ * directions and only you know which governs your business. Australian
5298
+ * financial-services records commonly run seven years; APP 11.2 says destroy
5299
+ * or de-identify personal information you no longer need. Those are compatible
5300
+ * only once someone decides what "no longer need" means here, and that someone
5301
+ * is not this file.
5302
+ *
5303
+ * Nothing calls this by default. Wire it to your own schedule, or run it by
5304
+ * hand, deliberately.
5305
+ */
5306
+ export function purgeAddressHistory(days: number): number {
5307
+ if (!Number.isFinite(days) || days <= 0) return 0;
5308
+ // THE COMPARISON HAPPENS INSIDE SQLITE, in SQLite's own datetime format,
5309
+ // for the same reason \`purgeQuarantine\` does it: \`recorded_at\` is written by
5310
+ // the column default \`datetime('now')\`, and comparing that to a JavaScript
5311
+ // ISO string works right up until it does not.
5312
+ const info = db.prepare(
5313
+ \`DELETE FROM address_history WHERE recorded_at < datetime('now', ?)\`,
5314
+ ).run(\`-\${Math.floor(days)} days\`);
5315
+ const n = Number(info.changes ?? 0);
5316
+ if (n > 0) {
5317
+ report.warn(
5318
+ \`[store] purged \${n} applied-change record(s) older than \${Math.floor(days)}d. \` +
5319
+ 'That is audit history and it is gone.',
5320
+ );
5321
+ }
5322
+ return n;
5323
+ }
5324
+
5244
5325
  /** Is the file on disk protected? Surfaced in the dashboard, in both states. */
5245
5326
  export const storeEncrypted = encrypted;
5246
5327
 
@@ -5301,7 +5382,18 @@ export async function verifyAddress(customer: Customer, incoming: Address): Prom
5301
5382
  * ours \u2014 but be aware two customers who share a name share a row. If that is
5302
5383
  * possible in your data, give this a key of your own instead.
5303
5384
  */
5304
- export async function saveAddress(customer: Customer, incoming: Address): Promise<Address> {
5385
+ export async function saveAddress(
5386
+ customer: Customer,
5387
+ incoming: Address,
5388
+ /**
5389
+ * On whose authority, recorded ALONGSIDE the change rather than beside it.
5390
+ *
5391
+ * Optional so an existing caller compiles, and the absence is honest: a row
5392
+ * written without it says "not recorded" rather than claiming an authority
5393
+ * it never had.
5394
+ */
5395
+ provenance?: { loaRef?: string | null; dispatchId?: string | null },
5396
+ ): Promise<Address> {
5305
5397
  const acct = (customer.accountNumber ?? '').trim() || customer.name.trim();
5306
5398
  const addressJson = JSON.stringify(incoming);
5307
5399
  const key = accountKey(acct);
@@ -5329,12 +5421,14 @@ export async function saveAddress(customer: Customer, incoming: Address): Promis
5329
5421
  });
5330
5422
 
5331
5423
  db.prepare(\`
5332
- INSERT INTO address_history (account_key, prev_address, address)
5333
- VALUES ($account_key, $prev_address, $address)
5424
+ INSERT INTO address_history (account_key, prev_address, address, loa_ref, dispatch_id)
5425
+ VALUES ($account_key, $prev_address, $address, $loa_ref, $dispatch_id)
5334
5426
  \`).run({
5335
5427
  account_key: key,
5336
5428
  prev_address: enc('address', JSON.stringify(previous)),
5337
5429
  address: enc('address', addressJson),
5430
+ loa_ref: provenance?.loaRef ?? null,
5431
+ dispatch_id: provenance?.dispatchId ?? null,
5338
5432
  });
5339
5433
 
5340
5434
  // Metadata only \u2014 the address itself is personal information, so the key is
@@ -5386,8 +5480,8 @@ export const store = {
5386
5480
  verifyAddress,
5387
5481
  saveAddress,
5388
5482
  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.
5483
+ // A demo roster, so a count is free. A store over a real customer table
5484
+ // should return null here; the contract file says why.
5391
5485
  count: customerCount,
5392
5486
  } satisfies CustomerStore;
5393
5487
  `
@@ -6186,7 +6280,10 @@ app.post('/webhook', async (req: Request, res: Response) => {
6186
6280
  // than marking test dispatches as safe to display: a marker is something an
6187
6281
  // attacker can try to forge onto a real dispatch, and a panel that never
6188
6282
  // renders an address has nothing to forge it into.
6189
- const replaced = await store.saveAddress(ctx, address);
6283
+ // PROVENANCE GOES IN WITH THE CHANGE. Both values were already computed
6284
+ // here and went only to the dashboard panel, which one more dispatch or one
6285
+ // restart wiped.
6286
+ const replaced = await store.saveAddress(ctx, address, { loaRef, dispatchId: dispatch || null });
6190
6287
  noteChange({
6191
6288
  accountNumber: ctx.accountNumber || '(no account reference)',
6192
6289
  accountChecked: config.verifiesAccountReference,
@@ -6574,6 +6671,135 @@ export async function ask(prompt: string): Promise<string> {
6574
6671
  stdin.on('data', onData);
6575
6672
  });
6576
6673
  }
6674
+ `
6675
+ },
6676
+ {
6677
+ name: "scripts/audit.ts",
6678
+ content: `/**
6679
+ * Export the applied-change record: what changed, when, and on whose authority.
6680
+ *
6681
+ * Usage:
6682
+ * npm run audit \u2014 every applied change, as CSV
6683
+ * npm run audit -- --json \u2014 the same as JSON
6684
+ * npm run audit -- --from 2026-07-01 --to 2026-09-30
6685
+ * npm run audit -- --out C:\\reports\\q3.csv
6686
+ *
6687
+ * ## Why this exists
6688
+ *
6689
+ * \`npm run show\` answers "did it land", for a person, now. This answers "show
6690
+ * me every address change you applied last quarter and what authorised each
6691
+ * one", for somebody who is not you and is not at your desk. Those are
6692
+ * different questions and the second one is the one an inspection asks.
6693
+ *
6694
+ * ## What is in it, and what deliberately is not
6695
+ *
6696
+ * The account reference, the name, both sides of the change, the timestamp, the
6697
+ * LOA reference and the dispatch id. That is the full record of a change this
6698
+ * receiver applied.
6699
+ *
6700
+ * It is NOT redacted, unlike \`npm run show\`, and that is the point: this file
6701
+ * is the thing you hand to an auditor, and a redacted audit trail is not one.
6702
+ * It therefore contains personal information in cleartext the moment it is
6703
+ * written, which is why it writes where you tell it, says where it went, and
6704
+ * says so out loud.
6705
+ *
6706
+ * ## The one thing it cannot do
6707
+ *
6708
+ * It cannot prove the authority is genuine. \`loa_ref\` is recomputed by THIS
6709
+ * receiver from the LOA it decrypted, so it is evidence this receiver saw a
6710
+ * valid authorisation, not a signature anybody else can re-verify from the CSV.
6711
+ * Verifying it independently needs OneAddress's LOA public key, which is a
6712
+ * different exercise and is not what this file claims to be.
6713
+ */
6714
+ import 'dotenv/config';
6715
+ import { writeFileSync } from 'node:fs';
6716
+ import { resolve } from 'node:path';
6717
+ import { config } from '../src/config.js';
6718
+ import { ask, databaseIsLocked } from '../src/unlock.js';
6719
+
6720
+ function arg(name: string): string | null {
6721
+ const i = process.argv.indexOf(\`--\${name}\`);
6722
+ return i >= 0 && process.argv[i + 1] ? String(process.argv[i + 1]) : null;
6723
+ }
6724
+
6725
+ /** CSV with the quoting rules that stop a comma in a street name shifting every column. */
6726
+ function csvCell(v: unknown): string {
6727
+ const s = String(v ?? '');
6728
+ return /[",\\n]/.test(s) ? \`"\${s.replace(/"/g, '""')}"\` : s;
6729
+ }
6730
+
6731
+ function fmtAddress(a: Record<string, string> | null): string {
6732
+ if (!a) return '';
6733
+ return [a.street, a.suburb, a.state, a.postcode, a.country].filter(Boolean).join(', ');
6734
+ }
6735
+
6736
+ async function askPassword(): Promise<void> {
6737
+ if (process.env.ONEADDRESS_DB_PASSPHRASE?.trim()) return;
6738
+ if (!process.stdin.isTTY) return;
6739
+ if (!(await databaseIsLocked())) return;
6740
+ const answer = await ask(' Password to unlock: ');
6741
+ if (answer) process.env.ONEADDRESS_DB_PASSPHRASE = answer;
6742
+ }
6743
+
6744
+ async function main(): Promise<void> {
6745
+ if (config.mode === 'inbox') {
6746
+ console.log('\\n This receiver is in inbox mode: your connector applies changes and holds the record.');
6747
+ console.log(' Export from there instead.\\n');
6748
+ return;
6749
+ }
6750
+
6751
+ await askPassword();
6752
+ const store = await import('../src/store.js');
6753
+
6754
+ const from = arg('from');
6755
+ const to = arg('to');
6756
+ // EVERY row, then filtered here. The store's reader takes a limit rather than
6757
+ // a date range, and a range that silently truncated at 20 would be worse than
6758
+ // useless in an audit file.
6759
+ const all = store.addressHistory(Number.MAX_SAFE_INTEGER);
6760
+ const rows = all.filter((h) => {
6761
+ const at = String(h.recorded_at ?? '');
6762
+ if (from && at < from) return false;
6763
+ // Inclusive of the whole end day: a --to of 2026-09-30 must include changes
6764
+ // applied at 14:00 that day, and 'YYYY-MM-DD HH:MM:SS' > 'YYYY-MM-DD'.
6765
+ if (to && at > \`\${to} 99\`) return false;
6766
+ return true;
6767
+ });
6768
+
6769
+ const asJson = process.argv.includes('--json');
6770
+ const body = asJson
6771
+ ? JSON.stringify({
6772
+ exported_at: new Date().toISOString(),
6773
+ range: { from: from ?? null, to: to ?? null },
6774
+ 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.',
6775
+ changes: rows,
6776
+ }, null, 2)
6777
+ : [
6778
+ ['recorded_at', 'account_number', 'name', 'previous_address', 'new_address', 'loa_ref', 'dispatch_id']
6779
+ .join(','),
6780
+ ...rows.map((h) => [
6781
+ h.recorded_at, h.account_number, h.name,
6782
+ fmtAddress(h.prev_address as unknown as Record<string, string> | null),
6783
+ fmtAddress(h.address as unknown as Record<string, string>),
6784
+ h.loa_ref ?? '', h.dispatch_id ?? '',
6785
+ ].map(csvCell).join(',')),
6786
+ ].join('\\n');
6787
+
6788
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
6789
+ const target = resolve(arg('out') ?? config.exportDir ?? '', arg('out')
6790
+ ? ''
6791
+ : \`oneaddress-audit-\${stamp}.\${asJson ? 'json' : 'csv'}\`);
6792
+ writeFileSync(target, body);
6793
+
6794
+ console.log(\`\\n Exported \${rows.length} applied change(s) to\\n \${target}\\n\`);
6795
+ console.log(' THIS FILE CONTAINS CUSTOMER ADDRESSES IN CLEARTEXT. It is the audit record,');
6796
+ console.log(' so it is deliberately not redacted. Store and send it accordingly.\\n');
6797
+ }
6798
+
6799
+ void main().catch((err: unknown) => {
6800
+ console.error(err instanceof Error ? err.message : String(err));
6801
+ process.exit(1);
6802
+ });
6577
6803
  `
6578
6804
  },
6579
6805
  {
@@ -6582,9 +6808,11 @@ export async function ask(prompt: string): Promise<string> {
6582
6808
  * Show what this receiver holds: the roster, and every change it has applied.
6583
6809
  *
6584
6810
  * 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
6811
+ * npm run show \u2014 a LIVE branded view, refreshing as changes land
6812
+ * npm run show 100 \u2014 keep 100 changes on screen instead of 20
6813
+ * npm run show -- --reveal \u2014 show street lines (hidden by default)
6814
+ * npm run show > f.txt \u2014 plain one-shot text, for a file or a pipe
6815
+ * npm run audit \u2014 export the applied-change record (see scripts/audit.ts)
6588
6816
  *
6589
6817
  * ## Why this script exists
6590
6818
  *
@@ -6634,10 +6862,32 @@ const DIM = '#8a7f6a';
6634
6862
  /** Escape blessed's tag syntax so a customer's name can never inject markup. */
6635
6863
  const esc = (s: unknown): string => String(s ?? '').replace(/[{}]/g, '');
6636
6864
 
6865
+ /**
6866
+ * REDACTED BY DEFAULT, revealed on purpose.
6867
+ *
6868
+ * This script's whole job is to show what the receiver holds, so refusing to
6869
+ * print addresses would defeat it. But the default run prints EVERY customer's
6870
+ * address into a terminal, where it lands in scrollback, in a \`| tee\` file and
6871
+ * in any screen recording, and the commonest reason to run it is to check one
6872
+ * record or prove one dispatch landed - neither of which needs the other 23.
6873
+ *
6874
+ * So the default shows enough to recognise a record (suburb, state, postcode)
6875
+ * and withholds the street line, and \`--reveal\` prints the lot. The suburb is
6876
+ * deliberately NOT withheld: an audit row you cannot tell apart from the next
6877
+ * one is not an audit row, and the suburb is what makes a change legible as a
6878
+ * change.
6879
+ */
6880
+ const REVEAL = process.argv.includes('--reveal') || process.env.ONEADDRESS_SHOW_REVEAL === '1';
6881
+
6637
6882
  function fmt(a: Record<string, string> | null): string {
6638
6883
  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)';
6884
+ const rest = [a.suburb, a.state, a.postcode, a.country].filter(Boolean).join(', ');
6885
+ if (REVEAL) {
6886
+ const parts = [a.street, a.suburb, a.state, a.postcode, a.country].filter(Boolean);
6887
+ return parts.join(', ') || '(empty)';
6888
+ }
6889
+ if (!a.street) return rest || '(empty)';
6890
+ return \`[street hidden], \${rest}\` || '(empty)';
6641
6891
  }
6642
6892
 
6643
6893
  async function askPassword(): Promise<void> {
@@ -6684,6 +6934,12 @@ function changeRows(store: Store, limit: number, query: string): string[] {
6684
6934
  out.push(\` {\${DIM}-fg}\${esc(h.recorded_at)}{/} {\${CREAM}-fg}\${esc(h.account_number)}{/} \${esc(h.name)}\`);
6685
6935
  out.push(\` {\${DIM}-fg}was:{/} \${esc(fmt(prev))}\`);
6686
6936
  out.push(\` {\${AMBER}-fg}now:{/} \${esc(fmt(now))}\`);
6937
+ // ON WHOSE AUTHORITY, on its own line. Null means the row predates the
6938
+ // column, which is not the same as a change nobody authorised, so it says
6939
+ // so rather than printing an empty field.
6940
+ out.push(h.loa_ref
6941
+ ? \` {\${DIM}-fg}auth:{/} \${esc(h.loa_ref)}\${h.dispatch_id ? \` {\${DIM}-fg}dispatch{/} \${esc(h.dispatch_id)}\` : ''}\`
6942
+ : \` {\${DIM}-fg}auth: not recorded (applied before this receiver kept it){/}\`);
6687
6943
  }
6688
6944
  return out;
6689
6945
  }
@@ -6781,7 +7037,7 @@ function stream(store: Store, limit: number): void {
6781
7037
  const drawFooter = (): void => {
6782
7038
  footer.setContent(query
6783
7039
  ? \`{\${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\`);
7040
+ : \`{\${DIM}-fg}live\${REVEAL ? '' : ' \u2014 streets hidden, --reveal to show'}{/} {\${CREAM}-fg}[/]{/} search {\${CREAM}-fg}[q]{/} quit\`);
6785
7041
  };
6786
7042
 
6787
7043
  const tick = (force = false): void => {
@@ -12246,7 +12502,7 @@ async function scaffold(platform, outputDir, partnerId, webhookSecret, webhookUr
12246
12502
 
12247
12503
  // src/register.ts
12248
12504
  var import_node_crypto2 = require("crypto");
12249
- var PKG_VERSION = true ? "2.8.0" : "dev";
12505
+ var PKG_VERSION = true ? "2.9.0" : "dev";
12250
12506
  var REGISTER_URL = "https://partners.oneaddress.io/api/partner/installs";
12251
12507
  function hmacSha256(secret, message) {
12252
12508
  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.9.0",
4
4
  "description": "Interactive setup wizard for OneAddress partner webhook integrations",
5
5
  "main": "dist/index.js",
6
6
  "bin": {