@serve.zone/cli 33.10.0 → 33.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts_cliclient/classes.cliclient.d.ts +7 -0
- package/dist_ts_cliclient/classes.cliclient.js +14 -1
- package/dist_ts_cliclient/commands.d.ts +6 -0
- package/dist_ts_cliclient/commands.js +39 -4
- package/dist_ts_cliclient/corestorecontroltoken.d.ts +33 -0
- package/dist_ts_cliclient/corestorecontroltoken.js +48 -0
- package/dist_ts_cliclient/index.d.ts +2 -1
- package/dist_ts_cliclient/index.js +23 -7
- package/dist_ts_cliclient/plugins.d.ts +2 -1
- package/dist_ts_cliclient/plugins.js +3 -2
- package/dist_ts_cliclient/tokeninput.d.ts +32 -15
- package/dist_ts_cliclient/tokeninput.js +61 -15
- package/package.json +3 -3
- package/readme.md +24 -2
- package/ts_cliclient/classes.cliclient.ts +18 -0
- package/ts_cliclient/commands.ts +46 -4
- package/ts_cliclient/corestorecontroltoken.ts +71 -0
- package/ts_cliclient/index.ts +37 -8
- package/ts_cliclient/plugins.ts +3 -1
- package/ts_cliclient/readme.md +24 -2
- package/ts_cliclient/tokeninput.ts +81 -18
|
@@ -14,5 +14,12 @@ export declare class CliClient {
|
|
|
14
14
|
* the cluster, the generation, the size, the SHA-256 and the audit receipt, never the bearer.
|
|
15
15
|
*/
|
|
16
16
|
exportRelayAuthorization(requestArg: plugins.servezoneInterfaces.data.IClusterRelayAuthorizationExportRequest, outputPathArg: string): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Stores the Corestore control token while it is still exactly at `expectedArg` and prints the
|
|
19
|
+
* state and the receipt the set produced, never the token. The token is the caller's to wipe.
|
|
20
|
+
*/
|
|
21
|
+
setCorestoreControlToken(expectedArg: plugins.servezoneInterfaces.data.TCorestoreControlTokenState, tokenArg: Uint8Array): Promise<void>;
|
|
22
|
+
/** Prints whether Cloudly holds the Corestore control token, its revision and its receipts. */
|
|
23
|
+
getCorestoreControlTokenState(): Promise<void>;
|
|
17
24
|
getGatewayStatus(): Promise<void>;
|
|
18
25
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import { CloudlyApiClient } from '@serve.zone/api';
|
|
3
3
|
import { exportRelayAuthorizationToFile } from './relayauthorization.js';
|
|
4
|
+
import { getCorestoreControlTokenState, setCorestoreControlToken } from './corestorecontroltoken.js';
|
|
4
5
|
export class CliClient {
|
|
5
6
|
constructor(cloudlyApiClientArg) {
|
|
6
7
|
this.cloudlyApiClient = cloudlyApiClientArg;
|
|
@@ -28,9 +29,21 @@ export class CliClient {
|
|
|
28
29
|
const output = await exportRelayAuthorizationToFile(this.cloudlyApiClient, requestArg, outputPathArg);
|
|
29
30
|
console.log(JSON.stringify({ ok: true, command: 'relay export-authorization', ...output }));
|
|
30
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Stores the Corestore control token while it is still exactly at `expectedArg` and prints the
|
|
34
|
+
* state and the receipt the set produced, never the token. The token is the caller's to wipe.
|
|
35
|
+
*/
|
|
36
|
+
async setCorestoreControlToken(expectedArg, tokenArg) {
|
|
37
|
+
const output = await setCorestoreControlToken(this.cloudlyApiClient, expectedArg, tokenArg);
|
|
38
|
+
console.log(JSON.stringify(output));
|
|
39
|
+
}
|
|
40
|
+
/** Prints whether Cloudly holds the Corestore control token, its revision and its receipts. */
|
|
41
|
+
async getCorestoreControlTokenState() {
|
|
42
|
+
console.log(JSON.stringify(await getCorestoreControlTokenState(this.cloudlyApiClient)));
|
|
43
|
+
}
|
|
31
44
|
async getGatewayStatus() {
|
|
32
45
|
const { state } = await this.cloudlyApiClient.settings.getExternalGatewayEnrollmentState();
|
|
33
46
|
console.log(JSON.stringify(state, null, 2));
|
|
34
47
|
}
|
|
35
48
|
}
|
|
36
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
49
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5jbGljbGllbnQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90c19jbGljbGllbnQvY2xhc3Nlcy5jbGljbGllbnQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE9BQU8sTUFBTSxjQUFjLENBQUM7QUFDeEMsT0FBTyxFQUFFLGdCQUFnQixFQUFFLE1BQU0saUJBQWlCLENBQUM7QUFDbkQsT0FBTyxFQUFFLDhCQUE4QixFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFDekUsT0FBTyxFQUFFLDZCQUE2QixFQUFFLHdCQUF3QixFQUFFLE1BQU0sNEJBQTRCLENBQUM7QUFFckcsTUFBTSxPQUFPLFNBQVM7SUFHcEIsWUFBWSxtQkFBcUM7UUFDL0MsSUFBSSxDQUFDLGdCQUFnQixHQUFHLG1CQUFtQixDQUFDO0lBQzlDLENBQUM7SUFFTSxLQUFLLENBQUMsV0FBVztRQUN0QixNQUFNLFFBQVEsR0FBRyxNQUFNLElBQUksQ0FBQyxnQkFBZ0IsQ0FBQyxPQUFPLENBQUMsV0FBVyxFQUFFLENBQUM7UUFDbkUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxRQUFRLENBQUMsQ0FBQztJQUN4QixDQUFDO0lBRUQ7OztPQUdHO0lBQ0ksS0FBSyxDQUFDLGdCQUFnQixDQUMzQixTQUErRCxFQUMvRCxpQkFBNkI7UUFFN0IsTUFBTSxFQUFFLEtBQUssRUFBRSxHQUFHLE1BQU0sSUFBSSxDQUFDLGdCQUFnQixDQUFDLFFBQVEsQ0FBQyxrQ0FBa0MsQ0FBQztZQUN4RixNQUFNLEVBQUUsU0FBUztZQUNqQixjQUFjLEVBQUUsaUJBQWlCO1NBQ2xDLENBQUMsQ0FBQztRQUNILE9BQU8sQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxLQUFLLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDOUMsQ0FBQztJQUVEOzs7T0FHRztJQUNJLEtBQUssQ0FBQyx3QkFBd0IsQ0FDbkMsVUFBb0YsRUFDcEYsYUFBcUI7UUFFckIsTUFBTSxNQUFNLEdBQUcsTUFBTSw4QkFBOEIsQ0FDakQsSUFBSSxDQUFDLGdCQUFnQixFQUFFLFVBQVUsRUFBRSxhQUFhLENBQUMsQ0FBQztRQUNwRCxPQUFPLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxTQUFTLENBQUMsRUFBRSxFQUFFLEVBQUUsSUFBSSxFQUFFLE9BQU8sRUFBRSw0QkFBNEIsRUFBRSxHQUFHLE1BQU0sRUFBRSxDQUFDLENBQUMsQ0FBQztJQUM5RixDQUFDO0lBRUQ7OztPQUdHO0lBQ0ksS0FBSyxDQUFDLHdCQUF3QixDQUNuQyxXQUF5RSxFQUN6RSxRQUFvQjtRQUVwQixNQUFNLE1BQU0sR0FBRyxNQUFNLHdCQUF3QixDQUFDLElBQUksQ0FBQyxnQkFBZ0IsRUFBRSxXQUFXLEVBQUUsUUFBUSxDQUFDLENBQUM7UUFDNUYsT0FBTyxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUM7SUFDdEMsQ0FBQztJQUVELCtGQUErRjtJQUN4RixLQUFLLENBQUMsNkJBQTZCO1FBQ3hDLE9BQU8sQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxNQUFNLDZCQUE2QixDQUFDLElBQUksQ0FBQyxnQkFBZ0IsQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUMxRixDQUFDO0lBRU0sS0FBSyxDQUFDLGdCQUFnQjtRQUMzQixNQUFNLEVBQUUsS0FBSyxFQUFFLEdBQUcsTUFBTSxJQUFJLENBQUMsZ0JBQWdCLENBQUMsUUFBUSxDQUFDLGlDQUFpQyxFQUFFLENBQUM7UUFDM0YsT0FBTyxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDLEtBQUssRUFBRSxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUM5QyxDQUFDO0NBQ0YifQ==
|
|
@@ -14,6 +14,12 @@ export type TCliCommand =
|
|
|
14
14
|
request: plugins.servezoneInterfaces.data.IClusterRelayAuthorizationExportRequest;
|
|
15
15
|
/** A normalized absolute path in a private directory; the file must not exist yet. */
|
|
16
16
|
outputPath: string;
|
|
17
|
+
} | {
|
|
18
|
+
kind: 'corestore-set-control-token';
|
|
19
|
+
/** Exactly the state the administrator last read; never a minimum. */
|
|
20
|
+
expected: plugins.servezoneInterfaces.data.TCorestoreControlTokenState;
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'corestore-control-token-state';
|
|
17
23
|
};
|
|
18
24
|
export declare const cliUsage: string;
|
|
19
25
|
/**
|
|
@@ -7,6 +7,9 @@ export const cliUsage = [
|
|
|
7
7
|
' The dcrouter admin token is read from stdin, never from arguments or the environment.',
|
|
8
8
|
' servezone relay export-authorization --cluster-id <id> --expected-generation <n> --output <absolute path>',
|
|
9
9
|
' Writes the relay bearer to a new 0600 file in a directory only you can write; never to stdout.',
|
|
10
|
+
' servezone corestore set-control-token --stdin --expect absent|<revision>',
|
|
11
|
+
' The Corestore control token is read from stdin, never from arguments or the environment.',
|
|
12
|
+
' servezone corestore control-token-state',
|
|
10
13
|
].join('\n');
|
|
11
14
|
/**
|
|
12
15
|
* An invocation the CLI does not run. It never quotes what it was given, because an argument may
|
|
@@ -42,6 +45,8 @@ export const parseCliCommand = (argsArg) => {
|
|
|
42
45
|
const [group, action, ...rest] = argsArg;
|
|
43
46
|
if (group === 'relay')
|
|
44
47
|
return parseRelayCommand(action, rest);
|
|
48
|
+
if (group === 'corestore')
|
|
49
|
+
return parseCorestoreCommand(action, rest);
|
|
45
50
|
requireUsage(group === 'gateway');
|
|
46
51
|
if (action === 'status') {
|
|
47
52
|
requireUsage(rest.length === 0);
|
|
@@ -54,19 +59,49 @@ export const parseCliCommand = (argsArg) => {
|
|
|
54
59
|
target: { gatewayUrl: options['--gateway-url'], gatewayClientId: options['--client-id'] },
|
|
55
60
|
};
|
|
56
61
|
};
|
|
62
|
+
/** A positive integer without leading zeros, as a generation or a revision is named. */
|
|
63
|
+
const parseCounter = (valueArg) => {
|
|
64
|
+
requireUsage(/^[1-9][0-9]{0,15}$/.test(valueArg) && Number.isSafeInteger(Number(valueArg)));
|
|
65
|
+
return Number(valueArg);
|
|
66
|
+
};
|
|
57
67
|
/** `relay export-authorization`: the cluster, the exact generation and a normalized absolute output. */
|
|
58
68
|
const parseRelayCommand = (actionArg, restArg) => {
|
|
59
69
|
requireUsage(actionArg === 'export-authorization');
|
|
60
70
|
const options = parseOptions(restArg, ['--cluster-id', '--expected-generation', '--output']);
|
|
61
|
-
const generation = options['--expected-generation'];
|
|
62
|
-
requireUsage(/^[1-9][0-9]{0,15}$/.test(generation) && Number.isSafeInteger(Number(generation)));
|
|
71
|
+
const generation = parseCounter(options['--expected-generation']);
|
|
63
72
|
const outputPath = options['--output'];
|
|
64
73
|
requireUsage(plugins.path.isAbsolute(outputPath) && plugins.path.normalize(outputPath) === outputPath
|
|
65
74
|
&& !outputPath.endsWith(plugins.path.sep));
|
|
66
75
|
return {
|
|
67
76
|
kind: 'relay-export-authorization',
|
|
68
|
-
request: { clusterId: options['--cluster-id'], expectedGeneration:
|
|
77
|
+
request: { clusterId: options['--cluster-id'], expectedGeneration: generation },
|
|
69
78
|
outputPath,
|
|
70
79
|
};
|
|
71
80
|
};
|
|
72
|
-
|
|
81
|
+
/**
|
|
82
|
+
* `corestore set-control-token --stdin --expect absent|<revision>` and
|
|
83
|
+
* `corestore control-token-state`.
|
|
84
|
+
*
|
|
85
|
+
* `--stdin` names where the token comes from, so no invocation reads it from anywhere else, and
|
|
86
|
+
* `--expect` is the state `control-token-state` answered: `absent`, or the stored revision.
|
|
87
|
+
*/
|
|
88
|
+
const parseCorestoreCommand = (actionArg, restArg) => {
|
|
89
|
+
if (actionArg === 'control-token-state') {
|
|
90
|
+
requireUsage(restArg.length === 0);
|
|
91
|
+
return { kind: 'corestore-control-token-state' };
|
|
92
|
+
}
|
|
93
|
+
requireUsage(actionArg === 'set-control-token');
|
|
94
|
+
// `--stdin` is a flag in an option's place, either before or after `--expect <value>`.
|
|
95
|
+
requireUsage(restArg.length === 3);
|
|
96
|
+
const stdinIndex = restArg[0] === '--stdin' ? 0 : 2;
|
|
97
|
+
requireUsage(restArg[stdinIndex] === '--stdin');
|
|
98
|
+
const options = parseOptions(restArg.filter((_argArg, indexArg) => indexArg !== stdinIndex), ['--expect']);
|
|
99
|
+
const expected = options['--expect'];
|
|
100
|
+
return {
|
|
101
|
+
kind: 'corestore-set-control-token',
|
|
102
|
+
expected: expected === 'absent'
|
|
103
|
+
? { state: 'absent' }
|
|
104
|
+
: { state: 'present', revision: parseCounter(expected) },
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29tbWFuZHMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90c19jbGljbGllbnQvY29tbWFuZHMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE9BQU8sTUFBTSxjQUFjLENBQUM7QUFxQnhDLE1BQU0sQ0FBQyxNQUFNLFFBQVEsR0FBRztJQUN0QixRQUFRO0lBQ1IsYUFBYTtJQUNiLDRCQUE0QjtJQUM1QiwwRUFBMEU7SUFDMUUsMkZBQTJGO0lBQzNGLDZHQUE2RztJQUM3RyxvR0FBb0c7SUFDcEcsNEVBQTRFO0lBQzVFLDhGQUE4RjtJQUM5RiwyQ0FBMkM7Q0FDNUMsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7QUFFYjs7O0dBR0c7QUFDSCxNQUFNLE9BQU8sYUFBYyxTQUFRLEtBQUs7SUFDdEM7UUFDRSxLQUFLLENBQUMsUUFBUSxDQUFDLENBQUM7UUFDaEIsSUFBSSxDQUFDLElBQUksR0FBRyxlQUFlLENBQUM7SUFDOUIsQ0FBQztDQUNGO0FBRUQsU0FBUyxZQUFZLENBQUMsWUFBcUI7SUFDekMsSUFBSSxDQUFDLFlBQVk7UUFBRSxNQUFNLElBQUksYUFBYSxFQUFFLENBQUM7QUFDL0MsQ0FBQztBQUVELG1GQUFtRjtBQUNuRixNQUFNLFlBQVksR0FBRyxDQUNuQixPQUEwQixFQUMxQixRQUEwQixFQUNILEVBQUU7SUFDekIsTUFBTSxPQUFPLEdBQUcsSUFBSSxHQUFHLEVBQWtCLENBQUM7SUFDMUMsWUFBWSxDQUFDLE9BQU8sQ0FBQyxNQUFNLEtBQUssUUFBUSxDQUFDLE1BQU0sR0FBRyxDQUFDLENBQUMsQ0FBQztJQUNyRCxLQUFLLElBQUksS0FBSyxHQUFHLENBQUMsRUFBRSxLQUFLLEdBQUcsT0FBTyxDQUFDLE1BQU0sRUFBRSxLQUFLLElBQUksQ0FBQyxFQUFFLENBQUM7UUFDdkQsTUFBTSxJQUFJLEdBQUcsT0FBTyxDQUFDLEtBQUssQ0FBQyxDQUFDO1FBQzVCLE1BQU0sS0FBSyxHQUFHLE9B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
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
type TCorestoreControlTokenSetRefusal = plugins.servezoneInterfaces.data.TCorestoreControlTokenSetRefusal;
|
|
3
|
+
/** Cloudly did not store the token, for one of the contract's refusals, which it names. */
|
|
4
|
+
export declare class CorestoreControlTokenSetRefusedError extends Error {
|
|
5
|
+
readonly refusal: TCorestoreControlTokenSetRefusal;
|
|
6
|
+
constructor(refusal: TCorestoreControlTokenSetRefusal);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The contract refusal an error is, or `undefined` for every other error.
|
|
10
|
+
*
|
|
11
|
+
* Cloudly answers a refusal by its name and nothing else, so the name is the whole of the error's
|
|
12
|
+
* message; any other message, Cloudly's one denial text included, is not a refusal.
|
|
13
|
+
*/
|
|
14
|
+
export declare const nameCorestoreControlTokenSetRefusal: (errorArg: unknown) => TCorestoreControlTokenSetRefusal | undefined;
|
|
15
|
+
/** What the CLI prints about a stored token: the state it produced and its receipt, never the token. */
|
|
16
|
+
export interface ICorestoreControlTokenSetOutput extends plugins.servezoneInterfaces.data.ICorestoreControlTokenSetResult {
|
|
17
|
+
ok: true;
|
|
18
|
+
command: 'corestore set-control-token';
|
|
19
|
+
}
|
|
20
|
+
/** What the CLI prints about the token's state: the state and its value-free receipts. */
|
|
21
|
+
export interface ICorestoreControlTokenStateOutput extends plugins.servezoneInterfaces.data.ICorestoreControlTokenStateRead {
|
|
22
|
+
ok: true;
|
|
23
|
+
command: 'corestore control-token-state';
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Stores the token through the API client while it is still exactly at `expected`, answering only
|
|
27
|
+
* the state and the receipt. The token bytes are the caller's to wipe; the text the request needs
|
|
28
|
+
* is decoded from them here and never printed, logged or put into an error.
|
|
29
|
+
*/
|
|
30
|
+
export declare const setCorestoreControlToken: (apiClientArg: plugins.servezoneApi.CloudlyApiClient, expectedArg: plugins.servezoneInterfaces.data.TCorestoreControlTokenState, tokenArg: Uint8Array) => Promise<ICorestoreControlTokenSetOutput>;
|
|
31
|
+
/** Whether Cloudly holds the token, at which revision, and its receipts, oldest first. */
|
|
32
|
+
export declare const getCorestoreControlTokenState: (apiClientArg: plugins.servezoneApi.CloudlyApiClient) => Promise<ICorestoreControlTokenStateOutput>;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import { decodeCorestoreControlToken } from './tokeninput.js';
|
|
3
|
+
/** Cloudly did not store the token, for one of the contract's refusals, which it names. */
|
|
4
|
+
export class CorestoreControlTokenSetRefusedError extends Error {
|
|
5
|
+
constructor(refusal) {
|
|
6
|
+
super(`The Corestore control token was not stored: ${refusal}.`);
|
|
7
|
+
this.refusal = refusal;
|
|
8
|
+
this.name = 'CorestoreControlTokenSetRefusedError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The contract refusal an error is, or `undefined` for every other error.
|
|
13
|
+
*
|
|
14
|
+
* Cloudly answers a refusal by its name and nothing else, so the name is the whole of the error's
|
|
15
|
+
* message; any other message, Cloudly's one denial text included, is not a refusal.
|
|
16
|
+
*/
|
|
17
|
+
export const nameCorestoreControlTokenSetRefusal = (errorArg) => {
|
|
18
|
+
if (!(errorArg instanceof Error))
|
|
19
|
+
return undefined;
|
|
20
|
+
return plugins.servezoneInterfaces.data.corestoreControlTokenSetRefusals
|
|
21
|
+
.find((refusalArg) => refusalArg === errorArg.message);
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Stores the token through the API client while it is still exactly at `expected`, answering only
|
|
25
|
+
* the state and the receipt. The token bytes are the caller's to wipe; the text the request needs
|
|
26
|
+
* is decoded from them here and never printed, logged or put into an error.
|
|
27
|
+
*/
|
|
28
|
+
export const setCorestoreControlToken = async (apiClientArg, expectedArg, tokenArg) => {
|
|
29
|
+
try {
|
|
30
|
+
const { state, receipt } = await apiClientArg.platform.setCorestoreControlToken({
|
|
31
|
+
token: decodeCorestoreControlToken(tokenArg),
|
|
32
|
+
expected: expectedArg,
|
|
33
|
+
});
|
|
34
|
+
return { ok: true, command: 'corestore set-control-token', state, receipt };
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
const refusal = nameCorestoreControlTokenSetRefusal(error);
|
|
38
|
+
if (refusal !== undefined)
|
|
39
|
+
throw new CorestoreControlTokenSetRefusedError(refusal);
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
/** Whether Cloudly holds the token, at which revision, and its receipts, oldest first. */
|
|
44
|
+
export const getCorestoreControlTokenState = async (apiClientArg) => {
|
|
45
|
+
const { state, receipts } = await apiClientArg.platform.getCorestoreControlTokenState();
|
|
46
|
+
return { ok: true, command: 'corestore control-token-state', state, receipts };
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29yZXN0b3JlY29udHJvbHRva2VuLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHNfY2xpY2xpZW50L2NvcmVzdG9yZWNvbnRyb2x0b2tlbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGNBQWMsQ0FBQztBQUN4QyxPQUFPLEVBQUUsMkJBQTJCLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUk5RCwyRkFBMkY7QUFDM0YsTUFBTSxPQUFPLG9DQUFxQyxTQUFRLEtBQUs7SUFDN0QsWUFBNEIsT0FBeUM7UUFDbkUsS0FBSyxDQUFDLCtDQUErQyxPQUFPLEdBQUcsQ0FBQyxDQUFDO1FBRHZDLFlBQU8sR0FBUCxPQUFPLENBQWtDO1FBRW5FLElBQUksQ0FBQyxJQUFJLEdBQUcsc0NBQXNDLENBQUM7SUFDckQsQ0FBQztDQUNGO0FBRUQ7Ozs7O0dBS0c7QUFDSCxNQUFNLENBQUMsTUFBTSxtQ0FBbUMsR0FBRyxDQUNqRCxRQUFpQixFQUM2QixFQUFFO0lBQ2hELElBQUksQ0FBQyxDQUFDLFFBQVEsWUFBWSxLQUFLLENBQUM7UUFBRSxPQUFPLFNBQVMsQ0FBQztJQUNuRCxPQUFPLE9BQU8sQ0FBQyxtQkFBbUIsQ0FBQyxJQUFJLENBQUMsZ0NBQWdDO1NBQ3JFLElBQUksQ0FBQyxDQUFDLFVBQVUsRUFBRSxFQUFFLENBQUMsVUFBVSxLQUFLLFFBQVEsQ0FBQyxPQUFPLENBQUMsQ0FBQztBQUMzRCxDQUFDLENBQUM7QUFnQkY7Ozs7R0FJRztBQUNILE1BQU0sQ0FBQyxNQUFNLHdCQUF3QixHQUFHLEtBQUssRUFDM0MsWUFBbUQsRUFDbkQsV0FBeUUsRUFDekUsUUFBb0IsRUFDc0IsRUFBRTtJQUM1QyxJQUFJLENBQUM7UUFDSCxNQUFNLEVBQUUsS0FBSyxFQUFFLE9BQU8sRUFBRSxHQUFHLE1BQU0sWUFBWSxDQUFDLFFBQVEsQ0FBQyx3QkFBd0IsQ0FBQztZQUM5RSxLQUFLLEVBQUUsMkJBQTJCLENBQUMsUUFBUSxDQUFDO1lBQzVDLFFBQVEsRUFBRSxXQUFXO1NBQ3RCLENBQUMsQ0FBQztRQUNILE9BQU8sRUFBRSxFQUFFLEVBQUUsSUFBSSxFQUFFLE9BQU8sRUFBRSw2QkFBNkIsRUFBRSxLQUFLLEVBQUUsT0FBTyxFQUFFLENBQUM7SUFDOUUsQ0FBQztJQUFDLE9BQU8sS0FBSyxFQUFFLENBQUM7UUFDZixNQUFNLE9BQU8sR0FBRyxtQ0FBbUMsQ0FBQyxLQUFLLENBQUMsQ0FBQztRQUMzRCxJQUFJLE9BQU8sS0FBSyxTQUFTO1lBQUUsTUFBTSxJQUFJLG9DQUFvQyxDQUFDLE9BQU8sQ0FBQyxDQUFDO1FBQ25GLE1BQU0sS0FBSyxDQUFDO0lBQ2QsQ0FBQztBQUNILENBQUMsQ0FBQztBQUVGLDBGQUEwRjtBQUMxRixNQUFNLENBQUMsTUFBTSw2QkFBNkIsR0FBRyxLQUFLLEVBQ2hELFlBQW1ELEVBQ1AsRUFBRTtJQUM5QyxNQUFNLEVBQUUsS0FBSyxFQUFFLFFBQVEsRUFBRSxHQUFHLE1BQU0sWUFBWSxDQUFDLFFBQVEsQ0FBQyw2QkFBNkIsRUFBRSxDQUFDO0lBQ3hGLE9BQU8sRUFBRSxFQUFFLEVBQUUsSUFBSSxFQUFFLE9BQU8sRUFBRSwrQkFBK0IsRUFBRSxLQUFLLEVBQUUsUUFBUSxFQUFFLENBQUM7QUFDakYsQ0FBQyxDQUFDIn0=
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { CliUsageError, parseCliCommand, type TCliCommand } from './commands.js';
|
|
2
2
|
export { RelayAuthorizationOutputError, exportRelayAuthorizationToFile, requirePrivateOutputDirectory, writeRelayAuthorization, type IRelayAuthorizationOutput, type TRelayAuthorizationOutputRefusal, } from './relayauthorization.js';
|
|
3
|
-
export { BootstrapTokenInputError, readBootstrapTokenInput, type TBootstrapTokenInputRefusal, } from './tokeninput.js';
|
|
3
|
+
export { BootstrapTokenInputError, CorestoreControlTokenInputError, TokenInputError, readBootstrapTokenInput, readCorestoreControlTokenInput, type TBootstrapTokenInputRefusal, type TTokenInputRefusal, } from './tokeninput.js';
|
|
4
|
+
export { CorestoreControlTokenSetRefusedError, getCorestoreControlTokenState, nameCorestoreControlTokenSetRefusal, setCorestoreControlToken, type ICorestoreControlTokenSetOutput, type ICorestoreControlTokenStateOutput, } from './corestorecontroltoken.js';
|
|
4
5
|
export declare const runCli: () => Promise<void>;
|
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import { CliClient } from './classes.cliclient.js';
|
|
3
3
|
import { parseCliCommand } from './commands.js';
|
|
4
|
-
import { readBootstrapTokenInput } from './tokeninput.js';
|
|
4
|
+
import { readBootstrapTokenInput, readCorestoreControlTokenInput } from './tokeninput.js';
|
|
5
5
|
import { requirePrivateOutputDirectory } from './relayauthorization.js';
|
|
6
6
|
export { CliUsageError, parseCliCommand } from './commands.js';
|
|
7
7
|
export { RelayAuthorizationOutputError, exportRelayAuthorizationToFile, requirePrivateOutputDirectory, writeRelayAuthorization, } from './relayauthorization.js';
|
|
8
|
-
export { BootstrapTokenInputError, readBootstrapTokenInput, } from './tokeninput.js';
|
|
8
|
+
export { BootstrapTokenInputError, CorestoreControlTokenInputError, TokenInputError, readBootstrapTokenInput, readCorestoreControlTokenInput, } from './tokeninput.js';
|
|
9
|
+
export { CorestoreControlTokenSetRefusedError, getCorestoreControlTokenState, nameCorestoreControlTokenSetRefusal, setCorestoreControlToken, } from './corestorecontroltoken.js';
|
|
10
|
+
/** Reads the token the command needs from stdin, before anything connects. */
|
|
11
|
+
const readInvocation = async (commandArg) => {
|
|
12
|
+
if (commandArg.kind === 'gateway-bootstrap') {
|
|
13
|
+
return { ...commandArg, bootstrapToken: await readBootstrapTokenInput(process.stdin) };
|
|
14
|
+
}
|
|
15
|
+
if (commandArg.kind === 'corestore-set-control-token') {
|
|
16
|
+
return { ...commandArg, controlToken: await readCorestoreControlTokenInput(process.stdin) };
|
|
17
|
+
}
|
|
18
|
+
return commandArg;
|
|
19
|
+
};
|
|
9
20
|
export const runCli = async () => {
|
|
10
21
|
// The command and the token are settled before anything connects, so a wrong invocation or a
|
|
11
22
|
// token that could never be one fails without reaching Cloudly.
|
|
12
23
|
let command;
|
|
13
24
|
try {
|
|
14
|
-
|
|
15
|
-
command = parsed.kind === 'gateway-bootstrap'
|
|
16
|
-
? { ...parsed, bootstrapToken: await readBootstrapTokenInput(process.stdin) }
|
|
17
|
-
: parsed;
|
|
25
|
+
command = await readInvocation(parseCliCommand(process.argv.slice(2)));
|
|
18
26
|
// An output the bearer could not be written to fetches no bearer and records no export.
|
|
19
27
|
if (command.kind === 'relay-export-authorization') {
|
|
20
28
|
await requirePrivateOutputDirectory(command.outputPath);
|
|
@@ -40,6 +48,12 @@ export const runCli = async () => {
|
|
|
40
48
|
else if (command.kind === 'relay-export-authorization') {
|
|
41
49
|
await cliClient.exportRelayAuthorization(command.request, command.outputPath);
|
|
42
50
|
}
|
|
51
|
+
else if (command.kind === 'corestore-set-control-token') {
|
|
52
|
+
await cliClient.setCorestoreControlToken(command.expected, command.controlToken);
|
|
53
|
+
}
|
|
54
|
+
else if (command.kind === 'corestore-control-token-state') {
|
|
55
|
+
await cliClient.getCorestoreControlTokenState();
|
|
56
|
+
}
|
|
43
57
|
else {
|
|
44
58
|
await cliClient.getGatewayStatus();
|
|
45
59
|
}
|
|
@@ -56,6 +70,8 @@ export const runCli = async () => {
|
|
|
56
70
|
finally {
|
|
57
71
|
if (command.kind === 'gateway-bootstrap')
|
|
58
72
|
command.bootstrapToken.fill(0);
|
|
73
|
+
if (command.kind === 'corestore-set-control-token')
|
|
74
|
+
command.controlToken.fill(0);
|
|
59
75
|
}
|
|
60
76
|
};
|
|
61
77
|
/** The API client, started and signed in the way every command of this CLI signs in. */
|
|
@@ -91,4 +107,4 @@ const connect = async () => {
|
|
|
91
107
|
}
|
|
92
108
|
return apiClient;
|
|
93
109
|
};
|
|
94
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
110
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90c19jbGljbGllbnQvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE9BQU8sTUFBTSxjQUFjLENBQUM7QUFDeEMsT0FBTyxFQUFFLFNBQVMsRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBQ25ELE9BQU8sRUFBRSxlQUFlLEVBQW9CLE1BQU0sZUFBZSxDQUFDO0FBQ2xFLE9BQU8sRUFBRSx1QkFBdUIsRUFBRSw4QkFBOEIsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBQzFGLE9BQU8sRUFBRSw2QkFBNkIsRUFBRSxNQUFNLHlCQUF5QixDQUFDO0FBRXhFLE9BQU8sRUFBRSxhQUFhLEVBQUUsZUFBZSxFQUFvQixNQUFNLGVBQWUsQ0FBQztBQUNqRixPQUFPLEVBQ0wsNkJBQTZCLEVBQzdCLDhCQUE4QixFQUM5Qiw2QkFBNkIsRUFDN0IsdUJBQXVCLEdBR3hCLE1BQU0seUJBQXlCLENBQUM7QUFDakMsT0FBTyxFQUNMLHdCQUF3QixFQUN4QiwrQkFBK0IsRUFDL0IsZUFBZSxFQUNmLHVCQUF1QixFQUN2Qiw4QkFBOEIsR0FHL0IsTUFBTSxpQkFBaUIsQ0FBQztBQUN6QixPQUFPLEVBQ0wsb0NBQW9DLEVBQ3BDLDZCQUE2QixFQUM3QixtQ0FBbUMsRUFDbkMsd0JBQXdCLEdBR3pCLE1BQU0sNEJBQTRCLENBQUM7QUFXcEMsOEVBQThFO0FBQzlFLE1BQU0sY0FBYyxHQUFHLEtBQUssRUFBRSxVQUF1QixFQUEyQixFQUFFO0lBQ2hGLElBQUksVUFBVSxDQUFDLElBQUksS0FBSyxtQkFBbUIsRUFBRSxDQUFDO1FBQzVDLE9BQU8sRUFBRSxHQUFHLFVBQVUsRUFBRSxjQUFjLEVBQUUsTUFBTSx1QkFBdUIsQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQztJQUN6RixDQUFDO0lBQ0QsSUFBSSxVQUFVLENBQUMsSUFBSSxLQUFLLDZCQUE2QixFQUFFLENB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
|
|
@@ -4,7 +4,8 @@ import * as path from 'node:path';
|
|
|
4
4
|
export { crypto, fs, path, };
|
|
5
5
|
import * as servezoneApi from '@serve.zone/api';
|
|
6
6
|
import * as servezoneInterfaces from '@serve.zone/interfaces';
|
|
7
|
-
|
|
7
|
+
import * as servezoneInterfacesRuntime from '@serve.zone/interfaces/runtime';
|
|
8
|
+
export { servezoneApi, servezoneInterfaces, servezoneInterfacesRuntime, };
|
|
8
9
|
import * as projectinfo from '@push.rocks/projectinfo';
|
|
9
10
|
import * as qenv from '@push.rocks/qenv';
|
|
10
11
|
import * as smartfs from '@push.rocks/smartfs';
|
|
@@ -6,10 +6,11 @@ export { crypto, fs, path, };
|
|
|
6
6
|
// @serve.zone scope
|
|
7
7
|
import * as servezoneApi from '@serve.zone/api';
|
|
8
8
|
import * as servezoneInterfaces from '@serve.zone/interfaces';
|
|
9
|
-
|
|
9
|
+
import * as servezoneInterfacesRuntime from '@serve.zone/interfaces/runtime';
|
|
10
|
+
export { servezoneApi, servezoneInterfaces, servezoneInterfacesRuntime, };
|
|
10
11
|
// @push.rocks scope
|
|
11
12
|
import * as projectinfo from '@push.rocks/projectinfo';
|
|
12
13
|
import * as qenv from '@push.rocks/qenv';
|
|
13
14
|
import * as smartfs from '@push.rocks/smartfs';
|
|
14
15
|
export { projectinfo, qenv, smartfs, };
|
|
15
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
16
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGx1Z2lucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzX2NsaWNsaWVudC9wbHVnaW5zLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLG9CQUFvQjtBQUNwQixPQUFPLEtBQUssTUFBTSxNQUFNLGFBQWEsQ0FBQztBQUN0QyxPQUFPLEtBQUssRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBQ3ZDLE9BQU8sS0FBSyxJQUFJLE1BQU0sV0FBVyxDQUFDO0FBRWxDLE9BQU8sRUFDTCxNQUFNLEVBQ04sRUFBRSxFQUNGLElBQUksR0FDTCxDQUFBO0FBRUQsb0JBQW9CO0FBQ3BCLE9BQU8sS0FBSyxZQUFZLE1BQU0saUJBQWlCLENBQUM7QUFDaEQsT0FBTyxLQUFLLG1CQUFtQixNQUFNLHdCQUF3QixDQUFDO0FBQzlELE9BQU8sS0FBSywwQkFBMEIsTUFBTSxnQ0FBZ0MsQ0FBQztBQUU3RSxPQUFPLEVBQ0wsWUFBWSxFQUNaLG1CQUFtQixFQUNuQiwwQkFBMEIsR0FDM0IsQ0FBQTtBQUVELG9CQUFvQjtBQUNwQixPQUFPLEtBQUssV0FBVyxNQUFNLHlCQUF5QixDQUFDO0FBQ3ZELE9BQU8sS0FBSyxJQUFJLE1BQU0sa0JBQWtCLENBQUM7QUFDekMsT0FBTyxLQUFLLE9BQU8sTUFBTSxxQkFBcUIsQ0FBQztBQUUvQyxPQUFPLEVBQ0wsV0FBVyxFQUNYLElBQUksRUFDSixPQUFPLEdBQ1IsQ0FBQSJ9
|
|
@@ -1,25 +1,42 @@
|
|
|
1
|
-
/** Why
|
|
2
|
-
export type
|
|
1
|
+
/** Why a token piped in was not taken, named without quoting any of it. */
|
|
2
|
+
export type TTokenInputRefusal =
|
|
3
3
|
/** stdin is a terminal, which would echo a token typed into it; pipe the token in instead. */
|
|
4
4
|
'token-input-terminal'
|
|
5
5
|
/** stdin carried more bytes than a token and one line break. */
|
|
6
6
|
| 'token-input-oversized'
|
|
7
|
-
/** What stdin carried is not a token: empty, or a byte
|
|
7
|
+
/** What stdin carried is not a token by the token's own rule: empty, or a byte the rule refuses. */
|
|
8
8
|
| 'token-input-invalid';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
/** Why the dcrouter admin token piped into `gateway bootstrap` was not taken. */
|
|
10
|
+
export type TBootstrapTokenInputRefusal = TTokenInputRefusal;
|
|
11
|
+
/** A refused token input: which token, and why, never a byte of what was read. */
|
|
12
|
+
export declare class TokenInputError extends Error {
|
|
13
|
+
readonly refusal: TTokenInputRefusal;
|
|
14
|
+
constructor(refusal: TTokenInputRefusal, subjectArg: string);
|
|
12
15
|
}
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
export declare class BootstrapTokenInputError extends TokenInputError {
|
|
17
|
+
constructor(refusalArg: TTokenInputRefusal);
|
|
18
|
+
}
|
|
19
|
+
export declare class CorestoreControlTokenInputError extends TokenInputError {
|
|
20
|
+
constructor(refusalArg: TTokenInputRefusal);
|
|
21
|
+
}
|
|
22
|
+
/** What a token is read from: the command's stdin, or a stream a test hands in. */
|
|
23
|
+
export type TTokenInput = AsyncIterable<Uint8Array> & {
|
|
15
24
|
isTTY?: boolean;
|
|
16
25
|
};
|
|
26
|
+
export type TBootstrapTokenInput = TTokenInput;
|
|
27
|
+
/**
|
|
28
|
+
* The dcrouter admin token piped into `gateway bootstrap`: at most the bootstrap contract's
|
|
29
|
+
* `maximumTokenBytes` of visible ASCII.
|
|
30
|
+
*/
|
|
31
|
+
export declare const readBootstrapTokenInput: (inputArg: TTokenInput) => Promise<Uint8Array>;
|
|
32
|
+
/**
|
|
33
|
+
* The token as text, exactly as its bytes say: invalid UTF-8 throws, and a leading byte order mark
|
|
34
|
+
* is kept, so the contract judges and Cloudly receives every character that was piped in.
|
|
35
|
+
*/
|
|
36
|
+
export declare const decodeCorestoreControlToken: (tokenArg: Uint8Array) => string;
|
|
17
37
|
/**
|
|
18
|
-
* The
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* the environment leaks into child processes. One trailing line break, `\n` or `\r\n`, is what
|
|
22
|
-
* `echo` or a file adds and is not part of the token; everything else must pass the contract's
|
|
23
|
-
* token rule. Every byte read is wiped before this answers, whatever it answers.
|
|
38
|
+
* The Corestore control token piped into `corestore set-control-token`: what the contract's
|
|
39
|
+
* `isCorestoreControlToken` accepts, so at most `corestoreCredentialRuntimeLimits
|
|
40
|
+
* .maximumControlTokenBytes` of UTF-8 without whitespace.
|
|
24
41
|
*/
|
|
25
|
-
export declare const
|
|
42
|
+
export declare const readCorestoreControlTokenInput: (inputArg: TTokenInput) => Promise<Uint8Array>;
|
|
@@ -1,26 +1,37 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
/** A refused token input: which token, and why, never a byte of what was read. */
|
|
3
|
+
export class TokenInputError extends Error {
|
|
4
|
+
constructor(refusal, subjectArg) {
|
|
5
|
+
super(`${subjectArg} on stdin was refused: ${refusal}.`);
|
|
5
6
|
this.refusal = refusal;
|
|
7
|
+
this.name = 'TokenInputError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export class BootstrapTokenInputError extends TokenInputError {
|
|
11
|
+
constructor(refusalArg) {
|
|
12
|
+
super(refusalArg, 'The dcrouter admin token');
|
|
6
13
|
this.name = 'BootstrapTokenInputError';
|
|
7
14
|
}
|
|
8
15
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
16
|
+
export class CorestoreControlTokenInputError extends TokenInputError {
|
|
17
|
+
constructor(refusalArg) {
|
|
18
|
+
super(refusalArg, 'The Corestore control token');
|
|
19
|
+
this.name = 'CorestoreControlTokenInputError';
|
|
20
|
+
}
|
|
12
21
|
}
|
|
13
22
|
/**
|
|
14
|
-
*
|
|
23
|
+
* A token piped into a command, as bytes the caller wipes.
|
|
15
24
|
*
|
|
16
25
|
* stdin is the only source: an argument would sit in the process list and the shell history, and
|
|
17
26
|
* the environment leaks into child processes. One trailing line break, `\n` or `\r\n`, is what
|
|
18
|
-
* `echo` or a file adds and is not part of the token; everything else must pass the
|
|
19
|
-
*
|
|
27
|
+
* `echo` or a file adds and is not part of the token; everything else must pass the token's rule.
|
|
28
|
+
* Reading stops as soon as stdin carries more than the rule's bound and one line break. Every byte
|
|
29
|
+
* read is wiped before this answers, whatever it answers.
|
|
20
30
|
*/
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
const readTokenInput = async (inputArg, ruleArg) => {
|
|
32
|
+
if (inputArg.isTTY === true)
|
|
33
|
+
throw ruleArg.refuse('token-input-terminal');
|
|
34
|
+
const maximumBytes = ruleArg.maximumTokenBytes + 2;
|
|
24
35
|
const chunks = [];
|
|
25
36
|
let input;
|
|
26
37
|
try {
|
|
@@ -28,7 +39,8 @@ export const readBootstrapTokenInput = async (inputArg) => {
|
|
|
28
39
|
for await (const chunk of inputArg) {
|
|
29
40
|
chunks.push(chunk);
|
|
30
41
|
byteLength += chunk.byteLength;
|
|
31
|
-
|
|
42
|
+
if (byteLength > maximumBytes)
|
|
43
|
+
throw ruleArg.refuse('token-input-oversized');
|
|
32
44
|
}
|
|
33
45
|
input = Buffer.concat(chunks, byteLength);
|
|
34
46
|
let end = input.byteLength;
|
|
@@ -38,7 +50,8 @@ export const readBootstrapTokenInput = async (inputArg) => {
|
|
|
38
50
|
end -= 1;
|
|
39
51
|
}
|
|
40
52
|
const token = input.subarray(0, end);
|
|
41
|
-
|
|
53
|
+
if (!ruleArg.isToken(token))
|
|
54
|
+
throw ruleArg.refuse('token-input-invalid');
|
|
42
55
|
// A copy, because the input and every view of it are wiped below.
|
|
43
56
|
return new Uint8Array(token);
|
|
44
57
|
}
|
|
@@ -48,4 +61,37 @@ export const readBootstrapTokenInput = async (inputArg) => {
|
|
|
48
61
|
chunk.fill(0);
|
|
49
62
|
}
|
|
50
63
|
};
|
|
51
|
-
|
|
64
|
+
/**
|
|
65
|
+
* The dcrouter admin token piped into `gateway bootstrap`: at most the bootstrap contract's
|
|
66
|
+
* `maximumTokenBytes` of visible ASCII.
|
|
67
|
+
*/
|
|
68
|
+
export const readBootstrapTokenInput = async (inputArg) => await readTokenInput(inputArg, {
|
|
69
|
+
maximumTokenBytes: plugins.servezoneInterfaces.data.externalGatewayBootstrapContract.maximumTokenBytes,
|
|
70
|
+
isToken: (tokenArg) => plugins.servezoneInterfaces.data.isExternalGatewayBootstrapToken(tokenArg),
|
|
71
|
+
refuse: (refusalArg) => new BootstrapTokenInputError(refusalArg),
|
|
72
|
+
});
|
|
73
|
+
/**
|
|
74
|
+
* The token as text, exactly as its bytes say: invalid UTF-8 throws, and a leading byte order mark
|
|
75
|
+
* is kept, so the contract judges and Cloudly receives every character that was piped in.
|
|
76
|
+
*/
|
|
77
|
+
export const decodeCorestoreControlToken = (tokenArg) => new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(tokenArg);
|
|
78
|
+
/**
|
|
79
|
+
* The Corestore control token piped into `corestore set-control-token`: what the contract's
|
|
80
|
+
* `isCorestoreControlToken` accepts, so at most `corestoreCredentialRuntimeLimits
|
|
81
|
+
* .maximumControlTokenBytes` of UTF-8 without whitespace.
|
|
82
|
+
*/
|
|
83
|
+
export const readCorestoreControlTokenInput = async (inputArg) => await readTokenInput(inputArg, {
|
|
84
|
+
maximumTokenBytes: plugins.servezoneInterfacesRuntime.corestoreCredentialRuntimeLimits.maximumControlTokenBytes,
|
|
85
|
+
isToken: (tokenArg) => {
|
|
86
|
+
let token;
|
|
87
|
+
try {
|
|
88
|
+
token = decodeCorestoreControlToken(tokenArg);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
return plugins.servezoneInterfacesRuntime.isCorestoreControlToken(token);
|
|
94
|
+
},
|
|
95
|
+
refuse: (refusalArg) => new CorestoreControlTokenInputError(refusalArg),
|
|
96
|
+
});
|
|
97
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidG9rZW5pbnB1dC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzX2NsaWNsaWVudC90b2tlbmlucHV0LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sY0FBYyxDQUFDO0FBY3hDLGtGQUFrRjtBQUNsRixNQUFNLE9BQU8sZUFBZ0IsU0FBUSxLQUFLO0lBQ3hDLFlBQTRCLE9BQTJCLEVBQUUsVUFBa0I7UUFDekUsS0FBSyxDQUFDLEdBQUcsVUFBVSwwQkFBMEIsT0FBTyxHQUFHLENBQUMsQ0FBQztRQUQvQixZQUFPLEdBQVAsT0FBTyxDQUFvQjtRQUVyRCxJQUFJLENBQUMsSUFBSSxHQUFHLGlCQUFpQixDQUFDO0lBQ2hDLENBQUM7Q0FDRjtBQUVELE1BQU0sT0FBTyx3QkFBeUIsU0FBUSxlQUFlO0lBQzNELFlBQVksVUFBOEI7UUFDeEMsS0FBSyxDQUFDLFVBQVUsRUFBRSwwQkFBMEIsQ0FBQyxDQUFDO1FBQzlDLElBQUksQ0FBQyxJQUFJLEdBQUcsMEJBQTBCLENBQUM7SUFDekMsQ0FBQztDQUNGO0FBRUQsTUFBTSxPQUFPLCtCQUFnQyxTQUFRLGVBQWU7SUFDbEUsWUFBWSxVQUE4QjtRQUN4QyxLQUFLLENBQUMsVUFBVSxFQUFFLDZCQUE2QixDQUFDLENBQUM7UUFDakQsSUFBSSxDQUFDLElBQUksR0FBRyxpQ0FBaUMsQ0FBQztJQUNoRCxDQUFDO0NBQ0Y7QUFlRDs7Ozs7Ozs7R0FRRztBQUNILE1BQU0sY0FBYyxHQUFHLEtBQUssRUFBRSxRQUFxQixFQUFFLE9BQXdCLEVBQXVCLEVBQUU7SUFDcEcsSUFBSSxRQUFRLENBQUMsS0FBSyxLQUFLLElBQUk7UUFBRSxNQUFNLE9BQU8sQ0FBQyxNQUFNLENBQUMsc0JBQXNCLENBQUMsQ0FBQztJQUMxRSxNQUFNLFlBQVksR0FBRyxPQUFPLENBQUMsaUJBQWlCLEdBQUcsQ0FBQyxDQUFDO0lBQ25ELE1BQU0sTUFBTSxHQUFpQixFQUFFLENBQUM7SUFD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
|
package/package.json
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"security"
|
|
39
39
|
],
|
|
40
40
|
"name": "@serve.zone/cli",
|
|
41
|
-
"version": "33.
|
|
41
|
+
"version": "33.11.0",
|
|
42
42
|
"type": "module",
|
|
43
43
|
"description": "A comprehensive tool for managing containerized applications across multiple cloud providers using Docker Swarmkit, featuring web, CLI, and API interfaces.",
|
|
44
44
|
"main": "./dist_ts_cliclient/index.js",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@serve.zone/api": "32.
|
|
54
|
-
"@serve.zone/interfaces": "32.
|
|
53
|
+
"@serve.zone/api": "32.5.0",
|
|
54
|
+
"@serve.zone/interfaces": "32.42.0",
|
|
55
55
|
"@push.rocks/projectinfo": "^5.1.0",
|
|
56
56
|
"@push.rocks/qenv": "^8.1.0",
|
|
57
57
|
"@push.rocks/smartfs": "^1.8.0",
|
package/readme.md
CHANGED
|
@@ -17,6 +17,7 @@ This submodule is intentionally small in the current codebase:
|
|
|
17
17
|
- Without arguments, calls `CliClient.getClusters()` and prints the result.
|
|
18
18
|
- Runs `servezone gateway bootstrap` and `servezone gateway status` for Cloudly's dcrouter gateway enrollment.
|
|
19
19
|
- Runs `servezone relay export-authorization` to write a cluster relay's bearer to a private file.
|
|
20
|
+
- Runs `servezone corestore set-control-token` and `servezone corestore control-token-state` for the Corestore control token the cluster relays use.
|
|
20
21
|
|
|
21
22
|
It is not currently a full command tree for services, secrets, deployments, logs, profiles, or shell completion. Those flows should be implemented against `@serve.zone/api` before documenting them as CLI commands.
|
|
22
23
|
|
|
@@ -91,6 +92,26 @@ servezone relay export-authorization --cluster-id <clusterId> --expected-generat
|
|
|
91
92
|
- The bearer is fetched with `@serve.zone/api` (`cluster.exportClusterRelayAuthorization`, which checks the answer against the request) and written with `@push.rocks/smartfs` as a new file, mode `0600` whatever the umask, exclusive and atomic: the whole bearer or no file, never over an existing entry and never through a symbolic link at the path.
|
|
92
93
|
- It is never written to stdout. stdout carries one JSON line of metadata: `clusterId`, `generation`, `outputPath`, `byteLength`, `sha256` (what `sha256sum` prints for the file) and `receiptId`, the audit receipt Cloudly recorded. A refusal is printed to stderr with exit code 1 and never carries the bearer.
|
|
93
94
|
|
|
95
|
+
## Corestore Control Token
|
|
96
|
+
|
|
97
|
+
Every cluster relay calls its nodes' Corestore control API with one control token, which Cloudly stores for both Corestore provider configs. These commands set and read it over the API, without any access to Cloudly's process. Both sign in exactly as the default action does; the set needs an administrator who signed in within the last five minutes.
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
CLOUDLY_URL=https://cloudly.example.com \
|
|
101
|
+
CLOUDLY_USERNAME=admin CLOUDLY_PASSWORD=change-me \
|
|
102
|
+
servezone corestore control-token-state
|
|
103
|
+
|
|
104
|
+
# Pipe the token in, naming the state the read answered:
|
|
105
|
+
servezone corestore set-control-token --stdin --expect absent < control-token.txt
|
|
106
|
+
servezone corestore set-control-token --stdin --expect 3 < control-token.txt
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
- `control-token-state` prints one JSON line: `{"ok":true,"command":"corestore control-token-state","state":…,"receipts":[…]}`. `state` is `{"state":"absent"}` or `{"state":"present","revision":<n>}`; `receipts` are the value-free audit receipts (`id`, `revision`, `via`, `actorId`, `setAt`), oldest first.
|
|
110
|
+
- `set-control-token --stdin --expect absent|<revision>` stores the token only while it is still exactly at the state `--expect` names: `absent`, or the revision `control-token-state` answered (a positive integer without leading zeros). It is never a minimum, so a set that committed in between is refused rather than overwritten.
|
|
111
|
+
- The token is read from stdin and from nowhere else: `--stdin` names that, and an argument, `--token`, or any other extra argument is refused as usage without echoing it; the environment is never read for it. stdin must not be a terminal. One trailing line break (`\n` or `\r\n`) is stripped; what remains must pass the contract's `isCorestoreControlToken` (32 to 4096 bytes of UTF-8 without whitespace). A refused stdin is named (`token-input-terminal`, `token-input-oversized`, `token-input-invalid`) before anything connects, and every byte read is wiped.
|
|
112
|
+
- The token is sent once with `@serve.zone/api` (`platform.setCorestoreControlToken`, past every TypedRequest hook), and the answer is checked against the request. stdout carries one JSON line with the state the set produced and its receipt, never the token: `{"ok":true,"command":"corestore set-control-token","state":{"state":"present","revision":<n>},"receipt":{…}}`.
|
|
113
|
+
- A refusal is printed to stderr with exit code 1 as `The Corestore control token was not stored: <refusal>.`, naming one of the contract's `corestoreControlTokenSetRefusals`: `token-present` (`--expect absent`, and a token is stored), `token-absent` (a revision was expected, and none is stored), `token-revision-mismatch` (the stored token is at another revision), `token-invalid` or `provider-unavailable` (a Corestore provider config is missing or disabled). Every other failure, an unauthorized or stale administrator included, is Cloudly's one denial text, `Corestore control token request denied.`, with exit code 1.
|
|
114
|
+
|
|
94
115
|
## Programmatic Use
|
|
95
116
|
|
|
96
117
|
The published submodule exports the `runCli()` entry point. For automation, most callers should use `@serve.zone/api` directly; the internal `CliClient` currently wraps `CloudlyApiClient` only to run the default cluster-list action:
|
|
@@ -116,9 +137,10 @@ await cli.getClusters();
|
|
|
116
137
|
| Path | Purpose |
|
|
117
138
|
| --- | --- |
|
|
118
139
|
| `index.ts` | Runtime entry point for the published CLI. |
|
|
119
|
-
| `classes.cliclient.ts` | Minimal client wrapper: `getClusters()`, `bootstrapGateway()`, `getGatewayStatus()` and `
|
|
140
|
+
| `classes.cliclient.ts` | Minimal client wrapper: `getClusters()`, `bootstrapGateway()`, `getGatewayStatus()`, `exportRelayAuthorization()`, `setCorestoreControlToken()` and `getCorestoreControlTokenState()`. |
|
|
120
141
|
| `commands.ts` | Parses the argument vector into one command, refusing anything else as usage. |
|
|
121
|
-
| `tokeninput.ts` | Reads the dcrouter admin token from stdin. |
|
|
142
|
+
| `tokeninput.ts` | Reads the dcrouter admin token and the Corestore control token from stdin, each bounded and checked by its contract. |
|
|
143
|
+
| `corestorecontroltoken.ts` | Sets and reads the Corestore control token through the API client and names the contract's refusals. |
|
|
122
144
|
| `relayauthorization.ts` | Checks the private output directory and writes a cluster relay's bearer as a new 0600 file. |
|
|
123
145
|
| `plugins.ts` | Centralized imports for the submodule. |
|
|
124
146
|
| `tspublish.json` | Published package name, dependencies, `servezone` bin metadata, and `useBase`, which takes the registries from the release's npm target. |
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import { CloudlyApiClient } from '@serve.zone/api';
|
|
3
3
|
import { exportRelayAuthorizationToFile } from './relayauthorization.js';
|
|
4
|
+
import { getCorestoreControlTokenState, setCorestoreControlToken } from './corestorecontroltoken.js';
|
|
4
5
|
|
|
5
6
|
export class CliClient {
|
|
6
7
|
public cloudlyApiClient: CloudlyApiClient;
|
|
@@ -42,6 +43,23 @@ export class CliClient {
|
|
|
42
43
|
console.log(JSON.stringify({ ok: true, command: 'relay export-authorization', ...output }));
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Stores the Corestore control token while it is still exactly at `expectedArg` and prints the
|
|
48
|
+
* state and the receipt the set produced, never the token. The token is the caller's to wipe.
|
|
49
|
+
*/
|
|
50
|
+
public async setCorestoreControlToken(
|
|
51
|
+
expectedArg: plugins.servezoneInterfaces.data.TCorestoreControlTokenState,
|
|
52
|
+
tokenArg: Uint8Array,
|
|
53
|
+
) {
|
|
54
|
+
const output = await setCorestoreControlToken(this.cloudlyApiClient, expectedArg, tokenArg);
|
|
55
|
+
console.log(JSON.stringify(output));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Prints whether Cloudly holds the Corestore control token, its revision and its receipts. */
|
|
59
|
+
public async getCorestoreControlTokenState() {
|
|
60
|
+
console.log(JSON.stringify(await getCorestoreControlTokenState(this.cloudlyApiClient)));
|
|
61
|
+
}
|
|
62
|
+
|
|
45
63
|
public async getGatewayStatus() {
|
|
46
64
|
const { state } = await this.cloudlyApiClient.settings.getExternalGatewayEnrollmentState();
|
|
47
65
|
console.log(JSON.stringify(state, null, 2));
|
package/ts_cliclient/commands.ts
CHANGED
|
@@ -11,7 +11,13 @@ export type TCliCommand =
|
|
|
11
11
|
request: plugins.servezoneInterfaces.data.IClusterRelayAuthorizationExportRequest;
|
|
12
12
|
/** A normalized absolute path in a private directory; the file must not exist yet. */
|
|
13
13
|
outputPath: string;
|
|
14
|
-
}
|
|
14
|
+
}
|
|
15
|
+
| {
|
|
16
|
+
kind: 'corestore-set-control-token';
|
|
17
|
+
/** Exactly the state the administrator last read; never a minimum. */
|
|
18
|
+
expected: plugins.servezoneInterfaces.data.TCorestoreControlTokenState;
|
|
19
|
+
}
|
|
20
|
+
| { kind: 'corestore-control-token-state' };
|
|
15
21
|
|
|
16
22
|
export const cliUsage = [
|
|
17
23
|
'Usage:',
|
|
@@ -21,6 +27,9 @@ export const cliUsage = [
|
|
|
21
27
|
' The dcrouter admin token is read from stdin, never from arguments or the environment.',
|
|
22
28
|
' servezone relay export-authorization --cluster-id <id> --expected-generation <n> --output <absolute path>',
|
|
23
29
|
' Writes the relay bearer to a new 0600 file in a directory only you can write; never to stdout.',
|
|
30
|
+
' servezone corestore set-control-token --stdin --expect absent|<revision>',
|
|
31
|
+
' The Corestore control token is read from stdin, never from arguments or the environment.',
|
|
32
|
+
' servezone corestore control-token-state',
|
|
24
33
|
].join('\n');
|
|
25
34
|
|
|
26
35
|
/**
|
|
@@ -60,6 +69,7 @@ export const parseCliCommand = (argsArg: readonly string[]): TCliCommand => {
|
|
|
60
69
|
if (argsArg.length === 0) return { kind: 'clusters' };
|
|
61
70
|
const [group, action, ...rest] = argsArg;
|
|
62
71
|
if (group === 'relay') return parseRelayCommand(action, rest);
|
|
72
|
+
if (group === 'corestore') return parseCorestoreCommand(action, rest);
|
|
63
73
|
requireUsage(group === 'gateway');
|
|
64
74
|
if (action === 'status') {
|
|
65
75
|
requireUsage(rest.length === 0);
|
|
@@ -73,18 +83,50 @@ export const parseCliCommand = (argsArg: readonly string[]): TCliCommand => {
|
|
|
73
83
|
};
|
|
74
84
|
};
|
|
75
85
|
|
|
86
|
+
/** A positive integer without leading zeros, as a generation or a revision is named. */
|
|
87
|
+
const parseCounter = (valueArg: string): number => {
|
|
88
|
+
requireUsage(/^[1-9][0-9]{0,15}$/.test(valueArg) && Number.isSafeInteger(Number(valueArg)));
|
|
89
|
+
return Number(valueArg);
|
|
90
|
+
};
|
|
91
|
+
|
|
76
92
|
/** `relay export-authorization`: the cluster, the exact generation and a normalized absolute output. */
|
|
77
93
|
const parseRelayCommand = (actionArg: string | undefined, restArg: readonly string[]): TCliCommand => {
|
|
78
94
|
requireUsage(actionArg === 'export-authorization');
|
|
79
95
|
const options = parseOptions(restArg, ['--cluster-id', '--expected-generation', '--output']);
|
|
80
|
-
const generation = options['--expected-generation'];
|
|
81
|
-
requireUsage(/^[1-9][0-9]{0,15}$/.test(generation) && Number.isSafeInteger(Number(generation)));
|
|
96
|
+
const generation = parseCounter(options['--expected-generation']);
|
|
82
97
|
const outputPath = options['--output'];
|
|
83
98
|
requireUsage(plugins.path.isAbsolute(outputPath) && plugins.path.normalize(outputPath) === outputPath
|
|
84
99
|
&& !outputPath.endsWith(plugins.path.sep));
|
|
85
100
|
return {
|
|
86
101
|
kind: 'relay-export-authorization',
|
|
87
|
-
request: { clusterId: options['--cluster-id'], expectedGeneration:
|
|
102
|
+
request: { clusterId: options['--cluster-id'], expectedGeneration: generation },
|
|
88
103
|
outputPath,
|
|
89
104
|
};
|
|
90
105
|
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* `corestore set-control-token --stdin --expect absent|<revision>` and
|
|
109
|
+
* `corestore control-token-state`.
|
|
110
|
+
*
|
|
111
|
+
* `--stdin` names where the token comes from, so no invocation reads it from anywhere else, and
|
|
112
|
+
* `--expect` is the state `control-token-state` answered: `absent`, or the stored revision.
|
|
113
|
+
*/
|
|
114
|
+
const parseCorestoreCommand = (actionArg: string | undefined, restArg: readonly string[]): TCliCommand => {
|
|
115
|
+
if (actionArg === 'control-token-state') {
|
|
116
|
+
requireUsage(restArg.length === 0);
|
|
117
|
+
return { kind: 'corestore-control-token-state' };
|
|
118
|
+
}
|
|
119
|
+
requireUsage(actionArg === 'set-control-token');
|
|
120
|
+
// `--stdin` is a flag in an option's place, either before or after `--expect <value>`.
|
|
121
|
+
requireUsage(restArg.length === 3);
|
|
122
|
+
const stdinIndex = restArg[0] === '--stdin' ? 0 : 2;
|
|
123
|
+
requireUsage(restArg[stdinIndex] === '--stdin');
|
|
124
|
+
const options = parseOptions(restArg.filter((_argArg, indexArg) => indexArg !== stdinIndex), ['--expect']);
|
|
125
|
+
const expected = options['--expect'];
|
|
126
|
+
return {
|
|
127
|
+
kind: 'corestore-set-control-token',
|
|
128
|
+
expected: expected === 'absent'
|
|
129
|
+
? { state: 'absent' }
|
|
130
|
+
: { state: 'present', revision: parseCounter(expected) },
|
|
131
|
+
};
|
|
132
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import { decodeCorestoreControlToken } from './tokeninput.js';
|
|
3
|
+
|
|
4
|
+
type TCorestoreControlTokenSetRefusal = plugins.servezoneInterfaces.data.TCorestoreControlTokenSetRefusal;
|
|
5
|
+
|
|
6
|
+
/** Cloudly did not store the token, for one of the contract's refusals, which it names. */
|
|
7
|
+
export class CorestoreControlTokenSetRefusedError extends Error {
|
|
8
|
+
constructor(public readonly refusal: TCorestoreControlTokenSetRefusal) {
|
|
9
|
+
super(`The Corestore control token was not stored: ${refusal}.`);
|
|
10
|
+
this.name = 'CorestoreControlTokenSetRefusedError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The contract refusal an error is, or `undefined` for every other error.
|
|
16
|
+
*
|
|
17
|
+
* Cloudly answers a refusal by its name and nothing else, so the name is the whole of the error's
|
|
18
|
+
* message; any other message, Cloudly's one denial text included, is not a refusal.
|
|
19
|
+
*/
|
|
20
|
+
export const nameCorestoreControlTokenSetRefusal = (
|
|
21
|
+
errorArg: unknown,
|
|
22
|
+
): TCorestoreControlTokenSetRefusal | undefined => {
|
|
23
|
+
if (!(errorArg instanceof Error)) return undefined;
|
|
24
|
+
return plugins.servezoneInterfaces.data.corestoreControlTokenSetRefusals
|
|
25
|
+
.find((refusalArg) => refusalArg === errorArg.message);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** What the CLI prints about a stored token: the state it produced and its receipt, never the token. */
|
|
29
|
+
export interface ICorestoreControlTokenSetOutput
|
|
30
|
+
extends plugins.servezoneInterfaces.data.ICorestoreControlTokenSetResult {
|
|
31
|
+
ok: true;
|
|
32
|
+
command: 'corestore set-control-token';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** What the CLI prints about the token's state: the state and its value-free receipts. */
|
|
36
|
+
export interface ICorestoreControlTokenStateOutput
|
|
37
|
+
extends plugins.servezoneInterfaces.data.ICorestoreControlTokenStateRead {
|
|
38
|
+
ok: true;
|
|
39
|
+
command: 'corestore control-token-state';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Stores the token through the API client while it is still exactly at `expected`, answering only
|
|
44
|
+
* the state and the receipt. The token bytes are the caller's to wipe; the text the request needs
|
|
45
|
+
* is decoded from them here and never printed, logged or put into an error.
|
|
46
|
+
*/
|
|
47
|
+
export const setCorestoreControlToken = async (
|
|
48
|
+
apiClientArg: plugins.servezoneApi.CloudlyApiClient,
|
|
49
|
+
expectedArg: plugins.servezoneInterfaces.data.TCorestoreControlTokenState,
|
|
50
|
+
tokenArg: Uint8Array,
|
|
51
|
+
): Promise<ICorestoreControlTokenSetOutput> => {
|
|
52
|
+
try {
|
|
53
|
+
const { state, receipt } = await apiClientArg.platform.setCorestoreControlToken({
|
|
54
|
+
token: decodeCorestoreControlToken(tokenArg),
|
|
55
|
+
expected: expectedArg,
|
|
56
|
+
});
|
|
57
|
+
return { ok: true, command: 'corestore set-control-token', state, receipt };
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const refusal = nameCorestoreControlTokenSetRefusal(error);
|
|
60
|
+
if (refusal !== undefined) throw new CorestoreControlTokenSetRefusedError(refusal);
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** Whether Cloudly holds the token, at which revision, and its receipts, oldest first. */
|
|
66
|
+
export const getCorestoreControlTokenState = async (
|
|
67
|
+
apiClientArg: plugins.servezoneApi.CloudlyApiClient,
|
|
68
|
+
): Promise<ICorestoreControlTokenStateOutput> => {
|
|
69
|
+
const { state, receipts } = await apiClientArg.platform.getCorestoreControlTokenState();
|
|
70
|
+
return { ok: true, command: 'corestore control-token-state', state, receipts };
|
|
71
|
+
};
|
package/ts_cliclient/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import { CliClient } from './classes.cliclient.js';
|
|
3
3
|
import { parseCliCommand, type TCliCommand } from './commands.js';
|
|
4
|
-
import { readBootstrapTokenInput } from './tokeninput.js';
|
|
4
|
+
import { readBootstrapTokenInput, readCorestoreControlTokenInput } from './tokeninput.js';
|
|
5
5
|
import { requirePrivateOutputDirectory } from './relayauthorization.js';
|
|
6
6
|
|
|
7
7
|
export { CliUsageError, parseCliCommand, type TCliCommand } from './commands.js';
|
|
@@ -15,24 +15,48 @@ export {
|
|
|
15
15
|
} from './relayauthorization.js';
|
|
16
16
|
export {
|
|
17
17
|
BootstrapTokenInputError,
|
|
18
|
+
CorestoreControlTokenInputError,
|
|
19
|
+
TokenInputError,
|
|
18
20
|
readBootstrapTokenInput,
|
|
21
|
+
readCorestoreControlTokenInput,
|
|
19
22
|
type TBootstrapTokenInputRefusal,
|
|
23
|
+
type TTokenInputRefusal,
|
|
20
24
|
} from './tokeninput.js';
|
|
25
|
+
export {
|
|
26
|
+
CorestoreControlTokenSetRefusedError,
|
|
27
|
+
getCorestoreControlTokenState,
|
|
28
|
+
nameCorestoreControlTokenSetRefusal,
|
|
29
|
+
setCorestoreControlToken,
|
|
30
|
+
type ICorestoreControlTokenSetOutput,
|
|
31
|
+
type ICorestoreControlTokenStateOutput,
|
|
32
|
+
} from './corestorecontroltoken.js';
|
|
21
33
|
|
|
22
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* A parsed command with everything it reads locally: a bootstrap carries the admin token from
|
|
36
|
+
* stdin, a control token set the Corestore control token.
|
|
37
|
+
*/
|
|
23
38
|
type TCliInvocation =
|
|
24
|
-
| Exclude<TCliCommand, { kind: 'gateway-bootstrap' }>
|
|
25
|
-
| (Extract<TCliCommand, { kind: 'gateway-bootstrap' }> & { bootstrapToken: Uint8Array })
|
|
39
|
+
| Exclude<TCliCommand, { kind: 'gateway-bootstrap' | 'corestore-set-control-token' }>
|
|
40
|
+
| (Extract<TCliCommand, { kind: 'gateway-bootstrap' }> & { bootstrapToken: Uint8Array })
|
|
41
|
+
| (Extract<TCliCommand, { kind: 'corestore-set-control-token' }> & { controlToken: Uint8Array });
|
|
42
|
+
|
|
43
|
+
/** Reads the token the command needs from stdin, before anything connects. */
|
|
44
|
+
const readInvocation = async (commandArg: TCliCommand): Promise<TCliInvocation> => {
|
|
45
|
+
if (commandArg.kind === 'gateway-bootstrap') {
|
|
46
|
+
return { ...commandArg, bootstrapToken: await readBootstrapTokenInput(process.stdin) };
|
|
47
|
+
}
|
|
48
|
+
if (commandArg.kind === 'corestore-set-control-token') {
|
|
49
|
+
return { ...commandArg, controlToken: await readCorestoreControlTokenInput(process.stdin) };
|
|
50
|
+
}
|
|
51
|
+
return commandArg;
|
|
52
|
+
};
|
|
26
53
|
|
|
27
54
|
export const runCli = async () => {
|
|
28
55
|
// The command and the token are settled before anything connects, so a wrong invocation or a
|
|
29
56
|
// token that could never be one fails without reaching Cloudly.
|
|
30
57
|
let command: TCliInvocation;
|
|
31
58
|
try {
|
|
32
|
-
|
|
33
|
-
command = parsed.kind === 'gateway-bootstrap'
|
|
34
|
-
? { ...parsed, bootstrapToken: await readBootstrapTokenInput(process.stdin) }
|
|
35
|
-
: parsed;
|
|
59
|
+
command = await readInvocation(parseCliCommand(process.argv.slice(2)));
|
|
36
60
|
// An output the bearer could not be written to fetches no bearer and records no export.
|
|
37
61
|
if (command.kind === 'relay-export-authorization') {
|
|
38
62
|
await requirePrivateOutputDirectory(command.outputPath);
|
|
@@ -56,6 +80,10 @@ export const runCli = async () => {
|
|
|
56
80
|
await cliClient.bootstrapGateway(command.target, command.bootstrapToken);
|
|
57
81
|
} else if (command.kind === 'relay-export-authorization') {
|
|
58
82
|
await cliClient.exportRelayAuthorization(command.request, command.outputPath);
|
|
83
|
+
} else if (command.kind === 'corestore-set-control-token') {
|
|
84
|
+
await cliClient.setCorestoreControlToken(command.expected, command.controlToken);
|
|
85
|
+
} else if (command.kind === 'corestore-control-token-state') {
|
|
86
|
+
await cliClient.getCorestoreControlTokenState();
|
|
59
87
|
} else {
|
|
60
88
|
await cliClient.getGatewayStatus();
|
|
61
89
|
}
|
|
@@ -68,6 +96,7 @@ export const runCli = async () => {
|
|
|
68
96
|
}
|
|
69
97
|
} finally {
|
|
70
98
|
if (command.kind === 'gateway-bootstrap') command.bootstrapToken.fill(0);
|
|
99
|
+
if (command.kind === 'corestore-set-control-token') command.controlToken.fill(0);
|
|
71
100
|
}
|
|
72
101
|
};
|
|
73
102
|
|
package/ts_cliclient/plugins.ts
CHANGED
|
@@ -12,10 +12,12 @@ export {
|
|
|
12
12
|
// @serve.zone scope
|
|
13
13
|
import * as servezoneApi from '@serve.zone/api';
|
|
14
14
|
import * as servezoneInterfaces from '@serve.zone/interfaces';
|
|
15
|
+
import * as servezoneInterfacesRuntime from '@serve.zone/interfaces/runtime';
|
|
15
16
|
|
|
16
17
|
export {
|
|
17
18
|
servezoneApi,
|
|
18
|
-
servezoneInterfaces
|
|
19
|
+
servezoneInterfaces,
|
|
20
|
+
servezoneInterfacesRuntime,
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
// @push.rocks scope
|
package/ts_cliclient/readme.md
CHANGED
|
@@ -17,6 +17,7 @@ This submodule is intentionally small in the current codebase:
|
|
|
17
17
|
- Without arguments, calls `CliClient.getClusters()` and prints the result.
|
|
18
18
|
- Runs `servezone gateway bootstrap` and `servezone gateway status` for Cloudly's dcrouter gateway enrollment.
|
|
19
19
|
- Runs `servezone relay export-authorization` to write a cluster relay's bearer to a private file.
|
|
20
|
+
- Runs `servezone corestore set-control-token` and `servezone corestore control-token-state` for the Corestore control token the cluster relays use.
|
|
20
21
|
|
|
21
22
|
It is not currently a full command tree for services, secrets, deployments, logs, profiles, or shell completion. Those flows should be implemented against `@serve.zone/api` before documenting them as CLI commands.
|
|
22
23
|
|
|
@@ -91,6 +92,26 @@ servezone relay export-authorization --cluster-id <clusterId> --expected-generat
|
|
|
91
92
|
- The bearer is fetched with `@serve.zone/api` (`cluster.exportClusterRelayAuthorization`, which checks the answer against the request) and written with `@push.rocks/smartfs` as a new file, mode `0600` whatever the umask, exclusive and atomic: the whole bearer or no file, never over an existing entry and never through a symbolic link at the path.
|
|
92
93
|
- It is never written to stdout. stdout carries one JSON line of metadata: `clusterId`, `generation`, `outputPath`, `byteLength`, `sha256` (what `sha256sum` prints for the file) and `receiptId`, the audit receipt Cloudly recorded. A refusal is printed to stderr with exit code 1 and never carries the bearer.
|
|
93
94
|
|
|
95
|
+
## Corestore Control Token
|
|
96
|
+
|
|
97
|
+
Every cluster relay calls its nodes' Corestore control API with one control token, which Cloudly stores for both Corestore provider configs. These commands set and read it over the API, without any access to Cloudly's process. Both sign in exactly as the default action does; the set needs an administrator who signed in within the last five minutes.
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
CLOUDLY_URL=https://cloudly.example.com \
|
|
101
|
+
CLOUDLY_USERNAME=admin CLOUDLY_PASSWORD=change-me \
|
|
102
|
+
servezone corestore control-token-state
|
|
103
|
+
|
|
104
|
+
# Pipe the token in, naming the state the read answered:
|
|
105
|
+
servezone corestore set-control-token --stdin --expect absent < control-token.txt
|
|
106
|
+
servezone corestore set-control-token --stdin --expect 3 < control-token.txt
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
- `control-token-state` prints one JSON line: `{"ok":true,"command":"corestore control-token-state","state":…,"receipts":[…]}`. `state` is `{"state":"absent"}` or `{"state":"present","revision":<n>}`; `receipts` are the value-free audit receipts (`id`, `revision`, `via`, `actorId`, `setAt`), oldest first.
|
|
110
|
+
- `set-control-token --stdin --expect absent|<revision>` stores the token only while it is still exactly at the state `--expect` names: `absent`, or the revision `control-token-state` answered (a positive integer without leading zeros). It is never a minimum, so a set that committed in between is refused rather than overwritten.
|
|
111
|
+
- The token is read from stdin and from nowhere else: `--stdin` names that, and an argument, `--token`, or any other extra argument is refused as usage without echoing it; the environment is never read for it. stdin must not be a terminal. One trailing line break (`\n` or `\r\n`) is stripped; what remains must pass the contract's `isCorestoreControlToken` (32 to 4096 bytes of UTF-8 without whitespace). A refused stdin is named (`token-input-terminal`, `token-input-oversized`, `token-input-invalid`) before anything connects, and every byte read is wiped.
|
|
112
|
+
- The token is sent once with `@serve.zone/api` (`platform.setCorestoreControlToken`, past every TypedRequest hook), and the answer is checked against the request. stdout carries one JSON line with the state the set produced and its receipt, never the token: `{"ok":true,"command":"corestore set-control-token","state":{"state":"present","revision":<n>},"receipt":{…}}`.
|
|
113
|
+
- A refusal is printed to stderr with exit code 1 as `The Corestore control token was not stored: <refusal>.`, naming one of the contract's `corestoreControlTokenSetRefusals`: `token-present` (`--expect absent`, and a token is stored), `token-absent` (a revision was expected, and none is stored), `token-revision-mismatch` (the stored token is at another revision), `token-invalid` or `provider-unavailable` (a Corestore provider config is missing or disabled). Every other failure, an unauthorized or stale administrator included, is Cloudly's one denial text, `Corestore control token request denied.`, with exit code 1.
|
|
114
|
+
|
|
94
115
|
## Programmatic Use
|
|
95
116
|
|
|
96
117
|
The published submodule exports the `runCli()` entry point. For automation, most callers should use `@serve.zone/api` directly; the internal `CliClient` currently wraps `CloudlyApiClient` only to run the default cluster-list action:
|
|
@@ -116,9 +137,10 @@ await cli.getClusters();
|
|
|
116
137
|
| Path | Purpose |
|
|
117
138
|
| --- | --- |
|
|
118
139
|
| `index.ts` | Runtime entry point for the published CLI. |
|
|
119
|
-
| `classes.cliclient.ts` | Minimal client wrapper: `getClusters()`, `bootstrapGateway()`, `getGatewayStatus()` and `
|
|
140
|
+
| `classes.cliclient.ts` | Minimal client wrapper: `getClusters()`, `bootstrapGateway()`, `getGatewayStatus()`, `exportRelayAuthorization()`, `setCorestoreControlToken()` and `getCorestoreControlTokenState()`. |
|
|
120
141
|
| `commands.ts` | Parses the argument vector into one command, refusing anything else as usage. |
|
|
121
|
-
| `tokeninput.ts` | Reads the dcrouter admin token from stdin. |
|
|
142
|
+
| `tokeninput.ts` | Reads the dcrouter admin token and the Corestore control token from stdin, each bounded and checked by its contract. |
|
|
143
|
+
| `corestorecontroltoken.ts` | Sets and reads the Corestore control token through the API client and names the contract's refusals. |
|
|
122
144
|
| `relayauthorization.ts` | Checks the private output directory and writes a cluster relay's bearer as a new 0600 file. |
|
|
123
145
|
| `plugins.ts` | Centralized imports for the submodule. |
|
|
124
146
|
| `tspublish.json` | Published package name, dependencies, `servezone` bin metadata, and `useBase`, which takes the registries from the release's npm target. |
|
|
@@ -1,39 +1,64 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
|
|
3
|
-
/** Why
|
|
4
|
-
export type
|
|
3
|
+
/** Why a token piped in was not taken, named without quoting any of it. */
|
|
4
|
+
export type TTokenInputRefusal =
|
|
5
5
|
/** stdin is a terminal, which would echo a token typed into it; pipe the token in instead. */
|
|
6
6
|
| 'token-input-terminal'
|
|
7
7
|
/** stdin carried more bytes than a token and one line break. */
|
|
8
8
|
| 'token-input-oversized'
|
|
9
|
-
/** What stdin carried is not a token: empty, or a byte
|
|
9
|
+
/** What stdin carried is not a token by the token's own rule: empty, or a byte the rule refuses. */
|
|
10
10
|
| 'token-input-invalid';
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
/** Why the dcrouter admin token piped into `gateway bootstrap` was not taken. */
|
|
13
|
+
export type TBootstrapTokenInputRefusal = TTokenInputRefusal;
|
|
14
|
+
|
|
15
|
+
/** A refused token input: which token, and why, never a byte of what was read. */
|
|
16
|
+
export class TokenInputError extends Error {
|
|
17
|
+
constructor(public readonly refusal: TTokenInputRefusal, subjectArg: string) {
|
|
18
|
+
super(`${subjectArg} on stdin was refused: ${refusal}.`);
|
|
19
|
+
this.name = 'TokenInputError';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class BootstrapTokenInputError extends TokenInputError {
|
|
24
|
+
constructor(refusalArg: TTokenInputRefusal) {
|
|
25
|
+
super(refusalArg, 'The dcrouter admin token');
|
|
15
26
|
this.name = 'BootstrapTokenInputError';
|
|
16
27
|
}
|
|
17
28
|
}
|
|
18
29
|
|
|
19
|
-
|
|
20
|
-
|
|
30
|
+
export class CorestoreControlTokenInputError extends TokenInputError {
|
|
31
|
+
constructor(refusalArg: TTokenInputRefusal) {
|
|
32
|
+
super(refusalArg, 'The Corestore control token');
|
|
33
|
+
this.name = 'CorestoreControlTokenInputError';
|
|
34
|
+
}
|
|
21
35
|
}
|
|
22
36
|
|
|
23
|
-
/** What
|
|
24
|
-
export type
|
|
37
|
+
/** What a token is read from: the command's stdin, or a stream a test hands in. */
|
|
38
|
+
export type TTokenInput = AsyncIterable<Uint8Array> & { isTTY?: boolean };
|
|
39
|
+
export type TBootstrapTokenInput = TTokenInput;
|
|
40
|
+
|
|
41
|
+
/** One token's rule: its contract's bound and check, and the error its refusals are named with. */
|
|
42
|
+
interface ITokenInputRule {
|
|
43
|
+
/** The most bytes the token itself may have; one line break more is read and stripped. */
|
|
44
|
+
maximumTokenBytes: number;
|
|
45
|
+
/** The contract's check of the token's bytes, line break stripped. */
|
|
46
|
+
isToken: (tokenArg: Uint8Array) => boolean;
|
|
47
|
+
refuse: (refusalArg: TTokenInputRefusal) => TokenInputError;
|
|
48
|
+
}
|
|
25
49
|
|
|
26
50
|
/**
|
|
27
|
-
*
|
|
51
|
+
* A token piped into a command, as bytes the caller wipes.
|
|
28
52
|
*
|
|
29
53
|
* stdin is the only source: an argument would sit in the process list and the shell history, and
|
|
30
54
|
* the environment leaks into child processes. One trailing line break, `\n` or `\r\n`, is what
|
|
31
|
-
* `echo` or a file adds and is not part of the token; everything else must pass the
|
|
32
|
-
*
|
|
55
|
+
* `echo` or a file adds and is not part of the token; everything else must pass the token's rule.
|
|
56
|
+
* Reading stops as soon as stdin carries more than the rule's bound and one line break. Every byte
|
|
57
|
+
* read is wiped before this answers, whatever it answers.
|
|
33
58
|
*/
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const maximumBytes =
|
|
59
|
+
const readTokenInput = async (inputArg: TTokenInput, ruleArg: ITokenInputRule): Promise<Uint8Array> => {
|
|
60
|
+
if (inputArg.isTTY === true) throw ruleArg.refuse('token-input-terminal');
|
|
61
|
+
const maximumBytes = ruleArg.maximumTokenBytes + 2;
|
|
37
62
|
const chunks: Uint8Array[] = [];
|
|
38
63
|
let input: Uint8Array | undefined;
|
|
39
64
|
try {
|
|
@@ -41,7 +66,7 @@ export const readBootstrapTokenInput = async (inputArg: TBootstrapTokenInput): P
|
|
|
41
66
|
for await (const chunk of inputArg) {
|
|
42
67
|
chunks.push(chunk);
|
|
43
68
|
byteLength += chunk.byteLength;
|
|
44
|
-
|
|
69
|
+
if (byteLength > maximumBytes) throw ruleArg.refuse('token-input-oversized');
|
|
45
70
|
}
|
|
46
71
|
input = Buffer.concat(chunks, byteLength);
|
|
47
72
|
let end = input.byteLength;
|
|
@@ -50,7 +75,7 @@ export const readBootstrapTokenInput = async (inputArg: TBootstrapTokenInput): P
|
|
|
50
75
|
if (end > 0 && input[end - 1] === 0x0d) end -= 1;
|
|
51
76
|
}
|
|
52
77
|
const token = input.subarray(0, end);
|
|
53
|
-
|
|
78
|
+
if (!ruleArg.isToken(token)) throw ruleArg.refuse('token-input-invalid');
|
|
54
79
|
// A copy, because the input and every view of it are wiped below.
|
|
55
80
|
return new Uint8Array(token);
|
|
56
81
|
} finally {
|
|
@@ -58,3 +83,41 @@ export const readBootstrapTokenInput = async (inputArg: TBootstrapTokenInput): P
|
|
|
58
83
|
for (const chunk of chunks) chunk.fill(0);
|
|
59
84
|
}
|
|
60
85
|
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The dcrouter admin token piped into `gateway bootstrap`: at most the bootstrap contract's
|
|
89
|
+
* `maximumTokenBytes` of visible ASCII.
|
|
90
|
+
*/
|
|
91
|
+
export const readBootstrapTokenInput = async (inputArg: TTokenInput): Promise<Uint8Array> =>
|
|
92
|
+
await readTokenInput(inputArg, {
|
|
93
|
+
maximumTokenBytes: plugins.servezoneInterfaces.data.externalGatewayBootstrapContract.maximumTokenBytes,
|
|
94
|
+
isToken: (tokenArg) => plugins.servezoneInterfaces.data.isExternalGatewayBootstrapToken(tokenArg),
|
|
95
|
+
refuse: (refusalArg) => new BootstrapTokenInputError(refusalArg),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The token as text, exactly as its bytes say: invalid UTF-8 throws, and a leading byte order mark
|
|
100
|
+
* is kept, so the contract judges and Cloudly receives every character that was piped in.
|
|
101
|
+
*/
|
|
102
|
+
export const decodeCorestoreControlToken = (tokenArg: Uint8Array): string =>
|
|
103
|
+
new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(tokenArg);
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The Corestore control token piped into `corestore set-control-token`: what the contract's
|
|
107
|
+
* `isCorestoreControlToken` accepts, so at most `corestoreCredentialRuntimeLimits
|
|
108
|
+
* .maximumControlTokenBytes` of UTF-8 without whitespace.
|
|
109
|
+
*/
|
|
110
|
+
export const readCorestoreControlTokenInput = async (inputArg: TTokenInput): Promise<Uint8Array> =>
|
|
111
|
+
await readTokenInput(inputArg, {
|
|
112
|
+
maximumTokenBytes: plugins.servezoneInterfacesRuntime.corestoreCredentialRuntimeLimits.maximumControlTokenBytes,
|
|
113
|
+
isToken: (tokenArg) => {
|
|
114
|
+
let token: string;
|
|
115
|
+
try {
|
|
116
|
+
token = decodeCorestoreControlToken(tokenArg);
|
|
117
|
+
} catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
return plugins.servezoneInterfacesRuntime.isCorestoreControlToken(token);
|
|
121
|
+
},
|
|
122
|
+
refuse: (refusalArg) => new CorestoreControlTokenInputError(refusalArg),
|
|
123
|
+
});
|