@oneaddress/setup 2.0.0 → 2.0.1

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.
Files changed (2) hide show
  1. package/dist/index.js +134 -22
  2. 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.0.0" : "?";
859
+ var WIZARD_VERSION = true ? "2.0.1" : "?";
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
- const { createInterface } = await import('node:readline/promises');
1833
- const rl = createInterface({ input: process.stdin, output: process.stdout });
1834
- try {
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
- } finally {
1842
- rl.close();
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
- console.log(
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
- console.log(\`[store] migrated: added column \${table}.\${column}\`);
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
- console.warn(\`[store] customers.json not loaded (\${msg}) \u2014 using the built-in demo roster. Edit customers.json to set your customers.\`);
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
- console.log(\`[store] roster ready (\${ROSTER.length} customers)\`);
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
- console.log(\`[store] verifyAddress \u2192 not_found (\${customer.accountNumber || customer.name || '(none)'})\`);
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
- console.log(\`[store] verifyAddress \u2192 \${result} (\${row.account_number})\`);
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
- console.log(\`[store] saved address for \${acct}\${customer.loaRef ? \` (loa_ref \${customer.loaRef})\` : ''}\`);
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.0.0" : "dev";
7993
+ var PKG_VERSION = true ? "2.0.1" : "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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oneaddress/setup",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "Interactive setup wizard for OneAddress partner webhook integrations",
5
5
  "main": "dist/index.js",
6
6
  "bin": {