@mason-data/cli 0.8.0 → 0.9.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 +13 -4
- package/dist/src/api/postgres/catalog-worker.js +49 -2
- package/dist/src/api/postgres/catalog.js +166 -32
- package/dist/src/api/postgres/listener.js +11 -3
- package/dist/src/api/postgres/proxy.js +283 -0
- package/dist/src/api/postgres/schema.js +6 -0
- package/dist/src/api/postgres/server.js +505 -99
- package/dist/src/api/postgres/translate.js +47 -2
- package/dist/src/cli/background.js +18 -19
- package/dist/src/cli/bridge-stdin.js +101 -0
- package/dist/src/cli/command.js +169 -11
- package/dist/src/cli/commands.js +207 -51
- package/dist/src/cli/credentials.js +182 -0
- package/dist/src/cli/demo.js +2 -2
- package/dist/src/cli/deploy.js +574 -0
- package/dist/src/cli/dev.js +148 -27
- package/dist/src/cli/doctor.js +116 -33
- package/dist/src/cli/generated-files.js +143 -0
- package/dist/src/cli/guides.js +97 -6
- package/dist/src/cli/hosted.js +850 -0
- package/dist/src/cli/init.js +47 -11
- package/dist/src/cli/login.js +289 -0
- package/dist/src/cli/machine.js +1 -1
- package/dist/src/cli/main.js +8 -2
- package/dist/src/cli/mason.cjs +17 -0
- package/dist/src/cli/mcp.js +2 -2
- package/dist/src/cli/output-gate.js +176 -0
- package/dist/src/cli/page-share.js +96 -0
- package/dist/src/cli/pg-tls.js +1 -1
- package/dist/src/cli/pin.js +6 -2
- package/dist/src/cli/pool-size.cjs +85 -0
- package/dist/src/cli/ports.js +2 -0
- package/dist/src/cli/preflight.js +130 -39
- package/dist/src/cli/project.js +99 -0
- package/dist/src/cli/readiness.js +42 -6
- package/dist/src/cli/registry.js +12 -4
- package/dist/src/cli/replica-hosted.js +665 -0
- package/dist/src/cli/replica.js +76 -0
- package/dist/src/cli/roles.js +2 -1
- package/dist/src/cli/run-server.js +154 -7
- package/dist/src/cli/security.js +57 -10
- package/dist/src/cli/serve.js +63 -7
- package/dist/src/cli/share-run.js +51 -8
- package/dist/src/cli/share.js +3 -3
- package/dist/src/cli/source-edit.js +10 -4
- package/dist/src/cli/source-role.js +344 -0
- package/dist/src/cli/sources.js +30 -20
- package/dist/src/cli/style.js +2 -1
- package/dist/src/cli/supervise.js +15 -23
- package/dist/src/cli/token-hosted.js +337 -0
- package/dist/src/cli/ui-ssh.js +460 -0
- package/dist/src/cli/ui.js +115 -11
- package/dist/src/cli/user-hosted.js +299 -0
- package/dist/src/cli/user.js +126 -74
- package/dist/src/demo/seed.js +26 -0
- package/dist/src/duck/analyze.js +36 -20
- package/dist/src/duck/collation.js +266 -0
- package/dist/src/duck/constant-key.js +94 -0
- package/dist/src/duck/distinct-key.js +283 -26
- package/dist/src/duck/events.js +39 -8
- package/dist/src/duck/executor-worker.js +2 -2
- package/dist/src/duck/executor.js +6 -5
- package/dist/src/duck/explain.js +4 -0
- package/dist/src/duck/functions.js +448 -49
- package/dist/src/duck/gaps.js +34 -16
- package/dist/src/duck/generated-plan.js +1070 -0
- package/dist/src/duck/generated.js +906 -0
- package/dist/src/duck/grants.js +1 -1
- package/dist/src/duck/held-types.js +64 -0
- package/dist/src/duck/history.js +4 -0
- package/dist/src/duck/jsonb-order.js +9 -12
- package/dist/src/duck/kept-route.js +6 -5
- package/dist/src/duck/kept.js +9 -7
- package/dist/src/duck/late.js +1469 -40
- package/dist/src/duck/libc-bound.js +341 -0
- package/dist/src/duck/libc-order.js +892 -126
- package/dist/src/duck/pg-eval-worker.js +7 -1
- package/dist/src/duck/pg-eval.js +6 -4
- package/dist/src/duck/pgtree.js +774 -23
- package/dist/src/duck/plan.js +275 -30
- package/dist/src/duck/publication.js +14 -0
- package/dist/src/duck/requests.js +9 -5
- package/dist/src/duck/resolved.js +117 -2
- package/dist/src/duck/rewrite.js +4567 -583
- package/dist/src/duck/row-filter.js +12 -6
- package/dist/src/duck/statement.js +19 -0
- package/dist/src/duck/store.js +294 -68
- package/dist/src/duck/stream.js +9 -5
- package/dist/src/duck/sync.js +602 -126
- package/dist/src/duck/token.js +4 -1
- package/dist/src/duck/types.js +63 -7
- package/dist/src/duck/usage.js +6 -4
- package/dist/src/duck/users.js +5 -0
- package/dist/src/duck/verified.js +159 -2
- package/dist/src/duck/views.js +4 -3
- package/dist/src/lib/database-url.js +55 -10
- package/dist/src/lib/instant.js +50 -0
- package/dist/src/lib/log-buffer.js +13 -4
- package/dist/src/lib/log-read.js +196 -0
- package/dist/src/lib/pg-numeric.js +387 -49
- package/dist/src/lib/secret-mask.js +142 -0
- package/dist/src/lib/table-sync.js +24 -9
- package/dist/src/web/app.js +9 -4
- package/dist/src/web/pages/status.js +178 -13
- package/dist/src/web/pages/syncs.js +1 -1
- package/dist/src/web/pages/tables.js +8 -3
- package/dist/src/web/pages/users.js +4 -3
- package/dist/src/web/shared.js +19 -3
- package/package.json +8 -7
|
@@ -27,6 +27,7 @@ import * as path from "node:path";
|
|
|
27
27
|
import { spawn, spawnSync } from "node:child_process";
|
|
28
28
|
import { fileURLToPath } from "node:url";
|
|
29
29
|
import { exitWhenWritten } from "../lib/exit-when-written.js";
|
|
30
|
+
import { LogReader } from "../lib/log-read.js";
|
|
30
31
|
export const READY_LINE = "mason start: ready";
|
|
31
32
|
export const RESTARTS_FILE = "restarts.json";
|
|
32
33
|
export const SUPERVISOR_FILE = "supervisor.json";
|
|
@@ -127,33 +128,24 @@ function stillRunning(home, note) {
|
|
|
127
128
|
}
|
|
128
129
|
}
|
|
129
130
|
/** What the run wrote to the log from `from` on: whether it said it was ready, and V8's fatal line if it printed one.
|
|
130
|
-
* Read after it exits, in chunks
|
|
131
|
+
* Read after it exits, in chunks, through the one reader (src/lib/log-read.ts: each line whole, masked): a run's log
|
|
132
|
+
* with every statement traced (MASON_PG_TRACE) runs to hundreds of MB. */
|
|
131
133
|
export function scanLog(file, from) {
|
|
132
134
|
let ready = false;
|
|
133
135
|
let fatal = null;
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
for (const l of lines) {
|
|
143
|
-
if (l === READY_LINE)
|
|
144
|
-
ready = true;
|
|
145
|
-
const f = /^FATAL ERROR: (.*)$/.exec(l.trim());
|
|
146
|
-
if (f)
|
|
147
|
-
fatal = f[1];
|
|
148
|
-
}
|
|
149
|
-
if (n === 0)
|
|
150
|
-
break;
|
|
151
|
-
at += n;
|
|
136
|
+
const reader = new LogReader(file, { from });
|
|
137
|
+
const scan = (text) => {
|
|
138
|
+
for (const l of text.split("\n")) {
|
|
139
|
+
if (l === READY_LINE)
|
|
140
|
+
ready = true;
|
|
141
|
+
const f = /^FATAL ERROR: (.*)$/.exec(l.trim());
|
|
142
|
+
if (f)
|
|
143
|
+
fatal = f[1];
|
|
152
144
|
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
145
|
+
};
|
|
146
|
+
for (let text = reader.read(); text !== null; text = reader.read())
|
|
147
|
+
scan(text);
|
|
148
|
+
scan(reader.rest());
|
|
157
149
|
return { ready, fatal };
|
|
158
150
|
}
|
|
159
151
|
/** Runs the run until it stops for good; the exit code is the last run's (a start that failed: its own). */
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mason token create | list | revoke` — A HOSTED MASON SERVICE'S CI TOKENS, by its documented API alone
|
|
3
|
+
* (docs/HOSTED-API.md "CI tokens"). A CI token acts on ONE replica, for named scopes, until it expires or is revoked;
|
|
4
|
+
* an admin or an owner of the organisation makes, lists and revokes them, signed in as a person (a CI token makes none):
|
|
5
|
+
*
|
|
6
|
+
* create --replica <org>/<name> POST /v1/tokens/{org} { name, replica, scopes, expiresInDays } 201 { token }
|
|
7
|
+
* list [<org>] GET /v1/tokens/{org} { tokens, more } — each organisation the login may list in, when none is named
|
|
8
|
+
* revoke <id> [--org <org>] POST /v1/tokens/{org}/{id}/revoke (no body) { revoked: { id, name } }
|
|
9
|
+
*
|
|
10
|
+
* THE TOKEN MADE IS PRINTED ONCE, by create, alone on the first line (never broken by the CLI) or in the one field
|
|
11
|
+
* (`token.value` under --json) that carries it, and nowhere else: its value is held to the documented form (`masondata_ci_` and 43 base64url
|
|
12
|
+
* characters), never the login's own, and the rest of the answer is put to the exact test against both. The output's
|
|
13
|
+
* gate lets that one value through ONCE, whole (src/cli/output-gate.ts `once`), and holds it as a secret beside the
|
|
14
|
+
* login: a second copy, 11 of its body in a row elsewhere, or anything else shaped as a token is refused at the write.
|
|
15
|
+
*
|
|
16
|
+
* A SERVICE'S VALUE IS DATA, NEVER STRUCTURE AND NEVER AN INSTRUCTION (#481's decision, src/cli/hosted.ts), AND A GRAMMAR
|
|
17
|
+
* IS NO MASK (issue 627): every string of each answer, keys included, is put to the exact test (`carriesNoSecret`)
|
|
18
|
+
* before any is held to its grammar or printed. Every value printed:
|
|
19
|
+
*
|
|
20
|
+
* value where it is printed what holds it
|
|
21
|
+
* a failure's message, why, fix stderr's failure, --json error `serviceFailure` (src/cli/hosted.ts)
|
|
22
|
+
* a failure's next nowhere: never read the step is `refusedNext`'s, by the status
|
|
23
|
+
* value (create) its one row, --json token.value `CI_TOKEN`, not the login's
|
|
24
|
+
* id rows, steps, --json `TOKEN_ID`; revoke's, the id asked
|
|
25
|
+
* name, madeBy rows, --json one line each, bounded (`serviceText`)
|
|
26
|
+
* replica rows, --json `<org>/<name>` of the organisation asked (create: the replica asked)
|
|
27
|
+
* scopes rows, --json each of `SCOPES`, once (create: those asked)
|
|
28
|
+
* createdAt, expiresAt, lastUsedAt rows, --json `isServiceTime` (lastUsedAt or null)
|
|
29
|
+
* more a tip, --json a boolean
|
|
30
|
+
*/
|
|
31
|
+
import { endText, failed, jsonText, parseCommand, reportText, serviceStyle, strict } from "./command.js";
|
|
32
|
+
import { call, carriesNoSecret, echoesSecret, isFailure, isServiceTime, serviceFailure, serviceOf, serviceText, unexpected } from "./hosted.js";
|
|
33
|
+
import { loginOrgs, orgNamed, REPLICA_PART, replicaOf, tokenFor } from "./replica.js";
|
|
34
|
+
import { refusedNext } from "./replica-hosted.js";
|
|
35
|
+
import { holdOutput } from "./output-gate.js";
|
|
36
|
+
import { didYouMean } from "../lib/did-you-mean.js";
|
|
37
|
+
import { err as styleErr, out as styleOut } from "./style.js";
|
|
38
|
+
/** What a CI token may be made for (docs/HOSTED-API.md): deploying the project, and the replica's users. */
|
|
39
|
+
export const SCOPES = ["deploy", "users"];
|
|
40
|
+
/** The documented form of a CI token: what create prints, once. */
|
|
41
|
+
const CI_TOKEN = /^masondata_ci_[A-Za-z0-9_-]{43}$/;
|
|
42
|
+
/** A token's id, as it goes into a path: letters, digits, `-` and `_`. */
|
|
43
|
+
export const TOKEN_ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
44
|
+
const DEFAULT_NAME = "ci";
|
|
45
|
+
const DEFAULT_DAYS = 90;
|
|
46
|
+
const MAX_DAYS = 365;
|
|
47
|
+
const MAX_TOKENS = 1000;
|
|
48
|
+
/** A list's answer is read up to this (a row a token). */
|
|
49
|
+
const MAX_LIST_BYTES = 1024 * 1024;
|
|
50
|
+
/** Who may make, list and revoke CI tokens: the roles of `GET /v1/me`. */
|
|
51
|
+
const MAKERS = ["admin", "owner"];
|
|
52
|
+
export const TOKEN = {
|
|
53
|
+
name: "token",
|
|
54
|
+
summary: "a hosted Mason service's CI tokens: make one for a replica (printed once), list them, revoke one",
|
|
55
|
+
usage: `usage: mason token create --replica <org>/<name> [--scope deploy|users …] [--name <name>] [--days <n>] [--json]
|
|
56
|
+
mason token list [<org>] [--json]
|
|
57
|
+
mason token revoke <id> [--org <org>] [--json]
|
|
58
|
+
|
|
59
|
+
A CI token lets CI act on ONE replica of a hosted Mason service, for the scopes it was made with (deploy: mason
|
|
60
|
+
deploy; users: mason user … --replica), until it expires or is revoked. CI reads it from MASON_TOKEN. An admin or
|
|
61
|
+
an owner of the organisation makes, lists and revokes them, signed in as a person (mason login): a CI token
|
|
62
|
+
makes none.
|
|
63
|
+
|
|
64
|
+
create makes one and prints it ONCE: the service does not keep it, and nothing shows it again, so store it at once
|
|
65
|
+
(as MASON_TOKEN in CI's secrets). It is alone on the first line printed, never broken, or, with --json, in the
|
|
66
|
+
one field token.value of the document, so a setup step can pipe it straight into a secret store. Nothing else
|
|
67
|
+
this command prints ever holds it.
|
|
68
|
+
|
|
69
|
+
list says the CI tokens of <org> that still act (never a token itself), or of every organisation this login is
|
|
70
|
+
an admin or an owner of when none is named; revoke ends one at once, by its id (list says it), in --org, else in
|
|
71
|
+
the one organisation this login is an admin or an owner of.
|
|
72
|
+
|
|
73
|
+
The login is MASON_TOKEN, else this machine's (mason login); the service is MASON_CLOUD_URL, else the default.
|
|
74
|
+
|
|
75
|
+
mason token create --replica acme/shop --name github-actions
|
|
76
|
+
mason token create --replica acme/shop --scope deploy --scope users --days 30 --json
|
|
77
|
+
mason token list acme
|
|
78
|
+
mason token revoke 3f1c9a2e --org acme
|
|
79
|
+
|
|
80
|
+
--replica <org>/<name> create: the replica it acts on (else MASON_REPLICA)
|
|
81
|
+
--scope <scope> create: what it may do, deploy or users; repeat it for both (default deploy)
|
|
82
|
+
--name <name> create: what the list calls it, 1 to 64 characters (default ${DEFAULT_NAME})
|
|
83
|
+
--days <n> create: the days it acts, 1 to ${MAX_DAYS} (default ${DEFAULT_DAYS})
|
|
84
|
+
--org <org> revoke: the organisation it is of
|
|
85
|
+
--json as data: { token (create: { id, value (the token itself, this once), name,
|
|
86
|
+
replica, scopes, createdAt, expiresAt }), tokens (list: [{ id, name, replica, scopes,
|
|
87
|
+
madeBy, createdAt, lastUsedAt (null: never used), expiresAt }]), more (list: true when an
|
|
88
|
+
organisation has more than the service lists), revoked (revoke: { org, id, name }), next }
|
|
89
|
+
|
|
90
|
+
Exit status: 0 done; 1 not signed in, refused by the service (its words printed as they came, the code the
|
|
91
|
+
CLI's own), or the service answered outside its contract; 2 a wrong command line.
|
|
92
|
+
`,
|
|
93
|
+
options: { replica: { type: "string" }, scope: { type: "string", multiple: true }, name: { type: "string" }, days: { type: "string" }, org: { type: "string" },
|
|
94
|
+
json: { type: "boolean", default: false } },
|
|
95
|
+
json: ["token", "tokens", "more", "revoked", "next"],
|
|
96
|
+
};
|
|
97
|
+
const outStyle = () => serviceStyle(styleOut());
|
|
98
|
+
const errStyle = () => serviceStyle(styleErr());
|
|
99
|
+
const out = (m) => { process.stdout.write(`${m}\n`); };
|
|
100
|
+
const fail = (f, json) => failed("token", f, { json, style: errStyle() });
|
|
101
|
+
const obj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
102
|
+
const MAY = "an admin or an owner";
|
|
103
|
+
/** Scopes as the service lists them: each one of `SCOPES`, once; null otherwise. In `SCOPES`' order. */
|
|
104
|
+
function scopesOf(v) {
|
|
105
|
+
if (!Array.isArray(v) || v.length < 1 || v.length > SCOPES.length || new Set(v).size !== v.length || !v.every((x) => SCOPES.includes(x)))
|
|
106
|
+
return null;
|
|
107
|
+
return SCOPES.filter((s) => v.includes(s));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* ONE CI TOKEN AS THE SERVICE SAYS IT, held: of the organisation asked (`org`), every value to its grammar, its words one
|
|
111
|
+
* line each; null when any does not hold. The answer around it has been put to the exact test.
|
|
112
|
+
*/
|
|
113
|
+
export function heldToken(v, org, secrets) {
|
|
114
|
+
if (!obj(v) || typeof v.id !== "string" || !TOKEN_ID.test(v.id) || typeof v.name !== "string" || v.name.length < 1 || v.name.length > 200)
|
|
115
|
+
return null;
|
|
116
|
+
const replica = typeof v.replica === "string" ? v.replica.split("/") : [];
|
|
117
|
+
if (replica.length !== 2 || replica[0] !== org || !REPLICA_PART.test(replica[1]))
|
|
118
|
+
return null;
|
|
119
|
+
const scopes = scopesOf(v.scopes);
|
|
120
|
+
if (scopes === null || !isServiceTime(v.createdAt) || !isServiceTime(v.expiresAt))
|
|
121
|
+
return null;
|
|
122
|
+
if (v.lastUsedAt !== undefined && v.lastUsedAt !== null && !isServiceTime(v.lastUsedAt))
|
|
123
|
+
return null;
|
|
124
|
+
if (v.madeBy !== undefined && v.madeBy !== null && (typeof v.madeBy !== "string" || v.madeBy.length > 320))
|
|
125
|
+
return null;
|
|
126
|
+
return { id: v.id, name: serviceText(v.name, 64, secrets), replica: `${org}/${replica[1]}`, scopes, madeBy: typeof v.madeBy === "string" ? serviceText(v.madeBy, 320, secrets) : null,
|
|
127
|
+
createdAt: v.createdAt, lastUsedAt: typeof v.lastUsedAt === "string" ? v.lastUsedAt : null, expiresAt: v.expiresAt };
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* THE TOKEN CREATE MADE (`201 { token }`): its value in the documented form and not the login's, the rest of the answer
|
|
131
|
+
* carrying neither (the exact test, against both: a copy of the value elsewhere is a second print), the replica and the
|
|
132
|
+
* scopes those asked. The token and its value, or null.
|
|
133
|
+
*/
|
|
134
|
+
export function heldMade(json, asked, login) {
|
|
135
|
+
if (!obj(json) || !obj(json.token))
|
|
136
|
+
return null;
|
|
137
|
+
const value = json.token.value;
|
|
138
|
+
// the value is shaped as a token by design: what it may not be is an echo of the login's
|
|
139
|
+
if (typeof value !== "string" || !CI_TOKEN.test(value) || login.includes(value) || echoesSecret(value, login))
|
|
140
|
+
return null;
|
|
141
|
+
const rest = { ...json, token: { ...json.token, value: null } };
|
|
142
|
+
if (!carriesNoSecret(rest, [...login, value]))
|
|
143
|
+
return null;
|
|
144
|
+
const token = heldToken(json.token, asked.org, [...login, value]);
|
|
145
|
+
if (token === null || token.replica !== `${asked.org}/${asked.name}` || token.scopes.join() !== SCOPES.filter((s) => asked.scopes.includes(s)).join())
|
|
146
|
+
return null;
|
|
147
|
+
return { token, value };
|
|
148
|
+
}
|
|
149
|
+
/** The service and the login, the output's gate holding the token; or the Failure. */
|
|
150
|
+
function signedIn(asked, again, json) {
|
|
151
|
+
const service = serviceOf();
|
|
152
|
+
if (isFailure(service))
|
|
153
|
+
return service;
|
|
154
|
+
const token = tokenFor(service, { asked, again });
|
|
155
|
+
if (isFailure(token))
|
|
156
|
+
return token;
|
|
157
|
+
holdOutput({ command: "token", json, secrets: [token] });
|
|
158
|
+
return { service, token };
|
|
159
|
+
}
|
|
160
|
+
const listStep = (org) => [`mason token list ${org}`, "the organisation's CI tokens"];
|
|
161
|
+
export async function token(argv) {
|
|
162
|
+
const f = parseCommand(TOKEN, argv);
|
|
163
|
+
if (typeof f === "number")
|
|
164
|
+
return f;
|
|
165
|
+
const json = f.values.json === true;
|
|
166
|
+
const usage = (message, fix) => fail({ message, code: "usage", ...(fix ? { fix } : {}), usage: TOKEN.usage }, json);
|
|
167
|
+
const [verb, arg, ...rest] = f.positionals;
|
|
168
|
+
const VERBS = ["create", "list", "revoke"];
|
|
169
|
+
if (verb === undefined)
|
|
170
|
+
return usage("create, list or revoke: mason token create --replica <org>/<name> makes one");
|
|
171
|
+
if (!VERBS.includes(verb))
|
|
172
|
+
return usage(`there is no mason token ${verb}: create, list or revoke${didYouMean(verb, VERBS)}`);
|
|
173
|
+
const extra = verb === "create" ? [arg, ...rest].filter((x) => x !== undefined) : rest;
|
|
174
|
+
if (extra.length)
|
|
175
|
+
return usage(`mason token ${verb} takes ${verb === "create" ? "no argument (the replica is --replica)" : verb === "list" ? "one organisation at most" : "one id"}: ${extra.join(" ")} is too many`);
|
|
176
|
+
for (const k of ["replica", "scope", "name", "days"])
|
|
177
|
+
if (verb !== "create" && f.values[k] !== undefined)
|
|
178
|
+
return usage(`--${k} goes with mason token create`);
|
|
179
|
+
if (verb !== "revoke" && f.values.org !== undefined)
|
|
180
|
+
return usage("--org goes with mason token revoke", verb === "list" ? "name the organisation: mason token list <org>" : "create names it: --replica <org>/<name>");
|
|
181
|
+
if (verb === "list") {
|
|
182
|
+
const org = arg === undefined ? null : orgNamed(arg, TOKEN.usage);
|
|
183
|
+
if (org !== null && isFailure(org))
|
|
184
|
+
return fail(org, json);
|
|
185
|
+
return list(org, json);
|
|
186
|
+
}
|
|
187
|
+
if (verb === "revoke") {
|
|
188
|
+
if (arg === undefined || !TOKEN_ID.test(arg))
|
|
189
|
+
return usage(arg === undefined ? "mason token revoke names the token's id: mason token revoke <id>" : `${JSON.stringify(serviceText(arg, 100))} is not a CI token's id`, "its id is what mason token list says first on its line, letters, digits, - and _ (never the token itself)");
|
|
190
|
+
const org = typeof f.values.org === "string" ? orgNamed(f.values.org, TOKEN.usage) : null;
|
|
191
|
+
if (org !== null && isFailure(org))
|
|
192
|
+
return fail(org, json);
|
|
193
|
+
return revoke(arg, org, json);
|
|
194
|
+
}
|
|
195
|
+
const r = replicaOf(typeof f.values.replica === "string" ? f.values.replica : undefined, TOKEN.usage);
|
|
196
|
+
if (r === null)
|
|
197
|
+
return usage("mason token create names the replica it acts on", "--replica <org>/<name>, or MASON_REPLICA=<org>/<name>");
|
|
198
|
+
if (isFailure(r))
|
|
199
|
+
return fail(r, json);
|
|
200
|
+
const asked = Array.isArray(f.values.scope) ? f.values.scope.filter((x) => typeof x === "string") : [];
|
|
201
|
+
for (const s of asked)
|
|
202
|
+
if (!SCOPES.includes(s))
|
|
203
|
+
return usage(`--scope ${JSON.stringify(serviceText(s, 100))} is not a scope${didYouMean(s, SCOPES)}`, `a CI token's scopes are ${SCOPES.join(" and ")}`);
|
|
204
|
+
if (new Set(asked).size !== asked.length)
|
|
205
|
+
return usage("a --scope is named twice", "name each scope once");
|
|
206
|
+
const scopes = (asked.length ? asked : ["deploy"]);
|
|
207
|
+
const name = typeof f.values.name === "string" ? f.values.name : DEFAULT_NAME;
|
|
208
|
+
if ([...name].length < 1 || [...name].length > 64 || /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(name) || name.trim() !== name)
|
|
209
|
+
return usage("--name is 1 to 64 characters, with no control character or space at either end", "--name github-actions");
|
|
210
|
+
const daysText = typeof f.values.days === "string" ? f.values.days : String(DEFAULT_DAYS);
|
|
211
|
+
if (!/^[1-9]\d{0,2}$/.test(daysText) || Number(daysText) > MAX_DAYS)
|
|
212
|
+
return usage(`--days ${JSON.stringify(serviceText(daysText, 100))} is not 1 to ${MAX_DAYS}`, "a CI token acts a whole number of days: --days 90");
|
|
213
|
+
return create({ org: r.org, name: r.name }, { label: name, scopes, days: Number(daysText) }, json);
|
|
214
|
+
}
|
|
215
|
+
/** What to run after the service refused: the CLI's own, by the status, naming the role that may. */
|
|
216
|
+
const refused = (a, org, again, secrets, service, step) => serviceFailure(a, secrets, (st) => st === 404 && again[0].startsWith("mason token revoke") ? [listStep(org)] : refusedNext(st, org, MAY, again)) ?? unexpected(service, a, step);
|
|
217
|
+
async function create(r, o, json) {
|
|
218
|
+
const again = [`mason token create --replica ${r.org}/${r.name}`, "make it again"];
|
|
219
|
+
const s = signedIn("a CI token is made", "mason token create", json);
|
|
220
|
+
if (isFailure(s))
|
|
221
|
+
return fail(s, json);
|
|
222
|
+
const { service, token: login } = s;
|
|
223
|
+
const a = await call(service, "POST", `/v1/tokens/${r.org}`, { token: login, json: { name: o.label, replica: r.name, scopes: o.scopes, expiresInDays: o.days } });
|
|
224
|
+
if (isFailure(a)) {
|
|
225
|
+
return fail({ ...a, why: `${a.why ?? "no answer"}; whether a token was made is not known, and it is never shown again`,
|
|
226
|
+
fix: `mason token list ${r.org} says the tokens that act: revoke any you did not see made, then make it again`, next: [listStep(r.org), again] }, json);
|
|
227
|
+
}
|
|
228
|
+
if (a.status !== 201)
|
|
229
|
+
return fail(refused(a, r.org, again, [login], service, "a CI token was made"), json);
|
|
230
|
+
const made = heldMade(a.json, { org: r.org, name: r.name, scopes: o.scopes }, [login]);
|
|
231
|
+
if (made === null) {
|
|
232
|
+
// the service answered 201: a token may have been made, whose value nobody has now been shown
|
|
233
|
+
const f = unexpected(service, a, "a CI token was made");
|
|
234
|
+
return fail({ ...f, why: `${f.why}; a token may have been made all the same, and it was not shown`,
|
|
235
|
+
fix: `mason token list ${r.org} says the tokens that act: revoke one you did not see made (mason token revoke <id> --org ${r.org}), then make it again`, next: [listStep(r.org)] }, json);
|
|
236
|
+
}
|
|
237
|
+
// the one print of the token: the gate lets this value through once, whole (src/cli/output-gate.ts)
|
|
238
|
+
holdOutput({ command: "token", json, secrets: [], once: made.value });
|
|
239
|
+
const t = made.token;
|
|
240
|
+
const next = [listStep(r.org), [`mason token revoke ${t.id} --org ${r.org}`, "end it"]];
|
|
241
|
+
if (json) {
|
|
242
|
+
out(jsonText(TOKEN, { token: { id: t.id, value: made.value, name: t.name, replica: t.replica, scopes: t.scopes, createdAt: t.createdAt, expiresAt: t.expiresAt } }, next));
|
|
243
|
+
return 0;
|
|
244
|
+
}
|
|
245
|
+
// THE TOKEN ON A LINE OF ITS OWN, FIRST, written alone and never broken by the CLI (PR 660's review, B2: a value to copy
|
|
246
|
+
// is no service text to break at the width — a line break inside it would be copied with it). A terminal narrower
|
|
247
|
+
// than it wraps it softly, which a copy joins. It cannot begin a line of anyone's making: held to `CI_TOKEN`, it has
|
|
248
|
+
// no space, no colon and no control character, so no piece of it is a step, a heading or a failure.
|
|
249
|
+
process.stdout.write(`${made.value}\n`);
|
|
250
|
+
out(reportText({ headline: `Made a CI token for ${t.replica}: the line above is the token, printed this once`,
|
|
251
|
+
details: [["id", t.id], ["name", t.name], ["scopes", t.scopes.join(", ")], ["expires", t.expiresAt]],
|
|
252
|
+
tips: ["Store it now, as MASON_TOKEN in your CI's secrets: the service does not keep it, and nothing shows it again.",
|
|
253
|
+
`It acts on ${t.replica} alone, for ${t.scopes.join(" and ")}, until it expires or is revoked.`], next }, outStyle()));
|
|
254
|
+
return 0;
|
|
255
|
+
}
|
|
256
|
+
async function list(org, json) {
|
|
257
|
+
const s = signedIn("the CI tokens are asked for", org === null ? "mason token list" : `mason token list ${org}`, json);
|
|
258
|
+
if (isFailure(s))
|
|
259
|
+
return fail(s, json);
|
|
260
|
+
const { service, token: login } = s, secrets = [login];
|
|
261
|
+
const orgs = org !== null ? [org] : await loginOrgs(service, login, "mason token list <org>", MAKERS);
|
|
262
|
+
if (isFailure(orgs))
|
|
263
|
+
return fail(orgs, json);
|
|
264
|
+
const slugs = orgs.map((o) => typeof o === "string" ? o : o.slug);
|
|
265
|
+
const tokens = [];
|
|
266
|
+
const more = [];
|
|
267
|
+
for (const slug of slugs) {
|
|
268
|
+
const a = await call(service, "GET", `/v1/tokens/${slug}`, { token: login, maxBytes: MAX_LIST_BYTES });
|
|
269
|
+
if (isFailure(a))
|
|
270
|
+
return fail(a, json);
|
|
271
|
+
if (a.status !== 200)
|
|
272
|
+
return fail(refused(a, slug, listStep(slug), secrets, service, "the CI tokens were asked for"), json);
|
|
273
|
+
const j = a.json;
|
|
274
|
+
if (!obj(j) || !Array.isArray(j.tokens) || j.tokens.length > MAX_TOKENS || typeof j.more !== "boolean" || !carriesNoSecret(j, secrets))
|
|
275
|
+
return fail(unexpected(service, a, "the CI tokens were asked for"), json);
|
|
276
|
+
for (const v of j.tokens) {
|
|
277
|
+
const t = heldToken(v, slug, secrets);
|
|
278
|
+
if (t === null)
|
|
279
|
+
return fail(unexpected(service, a, "the CI tokens were asked for"), json);
|
|
280
|
+
tokens.push(t);
|
|
281
|
+
}
|
|
282
|
+
if (j.more)
|
|
283
|
+
more.push(slug);
|
|
284
|
+
}
|
|
285
|
+
const where = slugs.length === 1 ? slugs[0] : slugs.length === 0 ? "no organisation this login is an admin or an owner of" : `${slugs.length} organisations`;
|
|
286
|
+
const first = tokens[0];
|
|
287
|
+
const next = first !== undefined
|
|
288
|
+
? [[`mason token revoke ${first.id} --org ${first.replica.split("/")[0]}`, "end one"], ["mason token create --replica <org>/<name>", "another"]]
|
|
289
|
+
: [["mason token create --replica <org>/<name>", "make one"]];
|
|
290
|
+
if (json) {
|
|
291
|
+
out(jsonText(TOKEN, { tokens, more: more.length > 0 }, next));
|
|
292
|
+
return 0;
|
|
293
|
+
}
|
|
294
|
+
const st = outStyle();
|
|
295
|
+
const head = tokens.length ? `${tokens.length} CI token${tokens.length === 1 ? "" : "s"} that act, in ${where}` : `No CI tokens that act, in ${where}`;
|
|
296
|
+
const lines = tokens.map((t) => ` ${t.id} ${t.name}: ${t.replica}, ${t.scopes.join(" and ")}, until ${t.expiresAt}, last used ${t.lastUsedAt ?? "never"}${t.madeBy ? `, made by ${t.madeBy}` : ""}`);
|
|
297
|
+
const tips = more.map((o) => `${o} has more CI tokens than the service lists at once: its web app lists them all`);
|
|
298
|
+
const end = endText({ tips, next }, st);
|
|
299
|
+
out(strict(`${st.bold(head)}${lines.length ? `\n${lines.join("\n")}` : ""}${end ? `\n${end}` : ""}`, st));
|
|
300
|
+
return 0;
|
|
301
|
+
}
|
|
302
|
+
async function revoke(id, named, json) {
|
|
303
|
+
const s = signedIn("a CI token is revoked", `mason token revoke ${id}`, json);
|
|
304
|
+
if (isFailure(s))
|
|
305
|
+
return fail(s, json);
|
|
306
|
+
const { service, token: login } = s, secrets = [login];
|
|
307
|
+
let org = named;
|
|
308
|
+
if (org === null) {
|
|
309
|
+
const orgs = await loginOrgs(service, login, `mason token revoke ${id} --org <org>`, MAKERS);
|
|
310
|
+
if (isFailure(orgs))
|
|
311
|
+
return fail(orgs, json);
|
|
312
|
+
if (orgs.length !== 1) {
|
|
313
|
+
return fail({ message: orgs.length ? `this login is an admin or an owner of ${orgs.length} organisations: which one is the token of?` : "this login is an admin or an owner of no organisation",
|
|
314
|
+
code: orgs.length ? "usage" : "refused", why: "an admin or an owner of a token's organisation revokes it",
|
|
315
|
+
fix: orgs.length ? `name it: mason token revoke ${id} --org <org> (${orgs.map((o) => o.slug).join(", ")})` : "sign in as an admin or an owner of its organisation",
|
|
316
|
+
next: orgs.length ? orgs.slice(0, 3).map((o) => [`mason token revoke ${id} --org ${o.slug}`]) : [["mason login", `sign in as ${MAY}`]] }, json);
|
|
317
|
+
}
|
|
318
|
+
org = orgs[0].slug;
|
|
319
|
+
}
|
|
320
|
+
const again = [`mason token revoke ${id} --org ${org}`, "revoke it again"];
|
|
321
|
+
const a = await call(service, "POST", `/v1/tokens/${org}/${id}/revoke`, { token: login });
|
|
322
|
+
if (isFailure(a))
|
|
323
|
+
return fail({ ...a, why: `${a.why ?? "no answer"}; whether it was revoked is not known`, fix: `mason token list ${org} says whether it still acts; revoke it again if it does`, next: [listStep(org), again] }, json);
|
|
324
|
+
if (a.status !== 200)
|
|
325
|
+
return fail(refused(a, org, again, secrets, service, "a CI token was revoked"), json);
|
|
326
|
+
const j = a.json;
|
|
327
|
+
if (!obj(j) || !carriesNoSecret(j, secrets) || !obj(j.revoked) || j.revoked.id !== id || typeof j.revoked.name !== "string" || j.revoked.name.length > 200)
|
|
328
|
+
return fail(unexpected(service, a, "a CI token was revoked"), json);
|
|
329
|
+
const revoked = { org, id, name: serviceText(j.revoked.name, 64, secrets) };
|
|
330
|
+
const next = [listStep(org)];
|
|
331
|
+
if (json) {
|
|
332
|
+
out(jsonText(TOKEN, { revoked }, next));
|
|
333
|
+
return 0;
|
|
334
|
+
}
|
|
335
|
+
out(reportText({ headline: `Revoked the CI token ${id} of ${org}: it acts no more`, details: [["name", revoked.name]], next }, outStyle()));
|
|
336
|
+
return 0;
|
|
337
|
+
}
|