@veilio-inc/cli 0.1.0 → 0.2.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/README.md CHANGED
@@ -15,9 +15,11 @@ npm install -g @veilio-inc/cli # or run it without installing:
15
15
  npx @veilio-inc/cli scrub src/billing.ts
16
16
  ```
17
17
 
18
- Requires Node 24 or newer. No account, no API key, and no network call on any of
19
- the commands below — `packages/cli/tests/purity.test.ts` traps the network
20
- globals and fails if one is ever introduced.
18
+ Requires Node 24 or newer. No account, no API key, and no network call on
19
+ `scrub`, `restore`, `scan` or `map` — `packages/cli/tests/purity.test.ts` and
20
+ `offline.test.ts` walk the import graph and trap the network globals to keep it
21
+ that way. Signing in is entirely optional and adds nothing to those commands'
22
+ behavior; it only unlocks the four commands below that name Cloud explicitly.
21
23
 
22
24
  ## Commands
23
25
 
@@ -28,6 +30,37 @@ globals and fails if one is ever introduced.
28
30
  | `scan [files...]` | Detect credentials only. Never rewrites. Exits 1 on findings. |
29
31
  | `map` | Show the symbol map (`--clear` to wipe it). |
30
32
 
33
+ ## Cloud sync (Individual plan and above)
34
+
35
+ > **None of this is in `0.1.0`, which is what npm currently has.** These
36
+ > commands are merged here and ship with the next release; until then they run
37
+ > from a clone — `npm run build:packages`, then
38
+ > `node packages/cli/dist/index.js --help`.
39
+
40
+ ```bash
41
+ veilio login # prompts for email/password; --instance for self-hosted
42
+ veilio whoami # who you're signed in as, no request made
43
+ veilio maps list # ids, scope and symbol counts for everything in Cloud
44
+ veilio maps pull <id> # decrypt locally, write second — never a partial write
45
+ veilio maps push <name> # upload the local map under a new Cloud name
46
+ veilio logout # revokes server-side, then removes the local credential
47
+ ```
48
+
49
+ The session token lives in a single file under your home directory
50
+ (`~/.veilio/credential.json` on Linux/macOS, `%USERPROFILE%\.veilio\credential.json`
51
+ on Windows), created at mode `0600` and never written until the server has
52
+ accepted the sign-in. `0600` is the floor, not the final answer — the OS
53
+ keychain is a stronger option and a planned follow-up. A personal map is
54
+ zero-knowledge: pulling or pushing one prompts for the vault passphrase and
55
+ derives the key here, on your machine — the passphrase and the key never
56
+ leave it. A team map arrives already open, because the server can read those
57
+ (stated in the privacy policy) and there is nothing local left to decrypt.
58
+
59
+ Every one of these six is a client of `GET /api/maps`, `GET /api/maps/:id` and
60
+ `POST /api/maps` — the exact same routes the browser uses, behind the exact
61
+ same entitlement check. There is no CLI-shaped API and nothing here can reach
62
+ a plan feature the web app couldn't.
63
+
31
64
  ## Options
32
65
 
33
66
  ```
package/dist/args.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type LanguageOption } from '@veilio-inc/engine';
2
2
  import type { SecretPolicy } from '@veilio-inc/engine';
