@oneaddress/setup 2.5.0 → 2.6.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 +217 -31
- 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.6.0" : "?";
|
|
860
860
|
function printCompactHeader() {
|
|
861
861
|
const INNER = 42;
|
|
862
862
|
const TOP = fn("\u250C") + dm("\u2500".repeat(INNER)) + fn("\u2510");
|
|
@@ -1013,7 +1013,8 @@ data.db-shm
|
|
|
1013
1013
|
"oneAddressApi": "%%ONEADDRESS_API%%",
|
|
1014
1014
|
"verifiesAccountReference": %%VERIFIES_ACCOUNT_REFERENCE%%,
|
|
1015
1015
|
"mode": "%%RECEIVER_MODE%%",
|
|
1016
|
-
"publicUrl": "%%WEBHOOK_URL%%"
|
|
1016
|
+
"publicUrl": "%%WEBHOOK_URL%%",
|
|
1017
|
+
"exportDir": ""
|
|
1017
1018
|
}
|
|
1018
1019
|
`
|
|
1019
1020
|
},
|
|
@@ -1713,7 +1714,8 @@ import { formatLine, report, type ReportLine } from './report.js';
|
|
|
1713
1714
|
// reads, with nothing here to change.
|
|
1714
1715
|
import { store } from './store.js';
|
|
1715
1716
|
import { pendingConfirmCount } from './confirm-queue.js';
|
|
1716
|
-
import { dismissHeld, exportHeld, heldCount, heldSummary } from './quarantine.js';
|
|
1717
|
+
import { dismissHeld, drillCount, exportHeld, heldCount, heldSummary } from './quarantine.js';
|
|
1718
|
+
import { config } from './config.js';
|
|
1717
1719
|
import { reachability } from './reachable.js';
|
|
1718
1720
|
|
|
1719
1721
|
/** blessed takes colours as strings; these mirror the site's palette. */
|
|
@@ -1874,6 +1876,7 @@ export function startDashboard({ partnerName, port, onQuit, onReplay, stats }: T
|
|
|
1874
1876
|
// ciphertext it cannot open; the customers live in the partner's own
|
|
1875
1877
|
// database, behind their own controls.
|
|
1876
1878
|
const reach = reachability();
|
|
1879
|
+
const drills = drillCount();
|
|
1877
1880
|
const vault = inbox
|
|
1878
1881
|
? \`{\${AMBER}-fg}{bold}NONE (inbox){/}\`
|
|
1879
1882
|
: store.encrypted
|
|
@@ -1892,6 +1895,11 @@ export function startDashboard({ partnerName, port, onQuit, onReplay, stats }: T
|
|
|
1892
1895
|
// third state. A receiver with no publicUrl configured cannot answer the
|
|
1893
1896
|
// question, and a green tick or a red cross would both be inventing an
|
|
1894
1897
|
// answer. Silence is the honest rendering of "not checked".
|
|
1898
|
+
// DRILLS ARE NAMED, not hidden. A conformance run leaves a trace and the
|
|
1899
|
+
// partner should be able to see it; what it must not do is look like a
|
|
1900
|
+
// fault. Absent when there are none, because a permanent "drills 0" is a
|
|
1901
|
+
// counter for a thing that has not happened.
|
|
1902
|
+
+ (drills > 0 ? \` {\${DIM}-fg}conformance probes{/} {\${CREAM}-fg}\${drills}{/}\` : '')
|
|
1895
1903
|
+ (reach.state === 'unknown'
|
|
1896
1904
|
? ''
|
|
1897
1905
|
: reach.state === 'reachable'
|
|
@@ -1939,6 +1947,16 @@ export function startDashboard({ partnerName, port, onQuit, onReplay, stats }: T
|
|
|
1939
1947
|
|
|
1940
1948
|
/** Shown while a replay is running, so [r] does not look like it did nothing. */
|
|
1941
1949
|
let replayNote = '';
|
|
1950
|
+
/**
|
|
1951
|
+
* The export confirmation, kept apart from \`replayNote\`.
|
|
1952
|
+
*
|
|
1953
|
+
* They are both one-line notes under the fault panel and they answer
|
|
1954
|
+
* different questions, so sharing a slot means a press of [r] silently wipes
|
|
1955
|
+
* the only place the export path was written down. The PATH is the entire
|
|
1956
|
+
* value of the message: a receiver started from a shortcut or a service
|
|
1957
|
+
* manager has a working directory the partner has never seen.
|
|
1958
|
+
*/
|
|
1959
|
+
let exportNote = '';
|
|
1942
1960
|
|
|
1943
1961
|
/**
|
|
1944
1962
|
* The customer count, refreshed out of band.
|
|
@@ -2101,10 +2119,19 @@ export function startDashboard({ partnerName, port, onQuit, onReplay, stats }: T
|
|
|
2101
2119
|
return;
|
|
2102
2120
|
}
|
|
2103
2121
|
try {
|
|
2104
|
-
|
|
2105
|
-
|
|
2122
|
+
// NO ARGUMENT. \`exportHeld\` resolves \`config.exportDir\`, then
|
|
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();
|
|
2127
|
+
exportNote = \`Saved \${count} to \${path}\`;
|
|
2128
|
+
replayNote = '';
|
|
2106
2129
|
} catch (err) {
|
|
2107
|
-
|
|
2130
|
+
// NAMES THE DIRECTORY IT TRIED. "EACCES" on its own sends a partner to
|
|
2131
|
+
// search for a file that was never written.
|
|
2132
|
+
exportNote = \`Export FAILED writing to \${config.exportDir || process.cwd()}: \` +
|
|
2133
|
+
\`\${err instanceof Error ? err.message : String(err)}\`;
|
|
2134
|
+
replayNote = '';
|
|
2108
2135
|
}
|
|
2109
2136
|
redraw();
|
|
2110
2137
|
});
|
|
@@ -3367,8 +3394,9 @@ export function resetTally(): void {
|
|
|
3367
3394
|
import db, { ensureColumn, parseStoredTime } from './db.js';
|
|
3368
3395
|
import { report } from './report.js';
|
|
3369
3396
|
import { createHash } from 'node:crypto';
|
|
3370
|
-
import { writeFileSync } from 'node:fs';
|
|
3371
|
-
import { join } from 'node:path';
|
|
3397
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
3398
|
+
import { join, resolve } from 'node:path';
|
|
3399
|
+
import { config } from './config.js';
|
|
3372
3400
|
|
|
3373
3401
|
/**
|
|
3374
3402
|
* Why a dispatch could not be applied. Shown verbatim on the dashboard.
|
|
@@ -3458,6 +3486,43 @@ ensureColumn('quarantine', 'attempts', 'INTEGER NOT NULL DEFAULT 0');
|
|
|
3458
3486
|
*/
|
|
3459
3487
|
ensureColumn('quarantine', 'dismissed_at', 'TEXT');
|
|
3460
3488
|
|
|
3489
|
+
/**
|
|
3490
|
+
* Was this a conformance DRILL rather than a real dispatch?
|
|
3491
|
+
*
|
|
3492
|
+
* ## The problem it solves
|
|
3493
|
+
*
|
|
3494
|
+
* Conformance check 11 sends a dispatch this receiver CANNOT open, on purpose,
|
|
3495
|
+
* and passes it for refusing one. So a receiver that quarantines ends a
|
|
3496
|
+
* SUCCESSFUL run with a red fault panel, a \`failed\` count that never returns to
|
|
3497
|
+
* zero, and a \`decrypt_failed\` line naming the partner's real \`key_id\` - telling
|
|
3498
|
+
* them to fix the one thing that is not broken. Reported from a real run.
|
|
3499
|
+
*
|
|
3500
|
+
* ## Why this is trusted and the dispatch id is not
|
|
3501
|
+
*
|
|
3502
|
+
* The flag comes from \`conformance: true\` in the body, and the HMAC covers
|
|
3503
|
+
* \`\${timestamp}.\${rawBody}\`. The body is signed; the headers are not. The
|
|
3504
|
+
* conformance dispatch also carries a recognisable \`X-OneAddress-Dispatch\`
|
|
3505
|
+
* prefix, and keying off THAT would be a mistake: it sits outside the signature,
|
|
3506
|
+
* so anything on the path could set it and change how an operator's fault panel
|
|
3507
|
+
* behaves. Same reasoning as \`crossover_key_ids\`, which is already a signed-body
|
|
3508
|
+
* field that tells a receiver how to treat a payload it cannot decrypt.
|
|
3509
|
+
*
|
|
3510
|
+
* ## Classified, NOT suppressed
|
|
3511
|
+
*
|
|
3512
|
+
* A drill is still recorded, still counted, still visible on the dashboard. It
|
|
3513
|
+
* is drawn calmly instead of as an alarm. Suppression is what creates a silent
|
|
3514
|
+
* failure; classification does not, and if the marker is ever wrong the operator
|
|
3515
|
+
* still sees the event, in the wrong colour rather than not at all.
|
|
3516
|
+
*
|
|
3517
|
+
* ## A drill holds NO payload
|
|
3518
|
+
*
|
|
3519
|
+
* \`raw_body\` is empty for a drill, which is a property rather than a saving.
|
|
3520
|
+
* The probe is sealed to a throwaway key pair the conformance run generated and
|
|
3521
|
+
* discarded, so no key that will ever exist can open it: replay cannot succeed,
|
|
3522
|
+
* and keeping the bytes would put an unopenable blob in the replay set forever.
|
|
3523
|
+
*/
|
|
3524
|
+
ensureColumn('quarantine', 'drill', 'INTEGER NOT NULL DEFAULT 0');
|
|
3525
|
+
|
|
3461
3526
|
export interface QuarantineInput {
|
|
3462
3527
|
dispatchId: string | null;
|
|
3463
3528
|
event: string;
|
|
@@ -3465,6 +3530,10 @@ export interface QuarantineInput {
|
|
|
3465
3530
|
keyId: string | null;
|
|
3466
3531
|
rawBody: string;
|
|
3467
3532
|
detail: string;
|
|
3533
|
+
/** A conformance probe rather than a consumer's dispatch. See the
|
|
3534
|
+
* \`drill\` column's docstring. Defaults to false, so a caller that does
|
|
3535
|
+
* not know about drills still records a real fault. */
|
|
3536
|
+
drill?: boolean;
|
|
3468
3537
|
}
|
|
3469
3538
|
|
|
3470
3539
|
/**
|
|
@@ -3476,27 +3545,44 @@ export interface QuarantineInput {
|
|
|
3476
3545
|
*/
|
|
3477
3546
|
export function quarantine(input: QuarantineInput): void {
|
|
3478
3547
|
const id = dispatchKey(input.dispatchId, input.rawBody);
|
|
3548
|
+
const drill = input.drill === true;
|
|
3479
3549
|
try {
|
|
3480
3550
|
db.prepare(
|
|
3481
|
-
\`INSERT INTO quarantine (id, dispatch_id, event, reason, key_id, raw_body, detail)
|
|
3482
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
3551
|
+
\`INSERT INTO quarantine (id, dispatch_id, event, reason, key_id, raw_body, detail, drill)
|
|
3552
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
3483
3553
|
ON CONFLICT(id) DO UPDATE SET
|
|
3484
3554
|
reason = excluded.reason,
|
|
3485
3555
|
key_id = excluded.key_id,
|
|
3486
|
-
detail = excluded.detail
|
|
3556
|
+
detail = excluded.detail,
|
|
3557
|
+
drill = excluded.drill\`,
|
|
3487
3558
|
).run(
|
|
3488
3559
|
id,
|
|
3489
3560
|
input.dispatchId,
|
|
3490
3561
|
input.event,
|
|
3491
3562
|
input.reason,
|
|
3492
3563
|
input.keyId,
|
|
3493
|
-
|
|
3564
|
+
// NO PAYLOAD FOR A DRILL. The probe is sealed to a throwaway key pair the
|
|
3565
|
+
// conformance run generated and discarded, so nothing that will ever exist
|
|
3566
|
+
// can open it. Keeping the bytes would put a permanently unopenable blob
|
|
3567
|
+
// in the replay set.
|
|
3568
|
+
drill ? '' : input.rawBody,
|
|
3494
3569
|
input.detail.slice(0, 500),
|
|
3570
|
+
drill ? 1 : 0,
|
|
3495
3571
|
);
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3572
|
+
if (drill) {
|
|
3573
|
+
// INFO, NOT WARN, and it says what happened rather than what to fix. The
|
|
3574
|
+
// old line ended a PASSING conformance run by telling the partner to go
|
|
3575
|
+
// and correct a key that is perfectly correct.
|
|
3576
|
+
report.info(
|
|
3577
|
+
\`[quarantine] \${input.event} was a conformance probe and was refused, which is how that \` +
|
|
3578
|
+
'check is passed. Nothing is wrong with your key.',
|
|
3579
|
+
);
|
|
3580
|
+
} else {
|
|
3581
|
+
report.warn(
|
|
3582
|
+
\`[quarantine] \${input.event} held (\${input.reason}\${input.keyId ? \`, key_id \${input.keyId}\` : ''}). \` +
|
|
3583
|
+
'Fix the cause and press [r] on the dashboard, or run \`npm run replay\`, to apply it.',
|
|
3584
|
+
);
|
|
3585
|
+
}
|
|
3500
3586
|
} catch (err) {
|
|
3501
3587
|
report.error('[quarantine] could not hold this dispatch:', err instanceof Error ? err.message : err);
|
|
3502
3588
|
}
|
|
@@ -3521,16 +3607,30 @@ export function heldDispatches(limit = 50): HeldDispatch[] {
|
|
|
3521
3607
|
\`SELECT id, dispatch_id, event, reason, key_id, raw_body, detail,
|
|
3522
3608
|
received_at, last_error, attempts
|
|
3523
3609
|
FROM quarantine
|
|
3524
|
-
WHERE replayed_at IS NULL AND dismissed_at IS NULL
|
|
3610
|
+
WHERE replayed_at IS NULL AND dismissed_at IS NULL AND drill = 0
|
|
3525
3611
|
ORDER BY received_at
|
|
3526
3612
|
LIMIT ?\`,
|
|
3527
3613
|
).all(limit) as unknown as HeldDispatch[];
|
|
3528
3614
|
}
|
|
3529
3615
|
|
|
3616
|
+
/**
|
|
3617
|
+
* How many conformance probes this receiver has refused.
|
|
3618
|
+
*
|
|
3619
|
+
* Its own figure rather than a slice of \`heldCount\`, because the two answer
|
|
3620
|
+
* different questions. Held means "waiting on you". A drill means "a check ran
|
|
3621
|
+
* and this receiver behaved correctly", which is not a queue and not work.
|
|
3622
|
+
*/
|
|
3623
|
+
export function drillCount(): number {
|
|
3624
|
+
const row = db.prepare(
|
|
3625
|
+
'SELECT count(*) AS n FROM quarantine WHERE drill = 1 AND dismissed_at IS NULL',
|
|
3626
|
+
).get() as { n: number };
|
|
3627
|
+
return row.n;
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3530
3630
|
/** How many dispatches are held. Shown on the dashboard. */
|
|
3531
3631
|
export function heldCount(): number {
|
|
3532
3632
|
const row = db.prepare(
|
|
3533
|
-
'SELECT count(*) AS n FROM quarantine WHERE replayed_at IS NULL AND dismissed_at IS NULL',
|
|
3633
|
+
'SELECT count(*) AS n FROM quarantine WHERE replayed_at IS NULL AND dismissed_at IS NULL AND drill = 0',
|
|
3534
3634
|
).get() as { n: number };
|
|
3535
3635
|
return row.n;
|
|
3536
3636
|
}
|
|
@@ -3547,7 +3647,7 @@ export function heldSummary(): string[] {
|
|
|
3547
3647
|
\`SELECT reason, key_id, count(*) AS n,
|
|
3548
3648
|
max(attempts) AS tries, min(received_at) AS oldest
|
|
3549
3649
|
FROM quarantine
|
|
3550
|
-
WHERE replayed_at IS NULL AND dismissed_at IS NULL
|
|
3650
|
+
WHERE replayed_at IS NULL AND dismissed_at IS NULL AND drill = 0
|
|
3551
3651
|
GROUP BY reason, key_id
|
|
3552
3652
|
ORDER BY n DESC\`,
|
|
3553
3653
|
).all() as unknown as {
|
|
@@ -3611,7 +3711,7 @@ export function dismissHeld(): number {
|
|
|
3611
3711
|
const info = db.prepare(
|
|
3612
3712
|
\`UPDATE quarantine
|
|
3613
3713
|
SET dismissed_at = ?, raw_body = ''
|
|
3614
|
-
WHERE replayed_at IS NULL AND dismissed_at IS NULL\`,
|
|
3714
|
+
WHERE replayed_at IS NULL AND dismissed_at IS NULL AND drill = 0\`,
|
|
3615
3715
|
).run(new Date().toISOString());
|
|
3616
3716
|
const n = Number(info.changes ?? 0);
|
|
3617
3717
|
if (n > 0) {
|
|
@@ -3691,10 +3791,25 @@ export function purgeQuarantine(days: number): { replayed: number; unreplayed: n
|
|
|
3691
3791
|
* what diagnoses a key problem, and it is safe to paste into a support ticket
|
|
3692
3792
|
* or send to us, which is what an export is for.
|
|
3693
3793
|
*/
|
|
3694
|
-
export function exportHeld(directory
|
|
3794
|
+
export function exportHeld(directory?: string): { path: string; count: number } {
|
|
3695
3795
|
const rows = heldDispatches(500);
|
|
3696
3796
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
3697
|
-
|
|
3797
|
+
// RESOLVED TO AN ABSOLUTE PATH, ALWAYS, and that is the point of the change.
|
|
3798
|
+
// "Exported 3 to oneaddress-faults-\u2026.json" is not an answer to "where is it";
|
|
3799
|
+
// a receiver is routinely started by a shortcut or a service manager whose
|
|
3800
|
+
// working directory is not where the partner thinks it is. Reported after a
|
|
3801
|
+
// real run: the file was written and could not be found.
|
|
3802
|
+
//
|
|
3803
|
+
// The explicit argument still wins, so a caller (and every existing test) can
|
|
3804
|
+
// name a directory; \`config.exportDir\` is the persistent choice and
|
|
3805
|
+
// \`ONEADDRESS_EXPORT_DIR\` the one-run override. Empty resolves to the working
|
|
3806
|
+
// directory, which is what this always did.
|
|
3807
|
+
const target = resolve(directory ?? config.exportDir ?? '');
|
|
3808
|
+
// Created rather than failed on. A partner who set \`exportDir\` to a folder
|
|
3809
|
+
// they have not made yet wants the file, not a lecture, and a directory they
|
|
3810
|
+
// named is one they have already consented to.
|
|
3811
|
+
mkdirSync(target, { recursive: true });
|
|
3812
|
+
const path = join(target, \`oneaddress-faults-\${stamp}.json\`);
|
|
3698
3813
|
writeFileSync(path, JSON.stringify({
|
|
3699
3814
|
exported_at: new Date().toISOString(),
|
|
3700
3815
|
note: 'Metadata only. The held payloads are deliberately not included: they are encrypted consumer addresses and stay under the receiver retention window.',
|
|
@@ -3711,7 +3826,11 @@ export function exportHeld(directory: string): { path: string; count: number } {
|
|
|
3711
3826
|
last_error: r.last_error,
|
|
3712
3827
|
})),
|
|
3713
3828
|
}, null, 2));
|
|
3714
|
-
|
|
3829
|
+
// THE PATH IS THE MESSAGE. Logged as well as shown on the dashboard, because
|
|
3830
|
+
// the dashboard line is gone the next time anything is pressed while the log
|
|
3831
|
+
// survives a scrollback, a \`npm start | tee\`, and a headless run where there
|
|
3832
|
+
// is no dashboard at all.
|
|
3833
|
+
report.info(\`[quarantine] exported \${rows.length} held dispatch(es) to \${path} - safe to send to support, no payloads inside\`);
|
|
3715
3834
|
return { path, count: rows.length };
|
|
3716
3835
|
}
|
|
3717
3836
|
|
|
@@ -4108,6 +4227,17 @@ export type ReceiverConfig = {
|
|
|
4108
4227
|
oneAddressApi: string;
|
|
4109
4228
|
verifiesAccountReference: boolean;
|
|
4110
4229
|
mode: ReceiverMode;
|
|
4230
|
+
/**
|
|
4231
|
+
* Where \`[e]\` writes the fault export.
|
|
4232
|
+
*
|
|
4233
|
+
* Empty means the working directory, which is what it always did. Named
|
|
4234
|
+
* because "it saved a file" is useless if you cannot find it: the receiver is
|
|
4235
|
+
* routinely started by a double-clicked shortcut or a service manager whose
|
|
4236
|
+
* working directory is not where the partner thinks it is.
|
|
4237
|
+
*
|
|
4238
|
+
* \`ONEADDRESS_EXPORT_DIR\` overrides it for one run without editing the file.
|
|
4239
|
+
*/
|
|
4240
|
+
exportDir: string;
|
|
4111
4241
|
/**
|
|
4112
4242
|
* The webhook URL registered with OneAddress: where dispatches actually
|
|
4113
4243
|
* arrive from the internet.
|
|
@@ -4134,6 +4264,9 @@ const DEFAULTS: ReceiverConfig = {
|
|
|
4134
4264
|
mode: 'write-through',
|
|
4135
4265
|
// No guess. See the field's docstring.
|
|
4136
4266
|
publicUrl: '',
|
|
4267
|
+
// Empty resolves to the working directory at use, not here, so a config file
|
|
4268
|
+
// written on one machine does not pin an absolute path from another.
|
|
4269
|
+
exportDir: '',
|
|
4137
4270
|
};
|
|
4138
4271
|
|
|
4139
4272
|
function parseMode(value: unknown): ReceiverMode | undefined {
|
|
@@ -4153,6 +4286,7 @@ function loadConfigFile(): Partial<ReceiverConfig> {
|
|
|
4153
4286
|
if (typeof parsed.oneAddressApi === 'string') out.oneAddressApi = parsed.oneAddressApi;
|
|
4154
4287
|
if (typeof parsed.verifiesAccountReference === 'boolean') out.verifiesAccountReference = parsed.verifiesAccountReference;
|
|
4155
4288
|
if (typeof parsed.publicUrl === 'string') out.publicUrl = parsed.publicUrl;
|
|
4289
|
+
if (typeof parsed.exportDir === 'string') out.exportDir = parsed.exportDir;
|
|
4156
4290
|
const mode = parseMode(parsed.mode);
|
|
4157
4291
|
if (mode) out.mode = mode;
|
|
4158
4292
|
return out;
|
|
@@ -4177,6 +4311,7 @@ export const config: ReceiverConfig = {
|
|
|
4177
4311
|
// start. Silent is the thing to avoid, not strict.
|
|
4178
4312
|
mode: parseMode(process.env.RECEIVER_MODE) ?? fromFile.mode ?? DEFAULTS.mode,
|
|
4179
4313
|
publicUrl: stripTrailingSlash(process.env.PUBLIC_URL || fromFile.publicUrl || DEFAULTS.publicUrl),
|
|
4314
|
+
exportDir: process.env.ONEADDRESS_EXPORT_DIR || fromFile.exportDir || DEFAULTS.exportDir,
|
|
4180
4315
|
};
|
|
4181
4316
|
|
|
4182
4317
|
report.info(
|
|
@@ -5595,9 +5730,27 @@ app.post('/webhook', async (req: Request, res: Response) => {
|
|
|
5595
5730
|
*/
|
|
5596
5731
|
const key = dispatchKey(dispatch || null, rawBody);
|
|
5597
5732
|
|
|
5733
|
+
/**
|
|
5734
|
+
* A DRILL, declared in the SIGNED body. See \`quarantine.ts\`'s \`drill\` column.
|
|
5735
|
+
*
|
|
5736
|
+
* Read from \`body\`, which the HMAC covers, and never from the dispatch-id
|
|
5737
|
+
* header, which it does not. The conformance run already puts a recognisable
|
|
5738
|
+
* prefix on that header and using it would let anything on the path decide
|
|
5739
|
+
* whether this receiver raises an alarm.
|
|
5740
|
+
*/
|
|
5741
|
+
const isDrill = (body as Record<string, unknown>).conformance === true;
|
|
5742
|
+
|
|
5598
5743
|
const hold = (reason: QuarantineReason, keyId: string | null, detail: string): void => {
|
|
5599
|
-
quarantine({ dispatchId: dispatch || null, event, reason, keyId, rawBody, detail });
|
|
5600
|
-
|
|
5744
|
+
quarantine({ dispatchId: dispatch || null, event, reason, keyId, rawBody, detail, drill: isDrill });
|
|
5745
|
+
// NOT COUNTED AS A FAILURE WHEN IT IS A DRILL. \`failed\` on the footer is a
|
|
5746
|
+
// figure about this partner's dispatches; a probe that was refused exactly
|
|
5747
|
+
// as the check requires is not one of them, and leaving it in is what made a
|
|
5748
|
+
// passing conformance run read as \`failed 1\` forever.
|
|
5749
|
+
//
|
|
5750
|
+
// Recorded as NOTHING rather than as a third outcome, deliberately:
|
|
5751
|
+
// \`tally.ts\` holds \`received === applied + failed\` as an invariant and a
|
|
5752
|
+
// drill is not a dispatch to this partner's business at all.
|
|
5753
|
+
if (!isDrill) recordOutcome(key, 'failed');
|
|
5601
5754
|
};
|
|
5602
5755
|
|
|
5603
5756
|
/**
|
|
@@ -5754,11 +5907,24 @@ app.post('/webhook', async (req: Request, res: Response) => {
|
|
|
5754
5907
|
decAccount = typeof data.account_number === 'string' ? data.account_number : '';
|
|
5755
5908
|
decKnownNames = Array.isArray(data.known_names) ? data.known_names : [];
|
|
5756
5909
|
} catch (err) {
|
|
5757
|
-
//
|
|
5758
|
-
//
|
|
5759
|
-
//
|
|
5760
|
-
|
|
5761
|
-
|
|
5910
|
+
// A DRILL SAYS SO INSTEAD OF SHOUTING. Conformance check 11 wraps its
|
|
5911
|
+
// session key to a throwaway pair so that decryption MUST fail; the two
|
|
5912
|
+
// lines below would then name the partner's real key_id, print a raw
|
|
5913
|
+
// crypto error, and advise them to go and fix a key that is correct - at
|
|
5914
|
+
// the end of a run that just passed. The refusal below is unchanged,
|
|
5915
|
+
// because the refusal is what the check is testing.
|
|
5916
|
+
if (isDrill) {
|
|
5917
|
+
report.info(
|
|
5918
|
+
\`[webhook] conformance probe for key_id \${keyId ?? '(none)'} could not be decrypted, which is \` +
|
|
5919
|
+
'what that check requires. Your key is fine.',
|
|
5920
|
+
);
|
|
5921
|
+
} else {
|
|
5922
|
+
// Names WHICH key answered. A rotation used to surface here as an
|
|
5923
|
+
// authentication-tag error indistinguishable from corruption, with the
|
|
5924
|
+
// single-key fallback having silently answered for a key id it never held.
|
|
5925
|
+
report.error(\`[webhook] D5 decryption failed for key_id \${keyId ?? '(none)'}: \${keyFailureAdvice(keyId, resolved)}\`);
|
|
5926
|
+
report.error('[webhook] underlying error:', err);
|
|
5927
|
+
}
|
|
5762
5928
|
hold('decrypt_failed', keyId, keyFailureAdvice(keyId, resolved));
|
|
5763
5929
|
return res.status(422).json({ ok: false, error: 'D5 decryption failed \u2014 partner key mismatch' });
|
|
5764
5930
|
}
|
|
@@ -6563,6 +6729,26 @@ customers(account_key, account_number, name, address JSON, updated_at)
|
|
|
6563
6729
|
address_history(id, account_key, prev_address JSON, address JSON, recorded_at)
|
|
6564
6730
|
\`\`\`
|
|
6565
6731
|
|
|
6732
|
+
### Where the fault export lands
|
|
6733
|
+
|
|
6734
|
+
\`[e]\` on the dashboard writes a metadata-only file (no payloads) you can send
|
|
6735
|
+
straight to support. It goes to the working directory by default, which is not
|
|
6736
|
+
always where you think it is when the receiver was started by a shortcut or a
|
|
6737
|
+
service manager, so you can name somewhere:
|
|
6738
|
+
|
|
6739
|
+
\`\`\`jsonc
|
|
6740
|
+
// oneaddress.config.json
|
|
6741
|
+
{ "exportDir": "/home/you/oneaddress-exports" }
|
|
6742
|
+
\`\`\`
|
|
6743
|
+
|
|
6744
|
+
\`\`\`bash
|
|
6745
|
+
ONEADDRESS_EXPORT_DIR=/tmp npm start # one run, without editing the file
|
|
6746
|
+
\`\`\`
|
|
6747
|
+
|
|
6748
|
+
The directory is created if it does not exist. The confirmation on the dashboard
|
|
6749
|
+
and the line in the log both name the **full path**, and the dashboard keeps it
|
|
6750
|
+
in its own slot so pressing \`[r]\` does not wipe it.
|
|
6751
|
+
|
|
6566
6752
|
### Seeing what changed
|
|
6567
6753
|
|
|
6568
6754
|
\`\`\`bash
|
|
@@ -11717,7 +11903,7 @@ async function scaffold(platform, outputDir, partnerId, webhookSecret, webhookUr
|
|
|
11717
11903
|
|
|
11718
11904
|
// src/register.ts
|
|
11719
11905
|
var import_node_crypto2 = require("crypto");
|
|
11720
|
-
var PKG_VERSION = true ? "2.
|
|
11906
|
+
var PKG_VERSION = true ? "2.6.0" : "dev";
|
|
11721
11907
|
var REGISTER_URL = "https://partners.oneaddress.io/api/partner/installs";
|
|
11722
11908
|
function hmacSha256(secret, message) {
|
|
11723
11909
|
return (0, import_node_crypto2.createHmac)("sha256", secret).update(message).digest("hex");
|