@oneaddress/setup 2.6.0 → 2.8.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 +379 -33
- 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.8.0" : "?";
|
|
860
860
|
function printCompactHeader() {
|
|
861
861
|
const INNER = 42;
|
|
862
862
|
const TOP = fn("\u250C") + dm("\u2500".repeat(INNER)) + fn("\u2510");
|
|
@@ -1023,9 +1023,30 @@ data.db-shm
|
|
|
1023
1023
|
content: `{
|
|
1024
1024
|
"//": "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
1025
|
"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",
|
|
1026
|
+
{ "account_number": "AUR-583920", "name": "Tim Hooper", "address": { "street": "12 Old Mill Road", "suburb": "Parramatta", "state": "NSW", "postcode": "2150" } },
|
|
1027
|
+
{ "account_number": "AUR-104772", "name": "Olivia Robinson", "address": { "street": "5 Rosewood Lane", "suburb": "Brisbane", "state": "QLD", "postcode": "4000" } },
|
|
1028
|
+
{ "account_number": "AUR-296815", "name": "Marcus Chen", "address": { "street": "31 Goldfield Drive", "suburb": "Richmond", "state": "VIC", "postcode": "3121" } },
|
|
1029
|
+
{ "account_number": "AUR-337401", "name": "Priya Raghavan", "address": { "street": "8 Kurrajong Street", "suburb": "Newtown", "state": "NSW", "postcode": "2042" } },
|
|
1030
|
+
{ "account_number": "AUR-418266", "name": "Daniel O'Sullivan", "address": { "street": "204/19 Marine Parade", "suburb": "Glenelg", "state": "SA", "postcode": "5045" } },
|
|
1031
|
+
{ "account_number": "AUR-452093", "name": "Amara Okafor", "address": { "street": "77 Beaufort Street", "suburb": "Mount Lawley", "state": "WA", "postcode": "6050" } },
|
|
1032
|
+
{ "account_number": "AUR-509118", "name": "Hannah Whitlock", "address": { "street": "3 Tasman Close", "suburb": "Sandy Bay", "state": "TAS", "postcode": "7005" } },
|
|
1033
|
+
{ "account_number": "AUR-563740", "name": "Joshua Nguyen", "address": { "street": "145 Given Terrace", "suburb": "Paddington", "state": "QLD", "postcode": "4064" } },
|
|
1034
|
+
{ "account_number": "AUR-617205", "name": "Isabella Moretti", "address": { "street": "62 Lygon Street", "suburb": "Carlton", "state": "VIC", "postcode": "3053" } },
|
|
1035
|
+
{ "account_number": "AUR-668931", "name": "Samuel Achterberg", "address": { "street": "11 Hutt Street", "suburb": "Adelaide", "state": "SA", "postcode": "5000" } },
|
|
1036
|
+
{ "account_number": "AUR-702884", "name": "Grace Tupou", "address": { "street": "9 Kingsford Smith Way", "suburb": "Belconnen", "state": "ACT", "postcode": "2617" } },
|
|
1037
|
+
{ "account_number": "AUR-745160", "name": "Ethan Marsh", "address": { "street": "38 Smith Street", "suburb": "Darwin City", "state": "NT", "postcode": "0800" } },
|
|
1038
|
+
{ "account_number": "AUR-781422", "name": "Chloe Bennett", "address": { "street": "26 Rundle Street", "suburb": "Kent Town", "state": "SA", "postcode": "5067" } },
|
|
1039
|
+
{ "account_number": "AUR-816559", "name": "Liam Fitzgerald", "address": { "street": "410/88 Queen Street", "suburb": "Melbourne", "state": "VIC", "postcode": "3000" } },
|
|
1040
|
+
{ "account_number": "AUR-849073", "name": "Sofia Delgado", "address": { "street": "17 Cavill Avenue", "suburb": "Surfers Paradise", "state": "QLD", "postcode": "4217" } },
|
|
1041
|
+
{ "account_number": "AUR-873318", "name": "Noah Kirkwood", "address": { "street": "54 Hunter Street", "suburb": "Newcastle", "state": "NSW", "postcode": "2300" } },
|
|
1042
|
+
{ "account_number": "AUR-905642", "name": "Mei Lin Tan", "address": { "street": "2/33 Rokeby Road", "suburb": "Subiaco", "state": "WA", "postcode": "6008" } },
|
|
1043
|
+
{ "account_number": "AUR-938177", "name": "Jack Rasmussen", "address": { "street": "6 Elizabeth Street", "suburb": "Launceston", "state": "TAS", "postcode": "7250" } },
|
|
1044
|
+
{ "account_number": "AUR-960845", "name": "Aaliyah Bennett", "address": { "street": "82 Charles Street", "suburb": "Northcote", "state": "VIC", "postcode": "3070" } },
|
|
1045
|
+
{ "account_number": "AUR-984312", "name": "Oscar Petrov", "address": { "street": "19 Macquarie Street", "suburb": "Hobart", "state": "TAS", "postcode": "7000" } },
|
|
1046
|
+
{ "account_number": "AUR-991026", "name": "Ruby Anderson", "address": { "street": "301/5 Cordelia Street", "suburb": "South Brisbane", "state": "QLD", "postcode": "4101" } },
|
|
1047
|
+
{ "account_number": "AUR-995518", "name": "Henry Abebe", "address": { "street": "14 Wakefield Street", "suburb": "Kent Town", "state": "SA", "postcode": "5067" } },
|
|
1048
|
+
{ "account_number": "AUR-997430", "name": "Zoe Castellano", "address": { "street": "67 Bay Street", "suburb": "Port Melbourne", "state": "VIC", "postcode": "3207" } },
|
|
1049
|
+
{ "account_number": "AUR-999201", "name": "Felix Nakamura", "address": { "street": "23 Anzac Parade", "suburb": "Kensington", "state": "NSW", "postcode": "2033" } }
|
|
1029
1050
|
]
|
|
1030
1051
|
}
|
|
1031
1052
|
`
|
|
@@ -2093,6 +2114,7 @@ export function startDashboard({ partnerName, port, onQuit, onReplay, stats }: T
|
|
|
2093
2114
|
// nothing, which is indistinguishable from a wedged UI.
|
|
2094
2115
|
let replaying = false;
|
|
2095
2116
|
screen.key(['r'], () => {
|
|
2117
|
+
if (promptOpen) return;
|
|
2096
2118
|
if (!onReplay || replaying) return;
|
|
2097
2119
|
replaying = true;
|
|
2098
2120
|
replayNote = 'Replaying\u2026';
|
|
@@ -2109,32 +2131,84 @@ export function startDashboard({ partnerName, port, onQuit, onReplay, stats }: T
|
|
|
2109
2131
|
.finally(() => { replaying = false; redraw(); });
|
|
2110
2132
|
});
|
|
2111
2133
|
|
|
2134
|
+
// A ONE-LINE PATH PROMPT DRAWN OVER THE DASHBOARD.
|
|
2135
|
+
//
|
|
2136
|
+
// \`screen.key\` handlers are GLOBAL in blessed and keep firing while a textbox
|
|
2137
|
+
// has focus, so [r], [d] and [q] would all act on keystrokes meant for the
|
|
2138
|
+
// path. \`promptOpen\` gates every one of them. Without it, typing a Windows
|
|
2139
|
+
// path containing a \`d\` arms a dismissal behind the prompt, and the \`q\` in
|
|
2140
|
+
// \`C:\\\\Users\\\\qmark\` quits the receiver mid-sentence.
|
|
2141
|
+
let promptOpen = false;
|
|
2142
|
+
function askForPath(initial: string, onAnswer: (dir: string) => void): void {
|
|
2143
|
+
promptOpen = true;
|
|
2144
|
+
const box = blessed.textbox({
|
|
2145
|
+
parent: screen,
|
|
2146
|
+
top: 'center', left: 'center', width: '80%', height: 3,
|
|
2147
|
+
border: { type: 'line' },
|
|
2148
|
+
style: { border: { fg: AMBER }, fg: CREAM },
|
|
2149
|
+
label: ' Export to which folder? Enter accepts, Esc cancels ',
|
|
2150
|
+
inputOnFocus: true,
|
|
2151
|
+
keys: true,
|
|
2152
|
+
});
|
|
2153
|
+
// PRE-FILLED WITH THE RESOLVED DEFAULT, not left empty. The answer to
|
|
2154
|
+
// "where does this go" has to be visible BEFORE the write, and an empty box
|
|
2155
|
+
// asks the partner to know something the dashboard never told them.
|
|
2156
|
+
box.setValue(initial);
|
|
2157
|
+
screen.render();
|
|
2158
|
+
box.readInput((_err, value) => {
|
|
2159
|
+
promptOpen = false;
|
|
2160
|
+
box.destroy();
|
|
2161
|
+
screen.render();
|
|
2162
|
+
// Escape gives no value. Treated as cancel, NOT as "use the default":
|
|
2163
|
+
// a partner who hits Esc has changed their mind about writing a file.
|
|
2164
|
+
if (typeof value !== 'string') { exportNote = 'Export cancelled.'; redraw(); return; }
|
|
2165
|
+
onAnswer(value.trim());
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2112
2169
|
// [e] EXPORTS THE FAULT LIST. Metadata only, no payloads: see exportHeld.
|
|
2113
2170
|
// Bound unconditionally for the same reason [r] is, so pressing it on a clean
|
|
2114
2171
|
// receiver says there is nothing to export rather than appearing to hang.
|
|
2172
|
+
//
|
|
2173
|
+
// IT ASKS WHERE, EVERY TIME (Tim's call). \`exportDir\` in
|
|
2174
|
+
// \`oneaddress.config.json\` and \`ONEADDRESS_EXPORT_DIR\` still decide the
|
|
2175
|
+
// DEFAULT, but they were the only ways to choose and neither is visible from
|
|
2176
|
+
// the dashboard - reported from a real run as "it looks like export location
|
|
2177
|
+
// cannot be picked". Pressing Enter on the pre-filled prompt is byte-for-byte
|
|
2178
|
+
// the old behaviour, so nothing that worked before needs changing.
|
|
2115
2179
|
screen.key(['e'], () => {
|
|
2180
|
+
if (promptOpen) return;
|
|
2116
2181
|
if (heldCount() === 0) {
|
|
2117
2182
|
replayNote = 'Nothing held, so nothing to export.';
|
|
2118
2183
|
redraw();
|
|
2119
2184
|
return;
|
|
2120
2185
|
}
|
|
2186
|
+
askForPath(config.exportDir || process.cwd(), (dir) => { writeExport(dir); redraw(); });
|
|
2187
|
+
});
|
|
2188
|
+
|
|
2189
|
+
/**
|
|
2190
|
+
* Write the fault export, and say where it landed.
|
|
2191
|
+
*
|
|
2192
|
+
* Shared by [e] and [d] rather than duplicated, because [d] exports FIRST and
|
|
2193
|
+
* the two must agree on the destination and on the message. Returns whether
|
|
2194
|
+
* it succeeded, which is what lets [d] refuse to discard anything after a
|
|
2195
|
+
* failed write.
|
|
2196
|
+
*/
|
|
2197
|
+
function writeExport(dir: string): boolean {
|
|
2121
2198
|
try {
|
|
2122
|
-
|
|
2123
|
-
// \`ONEADDRESS_EXPORT_DIR\`, then the working directory, and returns an
|
|
2124
|
-
// ABSOLUTE path. Passing \`process.cwd()\` here is what made the setting
|
|
2125
|
-
// unreachable from the one key that uses it.
|
|
2126
|
-
const { path, count } = exportHeld();
|
|
2199
|
+
const { path, count } = exportHeld(dir);
|
|
2127
2200
|
exportNote = \`Saved \${count} to \${path}\`;
|
|
2128
2201
|
replayNote = '';
|
|
2202
|
+
return true;
|
|
2129
2203
|
} catch (err) {
|
|
2130
2204
|
// NAMES THE DIRECTORY IT TRIED. "EACCES" on its own sends a partner to
|
|
2131
2205
|
// search for a file that was never written.
|
|
2132
|
-
exportNote = \`Export FAILED writing to \${
|
|
2206
|
+
exportNote = \`Export FAILED writing to \${dir || process.cwd()}: \` +
|
|
2133
2207
|
\`\${err instanceof Error ? err.message : String(err)}\`;
|
|
2134
2208
|
replayNote = '';
|
|
2209
|
+
return false;
|
|
2135
2210
|
}
|
|
2136
|
-
|
|
2137
|
-
});
|
|
2211
|
+
}
|
|
2138
2212
|
|
|
2139
2213
|
// [d] DISMISSES. TWO PRESSES, and the first one only asks.
|
|
2140
2214
|
//
|
|
@@ -2145,10 +2219,19 @@ export function startDashboard({ partnerName, port, onQuit, onReplay, stats }: T
|
|
|
2145
2219
|
// working correctly, with nothing on screen that clears it. Reported from a
|
|
2146
2220
|
// real run: "I do wish the conformance wouldn't come up as a fault."
|
|
2147
2221
|
//
|
|
2148
|
-
//
|
|
2149
|
-
//
|
|
2150
|
-
//
|
|
2151
|
-
//
|
|
2222
|
+
// THE RECEIVER DOES RECOGNISE THE PROBE NOW, AND THIS PARAGRAPH USED TO SAY
|
|
2223
|
+
// OTHERWISE. It argued the receiver could not, because "everything that
|
|
2224
|
+
// identifies one is chosen by the sender". That over-weighted the attacker:
|
|
2225
|
+
// anyone able to forge a signed dispatch already holds the webhook secret and
|
|
2226
|
+
// can make this receiver APPLY a bogus address change, so hiding a quarantine
|
|
2227
|
+
// row is not the prize. The real question was signed versus unsigned, and
|
|
2228
|
+
// \`conformance: true\` rides inside the signed body. A drill is classified, not
|
|
2229
|
+
// suppressed - still written, still counted, drawn calmly rather than as an
|
|
2230
|
+
// alarm - so [d] is no longer the only way to clear a passing run.
|
|
2231
|
+
//
|
|
2232
|
+
// IT STILL EXISTS, because a drill is not the only thing that gets held: a
|
|
2233
|
+
// genuinely undecryptable dispatch from a key that has since been rotated is
|
|
2234
|
+
// never going to replay, and without this the panel stays red forever.
|
|
2152
2235
|
//
|
|
2153
2236
|
// IRREVERSIBLE, SO IT IS CONFIRMED. Dismissal discards the held payload, which
|
|
2154
2237
|
// is the point (see \`dismissHeld\`) and also means a mistaken press cannot be
|
|
@@ -2163,6 +2246,7 @@ export function startDashboard({ partnerName, port, onQuit, onReplay, stats }: T
|
|
|
2163
2246
|
// that was described makes the question and the action the same question.
|
|
2164
2247
|
let dismissArmedFor: number | null = null;
|
|
2165
2248
|
screen.key(['d'], () => {
|
|
2249
|
+
if (promptOpen) return;
|
|
2166
2250
|
const held = heldCount();
|
|
2167
2251
|
if (held === 0) {
|
|
2168
2252
|
dismissArmedFor = null;
|
|
@@ -2179,9 +2263,29 @@ export function startDashboard({ partnerName, port, onQuit, onReplay, stats }: T
|
|
|
2179
2263
|
return;
|
|
2180
2264
|
}
|
|
2181
2265
|
dismissArmedFor = null;
|
|
2266
|
+
// EXPORTED BEFORE IT IS DISCARDED, ALWAYS.
|
|
2267
|
+
//
|
|
2268
|
+
// \`exportHeld\` reads \`heldDispatches()\`, which filters out dismissed rows,
|
|
2269
|
+
// so [d] then [e] exported NOTHING and the record was unrecoverable from the
|
|
2270
|
+
// UI. The payload is meant to go - it is an encrypted consumer address on a
|
|
2271
|
+
// third party's disk - but the metadata is the part you would send to
|
|
2272
|
+
// support, and dismissing destroyed exactly that. Reported from a real run:
|
|
2273
|
+
// "if they dismiss a fault, does that mean they cant export and record it?"
|
|
2274
|
+
//
|
|
2275
|
+
// WRITTEN WITHOUT ASKING, unlike [e]. The partner is one keypress from
|
|
2276
|
+
// discarding the evidence and an extra prompt here is a prompt at the worst
|
|
2277
|
+
// possible moment; the default destination is the right answer for a record
|
|
2278
|
+
// nobody asked for. A FAILED write ABORTS the dismissal rather than
|
|
2279
|
+
// proceeding, because "we could not save the record, so we destroyed it" is
|
|
2280
|
+
// the one outcome nothing recovers from.
|
|
2281
|
+
if (!writeExport(config.exportDir || process.cwd())) {
|
|
2282
|
+
replayNote = 'Dismiss ABORTED: nothing was discarded because the record could not be saved.';
|
|
2283
|
+
redraw();
|
|
2284
|
+
return;
|
|
2285
|
+
}
|
|
2182
2286
|
try {
|
|
2183
2287
|
const n = dismissHeld();
|
|
2184
|
-
replayNote = \`Dismissed \${n}. Nothing left held.\`;
|
|
2288
|
+
replayNote = \`Dismissed \${n}. Nothing left held - the record was exported first.\`;
|
|
2185
2289
|
} catch (err) {
|
|
2186
2290
|
replayNote = \`Dismiss failed: \${err instanceof Error ? err.message : String(err)}\`;
|
|
2187
2291
|
}
|
|
@@ -2189,6 +2293,7 @@ export function startDashboard({ partnerName, port, onQuit, onReplay, stats }: T
|
|
|
2189
2293
|
});
|
|
2190
2294
|
|
|
2191
2295
|
screen.key(['q', 'C-c'], () => {
|
|
2296
|
+
if (promptOpen) return;
|
|
2192
2297
|
detach();
|
|
2193
2298
|
screen.destroy();
|
|
2194
2299
|
onQuit();
|
|
@@ -4926,9 +5031,30 @@ type RosterEntry = { account_number: string; name: string; address: Address };
|
|
|
4926
5031
|
// runs out of the box. The wizard writes customers.json with these same demo
|
|
4927
5032
|
// customers \u2014 edit that file, not this list.
|
|
4928
5033
|
const DEFAULT_ROSTER: RosterEntry[] = [
|
|
4929
|
-
{ account_number: 'AUR-583920', name: 'Tim Hooper',
|
|
4930
|
-
{ account_number: 'AUR-104772', name: 'Olivia Robinson',
|
|
4931
|
-
{ account_number: 'AUR-296815', name: 'Marcus Chen',
|
|
5034
|
+
{ account_number: 'AUR-583920', name: 'Tim Hooper', address: { street: '12 Old Mill Road', suburb: 'Parramatta', state: 'NSW', postcode: '2150' } },
|
|
5035
|
+
{ account_number: 'AUR-104772', name: 'Olivia Robinson', address: { street: '5 Rosewood Lane', suburb: 'Brisbane', state: 'QLD', postcode: '4000' } },
|
|
5036
|
+
{ account_number: 'AUR-296815', name: 'Marcus Chen', address: { street: '31 Goldfield Drive', suburb: 'Richmond', state: 'VIC', postcode: '3121' } },
|
|
5037
|
+
{ account_number: 'AUR-337401', name: 'Priya Raghavan', address: { street: '8 Kurrajong Street', suburb: 'Newtown', state: 'NSW', postcode: '2042' } },
|
|
5038
|
+
{ account_number: 'AUR-418266', name: 'Daniel O\\'Sullivan', address: { street: '204/19 Marine Parade', suburb: 'Glenelg', state: 'SA', postcode: '5045' } },
|
|
5039
|
+
{ account_number: 'AUR-452093', name: 'Amara Okafor', address: { street: '77 Beaufort Street', suburb: 'Mount Lawley', state: 'WA', postcode: '6050' } },
|
|
5040
|
+
{ account_number: 'AUR-509118', name: 'Hannah Whitlock', address: { street: '3 Tasman Close', suburb: 'Sandy Bay', state: 'TAS', postcode: '7005' } },
|
|
5041
|
+
{ account_number: 'AUR-563740', name: 'Joshua Nguyen', address: { street: '145 Given Terrace', suburb: 'Paddington', state: 'QLD', postcode: '4064' } },
|
|
5042
|
+
{ account_number: 'AUR-617205', name: 'Isabella Moretti', address: { street: '62 Lygon Street', suburb: 'Carlton', state: 'VIC', postcode: '3053' } },
|
|
5043
|
+
{ account_number: 'AUR-668931', name: 'Samuel Achterberg', address: { street: '11 Hutt Street', suburb: 'Adelaide', state: 'SA', postcode: '5000' } },
|
|
5044
|
+
{ account_number: 'AUR-702884', name: 'Grace Tupou', address: { street: '9 Kingsford Smith Way', suburb: 'Belconnen', state: 'ACT', postcode: '2617' } },
|
|
5045
|
+
{ account_number: 'AUR-745160', name: 'Ethan Marsh', address: { street: '38 Smith Street', suburb: 'Darwin City', state: 'NT', postcode: '0800' } },
|
|
5046
|
+
{ account_number: 'AUR-781422', name: 'Chloe Bennett', address: { street: '26 Rundle Street', suburb: 'Kent Town', state: 'SA', postcode: '5067' } },
|
|
5047
|
+
{ account_number: 'AUR-816559', name: 'Liam Fitzgerald', address: { street: '410/88 Queen Street', suburb: 'Melbourne', state: 'VIC', postcode: '3000' } },
|
|
5048
|
+
{ account_number: 'AUR-849073', name: 'Sofia Delgado', address: { street: '17 Cavill Avenue', suburb: 'Surfers Paradise', state: 'QLD', postcode: '4217' } },
|
|
5049
|
+
{ account_number: 'AUR-873318', name: 'Noah Kirkwood', address: { street: '54 Hunter Street', suburb: 'Newcastle', state: 'NSW', postcode: '2300' } },
|
|
5050
|
+
{ account_number: 'AUR-905642', name: 'Mei Lin Tan', address: { street: '2/33 Rokeby Road', suburb: 'Subiaco', state: 'WA', postcode: '6008' } },
|
|
5051
|
+
{ account_number: 'AUR-938177', name: 'Jack Rasmussen', address: { street: '6 Elizabeth Street', suburb: 'Launceston', state: 'TAS', postcode: '7250' } },
|
|
5052
|
+
{ account_number: 'AUR-960845', name: 'Aaliyah Bennett', address: { street: '82 Charles Street', suburb: 'Northcote', state: 'VIC', postcode: '3070' } },
|
|
5053
|
+
{ account_number: 'AUR-984312', name: 'Oscar Petrov', address: { street: '19 Macquarie Street', suburb: 'Hobart', state: 'TAS', postcode: '7000' } },
|
|
5054
|
+
{ account_number: 'AUR-991026', name: 'Ruby Anderson', address: { street: '301/5 Cordelia Street', suburb: 'South Brisbane', state: 'QLD', postcode: '4101' } },
|
|
5055
|
+
{ account_number: 'AUR-995518', name: 'Henry Abebe', address: { street: '14 Wakefield Street', suburb: 'Kent Town', state: 'SA', postcode: '5067' } },
|
|
5056
|
+
{ account_number: 'AUR-997430', name: 'Zoe Castellano', address: { street: '67 Bay Street', suburb: 'Port Melbourne', state: 'VIC', postcode: '3207' } },
|
|
5057
|
+
{ account_number: 'AUR-999201', name: 'Felix Nakamura', address: { street: '23 Anzac Parade', suburb: 'Kensington', state: 'NSW', postcode: '2033' } },
|
|
4932
5058
|
];
|
|
4933
5059
|
|
|
4934
5060
|
function loadRoster(): RosterEntry[] {
|
|
@@ -6456,8 +6582,9 @@ export async function ask(prompt: string): Promise<string> {
|
|
|
6456
6582
|
* Show what this receiver holds: the roster, and every change it has applied.
|
|
6457
6583
|
*
|
|
6458
6584
|
* Usage:
|
|
6459
|
-
* npm run show \u2014
|
|
6460
|
-
* npm run show 100 \u2014
|
|
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
|
|
6461
6588
|
*
|
|
6462
6589
|
* ## Why this script exists
|
|
6463
6590
|
*
|
|
@@ -6468,6 +6595,23 @@ export async function ask(prompt: string): Promise<string> {
|
|
|
6468
6595
|
* dispatch: "where do I look to prove it end to end?" The dashboard's LAST
|
|
6469
6596
|
* CHANGE panel shows one change and nothing showed the rest.
|
|
6470
6597
|
*
|
|
6598
|
+
* ## Why it STREAMS rather than printing once
|
|
6599
|
+
*
|
|
6600
|
+
* The first version printed a snapshot and exited, which answers "did it land"
|
|
6601
|
+
* only if you already knew to run it again. Watching a dispatch arrive meant
|
|
6602
|
+
* re-running it by hand and diffing two screenfuls in your head. It holds the
|
|
6603
|
+
* screen now and polls, so a change appears as it is applied.
|
|
6604
|
+
*
|
|
6605
|
+
* POLLED, NOT SUBSCRIBED, and that is forced rather than lazy: this is a
|
|
6606
|
+
* SEPARATE PROCESS from the receiver, so there is no event to listen to. SQLite
|
|
6607
|
+
* readers do not block writers, so a poll costs the running receiver nothing.
|
|
6608
|
+
* The screen is only redrawn when the rendered content actually CHANGES, so an
|
|
6609
|
+
* idle receiver is not repainting a terminal once a second.
|
|
6610
|
+
*
|
|
6611
|
+
* NON-TTY FALLS BACK TO ONE-SHOT TEXT. \`npm run show > out.txt\`, a pipe, and a
|
|
6612
|
+
* CI log have no terminal to hold, and a blessed screen drawn into a pipe
|
|
6613
|
+
* produces escape-code soup. Same rule the dashboard itself follows.
|
|
6614
|
+
*
|
|
6471
6615
|
* It prompts for the password for the same reason \`src/index.ts\` does, and for
|
|
6472
6616
|
* the same reason it must do so BEFORE importing the store: the database
|
|
6473
6617
|
* derives its keys on import.
|
|
@@ -6478,8 +6622,17 @@ export async function ask(prompt: string): Promise<string> {
|
|
|
6478
6622
|
* in the connector's own store.
|
|
6479
6623
|
*/
|
|
6480
6624
|
import 'dotenv/config';
|
|
6625
|
+
import blessed from 'blessed';
|
|
6481
6626
|
import { config } from '../src/config.js';
|
|
6482
6627
|
import { ask, databaseIsLocked } from '../src/unlock.js';
|
|
6628
|
+
import { HEX, ONE_ROWS, ADDRESS_ROWS, terminalFitsFullMark } from '../src/brand.js';
|
|
6629
|
+
|
|
6630
|
+
const AMBER = HEX.amber.toLowerCase();
|
|
6631
|
+
const CREAM = HEX.cream.toLowerCase();
|
|
6632
|
+
const DIM = '#8a7f6a';
|
|
6633
|
+
|
|
6634
|
+
/** Escape blessed's tag syntax so a customer's name can never inject markup. */
|
|
6635
|
+
const esc = (s: unknown): string => String(s ?? '').replace(/[{}]/g, '');
|
|
6483
6636
|
|
|
6484
6637
|
function fmt(a: Record<string, string> | null): string {
|
|
6485
6638
|
if (!a) return '(nothing on file)';
|
|
@@ -6495,18 +6648,48 @@ async function askPassword(): Promise<void> {
|
|
|
6495
6648
|
if (answer) process.env.ONEADDRESS_DB_PASSPHRASE = answer;
|
|
6496
6649
|
}
|
|
6497
6650
|
|
|
6498
|
-
|
|
6499
|
-
if (config.mode === 'inbox') {
|
|
6500
|
-
console.log('\\n This receiver is in inbox mode: it holds no customer records and no key.');
|
|
6501
|
-
console.log(' Your connector applied the change and holds the result. Look there.\\n');
|
|
6502
|
-
return;
|
|
6503
|
-
}
|
|
6651
|
+
type Store = typeof import('../src/store.js');
|
|
6504
6652
|
|
|
6505
|
-
|
|
6653
|
+
/**
|
|
6654
|
+
* Does this row answer the filter?
|
|
6655
|
+
*
|
|
6656
|
+
* NAME OR ACCOUNT NUMBER, because a partner checking whether a dispatch landed
|
|
6657
|
+
* knows the person either way round, and typing "hoop" is faster than finding
|
|
6658
|
+
* the account number to type it exactly.
|
|
6659
|
+
*/
|
|
6660
|
+
function matches(query: string, name: string, account: string): boolean {
|
|
6661
|
+
if (!query) return true;
|
|
6662
|
+
const q = query.toLowerCase();
|
|
6663
|
+
return name.toLowerCase().includes(q) || account.toLowerCase().includes(q);
|
|
6664
|
+
}
|
|
6665
|
+
|
|
6666
|
+
/** The roster, as display rows, optionally filtered. */
|
|
6667
|
+
function rosterRows(store: Store, query: string): string[] {
|
|
6668
|
+
return store.allCustomers()
|
|
6669
|
+
.filter((c) => matches(query, c.name, c.account_number))
|
|
6670
|
+
.map((c) => {
|
|
6671
|
+
let addr: Record<string, string> | null = null;
|
|
6672
|
+
try { addr = JSON.parse(c.address) as Record<string, string>; } catch { addr = null; }
|
|
6673
|
+
return \` \${esc(c.account_number).padEnd(14)} \${esc(c.name).padEnd(20)} \${esc(fmt(addr))}\`;
|
|
6674
|
+
});
|
|
6675
|
+
}
|
|
6506
6676
|
|
|
6507
|
-
|
|
6508
|
-
|
|
6677
|
+
/** Applied changes, newest first, as display rows with both sides. */
|
|
6678
|
+
function changeRows(store: Store, limit: number, query: string): string[] {
|
|
6679
|
+
const out: string[] = [];
|
|
6680
|
+
for (const h of store.addressHistory(limit)) {
|
|
6681
|
+
if (!matches(query, h.name, h.account_number)) continue;
|
|
6682
|
+
const prev = h.prev_address as unknown as Record<string, string> | null;
|
|
6683
|
+
const now = h.address as unknown as Record<string, string>;
|
|
6684
|
+
out.push(\` {\${DIM}-fg}\${esc(h.recorded_at)}{/} {\${CREAM}-fg}\${esc(h.account_number)}{/} \${esc(h.name)}\`);
|
|
6685
|
+
out.push(\` {\${DIM}-fg}was:{/} \${esc(fmt(prev))}\`);
|
|
6686
|
+
out.push(\` {\${AMBER}-fg}now:{/} \${esc(fmt(now))}\`);
|
|
6687
|
+
}
|
|
6688
|
+
return out;
|
|
6689
|
+
}
|
|
6509
6690
|
|
|
6691
|
+
/** One-shot plain text. The pipe, the file, the CI log. */
|
|
6692
|
+
function printOnce(store: Store, limit: number): void {
|
|
6510
6693
|
const roster = store.allCustomers();
|
|
6511
6694
|
console.log(\`\\n ON FILE (\${roster.length})\\n\`);
|
|
6512
6695
|
for (const c of roster) {
|
|
@@ -6514,7 +6697,6 @@ async function main(): Promise<void> {
|
|
|
6514
6697
|
try { addr = JSON.parse(c.address) as Record<string, string>; } catch { addr = null; }
|
|
6515
6698
|
console.log(\` \${c.account_number.padEnd(14)} \${c.name.padEnd(24)} \${fmt(addr)}\`);
|
|
6516
6699
|
}
|
|
6517
|
-
|
|
6518
6700
|
const changes = store.addressHistory(limit);
|
|
6519
6701
|
console.log(\`\\n APPLIED CHANGES (\${changes.length}, newest first)\\n\`);
|
|
6520
6702
|
if (changes.length === 0) {
|
|
@@ -6529,6 +6711,167 @@ async function main(): Promise<void> {
|
|
|
6529
6711
|
console.log('');
|
|
6530
6712
|
}
|
|
6531
6713
|
|
|
6714
|
+
/**
|
|
6715
|
+
* The live view: the mark, the roster, the changes, refreshed as they land.
|
|
6716
|
+
*
|
|
6717
|
+
* SEARCH IS THE REASON THIS IS NOT JUST A PRINT. A roster of three fits on a
|
|
6718
|
+
* screen and a realistic one does not, so "is my own record right" stopped
|
|
6719
|
+
* being answerable by looking. \`/\` filters both panels at once on name or
|
|
6720
|
+
* account number, because a partner checking a dispatch landed knows the person
|
|
6721
|
+
* either way round.
|
|
6722
|
+
*/
|
|
6723
|
+
function stream(store: Store, limit: number): void {
|
|
6724
|
+
const screen = blessed.screen({
|
|
6725
|
+
smartCSR: true,
|
|
6726
|
+
title: 'What this receiver holds \u2014 OneAddress',
|
|
6727
|
+
fullUnicode: true,
|
|
6728
|
+
// WITHOUT THIS THE BRAND COMES OUT RED, and it came out red here for a
|
|
6729
|
+
// whole release because this is the SECOND blessed screen in the scaffold
|
|
6730
|
+
// and the fix lived only in the first. blessed resolves a hex colour
|
|
6731
|
+
// against the palette it infers from TERM, which Windows does not set, so
|
|
6732
|
+
// it assumes a tiny palette and snaps #e0a248 to the nearest thing it
|
|
6733
|
+
// believes exists. Naming the capability is telling it the truth.
|
|
6734
|
+
// \`scripts/__tests__\` pins that EVERY screen we open carries this.
|
|
6735
|
+
terminal: process.env.TERM || 'xterm-256color',
|
|
6736
|
+
});
|
|
6737
|
+
|
|
6738
|
+
const wide = terminalFitsFullMark(Number(screen.width));
|
|
6739
|
+
// THE MARK, PLUS A LINE SAYING WHICH SCREEN THIS IS. The dashboard and this
|
|
6740
|
+
// viewer open with the same wordmark, so without a subheader a screenshot of
|
|
6741
|
+
// one is indistinguishable from the other - and they answer different
|
|
6742
|
+
// questions. The dashboard is "is it running"; this is "what does it hold".
|
|
6743
|
+
const markHeight = (wide ? 9 : 3) + 1;
|
|
6744
|
+
blessed.box({
|
|
6745
|
+
parent: screen, top: 0, left: 0, width: '100%', height: markHeight,
|
|
6746
|
+
tags: true, padding: { left: 2 },
|
|
6747
|
+
content: (wide
|
|
6748
|
+
? ONE_ROWS.map((row, i) => \`{\${CREAM}-fg}\${row}{/} {\${AMBER}-fg}\${ADDRESS_ROWS[i]}{/}\`).join('\\n')
|
|
6749
|
+
: \`{\${CREAM}-fg}{bold}One{/bold}{/}{\${AMBER}-fg}{bold}Address{/bold}{/}\`)
|
|
6750
|
+
+ \`\\n{\${DIM}-fg}WHAT THIS RECEIVER HOLDS{/}\`,
|
|
6751
|
+
});
|
|
6752
|
+
|
|
6753
|
+
// THE ROSTER IS BOUNDED AND SCROLLS. It used to size itself to its contents,
|
|
6754
|
+
// which was fine for three customers and pushes the changes panel off the
|
|
6755
|
+
// bottom of the screen for a realistic roster.
|
|
6756
|
+
const usable = Math.max(8, Number(screen.height) - markHeight - 1);
|
|
6757
|
+
const rosterHeight = Math.max(5, Math.min(Math.floor(usable * 0.45), usable - 5));
|
|
6758
|
+
|
|
6759
|
+
const rosterBox = blessed.box({
|
|
6760
|
+
parent: screen, top: markHeight, left: 0, width: '100%', height: rosterHeight,
|
|
6761
|
+
tags: true, padding: { left: 1, right: 1 }, scrollable: true, alwaysScroll: true,
|
|
6762
|
+
border: { type: 'line' }, style: { border: { fg: AMBER } },
|
|
6763
|
+
label: ' ON FILE ',
|
|
6764
|
+
});
|
|
6765
|
+
|
|
6766
|
+
const changesBox = blessed.box({
|
|
6767
|
+
parent: screen, top: markHeight + rosterHeight, left: 0, width: '100%', bottom: 1,
|
|
6768
|
+
tags: true, padding: { left: 1, right: 1 }, scrollable: true, alwaysScroll: true,
|
|
6769
|
+
border: { type: 'line' }, style: { border: { fg: AMBER } },
|
|
6770
|
+
label: ' APPLIED CHANGES \u2014 newest first ',
|
|
6771
|
+
});
|
|
6772
|
+
|
|
6773
|
+
const footer = blessed.box({
|
|
6774
|
+
parent: screen, bottom: 0, left: 0, width: '100%', height: 1,
|
|
6775
|
+
tags: true, padding: { left: 2 },
|
|
6776
|
+
});
|
|
6777
|
+
|
|
6778
|
+
let query = '';
|
|
6779
|
+
let last = '';
|
|
6780
|
+
|
|
6781
|
+
const drawFooter = (): void => {
|
|
6782
|
+
footer.setContent(query
|
|
6783
|
+
? \`{\${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\`);
|
|
6785
|
+
};
|
|
6786
|
+
|
|
6787
|
+
const tick = (force = false): void => {
|
|
6788
|
+
const all = rosterRows(store, '');
|
|
6789
|
+
const roster = rosterRows(store, query);
|
|
6790
|
+
const changes = changeRows(store, limit, query);
|
|
6791
|
+
const next = \`\${query}\\u0000\${roster.join('\\n')}\\u0000\${changes.join('\\n')}\`;
|
|
6792
|
+
if (next === last && !force) return;
|
|
6793
|
+
last = next;
|
|
6794
|
+
|
|
6795
|
+
rosterBox.setLabel(query
|
|
6796
|
+
? \` ON FILE \u2014 \${roster.length} of \${all.length} matching "\${esc(query)}" \`
|
|
6797
|
+
: \` ON FILE \u2014 \${all.length} \`);
|
|
6798
|
+
rosterBox.setContent(roster.join('\\n') ||
|
|
6799
|
+
(query ? \` Nobody matching "\${esc(query)}".\` : ' (nobody on file yet)'));
|
|
6800
|
+
|
|
6801
|
+
changesBox.setLabel(query
|
|
6802
|
+
? \` APPLIED CHANGES \u2014 matching "\${esc(query)}", newest first \`
|
|
6803
|
+
: ' APPLIED CHANGES \u2014 newest first ');
|
|
6804
|
+
changesBox.setContent(changes.join('\\n') || (query
|
|
6805
|
+
? \` No applied change for "\${esc(query)}" yet.\`
|
|
6806
|
+
: ' None yet. Send a dispatch and this fills in.'));
|
|
6807
|
+
|
|
6808
|
+
drawFooter();
|
|
6809
|
+
screen.render();
|
|
6810
|
+
};
|
|
6811
|
+
|
|
6812
|
+
// \`/\` OPENS A LIVE FILTER. It filters as you type rather than on Enter,
|
|
6813
|
+
// because the question being asked is "is my record in here", and the answer
|
|
6814
|
+
// is usually visible three characters in.
|
|
6815
|
+
let searching = false;
|
|
6816
|
+
const openSearch = (): void => {
|
|
6817
|
+
searching = true;
|
|
6818
|
+
const box = blessed.textbox({
|
|
6819
|
+
parent: screen, bottom: 0, left: 0, width: '100%', height: 1,
|
|
6820
|
+
style: { fg: CREAM }, inputOnFocus: true, keys: true,
|
|
6821
|
+
});
|
|
6822
|
+
box.setValue(query);
|
|
6823
|
+
// The filter must update on every keystroke, and \`readInput\` only reports
|
|
6824
|
+
// the final value, so the live half is driven off keypress.
|
|
6825
|
+
box.on('keypress', () => {
|
|
6826
|
+
setImmediate(() => { query = String(box.getValue() ?? '').trim(); tick(); });
|
|
6827
|
+
});
|
|
6828
|
+
screen.render();
|
|
6829
|
+
box.readInput(() => {
|
|
6830
|
+
searching = false;
|
|
6831
|
+
query = String(box.getValue() ?? '').trim();
|
|
6832
|
+
box.destroy();
|
|
6833
|
+
tick(true);
|
|
6834
|
+
});
|
|
6835
|
+
};
|
|
6836
|
+
|
|
6837
|
+
screen.key(['/'], () => { if (!searching) openSearch(); });
|
|
6838
|
+
screen.key(['escape'], () => {
|
|
6839
|
+
if (searching) return;
|
|
6840
|
+
if (!query) return;
|
|
6841
|
+
query = '';
|
|
6842
|
+
tick(true);
|
|
6843
|
+
});
|
|
6844
|
+
screen.key(['q', 'C-c'], () => {
|
|
6845
|
+
if (searching) return;
|
|
6846
|
+
screen.destroy();
|
|
6847
|
+
process.exit(0);
|
|
6848
|
+
});
|
|
6849
|
+
|
|
6850
|
+
tick(true);
|
|
6851
|
+
// DELIBERATELY NOT unref'd. An unref'd timer does not hold the event loop
|
|
6852
|
+
// open, and with the screen drawn there is nothing else pending, so the
|
|
6853
|
+
// process would exit the instant it finished painting.
|
|
6854
|
+
setInterval(() => tick(), 1000);
|
|
6855
|
+
}
|
|
6856
|
+
|
|
6857
|
+
async function main(): Promise<void> {
|
|
6858
|
+
if (config.mode === 'inbox') {
|
|
6859
|
+
console.log('\\n This receiver is in inbox mode: it holds no customer records and no key.');
|
|
6860
|
+
console.log(' Your connector applied the change and holds the result. Look there.\\n');
|
|
6861
|
+
return;
|
|
6862
|
+
}
|
|
6863
|
+
|
|
6864
|
+
await askPassword();
|
|
6865
|
+
|
|
6866
|
+
const limit = Number(process.argv[2]) > 0 ? Number(process.argv[2]) : 20;
|
|
6867
|
+
const store = await import('../src/store.js');
|
|
6868
|
+
|
|
6869
|
+
// A pipe, a file or a CI log has no terminal to hold, and a blessed screen
|
|
6870
|
+
// drawn into one produces escape-code soup rather than output.
|
|
6871
|
+
if (!process.stdout.isTTY) { printOnce(store, limit); return; }
|
|
6872
|
+
stream(store, limit);
|
|
6873
|
+
}
|
|
6874
|
+
|
|
6532
6875
|
void main().catch((err: unknown) => {
|
|
6533
6876
|
console.error(err instanceof Error ? err.message : String(err));
|
|
6534
6877
|
process.exit(1);
|
|
@@ -11903,7 +12246,7 @@ async function scaffold(platform, outputDir, partnerId, webhookSecret, webhookUr
|
|
|
11903
12246
|
|
|
11904
12247
|
// src/register.ts
|
|
11905
12248
|
var import_node_crypto2 = require("crypto");
|
|
11906
|
-
var PKG_VERSION = true ? "2.
|
|
12249
|
+
var PKG_VERSION = true ? "2.8.0" : "dev";
|
|
11907
12250
|
var REGISTER_URL = "https://partners.oneaddress.io/api/partner/installs";
|
|
11908
12251
|
function hmacSha256(secret, message) {
|
|
11909
12252
|
return (0, import_node_crypto2.createHmac)("sha256", secret).update(message).digest("hex");
|
|
@@ -12972,6 +13315,9 @@ OneAddress and a leak of either must not also hand somebody your customers' addr
|
|
|
12972
13315
|
"No private key is collected for an inbox receiver. It belongs in your connector,\nwhich is the only process that decrypts. This one is meant never to hold one, and\nit refuses to start if it finds one in its environment."
|
|
12973
13316
|
);
|
|
12974
13317
|
} else {
|
|
13318
|
+
M2.info(
|
|
13319
|
+
"It looks like one of these:\n -----BEGIN PRIVATE KEY----- (a .pem file, paste the whole thing)\n MIGHAgEAMB... (the base64 body, copied from My Profile)\n C:\\keys\\oneaddress.pem (or just the path to the file)\nThis is your PRIVATE key, not the public one shown next to it in the portal."
|
|
13320
|
+
);
|
|
12975
13321
|
const privateKeyRaw = await he({
|
|
12976
13322
|
message: "Your ECDH Private Key",
|
|
12977
13323
|
placeholder: "Paste the base64 key body from My Profile, or enter a path to the .pem file",
|