@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 +36 -3
- package/dist/args.d.ts +25 -1
- package/dist/args.d.ts.map +1 -1
- package/dist/args.js +51 -1
- package/dist/args.js.map +1 -1
- package/dist/cloud-commands.d.ts +70 -0
- package/dist/cloud-commands.d.ts.map +1 -0
- package/dist/cloud-commands.js +394 -0
- package/dist/cloud-commands.js.map +1 -0
- package/dist/cloud.d.ts +100 -0
- package/dist/cloud.d.ts.map +1 -0
- package/dist/cloud.js +178 -0
- package/dist/cloud.js.map +1 -0
- package/dist/commands.d.ts +14 -1
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +14 -0
- package/dist/commands.js.map +1 -1
- package/dist/credential.d.ts +52 -0
- package/dist/credential.d.ts.map +1 -0
- package/dist/credential.js +135 -0
- package/dist/credential.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +136 -0
- package/dist/index.js.map +1 -1
- package/dist/store.d.ts +25 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +26 -1
- package/dist/store.js.map +1 -1
- package/package.json +7 -5
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
|
|
19
|
-
|
|
20
|
-
|
|
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
|
package/dist/args.d.ts.map
CHANGED
|
@@ -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,
|
|
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([
|
|
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":"AAAA,gFAAgF;AAChF,2EAA2E;AAC3E,8DAA8D;AAE9D,OAAO,EAAE,SAAS,EAAsC,MAAM,oBAAoB,CAAA;
|
|
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
|