@oneaddress/setup 2.3.0 → 2.4.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 +386 -136
- 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.4.0" : "?";
|
|
860
860
|
function printCompactHeader() {
|
|
861
861
|
const INNER = 42;
|
|
862
862
|
const TOP = fn("\u250C") + dm("\u2500".repeat(INNER)) + fn("\u2510");
|
|
@@ -2136,6 +2136,11 @@ export function noteChange(facts: ChangeFacts): void {
|
|
|
2136
2136
|
* \`--headless\` skips both: no prompt (a service has nobody to ask), no screen.
|
|
2137
2137
|
*/
|
|
2138
2138
|
import { printBanner } from './brand.js';
|
|
2139
|
+
// Config only - NOT the server, the store or the database. Those derive keys on
|
|
2140
|
+
// import, which is the whole reason this file loads them dynamically below,
|
|
2141
|
+
// after the passphrase is resolved. \`config.js\` reads a JSON file and the
|
|
2142
|
+
// environment and touches neither.
|
|
2143
|
+
import { config } from './config.js';
|
|
2139
2144
|
import { PassphraseRequiredError, WrongPassphraseError } from './vault.js';
|
|
2140
2145
|
|
|
2141
2146
|
/**
|
|
@@ -2310,29 +2315,71 @@ async function resolvePassphrase(): Promise<string | null> {
|
|
|
2310
2315
|
// Reported by exactly that partner: "there was no opportunity to set a
|
|
2311
2316
|
// passphrase". Without this line the only options are guess or search the
|
|
2312
2317
|
// internet, and the answer is one command.
|
|
2313
|
-
process.stdout.write('\\n This customer database is encrypted.\\n');
|
|
2314
|
-
process.stdout.write('
|
|
2315
|
-
process.stdout.write('
|
|
2316
|
-
process.stdout.write(' OneAddress
|
|
2317
|
-
|
|
2318
|
+
process.stdout.write('\\n This customer database is encrypted and needs its password.\\n\\n');
|
|
2319
|
+
process.stdout.write(' It is the one you chose on this receiver. It is NOT your Webhook\\n');
|
|
2320
|
+
process.stdout.write(' signing secret, NOT your ECDH private key, and NOT your\\n');
|
|
2321
|
+
process.stdout.write(' OneAddress sign-in.\\n\\n');
|
|
2322
|
+
process.stdout.write(' If you do not have it, delete data.db and start again: it holds\\n');
|
|
2323
|
+
process.stdout.write(' your demo roster and test dispatches, nothing OneAddress needs.\\n\\n');
|
|
2324
|
+
const answer = await ask(' Password to unlock: ');
|
|
2318
2325
|
return answer || null;
|
|
2319
2326
|
}
|
|
2320
2327
|
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2328
|
+
// SAYS WHAT IS BEING ASKED BEFORE IT WARNS ABOUT IT. The first version opened
|
|
2329
|
+
// with SET A PASSPHRASE and four lines of consequence, which reads as an
|
|
2330
|
+
// instruction to produce a passphrase you are assumed to already have - and
|
|
2331
|
+
// this prompt arrives moments after the partner has pasted two real
|
|
2332
|
+
// credentials, so assuming exactly that is the natural reading. Reported as
|
|
2333
|
+
// confusing, and the confusion was ours.
|
|
2334
|
+
//
|
|
2335
|
+
// It therefore leads with the decision, says plainly that the answer is
|
|
2336
|
+
// INVENTED HERE, and names the three secrets it is not, because those are the
|
|
2337
|
+
// three things a partner at this point in setup might reach for.
|
|
2338
|
+
// NOT ASKED IN INBOX MODE, because the honest answer to "protect your customer
|
|
2339
|
+
// records" is that there are none here: the connector holds them. The
|
|
2340
|
+
// database in this mode carries dispatch ciphertext - already encrypted to a
|
|
2341
|
+
// key this process does not have - and a queue of dispatch ids. Asking anyway
|
|
2342
|
+
// is the confusing-prompt problem twice over: a question about data that is
|
|
2343
|
+
// absent, put to somebody who has just been told this receiver cannot read
|
|
2344
|
+
// anything.
|
|
2345
|
+
//
|
|
2346
|
+
// The UNLOCK branch above is deliberately NOT skipped. A database encrypted
|
|
2347
|
+
// while the receiver ran write-through is still encrypted after a switch to
|
|
2348
|
+
// inbox, and skipping the question there would refuse to open a file whose
|
|
2349
|
+
// owner has the password.
|
|
2350
|
+
if (config.mode === 'inbox') {
|
|
2351
|
+
process.stdout.write('\\n No password needed: in inbox mode this receiver holds no customer records.\\n');
|
|
2352
|
+
process.stdout.write(' Your connector holds them, and the key.\\n\\n');
|
|
2353
|
+
return null;
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
process.stdout.write('\\n \u2500\u2500 Protect this receiver\\'s customer database? \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\\n\\n');
|
|
2357
|
+
process.stdout.write(' MAKE UP A NEW PASSWORD now. You are NOT being asked for anything\\n');
|
|
2358
|
+
process.stdout.write(' you already have: not your Webhook signing secret, not your ECDH\\n');
|
|
2359
|
+
process.stdout.write(' private key, not your OneAddress sign-in. This one is invented\\n');
|
|
2360
|
+
process.stdout.write(' here and used only on this machine.\\n\\n');
|
|
2361
|
+
process.stdout.write(' It encrypts the customer records this receiver keeps in data.db.\\n');
|
|
2362
|
+
process.stdout.write(' OneAddress never sees it and cannot reset it, so if you lose it\\n');
|
|
2363
|
+
process.stdout.write(' those records cannot be read again.\\n\\n');
|
|
2364
|
+
process.stdout.write(' OPTIONAL. Press Enter to skip and leave the database readable.\\n');
|
|
2365
|
+
// ACCURATE ABOUT "LATER", because the obvious reading is wrong. Setting a
|
|
2366
|
+
// password afterwards works and the receiver reports itself encrypted, but
|
|
2367
|
+
// rows written before it stay in the clear until something rewrites them -
|
|
2368
|
+
// confirmed by finding a seeded customer name in the database file after
|
|
2369
|
+
// exactly that sequence.
|
|
2370
|
+
process.stdout.write(' You can set one later with ONEADDRESS_DB_PASSPHRASE, but records\\n');
|
|
2371
|
+
process.stdout.write(' written before then stay readable until they change.\\n\\n');
|
|
2326
2372
|
|
|
2327
2373
|
for (;;) {
|
|
2328
|
-
const first = await ask(' New
|
|
2374
|
+
const first = await ask(' New password (or Enter to skip): ');
|
|
2329
2375
|
if (!first) {
|
|
2330
|
-
process.stdout.write(' Continuing WITHOUT encryption
|
|
2376
|
+
process.stdout.write(' Continuing WITHOUT encryption: anyone who can read data.db can\\n');
|
|
2377
|
+
process.stdout.write(' read your customer records.\\n\\n');
|
|
2331
2378
|
return null;
|
|
2332
2379
|
}
|
|
2333
|
-
const again = await ask('
|
|
2380
|
+
const again = await ask(' Type it again: ');
|
|
2334
2381
|
if (first === again) {
|
|
2335
|
-
process.stdout.write('
|
|
2382
|
+
process.stdout.write(' Password set. Keep it somewhere you will still have it.\\n\\n');
|
|
2336
2383
|
return first;
|
|
2337
2384
|
}
|
|
2338
2385
|
process.stdout.write(' Those do not match. Try again.\\n\\n');
|
|
@@ -2471,6 +2518,22 @@ import type {
|
|
|
2471
2518
|
* if anything awaited between the set and the read, which is why they are
|
|
2472
2519
|
* adjacent and why this comment exists.
|
|
2473
2520
|
*/
|
|
2521
|
+
/**
|
|
2522
|
+
* The connector could not be reached, so the question was never answered.
|
|
2523
|
+
*
|
|
2524
|
+
* A THROW RATHER THAN A VERDICT, because every value this function can return
|
|
2525
|
+
* is a statement about the customer's record and none of them means "we could
|
|
2526
|
+
* not ask". The handler turns this into a non-2xx, which the relay already
|
|
2527
|
+
* classifies as \`unreachable\` and shows the consumer as a transient network
|
|
2528
|
+
* problem rather than as a missing account.
|
|
2529
|
+
*/
|
|
2530
|
+
export class ConnectorUnreachableError extends Error {
|
|
2531
|
+
constructor(public readonly kind: string) {
|
|
2532
|
+
super(\`the connector could not be reached to answer \${kind}\`);
|
|
2533
|
+
this.name = 'ConnectorUnreachableError';
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2474
2537
|
let currentRawBody = '';
|
|
2475
2538
|
|
|
2476
2539
|
export function setCurrentRawBody(raw: string): void {
|
|
@@ -2496,14 +2559,26 @@ export const connectorStore = {
|
|
|
2496
2559
|
): Promise<AccountVerdict> {
|
|
2497
2560
|
const answer = await askConnector('account.verify', currentRawBody);
|
|
2498
2561
|
if (!answer.reached) {
|
|
2499
|
-
// NEVER GUESSED
|
|
2500
|
-
// a customer's account.
|
|
2501
|
-
//
|
|
2562
|
+
// NEVER GUESSED: a fabricated match authorises a stranger's address onto
|
|
2563
|
+
// a customer's account.
|
|
2564
|
+
//
|
|
2565
|
+
// BUT NOT \`no_account\` EITHER, which is what this returned. That is a
|
|
2566
|
+
// POSITIVE CLAIM - the consumer is shown "\u2717 No account found / This
|
|
2567
|
+
// service couldn't find an account with these details" and goes off to
|
|
2568
|
+
// re-check an account number that was correct all along, or concludes
|
|
2569
|
+
// they have no account with a provider they do.
|
|
2570
|
+
//
|
|
2571
|
+
// The trade was accepted for its safety, and the safety is real. What was
|
|
2572
|
+
// missed is that it is available WITHOUT the false claim: the relay
|
|
2573
|
+
// already classifies a failed check as \`unreachable\` and tells the
|
|
2574
|
+
// consumer "we couldn't reach this service just now". That stops them
|
|
2575
|
+
// before they pay AND is true. Throwing gets us there, because a
|
|
2576
|
+
// non-2xx is exactly what the relay reads as unreachable.
|
|
2502
2577
|
report.warn(
|
|
2503
2578
|
\`[connector] account check for \${accountNumber ?? '(none)'} could not be answered; \` +
|
|
2504
|
-
'
|
|
2579
|
+
'reporting the service as unreachable rather than claiming no account exists',
|
|
2505
2580
|
);
|
|
2506
|
-
|
|
2581
|
+
throw new ConnectorUnreachableError('account.verify');
|
|
2507
2582
|
}
|
|
2508
2583
|
const status = verdictOf(answer.body, 'status');
|
|
2509
2584
|
if (status === 'match' || status === 'no_match' || status === 'no_account') return status;
|
|
@@ -2513,7 +2588,9 @@ export const connectorStore = {
|
|
|
2513
2588
|
|
|
2514
2589
|
async verifyAddress(_customer: Customer, _incoming: Address): Promise<VerifyResult> {
|
|
2515
2590
|
const answer = await askConnector('address.verify', currentRawBody);
|
|
2516
|
-
|
|
2591
|
+
// Same reasoning as \`verifyAccount\`: \`not_found\` is a claim about the
|
|
2592
|
+
// customer's record, and "we could not ask" is not that claim.
|
|
2593
|
+
if (!answer.reached) throw new ConnectorUnreachableError('address.verify');
|
|
2517
2594
|
const result = verdictOf(answer.body, 'result');
|
|
2518
2595
|
if (result === 'match' || result === 'mismatch' || result === 'not_found') return result;
|
|
2519
2596
|
report.warn(\`[connector] address.verify answered "\${result ?? '(nothing)'}", which is not a result\`);
|
|
@@ -4177,6 +4254,7 @@ export interface CustomerStore {
|
|
|
4177
4254
|
import { readFileSync } from 'node:fs';
|
|
4178
4255
|
import { join } from 'node:path';
|
|
4179
4256
|
import { report } from './report.js';
|
|
4257
|
+
import { config } from './config.js';
|
|
4180
4258
|
import db, { accountKey, dec, enc, encrypted, ensureColumn, isEncrypted, once } from './db.js';
|
|
4181
4259
|
import type {
|
|
4182
4260
|
AccountVerdict,
|
|
@@ -4356,8 +4434,30 @@ function loadRoster(): RosterEntry[] {
|
|
|
4356
4434
|
}
|
|
4357
4435
|
}
|
|
4358
4436
|
|
|
4359
|
-
|
|
4360
|
-
|
|
4437
|
+
/**
|
|
4438
|
+
* IN INBOX MODE NOTHING IS SEEDED, and that is a privacy fix rather than a
|
|
4439
|
+
* saving.
|
|
4440
|
+
*
|
|
4441
|
+
* This block runs on IMPORT, and \`server.ts\` imports this module in both modes
|
|
4442
|
+
* because it picks the store at runtime. So an inbox receiver - the process that
|
|
4443
|
+
* exists specifically so it cannot read what it holds - was writing a full
|
|
4444
|
+
* customer roster to its own database on startup: names, account numbers and
|
|
4445
|
+
* the address on file for each, in the clear.
|
|
4446
|
+
*
|
|
4447
|
+
* The dispatches were never the leak. Those are stored as the ciphertext they
|
|
4448
|
+
* arrived as, and an end-to-end run confirms the new address appears nowhere in
|
|
4449
|
+
* the receiver's database file. The ROSTER was, and it was there before a
|
|
4450
|
+
* single dispatch arrived. Found by searching the receiver's database bytes for
|
|
4451
|
+
* a customer name after that run, which is the only check that would have found
|
|
4452
|
+
* it: every other one asks the receiver what it thinks it is holding.
|
|
4453
|
+
*
|
|
4454
|
+
* In inbox mode the connector owns the roster and answers every account
|
|
4455
|
+
* question over the loopback channel, so the receiver needs none of it.
|
|
4456
|
+
*/
|
|
4457
|
+
const ROSTER = config.mode === 'inbox' ? [] : loadRoster();
|
|
4458
|
+
if (config.mode === 'inbox') {
|
|
4459
|
+
report.info('[store] no roster held: in inbox mode your connector owns customer records');
|
|
4460
|
+
} else {
|
|
4361
4461
|
const upsert = db.prepare(\`
|
|
4362
4462
|
INSERT INTO customers (account_key, account_number, name, address)
|
|
4363
4463
|
VALUES ($account_key, $account_number, $name, $address)
|
|
@@ -4623,7 +4723,7 @@ import {
|
|
|
4623
4723
|
// exporting a \`CustomerStore\` (see src/customer-store.ts) and nothing else in
|
|
4624
4724
|
// the protocol layer changes.
|
|
4625
4725
|
import { store as writeThroughStore } from './store.js';
|
|
4626
|
-
import { connectorStore, setCurrentRawBody } from './connector-store.js';
|
|
4726
|
+
import { ConnectorUnreachableError, connectorStore, setCurrentRawBody } from './connector-store.js';
|
|
4627
4727
|
import { noteChange } from './tui.js';
|
|
4628
4728
|
import { config } from './config.js';
|
|
4629
4729
|
import { safeOneAddressCallbackUrl } from './callback-url.js';
|
|
@@ -4761,12 +4861,22 @@ report.info(
|
|
|
4761
4861
|
// lock out every install that has never set a passphrase - but choosing it by
|
|
4762
4862
|
// not being asked is not a choice, so the absence is stated as loudly as the
|
|
4763
4863
|
// presence.
|
|
4864
|
+
//
|
|
4865
|
+
// AND IN INBOX MODE IT SAYS SOMETHING ELSE, for the same reason the keys line
|
|
4866
|
+
// above does. There are no customer records in this mode - the connector holds
|
|
4867
|
+
// them - so "Customer records are readable by anyone who can read the file" was
|
|
4868
|
+
// alarming about data that is not here, and sent the partner to set a passphrase
|
|
4869
|
+
// protecting nothing. \`connectorStore.encrypted\` was already hardcoded false
|
|
4870
|
+
// with a comment saying exactly that; this line had simply never read it.
|
|
4764
4871
|
report.info(
|
|
4765
|
-
|
|
4766
|
-
? \`[startup] store: \${store.name}
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4872
|
+
config.mode === 'inbox'
|
|
4873
|
+
? \`[startup] store: \${store.name} \u2014 no customer records here by design. This database holds \` +
|
|
4874
|
+
'dispatch ciphertext your connector decrypts, and the confirm queue.'
|
|
4875
|
+
: store.encrypted
|
|
4876
|
+
? \`[startup] store: \${store.name} (encrypted at rest)\`
|
|
4877
|
+
: \`[startup] store: \${store.name} \u2014 NOT ENCRYPTED AT REST. \` +
|
|
4878
|
+
'Customer records are readable by anyone who can read the file. ' +
|
|
4879
|
+
'Set ONEADDRESS_DB_PASSPHRASE, or run \`npm start\` in a terminal to be asked.',
|
|
4770
4880
|
);
|
|
4771
4881
|
|
|
4772
4882
|
// In-memory dedup cache. Records a dispatch id only once it has been fully
|
|
@@ -4806,11 +4916,17 @@ async function confirmToOneAddress(dispatchId: number, status: ConfirmStatus): P
|
|
|
4806
4916
|
// and it drops non-numeric dispatch ids, so by the time a row is drained it
|
|
4807
4917
|
// is a real dispatch. Keeping a second copy of that rule would mean a probe
|
|
4808
4918
|
// could be queued forever and silently skipped on every drain.
|
|
4919
|
+
// REPORTED, not "Applied". The note is fixed text sent with EVERY confirm,
|
|
4920
|
+
// including \`failed\` ones - so a dispatch the connector could not open, or
|
|
4921
|
+
// one refused because the account reference matched nobody, arrived at
|
|
4922
|
+
// OneAddress reading "Applied by the OneAddress webhook receiver". That is
|
|
4923
|
+
// read by whoever is working out what happened, and it said the opposite of
|
|
4924
|
+
// \`status\` sitting beside it.
|
|
4809
4925
|
const bodyStr = JSON.stringify({
|
|
4810
4926
|
dispatch_id: dispatchId,
|
|
4811
4927
|
partner_id: PARTNER_ID,
|
|
4812
4928
|
status,
|
|
4813
|
-
note: '
|
|
4929
|
+
note: 'Reported by the OneAddress webhook receiver',
|
|
4814
4930
|
});
|
|
4815
4931
|
const ts = String(Math.floor(Date.now() / 1000));
|
|
4816
4932
|
const sig = createHmac('sha256', CONFIRM_SECRET).update(\`\${ts}.\${bodyStr}\`).digest('hex');
|
|
@@ -4961,7 +5077,19 @@ app.post('/webhook', async (req: Request, res: Response) => {
|
|
|
4961
5077
|
// and the customer records, so it answers. \`connectorStore\` sends the raw
|
|
4962
5078
|
// body; the arguments below are the contract's shape, not its source.
|
|
4963
5079
|
if (config.mode === 'inbox') {
|
|
4964
|
-
|
|
5080
|
+
let status: Awaited<ReturnType<typeof store.verifyAccount>>;
|
|
5081
|
+
try {
|
|
5082
|
+
status = await store.verifyAccount(null, '', []);
|
|
5083
|
+
} catch (err) {
|
|
5084
|
+
// 503 RATHER THAN A VERDICT. Every verdict is a claim about the
|
|
5085
|
+
// customer's record; "the connector is down" is not one of them. A
|
|
5086
|
+
// non-2xx is what OneAddress reads as \`unreachable\`, which the consumer
|
|
5087
|
+
// sees as a transient network problem instead of "no account found".
|
|
5088
|
+
if (err instanceof ConnectorUnreachableError) {
|
|
5089
|
+
return res.status(503).json({ ok: false, error: 'connector_unreachable' });
|
|
5090
|
+
}
|
|
5091
|
+
throw err;
|
|
5092
|
+
}
|
|
4965
5093
|
report.info(\`[webhook] account.verify \u2192 \${status} (answered by the connector)\`);
|
|
4966
5094
|
return res.status(200).json({ status });
|
|
4967
5095
|
}
|
|
@@ -5090,7 +5218,17 @@ app.post('/webhook', async (req: Request, res: Response) => {
|
|
|
5090
5218
|
if (config.mode === 'inbox' && event === 'address.verify') {
|
|
5091
5219
|
// The contract's arguments, not its source: \`connectorStore\` sends the raw
|
|
5092
5220
|
// body it was parked with and ignores these.
|
|
5093
|
-
|
|
5221
|
+
let result: Awaited<ReturnType<typeof store.verifyAddress>>;
|
|
5222
|
+
try {
|
|
5223
|
+
result = await store.verifyAddress({ email: null, name: '' }, {});
|
|
5224
|
+
} catch (err) {
|
|
5225
|
+
// As above, and the callback is deliberately NOT posted: a verdict posted
|
|
5226
|
+
// here would be a claim about a record nobody read.
|
|
5227
|
+
if (err instanceof ConnectorUnreachableError) {
|
|
5228
|
+
return res.status(503).json({ ok: false, error: 'connector_unreachable' });
|
|
5229
|
+
}
|
|
5230
|
+
throw err;
|
|
5231
|
+
}
|
|
5094
5232
|
report.info(\`[webhook] address.verify \u2192 \${result} (answered by the connector)\`);
|
|
5095
5233
|
const refused = await postVerifyVerdict(result);
|
|
5096
5234
|
if (refused) return refused;
|
|
@@ -5554,6 +5692,7 @@ export const PARTNER_NAME = process.env.PARTNER_NAME?.trim() || 'Your receiver';
|
|
|
5554
5692
|
import 'dotenv/config';
|
|
5555
5693
|
import { createPrivateKey, createPublicKey } from 'node:crypto';
|
|
5556
5694
|
import { spawnSync } from 'node:child_process';
|
|
5695
|
+
import { readFileSync } from 'node:fs';
|
|
5557
5696
|
|
|
5558
5697
|
const secret = process.env.WEBHOOK_SECRET ?? '';
|
|
5559
5698
|
const partnerId = process.env.PARTNER_ID ?? '%%PARTNER_ID%%';
|
|
@@ -5561,21 +5700,73 @@ const keyPem = (process.env.PARTNER_PRIVATE_KEY_PEM ?? '').replace(/\\\\n/g,
|
|
|
5561
5700
|
const port = process.env.PORT ?? '3001';
|
|
5562
5701
|
const targetUrl = process.argv[2] ?? \`http://localhost:\${port}/webhook\`;
|
|
5563
5702
|
|
|
5564
|
-
|
|
5565
|
-
|
|
5703
|
+
/**
|
|
5704
|
+
* Which shape of receiver is this?
|
|
5705
|
+
*
|
|
5706
|
+
* Read from the same config file the server reads, so the two cannot disagree.
|
|
5707
|
+
* An inbox receiver holds NO private key - that is the property it exists for -
|
|
5708
|
+
* so this runner used to die on its own env gate with "Missing required env
|
|
5709
|
+
* vars. Check your .env file", telling the partner to fix a file that was
|
|
5710
|
+
* exactly as the wizard wrote it. It was never run in that mode.
|
|
5711
|
+
*/
|
|
5712
|
+
let receiverMode = 'write-through';
|
|
5713
|
+
try {
|
|
5714
|
+
const cfg = JSON.parse(readFileSync(new URL('../oneaddress.config.json', import.meta.url), 'utf8'));
|
|
5715
|
+
if (cfg && typeof cfg.mode === 'string') receiverMode = cfg.mode;
|
|
5716
|
+
} catch {
|
|
5717
|
+
// No config, or unreadable: treat it as the default, which is what every
|
|
5718
|
+
// scaffold before the mode existed was.
|
|
5719
|
+
}
|
|
5720
|
+
const isInbox = receiverMode === 'inbox';
|
|
5721
|
+
|
|
5722
|
+
if (!secret || !partnerId) {
|
|
5723
|
+
console.error('[test] Missing WEBHOOK_SECRET or PARTNER_ID. Check your .env file.');
|
|
5566
5724
|
process.exit(1);
|
|
5567
5725
|
}
|
|
5568
5726
|
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5727
|
+
/**
|
|
5728
|
+
* The public key the conformance suite encrypts to.
|
|
5729
|
+
*
|
|
5730
|
+
* Write-through derives it from the private key it already holds. Inbox mode
|
|
5731
|
+
* has no private key to derive from, so it takes the PUBLIC half directly:
|
|
5732
|
+
* a public key is not a secret, and keeping one here does not give this process
|
|
5733
|
+
* the ability to read anything.
|
|
5734
|
+
*
|
|
5735
|
+
* Copy it from partners.oneaddress.io -> My Profile.
|
|
5736
|
+
*/
|
|
5737
|
+
const publicKeyFromEnv = (process.env.PARTNER_PUBLIC_KEY_B64 ?? '').trim();
|
|
5738
|
+
|
|
5739
|
+
if (isInbox && !publicKeyFromEnv) {
|
|
5740
|
+
console.log('[test] Inbox mode: this receiver holds no private key, so the encrypted checks');
|
|
5741
|
+
console.log('[test] cannot be built here. Two ways to run them:');
|
|
5742
|
+
console.log('[test]');
|
|
5743
|
+
console.log('[test] 1. Set PARTNER_PUBLIC_KEY_B64 in .env (copy it from My Profile in the');
|
|
5744
|
+
console.log('[test] portal). It is a public value and safe to keep on this machine.');
|
|
5745
|
+
console.log('[test] 2. Or run conformance from your connector host, which has the key.');
|
|
5746
|
+
console.log('[test]');
|
|
5747
|
+
console.log('[test] Signing and replay are covered by the wizard\\'s own checks either way.');
|
|
5748
|
+
process.exit(0);
|
|
5749
|
+
}
|
|
5750
|
+
|
|
5751
|
+
if (!isInbox && !keyPem) {
|
|
5752
|
+
console.error('[test] Missing PARTNER_PRIVATE_KEY_PEM. Check your .env file.');
|
|
5576
5753
|
process.exit(1);
|
|
5577
5754
|
}
|
|
5578
5755
|
|
|
5756
|
+
let publicKeyB64: string;
|
|
5757
|
+
if (publicKeyFromEnv) {
|
|
5758
|
+
publicKeyB64 = publicKeyFromEnv;
|
|
5759
|
+
} else {
|
|
5760
|
+
try {
|
|
5761
|
+
const priv = createPrivateKey({ key: keyPem, format: 'pem' });
|
|
5762
|
+
const pub = createPublicKey(priv);
|
|
5763
|
+
publicKeyB64 = pub.export({ type: 'spki', format: 'der' }).toString('base64');
|
|
5764
|
+
} catch (err) {
|
|
5765
|
+
console.error('[test] Failed to derive public key from PARTNER_PRIVATE_KEY_PEM:', err);
|
|
5766
|
+
process.exit(1);
|
|
5767
|
+
}
|
|
5768
|
+
}
|
|
5769
|
+
|
|
5579
5770
|
console.log(\`[test] Running conformance against \${targetUrl}\\n\`);
|
|
5580
5771
|
|
|
5581
5772
|
/**
|
|
@@ -5596,9 +5787,14 @@ console.log(\`[test] Running conformance against \${targetUrl}\\n\`);
|
|
|
5596
5787
|
* the check would be asking a question your code is not the one answering.
|
|
5597
5788
|
*/
|
|
5598
5789
|
const verifiesAccountReference =
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5790
|
+
// NEVER in inbox mode. The receiver cannot read an account reference there,
|
|
5791
|
+
// so check-14 asks it a question only the connector can answer, and a
|
|
5792
|
+
// correctly built system fails a check it was never the subject of.
|
|
5793
|
+
!isInbox && (
|
|
5794
|
+
process.env.OA_VERIFIES_ACCOUNT_REFERENCE === '1' ||
|
|
5795
|
+
process.env.OA_VERIFIES_ACCOUNT_REFERENCE === 'true' ||
|
|
5796
|
+
%%VERIFIES_ACCOUNT_REFERENCE%%
|
|
5797
|
+
);
|
|
5602
5798
|
|
|
5603
5799
|
const result = spawnSync(
|
|
5604
5800
|
'npx',
|
|
@@ -6310,7 +6506,7 @@ async def _confirm_to_oneaddress(dispatch: str, status: str) -> None:
|
|
|
6310
6506
|
"dispatch_id": dispatch_id,
|
|
6311
6507
|
"partner_id": PARTNER_ID,
|
|
6312
6508
|
"status": status,
|
|
6313
|
-
"note": "
|
|
6509
|
+
"note": "Reported by the OneAddress webhook receiver",
|
|
6314
6510
|
})
|
|
6315
6511
|
ts = str(int(time.time()))
|
|
6316
6512
|
sig = hmac_lib.new(CONFIRM_SECRET.encode(), f"{ts}.{body_str}".encode(), hashlib.sha256).hexdigest()
|
|
@@ -7325,7 +7521,7 @@ public class OneAddressWebhookController {
|
|
|
7325
7521
|
confirmBody.put("dispatch_id", dispatchId);
|
|
7326
7522
|
confirmBody.put("partner_id", partnerId);
|
|
7327
7523
|
confirmBody.put("status", status);
|
|
7328
|
-
confirmBody.put("note", "
|
|
7524
|
+
confirmBody.put("note", "Reported by the OneAddress webhook receiver");
|
|
7329
7525
|
String bodyStr = MAPPER.writeValueAsString(confirmBody);
|
|
7330
7526
|
String ts = String.valueOf(System.currentTimeMillis() / 1000L);
|
|
7331
7527
|
String sig = OneAddressVerifier.sign(ts + "." + bodyStr, confirmSecret);
|
|
@@ -8371,7 +8567,7 @@ async Task ConfirmToOneAddress(string oneAddressApi, string confirmSecret, strin
|
|
|
8371
8567
|
dispatch_id = dispatchId,
|
|
8372
8568
|
partner_id = pid,
|
|
8373
8569
|
status,
|
|
8374
|
-
note = "
|
|
8570
|
+
note = "Reported by the OneAddress webhook receiver",
|
|
8375
8571
|
});
|
|
8376
8572
|
var ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
|
|
8377
8573
|
var sig = Convert.ToHexString(
|
|
@@ -9473,7 +9669,7 @@ func confirmToOneAddress(oneAddressAPI, confirmSecret, partnerID, dispatch, stat
|
|
|
9473
9669
|
"dispatch_id": dispatchID,
|
|
9474
9670
|
"partner_id": partnerID,
|
|
9475
9671
|
"status": status,
|
|
9476
|
-
"note": "
|
|
9672
|
+
"note": "Reported by the OneAddress webhook receiver",
|
|
9477
9673
|
})
|
|
9478
9674
|
bodyStr := string(bodyBytes)
|
|
9479
9675
|
ts := strconv.FormatInt(time.Now().Unix(), 10)
|
|
@@ -10257,7 +10453,7 @@ function oaConfirmToOneAddress(string $oneAddressApi, string $confirmSecret, str
|
|
|
10257
10453
|
'dispatch_id' => (int) $dispatch,
|
|
10258
10454
|
'partner_id' => $partnerId,
|
|
10259
10455
|
'status' => $status,
|
|
10260
|
-
'note' => '
|
|
10456
|
+
'note' => 'Reported by the OneAddress webhook receiver',
|
|
10261
10457
|
]);
|
|
10262
10458
|
$ts = (string) time();
|
|
10263
10459
|
$sig = hash_hmac('sha256', $ts . '.' . $bodyStr, $confirmSecret);
|
|
@@ -10735,10 +10931,11 @@ npx @oneaddress/conformance test %%WEBHOOK_URL%%
|
|
|
10735
10931
|
};
|
|
10736
10932
|
|
|
10737
10933
|
// src/scaffold.ts
|
|
10738
|
-
function applyTokens(content, partnerId, webhookSecret, webhookUrl, privateKey, verifiesAccountReference, oneAddressApi, mode, connectorToken) {
|
|
10934
|
+
function applyTokens(content, partnerId, webhookSecret, webhookUrl, privateKey, verifiesAccountReference, oneAddressApi, mode, connectorToken, port) {
|
|
10739
10935
|
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
|
-
|
|
10741
|
-
|
|
10936
|
+
const withPort = applyPort(filled, port);
|
|
10937
|
+
if (mode !== "inbox") return withPort;
|
|
10938
|
+
const withReadme = withPort.replace(
|
|
10742
10939
|
"## Architecture",
|
|
10743
10940
|
`## Inbox mode
|
|
10744
10941
|
|
|
@@ -10773,11 +10970,17 @@ hand somebody your customers' addresses.
|
|
|
10773
10970
|
CONNECTOR_TOKEN=${connectorToken || generateConnectorToken()}`
|
|
10774
10971
|
);
|
|
10775
10972
|
}
|
|
10973
|
+
function applyPort(content, port) {
|
|
10974
|
+
if (!Number.isInteger(port) || port === DEFAULT_PORT) return content;
|
|
10975
|
+
return content.replace(/^PORT=\d+$/m, `PORT=${port}`);
|
|
10976
|
+
}
|
|
10977
|
+
var DEFAULT_PORT = 3001;
|
|
10978
|
+
var RESERVED_PORTS = [3002, 3003];
|
|
10776
10979
|
function generateConnectorToken() {
|
|
10777
10980
|
return (0, import_node_crypto.randomBytes)(24).toString("hex");
|
|
10778
10981
|
}
|
|
10779
10982
|
var SENSITIVE_FILES = /* @__PURE__ */ new Set([".env"]);
|
|
10780
|
-
async function scaffold(platform, outputDir, partnerId, webhookSecret, webhookUrl, privateKey = "", verifiesAccountReference = false, oneAddressApi = "https://oneaddress.io", mode = "write-through", connectorToken = "") {
|
|
10983
|
+
async function scaffold(platform, outputDir, partnerId, webhookSecret, webhookUrl, privateKey = "", verifiesAccountReference = false, oneAddressApi = "https://oneaddress.io", mode = "write-through", connectorToken = "", port = DEFAULT_PORT) {
|
|
10781
10984
|
const templates = TEMPLATES[platform];
|
|
10782
10985
|
if (!templates) throw new Error(`Unknown platform: ${platform}`);
|
|
10783
10986
|
const written = [];
|
|
@@ -10787,7 +10990,7 @@ async function scaffold(platform, outputDir, partnerId, webhookSecret, webhookUr
|
|
|
10787
10990
|
if (!(0, import_node_fs.existsSync)(dir)) {
|
|
10788
10991
|
await (0, import_promises.mkdir)(dir, { recursive: true });
|
|
10789
10992
|
}
|
|
10790
|
-
const filled = applyTokens(content, partnerId, webhookSecret, webhookUrl, privateKey, verifiesAccountReference, oneAddressApi, mode, connectorToken);
|
|
10993
|
+
const filled = applyTokens(content, partnerId, webhookSecret, webhookUrl, privateKey, verifiesAccountReference, oneAddressApi, mode, connectorToken, port);
|
|
10791
10994
|
const isSensitive = SENSITIVE_FILES.has((0, import_node_path.basename)(name));
|
|
10792
10995
|
await (0, import_promises.writeFile)(dest, filled, { encoding: "utf8", mode: isSensitive ? 384 : 420 });
|
|
10793
10996
|
if (isSensitive && process.platform !== "win32") {
|
|
@@ -10803,7 +11006,7 @@ async function scaffold(platform, outputDir, partnerId, webhookSecret, webhookUr
|
|
|
10803
11006
|
|
|
10804
11007
|
// src/register.ts
|
|
10805
11008
|
var import_node_crypto2 = require("crypto");
|
|
10806
|
-
var PKG_VERSION = true ? "2.
|
|
11009
|
+
var PKG_VERSION = true ? "2.4.0" : "dev";
|
|
10807
11010
|
var REGISTER_URL = "https://partners.oneaddress.io/api/partner/installs";
|
|
10808
11011
|
function hmacSha256(secret, message) {
|
|
10809
11012
|
return (0, import_node_crypto2.createHmac)("sha256", secret).update(message).digest("hex");
|
|
@@ -11054,15 +11257,17 @@ Timed out after 5 minutes.`.trim(),
|
|
|
11054
11257
|
|
|
11055
11258
|
// src/autostart.ts
|
|
11056
11259
|
var import_node_child_process2 = require("child_process");
|
|
11057
|
-
|
|
11058
|
-
|
|
11059
|
-
|
|
11060
|
-
|
|
11061
|
-
|
|
11062
|
-
|
|
11063
|
-
|
|
11064
|
-
|
|
11065
|
-
|
|
11260
|
+
function commandsFor(port) {
|
|
11261
|
+
return {
|
|
11262
|
+
"ts-node": { cmd: "npx", args: ["tsx", "src/server.ts"] },
|
|
11263
|
+
"python": { cmd: "uvicorn", args: ["app:app", "--port", String(port)] },
|
|
11264
|
+
"go-http": { cmd: "go", args: ["run", "."] },
|
|
11265
|
+
"php-laravel": { cmd: "php", args: ["artisan", "serve", `--port=${port}`] },
|
|
11266
|
+
"csharp-aspnet": { cmd: "dotnet", args: ["run"] },
|
|
11267
|
+
"java-spring": { cmd: "mvn", args: ["spring-boot:run"] }
|
|
11268
|
+
// no mvnw wrapper is scaffolded
|
|
11269
|
+
};
|
|
11270
|
+
}
|
|
11066
11271
|
var DASHBOARD_COMMANDS = {
|
|
11067
11272
|
"ts-node": { cmd: "npx", args: ["tsx", "src/index.ts"] }
|
|
11068
11273
|
};
|
|
@@ -11164,13 +11369,13 @@ async function pollHealth(port, timeoutMs) {
|
|
|
11164
11369
|
return false;
|
|
11165
11370
|
}
|
|
11166
11371
|
async function startServer(platform, outputDir, port = 3001, secrets = {}) {
|
|
11167
|
-
const spec =
|
|
11372
|
+
const spec = commandsFor(port)[platform];
|
|
11168
11373
|
if (!spec) {
|
|
11169
11374
|
return { ok: false, output: `No start command defined for platform: ${platform}`, manualCommand: "" };
|
|
11170
11375
|
}
|
|
11171
11376
|
serverOutput = "";
|
|
11172
11377
|
const manualCommand = `cd ${outputDir} && ${spec.cmd} ${spec.args.join(" ")}`;
|
|
11173
|
-
serverProcess = spawnReceiver(spec, outputDir, ["ignore", "pipe", "pipe"], secrets);
|
|
11378
|
+
serverProcess = spawnReceiver(spec, outputDir, ["ignore", "pipe", "pipe"], { ...secrets, PORT: String(port) });
|
|
11174
11379
|
serverProcess.stdout?.on("data", (d3) => {
|
|
11175
11380
|
serverOutput += d3.toString();
|
|
11176
11381
|
});
|
|
@@ -11217,7 +11422,7 @@ async function handOverTerminal(platform, outputDir, port, secrets = {}) {
|
|
|
11217
11422
|
}
|
|
11218
11423
|
}
|
|
11219
11424
|
process.stdin.pause();
|
|
11220
|
-
const child = spawnReceiver(spec, outputDir, "inherit", secrets);
|
|
11425
|
+
const child = spawnReceiver(spec, outputDir, "inherit", { ...secrets, PORT: String(port) });
|
|
11221
11426
|
serverProcess = child;
|
|
11222
11427
|
return new Promise((resolve2) => {
|
|
11223
11428
|
child.once("error", (err) => {
|
|
@@ -11747,6 +11952,25 @@ function validatePrivateKey(pem) {
|
|
|
11747
11952
|
function stripControlChars(line) {
|
|
11748
11953
|
return line.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/g, "");
|
|
11749
11954
|
}
|
|
11955
|
+
function validatePortChoice(v2) {
|
|
11956
|
+
const raw = (v2 ?? "").trim();
|
|
11957
|
+
if (!raw) return void 0;
|
|
11958
|
+
if (!/^\d+$/.test(raw)) return `Enter a port number, or leave blank for ${DEFAULT_PORT}`;
|
|
11959
|
+
const n = Number(raw);
|
|
11960
|
+
if (n < 1024 || n > 65535) return "Choose a port between 1024 and 65535";
|
|
11961
|
+
if (RESERVED_PORTS.includes(n)) {
|
|
11962
|
+
return `${n} is used by the connector channel. Choose another port.`;
|
|
11963
|
+
}
|
|
11964
|
+
return void 0;
|
|
11965
|
+
}
|
|
11966
|
+
function childEnvForMode(mode, v2) {
|
|
11967
|
+
return {
|
|
11968
|
+
WEBHOOK_SECRET: v2.secret,
|
|
11969
|
+
PARTNER_ID: v2.partnerId,
|
|
11970
|
+
VERIFIES_ACCOUNT_REFERENCE: String(v2.verifiesAccountReference),
|
|
11971
|
+
...mode === "inbox" ? { CONNECTOR_TOKEN: v2.connectorToken } : { PARTNER_PRIVATE_KEY_PEM: v2.privateKey }
|
|
11972
|
+
};
|
|
11973
|
+
}
|
|
11750
11974
|
async function main() {
|
|
11751
11975
|
printHeader();
|
|
11752
11976
|
Ie("OneAddress Partner Setup");
|
|
@@ -11796,23 +12020,6 @@ async function main() {
|
|
|
11796
12020
|
"The portal rejected that secret (HTTP 401): it does not match the one on your profile.\nRegenerate it at partners.oneaddress.io -> Webhook -> Webhook signing secret\n(use Revoke and regenerate, not Rotate), copy the value shown once, and paste it here."
|
|
11797
12021
|
);
|
|
11798
12022
|
}
|
|
11799
|
-
const privateKeyRaw = await he({
|
|
11800
|
-
message: "Your ECDH Private Key",
|
|
11801
|
-
placeholder: "Paste the base64 key body from My Profile, or enter a path to the .pem file",
|
|
11802
|
-
validate: (v2) => {
|
|
11803
|
-
if (!v2.trim()) return "Private key is required";
|
|
11804
|
-
const { pem, error } = normalisePrivateKey(v2);
|
|
11805
|
-
if (error) return error;
|
|
11806
|
-
return validatePrivateKey(pem);
|
|
11807
|
-
}
|
|
11808
|
-
});
|
|
11809
|
-
assertNotCancelled(privateKeyRaw);
|
|
11810
|
-
const normalised = normalisePrivateKey(privateKeyRaw.trim());
|
|
11811
|
-
if (normalised.error) {
|
|
11812
|
-
xe(`Private key parse failed unexpectedly: ${normalised.error}`);
|
|
11813
|
-
process.exit(1);
|
|
11814
|
-
}
|
|
11815
|
-
const privateKey = normalised.pem;
|
|
11816
12023
|
const platform = await ve({
|
|
11817
12024
|
message: "Which platform?",
|
|
11818
12025
|
options: [
|
|
@@ -11829,29 +12036,6 @@ async function main() {
|
|
|
11829
12036
|
]
|
|
11830
12037
|
});
|
|
11831
12038
|
assertNotCancelled(platform);
|
|
11832
|
-
let outDir;
|
|
11833
|
-
for (; ; ) {
|
|
11834
|
-
const outputDir = await he({
|
|
11835
|
-
message: "Where to write the files?",
|
|
11836
|
-
placeholder: "./oneaddress-webhook",
|
|
11837
|
-
defaultValue: "./oneaddress-webhook"
|
|
11838
|
-
});
|
|
11839
|
-
assertNotCancelled(outputDir);
|
|
11840
|
-
outDir = outputDir.trim() || "./oneaddress-webhook";
|
|
11841
|
-
if (!(0, import_node_fs3.existsSync)(outDir) || (0, import_node_fs3.readdirSync)(outDir).length === 0) break;
|
|
11842
|
-
const overwrite = await ye({
|
|
11843
|
-
message: `${outDir} already has files \u2014 overwrite?`,
|
|
11844
|
-
initialValue: false
|
|
11845
|
-
});
|
|
11846
|
-
assertNotCancelled(overwrite);
|
|
11847
|
-
if (overwrite) break;
|
|
11848
|
-
M2.info(`Keeping the files in ${outDir}. Enter a different directory to write into.`);
|
|
11849
|
-
}
|
|
11850
|
-
let accountRefReason = "";
|
|
11851
|
-
const accountRefDeclaration = await getVerifiesAccountReference(pid, secret, (r2) => {
|
|
11852
|
-
accountRefReason = r2;
|
|
11853
|
-
});
|
|
11854
|
-
const verifiesAccountReference = accountRefDeclaration === true;
|
|
11855
12039
|
const modeChoice = platform !== "ts-node" ? "write-through" : await ve({
|
|
11856
12040
|
message: "How should this receiver handle updates?",
|
|
11857
12041
|
options: [
|
|
@@ -11885,6 +12069,69 @@ not your webhook secret and not your confirm secret, because those are shared wi
|
|
|
11885
12069
|
OneAddress and a leak of either must not also hand somebody your customers' addresses.`
|
|
11886
12070
|
);
|
|
11887
12071
|
}
|
|
12072
|
+
let privateKey = "";
|
|
12073
|
+
if (receiverMode === "inbox") {
|
|
12074
|
+
M2.info(
|
|
12075
|
+
"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."
|
|
12076
|
+
);
|
|
12077
|
+
} else {
|
|
12078
|
+
const privateKeyRaw = await he({
|
|
12079
|
+
message: "Your ECDH Private Key",
|
|
12080
|
+
placeholder: "Paste the base64 key body from My Profile, or enter a path to the .pem file",
|
|
12081
|
+
validate: (v2) => {
|
|
12082
|
+
if (!v2.trim()) return "Private key is required";
|
|
12083
|
+
const { pem, error } = normalisePrivateKey(v2);
|
|
12084
|
+
if (error) return error;
|
|
12085
|
+
return validatePrivateKey(pem);
|
|
12086
|
+
}
|
|
12087
|
+
});
|
|
12088
|
+
assertNotCancelled(privateKeyRaw);
|
|
12089
|
+
const normalised = normalisePrivateKey(privateKeyRaw.trim());
|
|
12090
|
+
if (normalised.error) {
|
|
12091
|
+
xe(`Private key parse failed unexpectedly: ${normalised.error}`);
|
|
12092
|
+
process.exit(1);
|
|
12093
|
+
}
|
|
12094
|
+
privateKey = normalised.pem;
|
|
12095
|
+
}
|
|
12096
|
+
const portAnswer = await he({
|
|
12097
|
+
message: "Which port should the receiver listen on?",
|
|
12098
|
+
placeholder: String(DEFAULT_PORT),
|
|
12099
|
+
defaultValue: String(DEFAULT_PORT),
|
|
12100
|
+
initialValue: String(DEFAULT_PORT),
|
|
12101
|
+
validate: validatePortChoice
|
|
12102
|
+
});
|
|
12103
|
+
assertNotCancelled(portAnswer);
|
|
12104
|
+
const chosenPort = Number(String(portAnswer).trim() || DEFAULT_PORT);
|
|
12105
|
+
if (!await isPortFree(chosenPort)) {
|
|
12106
|
+
M2.warn(
|
|
12107
|
+
`Port ${chosenPort} looks busy. Free it (lsof -i :${chosenPort} on macOS or Linux,
|
|
12108
|
+
netstat -ano | findstr :${chosenPort} on Windows), or change PORT in the generated
|
|
12109
|
+
.env afterwards. Setup will carry on either way.`
|
|
12110
|
+
);
|
|
12111
|
+
}
|
|
12112
|
+
let outDir;
|
|
12113
|
+
for (; ; ) {
|
|
12114
|
+
const outputDir = await he({
|
|
12115
|
+
message: "Where to write the files?",
|
|
12116
|
+
placeholder: "./oneaddress-webhook",
|
|
12117
|
+
defaultValue: "./oneaddress-webhook"
|
|
12118
|
+
});
|
|
12119
|
+
assertNotCancelled(outputDir);
|
|
12120
|
+
outDir = outputDir.trim() || "./oneaddress-webhook";
|
|
12121
|
+
if (!(0, import_node_fs3.existsSync)(outDir) || (0, import_node_fs3.readdirSync)(outDir).length === 0) break;
|
|
12122
|
+
const overwrite = await ye({
|
|
12123
|
+
message: `${outDir} already has files \u2014 overwrite?`,
|
|
12124
|
+
initialValue: false
|
|
12125
|
+
});
|
|
12126
|
+
assertNotCancelled(overwrite);
|
|
12127
|
+
if (overwrite) break;
|
|
12128
|
+
M2.info(`Keeping the files in ${outDir}. Enter a different directory to write into.`);
|
|
12129
|
+
}
|
|
12130
|
+
let accountRefReason = "";
|
|
12131
|
+
const accountRefDeclaration = await getVerifiesAccountReference(pid, secret, (r2) => {
|
|
12132
|
+
accountRefReason = r2;
|
|
12133
|
+
});
|
|
12134
|
+
const verifiesAccountReference = accountRefDeclaration === true;
|
|
11888
12135
|
if (accountRefDeclaration === void 0) {
|
|
11889
12136
|
M2.warn(
|
|
11890
12137
|
"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."
|
|
@@ -11905,7 +12152,8 @@ OneAddress and a leak of either must not also hand somebody your customers' addr
|
|
|
11905
12152
|
verifiesAccountReference,
|
|
11906
12153
|
void 0,
|
|
11907
12154
|
receiverMode,
|
|
11908
|
-
connectorToken
|
|
12155
|
+
connectorToken,
|
|
12156
|
+
chosenPort
|
|
11909
12157
|
);
|
|
11910
12158
|
s1.stop(`Scaffolded ${written.length} files in ${outDir}`);
|
|
11911
12159
|
for (const f of written) M2.success(` ${f}`);
|
|
@@ -11942,22 +12190,18 @@ OneAddress and a leak of either must not also hand somebody your customers' addr
|
|
|
11942
12190
|
}
|
|
11943
12191
|
if (choice === "skip") break;
|
|
11944
12192
|
}
|
|
11945
|
-
const
|
|
11946
|
-
|
|
11947
|
-
|
|
11948
|
-
|
|
11949
|
-
|
|
11950
|
-
|
|
11951
|
-
}
|
|
12193
|
+
const childSecrets = childEnvForMode(receiverMode, {
|
|
12194
|
+
secret,
|
|
12195
|
+
partnerId: pid,
|
|
12196
|
+
verifiesAccountReference,
|
|
12197
|
+
privateKey,
|
|
12198
|
+
connectorToken
|
|
12199
|
+
});
|
|
12200
|
+
const SERVER_PORT = chosenPort;
|
|
11952
12201
|
const s3 = Y2();
|
|
11953
12202
|
s3.start(`Starting server (waiting up to 15 s for /health on :${SERVER_PORT})`);
|
|
11954
12203
|
onCleanup(stopServer);
|
|
11955
|
-
const start = await startServer(platform, outDir, SERVER_PORT,
|
|
11956
|
-
WEBHOOK_SECRET: secret,
|
|
11957
|
-
PARTNER_ID: pid,
|
|
11958
|
-
PARTNER_PRIVATE_KEY_PEM: privateKey,
|
|
11959
|
-
VERIFIES_ACCOUNT_REFERENCE: String(verifiesAccountReference)
|
|
11960
|
-
});
|
|
12204
|
+
const start = await startServer(platform, outDir, SERVER_PORT, childSecrets);
|
|
11961
12205
|
let serverRunning = false;
|
|
11962
12206
|
if (start.ok) {
|
|
11963
12207
|
s3.stop(`Server is healthy on port ${SERVER_PORT}`);
|
|
@@ -12031,7 +12275,7 @@ Continuing will overwrite it.`
|
|
|
12031
12275
|
s4.start("Starting Cloudflare Tunnel");
|
|
12032
12276
|
onCleanup(stopTunnel);
|
|
12033
12277
|
try {
|
|
12034
|
-
let tunnel = await startTunnel(
|
|
12278
|
+
let tunnel = await startTunnel(SERVER_PORT);
|
|
12035
12279
|
webhookUrl = `${tunnel.url}/webhook`;
|
|
12036
12280
|
tunnelBase = tunnel.url;
|
|
12037
12281
|
s4.stop(`Tunnel active: ${tunnel.url}`);
|
|
@@ -12044,7 +12288,7 @@ Continuing will overwrite it.`
|
|
|
12044
12288
|
if (!webhookReady) {
|
|
12045
12289
|
s4b.stop("First tunnel is slow to route, trying a fresh one");
|
|
12046
12290
|
stopTunnel();
|
|
12047
|
-
tunnel = await startTunnel(
|
|
12291
|
+
tunnel = await startTunnel(SERVER_PORT);
|
|
12048
12292
|
webhookUrl = `${tunnel.url}/webhook`;
|
|
12049
12293
|
tunnelBase = tunnel.url;
|
|
12050
12294
|
const s4c = Y2();
|
|
@@ -12057,7 +12301,7 @@ Continuing will overwrite it.`
|
|
|
12057
12301
|
if (!webhookReady) {
|
|
12058
12302
|
M2.warn(
|
|
12059
12303
|
`The tunnel edge is still not routing after a retry. This is almost always
|
|
12060
|
-
one of two things, and neither is your server (it is running on
|
|
12304
|
+
one of two things, and neither is your server (it is running on :${SERVER_PORT}):
|
|
12061
12305
|
1. Open ${tunnel.url}/health in a browser. If it never loads, your network
|
|
12062
12306
|
is blocking cloudflared and no wait will fix it, so try a different network.
|
|
12063
12307
|
2. The tunnel only lives while this wizard runs, so keep this window open.`
|
|
@@ -12067,7 +12311,8 @@ one of two things, and neither is your server (it is running on :3001):
|
|
|
12067
12311
|
s4.stop("Tunnel setup failed");
|
|
12068
12312
|
M2.warn(`Could not start tunnel: ${err instanceof Error ? err.message : String(err)}`);
|
|
12069
12313
|
M2.warn(
|
|
12070
|
-
|
|
12314
|
+
`To get a public URL manually, try: npx cloudflared tunnel --url http://localhost:${SERVER_PORT}
|
|
12315
|
+
Or deploy to any server and set the URL in partners.oneaddress.io \u2192 My Profile.`
|
|
12071
12316
|
);
|
|
12072
12317
|
}
|
|
12073
12318
|
}
|
|
@@ -12105,7 +12350,7 @@ one of two things, and neither is your server (it is running on :3001):
|
|
|
12105
12350
|
if (!webhookReady) {
|
|
12106
12351
|
M2.warn(
|
|
12107
12352
|
`Skipping the conformance checks: the tunnel edge is not routing.
|
|
12108
|
-
Your server is fine, it is running on
|
|
12353
|
+
Your server is fine, it is running on :${SERVER_PORT}. This is either your network
|
|
12109
12354
|
blocking cloudflared (open ${tunnelBase}/health in a browser to check) or the
|
|
12110
12355
|
tunnel needing this wizard to stay open. Re-run once that URL loads in a browser.`
|
|
12111
12356
|
);
|
|
@@ -12116,18 +12361,28 @@ tunnel needing this wizard to stay open. Re-run once that URL loads in a browser
|
|
|
12116
12361
|
await new Promise((r2) => setTimeout(r2, 200));
|
|
12117
12362
|
s6.stop("Conformance checks:");
|
|
12118
12363
|
await runConformance(webhookUrl, pid, secret);
|
|
12119
|
-
|
|
12120
|
-
|
|
12364
|
+
if (receiverMode === "inbox") {
|
|
12365
|
+
M2.info(
|
|
12366
|
+
"Decrypt check skipped: this receiver holds no private key, which is what inbox\nmode is for. Your connector is the process that decrypts, so prove the key there:\nstart it against this receiver and confirm it draws and applies a dispatch."
|
|
12367
|
+
);
|
|
12368
|
+
} else {
|
|
12369
|
+
await runDecryptCheck(secret, pid);
|
|
12370
|
+
}
|
|
12371
|
+
if (verifiesAccountReference && receiverMode !== "inbox") {
|
|
12121
12372
|
M2.info(
|
|
12122
12373
|
"You have declared that your receiver matches the account reference itself.\nRun `npm test` to include check-14, which sends an account reference that matches\nnothing and fails a receiver that reports it as applied. The three checks above\ncover signing and replay only."
|
|
12123
12374
|
);
|
|
12375
|
+
} else if (verifiesAccountReference) {
|
|
12376
|
+
M2.info(
|
|
12377
|
+
"You have declared that your receiver matches the account reference itself.\nIn inbox mode your CONNECTOR does that, before it applies anything, and it\nrefuses a dispatch whose reference matches none of your records. The three checks\nabove cover signing and replay only."
|
|
12378
|
+
);
|
|
12124
12379
|
}
|
|
12125
12380
|
}
|
|
12126
12381
|
}
|
|
12127
12382
|
const summary = [
|
|
12128
12383
|
`Partner ID: ${pid}`,
|
|
12129
12384
|
`Webhook URL: ${webhookUrl || "(not set \u2014 update in portal)"}`,
|
|
12130
|
-
`Server port:
|
|
12385
|
+
`Server port: ${SERVER_PORT}`,
|
|
12131
12386
|
`Files in: ${outDir}`
|
|
12132
12387
|
].join("\n");
|
|
12133
12388
|
const nextSteps = [
|
|
@@ -12142,12 +12397,7 @@ tunnel needing this wizard to stay open. Re-run once that URL loads in a browser
|
|
|
12142
12397
|
if (serverRunning) {
|
|
12143
12398
|
const openDashboard = platformHasDashboard(platform) ? await ye({ message: "Open the receiver dashboard now?", initialValue: true }) : false;
|
|
12144
12399
|
if (openDashboard === true) {
|
|
12145
|
-
const handover = await handOverTerminal(platform, outDir, SERVER_PORT,
|
|
12146
|
-
WEBHOOK_SECRET: secret,
|
|
12147
|
-
PARTNER_ID: pid,
|
|
12148
|
-
PARTNER_PRIVATE_KEY_PEM: privateKey,
|
|
12149
|
-
VERIFIES_ACCOUNT_REFERENCE: String(verifiesAccountReference)
|
|
12150
|
-
});
|
|
12400
|
+
const handover = await handOverTerminal(platform, outDir, SERVER_PORT, childSecrets);
|
|
12151
12401
|
if (!handover.ok) {
|
|
12152
12402
|
M2.warn(
|
|
12153
12403
|
`${handover.reason ?? "The dashboard could not start."}
|
|
@@ -12161,7 +12411,7 @@ Your receiver is set up and working. Start it yourself with:
|
|
|
12161
12411
|
console.log("");
|
|
12162
12412
|
console.log(` ${DIM2}\u250C${"\u2500".repeat(BOX_WIDTH)}\u2510${R3}`);
|
|
12163
12413
|
for (const inner of [
|
|
12164
|
-
` ${GRN}\u25C8${R3} ${CRM2}Server live${R3} ${MID2}on http://localhost
|
|
12414
|
+
` ${GRN}\u25C8${R3} ${CRM2}Server live${R3} ${MID2}on http://localhost:${SERVER_PORT}/webhook${R3}`,
|
|
12165
12415
|
` ${AMB2}\u25C8${R3} ${MID2}Edit ${CRM2}src/store.ts${R3} ${MID2}to wire your database${R3}`,
|
|
12166
12416
|
` ${MID2}Press Ctrl+C to stop the server and exit.${R3}`,
|
|
12167
12417
|
...platformHasDashboard(platform) ? [` ${MID2}Run ${CRM2}npm start${R3} ${MID2}here any time for the dashboard.${R3}`] : []
|