3
- export type Command = 'scrub' | 'restore' | 'scan' | 'map' | 'help' | 'version';
3
+ export type Command = 'scrub' | 'restore' | 'scan' | 'map' | 'login' | 'logout' | 'whoami' | 'maps' | 'help' | 'version';
4
4
  export interface ParsedArgs {
5
5
  command: Command;
6
6
  /** Positional file paths. Empty means read stdin (or, for `scan`, error). */
@@ -23,8 +23,32 @@ export interface ParsedArgs {
23
23
  keepDocs: boolean;
24
24
  /** Allow a map write that would drop entries already on disk. */
25
25
  force: boolean;
26
+ /**
27
+ * `maps` subcommand: list, pull or push. Null when `maps` was given alone.
28
+ *
29
+ * A subcommand rather than three top-level commands, because `map` (singular,
30
+ * the local store) already exists and `veilio push` next to `veilio map`
31
+ * reads like they are the same thing. `maps <verb>` keeps the cloud surface
32
+ * visibly separate from the offline one.
33
+ */
34
+ mapsAction: 'list' | 'pull' | 'push' | null;
35
+ /**
36
+ * Base URL of the Veilio instance to sign in to. Null means the public Cloud.
37
+ *
38
+ * Only meaningful to `login`: once signed in, the instance is read back from
39
+ * the stored credential, so a later command cannot be pointed at a different
40
+ * host while still holding the first one's token.
41
+ */
42
+ instance: string | null;
26
43
  }
27
44
  export declare class UsageError extends Error {
28
45
  }
46
+ /**
47
+ * The commands that reach the network. Named here so the split is one list
48
+ * rather than a condition repeated at each call site — and so that adding a
49
+ * command without deciding which side of the line it falls on is a compile
50
+ * error rather than an accident.
51
+ */
52
+ export declare const CLOUD_COMMANDS: Set<Command>;
29
53
  export declare function parseArgs(argv: readonly string[]): ParsedArgs;
30
54
  //# sourceMappingURL=args.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"args.d.ts","sourceRoot":"","sources":["../src/args.ts"],"names":[],"mappings":"AAIA,OAAO,EAA4B,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAClF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAEtD,MAAM,MAAM,OAAO,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,SAAS,CAAA;AAE/E,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAA;IAChB,6EAA6E;IAC7E,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,QAAQ,EAAE,cAAc,CAAA;IACxB,OAAO,EAAE,YAAY,CAAA;IACrB,qEAAqE;IACrE,QAAQ,EAAE,OAAO,CAAA;IACjB,uCAAuC;IACvC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,sCAAsC;IACtC,IAAI,EAAE,OAAO,CAAA;IACb,qDAAqD;IACrD,KAAK,EAAE,OAAO,CAAA;IACd,qCAAqC;IACrC,KAAK,EAAE,OAAO,CAAA;IACd,6DAA6D;IAC7D,MAAM,EAAE,OAAO,CAAA;IACf,wCAAwC;IACxC,QAAQ,EAAE,OAAO,CAAA;IACjB,iEAAiE;IACjE,KAAK,EAAE,OAAO,CAAA;CACf;AAED,qBAAa,UAAW,SAAQ,KAAK;CAAG;AAaxC,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,UAAU,CAgG7D"}
1
+ {"version":3,"file":"args.d.ts","sourceRoot":"","sources":["../src/args.ts"],"names":[],"mappings":"AAIA,OAAO,EAA4B,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAClF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAEtD,MAAM,MAAM,OAAO,GACjB,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;AAEpG,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAA;IAChB,6EAA6E;IAC7E,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,QAAQ,EAAE,cAAc,CAAA;IACxB,OAAO,EAAE,YAAY,CAAA;IACrB,qEAAqE;IACrE,QAAQ,EAAE,OAAO,CAAA;IACjB,uCAAuC;IACvC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,sCAAsC;IACtC,IAAI,EAAE,OAAO,CAAA;IACb,qDAAqD;IACrD,KAAK,EAAE,OAAO,CAAA;IACd,qCAAqC;IACrC,KAAK,EAAE,OAAO,CAAA;IACd,6DAA6D;IAC7D,MAAM,EAAE,OAAO,CAAA;IACf,wCAAwC;IACxC,QAAQ,EAAE,OAAO,CAAA;IACjB,iEAAiE;IACjE,KAAK,EAAE,OAAO,CAAA;IACd;;;;;;;OAOG;IACH,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IAE3C;;;;;;OAMG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB;AAED,qBAAa,UAAW,SAAQ,KAAK;CAAG;AAexC;;;;;GAKG;AACH,eAAO,MAAM,cAAc,cAA0D,CAAA;AAWrF,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,UAAU,CAkI7D"}
package/dist/args.js CHANGED
@@ -4,7 +4,25 @@
4
4
  import { LANGUAGES } from '@veilio-inc/engine';
5
5
  export class UsageError extends Error {
6
6
  }
7
- const COMMANDS = new Set(['scrub', 'restore', 'scan', 'map', 'help', 'version']);
7
+ const COMMANDS = new Set([
8
+ 'scrub',
9
+ 'restore',
10
+ 'scan',
11
+ 'map',
12
+ 'login',
13
+ 'logout',
14
+ 'whoami',
15
+ 'maps',
16
+ 'help',
17
+ 'version',
18
+ ]);
19
+ /**
20
+ * The commands that reach the network. Named here so the split is one list
21
+ * rather than a condition repeated at each call site — and so that adding a
22
+ * command without deciding which side of the line it falls on is a compile
23
+ * error rather than an accident.
24
+ */
25
+ export const CLOUD_COMMANDS = new Set(['login', 'logout', 'whoami', 'maps']);
8
26
  const SECRET_POLICIES = new Set(['redact', 'warn', 'off']);
9
27
  const LANGUAGE_VALUES = new Set([...LANGUAGES, 'auto']);
10
28
  function requireValue(flag, value) {
@@ -27,6 +45,8 @@ export function parseArgs(argv) {
27
45
  strict: false,
28
46
  keepDocs: false,
29
47
  force: false,
48
+ instance: null,
49
+ mapsAction: null,
30
50
  };
31
51
  if (argv.length === 0)
32
52
  return parsed;
@@ -38,6 +58,18 @@ export function parseArgs(argv) {
38
58
  }
39
59
  parsed.command = first;
40
60
  i = 1;
61
+ // `maps` takes a verb. Read here rather than as a positional later, so that
62
+ // `veilio maps pull abc` does not treat `pull` as a file to anonymize.
63
+ if (parsed.command === 'maps') {
64
+ const verb = argv[1];
65
+ if (verb !== undefined && !verb.startsWith('-')) {
66
+ if (verb !== 'list' && verb !== 'pull' && verb !== 'push') {
67
+ throw new UsageError(`unknown maps action "${verb}" — expected list, pull or push`);
68
+ }
69
+ parsed.mapsAction = verb;
70
+ i = 2;
71
+ }
72
+ }
41
73
  }
42
74
  for (; i < argv.length; i++) {
43
75
  const arg = argv[i];
@@ -96,6 +128,24 @@ export function parseArgs(argv) {
96
128
  case '--force':
97
129
  parsed.force = true;
98
130
  break;
131
+ case '--instance': {
132
+ const value = requireValue(arg, argv[++i]);
133
+ // Parsed here rather than at use, so a typo is a usage error before any
134
+ // password is typed rather than a confusing failure after.
135
+ let url;
136
+ try {
137
+ url = new URL(value);
138
+ }
139
+ catch {
140
+ throw new UsageError(`--instance must be a URL (got "${value}")`);
141
+ }
142
+ if (url.protocol !== 'https:' && url.hostname !== 'localhost') {
143
+ throw new UsageError(`--instance must be https (got "${value}") — a password and a session token over ` +
144
+ 'plain http are readable by anything on the path. localhost is allowed for development.');
145
+ }
146
+ parsed.instance = value;
147
+ break;
148
+ }
99
149
  default:
100
150
  if (arg.startsWith('-') && arg !== '-') {
101
151
  throw new UsageError(`unknown flag "${arg}"`);
package/dist/args.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"args.js","sourceRoot":"","sources":["../src/args.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"args.js","sourceRoot":"","sources":["../src/args.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,70 @@
1
+ import { type Io } from './commands.js';
2
+ /**
3
+ * Sign in, and store the credential only if the account can actually use it.
4
+ *
5
+ * Two requests, and the second one is the interesting one.
6
+ *
7
+ * `POST /api/auth/login` answers "is this the right password". It does NOT
8
+ * answer "does this plan include the terminal" — a free account signs in
9
+ * perfectly well, because signing in is how you reach the billing page. So a
10
+ * CLI that stopped after the first request would store a credential for an
11
+ * account that gets refused by every command that follows, and would report
12
+ * that refusal as though the sign-in had worked.
13
+ *
14
+ * The obvious shortcut is to read `user.plan` out of the login response and
15
+ * decide locally. That is exactly what FR-009 forbids: entitlement is the
16
+ * server's answer on each request, never the client's reading of a plan name.
17
+ * A copy of the rule shipped to other people's laptops is a second definition of
18
+ * it, and the one that goes stale first.
19
+ *
20
+ * So the second request asks the server a real question and lets it refuse. Only
21
+ * then is anything written — validate, then persist (Constitution IV). A
22
+ * credential written first and validated after leaves a file behind for an
23
+ * attempt that failed, and everything looks signed in until the first command
24
+ * that isn't.
25
+ */
26
+ export declare function runLogin(instance: string | null, io: Io): Promise<number>;
27
+ /**
28
+ * Sign out: revoke server-side, THEN remove the file.
29
+ *
30
+ * The order is the control. Removing the file first makes `logout` always look
31
+ * like it worked, while leaving a live session on the server that the user has
32
+ * been told is gone — and nothing anywhere reports that. So a failed revocation
33
+ * keeps the credential and says so, which is the loud failure rather than the
34
+ * quiet one (Constitution V).
35
+ */
36
+ export declare function runLogout(io: Io): Promise<number>;
37
+ /**
38
+ * Who is signed in, answered from disk.
39
+ *
40
+ * Makes no request, on purpose. Its job is "am I signed in", not "is Cloud up" —
41
+ * an answer that needs the network is useless at exactly the moment somebody is
42
+ * trying to work out why the network isn't working.
43
+ */
44
+ export declare function runWhoami(io: Io): number;
45
+ /**
46
+ * List what the account holds.
47
+ *
48
+ * Prints the id, because `pull` takes one. A name would be friendlier and is
49
+ * wrong: names are not unique, and picking "the first one that matches" is how
50
+ * somebody pulls a different map over the one they were using.
51
+ */
52
+ export declare function runMapsList(io: Io): Promise<number>;
53
+ /**
54
+ * Pull a map into the local store: decrypt FIRST, write second.
55
+ *
56
+ * The ordering is the control (Constitution IV). Writing before decrypting means
57
+ * a wrong passphrase leaves a half-built store behind — and the store is the one
58
+ * thing that can restore text already anonymized, so a partial write there is
59
+ * not an inconvenience, it is unrecoverable text.
60
+ *
61
+ * A personal map is opened HERE. The server holds the salt and a verifier and
62
+ * nothing else; the passphrase never leaves this machine, and there is no code
63
+ * path in this package that could send it.
64
+ */
65
+ export declare function runMapsPull(id: string | null, mapPath: string, io: Io, options?: {
66
+ force?: boolean;
67
+ }): Promise<number>;
68
+ /** Upload the local map. Same endpoint, same quotas, same validation. */
69
+ export declare function runMapsPush(name: string | null, mapPath: string, io: Io): Promise<number>;
70
+ //# sourceMappingURL=cloud-commands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloud-commands.d.ts","sourceRoot":"","sources":["../src/cloud-commands.ts"],"names":[],"mappings":"AA+BA,OAAO,EAAuB,KAAK,EAAE,EAAE,MAAM,eAAe,CAAA;AAW5D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA+C/E;AAED;;;;;;;;GAQG;AACH,wBAAsB,SAAS,CAAC,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAgDvD;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,EAAE,EAAE,EAAE,GAAG,MAAM,CAQxC;AAyDD;;;;;;GAMG;AACH,wBAAsB,WAAW,CAAC,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAqBzD;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,WAAW,CAC/B,EAAE,EAAE,MAAM,GAAG,IAAI,EACjB,OAAO,EAAE,MAAM,EACf,EAAE,EAAE,EAAE,EACN,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAChC,OAAO,CAAC,MAAM,CAAC,CAgGjB;AAED,yEAAyE;AACzE,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAgE/F"}
@@ -0,0 +1,394 @@
1
+ // The commands that reach the network: login, logout, whoami.
2
+ //
3
+ // Deliberately NOT in commands.ts. That module is the local command path, and
4
+ // `offline.test.ts` walks its import graph and fails if it can reach `cloud.ts`
5
+ // — which is the structural half of the promise that `veilio scrub` on a laptop
6
+ // with no account touches nothing (FR-002). Putting these here is what keeps
7
+ // that walk meaningful instead of something to be deleted the moment it becomes
8
+ // inconvenient.
9
+ import { CloudError, createMap, getMap, getVault, listMaps, request, } from './cloud.js';
10
+ import { checkVaultVerifier, decryptMapFromVault, deriveVaultKey, encryptMapForVault, fromBase64, parseVaultEnvelope, } from '@veilio-inc/engine';
11
+ import { loadMap, loadRemote, saveMap } from './store.js';
12
+ import { readCredential, removeCredential, writeCredential } from './credential.js';
13
+ import { EXIT_ERROR, EXIT_OK } from './commands.js';
14
+ import { DEFAULT_INSTANCE } from './cloud.js';
15
+ /** The endpoint a sign-in is checked against. See `runLogin`. */
16
+ const ENTITLED_PROBE = '/api/maps';
17
+ /**
18
+ * Sign in, and store the credential only if the account can actually use it.
19
+ *
20
+ * Two requests, and the second one is the interesting one.
21
+ *
22
+ * `POST /api/auth/login` answers "is this the right password". It does NOT
23
+ * answer "does this plan include the terminal" — a free account signs in
24
+ * perfectly well, because signing in is how you reach the billing page. So a
25
+ * CLI that stopped after the first request would store a credential for an
26
+ * account that gets refused by every command that follows, and would report
27
+ * that refusal as though the sign-in had worked.
28
+ *
29
+ * The obvious shortcut is to read `user.plan` out of the login response and
30
+ * decide locally. That is exactly what FR-009 forbids: entitlement is the
31
+ * server's answer on each request, never the client's reading of a plan name.
32
+ * A copy of the rule shipped to other people's laptops is a second definition of
33
+ * it, and the one that goes stale first.
34
+ *
35
+ * So the second request asks the server a real question and lets it refuse. Only
36
+ * then is anything written — validate, then persist (Constitution IV). A
37
+ * credential written first and validated after leaves a file behind for an
38
+ * attempt that failed, and everything looks signed in until the first command
39
+ * that isn't.
40
+ */
41
+ export async function runLogin(instance, io) {
42
+ const base = instance ?? DEFAULT_INSTANCE;
43
+ const account = (await requirePrompt(io, `Email for ${base}: `)).trim();
44
+ if (account === '') {
45
+ io.stderr('veilio: no email given\n');
46
+ return EXIT_ERROR;
47
+ }
48
+ const password = await requirePassword(io, 'Password: ');
49
+ if (password === '') {
50
+ io.stderr('veilio: no password given\n');
51
+ return EXIT_ERROR;
52
+ }
53
+ let session;
54
+ try {
55
+ session = await request('/api/auth/login', {
56
+ instance: base,
57
+ method: 'POST',
58
+ body: { email: account, password },
59
+ });
60
+ }
61
+ catch (err) {
62
+ return reportLoginFailure(err, io);
63
+ }
64
+ if (typeof session.token !== 'string' || session.token === '') {
65
+ // A 200 with no token is not a sign-in. Writing an empty credential here
66
+ // would produce a "signed in" state that fails on every use.
67
+ io.stderr(`veilio: ${base} accepted the sign-in but returned no session\n`);
68
+ return EXIT_ERROR;
69
+ }
70
+ const credential = {
71
+ token: session.token,
72
+ account: session.user?.email ?? account,
73
+ instance: base,
74
+ };
75
+ try {
76
+ await request(ENTITLED_PROBE, { credential });
77
+ }
78
+ catch (err) {
79
+ return reportLoginFailure(err, io);
80
+ }
81
+ writeCredential(credential, io.home);
82
+ io.stdout(`Signed in to ${base} as ${credential.account}\n`);
83
+ return EXIT_OK;
84
+ }
85
+ /**
86
+ * Sign out: revoke server-side, THEN remove the file.
87
+ *
88
+ * The order is the control. Removing the file first makes `logout` always look
89
+ * like it worked, while leaving a live session on the server that the user has
90
+ * been told is gone — and nothing anywhere reports that. So a failed revocation
91
+ * keeps the credential and says so, which is the loud failure rather than the
92
+ * quiet one (Constitution V).
93
+ */
94
+ export async function runLogout(io) {
95
+ const credential = readCredential(io.home);
96
+ if (!credential) {
97
+ io.stdout('Not signed in.\n');
98
+ return EXIT_OK;
99
+ }
100
+ try {
101
+ await request('/api/auth/logout', { credential, method: 'POST', body: {} });
102
+ }
103
+ catch (err) {
104
+ if (err instanceof CloudError && err.kind === 'unauthenticated') {
105
+ // Already revoked, or expired. The server-side state is what logout
106
+ // wanted, so finishing the job locally is correct rather than an error.
107
+ try {
108
+ removeCredential(io.home);
109
+ }
110
+ catch (removeErr) {
111
+ io.stderr(`veilio: that session was already gone, but the credential file could not be removed ` +
112
+ `(${removeErr instanceof Error ? removeErr.message : String(removeErr)}).\n`);
113
+ return EXIT_ERROR;
114
+ }
115
+ io.stdout('That session was already revoked or expired. Signed out locally.\n');
116
+ return EXIT_OK;
117
+ }
118
+ const detail = err instanceof Error ? err.message : String(err);
119
+ io.stderr(`veilio: could not revoke the session (${detail}).\n` +
120
+ 'You are still signed in — the credential has been kept so you can try again. ' +
121
+ 'To end the session now, sign out from the web app.\n');
122
+ return EXIT_ERROR;
123
+ }
124
+ try {
125
+ removeCredential(io.home);
126
+ }
127
+ catch (err) {
128
+ // The session IS revoked — that part worked. What failed is removing the
129
+ // local file, and saying "signed out" while a dead token sits on disk is a
130
+ // smaller lie than the reverse but still a lie.
131
+ io.stderr(`veilio: the session was revoked, but the credential file could not be removed ` +
132
+ `(${err instanceof Error ? err.message : String(err)}). Delete it by hand.\n`);
133
+ return EXIT_ERROR;
134
+ }
135
+ io.stdout(`Signed out of ${credential.instance}.\n`);
136
+ return EXIT_OK;
137
+ }
138
+ /**
139
+ * Who is signed in, answered from disk.
140
+ *
141
+ * Makes no request, on purpose. Its job is "am I signed in", not "is Cloud up" —
142
+ * an answer that needs the network is useless at exactly the moment somebody is
143
+ * trying to work out why the network isn't working.
144
+ */
145
+ export function runWhoami(io) {
146
+ const credential = readCredential(io.home);
147
+ if (!credential) {
148
+ io.stdout('Not signed in. Run `veilio login` to connect a Veilio Cloud account.\n');
149
+ return EXIT_OK;
150
+ }
151
+ io.stdout(`${credential.account} on ${credential.instance}\n`);
152
+ return EXIT_OK;
153
+ }
154
+ /**
155
+ * Say which kind of refusal this was.
156
+ *
157
+ * FR-007: "wrong password" and "not on your plan" have different remedies and
158
+ * must never print the same thing. Sending someone to re-type a password that
159
+ * was correct is worse than saying nothing.
160
+ */
161
+ function reportLoginFailure(err, io) {
162
+ if (err instanceof CloudError) {
163
+ switch (err.kind) {
164
+ case 'unauthenticated':
165
+ io.stderr('veilio: that email and password were not accepted.\n');
166
+ return EXIT_ERROR;
167
+ case 'unentitled':
168
+ io.stderr(`veilio: signed in, but this account’s plan does not include terminal access — ${err.message}\n` +
169
+ 'Your password is fine. Change the plan in the web app to use the CLI with Cloud.\n');
170
+ return EXIT_ERROR;
171
+ case 'suspended':
172
+ // A different refusal with a different remedy again, and the one that
173
+ // must not read as a billing problem: this account is under review, and
174
+ // buying a bigger plan changes nothing.
175
+ io.stderr(`veilio: ${err.message}\n` +
176
+ 'Export and deletion still work in the web app. Nothing else will until the review ends.\n');
177
+ return EXIT_ERROR;
178
+ case 'forbidden':
179
+ io.stderr(`veilio: ${err.message}\n`);
180
+ return EXIT_ERROR;
181
+ case 'unreachable':
182
+ io.stderr(`veilio: ${err.message}\n`);
183
+ return EXIT_ERROR;
184
+ case 'server':
185
+ io.stderr(`veilio: the instance reported an error — ${err.message}\n`);
186
+ return EXIT_ERROR;
187
+ }
188
+ }
189
+ io.stderr(`veilio: ${err instanceof Error ? err.message : String(err)}\n`);
190
+ return EXIT_ERROR;
191
+ }
192
+ async function requirePrompt(io, label) {
193
+ if (!io.prompt)
194
+ throw new Error('this build cannot read input interactively');
195
+ return io.prompt(label);
196
+ }
197
+ async function requirePassword(io, label) {
198
+ if (!io.password)
199
+ throw new Error('this build cannot read a password interactively');
200
+ return io.password(label);
201
+ }
202
+ // ─── maps: list, pull, push ──────────────────────────────────────────────────
203
+ /**
204
+ * List what the account holds.
205
+ *
206
+ * Prints the id, because `pull` takes one. A name would be friendlier and is
207
+ * wrong: names are not unique, and picking "the first one that matches" is how
208
+ * somebody pulls a different map over the one they were using.
209
+ */
210
+ export async function runMapsList(io) {
211
+ const credential = readCredential(io.home);
212
+ if (!credential)
213
+ return notSignedIn(io);
214
+ let list;
215
+ try {
216
+ list = await listMaps(credential);
217
+ }
218
+ catch (err) {
219
+ return reportCloudFailure(err, io);
220
+ }
221
+ const all = [...list.personalMaps, ...list.teamMaps];
222
+ if (all.length === 0) {
223
+ io.stdout('No maps in Cloud yet. `veilio maps push <name>` uploads the local one.\n');
224
+ return EXIT_OK;
225
+ }
226
+ for (const map of all) {
227
+ const count = map.identifier_count === null ? '?' : String(map.identifier_count);
228
+ io.stdout(`${map.id} ${map.scope.padEnd(8)} ${count.padStart(5)} symbols ${map.name}\n`);
229
+ }
230
+ return EXIT_OK;
231
+ }
232
+ /**
233
+ * Pull a map into the local store: decrypt FIRST, write second.
234
+ *
235
+ * The ordering is the control (Constitution IV). Writing before decrypting means
236
+ * a wrong passphrase leaves a half-built store behind — and the store is the one
237
+ * thing that can restore text already anonymized, so a partial write there is
238
+ * not an inconvenience, it is unrecoverable text.
239
+ *
240
+ * A personal map is opened HERE. The server holds the salt and a verifier and
241
+ * nothing else; the passphrase never leaves this machine, and there is no code
242
+ * path in this package that could send it.
243
+ */
244
+ export async function runMapsPull(id, mapPath, io, options = {}) {
245
+ const credential = readCredential(io.home);
246
+ if (!credential)
247
+ return notSignedIn(io);
248
+ if (!id) {
249
+ io.stderr('veilio: which map? Run `veilio maps list` for the ids.\n');
250
+ return EXIT_ERROR;
251
+ }
252
+ let remote;
253
+ try {
254
+ remote = await getMap(credential, id);
255
+ }
256
+ catch (err) {
257
+ return reportCloudFailure(err, io);
258
+ }
259
+ // A team map arrives already open — the server can read those, and says so in
260
+ // the privacy policy. Only a client envelope is ours to decrypt.
261
+ let map;
262
+ if (typeof remote.map_data !== 'string') {
263
+ map = remote.map_data;
264
+ }
265
+ else {
266
+ let vault;
267
+ try {
268
+ vault = await getVault(credential);
269
+ }
270
+ catch (err) {
271
+ return reportCloudFailure(err, io);
272
+ }
273
+ if (!vault.initialized) {
274
+ // Stated plainly rather than surfaced as a decryption failure. There is
275
+ // nothing to try a passphrase against, and inviting one would be asking
276
+ // somebody to guess at a lock that does not exist.
277
+ io.stderr('veilio: this account has no vault, so there is no key for that map. ' +
278
+ 'Create one in the web app first — it is the passphrase your maps are encrypted under.\n');
279
+ return EXIT_ERROR;
280
+ }
281
+ const passphrase = io.password ? await io.password('Vault passphrase: ') : '';
282
+ if (passphrase === '') {
283
+ io.stderr('veilio: no passphrase given\n');
284
+ return EXIT_ERROR;
285
+ }
286
+ const key = await deriveVaultKey(passphrase, fromBase64(vault.salt), vault.kdf ? { name: 'PBKDF2-SHA256', iterations: vault.kdf.iterations } : undefined);
287
+ // The verifier answers "is this the right passphrase" before anything is
288
+ // decrypted, so a wrong one is named as a wrong passphrase rather than
289
+ // surfacing as an authentication-tag failure that reads like corruption.
290
+ if (!(await checkVaultVerifier(key, vault.verifier))) {
291
+ io.stderr('veilio: that vault passphrase is not right. Nothing was written.\n');
292
+ return EXIT_ERROR;
293
+ }
294
+ try {
295
+ map = await decryptMapFromVault(key, parseVaultEnvelope(remote.map_data));
296
+ }
297
+ catch (err) {
298
+ io.stderr(`veilio: that map could not be opened — ${err instanceof Error ? err.message : String(err)}. ` +
299
+ 'Nothing was written.\n');
300
+ return EXIT_ERROR;
301
+ }
302
+ }
303
+ // Only now is anything touched on disk.
304
+ const existing = loadRemote(mapPath);
305
+ const local = loadMap(mapPath);
306
+ const hasLocal = Object.keys(local).length > 0;
307
+ const diverged = hasLocal &&
308
+ existing !== undefined &&
309
+ existing.id === remote.id &&
310
+ existing.updatedAt !== remote.updated_at &&
311
+ JSON.stringify(local) !== JSON.stringify(map);
312
+ if (diverged && options.force !== true) {
313
+ // Reported, not resolved. Overwriting either side silently loses entries
314
+ // that some text out there still depends on.
315
+ io.stderr(`veilio: this map changed in Cloud (${existing.updatedAt} → ${remote.updated_at}) and ` +
316
+ 'locally since it was pulled. Neither copy has been touched.\n' +
317
+ `Pass --force to overwrite the local copy, or export it first with \`veilio map --json\`.\n`);
318
+ return EXIT_ERROR;
319
+ }
320
+ const now = new Date().toISOString();
321
+ saveMap(mapPath, map, {
322
+ force: true,
323
+ remote: { id: remote.id, updatedAt: remote.updated_at, pulledAt: now },
324
+ });
325
+ io.stdout(`Pulled "${remote.name}" (${Object.keys(map).length} symbols) into ${mapPath}\n`);
326
+ return EXIT_OK;
327
+ }
328
+ /** Upload the local map. Same endpoint, same quotas, same validation. */
329
+ export async function runMapsPush(name, mapPath, io) {
330
+ const credential = readCredential(io.home);
331
+ if (!credential)
332
+ return notSignedIn(io);
333
+ const map = loadMap(mapPath);
334
+ if (Object.keys(map).length === 0) {
335
+ io.stderr(`veilio: there is no local map at ${mapPath} to push.\n`);
336
+ return EXIT_ERROR;
337
+ }
338
+ let vault;
339
+ try {
340
+ vault = await getVault(credential);
341
+ }
342
+ catch (err) {
343
+ return reportCloudFailure(err, io);
344
+ }
345
+ if (!vault.initialized) {
346
+ io.stderr('veilio: this account has no vault, so there is no key to encrypt with. ' +
347
+ 'Create one in the web app first.\n');
348
+ return EXIT_ERROR;
349
+ }
350
+ const passphrase = io.password ? await io.password('Vault passphrase: ') : '';
351
+ if (passphrase === '') {
352
+ io.stderr('veilio: no passphrase given\n');
353
+ return EXIT_ERROR;
354
+ }
355
+ const key = await deriveVaultKey(passphrase, fromBase64(vault.salt), vault.kdf ? { name: 'PBKDF2-SHA256', iterations: vault.kdf.iterations } : undefined);
356
+ if (!(await checkVaultVerifier(key, vault.verifier))) {
357
+ // Caught before anything is uploaded. Pushing under the wrong key would
358
+ // store a map the account can never open again.
359
+ io.stderr('veilio: that vault passphrase is not right. Nothing was uploaded.\n');
360
+ return EXIT_ERROR;
361
+ }
362
+ const envelope = await encryptMapForVault(key, map);
363
+ let created;
364
+ try {
365
+ created = await createMap(credential, {
366
+ name: name ?? 'from the terminal',
367
+ map_data: JSON.stringify(envelope),
368
+ identifier_count: Object.keys(map).length,
369
+ scope: 'personal',
370
+ });
371
+ }
372
+ catch (err) {
373
+ return reportCloudFailure(err, io);
374
+ }
375
+ saveMap(mapPath, map, {
376
+ force: true,
377
+ remote: {
378
+ id: created.id,
379
+ updatedAt: new Date().toISOString(),
380
+ pushedAt: new Date().toISOString(),
381
+ },
382
+ });
383
+ io.stdout(`Pushed ${Object.keys(map).length} symbols to Cloud as ${created.id}\n`);
384
+ return EXIT_OK;
385
+ }
386
+ function notSignedIn(io) {
387
+ io.stderr('veilio: not signed in. Run `veilio login` first.\n');
388
+ return EXIT_ERROR;
389
+ }
390
+ /** Same distinctions as a failed sign-in, minus the ones only login can hit. */
391
+ function reportCloudFailure(err, io) {
392
+ return reportLoginFailure(err, io);
393
+ }
394
+ //# sourceMappingURL=cloud-commands.js.map