@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.
- package/dist/index.js +279 -15
- 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.
|
|
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
|
-
|
|
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(
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
6586
|
-
* npm run show 100
|
|
6587
|
-
* npm run show
|
|
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
|
|
6640
|
-
|
|
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
|
|
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.
|
|
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");
|