@oneaddress/setup 2.7.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.
- package/dist/index.js +450 -55
- 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.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": {
|
|
@@ -1023,9 +1024,30 @@ data.db-shm
|
|
|
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.",
|
|
1025
1026
|
"customers": [
|
|
1026
|
-
{ "account_number": "AUR-583920", "name": "Tim Hooper",
|
|
1027
|
-
{ "account_number": "AUR-104772", "name": "Olivia Robinson",
|
|
1028
|
-
{ "account_number": "AUR-296815", "name": "Marcus Chen",
|
|
1027
|
+
{ "account_number": "AUR-583920", "name": "Tim Hooper", "address": { "street": "12 Old Mill Road", "suburb": "Parramatta", "state": "NSW", "postcode": "2150" } },
|
|
1028
|
+
{ "account_number": "AUR-104772", "name": "Olivia Robinson", "address": { "street": "5 Rosewood Lane", "suburb": "Brisbane", "state": "QLD", "postcode": "4000" } },
|
|
1029
|
+
{ "account_number": "AUR-296815", "name": "Marcus Chen", "address": { "street": "31 Goldfield Drive", "suburb": "Richmond", "state": "VIC", "postcode": "3121" } },
|
|
1030
|
+
{ "account_number": "AUR-337401", "name": "Priya Raghavan", "address": { "street": "8 Kurrajong Street", "suburb": "Newtown", "state": "NSW", "postcode": "2042" } },
|
|
1031
|
+
{ "account_number": "AUR-418266", "name": "Daniel O'Sullivan", "address": { "street": "204/19 Marine Parade", "suburb": "Glenelg", "state": "SA", "postcode": "5045" } },
|
|
1032
|
+
{ "account_number": "AUR-452093", "name": "Amara Okafor", "address": { "street": "77 Beaufort Street", "suburb": "Mount Lawley", "state": "WA", "postcode": "6050" } },
|
|
1033
|
+
{ "account_number": "AUR-509118", "name": "Hannah Whitlock", "address": { "street": "3 Tasman Close", "suburb": "Sandy Bay", "state": "TAS", "postcode": "7005" } },
|
|
1034
|
+
{ "account_number": "AUR-563740", "name": "Joshua Nguyen", "address": { "street": "145 Given Terrace", "suburb": "Paddington", "state": "QLD", "postcode": "4064" } },
|
|
1035
|
+
{ "account_number": "AUR-617205", "name": "Isabella Moretti", "address": { "street": "62 Lygon Street", "suburb": "Carlton", "state": "VIC", "postcode": "3053" } },
|
|
1036
|
+
{ "account_number": "AUR-668931", "name": "Samuel Achterberg", "address": { "street": "11 Hutt Street", "suburb": "Adelaide", "state": "SA", "postcode": "5000" } },
|
|
1037
|
+
{ "account_number": "AUR-702884", "name": "Grace Tupou", "address": { "street": "9 Kingsford Smith Way", "suburb": "Belconnen", "state": "ACT", "postcode": "2617" } },
|
|
1038
|
+
{ "account_number": "AUR-745160", "name": "Ethan Marsh", "address": { "street": "38 Smith Street", "suburb": "Darwin City", "state": "NT", "postcode": "0800" } },
|
|
1039
|
+
{ "account_number": "AUR-781422", "name": "Chloe Bennett", "address": { "street": "26 Rundle Street", "suburb": "Kent Town", "state": "SA", "postcode": "5067" } },
|
|
1040
|
+
{ "account_number": "AUR-816559", "name": "Liam Fitzgerald", "address": { "street": "410/88 Queen Street", "suburb": "Melbourne", "state": "VIC", "postcode": "3000" } },
|
|
1041
|
+
{ "account_number": "AUR-849073", "name": "Sofia Delgado", "address": { "street": "17 Cavill Avenue", "suburb": "Surfers Paradise", "state": "QLD", "postcode": "4217" } },
|
|
1042
|
+
{ "account_number": "AUR-873318", "name": "Noah Kirkwood", "address": { "street": "54 Hunter Street", "suburb": "Newcastle", "state": "NSW", "postcode": "2300" } },
|
|
1043
|
+
{ "account_number": "AUR-905642", "name": "Mei Lin Tan", "address": { "street": "2/33 Rokeby Road", "suburb": "Subiaco", "state": "WA", "postcode": "6008" } },
|
|
1044
|
+
{ "account_number": "AUR-938177", "name": "Jack Rasmussen", "address": { "street": "6 Elizabeth Street", "suburb": "Launceston", "state": "TAS", "postcode": "7250" } },
|
|
1045
|
+
{ "account_number": "AUR-960845", "name": "Aaliyah Bennett", "address": { "street": "82 Charles Street", "suburb": "Northcote", "state": "VIC", "postcode": "3070" } },
|
|
1046
|
+
{ "account_number": "AUR-984312", "name": "Oscar Petrov", "address": { "street": "19 Macquarie Street", "suburb": "Hobart", "state": "TAS", "postcode": "7000" } },
|
|
1047
|
+
{ "account_number": "AUR-991026", "name": "Ruby Anderson", "address": { "street": "301/5 Cordelia Street", "suburb": "South Brisbane", "state": "QLD", "postcode": "4101" } },
|
|
1048
|
+
{ "account_number": "AUR-995518", "name": "Henry Abebe", "address": { "street": "14 Wakefield Street", "suburb": "Kent Town", "state": "SA", "postcode": "5067" } },
|
|
1049
|
+
{ "account_number": "AUR-997430", "name": "Zoe Castellano", "address": { "street": "67 Bay Street", "suburb": "Port Melbourne", "state": "VIC", "postcode": "3207" } },
|
|
1050
|
+
{ "account_number": "AUR-999201", "name": "Felix Nakamura", "address": { "street": "23 Anzac Parade", "suburb": "Kensington", "state": "NSW", "postcode": "2033" } }
|
|
1029
1051
|
]
|
|
1030
1052
|
}
|
|
1031
1053
|
`
|
|
@@ -4903,6 +4925,24 @@ db.exec(\`
|
|
|
4903
4925
|
-- rather than only what it became.
|
|
4904
4926
|
prev_address TEXT NOT NULL DEFAULT '{}',
|
|
4905
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,
|
|
4906
4946
|
recorded_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
4907
4947
|
);
|
|
4908
4948
|
CREATE INDEX IF NOT EXISTS idx_history_account ON address_history(account_key, id DESC);
|
|
@@ -4918,6 +4958,11 @@ ensureColumn('customers', 'updated_at', 'TEXT'); // nullable on migrate; set on
|
|
|
4918
4958
|
ensureColumn('customers', 'account_key', 'TEXT');
|
|
4919
4959
|
ensureColumn('address_history', 'account_key', 'TEXT');
|
|
4920
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');
|
|
4921
4966
|
|
|
4922
4967
|
// A database written before at-rest encryption existed holds plaintext rows and
|
|
4923
4968
|
// no account_key. Backfill the key and encrypt in place.
|
|
@@ -5010,9 +5055,30 @@ type RosterEntry = { account_number: string; name: string; address: Address };
|
|
|
5010
5055
|
// runs out of the box. The wizard writes customers.json with these same demo
|
|
5011
5056
|
// customers \u2014 edit that file, not this list.
|
|
5012
5057
|
const DEFAULT_ROSTER: RosterEntry[] = [
|
|
5013
|
-
{ account_number: 'AUR-583920', name: 'Tim Hooper',
|
|
5014
|
-
{ account_number: 'AUR-104772', name: 'Olivia Robinson',
|
|
5015
|
-
{ account_number: 'AUR-296815', name: 'Marcus Chen',
|
|
5058
|
+
{ account_number: 'AUR-583920', name: 'Tim Hooper', address: { street: '12 Old Mill Road', suburb: 'Parramatta', state: 'NSW', postcode: '2150' } },
|
|
5059
|
+
{ account_number: 'AUR-104772', name: 'Olivia Robinson', address: { street: '5 Rosewood Lane', suburb: 'Brisbane', state: 'QLD', postcode: '4000' } },
|
|
5060
|
+
{ account_number: 'AUR-296815', name: 'Marcus Chen', address: { street: '31 Goldfield Drive', suburb: 'Richmond', state: 'VIC', postcode: '3121' } },
|
|
5061
|
+
{ account_number: 'AUR-337401', name: 'Priya Raghavan', address: { street: '8 Kurrajong Street', suburb: 'Newtown', state: 'NSW', postcode: '2042' } },
|
|
5062
|
+
{ account_number: 'AUR-418266', name: 'Daniel O\\'Sullivan', address: { street: '204/19 Marine Parade', suburb: 'Glenelg', state: 'SA', postcode: '5045' } },
|
|
5063
|
+
{ account_number: 'AUR-452093', name: 'Amara Okafor', address: { street: '77 Beaufort Street', suburb: 'Mount Lawley', state: 'WA', postcode: '6050' } },
|
|
5064
|
+
{ account_number: 'AUR-509118', name: 'Hannah Whitlock', address: { street: '3 Tasman Close', suburb: 'Sandy Bay', state: 'TAS', postcode: '7005' } },
|
|
5065
|
+
{ account_number: 'AUR-563740', name: 'Joshua Nguyen', address: { street: '145 Given Terrace', suburb: 'Paddington', state: 'QLD', postcode: '4064' } },
|
|
5066
|
+
{ account_number: 'AUR-617205', name: 'Isabella Moretti', address: { street: '62 Lygon Street', suburb: 'Carlton', state: 'VIC', postcode: '3053' } },
|
|
5067
|
+
{ account_number: 'AUR-668931', name: 'Samuel Achterberg', address: { street: '11 Hutt Street', suburb: 'Adelaide', state: 'SA', postcode: '5000' } },
|
|
5068
|
+
{ account_number: 'AUR-702884', name: 'Grace Tupou', address: { street: '9 Kingsford Smith Way', suburb: 'Belconnen', state: 'ACT', postcode: '2617' } },
|
|
5069
|
+
{ account_number: 'AUR-745160', name: 'Ethan Marsh', address: { street: '38 Smith Street', suburb: 'Darwin City', state: 'NT', postcode: '0800' } },
|
|
5070
|
+
{ account_number: 'AUR-781422', name: 'Chloe Bennett', address: { street: '26 Rundle Street', suburb: 'Kent Town', state: 'SA', postcode: '5067' } },
|
|
5071
|
+
{ account_number: 'AUR-816559', name: 'Liam Fitzgerald', address: { street: '410/88 Queen Street', suburb: 'Melbourne', state: 'VIC', postcode: '3000' } },
|
|
5072
|
+
{ account_number: 'AUR-849073', name: 'Sofia Delgado', address: { street: '17 Cavill Avenue', suburb: 'Surfers Paradise', state: 'QLD', postcode: '4217' } },
|
|
5073
|
+
{ account_number: 'AUR-873318', name: 'Noah Kirkwood', address: { street: '54 Hunter Street', suburb: 'Newcastle', state: 'NSW', postcode: '2300' } },
|
|
5074
|
+
{ account_number: 'AUR-905642', name: 'Mei Lin Tan', address: { street: '2/33 Rokeby Road', suburb: 'Subiaco', state: 'WA', postcode: '6008' } },
|
|
5075
|
+
{ account_number: 'AUR-938177', name: 'Jack Rasmussen', address: { street: '6 Elizabeth Street', suburb: 'Launceston', state: 'TAS', postcode: '7250' } },
|
|
5076
|
+
{ account_number: 'AUR-960845', name: 'Aaliyah Bennett', address: { street: '82 Charles Street', suburb: 'Northcote', state: 'VIC', postcode: '3070' } },
|
|
5077
|
+
{ account_number: 'AUR-984312', name: 'Oscar Petrov', address: { street: '19 Macquarie Street', suburb: 'Hobart', state: 'TAS', postcode: '7000' } },
|
|
5078
|
+
{ account_number: 'AUR-991026', name: 'Ruby Anderson', address: { street: '301/5 Cordelia Street', suburb: 'South Brisbane', state: 'QLD', postcode: '4101' } },
|
|
5079
|
+
{ account_number: 'AUR-995518', name: 'Henry Abebe', address: { street: '14 Wakefield Street', suburb: 'Kent Town', state: 'SA', postcode: '5067' } },
|
|
5080
|
+
{ account_number: 'AUR-997430', name: 'Zoe Castellano', address: { street: '67 Bay Street', suburb: 'Port Melbourne', state: 'VIC', postcode: '3207' } },
|
|
5081
|
+
{ account_number: 'AUR-999201', name: 'Felix Nakamura', address: { street: '23 Anzac Parade', suburb: 'Kensington', state: 'NSW', postcode: '2033' } },
|
|
5016
5082
|
];
|
|
5017
5083
|
|
|
5018
5084
|
function loadRoster(): RosterEntry[] {
|
|
@@ -5145,6 +5211,10 @@ export interface AddressChange {
|
|
|
5145
5211
|
name: string;
|
|
5146
5212
|
prev_address: Address | null;
|
|
5147
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;
|
|
5148
5218
|
recorded_at: string;
|
|
5149
5219
|
}
|
|
5150
5220
|
|
|
@@ -5171,6 +5241,8 @@ export function addressHistory(limit = 20): AddressChange[] {
|
|
|
5171
5241
|
const rows = db.prepare(
|
|
5172
5242
|
\`SELECT h.prev_address AS prev_address,
|
|
5173
5243
|
h.address AS address,
|
|
5244
|
+
h.loa_ref AS loa_ref,
|
|
5245
|
+
h.dispatch_id AS dispatch_id,
|
|
5174
5246
|
h.recorded_at AS recorded_at,
|
|
5175
5247
|
c.account_number AS account_number,
|
|
5176
5248
|
c.name AS name
|
|
@@ -5194,11 +5266,62 @@ export function addressHistory(limit = 20): AddressChange[] {
|
|
|
5194
5266
|
name: dec('name', r.name) ?? '(unknown)',
|
|
5195
5267
|
prev_address: prev,
|
|
5196
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,
|
|
5197
5272
|
recorded_at: r.recorded_at ?? '',
|
|
5198
5273
|
};
|
|
5199
5274
|
});
|
|
5200
5275
|
}
|
|
5201
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
|
+
|
|
5202
5325
|
/** Is the file on disk protected? Surfaced in the dashboard, in both states. */
|
|
5203
5326
|
export const storeEncrypted = encrypted;
|
|
5204
5327
|
|
|
@@ -5259,7 +5382,18 @@ export async function verifyAddress(customer: Customer, incoming: Address): Prom
|
|
|
5259
5382
|
* ours \u2014 but be aware two customers who share a name share a row. If that is
|
|
5260
5383
|
* possible in your data, give this a key of your own instead.
|
|
5261
5384
|
*/
|
|
5262
|
-
export async function saveAddress(
|
|
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> {
|
|
5263
5397
|
const acct = (customer.accountNumber ?? '').trim() || customer.name.trim();
|
|
5264
5398
|
const addressJson = JSON.stringify(incoming);
|
|
5265
5399
|
const key = accountKey(acct);
|
|
@@ -5287,12 +5421,14 @@ export async function saveAddress(customer: Customer, incoming: Address): Promis
|
|
|
5287
5421
|
});
|
|
5288
5422
|
|
|
5289
5423
|
db.prepare(\`
|
|
5290
|
-
INSERT INTO address_history (account_key, prev_address, address)
|
|
5291
|
-
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)
|
|
5292
5426
|
\`).run({
|
|
5293
5427
|
account_key: key,
|
|
5294
5428
|
prev_address: enc('address', JSON.stringify(previous)),
|
|
5295
5429
|
address: enc('address', addressJson),
|
|
5430
|
+
loa_ref: provenance?.loaRef ?? null,
|
|
5431
|
+
dispatch_id: provenance?.dispatchId ?? null,
|
|
5296
5432
|
});
|
|
5297
5433
|
|
|
5298
5434
|
// Metadata only \u2014 the address itself is personal information, so the key is
|
|
@@ -5344,8 +5480,8 @@ export const store = {
|
|
|
5344
5480
|
verifyAddress,
|
|
5345
5481
|
saveAddress,
|
|
5346
5482
|
find: findByAccount,
|
|
5347
|
-
//
|
|
5348
|
-
// 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.
|
|
5349
5485
|
count: customerCount,
|
|
5350
5486
|
} satisfies CustomerStore;
|
|
5351
5487
|
`
|
|
@@ -6144,7 +6280,10 @@ app.post('/webhook', async (req: Request, res: Response) => {
|
|
|
6144
6280
|
// than marking test dispatches as safe to display: a marker is something an
|
|
6145
6281
|
// attacker can try to forge onto a real dispatch, and a panel that never
|
|
6146
6282
|
// renders an address has nothing to forge it into.
|
|
6147
|
-
|
|
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 });
|
|
6148
6287
|
noteChange({
|
|
6149
6288
|
accountNumber: ctx.accountNumber || '(no account reference)',
|
|
6150
6289
|
accountChecked: config.verifiesAccountReference,
|
|
@@ -6532,6 +6671,135 @@ export async function ask(prompt: string): Promise<string> {
|
|
|
6532
6671
|
stdin.on('data', onData);
|
|
6533
6672
|
});
|
|
6534
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
|
+
});
|
|
6535
6803
|
`
|
|
6536
6804
|
},
|
|
6537
6805
|
{
|
|
@@ -6540,9 +6808,11 @@ export async function ask(prompt: string): Promise<string> {
|
|
|
6540
6808
|
* Show what this receiver holds: the roster, and every change it has applied.
|
|
6541
6809
|
*
|
|
6542
6810
|
* Usage:
|
|
6543
|
-
* npm run show
|
|
6544
|
-
* npm run show 100
|
|
6545
|
-
* npm run show
|
|
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)
|
|
6546
6816
|
*
|
|
6547
6817
|
* ## Why this script exists
|
|
6548
6818
|
*
|
|
@@ -6592,10 +6862,32 @@ const DIM = '#8a7f6a';
|
|
|
6592
6862
|
/** Escape blessed's tag syntax so a customer's name can never inject markup. */
|
|
6593
6863
|
const esc = (s: unknown): string => String(s ?? '').replace(/[{}]/g, '');
|
|
6594
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
|
+
|
|
6595
6882
|
function fmt(a: Record<string, string> | null): string {
|
|
6596
6883
|
if (!a) return '(nothing on file)';
|
|
6597
|
-
const
|
|
6598
|
-
|
|
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)';
|
|
6599
6891
|
}
|
|
6600
6892
|
|
|
6601
6893
|
async function askPassword(): Promise<void> {
|
|
@@ -6608,24 +6900,46 @@ async function askPassword(): Promise<void> {
|
|
|
6608
6900
|
|
|
6609
6901
|
type Store = typeof import('../src/store.js');
|
|
6610
6902
|
|
|
6611
|
-
/**
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6903
|
+
/**
|
|
6904
|
+
* Does this row answer the filter?
|
|
6905
|
+
*
|
|
6906
|
+
* NAME OR ACCOUNT NUMBER, because a partner checking whether a dispatch landed
|
|
6907
|
+
* knows the person either way round, and typing "hoop" is faster than finding
|
|
6908
|
+
* the account number to type it exactly.
|
|
6909
|
+
*/
|
|
6910
|
+
function matches(query: string, name: string, account: string): boolean {
|
|
6911
|
+
if (!query) return true;
|
|
6912
|
+
const q = query.toLowerCase();
|
|
6913
|
+
return name.toLowerCase().includes(q) || account.toLowerCase().includes(q);
|
|
6914
|
+
}
|
|
6915
|
+
|
|
6916
|
+
/** The roster, as display rows, optionally filtered. */
|
|
6917
|
+
function rosterRows(store: Store, query: string): string[] {
|
|
6918
|
+
return store.allCustomers()
|
|
6919
|
+
.filter((c) => matches(query, c.name, c.account_number))
|
|
6920
|
+
.map((c) => {
|
|
6921
|
+
let addr: Record<string, string> | null = null;
|
|
6922
|
+
try { addr = JSON.parse(c.address) as Record<string, string>; } catch { addr = null; }
|
|
6923
|
+
return \` \${esc(c.account_number).padEnd(14)} \${esc(c.name).padEnd(20)} \${esc(fmt(addr))}\`;
|
|
6924
|
+
});
|
|
6618
6925
|
}
|
|
6619
6926
|
|
|
6620
6927
|
/** Applied changes, newest first, as display rows with both sides. */
|
|
6621
|
-
function changeRows(store: Store, limit: number): string[] {
|
|
6928
|
+
function changeRows(store: Store, limit: number, query: string): string[] {
|
|
6622
6929
|
const out: string[] = [];
|
|
6623
6930
|
for (const h of store.addressHistory(limit)) {
|
|
6931
|
+
if (!matches(query, h.name, h.account_number)) continue;
|
|
6624
6932
|
const prev = h.prev_address as unknown as Record<string, string> | null;
|
|
6625
6933
|
const now = h.address as unknown as Record<string, string>;
|
|
6626
6934
|
out.push(\` {\${DIM}-fg}\${esc(h.recorded_at)}{/} {\${CREAM}-fg}\${esc(h.account_number)}{/} \${esc(h.name)}\`);
|
|
6627
6935
|
out.push(\` {\${DIM}-fg}was:{/} \${esc(fmt(prev))}\`);
|
|
6628
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){/}\`);
|
|
6629
6943
|
}
|
|
6630
6944
|
return out;
|
|
6631
6945
|
}
|
|
@@ -6653,66 +6967,147 @@ function printOnce(store: Store, limit: number): void {
|
|
|
6653
6967
|
console.log('');
|
|
6654
6968
|
}
|
|
6655
6969
|
|
|
6656
|
-
/**
|
|
6970
|
+
/**
|
|
6971
|
+
* The live view: the mark, the roster, the changes, refreshed as they land.
|
|
6972
|
+
*
|
|
6973
|
+
* SEARCH IS THE REASON THIS IS NOT JUST A PRINT. A roster of three fits on a
|
|
6974
|
+
* screen and a realistic one does not, so "is my own record right" stopped
|
|
6975
|
+
* being answerable by looking. \`/\` filters both panels at once on name or
|
|
6976
|
+
* account number, because a partner checking a dispatch landed knows the person
|
|
6977
|
+
* either way round.
|
|
6978
|
+
*/
|
|
6657
6979
|
function stream(store: Store, limit: number): void {
|
|
6658
|
-
const screen = blessed.screen({
|
|
6980
|
+
const screen = blessed.screen({
|
|
6981
|
+
smartCSR: true,
|
|
6982
|
+
title: 'What this receiver holds \u2014 OneAddress',
|
|
6983
|
+
fullUnicode: true,
|
|
6984
|
+
// WITHOUT THIS THE BRAND COMES OUT RED, and it came out red here for a
|
|
6985
|
+
// whole release because this is the SECOND blessed screen in the scaffold
|
|
6986
|
+
// and the fix lived only in the first. blessed resolves a hex colour
|
|
6987
|
+
// against the palette it infers from TERM, which Windows does not set, so
|
|
6988
|
+
// it assumes a tiny palette and snaps #e0a248 to the nearest thing it
|
|
6989
|
+
// believes exists. Naming the capability is telling it the truth.
|
|
6990
|
+
// \`scripts/__tests__\` pins that EVERY screen we open carries this.
|
|
6991
|
+
terminal: process.env.TERM || 'xterm-256color',
|
|
6992
|
+
});
|
|
6659
6993
|
|
|
6660
|
-
// Same rule as the dashboard: the pixel wordmark needs 88 columns, and below
|
|
6661
|
-
// that it wraps and reads as broken rather than as large.
|
|
6662
6994
|
const wide = terminalFitsFullMark(Number(screen.width));
|
|
6663
|
-
|
|
6995
|
+
// THE MARK, PLUS A LINE SAYING WHICH SCREEN THIS IS. The dashboard and this
|
|
6996
|
+
// viewer open with the same wordmark, so without a subheader a screenshot of
|
|
6997
|
+
// one is indistinguishable from the other - and they answer different
|
|
6998
|
+
// questions. The dashboard is "is it running"; this is "what does it hold".
|
|
6999
|
+
const markHeight = (wide ? 9 : 3) + 1;
|
|
6664
7000
|
blessed.box({
|
|
6665
7001
|
parent: screen, top: 0, left: 0, width: '100%', height: markHeight,
|
|
6666
7002
|
tags: true, padding: { left: 2 },
|
|
6667
|
-
content: wide
|
|
7003
|
+
content: (wide
|
|
6668
7004
|
? ONE_ROWS.map((row, i) => \`{\${CREAM}-fg}\${row}{/} {\${AMBER}-fg}\${ADDRESS_ROWS[i]}{/}\`).join('\\n')
|
|
6669
|
-
: \`{\${CREAM}-fg}{bold}One{/bold}{/}{\${AMBER}-fg}{bold}Address{/bold}{/}
|
|
7005
|
+
: \`{\${CREAM}-fg}{bold}One{/bold}{/}{\${AMBER}-fg}{bold}Address{/bold}{/}\`)
|
|
7006
|
+
+ \`\\n{\${DIM}-fg}WHAT THIS RECEIVER HOLDS{/}\`,
|
|
6670
7007
|
});
|
|
6671
7008
|
|
|
7009
|
+
// THE ROSTER IS BOUNDED AND SCROLLS. It used to size itself to its contents,
|
|
7010
|
+
// which was fine for three customers and pushes the changes panel off the
|
|
7011
|
+
// bottom of the screen for a realistic roster.
|
|
7012
|
+
const usable = Math.max(8, Number(screen.height) - markHeight - 1);
|
|
7013
|
+
const rosterHeight = Math.max(5, Math.min(Math.floor(usable * 0.45), usable - 5));
|
|
7014
|
+
|
|
6672
7015
|
const rosterBox = blessed.box({
|
|
6673
|
-
parent: screen, top: markHeight, left: 0, width: '100%', height:
|
|
6674
|
-
tags: true, padding: { left: 1, right: 1 },
|
|
7016
|
+
parent: screen, top: markHeight, left: 0, width: '100%', height: rosterHeight,
|
|
7017
|
+
tags: true, padding: { left: 1, right: 1 }, scrollable: true, alwaysScroll: true,
|
|
6675
7018
|
border: { type: 'line' }, style: { border: { fg: AMBER } },
|
|
6676
7019
|
label: ' ON FILE ',
|
|
6677
7020
|
});
|
|
6678
7021
|
|
|
6679
7022
|
const changesBox = blessed.box({
|
|
6680
|
-
parent: screen, top: markHeight +
|
|
7023
|
+
parent: screen, top: markHeight + rosterHeight, left: 0, width: '100%', bottom: 1,
|
|
6681
7024
|
tags: true, padding: { left: 1, right: 1 }, scrollable: true, alwaysScroll: true,
|
|
6682
7025
|
border: { type: 'line' }, style: { border: { fg: AMBER } },
|
|
6683
7026
|
label: ' APPLIED CHANGES \u2014 newest first ',
|
|
6684
7027
|
});
|
|
6685
7028
|
|
|
6686
|
-
blessed.box({
|
|
7029
|
+
const footer = blessed.box({
|
|
6687
7030
|
parent: screen, bottom: 0, left: 0, width: '100%', height: 1,
|
|
6688
7031
|
tags: true, padding: { left: 2 },
|
|
6689
|
-
content: \`{\${DIM}-fg}live \u2014 updates as dispatches land{/} {\${CREAM}-fg}[q]{/} quit\`,
|
|
6690
7032
|
});
|
|
6691
7033
|
|
|
6692
|
-
|
|
6693
|
-
// makes an idle receiver flicker once a second and destroys any scrollback
|
|
6694
|
-
// position the operator had.
|
|
7034
|
+
let query = '';
|
|
6695
7035
|
let last = '';
|
|
6696
|
-
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
|
|
7036
|
+
|
|
7037
|
+
const drawFooter = (): void => {
|
|
7038
|
+
footer.setContent(query
|
|
7039
|
+
? \`{\${AMBER}-fg}filter: \${esc(query)}{/} {\${CREAM}-fg}[/]{/} change {\${CREAM}-fg}[esc]{/} clear {\${CREAM}-fg}[q]{/} quit\`
|
|
7040
|
+
: \`{\${DIM}-fg}live\${REVEAL ? '' : ' \u2014 streets hidden, --reveal to show'}{/} {\${CREAM}-fg}[/]{/} search {\${CREAM}-fg}[q]{/} quit\`);
|
|
7041
|
+
};
|
|
7042
|
+
|
|
7043
|
+
const tick = (force = false): void => {
|
|
7044
|
+
const all = rosterRows(store, '');
|
|
7045
|
+
const roster = rosterRows(store, query);
|
|
7046
|
+
const changes = changeRows(store, limit, query);
|
|
7047
|
+
const next = \`\${query}\\u0000\${roster.join('\\n')}\\u0000\${changes.join('\\n')}\`;
|
|
7048
|
+
if (next === last && !force) return;
|
|
6701
7049
|
last = next;
|
|
6702
|
-
|
|
6703
|
-
rosterBox.
|
|
6704
|
-
|
|
6705
|
-
|
|
7050
|
+
|
|
7051
|
+
rosterBox.setLabel(query
|
|
7052
|
+
? \` ON FILE \u2014 \${roster.length} of \${all.length} matching "\${esc(query)}" \`
|
|
7053
|
+
: \` ON FILE \u2014 \${all.length} \`);
|
|
7054
|
+
rosterBox.setContent(roster.join('\\n') ||
|
|
7055
|
+
(query ? \` Nobody matching "\${esc(query)}".\` : ' (nobody on file yet)'));
|
|
7056
|
+
|
|
7057
|
+
changesBox.setLabel(query
|
|
7058
|
+
? \` APPLIED CHANGES \u2014 matching "\${esc(query)}", newest first \`
|
|
7059
|
+
: ' APPLIED CHANGES \u2014 newest first ');
|
|
7060
|
+
changesBox.setContent(changes.join('\\n') || (query
|
|
7061
|
+
? \` No applied change for "\${esc(query)}" yet.\`
|
|
7062
|
+
: ' None yet. Send a dispatch and this fills in.'));
|
|
7063
|
+
|
|
7064
|
+
drawFooter();
|
|
6706
7065
|
screen.render();
|
|
6707
7066
|
};
|
|
6708
7067
|
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
7068
|
+
// \`/\` OPENS A LIVE FILTER. It filters as you type rather than on Enter,
|
|
7069
|
+
// because the question being asked is "is my record in here", and the answer
|
|
7070
|
+
// is usually visible three characters in.
|
|
7071
|
+
let searching = false;
|
|
7072
|
+
const openSearch = (): void => {
|
|
7073
|
+
searching = true;
|
|
7074
|
+
const box = blessed.textbox({
|
|
7075
|
+
parent: screen, bottom: 0, left: 0, width: '100%', height: 1,
|
|
7076
|
+
style: { fg: CREAM }, inputOnFocus: true, keys: true,
|
|
7077
|
+
});
|
|
7078
|
+
box.setValue(query);
|
|
7079
|
+
// The filter must update on every keystroke, and \`readInput\` only reports
|
|
7080
|
+
// the final value, so the live half is driven off keypress.
|
|
7081
|
+
box.on('keypress', () => {
|
|
7082
|
+
setImmediate(() => { query = String(box.getValue() ?? '').trim(); tick(); });
|
|
7083
|
+
});
|
|
7084
|
+
screen.render();
|
|
7085
|
+
box.readInput(() => {
|
|
7086
|
+
searching = false;
|
|
7087
|
+
query = String(box.getValue() ?? '').trim();
|
|
7088
|
+
box.destroy();
|
|
7089
|
+
tick(true);
|
|
7090
|
+
});
|
|
7091
|
+
};
|
|
7092
|
+
|
|
7093
|
+
screen.key(['/'], () => { if (!searching) openSearch(); });
|
|
7094
|
+
screen.key(['escape'], () => {
|
|
7095
|
+
if (searching) return;
|
|
7096
|
+
if (!query) return;
|
|
7097
|
+
query = '';
|
|
7098
|
+
tick(true);
|
|
7099
|
+
});
|
|
7100
|
+
screen.key(['q', 'C-c'], () => {
|
|
7101
|
+
if (searching) return;
|
|
7102
|
+
screen.destroy();
|
|
7103
|
+
process.exit(0);
|
|
7104
|
+
});
|
|
7105
|
+
|
|
7106
|
+
tick(true);
|
|
6712
7107
|
// DELIBERATELY NOT unref'd. An unref'd timer does not hold the event loop
|
|
6713
7108
|
// open, and with the screen drawn there is nothing else pending, so the
|
|
6714
7109
|
// process would exit the instant it finished painting.
|
|
6715
|
-
setInterval(tick, 1000);
|
|
7110
|
+
setInterval(() => tick(), 1000);
|
|
6716
7111
|
}
|
|
6717
7112
|
|
|
6718
7113
|
async function main(): Promise<void> {
|
|
@@ -12107,7 +12502,7 @@ async function scaffold(platform, outputDir, partnerId, webhookSecret, webhookUr
|
|
|
12107
12502
|
|
|
12108
12503
|
// src/register.ts
|
|
12109
12504
|
var import_node_crypto2 = require("crypto");
|
|
12110
|
-
var PKG_VERSION = true ? "2.
|
|
12505
|
+
var PKG_VERSION = true ? "2.9.0" : "dev";
|
|
12111
12506
|
var REGISTER_URL = "https://partners.oneaddress.io/api/partner/installs";
|
|
12112
12507
|
function hmacSha256(secret, message) {
|
|
12113
12508
|
return (0, import_node_crypto2.createHmac)("sha256", secret).update(message).digest("hex");
|