@suveren/gateway 0.18.2 → 0.19.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.
package/README.md CHANGED
@@ -142,7 +142,22 @@ For laptops managed by a company's IT: no admin rights, no Node.js and no access
142
142
 
143
143
  **The installer is not signed by us.** IT checks where it comes from (`sha256sum -c SHA256SUMS` and `gh attestation verify <file> -R suverenai/suveren-gateway`, which shows the exact commit it was built from), signs it, and distributes it, e.g. with Intune. IT can preset the Authority Server, simulation mode, proxy and certificate centrally; the employee cannot change them, and updates come from IT. Step by step: [Windows IT guide](docs/windows-it-guide.md) · settings: [Managed settings](docs/managed-settings.md).
144
144
 
145
- Open `http://localhost:3400` (Start menu: *Suveren Gateway*). The MCP server is at `http://localhost:3430`.
145
+ Open `http://localhost:3400` (Start menu: *Suveren Gateway*). The MCP server is at `http://localhost:3430`. Other ports or another data folder: `msiexec /i suveren-gateway.msi PORT=3500 MCP_PORT=3530 DATA_DIR="D:\Suveren"`, or the "Gateway settings" page of a double-click install — see the [IT guide](docs/windows-it-guide.md#port-and-data-folder-optional).
146
+
147
+ ### Ports and data folder
148
+
149
+ npm and the Windows installer default to port 3400 (app), 3430 (AI assistants) and the data folder `~/.suveren`. To change them for good — kept across restarts, upgrades and autostart:
150
+
151
+ ```bash
152
+ suveren-gateway stop # if you started it with --detach
153
+ suveren-gateway config set port 3500
154
+ suveren-gateway config set mcp-port 3530
155
+ suveren-gateway config set data-dir /Volumes/Data/suveren
156
+ suveren-gateway config get # shows each value and where it comes from
157
+ suveren-gateway config unset port # back to the default
158
+ ```
159
+
160
+ Changing the data folder **does not move** existing data — copy the old folder yourself first if you want to keep it. After changing `mcp-port`, update the MCP address in every AI assistant you connected. Environment variables (`SUVEREN_CP_PORT`, `SUVEREN_MCP_PORT`, `SUVEREN_DATA_DIR`) still win over saved values; IT policy wins over both ([Managed settings](docs/managed-settings.md)).
146
161
 
147
162
  ### Connecting an MCP client
148
163
 
@@ -12,7 +12,7 @@
12
12
 
13
13
  import { spawn, spawnSync } from 'node:child_process';
14
14
  import { createConnection } from 'node:net';
15
- import { existsSync, mkdirSync, openSync, readFileSync, writeFileSync, unlinkSync, statSync } from 'node:fs';
15
+ import { existsSync, mkdirSync, openSync, readFileSync, readdirSync, writeFileSync, unlinkSync, statSync } from 'node:fs';
16
16
  import { homedir, platform, userInfo } from 'node:os';
17
17
  import { dirname, join, resolve } from 'node:path';
18
18
  import { fileURLToPath, pathToFileURL } from 'node:url';
@@ -22,6 +22,7 @@ import { createInterface } from 'node:readline/promises';
22
22
  import { readPairing as readAsPairing, recordTlsPin, formatFingerprint, normalizeFingerprint } from '../lib/as-pairing.mjs';
23
23
  import { unsupportedNodeReason } from '../lib/node-version.mjs';
24
24
  import { readPolicy, isPolicyLocked } from '../lib/policy.mjs';
25
+ import { resolveInstallSettings, describeSource, installSettingsLocation, validatePort, validateDataDir, writeInstallSettings } from '../lib/install-settings.mjs';
25
26
 
26
27
  // Before any command: on an unsupported Node the connectors cannot run (see
27
28
  // lib/node-version.mjs), so refuse with the reason instead of starting.
@@ -35,11 +36,29 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
35
36
  const PKG_ROOT = resolve(__dirname, '..');
36
37
  const SERVER_ENTRY = join(PKG_ROOT, 'server.js');
37
38
 
38
- const DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join(homedir(), '.suveren');
39
+ // Ports + data folder: IT policy > env > saved install setting > default
40
+ // (lib/install-settings.mjs — the same resolver server.js uses, so the CLI
41
+ // and the running gateway always agree). An invalid value stops every
42
+ // command except `config` and `help`, which stay usable so the person can
43
+ // see and fix it.
44
+ const COMMAND = process.argv[2] ?? 'help';
45
+ const TOLERANT = ['config', 'help', '--help', '-h'].includes(COMMAND);
46
+ let INSTALL;
47
+ try {
48
+ INSTALL = resolveInstallSettings({ tolerant: TOLERANT });
49
+ } catch (err) {
50
+ console.error(err instanceof Error ? err.message : String(err));
51
+ console.error('Fix it with `suveren-gateway config set …` — see `suveren-gateway config help`.');
52
+ process.exit(1);
53
+ }
54
+ for (const warning of INSTALL.errors) console.error(`Warning: ${warning}`);
55
+
56
+ const DATA_DIR = INSTALL.dataDir;
39
57
  const PID_FILE = join(DATA_DIR, 'gateway.pid');
40
58
  const LOG_FILE = join(DATA_DIR, 'gateway.log');
41
59
 
42
- const SUVEREN_PORT = process.env.SUVEREN_CP_PORT ?? '3400';
60
+ const SUVEREN_PORT = String(INSTALL.cpPort);
61
+ const MCP_PORT = String(INSTALL.mcpPort);
43
62
 
44
63
  /** Version of THIS CLI (the binary on disk). Compared against the
45
64
  * running gateway's version inside `status` so users see a mismatch
@@ -267,11 +286,21 @@ async function start(args) {
267
286
  console.error(`another terminal, which leaves no PID file for this CLI to find.`);
268
287
  console.error(``);
269
288
  console.error(` Check what it is: suveren-gateway status`);
270
- console.error(` Or use another port: SUVEREN_CP_PORT=3410 suveren-gateway start`);
289
+ console.error(` Or use another port: suveren-gateway config set port 3410`);
271
290
  }
272
291
  process.exit(1);
273
292
  }
274
293
 
294
+ // The MCP port too: before this check, a taken MCP port let the gateway
295
+ // "start", then its MCP server died on EADDRINUSE and took the rest down
296
+ // with it — reported only in the log.
297
+ if (await isPortListening(MCP_PORT)) {
298
+ console.error(`Port ${MCP_PORT} (for AI assistants, MCP) is already in use.`);
299
+ console.error(``);
300
+ console.error(` Use another port: suveren-gateway config set mcp-port 3440`);
301
+ process.exit(1);
302
+ }
303
+
275
304
  ensureDataDir();
276
305
 
277
306
  // NOW save — a start we know is actually going to happen. Also override
@@ -543,15 +572,26 @@ function printConfigHelp() {
543
572
  console.log(`suveren-gateway config — read or save gateway settings
544
573
 
545
574
  Usage:
546
- suveren-gateway config get [as-url|ca-file|pin-tls|proxy] Print the resolved value(s)
575
+ suveren-gateway config get [as-url|ca-file|pin-tls|proxy|port|mcp-port|data-dir]
576
+ Print the resolved value(s)
547
577
  suveren-gateway config set as-url <url> Save the Authority Server URL
548
578
  suveren-gateway config set ca-file <path> Save a CA bundle for internal TLS
549
579
  suveren-gateway config set pin-tls on --expect-fingerprint <sha256-hex>
550
580
  Pin the Authority Server's TLS certificate
551
581
  suveren-gateway config set pin-tls off Stop enforcing the TLS certificate pin
552
582
  suveren-gateway config set proxy <url> Save an HTTP(S) proxy (http(s)://host:port)
553
-
554
- Saved in ${join(DATA_DIR, 'config.json')}.
583
+ suveren-gateway config set port <port> Port for the gateway app in the browser (default 3400)
584
+ suveren-gateway config set mcp-port <port> Port AI assistants connect to (default 3430)
585
+ suveren-gateway config set data-dir <folder> Folder for the vault, mandates and logs (default ~/.suveren)
586
+ suveren-gateway config unset port|mcp-port|data-dir Back to the default
587
+
588
+ Saved in ${join(DATA_DIR, 'config.json')} — except port, mcp-port and
589
+ data-dir, which live outside the data folder, in ${installSettingsLocation()}.
590
+ They are kept across restarts and autostart. Ports must be 1024–65535 and
591
+ differ from each other; the data folder must be a full path. Changing the
592
+ data folder does NOT move existing data. Precedence: IT policy > env var
593
+ (SUVEREN_CP_PORT / SUVEREN_MCP_PORT / SUVEREN_DATA_DIR) > saved > default.
594
+ Stop a gateway started with --detach before changing them.
555
595
  Precedence at start: IT policy > --as-url flag > env SUVEREN_AS_URL > saved
556
596
  as-url > default (${DEFAULT_AS_URL}).
557
597
 
@@ -631,6 +671,106 @@ function refuseIfLockedByPolicy(key, humanName, display) {
631
671
  process.exit(1);
632
672
  }
633
673
 
674
+ // ─── config: ports + data folder (lib/install-settings.mjs) ─────────────
675
+
676
+ /** `config` key → resolved-settings field, policy key and env var. */
677
+ const INSTALL_KEYS = {
678
+ port: { field: 'cpPort', policyKey: 'port', env: 'SUVEREN_CP_PORT', name: 'The gateway port' },
679
+ 'mcp-port': { field: 'mcpPort', policyKey: 'mcpPort', env: 'SUVEREN_MCP_PORT', name: 'The MCP port' },
680
+ 'data-dir': { field: 'dataDir', policyKey: 'dataDir', env: 'SUVEREN_DATA_DIR', name: 'The data folder' },
681
+ };
682
+
683
+ /**
684
+ * `config set port|mcp-port|data-dir <value>` and `config unset <key>`
685
+ * (`value` null). Saved outside the data folder — see install-settings.mjs.
686
+ */
687
+ async function setInstallSetting(key, value) {
688
+ const { field, policyKey, env, name } = INSTALL_KEYS[key];
689
+ refuseIfLockedByPolicy(policyKey, key);
690
+ const clearing = value === null;
691
+
692
+ let next;
693
+ if (!clearing) {
694
+ if (!value) {
695
+ console.error(`Usage: suveren-gateway config set ${key} <${key === 'data-dir' ? 'folder' : 'port'}>`);
696
+ process.exit(2);
697
+ }
698
+ const v = field === 'dataDir' ? validateDataDir(value) : validatePort(value);
699
+ if (!v.ok) {
700
+ console.error(`Invalid ${key}: ${v.error}`);
701
+ process.exit(1);
702
+ }
703
+ next = field === 'dataDir' ? v.path : v.port;
704
+ const other = field === 'cpPort' ? 'mcpPort' : field === 'mcpPort' ? 'cpPort' : null;
705
+ if (other && INSTALL[other] === next) {
706
+ console.error(`Invalid ${key}: ${next} is already the ${other === 'cpPort' ? 'gateway port' : 'MCP port'} — the two must differ.`);
707
+ process.exit(1);
708
+ }
709
+ }
710
+
711
+ // A gateway this CLI started with --detach keeps its PID file in the
712
+ // CURRENT data folder and listens on the CURRENT ports. Changing either
713
+ // underneath it leaves `stop`/`restart` unable to find it, and a second
714
+ // gateway fighting it for the port. Stop first. (A login-service gateway
715
+ // is fine: `restart` finds that one through the service manager.)
716
+ if (await isAlreadyRunning()) {
717
+ console.error(`The gateway is running (pid ${readPid()}). Stop it first, then change ${key}:`);
718
+ console.error(' suveren-gateway stop');
719
+ process.exit(1);
720
+ }
721
+
722
+ try {
723
+ writeInstallSettings({ [field]: clearing ? null : next });
724
+ } catch (err) {
725
+ console.error(err instanceof Error ? err.message : String(err));
726
+ process.exit(1);
727
+ }
728
+
729
+ const shown = clearing ? `back to the default` : `${next}`;
730
+ console.log(`Saved ${key}: ${shown} (in ${installSettingsLocation()})`);
731
+
732
+ if (process.env[env]) {
733
+ console.log(``);
734
+ console.log(` Note: ${env} is set in this shell (${process.env[env]}) and wins over the saved value.`);
735
+ }
736
+
737
+ if (field === 'mcpPort') {
738
+ const port = clearing ? 3430 : next;
739
+ console.log(``);
740
+ console.log(` AI assistants connect to the new address from the next start:`);
741
+ console.log(` http://localhost:${port}/mcp (SSE: http://localhost:${port}/sse)`);
742
+ console.log(` Update the MCP settings of every assistant you connected before.`);
743
+ }
744
+
745
+ if (field === 'dataDir') {
746
+ const newDir = clearing ? join(homedir(), '.suveren') : next;
747
+ const oldDir = INSTALL.dataDir;
748
+ let oldHasData = false;
749
+ try { oldHasData = resolve(newDir) !== resolve(oldDir) && readdirSync(oldDir).length > 0; } catch { /* no old folder */ }
750
+ if (oldHasData) {
751
+ console.log(``);
752
+ console.log(` Your existing data stays in ${oldDir} — nothing is moved or deleted.`);
753
+ console.log(` The gateway starts EMPTY in ${newDir}: you sign in and set up again.`);
754
+ console.log(` To keep your data instead, copy the folder there yourself before the next start.`);
755
+ }
756
+ // An autostart registered from a shell that had SUVEREN_DATA_DIR set
757
+ // carries it in the service definition, where it wins over this setting.
758
+ const unit = platform() === 'darwin' ? launchAgentPath() : platform() === 'linux' ? systemdUnitPath() : null;
759
+ try {
760
+ if (unit && existsSync(unit) && readFileSync(unit, 'utf8').includes('SUVEREN_DATA_DIR')) {
761
+ console.log(``);
762
+ console.log(` Your login service still sets SUVEREN_DATA_DIR itself (${unit}).`);
763
+ console.log(` Register it again from a shell without that variable: suveren-gateway service install`);
764
+ }
765
+ } catch { /* unreadable — nothing to warn about */ }
766
+ }
767
+
768
+ console.log(``);
769
+ console.log(serviceRunning()
770
+ ? ' Restart to apply it: suveren-gateway restart'
771
+ : ' It applies from the next start.');
772
+ }
773
+
634
774
  async function config(args) {
635
775
  const sub = args[0];
636
776
 
@@ -644,6 +784,14 @@ async function config(args) {
644
784
  console.log(`ca-file: ${(policyCaFile ?? saved.caFile) ?? '(not set)'}${policySuffix('caFile')}`);
645
785
  console.log(`pin-tls: ${resolvePinTls(DATA_DIR) ? 'on' : 'off'}${policySuffix('pinTls')}`);
646
786
  console.log(`proxy: ${(policyProxy ?? saved.proxyUrl) ?? '(not set)'}${policySuffix('proxy')}`);
787
+ console.log(`port: ${INSTALL.cpPort} (${describeSource(INSTALL.source.cpPort, 'cpPort')})`);
788
+ console.log(`mcp-port: ${INSTALL.mcpPort} (${describeSource(INSTALL.source.mcpPort, 'mcpPort')})`);
789
+ console.log(`data-dir: ${INSTALL.dataDir} (${describeSource(INSTALL.source.dataDir, 'dataDir')})`);
790
+ return;
791
+ }
792
+ const install = INSTALL_KEYS[key];
793
+ if (install) {
794
+ console.log(`${INSTALL[install.field]} (${describeSource(INSTALL.source[install.field], install.field)})`);
647
795
  return;
648
796
  }
649
797
  if (key === 'as-url') { console.log(`${resolveAsUrl(DATA_DIR)}${policySuffix('asUrl')}`); return; }
@@ -655,9 +803,18 @@ async function config(args) {
655
803
  process.exit(2);
656
804
  }
657
805
 
658
- if (sub === 'set') {
806
+ if (sub === 'set' || sub === 'unset') {
659
807
  const key = args[1];
660
808
  const value = args[2];
809
+ if (INSTALL_KEYS[key]) {
810
+ await setInstallSetting(key, sub === 'unset' ? null : value);
811
+ return;
812
+ }
813
+ if (sub === 'unset') {
814
+ console.error(`config unset works for port, mcp-port and data-dir only.\n`);
815
+ printConfigHelp();
816
+ process.exit(2);
817
+ }
661
818
  if (key === 'as-url') {
662
819
  refuseIfLockedByPolicy('asUrl', 'as-url');
663
820
  if (!value) {
@@ -1368,6 +1525,9 @@ function sleep(ms) {
1368
1525
  * Delegates entirely to the compiled `dist/mcp-server/report-verify-cli.mjs`
1369
1526
  * (built from apps/mcp-server's OWN tsup entry, next to dist/mcp-server/
1370
1527
  * http.mjs) — this script never reimplements the signature-checking logic.
1528
+ * It checks the signatures AND every value the page shows (RR7: the page
1529
+ * must re-draw from the bundle byte for byte; each drawn field is checked
1530
+ * against its signed source, recomputed, or listed as not checkable offline).
1371
1531
  * Exit codes are documented there (`--help`): 0 valid+key confirmed,
1372
1532
  * 1 invalid, 2 valid but key unconfirmed.
1373
1533
  */
@@ -1383,6 +1543,29 @@ async function verifyReport(args) {
1383
1543
  process.exitCode = await mod.runVerifyReportCli(args);
1384
1544
  }
1385
1545
 
1546
+ /**
1547
+ * `suveren-gateway open` — open the UI in the default browser, on whatever
1548
+ * port is configured. The Windows Start Menu entry and the installer's
1549
+ * Finish checkbox use this (via launcher.cmd) instead of a hard-coded port.
1550
+ */
1551
+ function openUi() {
1552
+ const url = `http://localhost:${SUVEREN_PORT}`;
1553
+ const os = platform();
1554
+ let child;
1555
+ if (os === 'win32') {
1556
+ // `start ""` — the empty title is required before a quoted target.
1557
+ // Verbatim, or Node escapes those quotes into something cmd misreads.
1558
+ child = spawn('cmd.exe', ['/d', '/s', '/c', `start "" "${url}"`], {
1559
+ detached: true, stdio: 'ignore', windowsHide: true, windowsVerbatimArguments: true,
1560
+ });
1561
+ } else {
1562
+ child = spawn(os === 'darwin' ? 'open' : 'xdg-open', [url], { detached: true, stdio: 'ignore' });
1563
+ }
1564
+ child.on('error', () => { console.log(`Open ${url} in your browser.`); });
1565
+ child.unref();
1566
+ console.log(`Opening ${url}`);
1567
+ }
1568
+
1386
1569
  function printHelp() {
1387
1570
  console.log(`suveren-gateway — Suveren gateway (Human Agency Protocol)
1388
1571
 
@@ -1396,13 +1579,19 @@ Usage:
1396
1579
  suveren-gateway stop Stop a detached gateway
1397
1580
  suveren-gateway restart Stop, then start --detach
1398
1581
  suveren-gateway status Show running state + health (incl. simulation mode)
1582
+ suveren-gateway open Open the gateway in your browser
1399
1583
  suveren-gateway logs [--tail] Print or tail ~/.suveren/gateway.log
1400
1584
  suveren-gateway service <cmd> Run as a login service that survives reboot
1401
1585
  (install | uninstall | status)
1402
- suveren-gateway config get [as-url|ca-file|pin-tls|proxy] Print the resolved value(s)
1586
+ suveren-gateway config get [as-url|ca-file|pin-tls|proxy|port|mcp-port|data-dir]
1587
+ Print the resolved value(s)
1403
1588
  suveren-gateway config set as-url <url> Save the Authority Server URL
1404
1589
  suveren-gateway config set ca-file <path> Save a CA bundle for internal TLS
1405
1590
  suveren-gateway config set proxy <url> Save an HTTP(S) proxy (see below)
1591
+ suveren-gateway config set port <port> Save the gateway port (default 3400)
1592
+ suveren-gateway config set mcp-port <port> Save the port AI assistants connect to (default 3430)
1593
+ suveren-gateway config set data-dir <folder>
1594
+ Save the data folder (default ~/.suveren)
1406
1595
  suveren-gateway config set pin-tls on --expect-fingerprint <hex>
1407
1596
  Pin the Authority Server's TLS certificate
1408
1597
  (see \`suveren-gateway config help\` for the fingerprint check)
@@ -1410,14 +1599,15 @@ Usage:
1410
1599
  suveren-gateway simulation on|off|status Block (or unblock) every real system
1411
1600
  (see \`suveren-gateway simulation help\`)
1412
1601
  suveren-gateway verify-report <file> [--key <hex>] [--online]
1413
- Verify an "Export with proof" report file offline
1602
+ Verify an "Export with proof" report file offline —
1603
+ every signature and every value the page shows
1414
1604
  (see \`suveren-gateway verify-report --help\`)
1415
1605
  suveren-gateway help Print this help
1416
1606
 
1417
1607
  Environment:
1418
- SUVEREN_CP_PORT UI + API port (default 3400)
1419
- SUVEREN_MCP_PORT MCP server port (default 3430)
1420
- SUVEREN_DATA_DIR Data directory (default ~/.suveren)
1608
+ SUVEREN_CP_PORT UI + API port (default 3400) — overrides a saved \`config set port\`
1609
+ SUVEREN_MCP_PORT MCP server port (default 3430) — overrides a saved \`config set mcp-port\`
1610
+ SUVEREN_DATA_DIR Data directory (default ~/.suveren) — overrides a saved \`config set data-dir\`
1421
1611
  SUVEREN_AS_URL Authority Server URL — overrides the saved as-url
1422
1612
  SUVEREN_SIMULATION 1 to force simulation mode for this run — overrides the saved setting
1423
1613
  HTTP_PROXY / HTTPS_PROXY / NO_PROXY (upper or lower case)
@@ -1446,6 +1636,7 @@ async function main() {
1446
1636
  case 'stop': await stop(); break;
1447
1637
  case 'status': await status(); break;
1448
1638
  case 'restart': await restart(); break;
1639
+ case 'open': openUi(); break;
1449
1640
  case 'logs': await logs(argv.slice(1)); break;
1450
1641
  case 'service': await service(argv.slice(1)); break;
1451
1642
  case 'config': await config(argv.slice(1)); break;
@@ -2786,6 +2786,21 @@ function requireAllowedHost(req, res, next) {
2786
2786
  next();
2787
2787
  }
2788
2788
 
2789
+ // src/lib/mcp-public-port.ts
2790
+ function mcpPublicPort(env = process.env, internalUrl) {
2791
+ for (const raw of [env.SUVEREN_MCP_PUBLIC_PORT, env.SUVEREN_MCP_PORT]) {
2792
+ const n = Number(raw);
2793
+ if (raw && Number.isInteger(n) && n > 0 && n <= 65535) return n;
2794
+ }
2795
+ try {
2796
+ const url = new URL(internalUrl ?? env.SUVEREN_MCP_INTERNAL_URL ?? "");
2797
+ const n = Number(url.port);
2798
+ if (Number.isInteger(n) && n > 0) return n;
2799
+ } catch {
2800
+ }
2801
+ return null;
2802
+ }
2803
+
2789
2804
  // src/lib/read-policy-defaults.ts
2790
2805
  async function backfillReadPolicyDefaults(data) {
2791
2806
  const defaults = /* @__PURE__ */ new Map();
@@ -3976,6 +3991,10 @@ async function planMandate(req, deps) {
3976
3991
  if (bt?.kind === "enum" && Array.isArray(bt.values) && !bt.values.includes(String(value))) {
3977
3992
  errors.push(`Field "${field}" must be one of ${bt.values.join(", ")}.`);
3978
3993
  }
3994
+ const maximum = profile.boundsSchema?.fields[field]?.maximum;
3995
+ if (typeof maximum === "number" && typeof value === "number" && value > maximum) {
3996
+ errors.push(`Field "${field}" may be at most ${maximum}.`);
3997
+ }
3979
3998
  }
3980
3999
  const context = scalarRecord("scope", req.scope);
3981
4000
  errors.push(...validateContextParams(context, profile).errors);
@@ -4997,6 +5016,9 @@ app.get("/health", async (req, res) => {
4997
5016
  // purely so Settings can render "set by your IT" instead of controls
4998
5017
  // that would silently do nothing.
4999
5018
  policyLocked: POLICY_LOCKED,
5019
+ // Where AI assistants connect (the UI's setup snippet) — see
5020
+ // lib/mcp-public-port.ts for why the UI must not guess it.
5021
+ mcpPort: mcpPublicPort(process.env, MCP_BASE),
5000
5022
  session: buildSessionHealth(vault),
5001
5023
  security: {
5002
5024
  note: "Gateway secures tool execution. Agent host isolation is the user's responsibility."