@oneaddress/setup 2.2.0 → 2.3.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 +314 -106
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -828,7 +828,7 @@ var Y2 = ({ indicator: t = "dots" } = {}) => {
|
|
|
828
828
|
};
|
|
829
829
|
|
|
830
830
|
// src/prompts.ts
|
|
831
|
-
var
|
|
831
|
+
var import_node_crypto6 = require("crypto");
|
|
832
832
|
var import_node_net = require("net");
|
|
833
833
|
var import_node_fs3 = require("fs");
|
|
834
834
|
var import_node_os2 = require("os");
|
|
@@ -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.3.0" : "?";
|
|
860
860
|
function printCompactHeader() {
|
|
861
861
|
const INNER = 42;
|
|
862
862
|
const TOP = fn("\u250C") + dm("\u2500".repeat(INNER)) + fn("\u2510");
|
|
@@ -909,6 +909,7 @@ function stripAnsi(s) {
|
|
|
909
909
|
|
|
910
910
|
// src/scaffold.ts
|
|
911
911
|
var import_promises = require("fs/promises");
|
|
912
|
+
var import_node_crypto = require("crypto");
|
|
912
913
|
var import_node_fs = require("fs");
|
|
913
914
|
var import_node_path = require("path");
|
|
914
915
|
|
|
@@ -1006,10 +1007,11 @@ data.db-shm
|
|
|
1006
1007
|
{
|
|
1007
1008
|
name: "oneaddress.config.json",
|
|
1008
1009
|
content: `{
|
|
1009
|
-
"//": "Written by npx @oneaddress/setup, read by src/config.ts. Non-secret config only \u2014 secrets live in .env. Any field can be overridden by an env var of the matching name (PARTNER_ID / ONEADDRESS_API / VERIFIES_ACCOUNT_REFERENCE).",
|
|
1010
|
+
"//": "Written by npx @oneaddress/setup, read by src/config.ts. Non-secret config only \u2014 secrets live in .env. Any field can be overridden by an env var of the matching name (PARTNER_ID / ONEADDRESS_API / VERIFIES_ACCOUNT_REFERENCE / RECEIVER_MODE).",
|
|
1010
1011
|
"partnerId": "%%PARTNER_ID%%",
|
|
1011
1012
|
"oneAddressApi": "%%ONEADDRESS_API%%",
|
|
1012
|
-
"verifiesAccountReference": %%VERIFIES_ACCOUNT_REFERENCE
|
|
1013
|
+
"verifiesAccountReference": %%VERIFIES_ACCOUNT_REFERENCE%%,
|
|
1014
|
+
"mode": "%%RECEIVER_MODE%%"
|
|
1013
1015
|
}
|
|
1014
1016
|
`
|
|
1015
1017
|
},
|
|
@@ -1231,6 +1233,34 @@ export { PassphraseRequiredError, WrongPassphraseError };
|
|
|
1231
1233
|
*/
|
|
1232
1234
|
export { isEncrypted };
|
|
1233
1235
|
|
|
1236
|
+
/**
|
|
1237
|
+
* Read a timestamp out of this database, whichever of the two formats it is in.
|
|
1238
|
+
*
|
|
1239
|
+
* THIS DATABASE HOLDS TWO, AND NOTHING DECLARED WHICH WAS WHICH.
|
|
1240
|
+
*
|
|
1241
|
+
* a SQL default, \`datetime('now')\` -> "2026-09-14 22:39:52" UTC, no zone
|
|
1242
|
+
* a JS write, \`toISOString()\` -> "2026-09-14T22:39:52.695Z"
|
|
1243
|
+
*
|
|
1244
|
+
* Both appear in the SAME table: \`quarantine.received_at\` is the first and
|
|
1245
|
+
* \`quarantine.replayed_at\` is the second.
|
|
1246
|
+
*
|
|
1247
|
+
* The first is not valid ISO-8601, so \`new Date(...)\` falls to implementation
|
|
1248
|
+
* -defined parsing and V8 reads it as LOCAL time. A row written a minute ago
|
|
1249
|
+
* therefore reads as an hour old for every hour the machine is from UTC. It
|
|
1250
|
+
* reported \`8h ago\` for a two-minute-old row on a partner's machine in Perth,
|
|
1251
|
+
* which is exactly UTC+8 and is how it was found.
|
|
1252
|
+
*
|
|
1253
|
+
* TOLERANT RATHER THAN MIGRATING, deliberately: a receiver that has been
|
|
1254
|
+
* running has rows in the old shape and rewriting somebody's database to fix a
|
|
1255
|
+
* display bug is the wrong trade. This is the same shape as the transition
|
|
1256
|
+
* -tolerant blind-index reads elsewhere in this project.
|
|
1257
|
+
*/
|
|
1258
|
+
export function parseStoredTime(value: string): Date {
|
|
1259
|
+
// Zone-less \`YYYY-MM-DD HH:MM:SS\` is SQLite's, and SQLite's \`now\` is UTC.
|
|
1260
|
+
const sqlite = /^(\\d{4}-\\d{2}-\\d{2}) (\\d{2}:\\d{2}:\\d{2})$/.exec(value.trim());
|
|
1261
|
+
return new Date(sqlite ? \`\${sqlite[1]}T\${sqlite[2]}Z\` : value);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1234
1264
|
/**
|
|
1235
1265
|
* Add a column to an existing table if it is not already there.
|
|
1236
1266
|
*
|
|
@@ -1680,7 +1710,6 @@ import { formatLine, report, type ReportLine } from './report.js';
|
|
|
1680
1710
|
// One import, same as server.ts. Swapping the store swaps what the dashboard
|
|
1681
1711
|
// reads, with nothing here to change.
|
|
1682
1712
|
import { store } from './store.js';
|
|
1683
|
-
import type { StoredCustomer } from './customer-store.js';
|
|
1684
1713
|
import { pendingConfirmCount } from './confirm-queue.js';
|
|
1685
1714
|
import { exportHeld, heldCount, heldSummary } from './quarantine.js';
|
|
1686
1715
|
|
|
@@ -1701,7 +1730,7 @@ export interface TuiOptions {
|
|
|
1701
1730
|
* Re-apply everything the receiver could not open, bound to [r].
|
|
1702
1731
|
*
|
|
1703
1732
|
* Passed IN rather than imported: \`server.ts\` already imports this file for
|
|
1704
|
-
* \`
|
|
1733
|
+
* \`noteChange\`, so importing it back would be a cycle. Optional so
|
|
1705
1734
|
* the dashboard still renders for a caller that has no replay to offer.
|
|
1706
1735
|
*/
|
|
1707
1736
|
onReplay?: () => Promise<{ applied: number; failed: number }>;
|
|
@@ -1722,10 +1751,26 @@ export interface TuiOptions {
|
|
|
1722
1751
|
};
|
|
1723
1752
|
}
|
|
1724
1753
|
|
|
1725
|
-
/**
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1754
|
+
/**
|
|
1755
|
+
* What the server reports about a dispatch it applied.
|
|
1756
|
+
*
|
|
1757
|
+
* DELIBERATELY CARRIES NO ADDRESS AND NO NAME. This is the whole interface
|
|
1758
|
+
* between the protocol layer and the screen, so anything absent from this type
|
|
1759
|
+
* is something the dashboard cannot render however it is rewritten later. The
|
|
1760
|
+
* address-formatting helper that used to live here went with it: a function
|
|
1761
|
+
* that turns an address into a display string, sitting in the UI module, is an
|
|
1762
|
+
* invitation.
|
|
1763
|
+
*/
|
|
1764
|
+
export interface ChangeFacts {
|
|
1765
|
+
/** The partner's own identifier for their own record. Never the consumer's name. */
|
|
1766
|
+
accountNumber: string;
|
|
1767
|
+
/** Did the account-reference guard run and pass? False means you do not verify them. */
|
|
1768
|
+
accountChecked: boolean;
|
|
1769
|
+
/** Did the record actually move, or was it already current? */
|
|
1770
|
+
changed: boolean;
|
|
1771
|
+
/** Proof of consent, for your audit trail. */
|
|
1772
|
+
loaRef: string | null;
|
|
1773
|
+
dispatchId: string | null;
|
|
1729
1774
|
}
|
|
1730
1775
|
|
|
1731
1776
|
export function startDashboard({ partnerName, port, onQuit, onReplay, stats }: TuiOptions): void {
|
|
@@ -1808,13 +1853,13 @@ export function startDashboard({ partnerName, port, onQuit, onReplay, stats }: T
|
|
|
1808
1853
|
});
|
|
1809
1854
|
|
|
1810
1855
|
// \u2500\u2500 State \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1811
|
-
let lastChange:
|
|
1812
|
-
let pendingPrevious: Record<string, unknown> = {};
|
|
1856
|
+
let lastChange: ChangeFacts | null = null;
|
|
1813
1857
|
|
|
1814
|
-
// The server hands over
|
|
1815
|
-
//
|
|
1816
|
-
//
|
|
1817
|
-
|
|
1858
|
+
// The server hands over FACTS about a dispatch, never the address. See
|
|
1859
|
+
// \`ChangeFacts\` and the note at the call site in server.ts: this module is
|
|
1860
|
+
// the screen, and a screen is the one place a consumer's home address is
|
|
1861
|
+
// most likely to be photographed.
|
|
1862
|
+
setChange = (facts) => { lastChange = facts; redraw(); };
|
|
1818
1863
|
|
|
1819
1864
|
function renderStatus(): void {
|
|
1820
1865
|
// Both states are shown, and the unprotected one is the loud colour. A
|
|
@@ -1846,31 +1891,36 @@ export function startDashboard({ partnerName, port, onQuit, onReplay, stats }: T
|
|
|
1846
1891
|
if (!lastChange) {
|
|
1847
1892
|
changeBox.setContent(
|
|
1848
1893
|
\`\\n {\${DIM}-fg}Waiting for a dispatch.{/}\\n\\n\` +
|
|
1849
|
-
\` {\${DIM}-fg}When one arrives,
|
|
1850
|
-
\` {\${DIM}-fg}
|
|
1894
|
+
\` {\${DIM}-fg}When one arrives, what happened to it appears here.{/}\\n\` +
|
|
1895
|
+
\` {\${DIM}-fg}The address itself never does \u2014 see below.{/}\`,
|
|
1851
1896
|
);
|
|
1852
1897
|
return;
|
|
1853
1898
|
}
|
|
1854
|
-
const
|
|
1855
|
-
|
|
1856
|
-
try { now = JSON.parse(customer.address) as Record<string, unknown>; } catch { /* keep empty */ }
|
|
1857
|
-
// COMPACT, AND THAT IS A BUG FIX RATHER THAN A TIDY-UP.
|
|
1899
|
+
const f = lastChange;
|
|
1900
|
+
// WHAT HAPPENED, NOT WHAT IT SAYS.
|
|
1858
1901
|
//
|
|
1859
|
-
//
|
|
1860
|
-
//
|
|
1861
|
-
//
|
|
1862
|
-
//
|
|
1863
|
-
// it
|
|
1902
|
+
// Every line here is a fact about the dispatch and none of them is content.
|
|
1903
|
+
// An operator watching this can tell that the envelope opened, that the
|
|
1904
|
+
// account was one of theirs, and whether the record actually moved - which
|
|
1905
|
+
// is everything the panel was ever for. To read the address itself, look in
|
|
1906
|
+
// your own database, which is where it lives and which is a deliberate act
|
|
1907
|
+
// rather than a screen left open.
|
|
1864
1908
|
//
|
|
1865
|
-
//
|
|
1866
|
-
//
|
|
1867
|
-
//
|
|
1868
|
-
//
|
|
1909
|
+
// COMPACT ON PURPOSE. The faults band takes rows off this panel when it
|
|
1910
|
+
// appears and blessed clips silently, so a tall panel loses its last line
|
|
1911
|
+
// and reads as a dispatch that half worked. That cost a real end-to-end run
|
|
1912
|
+
// an hour of doubt.
|
|
1913
|
+
const check = f.accountChecked
|
|
1914
|
+
? \`{green-fg}account matched{/}\`
|
|
1915
|
+
: \`{\${DIM}-fg}account not checked{/}\`;
|
|
1916
|
+
const moved = f.changed
|
|
1917
|
+
? \`{green-fg}address changed{/}\`
|
|
1918
|
+
: \`{\${AMBER}-fg}already current{/}\`;
|
|
1869
1919
|
changeBox.setContent(
|
|
1870
|
-
\`\\n {bold}\${esc(
|
|
1871
|
-
\` {
|
|
1872
|
-
\` {
|
|
1873
|
-
\` {
|
|
1920
|
+
\`\\n {\${DIM}-fg}account{/} {\${AMBER}-fg}{bold}\${esc(f.accountNumber)}{/}{/}\\n\\n\` +
|
|
1921
|
+
\` {green-fg}decrypted{/} \${check} \${moved}\\n\\n\` +
|
|
1922
|
+
\` {\${DIM}-fg}loa_ref{/} {\${CREAM}-fg}\${esc(f.loaRef ?? '(none)')}{/}\\n\` +
|
|
1923
|
+
\` {\${DIM}-fg}dispatch{/} {\${CREAM}-fg}\${esc(f.dispatchId ?? '(none)')}{/}\`,
|
|
1874
1924
|
);
|
|
1875
1925
|
}
|
|
1876
1926
|
|
|
@@ -1979,22 +2029,12 @@ export function startDashboard({ partnerName, port, onQuit, onReplay, stats }: T
|
|
|
1979
2029
|
const colour = line.level === 'error' ? 'red' : line.level === 'warn' ? 'yellow' : CREAM;
|
|
1980
2030
|
const time = new Date(line.at).toTimeString().slice(0, 8);
|
|
1981
2031
|
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
// million, which is the scale a real store is pointed at.
|
|
1989
|
-
const prev = pendingPrevious;
|
|
1990
|
-
if (acct) {
|
|
1991
|
-
void Promise.resolve(store.find(acct)).then((customer) => {
|
|
1992
|
-
if (!customer) return;
|
|
1993
|
-
lastChange = { customer, previous: prev };
|
|
1994
|
-
redraw();
|
|
1995
|
-
});
|
|
1996
|
-
}
|
|
1997
|
-
}
|
|
2032
|
+
// NOTHING IS PARSED OUT OF A LOG LINE HERE ANY MORE, and nothing is looked
|
|
2033
|
+
// up from the store. The panel used to scrape the account out of \`[store]
|
|
2034
|
+
// saved address for ...\` and then fetch that customer's record to render
|
|
2035
|
+
// it, which is how a customer's name and address reached the screen. The
|
|
2036
|
+
// server calls \`noteChange\` with facts instead, so this module has no path
|
|
2037
|
+
// to a customer record at all.
|
|
1998
2038
|
|
|
1999
2039
|
logBox.log(\`{\${DIM}-fg}\${time}{/} {\${colour}-fg}\${esc(text)}{/}\`);
|
|
2000
2040
|
redraw();
|
|
@@ -2065,10 +2105,10 @@ export function startDashboard({ partnerName, port, onQuit, onReplay, stats }: T
|
|
|
2065
2105
|
* reason \`report\` has a default sink: \`--headless\` must not need a second code
|
|
2066
2106
|
* path through the handler.
|
|
2067
2107
|
*/
|
|
2068
|
-
let
|
|
2108
|
+
let setChange: (facts: ChangeFacts) => void = () => {};
|
|
2069
2109
|
|
|
2070
|
-
export function
|
|
2071
|
-
|
|
2110
|
+
export function noteChange(facts: ChangeFacts): void {
|
|
2111
|
+
setChange(facts);
|
|
2072
2112
|
}
|
|
2073
2113
|
`
|
|
2074
2114
|
},
|
|
@@ -3118,7 +3158,7 @@ export function resetTally(): void {
|
|
|
3118
3158
|
* replay would apply an address for an account they do not recognise. It stays
|
|
3119
3159
|
* a refusal.
|
|
3120
3160
|
*/
|
|
3121
|
-
import db, { ensureColumn } from './db.js';
|
|
3161
|
+
import db, { ensureColumn, parseStoredTime } from './db.js';
|
|
3122
3162
|
import { report } from './report.js';
|
|
3123
3163
|
import { createHash } from 'node:crypto';
|
|
3124
3164
|
import { writeFileSync } from 'node:fs';
|
|
@@ -3288,7 +3328,10 @@ export function heldSummary(): string[] {
|
|
|
3288
3328
|
|
|
3289
3329
|
/** "3m ago", "2h ago". Coarse on purpose: nobody acts on seconds. */
|
|
3290
3330
|
export function describeAge(iso: string): string {
|
|
3291
|
-
|
|
3331
|
+
// \`parseStoredTime\`, not \`new Date\`. See its docstring: this column is written
|
|
3332
|
+
// by a SQL default in SQLite's zone-less UTC format, which V8 parses as local
|
|
3333
|
+
// time, so a fresh row read as hours old by the machine's UTC offset.
|
|
3334
|
+
const ms = Date.now() - parseStoredTime(iso).getTime();
|
|
3292
3335
|
if (!Number.isFinite(ms) || ms < 0) return 'just now';
|
|
3293
3336
|
const mins = Math.floor(ms / 60_000);
|
|
3294
3337
|
if (mins < 1) return 'moments ago';
|
|
@@ -3324,13 +3367,27 @@ export function markReplayFailed(id: string, error: string): void {
|
|
|
3324
3367
|
* is an update the partner never applied and is now no longer able to.
|
|
3325
3368
|
*/
|
|
3326
3369
|
export function purgeQuarantine(days: number): { replayed: number; unreplayed: number } {
|
|
3327
|
-
|
|
3370
|
+
// THE COMPARISON HAPPENS INSIDE SQLITE, IN SQLITE'S OWN FORMAT, and that is
|
|
3371
|
+
// the fix rather than a tidy-up.
|
|
3372
|
+
//
|
|
3373
|
+
// It used to build an ISO cutoff in JS and compare it against a column
|
|
3374
|
+
// written by a SQL default. SQLite compares those as STRINGS, and the two
|
|
3375
|
+
// formats differ at the separator: ' ' is 0x20 and 'T' is 0x54. So on the
|
|
3376
|
+
// boundary DAY a row still inside the window sorted BEFORE the cutoff and was
|
|
3377
|
+
// deleted, up to about a day early. The table holds a consumer's encrypted
|
|
3378
|
+
// address under a window we have written down, so "saved by the date prefix
|
|
3379
|
+
// usually differing" is not good enough.
|
|
3380
|
+
//
|
|
3381
|
+
// \`datetime('now', ?)\` keeps both sides in one format and one clock. The
|
|
3382
|
+
// modifier is a bound parameter rather than interpolated, so \`days\` cannot
|
|
3383
|
+
// reach the SQL text.
|
|
3384
|
+
const cutoffExpr = \`-\${Math.max(0, Math.floor(days))} days\`;
|
|
3328
3385
|
const doomed = db.prepare(
|
|
3329
|
-
|
|
3330
|
-
).all(
|
|
3386
|
+
"SELECT id, replayed_at FROM quarantine WHERE received_at < datetime('now', ?)",
|
|
3387
|
+
).all(cutoffExpr) as unknown as { id: string; replayed_at: string | null }[];
|
|
3331
3388
|
if (doomed.length === 0) return { replayed: 0, unreplayed: 0 };
|
|
3332
3389
|
|
|
3333
|
-
db.prepare(
|
|
3390
|
+
db.prepare("DELETE FROM quarantine WHERE received_at < datetime('now', ?)").run(cutoffExpr);
|
|
3334
3391
|
|
|
3335
3392
|
const unreplayed = doomed.filter((d) => d.replayed_at === null).length;
|
|
3336
3393
|
const replayed = doomed.length - unreplayed;
|
|
@@ -3956,6 +4013,30 @@ export interface StoredCustomer {
|
|
|
3956
4013
|
address: string;
|
|
3957
4014
|
}
|
|
3958
4015
|
|
|
4016
|
+
/**
|
|
4017
|
+
* Did this dispatch actually change anything?
|
|
4018
|
+
*
|
|
4019
|
+
* THE ONLY QUESTION THE RECEIVER MAY ASK ABOUT AN ADDRESS, and the reason it
|
|
4020
|
+
* exists here rather than inside a store implementation is that the answer is
|
|
4021
|
+
* a single boolean the dashboard can show, where the two addresses are a
|
|
4022
|
+
* customer's home and must not travel any further than the apply.
|
|
4023
|
+
*
|
|
4024
|
+
* Order- and case-insensitive over the WHOLE object, so it keeps working
|
|
4025
|
+
* whatever fields OneAddress adds. \`saveAddress\` returns the address it
|
|
4026
|
+
* replaced; feed that and the incoming one in here, show the boolean, and let
|
|
4027
|
+
* both go.
|
|
4028
|
+
*/
|
|
4029
|
+
export function sameAddress(a: Address, b: Address): boolean {
|
|
4030
|
+
const canonical = (x: Address): string =>
|
|
4031
|
+
JSON.stringify(
|
|
4032
|
+
Object.entries(x)
|
|
4033
|
+
.filter(([, v]) => typeof v === 'string' && (v as string).trim() !== '')
|
|
4034
|
+
.map(([k, v]) => [k.toLowerCase(), (v as string).trim().toLowerCase().replace(/\\s+/g, ' ')] as [string, string])
|
|
4035
|
+
.sort((p, q) => p[0].localeCompare(q[0])),
|
|
4036
|
+
);
|
|
4037
|
+
return canonical(a) === canonical(b);
|
|
4038
|
+
}
|
|
4039
|
+
|
|
3959
4040
|
export type AccountVerdict = 'match' | 'no_match' | 'no_account';
|
|
3960
4041
|
export type VerifyResult = 'match' | 'mismatch' | 'not_found';
|
|
3961
4042
|
|
|
@@ -4449,7 +4530,12 @@ export async function saveAddress(customer: Customer, incoming: Address): Promis
|
|
|
4449
4530
|
// Metadata only \u2014 the address itself is personal information, so the key is
|
|
4450
4531
|
// logged and the address never is. Centralised log aggregation turns every
|
|
4451
4532
|
// log line into a place customer addresses can be read.
|
|
4452
|
-
|
|
4533
|
+
// The ACCOUNT REFERENCE, or a marker - never the name \`acct\` may have fallen
|
|
4534
|
+
// back to. The row is legitimately keyed on the name when you do not verify
|
|
4535
|
+
// account references (see the docstring above); logging it is a different
|
|
4536
|
+
// decision, and the wrong one.
|
|
4537
|
+
const loggable = (customer.accountNumber ?? '').trim() || '(name-keyed record)';
|
|
4538
|
+
report.info(\`[store] saved address for \${loggable}\${customer.loaRef ? \` (loa_ref \${customer.loaRef})\` : ''}\`);
|
|
4453
4539
|
|
|
4454
4540
|
return previous;
|
|
4455
4541
|
}
|
|
@@ -4538,9 +4624,10 @@ import {
|
|
|
4538
4624
|
// the protocol layer changes.
|
|
4539
4625
|
import { store as writeThroughStore } from './store.js';
|
|
4540
4626
|
import { connectorStore, setCurrentRawBody } from './connector-store.js';
|
|
4541
|
-
import {
|
|
4627
|
+
import { noteChange } from './tui.js';
|
|
4542
4628
|
import { config } from './config.js';
|
|
4543
4629
|
import { safeOneAddressCallbackUrl } from './callback-url.js';
|
|
4630
|
+
import { sameAddress } from './customer-store.js';
|
|
4544
4631
|
import { configuredKeyIds, describeKeys, keyFailureAdvice, resolvePrivateKey } from './keys.js';
|
|
4545
4632
|
import {
|
|
4546
4633
|
drainConfirms,
|
|
@@ -5157,7 +5244,12 @@ app.post('/webhook', async (req: Request, res: Response) => {
|
|
|
5157
5244
|
|
|
5158
5245
|
// \u2500\u2500 address.updated: consumer changed their address \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
5159
5246
|
if (event === 'address.updated') {
|
|
5160
|
-
|
|
5247
|
+
// THE ACCOUNT, NEVER THE NAME. This used to fall back to \`ctx.name\`, so a
|
|
5248
|
+
// partner who does not verify account references wrote a real consumer's
|
|
5249
|
+
// name into every log line, and from there into whatever aggregates their
|
|
5250
|
+
// logs. The account reference is the partner's own identifier for their own
|
|
5251
|
+
// record; the name is the consumer's.
|
|
5252
|
+
report.info(\`[webhook] address.updated for \${ctx.accountNumber || '(no account reference)'}\`);
|
|
5161
5253
|
|
|
5162
5254
|
// AN ACCOUNT REFERENCE THAT MATCHES NOTHING MUST BE A HARD NO-MATCH.
|
|
5163
5255
|
//
|
|
@@ -5196,12 +5288,32 @@ app.post('/webhook', async (req: Request, res: Response) => {
|
|
|
5196
5288
|
}
|
|
5197
5289
|
}
|
|
5198
5290
|
|
|
5199
|
-
//
|
|
5200
|
-
//
|
|
5201
|
-
//
|
|
5202
|
-
//
|
|
5291
|
+
// THE DASHBOARD IS HANDED FACTS, NOT THE ADDRESS, and that is structural
|
|
5292
|
+
// rather than a formatting choice.
|
|
5293
|
+
//
|
|
5294
|
+
// It used to receive the address this dispatch REPLACED and look the new
|
|
5295
|
+
// one up from the store, and it painted both on a panel. Everything around
|
|
5296
|
+
// here is careful that an address never reaches a log line - two comments
|
|
5297
|
+
// one screen apart say so - and then it was drawn in large text on a screen
|
|
5298
|
+
// that gets screenshotted, screen-shared and left open in an office.
|
|
5299
|
+
//
|
|
5300
|
+
// The receiver does not need to show an address to prove it works. It needs
|
|
5301
|
+
// to show that the envelope opened, that the account was one of yours, and
|
|
5302
|
+
// whether anything actually changed. Those are the facts below, and nothing
|
|
5303
|
+
// that reaches \`tui.ts\` can be turned back into a consumer's home.
|
|
5304
|
+
//
|
|
5305
|
+
// There is nothing here to sneak past, either, which is why this is better
|
|
5306
|
+
// than marking test dispatches as safe to display: a marker is something an
|
|
5307
|
+
// attacker can try to forge onto a real dispatch, and a panel that never
|
|
5308
|
+
// renders an address has nothing to forge it into.
|
|
5203
5309
|
const replaced = await store.saveAddress(ctx, address);
|
|
5204
|
-
|
|
5310
|
+
noteChange({
|
|
5311
|
+
accountNumber: ctx.accountNumber || '(no account reference)',
|
|
5312
|
+
accountChecked: config.verifiesAccountReference,
|
|
5313
|
+
changed: !sameAddress(replaced, address),
|
|
5314
|
+
loaRef,
|
|
5315
|
+
dispatchId: dispatch || null,
|
|
5316
|
+
});
|
|
5205
5317
|
if (dispatch) rememberDispatch(dispatch); // remember only after it is stored
|
|
5206
5318
|
// Close the loop back to OneAddress so the service flips to "Confirmed".
|
|
5207
5319
|
// QUEUED, not sent: this is a local INSERT, so it cannot delay the 200 that
|
|
@@ -5214,7 +5326,7 @@ app.post('/webhook', async (req: Request, res: Response) => {
|
|
|
5214
5326
|
|
|
5215
5327
|
// \u2500\u2500 address.verify: consumer is running an address check \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
5216
5328
|
if (event === 'address.verify') {
|
|
5217
|
-
report.info(\`[webhook] address.verify for \${ctx.accountNumber ||
|
|
5329
|
+
report.info(\`[webhook] address.verify for \${ctx.accountNumber || '(no account reference)'}\`);
|
|
5218
5330
|
const result = await store.verifyAddress(ctx, address);
|
|
5219
5331
|
report.info(\`[webhook] address.verify \u2192 \${result}\`);
|
|
5220
5332
|
const refused = await postVerifyVerdict(result);
|
|
@@ -10623,11 +10735,49 @@ npx @oneaddress/conformance test %%WEBHOOK_URL%%
|
|
|
10623
10735
|
};
|
|
10624
10736
|
|
|
10625
10737
|
// src/scaffold.ts
|
|
10626
|
-
function applyTokens(content, partnerId, webhookSecret, webhookUrl, privateKey, verifiesAccountReference, oneAddressApi) {
|
|
10627
|
-
|
|
10738
|
+
function applyTokens(content, partnerId, webhookSecret, webhookUrl, privateKey, verifiesAccountReference, oneAddressApi, mode, connectorToken) {
|
|
10739
|
+
const filled = content.replaceAll("%%PARTNER_ID%%", partnerId).replaceAll("%%WEBHOOK_SECRET%%", webhookSecret).replaceAll("%%WEBHOOK_URL%%", webhookUrl || "<your-webhook-url>").replaceAll("%%PRIVATE_KEY%%", privateKey || "<paste your PKCS8 PEM private key here>").replaceAll("%%ONEADDRESS_API%%", oneAddressApi || "https://oneaddress.io").replaceAll("%%VERIFIES_ACCOUNT_REFERENCE%%", verifiesAccountReference ? "true" : "false").replaceAll("%%RECEIVER_MODE%%", mode === "inbox" ? "inbox" : "write-through");
|
|
10740
|
+
if (mode !== "inbox") return filled;
|
|
10741
|
+
const withReadme = filled.replace(
|
|
10742
|
+
"## Architecture",
|
|
10743
|
+
`## Inbox mode
|
|
10744
|
+
|
|
10745
|
+
This receiver holds dispatches as ciphertext and keeps **no private key** - it refuses to
|
|
10746
|
+
start with one - so it cannot read what it stores. A **connector** you run separately draws
|
|
10747
|
+
from it, decrypts, applies the change to your systems and acknowledges; only then does
|
|
10748
|
+
OneAddress confirm the update to the consumer.
|
|
10749
|
+
|
|
10750
|
+
Until that connector runs, updates are held and the dashboard counts them as awaiting your
|
|
10751
|
+
systems. Nothing is lost and nothing is applied.
|
|
10752
|
+
|
|
10753
|
+
| What | Where |
|
|
10754
|
+
|------|-------|
|
|
10755
|
+
| The decryption key | Your connector's environment, never this one |
|
|
10756
|
+
| \`CONNECTOR_TOKEN\` | \`.env\` here, and the same value in your connector |
|
|
10757
|
+
| The draw channel | \`http://127.0.0.1:3002\`, loopback only |
|
|
10758
|
+
|
|
10759
|
+
\`CONNECTOR_TOKEN\` is its own credential: **not** your webhook secret and **not** your
|
|
10760
|
+
confirm secret. Those two are shared with OneAddress, and a leak of either must not also
|
|
10761
|
+
hand somebody your customers' addresses.
|
|
10762
|
+
|
|
10763
|
+
## Architecture`
|
|
10764
|
+
);
|
|
10765
|
+
return withReadme.replace(
|
|
10766
|
+
/^PARTNER_PRIVATE_KEY_PEM=.*$/m,
|
|
10767
|
+
`# No PARTNER_PRIVATE_KEY_PEM here, on purpose: this receiver runs in inbox
|
|
10768
|
+
# mode and refuses to start with one. The key belongs in your connector.
|
|
10769
|
+
|
|
10770
|
+
# The credential your connector presents to draw from this inbox. It must
|
|
10771
|
+
# match CONNECTOR_TOKEN in the connector's own environment, and it must not
|
|
10772
|
+
# be your webhook secret or your confirm secret.
|
|
10773
|
+
CONNECTOR_TOKEN=${connectorToken || generateConnectorToken()}`
|
|
10774
|
+
);
|
|
10775
|
+
}
|
|
10776
|
+
function generateConnectorToken() {
|
|
10777
|
+
return (0, import_node_crypto.randomBytes)(24).toString("hex");
|
|
10628
10778
|
}
|
|
10629
10779
|
var SENSITIVE_FILES = /* @__PURE__ */ new Set([".env"]);
|
|
10630
|
-
async function scaffold(platform, outputDir, partnerId, webhookSecret, webhookUrl, privateKey = "", verifiesAccountReference = false, oneAddressApi = "https://oneaddress.io") {
|
|
10780
|
+
async function scaffold(platform, outputDir, partnerId, webhookSecret, webhookUrl, privateKey = "", verifiesAccountReference = false, oneAddressApi = "https://oneaddress.io", mode = "write-through", connectorToken = "") {
|
|
10631
10781
|
const templates = TEMPLATES[platform];
|
|
10632
10782
|
if (!templates) throw new Error(`Unknown platform: ${platform}`);
|
|
10633
10783
|
const written = [];
|
|
@@ -10637,7 +10787,7 @@ async function scaffold(platform, outputDir, partnerId, webhookSecret, webhookUr
|
|
|
10637
10787
|
if (!(0, import_node_fs.existsSync)(dir)) {
|
|
10638
10788
|
await (0, import_promises.mkdir)(dir, { recursive: true });
|
|
10639
10789
|
}
|
|
10640
|
-
const filled = applyTokens(content, partnerId, webhookSecret, webhookUrl, privateKey, verifiesAccountReference, oneAddressApi);
|
|
10790
|
+
const filled = applyTokens(content, partnerId, webhookSecret, webhookUrl, privateKey, verifiesAccountReference, oneAddressApi, mode, connectorToken);
|
|
10641
10791
|
const isSensitive = SENSITIVE_FILES.has((0, import_node_path.basename)(name));
|
|
10642
10792
|
await (0, import_promises.writeFile)(dest, filled, { encoding: "utf8", mode: isSensitive ? 384 : 420 });
|
|
10643
10793
|
if (isSensitive && process.platform !== "win32") {
|
|
@@ -10652,11 +10802,11 @@ async function scaffold(platform, outputDir, partnerId, webhookSecret, webhookUr
|
|
|
10652
10802
|
}
|
|
10653
10803
|
|
|
10654
10804
|
// src/register.ts
|
|
10655
|
-
var
|
|
10656
|
-
var PKG_VERSION = true ? "2.
|
|
10805
|
+
var import_node_crypto2 = require("crypto");
|
|
10806
|
+
var PKG_VERSION = true ? "2.3.0" : "dev";
|
|
10657
10807
|
var REGISTER_URL = "https://partners.oneaddress.io/api/partner/installs";
|
|
10658
10808
|
function hmacSha256(secret, message) {
|
|
10659
|
-
return (0,
|
|
10809
|
+
return (0, import_node_crypto2.createHmac)("sha256", secret).update(message).digest("hex");
|
|
10660
10810
|
}
|
|
10661
10811
|
async function registerInstall(installId, partnerId, webhookSecret, platform, webhookUrl) {
|
|
10662
10812
|
try {
|
|
@@ -10689,10 +10839,10 @@ async function registerInstall(installId, partnerId, webhookSecret, platform, we
|
|
|
10689
10839
|
}
|
|
10690
10840
|
|
|
10691
10841
|
// src/conformance.ts
|
|
10692
|
-
var
|
|
10842
|
+
var import_node_crypto3 = require("crypto");
|
|
10693
10843
|
async function runConformance(webhookUrl, _partnerId, webhookSecret) {
|
|
10694
10844
|
function sign(ts, body) {
|
|
10695
|
-
return (0,
|
|
10845
|
+
return (0, import_node_crypto3.createHmac)("sha256", webhookSecret).update(`${ts}.${body}`).digest("hex");
|
|
10696
10846
|
}
|
|
10697
10847
|
const validBody = JSON.stringify({ event: "conformance.ping", test: true });
|
|
10698
10848
|
const now = String(Math.floor(Date.now() / 1e3));
|
|
@@ -10733,7 +10883,7 @@ async function runConformance(webhookUrl, _partnerId, webhookSecret) {
|
|
|
10733
10883
|
// Unique per test run so a partner re-running `npm test` doesn't
|
|
10734
10884
|
// hit the scaffolded server's dedup cache and short-circuit the
|
|
10735
10885
|
// "accepted" assertion with { ok: true, duplicate: true }.
|
|
10736
|
-
"X-OneAddress-Dispatch": `conformance-${(0,
|
|
10886
|
+
"X-OneAddress-Dispatch": `conformance-${(0, import_node_crypto3.randomUUID)()}`
|
|
10737
10887
|
},
|
|
10738
10888
|
body: validBody,
|
|
10739
10889
|
signal: AbortSignal.timeout(8e3)
|
|
@@ -10785,7 +10935,7 @@ async function runDecryptCheck(webhookSecret, partnerId) {
|
|
|
10785
10935
|
async function callOnce() {
|
|
10786
10936
|
try {
|
|
10787
10937
|
const ts = String(Math.floor(Date.now() / 1e3));
|
|
10788
|
-
const sig = (0,
|
|
10938
|
+
const sig = (0, import_node_crypto3.createHmac)("sha256", webhookSecret).update(`${ts}.`).digest("hex");
|
|
10789
10939
|
const res2 = await fetch(VERIFY_DECRYPT_API, {
|
|
10790
10940
|
method: "POST",
|
|
10791
10941
|
headers: {
|
|
@@ -10860,24 +11010,46 @@ var COMMANDS = {
|
|
|
10860
11010
|
function installDependencies(platform, outputDir) {
|
|
10861
11011
|
const spec = COMMANDS[platform];
|
|
10862
11012
|
if (!spec) {
|
|
10863
|
-
return { ok: true, output: "", manualCommand: "" };
|
|
11013
|
+
return Promise.resolve({ ok: true, output: "", manualCommand: "" });
|
|
10864
11014
|
}
|
|
10865
11015
|
const cwd = spec.cwd ? (0, import_node_path2.join)(outputDir, spec.cwd) : outputDir;
|
|
10866
11016
|
const manualCommand = `cd ${outputDir} && ${spec.cmd} ${spec.args.join(" ")}`;
|
|
10867
|
-
|
|
10868
|
-
|
|
10869
|
-
|
|
10870
|
-
|
|
10871
|
-
|
|
10872
|
-
|
|
10873
|
-
|
|
11017
|
+
return new Promise((resolve2) => {
|
|
11018
|
+
const child = (0, import_node_child_process.spawn)(spec.cmd, spec.args, {
|
|
11019
|
+
cwd,
|
|
11020
|
+
stdio: "pipe",
|
|
11021
|
+
shell: process.platform === "win32"
|
|
11022
|
+
});
|
|
11023
|
+
let output = "";
|
|
11024
|
+
const collect = (chunk) => {
|
|
11025
|
+
if (output.length < 64e3) output += chunk.toString("utf8");
|
|
11026
|
+
};
|
|
11027
|
+
child.stdout?.on("data", collect);
|
|
11028
|
+
child.stderr?.on("data", collect);
|
|
11029
|
+
let settled = false;
|
|
11030
|
+
const finish = (result) => {
|
|
11031
|
+
if (settled) return;
|
|
11032
|
+
settled = true;
|
|
11033
|
+
clearTimeout(timer);
|
|
11034
|
+
resolve2(result);
|
|
11035
|
+
};
|
|
11036
|
+
const timer = setTimeout(() => {
|
|
11037
|
+
child.kill("SIGKILL");
|
|
11038
|
+
finish({
|
|
11039
|
+
ok: false,
|
|
11040
|
+
output: `${output.trim()}
|
|
11041
|
+
|
|
11042
|
+
Timed out after 5 minutes.`.trim(),
|
|
11043
|
+
manualCommand
|
|
11044
|
+
});
|
|
11045
|
+
}, 5 * 60 * 1e3);
|
|
11046
|
+
child.on("error", (err) => {
|
|
11047
|
+
finish({ ok: false, output: `${output}${err.message}`.trim(), manualCommand });
|
|
11048
|
+
});
|
|
11049
|
+
child.on("close", (code) => {
|
|
11050
|
+
finish({ ok: code === 0, output: output.trim(), manualCommand });
|
|
11051
|
+
});
|
|
10874
11052
|
});
|
|
10875
|
-
const output = [result.stdout ?? "", result.stderr ?? ""].join("").trim();
|
|
10876
|
-
return {
|
|
10877
|
-
ok: result.status === 0 && !result.error,
|
|
10878
|
-
output,
|
|
10879
|
-
manualCommand
|
|
10880
|
-
};
|
|
10881
11053
|
}
|
|
10882
11054
|
|
|
10883
11055
|
// src/autostart.ts
|
|
@@ -11062,7 +11234,7 @@ async function handOverTerminal(platform, outputDir, port, secrets = {}) {
|
|
|
11062
11234
|
var import_node_child_process3 = require("child_process");
|
|
11063
11235
|
var import_promises2 = require("fs/promises");
|
|
11064
11236
|
var import_node_fs2 = require("fs");
|
|
11065
|
-
var
|
|
11237
|
+
var import_node_crypto4 = require("crypto");
|
|
11066
11238
|
var import_node_path3 = require("path");
|
|
11067
11239
|
var import_node_os = __toESM(require("os"));
|
|
11068
11240
|
var tunnelProcess = null;
|
|
@@ -11101,7 +11273,7 @@ function platformKey() {
|
|
|
11101
11273
|
return "linux-x64";
|
|
11102
11274
|
}
|
|
11103
11275
|
async function sha256File(path) {
|
|
11104
|
-
const hash = (0,
|
|
11276
|
+
const hash = (0, import_node_crypto4.createHash)("sha256");
|
|
11105
11277
|
hash.update(await (0, import_promises2.readFile)(path));
|
|
11106
11278
|
return hash.digest("hex");
|
|
11107
11279
|
}
|
|
@@ -11235,7 +11407,7 @@ async function startTunnel(port) {
|
|
|
11235
11407
|
}
|
|
11236
11408
|
|
|
11237
11409
|
// src/register-url.ts
|
|
11238
|
-
var
|
|
11410
|
+
var import_node_crypto5 = require("crypto");
|
|
11239
11411
|
var PORTAL_API = "https://partners.oneaddress.io/api/partner/webhook-url";
|
|
11240
11412
|
function describeReadFailure(status) {
|
|
11241
11413
|
if (status === 401) return "the portal rejected the signature (HTTP 401), which means your Webhook signing secret does not match the one on your profile. Copy it again from partners.oneaddress.io \u2192 Webhook \u2192 Webhook signing secret.";
|
|
@@ -11246,7 +11418,7 @@ function describeReadFailure(status) {
|
|
|
11246
11418
|
async function getExistingWebhookUrl(partnerId, webhookSecret, onError) {
|
|
11247
11419
|
try {
|
|
11248
11420
|
const ts = String(Math.floor(Date.now() / 1e3));
|
|
11249
|
-
const sig = (0,
|
|
11421
|
+
const sig = (0, import_node_crypto5.createHmac)("sha256", webhookSecret).update(`${ts}.`).digest("hex");
|
|
11250
11422
|
const res = await fetch(PORTAL_API, {
|
|
11251
11423
|
method: "GET",
|
|
11252
11424
|
headers: {
|
|
@@ -11270,7 +11442,7 @@ async function getExistingWebhookUrl(partnerId, webhookSecret, onError) {
|
|
|
11270
11442
|
async function getVerifiesAccountReference(partnerId, webhookSecret, onError) {
|
|
11271
11443
|
try {
|
|
11272
11444
|
const ts = String(Math.floor(Date.now() / 1e3));
|
|
11273
|
-
const sig = (0,
|
|
11445
|
+
const sig = (0, import_node_crypto5.createHmac)("sha256", webhookSecret).update(`${ts}.`).digest("hex");
|
|
11274
11446
|
const res = await fetch(PORTAL_API, {
|
|
11275
11447
|
method: "GET",
|
|
11276
11448
|
headers: {
|
|
@@ -11297,7 +11469,7 @@ async function getVerifiesAccountReference(partnerId, webhookSecret, onError) {
|
|
|
11297
11469
|
async function verifyWebhookSecret(partnerId, webhookSecret) {
|
|
11298
11470
|
try {
|
|
11299
11471
|
const ts = String(Math.floor(Date.now() / 1e3));
|
|
11300
|
-
const sig = (0,
|
|
11472
|
+
const sig = (0, import_node_crypto5.createHmac)("sha256", webhookSecret).update(`${ts}.`).digest("hex");
|
|
11301
11473
|
const res = await fetch(PORTAL_API, {
|
|
11302
11474
|
method: "GET",
|
|
11303
11475
|
headers: {
|
|
@@ -11318,7 +11490,7 @@ async function registerWebhookUrl(partnerId, webhookSecret, webhookUrl) {
|
|
|
11318
11490
|
try {
|
|
11319
11491
|
const body = JSON.stringify({ webhook_url: webhookUrl });
|
|
11320
11492
|
const ts = String(Math.floor(Date.now() / 1e3));
|
|
11321
|
-
const sig = (0,
|
|
11493
|
+
const sig = (0, import_node_crypto5.createHmac)("sha256", webhookSecret).update(`${ts}.${body}`).digest("hex");
|
|
11322
11494
|
const res = await fetch(PORTAL_API, {
|
|
11323
11495
|
method: "PATCH",
|
|
11324
11496
|
headers: {
|
|
@@ -11515,7 +11687,7 @@ ${wrapped}
|
|
|
11515
11687
|
-----END EC PRIVATE KEY-----
|
|
11516
11688
|
`;
|
|
11517
11689
|
try {
|
|
11518
|
-
const pkcs8 = (0,
|
|
11690
|
+
const pkcs8 = (0, import_node_crypto6.createPrivateKey)({ key: sec1Pem, format: "pem", type: "sec1" }).export({ type: "pkcs8", format: "pem" });
|
|
11519
11691
|
return { pem: pkcs8 };
|
|
11520
11692
|
} catch (err) {
|
|
11521
11693
|
return { pem: "", error: translateKeyError(err) };
|
|
@@ -11559,7 +11731,7 @@ function translateKeyError(err) {
|
|
|
11559
11731
|
function validatePrivateKey(pem) {
|
|
11560
11732
|
let key;
|
|
11561
11733
|
try {
|
|
11562
|
-
key = (0,
|
|
11734
|
+
key = (0, import_node_crypto6.createPrivateKey)({ key: pem, format: "pem", type: "pkcs8" });
|
|
11563
11735
|
} catch (err) {
|
|
11564
11736
|
return translateKeyError(err);
|
|
11565
11737
|
}
|
|
@@ -11680,6 +11852,39 @@ async function main() {
|
|
|
11680
11852
|
accountRefReason = r2;
|
|
11681
11853
|
});
|
|
11682
11854
|
const verifiesAccountReference = accountRefDeclaration === true;
|
|
11855
|
+
const modeChoice = platform !== "ts-node" ? "write-through" : await ve({
|
|
11856
|
+
message: "How should this receiver handle updates?",
|
|
11857
|
+
options: [
|
|
11858
|
+
{
|
|
11859
|
+
value: "write-through",
|
|
11860
|
+
label: "Apply them itself (simplest)",
|
|
11861
|
+
hint: "Decrypts and writes to a customer store on this machine. Right for a sole trader, or to try it out."
|
|
11862
|
+
},
|
|
11863
|
+
{
|
|
11864
|
+
value: "inbox",
|
|
11865
|
+
label: "Hold them for your own systems to collect",
|
|
11866
|
+
hint: "Holds ciphertext and keeps NO private key, so this process cannot read what it stores. Needs a connector you run separately."
|
|
11867
|
+
}
|
|
11868
|
+
],
|
|
11869
|
+
initialValue: "write-through"
|
|
11870
|
+
});
|
|
11871
|
+
assertNotCancelled(modeChoice);
|
|
11872
|
+
const receiverMode = modeChoice;
|
|
11873
|
+
const connectorToken = receiverMode === "inbox" ? generateConnectorToken() : "";
|
|
11874
|
+
if (receiverMode === "inbox") {
|
|
11875
|
+
M2.warn(
|
|
11876
|
+
"Inbox mode needs a second process. This receiver will hold dispatches as ciphertext and\nkeep NO private key \u2014 it refuses to start with one \u2014 so a connector of yours must draw,\ndecrypt and apply them, then acknowledge. Until that connector runs, updates are held and\nthe dashboard counts them as awaiting your systems."
|
|
11877
|
+
);
|
|
11878
|
+
M2.info(
|
|
11879
|
+
`Your connector credential (also written to .env as CONNECTOR_TOKEN):
|
|
11880
|
+
|
|
11881
|
+
${connectorToken}
|
|
11882
|
+
|
|
11883
|
+
Set the same value as CONNECTOR_TOKEN in your connector. It is its own credential:
|
|
11884
|
+
not your webhook secret and not your confirm secret, because those are shared with
|
|
11885
|
+
OneAddress and a leak of either must not also hand somebody your customers' addresses.`
|
|
11886
|
+
);
|
|
11887
|
+
}
|
|
11683
11888
|
if (accountRefDeclaration === void 0) {
|
|
11684
11889
|
M2.warn(
|
|
11685
11890
|
"Could not read your account-reference declaration from the portal.\n" + (accountRefReason ? "Reason: " + accountRefReason + "\n" : "") + "Conformance check-14 has been left out of the generated `npm test`, so a\npassing run does NOT mean your receiver matches account references correctly.\nSet it at partners.oneaddress.io \u2192 My Profile, then re-run this wizard."
|
|
@@ -11697,7 +11902,10 @@ async function main() {
|
|
|
11697
11902
|
"",
|
|
11698
11903
|
// webhookUrl — filled in after server starts
|
|
11699
11904
|
privateKey,
|
|
11700
|
-
verifiesAccountReference
|
|
11905
|
+
verifiesAccountReference,
|
|
11906
|
+
void 0,
|
|
11907
|
+
receiverMode,
|
|
11908
|
+
connectorToken
|
|
11701
11909
|
);
|
|
11702
11910
|
s1.stop(`Scaffolded ${written.length} files in ${outDir}`);
|
|
11703
11911
|
for (const f of written) M2.success(` ${f}`);
|
|
@@ -11709,7 +11917,7 @@ async function main() {
|
|
|
11709
11917
|
for (; ; ) {
|
|
11710
11918
|
const s2 = Y2();
|
|
11711
11919
|
s2.start("Installing dependencies (this can take 30\u201360 s)");
|
|
11712
|
-
const install = installDependencies(platform, outDir);
|
|
11920
|
+
const install = await installDependencies(platform, outDir);
|
|
11713
11921
|
if (install.ok) {
|
|
11714
11922
|
s2.stop("Dependencies installed");
|
|
11715
11923
|
break;
|
|
@@ -11879,7 +12087,7 @@ one of two things, and neither is your server (it is running on :3001):
|
|
|
11879
12087
|
);
|
|
11880
12088
|
}
|
|
11881
12089
|
registerInstall(
|
|
11882
|
-
`OA-${(0,
|
|
12090
|
+
`OA-${(0, import_node_crypto6.randomUUID)()}`,
|
|
11883
12091
|
pid,
|
|
11884
12092
|
secret,
|
|
11885
12093
|
platform,
|