@oneaddress/setup 2.0.0 → 2.1.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 +251 -74
- 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.1.0" : "?";
|
|
860
860
|
function printCompactHeader() {
|
|
861
861
|
const INNER = 42;
|
|
862
862
|
const TOP = fn("\u250C") + dm("\u2500".repeat(INNER)) + fn("\u2510");
|
|
@@ -1598,8 +1598,8 @@ import { formatLine, report, type ReportLine } from './report.js';
|
|
|
1598
1598
|
import { allCustomers, customerCount, storeEncrypted, type StoredCustomer } from './store.js';
|
|
1599
1599
|
|
|
1600
1600
|
/** blessed takes colours as strings; these mirror the site's palette. */
|
|
1601
|
-
const AMBER = HEX.amber;
|
|
1602
|
-
const CREAM = HEX.cream;
|
|
1601
|
+
const AMBER = HEX.amber.toLowerCase();
|
|
1602
|
+
const CREAM = HEX.cream.toLowerCase();
|
|
1603
1603
|
const DIM = '#8a7f6a';
|
|
1604
1604
|
|
|
1605
1605
|
/** Escape blessed's tag syntax so a customer's name can never inject markup. */
|
|
@@ -1623,6 +1623,14 @@ export function startDashboard({ partnerName, port, onQuit }: TuiOptions): void
|
|
|
1623
1623
|
smartCSR: true,
|
|
1624
1624
|
title: \`\${partnerName} \u2014 OneAddress receiver\`,
|
|
1625
1625
|
fullUnicode: true,
|
|
1626
|
+
// WITHOUT THIS THE BRAND COMES OUT RED ON WINDOWS. blessed resolves a hex
|
|
1627
|
+
// colour against whatever palette it believes the terminal has, and it
|
|
1628
|
+
// infers that from TERM, which Windows does not set. With no TERM it
|
|
1629
|
+
// assumes a tiny palette and snaps #e0a248 to the nearest thing it thinks
|
|
1630
|
+
// exists, which is red. Windows Terminal and PowerShell both do 256 colours
|
|
1631
|
+
// and more, so naming the capability is simply telling it the truth.
|
|
1632
|
+
// Anything that DOES set TERM keeps its own value.
|
|
1633
|
+
terminal: process.env.TERM || 'xterm-256color',
|
|
1626
1634
|
});
|
|
1627
1635
|
|
|
1628
1636
|
// \u2500\u2500 Masthead \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
|
|
@@ -1808,9 +1816,88 @@ export function notePreviousAddress(prev: Record<string, unknown>): void {
|
|
|
1808
1816
|
import { printBanner } from './brand.js';
|
|
1809
1817
|
import { WrongPassphraseError } from './vault.js';
|
|
1810
1818
|
|
|
1819
|
+
/**
|
|
1820
|
+
* No dashboard without a terminal to draw it in.
|
|
1821
|
+
*
|
|
1822
|
+
* FOUND THE FIRST TIME A PARTNER RAN THIS, 14 Sep 2026. The setup wizard starts
|
|
1823
|
+
* your receiver for you as a CHILD PROCESS with its output piped, so there is no
|
|
1824
|
+
* TTY. The passphrase prompt already handled that correctly by skipping, but the
|
|
1825
|
+
* dashboard went ahead and started anyway, drawing into a pipe: the server ran
|
|
1826
|
+
* fine and conformance passed while the UI rendered to nobody.
|
|
1827
|
+
*
|
|
1828
|
+
* \`process.stdout.isTTY\` is the honest test. It is false under the wizard's
|
|
1829
|
+
* autostart, under a systemd or pm2 unit, in CI, and behind any pipe \u2014 every
|
|
1830
|
+
* case where a full-screen UI is the wrong answer and plain log lines are the
|
|
1831
|
+
* right one. Run \`npm start\` yourself in a real terminal and you get the
|
|
1832
|
+
* dashboard; anything else gets readable output instead of escape codes.
|
|
1833
|
+
*/
|
|
1811
1834
|
const headless =
|
|
1812
1835
|
process.argv.includes('--headless') ||
|
|
1813
|
-
process.env.ONEADDRESS_HEADLESS === '1'
|
|
1836
|
+
process.env.ONEADDRESS_HEADLESS === '1' ||
|
|
1837
|
+
!process.stdout.isTTY;
|
|
1838
|
+
|
|
1839
|
+
/**
|
|
1840
|
+
* Has this database already been locked?
|
|
1841
|
+
*
|
|
1842
|
+
* Asked BEFORE the passphrase, and without one, so the prompt can say which of
|
|
1843
|
+
* two completely different things it is doing. \`db_meta.verifier\` is written the
|
|
1844
|
+
* first time a passphrase is set, so its presence is the whole answer.
|
|
1845
|
+
*
|
|
1846
|
+
* Read through its own connection rather than importing \`db.ts\`, which derives
|
|
1847
|
+
* its keys the moment it is imported and would therefore have to run BEFORE we
|
|
1848
|
+
* know what to ask for.
|
|
1849
|
+
*/
|
|
1850
|
+
async function databaseIsLocked(): Promise<boolean> {
|
|
1851
|
+
try {
|
|
1852
|
+
const { DatabaseSync } = await import('node:sqlite');
|
|
1853
|
+
const { join } = await import('node:path');
|
|
1854
|
+
const path = process.env.DB_PATH ?? join(process.cwd(), 'data.db');
|
|
1855
|
+
const db = new DatabaseSync(path, { readOnly: true });
|
|
1856
|
+
try {
|
|
1857
|
+
const row = db
|
|
1858
|
+
.prepare("SELECT value FROM db_meta WHERE key = 'verifier'")
|
|
1859
|
+
.get() as { value?: string } | undefined;
|
|
1860
|
+
return Boolean(row?.value);
|
|
1861
|
+
} finally {
|
|
1862
|
+
db.close();
|
|
1863
|
+
}
|
|
1864
|
+
} catch {
|
|
1865
|
+
// No file yet, or no db_meta table yet. Either way: not locked.
|
|
1866
|
+
return false;
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
/**
|
|
1871
|
+
* Ask for a passphrase without echoing it to the screen.
|
|
1872
|
+
*
|
|
1873
|
+
* A passphrase typed in clear on a shared screen, in a screen-share, or into a
|
|
1874
|
+
* terminal that keeps scrollback is not much of a secret. readline echoes by
|
|
1875
|
+
* default, so its output hook is replaced for the duration of the question.
|
|
1876
|
+
*
|
|
1877
|
+
* Degrades to a visible prompt rather than failing: on a terminal where the
|
|
1878
|
+
* hook is not available, being asked in the clear beats not being asked.
|
|
1879
|
+
*/
|
|
1880
|
+
async function ask(prompt: string): Promise<string> {
|
|
1881
|
+
const { createInterface } = await import('node:readline/promises');
|
|
1882
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
1883
|
+
const hooked = rl as unknown as { _writeToOutput?: (s: string) => void };
|
|
1884
|
+
const original = hooked._writeToOutput;
|
|
1885
|
+
try {
|
|
1886
|
+
hooked._writeToOutput = function (chunk: string): void {
|
|
1887
|
+
// Echo the prompt itself, mask everything the user types.
|
|
1888
|
+
if (chunk.includes(prompt)) process.stdout.write(chunk);
|
|
1889
|
+
else process.stdout.write('*');
|
|
1890
|
+
};
|
|
1891
|
+
} catch { /* keep the visible prompt */ }
|
|
1892
|
+
try {
|
|
1893
|
+
const answer = await rl.question(prompt);
|
|
1894
|
+
process.stdout.write('\\n');
|
|
1895
|
+
return answer.trim();
|
|
1896
|
+
} finally {
|
|
1897
|
+
if (original) hooked._writeToOutput = original;
|
|
1898
|
+
rl.close();
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1814
1901
|
|
|
1815
1902
|
/**
|
|
1816
1903
|
* Where the at-rest passphrase comes from.
|
|
@@ -1820,6 +1907,12 @@ const headless =
|
|
|
1820
1907
|
* headless deployment or a piped stdin has nobody to answer, and blocking on a
|
|
1821
1908
|
* prompt nobody can see is the worst of the three outcomes.
|
|
1822
1909
|
*
|
|
1910
|
+
* SETTING AND UNLOCKING ARE DIFFERENT QUESTIONS AND ARE ASKED DIFFERENTLY.
|
|
1911
|
+
* Until 14 Sep 2026 both said the same thing, so the first run gave no way to
|
|
1912
|
+
* tell whether a passphrase was being created or checked, and no confirmation.
|
|
1913
|
+
* Getting a new passphrase wrong twice in agreement is recoverable; getting it
|
|
1914
|
+
* wrong once, silently, means a database nothing can open.
|
|
1915
|
+
*
|
|
1823
1916
|
* An empty answer is legitimate, not a failure. It runs the database in the
|
|
1824
1917
|
* clear, which is what this receiver did before at-rest encryption existed, and
|
|
1825
1918
|
* the dashboard shows that state in red rather than letting it pass unnoticed.
|
|
@@ -1827,19 +1920,36 @@ const headless =
|
|
|
1827
1920
|
async function resolvePassphrase(): Promise<string | null> {
|
|
1828
1921
|
const fromEnv = process.env.ONEADDRESS_DB_PASSPHRASE;
|
|
1829
1922
|
if (fromEnv && fromEnv.trim()) return fromEnv;
|
|
1923
|
+
// Both streams are checked, and they are not the same question: output can be
|
|
1924
|
+
// a pipe while input is still a terminal (\`npm start | tee log\`), and stdin
|
|
1925
|
+
// can be closed while stdout is a terminal. Prompting needs stdin; drawing
|
|
1926
|
+
// needs stdout.
|
|
1830
1927
|
if (headless || !process.stdin.isTTY) return null;
|
|
1831
1928
|
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
process.stdout.write(' Your customer records can be encrypted at rest.\\n');
|
|
1836
|
-
process.stdout.write(' This passphrase is YOURS. It is not the OneAddress private key,\\n');
|
|
1837
|
-
process.stdout.write(' and OneAddress never sees it and cannot recover it.\\n');
|
|
1838
|
-
process.stdout.write(' Leave blank to store records unencrypted.\\n\\n');
|
|
1839
|
-
const answer = (await rl.question(' Passphrase: ')).trim();
|
|
1929
|
+
if (await databaseIsLocked()) {
|
|
1930
|
+
process.stdout.write('\\n This customer database is encrypted.\\n\\n');
|
|
1931
|
+
const answer = await ask(' Passphrase to unlock: ');
|
|
1840
1932
|
return answer || null;
|
|
1841
|
-
}
|
|
1842
|
-
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
process.stdout.write('\\n SET A PASSPHRASE to encrypt your customer records at rest.\\n');
|
|
1936
|
+
process.stdout.write(' It is YOURS. It is not the OneAddress private key, and OneAddress\\n');
|
|
1937
|
+
process.stdout.write(' never sees it and CANNOT RECOVER IT. Lose it and the records in\\n');
|
|
1938
|
+
process.stdout.write(' data.db cannot be read again.\\n');
|
|
1939
|
+
process.stdout.write(' Press Enter to skip and store records unencrypted.\\n\\n');
|
|
1940
|
+
|
|
1941
|
+
for (;;) {
|
|
1942
|
+
const first = await ask(' New passphrase: ');
|
|
1943
|
+
if (!first) {
|
|
1944
|
+
process.stdout.write(' Continuing WITHOUT encryption.\\n\\n');
|
|
1945
|
+
return null;
|
|
1946
|
+
}
|
|
1947
|
+
const again = await ask(' Confirm passphrase: ');
|
|
1948
|
+
if (first === again) {
|
|
1949
|
+
process.stdout.write(' Passphrase set. Keep it somewhere you will still have it.\\n\\n');
|
|
1950
|
+
return first;
|
|
1951
|
+
}
|
|
1952
|
+
process.stdout.write(' Those do not match. Try again.\\n\\n');
|
|
1843
1953
|
}
|
|
1844
1954
|
}
|
|
1845
1955
|
|
|
@@ -1898,6 +2008,7 @@ main().catch((err) => {
|
|
|
1898
2008
|
* override), then oneaddress.config.json (what setup wrote), then a built-in
|
|
1899
2009
|
* default. A missing config file is not an error \u2014 the handler still runs.
|
|
1900
2010
|
*/
|
|
2011
|
+
import { report } from './report.js';
|
|
1901
2012
|
import { readFileSync } from 'node:fs';
|
|
1902
2013
|
import { join } from 'node:path';
|
|
1903
2014
|
|
|
@@ -1947,7 +2058,7 @@ export const config: ReceiverConfig = {
|
|
|
1947
2058
|
: (fromFile.verifiesAccountReference ?? DEFAULTS.verifiesAccountReference),
|
|
1948
2059
|
};
|
|
1949
2060
|
|
|
1950
|
-
|
|
2061
|
+
report.info(
|
|
1951
2062
|
'[config] loaded (oneAddressApi=' + config.oneAddressApi +
|
|
1952
2063
|
', verifiesAccountReference=' + config.verifiesAccountReference + ')',
|
|
1953
2064
|
);
|
|
@@ -2005,6 +2116,7 @@ export function safeOneAddressCallbackUrl(raw: string): string | null {
|
|
|
2005
2116
|
*/
|
|
2006
2117
|
import { readFileSync } from 'node:fs';
|
|
2007
2118
|
import { join } from 'node:path';
|
|
2119
|
+
import { report } from './report.js';
|
|
2008
2120
|
import db, { accountKey, dec, enc, encrypted, once } from './db.js';
|
|
2009
2121
|
|
|
2010
2122
|
export type Address = Record<string, unknown>;
|
|
@@ -2049,7 +2161,7 @@ function ensureColumn(table: string, column: string, definition: string): void {
|
|
|
2049
2161
|
const cols = db.prepare(\`PRAGMA table_info(\${table})\`).all() as Array<{ name: string }>;
|
|
2050
2162
|
if (!cols.some((c) => c.name === column)) {
|
|
2051
2163
|
db.exec(\`ALTER TABLE \${table} ADD COLUMN \${column} \${definition}\`);
|
|
2052
|
-
|
|
2164
|
+
report.info(\`[store] migrated: added column \${table}.\${column}\`);
|
|
2053
2165
|
}
|
|
2054
2166
|
}
|
|
2055
2167
|
ensureColumn('customers', 'address', "TEXT NOT NULL DEFAULT '{}'");
|
|
@@ -2134,7 +2246,7 @@ function loadRoster(): RosterEntry[] {
|
|
|
2134
2246
|
return roster;
|
|
2135
2247
|
} catch (err) {
|
|
2136
2248
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2137
|
-
|
|
2249
|
+
report.warn(\`[store] customers.json not loaded (\${msg}) \u2014 using the built-in demo roster. Edit customers.json to set your customers.\`);
|
|
2138
2250
|
return DEFAULT_ROSTER;
|
|
2139
2251
|
}
|
|
2140
2252
|
}
|
|
@@ -2154,7 +2266,7 @@ const ROSTER = loadRoster();
|
|
|
2154
2266
|
address: enc('address', JSON.stringify(c.address)),
|
|
2155
2267
|
});
|
|
2156
2268
|
}
|
|
2157
|
-
|
|
2269
|
+
report.info(\`[store] roster ready (\${ROSTER.length} customers)\`);
|
|
2158
2270
|
}
|
|
2159
2271
|
|
|
2160
2272
|
// \u2500\u2500 Identity handed in by server.ts after it verifies + decrypts \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
@@ -2277,13 +2389,13 @@ export type VerifyResult = 'match' | 'mismatch' | 'not_found';
|
|
|
2277
2389
|
export async function verifyAddress(customer: Customer, incoming: Address): Promise<VerifyResult> {
|
|
2278
2390
|
const row = findCustomer(customer.accountNumber, customer.name);
|
|
2279
2391
|
if (!row) {
|
|
2280
|
-
|
|
2392
|
+
report.info(\`[store] verifyAddress \u2192 not_found (\${customer.accountNumber || customer.name || '(none)'})\`);
|
|
2281
2393
|
return 'not_found';
|
|
2282
2394
|
}
|
|
2283
2395
|
let stored: Address = {};
|
|
2284
2396
|
try { stored = JSON.parse(row.address) as Address; } catch { /* corrupt row \u2192 treat as empty */ }
|
|
2285
2397
|
const result: VerifyResult = canonicalAddress(stored) === canonicalAddress(incoming) ? 'match' : 'mismatch';
|
|
2286
|
-
|
|
2398
|
+
report.info(\`[store] verifyAddress \u2192 \${result} (\${row.account_number})\`);
|
|
2287
2399
|
return result;
|
|
2288
2400
|
}
|
|
2289
2401
|
|
|
@@ -2340,7 +2452,7 @@ export async function saveAddress(customer: Customer, incoming: Address): Promis
|
|
|
2340
2452
|
// Metadata only \u2014 the address itself is personal information, so the key is
|
|
2341
2453
|
// logged and the address never is. Centralised log aggregation turns every
|
|
2342
2454
|
// log line into a place customer addresses can be read.
|
|
2343
|
-
|
|
2455
|
+
report.info(\`[store] saved address for \${acct}\${customer.loaRef ? \` (loa_ref \${customer.loaRef})\` : ''}\`);
|
|
2344
2456
|
|
|
2345
2457
|
return previous;
|
|
2346
2458
|
}
|
|
@@ -7878,7 +7990,7 @@ async function scaffold(platform, outputDir, partnerId, webhookSecret, webhookUr
|
|
|
7878
7990
|
|
|
7879
7991
|
// src/register.ts
|
|
7880
7992
|
var import_node_crypto = require("crypto");
|
|
7881
|
-
var PKG_VERSION = true ? "2.
|
|
7993
|
+
var PKG_VERSION = true ? "2.1.0" : "dev";
|
|
7882
7994
|
var REGISTER_URL = "https://partners.oneaddress.io/api/partner/installs";
|
|
7883
7995
|
function hmacSha256(secret, message) {
|
|
7884
7996
|
return (0, import_node_crypto.createHmac)("sha256", secret).update(message).digest("hex");
|
|
@@ -8118,6 +8230,55 @@ var COMMANDS2 = {
|
|
|
8118
8230
|
};
|
|
8119
8231
|
var serverProcess = null;
|
|
8120
8232
|
var serverOutput = "";
|
|
8233
|
+
var PASSTHROUGH_KEYS = [
|
|
8234
|
+
"PATH",
|
|
8235
|
+
// every runtime resolves binaries from PATH
|
|
8236
|
+
"HOME",
|
|
8237
|
+
// pip/npm/composer caches, ~/.m2 for Maven
|
|
8238
|
+
"USERPROFILE",
|
|
8239
|
+
// Windows equivalent of HOME
|
|
8240
|
+
"APPDATA",
|
|
8241
|
+
"LOCALAPPDATA",
|
|
8242
|
+
// Windows config dirs
|
|
8243
|
+
"SystemRoot",
|
|
8244
|
+
"SYSTEMROOT",
|
|
8245
|
+
"ComSpec",
|
|
8246
|
+
"WINDIR",
|
|
8247
|
+
// cmd.exe needs these
|
|
8248
|
+
"TEMP",
|
|
8249
|
+
"TMP",
|
|
8250
|
+
"TMPDIR",
|
|
8251
|
+
// tempdir resolution for tsx/uvicorn
|
|
8252
|
+
"LANG",
|
|
8253
|
+
"LC_ALL",
|
|
8254
|
+
"LC_CTYPE",
|
|
8255
|
+
// utf-8 in subprocess output
|
|
8256
|
+
"JAVA_HOME",
|
|
8257
|
+
"GOPATH",
|
|
8258
|
+
"GOROOT",
|
|
8259
|
+
// toolchain locations if set
|
|
8260
|
+
"PORT"
|
|
8261
|
+
// partner might override port via .env or shell
|
|
8262
|
+
];
|
|
8263
|
+
function buildChildEnv(secrets) {
|
|
8264
|
+
const childEnv = {};
|
|
8265
|
+
for (const key of PASSTHROUGH_KEYS) {
|
|
8266
|
+
const v2 = process.env[key];
|
|
8267
|
+
if (typeof v2 === "string") childEnv[key] = v2;
|
|
8268
|
+
}
|
|
8269
|
+
for (const [key, value] of Object.entries(secrets)) {
|
|
8270
|
+
if (value) childEnv[key] = value;
|
|
8271
|
+
}
|
|
8272
|
+
return childEnv;
|
|
8273
|
+
}
|
|
8274
|
+
function spawnReceiver(spec, outputDir, stdio, secrets) {
|
|
8275
|
+
const isWin = process.platform === "win32";
|
|
8276
|
+
return (0, import_node_child_process2.spawn)(
|
|
8277
|
+
isWin ? "cmd.exe" : spec.cmd,
|
|
8278
|
+
isWin ? ["/c", spec.cmd, ...spec.args] : spec.args,
|
|
8279
|
+
{ cwd: outputDir, stdio, detached: false, shell: false, env: buildChildEnv(secrets) }
|
|
8280
|
+
);
|
|
8281
|
+
}
|
|
8121
8282
|
function stopServer() {
|
|
8122
8283
|
if (serverProcess) {
|
|
8123
8284
|
try {
|
|
@@ -8168,56 +8329,7 @@ async function startServer(platform, outputDir, port = 3001, secrets = {}) {
|
|
|
8168
8329
|
}
|
|
8169
8330
|
serverOutput = "";
|
|
8170
8331
|
const manualCommand = `cd ${outputDir} && ${spec.cmd} ${spec.args.join(" ")}`;
|
|
8171
|
-
|
|
8172
|
-
"PATH",
|
|
8173
|
-
// every runtime resolves binaries from PATH
|
|
8174
|
-
"HOME",
|
|
8175
|
-
// pip/npm/composer caches, ~/.m2 for Maven
|
|
8176
|
-
"USERPROFILE",
|
|
8177
|
-
// Windows equivalent of HOME
|
|
8178
|
-
"APPDATA",
|
|
8179
|
-
"LOCALAPPDATA",
|
|
8180
|
-
// Windows config dirs
|
|
8181
|
-
"SystemRoot",
|
|
8182
|
-
"SYSTEMROOT",
|
|
8183
|
-
"ComSpec",
|
|
8184
|
-
"WINDIR",
|
|
8185
|
-
// cmd.exe needs these
|
|
8186
|
-
"TEMP",
|
|
8187
|
-
"TMP",
|
|
8188
|
-
"TMPDIR",
|
|
8189
|
-
// tempdir resolution for tsx/uvicorn
|
|
8190
|
-
"LANG",
|
|
8191
|
-
"LC_ALL",
|
|
8192
|
-
"LC_CTYPE",
|
|
8193
|
-
// utf-8 in subprocess output
|
|
8194
|
-
"JAVA_HOME",
|
|
8195
|
-
"GOPATH",
|
|
8196
|
-
"GOROOT",
|
|
8197
|
-
// toolchain locations if set
|
|
8198
|
-
"PORT"
|
|
8199
|
-
// partner might override port via .env or shell
|
|
8200
|
-
];
|
|
8201
|
-
const childEnv = {};
|
|
8202
|
-
for (const key of PASSTHROUGH_KEYS) {
|
|
8203
|
-
const v2 = process.env[key];
|
|
8204
|
-
if (typeof v2 === "string") childEnv[key] = v2;
|
|
8205
|
-
}
|
|
8206
|
-
for (const [key, value] of Object.entries(secrets)) {
|
|
8207
|
-
if (value) childEnv[key] = value;
|
|
8208
|
-
}
|
|
8209
|
-
const isWin = process.platform === "win32";
|
|
8210
|
-
serverProcess = (0, import_node_child_process2.spawn)(
|
|
8211
|
-
isWin ? "cmd.exe" : spec.cmd,
|
|
8212
|
-
isWin ? ["/c", spec.cmd, ...spec.args] : spec.args,
|
|
8213
|
-
{
|
|
8214
|
-
cwd: outputDir,
|
|
8215
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
8216
|
-
detached: false,
|
|
8217
|
-
shell: false,
|
|
8218
|
-
env: childEnv
|
|
8219
|
-
}
|
|
8220
|
-
);
|
|
8332
|
+
serverProcess = spawnReceiver(spec, outputDir, ["ignore", "pipe", "pipe"], secrets);
|
|
8221
8333
|
serverProcess.stdout?.on("data", (d3) => {
|
|
8222
8334
|
serverOutput += d3.toString();
|
|
8223
8335
|
});
|
|
@@ -8231,6 +8343,49 @@ async function startServer(platform, outputDir, port = 3001, secrets = {}) {
|
|
|
8231
8343
|
const ok = await pollHealth(port, 15e3);
|
|
8232
8344
|
return { ok, output: serverOutput, manualCommand };
|
|
8233
8345
|
}
|
|
8346
|
+
async function waitForPortFree(port, timeoutMs = 8e3) {
|
|
8347
|
+
const deadline = Date.now() + timeoutMs;
|
|
8348
|
+
while (Date.now() < deadline) {
|
|
8349
|
+
try {
|
|
8350
|
+
await fetch(`http://localhost:${port}/health`, { signal: AbortSignal.timeout(1e3) });
|
|
8351
|
+
} catch {
|
|
8352
|
+
return true;
|
|
8353
|
+
}
|
|
8354
|
+
await new Promise((r2) => setTimeout(r2, 300));
|
|
8355
|
+
}
|
|
8356
|
+
return false;
|
|
8357
|
+
}
|
|
8358
|
+
async function handOverTerminal(platform, outputDir, port, secrets = {}) {
|
|
8359
|
+
const spec = COMMANDS2[platform];
|
|
8360
|
+
const manualCommand = spec ? `cd ${outputDir} && ${spec.cmd} ${spec.args.join(" ")}` : `cd ${outputDir} && npm start`;
|
|
8361
|
+
if (!spec) return { ok: false, reason: `No start command defined for platform: ${platform}`, manualCommand };
|
|
8362
|
+
stopServer();
|
|
8363
|
+
if (!await waitForPortFree(port)) {
|
|
8364
|
+
return {
|
|
8365
|
+
ok: false,
|
|
8366
|
+
reason: `Port ${port} is still in use, so the dashboard could not take it over.`,
|
|
8367
|
+
manualCommand
|
|
8368
|
+
};
|
|
8369
|
+
}
|
|
8370
|
+
if (process.stdin.isTTY) {
|
|
8371
|
+
try {
|
|
8372
|
+
process.stdin.setRawMode(false);
|
|
8373
|
+
} catch {
|
|
8374
|
+
}
|
|
8375
|
+
}
|
|
8376
|
+
process.stdin.pause();
|
|
8377
|
+
const child = spawnReceiver(spec, outputDir, "inherit", secrets);
|
|
8378
|
+
serverProcess = child;
|
|
8379
|
+
return new Promise((resolve2) => {
|
|
8380
|
+
child.once("error", (err) => {
|
|
8381
|
+
resolve2({ ok: false, reason: err.message, manualCommand });
|
|
8382
|
+
});
|
|
8383
|
+
child.once("exit", () => {
|
|
8384
|
+
serverProcess = null;
|
|
8385
|
+
resolve2({ ok: true, manualCommand });
|
|
8386
|
+
});
|
|
8387
|
+
});
|
|
8388
|
+
}
|
|
8234
8389
|
|
|
8235
8390
|
// src/tunnel.ts
|
|
8236
8391
|
var import_node_child_process3 = require("child_process");
|
|
@@ -9098,7 +9253,7 @@ tunnel needing this wizard to stay open. Re-run once that URL loads in a browser
|
|
|
9098
9253
|
].join("\n");
|
|
9099
9254
|
const nextSteps = [
|
|
9100
9255
|
"1. Trigger a live event: update your address at oneaddress.io\n \u2192 src/store.ts will save it to data.db automatically",
|
|
9101
|
-
"2.
|
|
9256
|
+
"2. Watch it arrive: the dashboard shows each dispatch and the old-to-new change.\n Set a passphrase there and data.db is encrypted at rest, which also means\n DB Browser for SQLite will show you ciphertext rather than addresses.\n Skip the passphrase and the file stays readable in any SQLite viewer.",
|
|
9102
9257
|
"3. For production: swap src/store.ts for your real database (Postgres, MySQL, etc.)\n then deploy and update the webhook URL in the portal",
|
|
9103
9258
|
"4. Questions? partners.oneaddress.io or partners@oneaddress.io"
|
|
9104
9259
|
].join("\n\n");
|
|
@@ -9106,12 +9261,34 @@ tunnel needing this wizard to stay open. Re-run once that URL loads in a browser
|
|
|
9106
9261
|
Me(nextSteps, "Next steps");
|
|
9107
9262
|
Se("Setup complete! Welcome to the OneAddress Partner Network.");
|
|
9108
9263
|
if (serverRunning) {
|
|
9264
|
+
const openDashboard = await ye({
|
|
9265
|
+
message: "Open the receiver dashboard now?",
|
|
9266
|
+
initialValue: true
|
|
9267
|
+
});
|
|
9268
|
+
if (openDashboard === true) {
|
|
9269
|
+
const handover = await handOverTerminal(platform, outDir, SERVER_PORT, {
|
|
9270
|
+
WEBHOOK_SECRET: secret,
|
|
9271
|
+
PARTNER_ID: pid,
|
|
9272
|
+
PARTNER_PRIVATE_KEY_PEM: privateKey,
|
|
9273
|
+
VERIFIES_ACCOUNT_REFERENCE: String(verifiesAccountReference)
|
|
9274
|
+
});
|
|
9275
|
+
if (!handover.ok) {
|
|
9276
|
+
M2.warn(
|
|
9277
|
+
`${handover.reason ?? "The dashboard could not start."}
|
|
9278
|
+
Your receiver is set up and working. Start it yourself with:
|
|
9279
|
+
${handover.manualCommand}`
|
|
9280
|
+
);
|
|
9281
|
+
}
|
|
9282
|
+
runCleanup();
|
|
9283
|
+
process.exit(0);
|
|
9284
|
+
}
|
|
9109
9285
|
console.log("");
|
|
9110
9286
|
console.log(` ${DIM2}\u250C${"\u2500".repeat(BOX_WIDTH)}\u2510${R3}`);
|
|
9111
9287
|
for (const inner of [
|
|
9112
9288
|
` ${GRN}\u25C8${R3} ${CRM2}Server live${R3} ${MID2}on http://localhost:3001/webhook${R3}`,
|
|
9113
9289
|
` ${AMB2}\u25C8${R3} ${MID2}Edit ${CRM2}src/store.ts${R3} ${MID2}to wire your database${R3}`,
|
|
9114
|
-
` ${MID2}Press Ctrl+C to stop the server and exit.${R3}
|
|
9290
|
+
` ${MID2}Press Ctrl+C to stop the server and exit.${R3}`,
|
|
9291
|
+
` ${MID2}Run ${CRM2}npm start${R3} ${MID2}here any time for the dashboard.${R3}`
|
|
9115
9292
|
]) {
|
|
9116
9293
|
console.log(boxRow(inner, BOX_WIDTH, DIM2, R3));
|
|
9117
9294
|
}
|