wawesome 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 +113 -5
- package/dist/index.mjs +514 -92
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -116,6 +116,9 @@ npx wawesome deploy
|
|
|
116
116
|
| `npx wawesome env list` | View environment variables for the current app |
|
|
117
117
|
| `npx wawesome env set <key> <val>` | Set an environment variable (add `--secret` for write-only) |
|
|
118
118
|
| `npx wawesome env rm <key>` | Delete an environment variable |
|
|
119
|
+
| `npx wawesome credentials` | List deploy credentials — name, prefix, capabilities, Apps, last use |
|
|
120
|
+
| `npx wawesome credentials mint <name>` | Mint a deploy credential and print its secret, once |
|
|
121
|
+
| `npx wawesome credentials revoke <name>`| Revoke a deploy credential by name or prefix |
|
|
119
122
|
|
|
120
123
|
---
|
|
121
124
|
|
|
@@ -279,6 +282,74 @@ npx wawesome cron history my-function --state failed
|
|
|
279
282
|
|
|
280
283
|
---
|
|
281
284
|
|
|
285
|
+
## 🔑 Deploy Credentials
|
|
286
|
+
|
|
287
|
+
A deploy credential is what a CI pipeline or an agent authenticates with, where there is nobody at a
|
|
288
|
+
keyboard to log in. It belongs to the workspace rather than to you: it keeps working after you leave,
|
|
289
|
+
and revoking it does not end your own session. Once you have one, [Deploying from CI](#-deploying-from-ci)
|
|
290
|
+
is what to do with it.
|
|
291
|
+
|
|
292
|
+
Minting, listing and revoking are the workspace owner's, and are closed to deploy credentials
|
|
293
|
+
themselves whatever they carry — a leaked credential cannot mint another. So these three run on your
|
|
294
|
+
ordinary `npx wawesome login` session.
|
|
295
|
+
|
|
296
|
+
### Mint one
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
npx wawesome credentials mint ci-pipeline
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
The secret is printed once and never again — the platform stores a digest of it, so no later read can
|
|
303
|
+
rebuild it. It goes to stdout on a line of its own and everything else the command prints goes to
|
|
304
|
+
stderr, so a redirect catches the secret and not one character besides:
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
npx wawesome credentials mint ci-pipeline > secret.txt
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
A credential carries `deploy`, reaches every App in the workspace, and expires in ninety days unless
|
|
311
|
+
you say otherwise:
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
# reads logs and cannot deploy, restricted to one App, never expires
|
|
315
|
+
npx wawesome credentials mint agent -c logs:read -a prod --expires never
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
| Flag | Meaning |
|
|
319
|
+
|:---------------------|:------------------------------------------------------------------------------|
|
|
320
|
+
| `-c, --capability` | `deploy`, `logs:read`, or both. Repeatable or comma-separated. Default `deploy` |
|
|
321
|
+
| `-a, --app` | Restrict to these Apps, by slug. Repeatable. Default: every App in the workspace |
|
|
322
|
+
| `--expires` | Days, or `never`. Default: 90 |
|
|
323
|
+
|
|
324
|
+
An App named in a restriction does not have to exist yet — a pipeline whose first deploy creates the
|
|
325
|
+
App it was minted for is the ordinary case. A restriction bounds what a credential can disturb, not
|
|
326
|
+
what it can read: a Function deployed into one App still reads the whole workspace's environment at
|
|
327
|
+
runtime.
|
|
328
|
+
|
|
329
|
+
### List them
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
npx wawesome credentials
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Name, displayable prefix, capabilities, Apps, last use and expiry — never the secret, which the
|
|
336
|
+
platform no longer holds. `LAST USED` lags by a few minutes and reads `never` for a credential
|
|
337
|
+
nothing has ever presented, which is a better reason to revoke one than any calendar date.
|
|
338
|
+
|
|
339
|
+
### Revoke one
|
|
340
|
+
|
|
341
|
+
```bash
|
|
342
|
+
npx wawesome credentials revoke ci-pipeline
|
|
343
|
+
npx wawesome credentials revoke wawe_ab3k9x --yes
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
By name or by displayable prefix, whole or partial. It asks before it does it, `--yes` is the answer
|
|
347
|
+
where nobody is at the keyboard, and a prefix naming more than one credential is refused rather than
|
|
348
|
+
guessed at. A revocation lands on the very next request presenting the secret, and nothing un-revokes
|
|
349
|
+
it.
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
282
353
|
## ⚙️ Configuration & Custom Gateway
|
|
283
354
|
|
|
284
355
|
### `wawesome-function.json`
|
|
@@ -340,11 +411,14 @@ Two rules to know about:
|
|
|
340
411
|
**HTML is refused at deploy time.** Your Function renders its own markup, and a document served from
|
|
341
412
|
your App's own origin is the sharpest same-origin vector a static file has.
|
|
342
413
|
|
|
343
|
-
**An SVG is served
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
414
|
+
**An SVG is served inert.** The rule behind the refusal above is that nothing you deploy as a file
|
|
415
|
+
becomes a page on your App's own origin, and an SVG opened directly in a browser would — it runs
|
|
416
|
+
script, and it renders whatever HTML a `<foreignObject>` holds. It is served rather than refused
|
|
417
|
+
because making it inert costs the file nothing: an `<img src="logo.svg">` never ran that script and
|
|
418
|
+
is never checked against the policy, so your drawings render as they always did. Every SVG and XML
|
|
419
|
+
file carries `Content-Security-Policy: script-src 'none'; sandbox`, and only navigating straight to
|
|
420
|
+
one loses anything — such a file is sandboxed onto an origin of its own, so its links no longer
|
|
421
|
+
navigate and a page embedding it through `<object>` or `<iframe>` cannot reach into its DOM.
|
|
348
422
|
|
|
349
423
|
### Schedules
|
|
350
424
|
|
|
@@ -497,6 +571,40 @@ export WAWESOME_GATEWAY_URL="http://localhost:3000"
|
|
|
497
571
|
npx wawesome login --gateway http://localhost:3000
|
|
498
572
|
```
|
|
499
573
|
|
|
574
|
+
A successful login records the gateway it used, and every later command goes there, because a token is only valid at
|
|
575
|
+
the gateway that issued it. `login` itself ignores that record and reads only the three overrides above, so a bare
|
|
576
|
+
`npx wawesome login` with none of them set goes to `https://api.wawesome.io`. `npx wawesome logout` clears the recorded
|
|
577
|
+
gateway along with the credentials.
|
|
578
|
+
|
|
579
|
+
---
|
|
580
|
+
|
|
581
|
+
## 🤖 Deploying from CI
|
|
582
|
+
|
|
583
|
+
A **deploy credential** is what deploys without a person at a keyboard. Your workspace mints one, and the CLI reads it
|
|
584
|
+
from a single environment variable — there is no login, and no credentials file is involved at any point:
|
|
585
|
+
|
|
586
|
+
```bash
|
|
587
|
+
export WAWESOME_DEPLOY_CREDENTIAL="wawe_..."
|
|
588
|
+
export WAWESOME_GATEWAY_URL="https://api.wawesome.io"
|
|
589
|
+
|
|
590
|
+
npx wawesome deploy
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
The workspace is resolved from the credential itself, so nothing in your pipeline has to carry a workspace identifier.
|
|
594
|
+
The variable name is prefixed and has no shorter alias, so it cannot collide with another tool's CI variable.
|
|
595
|
+
|
|
596
|
+
While it is set:
|
|
597
|
+
|
|
598
|
+
- it wins over any `~/.wawesome/credentials.json` on the machine, so a runner with a stale cached home directory still
|
|
599
|
+
deploys as the credential rather than as whoever last logged in there;
|
|
600
|
+
- nothing is written to disk, and the secret stays in the environment where you put it;
|
|
601
|
+
- a refusal says which of four things happened — the credential is unknown, revoked, expired, or does not carry what the
|
|
602
|
+
command needs — instead of asking you to sign in again, which a pipeline cannot do;
|
|
603
|
+
- `login` and `logout` refuse, because neither would change what the next command authenticates as;
|
|
604
|
+
- `credentials` is refused by the platform, because minting, listing and revoking are closed to
|
|
605
|
+
credentials whatever they carry;
|
|
606
|
+
- `whoami` names the credential by its prefix, so you can match it against the one in your CI settings.
|
|
607
|
+
|
|
500
608
|
---
|
|
501
609
|
|
|
502
610
|
## 🔒 Security & Secrets
|
package/dist/index.mjs
CHANGED
|
@@ -12,12 +12,90 @@ import { Readable } from "node:stream";
|
|
|
12
12
|
import crypto from "node:crypto";
|
|
13
13
|
import { confirm, select } from "@inquirer/prompts";
|
|
14
14
|
import zlib from "node:zlib";
|
|
15
|
+
//#region src/errors.ts
|
|
16
|
+
/** A rejection carrying the gateway's own prose and, where given, its reason. */
|
|
17
|
+
var GatewayError = class extends Error {
|
|
18
|
+
status;
|
|
19
|
+
reason;
|
|
20
|
+
/** The raw response, for `--verbose` to print when the prose is not enough. */
|
|
21
|
+
body;
|
|
22
|
+
constructor(message, status, reason, body = "") {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "GatewayError";
|
|
25
|
+
this.status = status;
|
|
26
|
+
this.reason = reason;
|
|
27
|
+
this.body = body;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Read a failed response into a `GatewayError`.
|
|
32
|
+
*
|
|
33
|
+
* `fallback` is used only when the response carries no prose of its own — a
|
|
34
|
+
* proxy's HTML error page, or an empty body. A 401 is deliberately *not* special
|
|
35
|
+
* cased here: what to say about an expired session depends on what the caller
|
|
36
|
+
* was doing, so callers handle it before reaching this.
|
|
37
|
+
*/
|
|
38
|
+
async function asGatewayError(res, fallback) {
|
|
39
|
+
return rejectionOf(await res.text().catch(() => ""), res.status, fallback);
|
|
40
|
+
}
|
|
41
|
+
/** The same reading, for a caller that has already taken the body off the wire. */
|
|
42
|
+
function rejectionOf(body, status, fallback) {
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(body);
|
|
45
|
+
return new GatewayError(parsed.error || fallback, status, parsed.reason, body);
|
|
46
|
+
} catch {
|
|
47
|
+
return new GatewayError(fallback, status, void 0, body);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Why the gateway refused, where what was thrown might not be a refusal at all. */
|
|
51
|
+
function reasonOf(err) {
|
|
52
|
+
return err instanceof GatewayError ? err.reason : void 0;
|
|
53
|
+
}
|
|
54
|
+
/** What went wrong, as text, whatever was thrown. */
|
|
55
|
+
function errorText(err) {
|
|
56
|
+
return err instanceof Error ? err.message : String(err);
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/credential.ts
|
|
60
|
+
const CREDENTIAL_ENV_VAR = "WAWESOME_DEPLOY_CREDENTIAL";
|
|
61
|
+
function credentialFromEnvironment() {
|
|
62
|
+
const secret = process.env[CREDENTIAL_ENV_VAR]?.trim();
|
|
63
|
+
return secret ? secret : null;
|
|
64
|
+
}
|
|
65
|
+
/** Enough to match a credential against the one in a CI settings page, and no more. */
|
|
66
|
+
function credentialPrefix(secret) {
|
|
67
|
+
return secret.slice(0, 11);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* A credential has no session behind it, so the platform's own reason — unknown,
|
|
71
|
+
* revoked, expired, or a scope it does not carry — is the whole answer, and
|
|
72
|
+
* "sign in again" names a door a CI runner cannot open.
|
|
73
|
+
*/
|
|
74
|
+
function unauthorizedMessage(status, body) {
|
|
75
|
+
const secret = credentialFromEnvironment();
|
|
76
|
+
if (!secret) return status === 401 ? "Authentication expired. Run 'wawesome login' again." : rejectionOf(body, status, `The platform refused this request (HTTP ${status}).`).message;
|
|
77
|
+
return rejectionOf(body, status, `The deploy credential ${credentialPrefix(secret)} was refused (HTTP ${status}).`).message;
|
|
78
|
+
}
|
|
79
|
+
function reportSessionCommandRefused(secret, consequence) {
|
|
80
|
+
console.error(`[wawesome] Error: ${CREDENTIAL_ENV_VAR} is set, so this CLI authenticates as deploy credential ${credentialPrefix(secret)}.`);
|
|
81
|
+
console.error(`[wawesome] ${consequence} Unset ${CREDENTIAL_ENV_VAR} first.`);
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
15
84
|
//#region src/config.ts
|
|
16
85
|
/**
|
|
17
86
|
* Hardcoded Supabase project config for the wawesome.io platform.
|
|
18
87
|
* These are public values — the anon key is designed to be embedded in clients.
|
|
19
88
|
*/
|
|
20
89
|
const SUPABASE_URL = "https://vclasavxxoufwymrutai.supabase.co";
|
|
90
|
+
/**
|
|
91
|
+
* Where the documents accepted at signup are published. Fixed rather than
|
|
92
|
+
* resolved per environment: there is one website, and a signup has to be able
|
|
93
|
+
* to link at something a person can actually read before they accept it.
|
|
94
|
+
*/
|
|
95
|
+
const LEGAL_URLS = {
|
|
96
|
+
terms: "https://wawesome.io/legal/terms-of-service",
|
|
97
|
+
privacy: "https://wawesome.io/legal/privacy-policy"
|
|
98
|
+
};
|
|
21
99
|
const SUPABASE_ANON_KEY = "sb_publishable_ZlgDAI9cNZkefEwVPKIngA_ktuvTpjD";
|
|
22
100
|
/** Path to the user-level settings file (~/.wawesome/settings.json) */
|
|
23
101
|
function getSettingsPath() {
|
|
@@ -51,11 +129,22 @@ function resolveAddress(overrideUrl, envNames, settingsKey, stored, fallback) {
|
|
|
51
129
|
return stored || fallback;
|
|
52
130
|
}
|
|
53
131
|
function getGatewayUrl(overrideUrl) {
|
|
54
|
-
return resolveAddress(overrideUrl, GATEWAY_ENV_NAMES, "gateway_url",
|
|
132
|
+
return resolveAddress(overrideUrl, GATEWAY_ENV_NAMES, "gateway_url", readStoredCredentials()?.gateway_url, GATEWAY_URL_FALLBACK);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* A tenant token is only valid at the gateway that issued it, so every other
|
|
136
|
+
* command has to follow the credentials. A login holds no token yet, and
|
|
137
|
+
* reading that rung here made a local gateway outlive the removal of every
|
|
138
|
+
* documented override, since only `logout` clears it. A **Deploy credential**
|
|
139
|
+
* reads the same rungs, and for a sharper reason: following a stale file would
|
|
140
|
+
* present it at a gateway that never minted it.
|
|
141
|
+
*/
|
|
142
|
+
function resolveGatewayUrlWithoutSession(overrideUrl) {
|
|
143
|
+
return resolveAddress(overrideUrl, GATEWAY_ENV_NAMES, "gateway_url", void 0, GATEWAY_URL_FALLBACK);
|
|
55
144
|
}
|
|
56
145
|
getGatewayUrl();
|
|
57
146
|
function getDashboardUrl(overrideUrl) {
|
|
58
|
-
return resolveAddress(overrideUrl, DASHBOARD_ENV_NAMES, "dashboard_url",
|
|
147
|
+
return resolveAddress(overrideUrl, DASHBOARD_ENV_NAMES, "dashboard_url", readStoredCredentials()?.dashboard_url, DASHBOARD_URL_FALLBACK);
|
|
59
148
|
}
|
|
60
149
|
/**
|
|
61
150
|
* The dashboard for the environment this login is going to.
|
|
@@ -66,7 +155,7 @@ function getDashboardUrl(overrideUrl) {
|
|
|
66
155
|
* so is dropping one where the gateway did not change at all.
|
|
67
156
|
*/
|
|
68
157
|
function resolveDashboardUrlForLogin(overrideUrl, gatewayUrl) {
|
|
69
|
-
const creds =
|
|
158
|
+
const creds = readStoredCredentials();
|
|
70
159
|
const sameEnvironment = creds?.gateway_url === gatewayUrl;
|
|
71
160
|
return resolveAddress(overrideUrl, DASHBOARD_ENV_NAMES, "dashboard_url", sameEnvironment ? creds?.dashboard_url : void 0, DASHBOARD_URL_FALLBACK);
|
|
72
161
|
}
|
|
@@ -77,9 +166,31 @@ function getCredentialsPath() {
|
|
|
77
166
|
return path.join(os.homedir(), ".wawesome", "credentials.json");
|
|
78
167
|
}
|
|
79
168
|
/**
|
|
80
|
-
*
|
|
169
|
+
* What this process authenticates with: the **Deploy credential** in the
|
|
170
|
+
* environment where one is set, and otherwise the session `login` left on disk.
|
|
171
|
+
*
|
|
172
|
+
* The environment wins outright rather than filling a gap, so a CI runner
|
|
173
|
+
* carrying a stale cached home directory still authenticates as itself.
|
|
81
174
|
*/
|
|
82
175
|
function readCredentials() {
|
|
176
|
+
const secret = credentialFromEnvironment();
|
|
177
|
+
return secret ? environmentCredential(secret) : readStoredCredentials();
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* A credential wears the shape of a session so that every command reaches the
|
|
181
|
+
* platform the same way. What it does not carry is the point: no refresh token,
|
|
182
|
+
* because there is nothing behind it to renew, and no Tenant, because the
|
|
183
|
+
* platform resolves that from the credential.
|
|
184
|
+
*/
|
|
185
|
+
function environmentCredential(secret) {
|
|
186
|
+
return {
|
|
187
|
+
gateway_url: resolveGatewayUrlWithoutSession(void 0),
|
|
188
|
+
tenant_jwt: secret,
|
|
189
|
+
credential_prefix: credentialPrefix(secret)
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/** The session on disk, whatever the environment says. */
|
|
193
|
+
function readStoredCredentials() {
|
|
83
194
|
const credPath = getCredentialsPath();
|
|
84
195
|
if (!fs.existsSync(credPath)) return null;
|
|
85
196
|
try {
|
|
@@ -97,6 +208,7 @@ function readCredentials() {
|
|
|
97
208
|
* `writeFileSync` applies only where it creates the file.
|
|
98
209
|
*/
|
|
99
210
|
function writeCredentials(creds) {
|
|
211
|
+
if (credentialFromEnvironment()) return;
|
|
100
212
|
const credPath = getCredentialsPath();
|
|
101
213
|
const dir = path.dirname(credPath);
|
|
102
214
|
fs.mkdirSync(dir, {
|
|
@@ -771,7 +883,7 @@ async function buildJs(entryInput, options) {
|
|
|
771
883
|
* that has to name this version — `--version`, the dependency a scaffolded
|
|
772
884
|
* project pins — reads it here, so a release bumps one file.
|
|
773
885
|
*/
|
|
774
|
-
const CLI_VERSION = "0.
|
|
886
|
+
const CLI_VERSION = "0.9.0";
|
|
775
887
|
//#endregion
|
|
776
888
|
//#region src/prompt.ts
|
|
777
889
|
/**
|
|
@@ -844,6 +956,7 @@ async function authorizedFetch(url, init = {}) {
|
|
|
844
956
|
const sentAt = Date.now();
|
|
845
957
|
const res = await fetch(url, renewedJwt ? withBearer(request(), renewedJwt) : request());
|
|
846
958
|
if (res.status !== 401) return res;
|
|
959
|
+
if (credentialFromEnvironment()) return res;
|
|
847
960
|
if (renewedJwt && renewedAt > sentAt) return retry(url, request, renewedJwt, res);
|
|
848
961
|
if (renewedJwt && Date.now() - renewedAt < JUST_RENEWED_MS) return res;
|
|
849
962
|
const renewed = await renewSession();
|
|
@@ -948,50 +1061,6 @@ function endSession(reason) {
|
|
|
948
1061
|
process.exit(1);
|
|
949
1062
|
}
|
|
950
1063
|
//#endregion
|
|
951
|
-
//#region src/errors.ts
|
|
952
|
-
/** A rejection carrying the gateway's own prose and, where given, its reason. */
|
|
953
|
-
var GatewayError = class extends Error {
|
|
954
|
-
status;
|
|
955
|
-
reason;
|
|
956
|
-
/** The raw response, for `--verbose` to print when the prose is not enough. */
|
|
957
|
-
body;
|
|
958
|
-
constructor(message, status, reason, body = "") {
|
|
959
|
-
super(message);
|
|
960
|
-
this.name = "GatewayError";
|
|
961
|
-
this.status = status;
|
|
962
|
-
this.reason = reason;
|
|
963
|
-
this.body = body;
|
|
964
|
-
}
|
|
965
|
-
};
|
|
966
|
-
/**
|
|
967
|
-
* Read a failed response into a `GatewayError`.
|
|
968
|
-
*
|
|
969
|
-
* `fallback` is used only when the response carries no prose of its own — a
|
|
970
|
-
* proxy's HTML error page, or an empty body. A 401 is deliberately *not* special
|
|
971
|
-
* cased here: what to say about an expired session depends on what the caller
|
|
972
|
-
* was doing, so callers handle it before reaching this.
|
|
973
|
-
*/
|
|
974
|
-
async function asGatewayError(res, fallback) {
|
|
975
|
-
return rejectionOf(await res.text().catch(() => ""), res.status, fallback);
|
|
976
|
-
}
|
|
977
|
-
/** The same reading, for a caller that has already taken the body off the wire. */
|
|
978
|
-
function rejectionOf(body, status, fallback) {
|
|
979
|
-
try {
|
|
980
|
-
const parsed = JSON.parse(body);
|
|
981
|
-
return new GatewayError(parsed.error || fallback, status, parsed.reason, body);
|
|
982
|
-
} catch {
|
|
983
|
-
return new GatewayError(fallback, status, void 0, body);
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
/** Why the gateway refused, where what was thrown might not be a refusal at all. */
|
|
987
|
-
function reasonOf(err) {
|
|
988
|
-
return err instanceof GatewayError ? err.reason : void 0;
|
|
989
|
-
}
|
|
990
|
-
/** What went wrong, as text, whatever was thrown. */
|
|
991
|
-
function errorText(err) {
|
|
992
|
-
return err instanceof Error ? err.message : String(err);
|
|
993
|
-
}
|
|
994
|
-
//#endregion
|
|
995
1064
|
//#region src/tenant.ts
|
|
996
1065
|
async function fetchTenantDetails(creds) {
|
|
997
1066
|
const res = await authorizedFetch(`${creds.gateway_url}/v1/tenant`, { headers: { Authorization: `Bearer ${creds.tenant_jwt}` } });
|
|
@@ -1104,6 +1173,31 @@ async function promptForWorkspaceName(options) {
|
|
|
1104
1173
|
throw new Error("No workspace name given. Nothing was created.");
|
|
1105
1174
|
}
|
|
1106
1175
|
/**
|
|
1176
|
+
* Ask the person to accept the terms, which is what creating a workspace
|
|
1177
|
+
* through this door means too.
|
|
1178
|
+
*
|
|
1179
|
+
* Declining is the default answer, not the one that needs typing: end of input
|
|
1180
|
+
* takes the default, so a piped login that says nothing has accepted nothing —
|
|
1181
|
+
* the terminal's version of an unticked box. The flag is a word from the person
|
|
1182
|
+
* running the command rather than a prompt, so a pipeline reads the same as a
|
|
1183
|
+
* terminal, and both documents are linked either way.
|
|
1184
|
+
*/
|
|
1185
|
+
async function confirmTermsAccepted(options) {
|
|
1186
|
+
if (options.acceptTerms) return;
|
|
1187
|
+
if (!isInteractive()) throw new Error(`Creating a workspace means accepting the terms of service and the privacy policy, and there is no terminal to ask in. Read ${LEGAL_URLS.terms} and ${LEGAL_URLS.privacy}, then re-run with --accept-terms.`);
|
|
1188
|
+
console.log("\n[wawesome] Creating a workspace means accepting:");
|
|
1189
|
+
console.log(` Terms of Service ${LEGAL_URLS.terms}`);
|
|
1190
|
+
console.log(` Privacy Policy ${LEGAL_URLS.privacy}`);
|
|
1191
|
+
const session = openPromptSession();
|
|
1192
|
+
try {
|
|
1193
|
+
const answer = await session.ask(" Accept both?", "n");
|
|
1194
|
+
if (/^y(es)?$/i.test(answer.trim())) return;
|
|
1195
|
+
} finally {
|
|
1196
|
+
session.close();
|
|
1197
|
+
}
|
|
1198
|
+
throw new Error("The terms were not accepted. Nothing was created.");
|
|
1199
|
+
}
|
|
1200
|
+
/**
|
|
1107
1201
|
* Turn a finished OAuth callback into stored credentials.
|
|
1108
1202
|
*
|
|
1109
1203
|
* Split from the callback server so what login stores can be exercised without
|
|
@@ -1120,6 +1214,7 @@ async function completeLogin({ gatewayUrl, dashboardUrl, accessToken, refreshTok
|
|
|
1120
1214
|
let workspaceSlug;
|
|
1121
1215
|
if (tenants.length === 0) {
|
|
1122
1216
|
const chosenName = await promptForWorkspaceName(options);
|
|
1217
|
+
await confirmTermsAccepted(options);
|
|
1123
1218
|
console.log(`[wawesome] Creating workspace "${chosenName}"...`);
|
|
1124
1219
|
const initRes = await fetch(`${gatewayUrl}/api/v1/onboarding/init`, {
|
|
1125
1220
|
method: "POST",
|
|
@@ -1127,9 +1222,12 @@ async function completeLogin({ gatewayUrl, dashboardUrl, accessToken, refreshTok
|
|
|
1127
1222
|
Authorization: `Bearer ${accessToken}`,
|
|
1128
1223
|
"Content-Type": "application/json"
|
|
1129
1224
|
},
|
|
1130
|
-
body: JSON.stringify({
|
|
1225
|
+
body: JSON.stringify({
|
|
1226
|
+
tenant_name: chosenName,
|
|
1227
|
+
accepted_terms: true
|
|
1228
|
+
})
|
|
1131
1229
|
});
|
|
1132
|
-
if (!initRes.ok) throw
|
|
1230
|
+
if (!initRes.ok) throw await asGatewayError(initRes, `Failed to initialize workspace (HTTP ${initRes.status}).`);
|
|
1133
1231
|
const initData = await initRes.json();
|
|
1134
1232
|
primaryTenantId = initData.tenant_id;
|
|
1135
1233
|
workspaceSlug = initData.tenant_slug;
|
|
@@ -1186,7 +1284,12 @@ async function completeLogin({ gatewayUrl, dashboardUrl, accessToken, refreshTok
|
|
|
1186
1284
|
* 5. Store credentials in ~/.wawesome/credentials.json
|
|
1187
1285
|
*/
|
|
1188
1286
|
async function login(options) {
|
|
1189
|
-
const
|
|
1287
|
+
const credential = credentialFromEnvironment();
|
|
1288
|
+
if (credential) {
|
|
1289
|
+
reportSessionCommandRefused(credential, "A session signed in here would be read by nothing.");
|
|
1290
|
+
process.exit(1);
|
|
1291
|
+
}
|
|
1292
|
+
const gatewayUrl = resolveGatewayUrlWithoutSession(options.gateway || options.api);
|
|
1190
1293
|
const dashboardUrl = resolveDashboardUrlForLogin(options.dashboard, gatewayUrl);
|
|
1191
1294
|
const provider = options.provider || "github";
|
|
1192
1295
|
const isVerbose = Boolean(options.verbose);
|
|
@@ -1278,6 +1381,11 @@ async function login(options) {
|
|
|
1278
1381
|
* Clear stored credentials.
|
|
1279
1382
|
*/
|
|
1280
1383
|
function logout() {
|
|
1384
|
+
const credential = credentialFromEnvironment();
|
|
1385
|
+
if (credential) {
|
|
1386
|
+
reportSessionCommandRefused(credential, "The next command would authenticate all the same.");
|
|
1387
|
+
process.exit(1);
|
|
1388
|
+
}
|
|
1281
1389
|
forgetRenewedSession();
|
|
1282
1390
|
if (deleteCredentials()) console.log("[wawesome] ✅ Logged out. Credentials removed.");
|
|
1283
1391
|
else console.log("[wawesome] Not logged in (no credentials found).");
|
|
@@ -1287,6 +1395,7 @@ function logout() {
|
|
|
1287
1395
|
*/
|
|
1288
1396
|
async function whoami() {
|
|
1289
1397
|
const creds = readCredentials();
|
|
1398
|
+
if (creds?.credential_prefix) return reportCredential(creds);
|
|
1290
1399
|
if (!creds) {
|
|
1291
1400
|
console.log("[wawesome] Not logged in. Run 'wawesome login' to authenticate.");
|
|
1292
1401
|
return;
|
|
@@ -1306,6 +1415,33 @@ async function whoami() {
|
|
|
1306
1415
|
console.log(` Tenant: ${creds.tenant_id}`);
|
|
1307
1416
|
console.log(` Gateway: ${creds.gateway_url}\n`);
|
|
1308
1417
|
}
|
|
1418
|
+
/**
|
|
1419
|
+
* Name the credential the commands here run as.
|
|
1420
|
+
*
|
|
1421
|
+
* By its prefix, never the secret: what a developer needs is to match the
|
|
1422
|
+
* identity in front of them against the one in their CI settings, and the
|
|
1423
|
+
* output of `whoami` is the sort of thing that ends up pasted into an issue.
|
|
1424
|
+
* The workspace is asked for rather than read, because a credential names no
|
|
1425
|
+
* Tenant of its own — the platform resolves that from the credential.
|
|
1426
|
+
*/
|
|
1427
|
+
async function reportCredential(creds) {
|
|
1428
|
+
let workspace = null;
|
|
1429
|
+
let refusal = null;
|
|
1430
|
+
try {
|
|
1431
|
+
workspace = await resolveWorkspace(creds);
|
|
1432
|
+
} catch (err) {
|
|
1433
|
+
if (err instanceof GatewayError && (err.status === 401 || err.status === 403)) refusal = err.message;
|
|
1434
|
+
}
|
|
1435
|
+
console.log("\n[wawesome] Acting as a deploy credential:");
|
|
1436
|
+
console.log(` Credential: ${creds.credential_prefix} (from ${CREDENTIAL_ENV_VAR})`);
|
|
1437
|
+
if (workspace) {
|
|
1438
|
+
console.log(` Workspace: ${workspace.name}`);
|
|
1439
|
+
console.log(` Address: ${workspace.slug}`);
|
|
1440
|
+
}
|
|
1441
|
+
console.log(` Gateway: ${creds.gateway_url}`);
|
|
1442
|
+
if (refusal) console.log(` Refused: ${refusal}`);
|
|
1443
|
+
console.log("");
|
|
1444
|
+
}
|
|
1309
1445
|
//#endregion
|
|
1310
1446
|
//#region src/schedules.ts
|
|
1311
1447
|
/**
|
|
@@ -1316,15 +1452,18 @@ async function whoami() {
|
|
|
1316
1452
|
* press resume, and file a bug when neither starts it.
|
|
1317
1453
|
*/
|
|
1318
1454
|
function whenItRuns(schedule) {
|
|
1319
|
-
if (schedule.state === "suspended") return "suspended — the workspace owes payment";
|
|
1320
1455
|
const undeclared = schedule.declared === false;
|
|
1456
|
+
if (schedule.state === "suspended") {
|
|
1457
|
+
const why = "the workspace owes payment, its subscription ended, or it has spent past its plan's allowance";
|
|
1458
|
+
return undeclared ? `suspended, and not in this config file — ${why}; declare it again too` : `suspended — ${why}`;
|
|
1459
|
+
}
|
|
1321
1460
|
if (schedule.state === "paused") return undeclared ? "paused, and not in this config file — declare it again, then resume" : "paused — resume it to run again";
|
|
1322
1461
|
if (undeclared) return "not in this config file — disabled, kept";
|
|
1323
1462
|
if (schedule.state !== "active") return schedule.state;
|
|
1324
|
-
return schedule.next_fire_at ? `next run ${asUtc(schedule.next_fire_at)}` : "next run unknown";
|
|
1463
|
+
return schedule.next_fire_at ? `next run ${asUtc$1(schedule.next_fire_at)}` : "next run unknown";
|
|
1325
1464
|
}
|
|
1326
1465
|
/** `2026-08-23 03:00 UTC`, which is the zone every expression is read in. */
|
|
1327
|
-
function asUtc(iso) {
|
|
1466
|
+
function asUtc$1(iso) {
|
|
1328
1467
|
const at = new Date(iso);
|
|
1329
1468
|
if (Number.isNaN(at.getTime())) return iso;
|
|
1330
1469
|
const pad = (n) => String(n).padStart(2, "0");
|
|
@@ -1872,7 +2011,7 @@ async function deploy(entryInput, options) {
|
|
|
1872
2011
|
if (!uploadRes.ok) {
|
|
1873
2012
|
const errorBody = await uploadRes.text();
|
|
1874
2013
|
const refusal = rejectionOf(errorBody, uploadRes.status, "Version with this code bundle already exists.");
|
|
1875
|
-
if (uploadRes.status === 401) console.error(
|
|
2014
|
+
if (uploadRes.status === 401 || uploadRes.status === 403) console.error(`[wawesome] Error: ${unauthorizedMessage(uploadRes.status, errorBody)}`);
|
|
1876
2015
|
else if (refusal.reason === "paid-plan-required") {
|
|
1877
2016
|
console.error(`\n[wawesome] \x1b[31mError: ${refusal.message}\x1b[0m`);
|
|
1878
2017
|
console.error(`[wawesome] ${planLimitAdvice(refusal.reason)}\n`);
|
|
@@ -1917,7 +2056,7 @@ async function deploy(entryInput, options) {
|
|
|
1917
2056
|
});
|
|
1918
2057
|
if (!deployRes.ok) {
|
|
1919
2058
|
const errorBody = await deployRes.text();
|
|
1920
|
-
if (deployRes.status === 401) console.error(
|
|
2059
|
+
if (deployRes.status === 401 || deployRes.status === 403) console.error(`[wawesome] Error: ${unauthorizedMessage(deployRes.status, errorBody)}`);
|
|
1921
2060
|
else {
|
|
1922
2061
|
console.error(`[wawesome] Error: Deployment failed (HTTP ${deployRes.status}).`);
|
|
1923
2062
|
if (isVerbose) console.error(`[wawesome:verbose] Response: ${errorBody}`);
|
|
@@ -2002,7 +2141,7 @@ async function uploadAssets(creds, app, funcName, bundle, assets, declared, isVe
|
|
|
2002
2141
|
});
|
|
2003
2142
|
if (!manifestRes.ok) {
|
|
2004
2143
|
const errorBody = await manifestRes.text();
|
|
2005
|
-
if (manifestRes.status === 401) console.error(
|
|
2144
|
+
if (manifestRes.status === 401 || manifestRes.status === 403) console.error(`[wawesome] Error: ${unauthorizedMessage(manifestRes.status, errorBody)}`);
|
|
2006
2145
|
else if (manifestRes.status === 409) {
|
|
2007
2146
|
if (continueWhenUnchanged) {
|
|
2008
2147
|
console.log("[wawesome] Code and files unchanged since the last deploy; applying schedules.");
|
|
@@ -2101,7 +2240,7 @@ async function putEnvVar(creds, app, key, value, isSecret) {
|
|
|
2101
2240
|
})
|
|
2102
2241
|
});
|
|
2103
2242
|
if (!res.ok) {
|
|
2104
|
-
if (res.status === 401) throw new GatewayError(
|
|
2243
|
+
if (res.status === 401 || res.status === 403) throw new GatewayError(unauthorizedMessage(res.status, await res.text().catch(() => "")), res.status);
|
|
2105
2244
|
throw await asGatewayError(res, `Failed to set variable '${key}' (HTTP ${res.status}).`);
|
|
2106
2245
|
}
|
|
2107
2246
|
return await res.json();
|
|
@@ -2157,7 +2296,7 @@ async function listEnvVars(options) {
|
|
|
2157
2296
|
});
|
|
2158
2297
|
if (!res.ok) {
|
|
2159
2298
|
const errorBody = await res.text();
|
|
2160
|
-
if (res.status === 401) console.error(
|
|
2299
|
+
if (res.status === 401 || res.status === 403) console.error(`[wawesome] Error: ${unauthorizedMessage(res.status, errorBody)}`);
|
|
2161
2300
|
else {
|
|
2162
2301
|
console.error(`[wawesome] Error: Failed to list variables (HTTP ${res.status}).`);
|
|
2163
2302
|
if (isVerbose) console.error(`[wawesome:verbose] Response: ${errorBody}`);
|
|
@@ -2211,7 +2350,7 @@ async function removeEnvVar(key, options) {
|
|
|
2211
2350
|
});
|
|
2212
2351
|
if (!res.ok) {
|
|
2213
2352
|
const errorBody = await res.text();
|
|
2214
|
-
if (res.status === 401) console.error(
|
|
2353
|
+
if (res.status === 401 || res.status === 403) console.error(`[wawesome] Error: ${unauthorizedMessage(res.status, errorBody)}`);
|
|
2215
2354
|
else if (res.status === 404) console.error(`[wawesome] Error: Variable '${key}' not found on app '${app}'.`);
|
|
2216
2355
|
else {
|
|
2217
2356
|
console.error(`[wawesome] Error: Failed to delete variable '${key}' (HTTP ${res.status}).`);
|
|
@@ -2287,7 +2426,7 @@ function installDependencies(dir, options = {}) {
|
|
|
2287
2426
|
//#region src/provision.ts
|
|
2288
2427
|
/** The gateway's own words for a failure, or a plain HTTP status if it gave none. */
|
|
2289
2428
|
async function rejected(res, fallback) {
|
|
2290
|
-
if (res.status === 401) return new GatewayError(
|
|
2429
|
+
if (res.status === 401 || res.status === 403) return new GatewayError(unauthorizedMessage(res.status, await res.text().catch(() => "")), res.status);
|
|
2291
2430
|
return asGatewayError(res, `${fallback} (HTTP ${res.status}).`);
|
|
2292
2431
|
}
|
|
2293
2432
|
function authHeaders(creds) {
|
|
@@ -2794,7 +2933,7 @@ function anArticle(label, capitalized = false) {
|
|
|
2794
2933
|
return `${capitalized ? article[0].toUpperCase() + article.slice(1) : article} ${label}`;
|
|
2795
2934
|
}
|
|
2796
2935
|
/** Report a failure the user can act on, and stop. */
|
|
2797
|
-
function fail(message, ...advice) {
|
|
2936
|
+
function fail$1(message, ...advice) {
|
|
2798
2937
|
console.error(`[wawesome] Error: ${message}`);
|
|
2799
2938
|
for (const line of advice) console.error(`[wawesome] ${line}`);
|
|
2800
2939
|
process.exit(1);
|
|
@@ -2974,7 +3113,7 @@ async function wireUp(creds, appSlug, manifest, answers) {
|
|
|
2974
3113
|
try {
|
|
2975
3114
|
await ensureApp(creds, appSlug);
|
|
2976
3115
|
} catch (err) {
|
|
2977
|
-
fail(errorText(err), ...[planLimitAdvice(reasonOf(err)), scaffolded].filter(Boolean));
|
|
3116
|
+
fail$1(errorText(err), ...[planLimitAdvice(reasonOf(err)), scaffolded].filter(Boolean));
|
|
2978
3117
|
}
|
|
2979
3118
|
for (const { declared, value } of answers) {
|
|
2980
3119
|
if (!value) {
|
|
@@ -2985,7 +3124,7 @@ async function wireUp(creds, appSlug, manifest, answers) {
|
|
|
2985
3124
|
try {
|
|
2986
3125
|
await putEnvVar(creds, appSlug, declared.key, value, declared.secret);
|
|
2987
3126
|
} catch (err) {
|
|
2988
|
-
fail(errorText(err), scaffolded);
|
|
3127
|
+
fail$1(errorText(err), scaffolded);
|
|
2989
3128
|
}
|
|
2990
3129
|
console.log(`[wawesome] ✅ Stored ${declared.key}${declared.secret ? " (secret)" : ""}.`);
|
|
2991
3130
|
}
|
|
@@ -2993,7 +3132,7 @@ async function wireUp(creds, appSlug, manifest, answers) {
|
|
|
2993
3132
|
try {
|
|
2994
3133
|
await enableEgressProvider(creds, appSlug, provider);
|
|
2995
3134
|
} catch (err) {
|
|
2996
|
-
fail(errorText(err), scaffolded);
|
|
3135
|
+
fail$1(errorText(err), scaffolded);
|
|
2997
3136
|
}
|
|
2998
3137
|
console.log(`[wawesome] ✅ Enabled outbound calls to '${provider}'.`);
|
|
2999
3138
|
}
|
|
@@ -3015,7 +3154,7 @@ async function initFromTemplate(templateName, options) {
|
|
|
3015
3154
|
try {
|
|
3016
3155
|
template = await resolveTemplate(gatewayUrl, templateName);
|
|
3017
3156
|
} catch (err) {
|
|
3018
|
-
fail(errorText(err));
|
|
3157
|
+
fail$1(errorText(err));
|
|
3019
3158
|
}
|
|
3020
3159
|
console.log(`[wawesome] Fetching ${template.title} (${template.ref})...`);
|
|
3021
3160
|
if (isVerbose) console.log(`[wawesome:verbose] GET ${template.archive_url}`);
|
|
@@ -3023,7 +3162,7 @@ async function initFromTemplate(templateName, options) {
|
|
|
3023
3162
|
try {
|
|
3024
3163
|
archive = await downloadTemplateArchive(template.archive_url);
|
|
3025
3164
|
} catch (err) {
|
|
3026
|
-
fail(errorText(err));
|
|
3165
|
+
fail$1(errorText(err));
|
|
3027
3166
|
}
|
|
3028
3167
|
const staging = fs.mkdtempSync(path.join(os.tmpdir(), "wawesome-template-"));
|
|
3029
3168
|
const sweepStaging = () => fs.rmSync(staging, {
|
|
@@ -3045,10 +3184,10 @@ async function initFromTemplate(templateName, options) {
|
|
|
3045
3184
|
});
|
|
3046
3185
|
manifest = readTemplateManifest(staging);
|
|
3047
3186
|
} catch (err) {
|
|
3048
|
-
fail(errorText(err));
|
|
3187
|
+
fail$1(errorText(err));
|
|
3049
3188
|
}
|
|
3050
3189
|
const collisions = collidingPaths(cwd, files);
|
|
3051
|
-
if (collisions.length > 0) fail(`${collisions.join(", ")} already exist${collisions.length === 1 ? "s" : ""} here.`, "Run this in an empty directory, or move those files aside first. Nothing was written.");
|
|
3190
|
+
if (collisions.length > 0) fail$1(`${collisions.join(", ")} already exist${collisions.length === 1 ? "s" : ""} here.`, "Run this in an empty directory, or move those files aside first. Nothing was written.");
|
|
3052
3191
|
tenantSession = await ensureSession(options);
|
|
3053
3192
|
const session = openPromptSession();
|
|
3054
3193
|
try {
|
|
@@ -3064,7 +3203,7 @@ async function initFromTemplate(templateName, options) {
|
|
|
3064
3203
|
});
|
|
3065
3204
|
repinned = alignCliDependency(cwd, CLI_VERSION);
|
|
3066
3205
|
} catch (err) {
|
|
3067
|
-
fail(errorText(err), `Some of '${template.name}' may have been written to ${cwd}.`);
|
|
3206
|
+
fail$1(errorText(err), `Some of '${template.name}' may have been written to ${cwd}.`);
|
|
3068
3207
|
}
|
|
3069
3208
|
console.log(`\n[wawesome] ✅ Scaffolded ${files.length} files from '${template.name}'.`);
|
|
3070
3209
|
if (repinned) console.log(`[wawesome] Using wawesome ${repinned.to} — the template pinned ${repinned.from}.`);
|
|
@@ -3241,7 +3380,7 @@ async function fetchVersions(gatewayUrl, tenantJwt, app, funcName, env, isVerbos
|
|
|
3241
3380
|
});
|
|
3242
3381
|
if (!res.ok) {
|
|
3243
3382
|
const errorBody = await res.text();
|
|
3244
|
-
if (res.status === 401) console.error(
|
|
3383
|
+
if (res.status === 401 || res.status === 403) console.error(`[wawesome] Error: ${unauthorizedMessage(res.status, errorBody)}`);
|
|
3245
3384
|
else {
|
|
3246
3385
|
console.error(`[wawesome] Error: Failed to list versions (HTTP ${res.status}).`);
|
|
3247
3386
|
if (isVerbose) console.error(`[wawesome:verbose] Response: ${errorBody}`);
|
|
@@ -3444,7 +3583,7 @@ async function fetchInvocationLogBody(gatewayUrl, tenantJwt, invocationId, isVer
|
|
|
3444
3583
|
});
|
|
3445
3584
|
if (!res.ok) {
|
|
3446
3585
|
const errorText = await res.text();
|
|
3447
|
-
if (res.status === 401) console.error(
|
|
3586
|
+
if (res.status === 401 || res.status === 403) console.error(`[wawesome] Error: ${unauthorizedMessage(res.status, errorText)}`);
|
|
3448
3587
|
else if (res.status === 404) console.error(`[wawesome] Error: Invocation log '${invocationId}' not found or expired (logs are retained for 14 days).`);
|
|
3449
3588
|
else if (res.status === 400) console.error(`[wawesome] Error: Invalid invocation ID '${invocationId}'.`);
|
|
3450
3589
|
else {
|
|
@@ -3474,7 +3613,7 @@ async function listInvocations(gatewayUrl, tenantJwt, funcNameInput, appOverride
|
|
|
3474
3613
|
const res = await fetchInvocationsResponse(gatewayUrl, tenantJwt, funcName, appSlug, appOverride, `?${queryParams.toString()}`, isVerbose);
|
|
3475
3614
|
if (!res.ok) {
|
|
3476
3615
|
const errorText = await res.text();
|
|
3477
|
-
if (res.status === 401) console.error(
|
|
3616
|
+
if (res.status === 401 || res.status === 403) console.error(`[wawesome] Error: ${unauthorizedMessage(res.status, errorText)}`);
|
|
3478
3617
|
else if (res.status === 404) console.error(`[wawesome] Error: Function '${funcName}' not found.`);
|
|
3479
3618
|
else {
|
|
3480
3619
|
console.error(`[wawesome] Error: Failed to list invocations (HTTP ${res.status}).`);
|
|
@@ -3656,8 +3795,8 @@ async function followInvocationLog(gatewayUrl, tenantJwt, invocationId, isVerbos
|
|
|
3656
3795
|
if (isVerbose) console.error(`[wawesome:verbose] ${String(err)}`);
|
|
3657
3796
|
process.exit(1);
|
|
3658
3797
|
}
|
|
3659
|
-
if (res.status === 401) {
|
|
3660
|
-
console.error(
|
|
3798
|
+
if (res.status === 401 || res.status === 403) {
|
|
3799
|
+
console.error(`[wawesome] Error: ${unauthorizedMessage(res.status, await res.text().catch(() => ""))}`);
|
|
3661
3800
|
process.exit(1);
|
|
3662
3801
|
}
|
|
3663
3802
|
if (res.status === 404) {
|
|
@@ -3794,8 +3933,8 @@ async function followFunctionLog(gatewayUrl, tenantJwt, funcNameInput, appOverri
|
|
|
3794
3933
|
console.error("[wawesome] Error: Failed to connect to the live tail after retries.");
|
|
3795
3934
|
process.exit(1);
|
|
3796
3935
|
}
|
|
3797
|
-
if (res.status === 401) {
|
|
3798
|
-
console.error(
|
|
3936
|
+
if (res.status === 401 || res.status === 403) {
|
|
3937
|
+
console.error(`[wawesome] Error: ${unauthorizedMessage(res.status, await res.text().catch(() => ""))}`);
|
|
3799
3938
|
process.exit(1);
|
|
3800
3939
|
}
|
|
3801
3940
|
if (res.status === 404) {
|
|
@@ -3982,8 +4121,8 @@ async function followRun(gatewayUrl, tenantJwt, invocationId, isVerbose, pollInt
|
|
|
3982
4121
|
console.error(`[wawesome] ✖ Run ${meta.status}${meta.failure_reason ? `: ${meta.failure_reason}` : ""}`);
|
|
3983
4122
|
return;
|
|
3984
4123
|
}
|
|
3985
|
-
if (res.status === 401) {
|
|
3986
|
-
console.error(
|
|
4124
|
+
if (res.status === 401 || res.status === 403) {
|
|
4125
|
+
console.error(`[wawesome] Error: ${unauthorizedMessage(res.status, await res.text().catch(() => ""))}`);
|
|
3987
4126
|
process.exit(1);
|
|
3988
4127
|
}
|
|
3989
4128
|
if (!res.ok && res.status >= 500) {
|
|
@@ -4068,8 +4207,8 @@ async function invokeCommand(target, options = {}) {
|
|
|
4068
4207
|
const res = await triggerRun(creds.gateway_url, creds.tenant_jwt, funcName, appSlug, options.app, triggerPayload, isVerbose);
|
|
4069
4208
|
if (!res.ok) {
|
|
4070
4209
|
const errorBody = await res.text();
|
|
4071
|
-
if (res.status === 401) {
|
|
4072
|
-
console.error(
|
|
4210
|
+
if (res.status === 401 || res.status === 403) {
|
|
4211
|
+
console.error(`[wawesome] Error: ${unauthorizedMessage(res.status, errorBody)}`);
|
|
4073
4212
|
process.exit(1);
|
|
4074
4213
|
}
|
|
4075
4214
|
if (res.status === 404) {
|
|
@@ -4091,7 +4230,7 @@ async function invokeCommand(target, options = {}) {
|
|
|
4091
4230
|
}
|
|
4092
4231
|
//#endregion
|
|
4093
4232
|
//#region src/workspace.ts
|
|
4094
|
-
function requireCredentials$
|
|
4233
|
+
function requireCredentials$2() {
|
|
4095
4234
|
const creds = readCredentials();
|
|
4096
4235
|
if (!creds) {
|
|
4097
4236
|
console.error("[wawesome] Error: Not logged in. Run 'wawesome login' first.");
|
|
@@ -4104,7 +4243,7 @@ function requireCredentials$1() {
|
|
|
4104
4243
|
* change — the rename is offered here rather than discovered by attempting it.
|
|
4105
4244
|
*/
|
|
4106
4245
|
async function showWorkspace(options) {
|
|
4107
|
-
const creds = requireCredentials$
|
|
4246
|
+
const creds = requireCredentials$2();
|
|
4108
4247
|
let tenant;
|
|
4109
4248
|
try {
|
|
4110
4249
|
tenant = await fetchTenantDetails(creds);
|
|
@@ -4139,7 +4278,7 @@ async function showWorkspace(options) {
|
|
|
4139
4278
|
* gateway knows something this command could not.
|
|
4140
4279
|
*/
|
|
4141
4280
|
async function renameWorkspace(slug, options) {
|
|
4142
|
-
const creds = requireCredentials$
|
|
4281
|
+
const creds = requireCredentials$2();
|
|
4143
4282
|
if (!slug || !slug.trim()) {
|
|
4144
4283
|
console.error("[wawesome] Error: No name given. Usage: wawesome workspace rename <name>");
|
|
4145
4284
|
process.exit(1);
|
|
@@ -4183,6 +4322,267 @@ async function workspaceCommand(action, target, options) {
|
|
|
4183
4322
|
process.exit(1);
|
|
4184
4323
|
}
|
|
4185
4324
|
//#endregion
|
|
4325
|
+
//#region src/deploy-credentials.ts
|
|
4326
|
+
const SECRET_SHOWN_ONCE = "Copy it now. This is the only time it will be shown. If you lose it, revoke this credential and mint another.";
|
|
4327
|
+
function requireCredentials$1() {
|
|
4328
|
+
const creds = readCredentials();
|
|
4329
|
+
if (!creds) {
|
|
4330
|
+
console.error("[wawesome] Error: Not logged in. Run 'wawesome login' first.");
|
|
4331
|
+
process.exit(1);
|
|
4332
|
+
}
|
|
4333
|
+
return creds;
|
|
4334
|
+
}
|
|
4335
|
+
function fail(message, ...rest) {
|
|
4336
|
+
console.error(`[wawesome] Error: ${message}`);
|
|
4337
|
+
for (const line of rest) console.error(`[wawesome] ${line}`);
|
|
4338
|
+
process.exit(1);
|
|
4339
|
+
}
|
|
4340
|
+
/** A repeated flag and a comma-separated one, read as the same list. */
|
|
4341
|
+
function asList(raw) {
|
|
4342
|
+
if (raw === void 0) return void 0;
|
|
4343
|
+
const words = (Array.isArray(raw) ? raw : [raw]).flatMap((word) => String(word).split(",")).map((word) => word.trim()).filter((word) => word.length > 0);
|
|
4344
|
+
return words.length > 0 ? words : void 0;
|
|
4345
|
+
}
|
|
4346
|
+
function parseExpiry(raw) {
|
|
4347
|
+
if (raw === void 0) return void 0;
|
|
4348
|
+
const asked = String(raw).trim().toLowerCase();
|
|
4349
|
+
if (asked === "never") return "never";
|
|
4350
|
+
const days = /^(\d+)\s*d(ays?)?$/.exec(asked) ?? /^(\d+)$/.exec(asked);
|
|
4351
|
+
if (!days) fail(`'${raw}' is not an expiry. Name a number of days (--expires 30) or 'never' (--expires never).`);
|
|
4352
|
+
return { days: Number(days[1]) };
|
|
4353
|
+
}
|
|
4354
|
+
/**
|
|
4355
|
+
* What a target may be repeated back as.
|
|
4356
|
+
*
|
|
4357
|
+
* Somebody revoking a leak pastes the whole secret as often as the prefix, and
|
|
4358
|
+
* a refusal that echoed it would write it into a shell history and a CI log —
|
|
4359
|
+
* for the one feature built on the secret being shown exactly once.
|
|
4360
|
+
*/
|
|
4361
|
+
function asShown(target) {
|
|
4362
|
+
if (!target.startsWith("wawe_")) return target;
|
|
4363
|
+
const shown = credentialPrefix(target);
|
|
4364
|
+
return shown === target ? target : `${shown}…`;
|
|
4365
|
+
}
|
|
4366
|
+
/** The Apps a credential names, or `null` where it names none and reaches all. */
|
|
4367
|
+
function namedApps(apps) {
|
|
4368
|
+
return apps && apps.scope === "only" ? apps.apps.join(", ") : null;
|
|
4369
|
+
}
|
|
4370
|
+
/** `2026-11-30 12:00 UTC`. Minutes, because nothing here is read to the second. */
|
|
4371
|
+
function asUtc(iso) {
|
|
4372
|
+
if (!iso) return "never";
|
|
4373
|
+
const at = new Date(iso);
|
|
4374
|
+
if (Number.isNaN(at.getTime())) return iso;
|
|
4375
|
+
return `${at.toISOString().slice(0, 16).replace("T", " ")} UTC`;
|
|
4376
|
+
}
|
|
4377
|
+
function isExpired(credential, now) {
|
|
4378
|
+
if (!credential.expires_at) return false;
|
|
4379
|
+
const at = new Date(credential.expires_at).getTime();
|
|
4380
|
+
return !Number.isNaN(at) && at <= now;
|
|
4381
|
+
}
|
|
4382
|
+
function statusOf(credential, now) {
|
|
4383
|
+
if (credential.revoked_at) return "revoked";
|
|
4384
|
+
if (isExpired(credential, now)) return "expired";
|
|
4385
|
+
return "active";
|
|
4386
|
+
}
|
|
4387
|
+
async function refusal(res, fallback) {
|
|
4388
|
+
if (res.status === 401 || res.status === 403) return new GatewayError(unauthorizedMessage(res.status, await res.text().catch(() => "")), res.status);
|
|
4389
|
+
return asGatewayError(res, fallback);
|
|
4390
|
+
}
|
|
4391
|
+
async function mintCredential(creds, payload) {
|
|
4392
|
+
const res = await authorizedFetch(`${creds.gateway_url}/v1/tenant/credentials`, {
|
|
4393
|
+
method: "POST",
|
|
4394
|
+
headers: {
|
|
4395
|
+
Authorization: `Bearer ${creds.tenant_jwt}`,
|
|
4396
|
+
"Content-Type": "application/json"
|
|
4397
|
+
},
|
|
4398
|
+
body: JSON.stringify(payload)
|
|
4399
|
+
});
|
|
4400
|
+
if (!res.ok) throw await refusal(res, `Failed to mint the credential (HTTP ${res.status}).`);
|
|
4401
|
+
return await res.json();
|
|
4402
|
+
}
|
|
4403
|
+
async function fetchCredentials(creds) {
|
|
4404
|
+
const res = await authorizedFetch(`${creds.gateway_url}/v1/tenant/credentials`, { headers: { Authorization: `Bearer ${creds.tenant_jwt}` } });
|
|
4405
|
+
if (!res.ok) throw await refusal(res, `Failed to read the credentials (HTTP ${res.status}).`);
|
|
4406
|
+
return (await res.json()).credentials ?? [];
|
|
4407
|
+
}
|
|
4408
|
+
async function revokeCredential(creds, credentialId) {
|
|
4409
|
+
const res = await authorizedFetch(`${creds.gateway_url}/v1/tenant/credentials/${encodeURIComponent(credentialId)}/revoke`, {
|
|
4410
|
+
method: "POST",
|
|
4411
|
+
headers: { Authorization: `Bearer ${creds.tenant_jwt}` }
|
|
4412
|
+
});
|
|
4413
|
+
if (!res.ok) throw await refusal(res, `Failed to revoke the credential (HTTP ${res.status}).`);
|
|
4414
|
+
return await res.json();
|
|
4415
|
+
}
|
|
4416
|
+
/**
|
|
4417
|
+
* Mint one credential.
|
|
4418
|
+
*
|
|
4419
|
+
* The secret goes to stdout on a line of its own and everything a person reads
|
|
4420
|
+
* goes to stderr, so `wawesome credentials mint ci > secret` writes the secret
|
|
4421
|
+
* and not one character besides.
|
|
4422
|
+
*/
|
|
4423
|
+
async function mintCredentialCommand(name, options) {
|
|
4424
|
+
if (!name || !name.trim()) fail("No name given. Usage: wawesome credentials mint <name> [--capability deploy] [--app <app>] [--expires 90|never]", "The name is what tells the CI one from the agent one a year from now.");
|
|
4425
|
+
const creds = requireCredentials$1();
|
|
4426
|
+
const payload = { name: name.trim() };
|
|
4427
|
+
const capabilities = asList(options.capability);
|
|
4428
|
+
if (capabilities) payload.capabilities = capabilities;
|
|
4429
|
+
const apps = asList(options.app);
|
|
4430
|
+
if (apps) payload.apps = apps;
|
|
4431
|
+
const expiry = parseExpiry(options.expires);
|
|
4432
|
+
if (expiry !== void 0) payload.expiry = expiry;
|
|
4433
|
+
if (options.verbose) {
|
|
4434
|
+
console.error(`[wawesome:verbose] POST ${creds.gateway_url}/v1/tenant/credentials`);
|
|
4435
|
+
console.error(`[wawesome:verbose] Payload: ${JSON.stringify({ ...payload })}`);
|
|
4436
|
+
}
|
|
4437
|
+
let credential;
|
|
4438
|
+
try {
|
|
4439
|
+
credential = await mintCredential(creds, payload);
|
|
4440
|
+
} catch (err) {
|
|
4441
|
+
fail(errorText(err));
|
|
4442
|
+
}
|
|
4443
|
+
console.error(`\n[wawesome] ✔ Minted deploy credential '${credential.name}'.\n`);
|
|
4444
|
+
console.error(` Prefix: ${credential.prefix}`);
|
|
4445
|
+
console.error(` Capabilities: ${credential.capabilities.join(", ")}`);
|
|
4446
|
+
console.error(` Apps: ${namedApps(credential.apps) ?? "every App in the workspace"}`);
|
|
4447
|
+
console.error(` Expires: ${asUtc(credential.expires_at)}`);
|
|
4448
|
+
console.error(`\n ${SECRET_SHOWN_ONCE}\n`);
|
|
4449
|
+
console.log(credential.secret);
|
|
4450
|
+
console.error("");
|
|
4451
|
+
}
|
|
4452
|
+
async function listCredentialsCommand(options) {
|
|
4453
|
+
const creds = requireCredentials$1();
|
|
4454
|
+
if (options.verbose) console.error(`[wawesome:verbose] GET ${creds.gateway_url}/v1/tenant/credentials`);
|
|
4455
|
+
let credentials;
|
|
4456
|
+
try {
|
|
4457
|
+
credentials = await fetchCredentials(creds);
|
|
4458
|
+
} catch (err) {
|
|
4459
|
+
fail(errorText(err));
|
|
4460
|
+
}
|
|
4461
|
+
if (credentials.length === 0) {
|
|
4462
|
+
console.log("\n[wawesome] This workspace has no deploy credentials.");
|
|
4463
|
+
console.log("[wawesome] Mint one: \x1B[36mwawesome credentials mint <name>\x1B[0m\n");
|
|
4464
|
+
return;
|
|
4465
|
+
}
|
|
4466
|
+
console.log("\n🔑 \x1B[1mDeploy credentials\x1B[0m\n");
|
|
4467
|
+
for (const line of formatCredentials(credentials, Date.now())) console.log(line);
|
|
4468
|
+
console.log("");
|
|
4469
|
+
}
|
|
4470
|
+
/** Never the secret: no read after minting carries one. */
|
|
4471
|
+
function formatCredentials(credentials, now) {
|
|
4472
|
+
const rows = credentials.map((credential) => ({
|
|
4473
|
+
name: credential.name,
|
|
4474
|
+
prefix: credential.prefix,
|
|
4475
|
+
capabilities: credential.capabilities.join(", "),
|
|
4476
|
+
apps: namedApps(credential.apps) ?? "every App",
|
|
4477
|
+
lastUsed: asUtc(credential.last_used_at),
|
|
4478
|
+
expires: asUtc(credential.expires_at),
|
|
4479
|
+
status: statusOf(credential, now)
|
|
4480
|
+
}));
|
|
4481
|
+
const columns = [
|
|
4482
|
+
{
|
|
4483
|
+
header: "NAME",
|
|
4484
|
+
of: (row) => row.name
|
|
4485
|
+
},
|
|
4486
|
+
{
|
|
4487
|
+
header: "PREFIX",
|
|
4488
|
+
of: (row) => row.prefix
|
|
4489
|
+
},
|
|
4490
|
+
{
|
|
4491
|
+
header: "CAPABILITIES",
|
|
4492
|
+
of: (row) => row.capabilities
|
|
4493
|
+
},
|
|
4494
|
+
{
|
|
4495
|
+
header: "APPS",
|
|
4496
|
+
of: (row) => row.apps
|
|
4497
|
+
},
|
|
4498
|
+
{
|
|
4499
|
+
header: "LAST USED",
|
|
4500
|
+
of: (row) => row.lastUsed
|
|
4501
|
+
},
|
|
4502
|
+
{
|
|
4503
|
+
header: "EXPIRES",
|
|
4504
|
+
of: (row) => row.expires
|
|
4505
|
+
},
|
|
4506
|
+
{
|
|
4507
|
+
header: "STATUS",
|
|
4508
|
+
of: (row) => row.status
|
|
4509
|
+
}
|
|
4510
|
+
];
|
|
4511
|
+
const widths = columns.map((column) => Math.max(column.header.length, ...rows.map((row) => column.of(row).length)));
|
|
4512
|
+
const line = (cells) => ` ${cells.map((cell, i) => cell.padEnd(widths[i])).join(" | ")}`.trimEnd();
|
|
4513
|
+
return [
|
|
4514
|
+
line(columns.map((column) => column.header)),
|
|
4515
|
+
` ${widths.map((width) => "-".repeat(width)).join("-|-")}`,
|
|
4516
|
+
...rows.map((row) => line(columns.map((column) => column.of(row))))
|
|
4517
|
+
];
|
|
4518
|
+
}
|
|
4519
|
+
/**
|
|
4520
|
+
* Which credential a user meant by a name or a prefix.
|
|
4521
|
+
*
|
|
4522
|
+
* A partial prefix matches, because the point of the displayable prefix is
|
|
4523
|
+
* matching one against a CI settings page by eye — and a partial that names
|
|
4524
|
+
* more than one is refused rather than guessed at, since the guess would be a
|
|
4525
|
+
* revocation.
|
|
4526
|
+
*/
|
|
4527
|
+
function matchCredentials(credentials, target) {
|
|
4528
|
+
const typed = target.trim();
|
|
4529
|
+
const named = credentials.filter((credential) => credential.name === typed);
|
|
4530
|
+
if (named.length > 0) return named;
|
|
4531
|
+
const wanted = typed.startsWith("wawe_") ? credentialPrefix(typed) : typed;
|
|
4532
|
+
return credentials.filter((credential) => credential.prefix.startsWith(wanted));
|
|
4533
|
+
}
|
|
4534
|
+
async function revokeCredentialCommand(target, options) {
|
|
4535
|
+
if (!target || !target.trim()) fail("No credential given. Usage: wawesome credentials revoke <name-or-prefix>");
|
|
4536
|
+
const creds = requireCredentials$1();
|
|
4537
|
+
if (options.verbose) console.error(`[wawesome:verbose] GET ${creds.gateway_url}/v1/tenant/credentials`);
|
|
4538
|
+
let credentials;
|
|
4539
|
+
try {
|
|
4540
|
+
credentials = await fetchCredentials(creds);
|
|
4541
|
+
} catch (err) {
|
|
4542
|
+
fail(errorText(err));
|
|
4543
|
+
}
|
|
4544
|
+
const matched = matchCredentials(credentials, target);
|
|
4545
|
+
if (matched.length === 0) fail(`No deploy credential named or prefixed '${asShown(target)}' in this workspace.`, "Run 'wawesome credentials list' to see what this workspace has.");
|
|
4546
|
+
if (matched.length > 1) fail(`'${asShown(target)}' names more than one credential:`, ...matched.map((credential) => ` ${credential.name} (${credential.prefix})`), "Name one of them, or give more of its prefix.");
|
|
4547
|
+
const credential = matched[0];
|
|
4548
|
+
if (credential.revoked_at) {
|
|
4549
|
+
console.log(`[wawesome] '${credential.name}' (${credential.prefix}) was already revoked at ${asUtc(credential.revoked_at)}. Nothing to do.`);
|
|
4550
|
+
return;
|
|
4551
|
+
}
|
|
4552
|
+
if (!options.yes) {
|
|
4553
|
+
if (!isInteractive()) fail(`Revoking asks for confirmation, and there is nobody to ask.`, `Pass --yes to revoke '${credential.name}' (${credential.prefix}) without being asked.`);
|
|
4554
|
+
let confirmed;
|
|
4555
|
+
try {
|
|
4556
|
+
confirmed = await confirm({
|
|
4557
|
+
message: `Revoke '${credential.name}' (${credential.prefix})? Nothing un-revokes it.`,
|
|
4558
|
+
default: false
|
|
4559
|
+
});
|
|
4560
|
+
} catch {
|
|
4561
|
+
confirmed = false;
|
|
4562
|
+
}
|
|
4563
|
+
if (!confirmed) {
|
|
4564
|
+
console.log(`[wawesome] Cancelled. '${credential.name}' still works.`);
|
|
4565
|
+
return;
|
|
4566
|
+
}
|
|
4567
|
+
}
|
|
4568
|
+
if (options.verbose) console.error(`[wawesome:verbose] POST ${creds.gateway_url}/v1/tenant/credentials/${credential.id}/revoke`);
|
|
4569
|
+
let revoked;
|
|
4570
|
+
try {
|
|
4571
|
+
revoked = await revokeCredential(creds, credential.id);
|
|
4572
|
+
} catch (err) {
|
|
4573
|
+
if (err instanceof GatewayError && err.status === 404) fail(`'${credential.name}' (${credential.prefix}) no longer exists in this workspace.`);
|
|
4574
|
+
fail(errorText(err));
|
|
4575
|
+
}
|
|
4576
|
+
console.log(`\n[wawesome] ✔ Revoked '${revoked.name}' (${revoked.prefix}).`);
|
|
4577
|
+
console.log("[wawesome] Anything still presenting it is refused from its very next request.\n");
|
|
4578
|
+
}
|
|
4579
|
+
async function credentialsCommand(action, target, options) {
|
|
4580
|
+
if (!action || action === "list" || action === "ls") return listCredentialsCommand(options);
|
|
4581
|
+
if (action === "mint") return mintCredentialCommand(target, options);
|
|
4582
|
+
if (action === "revoke") return revokeCredentialCommand(target, options);
|
|
4583
|
+
fail(`Unknown credentials action '${action}'.`, "Usage: wawesome credentials [list|mint <name>|revoke <name-or-prefix>]");
|
|
4584
|
+
}
|
|
4585
|
+
//#endregion
|
|
4186
4586
|
//#region src/cron.ts
|
|
4187
4587
|
/** `2026-08-23 03:00:00 UTC` with second-precision for run history timestamps. */
|
|
4188
4588
|
function asUtcWithSeconds(iso) {
|
|
@@ -4487,8 +4887,8 @@ async function pauseScheduleCommand(target, subtarget, options = {}) {
|
|
|
4487
4887
|
const res = await pauseScheduleApi(creds.gateway_url, creds.tenant_jwt, funcName, scheduleName, appSlug, options.reason, isVerbose);
|
|
4488
4888
|
if (!res.ok) {
|
|
4489
4889
|
const errorBody = await res.text();
|
|
4490
|
-
if (res.status === 401) {
|
|
4491
|
-
console.error(
|
|
4890
|
+
if (res.status === 401 || res.status === 403) {
|
|
4891
|
+
console.error(`[wawesome] Error: ${unauthorizedMessage(res.status, errorBody)}`);
|
|
4492
4892
|
process.exit(1);
|
|
4493
4893
|
}
|
|
4494
4894
|
if (res.status === 404) {
|
|
@@ -4544,8 +4944,8 @@ async function resumeScheduleCommand(target, subtarget, options = {}) {
|
|
|
4544
4944
|
const res = await resumeScheduleApi(creds.gateway_url, creds.tenant_jwt, funcName, scheduleName, appSlug, isVerbose);
|
|
4545
4945
|
if (!res.ok) {
|
|
4546
4946
|
const errorBody = await res.text();
|
|
4547
|
-
if (res.status === 401) {
|
|
4548
|
-
console.error(
|
|
4947
|
+
if (res.status === 401 || res.status === 403) {
|
|
4948
|
+
console.error(`[wawesome] Error: ${unauthorizedMessage(res.status, errorBody)}`);
|
|
4549
4949
|
process.exit(1);
|
|
4550
4950
|
}
|
|
4551
4951
|
if (res.status === 404) {
|
|
@@ -4583,8 +4983,8 @@ async function runHistoryCommand(target, options = {}) {
|
|
|
4583
4983
|
}, isVerbose);
|
|
4584
4984
|
if (!res.ok) {
|
|
4585
4985
|
const errorBody = await res.text();
|
|
4586
|
-
if (res.status === 401) {
|
|
4587
|
-
console.error(
|
|
4986
|
+
if (res.status === 401 || res.status === 403) {
|
|
4987
|
+
console.error(`[wawesome] Error: ${unauthorizedMessage(res.status, errorBody)}`);
|
|
4588
4988
|
process.exit(1);
|
|
4589
4989
|
}
|
|
4590
4990
|
if (res.status === 404) {
|
|
@@ -4634,11 +5034,33 @@ cli.command("env [action] [key] [value]", "Manage environment variables (set, li
|
|
|
4634
5034
|
cli.command("env set <key> <value>", "Set or overwrite an environment variable on the current app").option("-s, --secret", "Flag variable as secret (write-only)").option("-v, --verbose", "Enable verbose debug output").action((key, value, options) => setEnvVar(key, value, options));
|
|
4635
5035
|
cli.command("env list", "List environment variables for the current app").alias("env ls").option("-v, --verbose", "Enable verbose debug output").action((options) => listEnvVars(options));
|
|
4636
5036
|
cli.command("env rm <key>", "Delete an environment variable from the current app").alias("env remove").alias("env delete").alias("env unset").option("-v, --verbose", "Enable verbose debug output").action((key, options) => removeEnvVar(key, options));
|
|
4637
|
-
cli.command("login", "Authenticate with the wawesome.io platform").option("--api <url>", "API URL (default: https://api.wawesome.io)").option("--gateway <url>", "Alias for --api <url>").option("--dashboard <url>", "Dashboard URL (default: https://dashboard.wawesome.io)").option("--provider <name>", "OAuth provider (default: github)").option("--workspace <name>", "Name for the workspace, when signing up without a terminal to prompt").option("-v, --verbose", "Enable verbose debug output").action((options) => login(options));
|
|
5037
|
+
cli.command("login", "Authenticate with the wawesome.io platform").option("--api <url>", "API URL (default: https://api.wawesome.io)").option("--gateway <url>", "Alias for --api <url>").option("--dashboard <url>", "Dashboard URL (default: https://dashboard.wawesome.io)").option("--provider <name>", "OAuth provider (default: github)").option("--workspace <name>", "Name for the workspace, when signing up without a terminal to prompt").option("--accept-terms", "Accept the terms of service and privacy policy, when signing up without a terminal to prompt").option("-v, --verbose", "Enable verbose debug output").action((options) => login(options));
|
|
4638
5038
|
cli.command("logout", "Clear stored authentication credentials").action(() => logout());
|
|
4639
5039
|
cli.command("whoami", "Show current login session info").action(() => whoami());
|
|
4640
5040
|
cli.command("workspace [action] [name]", "Show the workspace, or rename its public address").usage("workspace <action> [name]\n\nActions:\n show Show the workspace name, address, and whether it can still change\n rename <name> Change the public address, while nothing live depends on it").example("wawesome workspace").example("wawesome workspace rename northwind").option("-v, --verbose", "Enable verbose debug output").action((action, name, options) => workspaceCommand(action, name, options));
|
|
4641
5041
|
cli.command("templates [action]", "Browse the template catalog").usage("templates [action]\n\nActions:\n list (ls) Show every available template (default)").example("wawesome templates").example("wawesome templates list").option("--api <url>", "API URL (default: https://api.wawesome.io)").option("-v, --verbose", "Enable verbose debug output").action((action, options) => templatesCommand(action, options));
|
|
5042
|
+
cli.command("credentials [action] [target]", "Mint, list and revoke deploy credentials").alias("credential").usage(`credentials <action> [options]
|
|
5043
|
+
|
|
5044
|
+
A deploy credential is what a CI pipeline or an agent authenticates with, where
|
|
5045
|
+
there is nobody at a keyboard to log in. Minting is the only time the secret is
|
|
5046
|
+
shown. It is printed on its own line, so a pipe catches it and nothing else.
|
|
5047
|
+
|
|
5048
|
+
Actions:
|
|
5049
|
+
list (ls) Show every credential: name, prefix, capabilities, Apps, last use, expiry (default)
|
|
5050
|
+
mint <name> Mint one, printing the secret exactly once
|
|
5051
|
+
revoke <name-or-prefix> Revoke one, immediately and for good
|
|
5052
|
+
|
|
5053
|
+
Examples:
|
|
5054
|
+
wawesome credentials
|
|
5055
|
+
wawesome credentials mint ci-pipeline
|
|
5056
|
+
wawesome credentials mint agent -c logs:read --expires never
|
|
5057
|
+
wawesome credentials mint prod-deploy -c deploy -a prod --expires 30
|
|
5058
|
+
wawesome credentials mint ci-pipeline > /tmp/secret
|
|
5059
|
+
wawesome credentials revoke ci-pipeline
|
|
5060
|
+
wawesome credentials revoke wawe_ab3k9x --yes`).option("-c, --capability <capability>", "Capability to grant: deploy, logs:read (repeatable or comma-separated, default: deploy)").option("-a, --app <app>", "Restrict the credential to this App (repeatable or comma-separated, default: every App)").option("--expires <days|never>", "Days until the credential expires, or 'never' (default: 90)").option("-y, --yes", "Skip the revoke confirmation").option("-v, --verbose", "Enable verbose debug output").action((action, target, options) => credentialsCommand(action, target, options));
|
|
5061
|
+
cli.command("credentials mint <name>", "Mint a deploy credential, printing its secret exactly once").option("-c, --capability <capability>", "Capability to grant: deploy, logs:read (repeatable or comma-separated)").option("-a, --app <app>", "Restrict the credential to this App (repeatable or comma-separated)").option("--expires <days|never>", "Days until the credential expires, or 'never' (default: 90)").option("-v, --verbose", "Enable verbose debug output").action((name, options) => mintCredentialCommand(name, options));
|
|
5062
|
+
cli.command("credentials list", "List deploy credentials — never their secrets").alias("credentials ls").option("-v, --verbose", "Enable verbose debug output").action((options) => listCredentialsCommand(options));
|
|
5063
|
+
cli.command("credentials revoke <name-or-prefix>", "Revoke a deploy credential, immediately and for good").option("-y, --yes", "Revoke without being asked to confirm").option("-v, --verbose", "Enable verbose debug output").action((target, options) => revokeCredentialCommand(target, options));
|
|
4642
5064
|
cli.command("init", "Scaffold a new function project in the current directory").usage("init [options]\n\nWith --template, the project is fetched from the template catalog, wired up\nfrom what the template declares it needs, and deployed. Run 'wawesome templates'\nto see what is available.").example("wawesome init").example("wawesome init --template stripe-webhook").option("-t, --template <name>", "Scaffold from a catalog template and deploy it").option("--api <url>", "API URL (default: https://api.wawesome.io)").option("--dashboard <url>", "Dashboard URL (default: https://dashboard.wawesome.io)").option("--no-install", "Skip installing dependencies after scaffolding").option("--root", "Generate a root function router template").option("-v, --verbose", "Enable verbose debug output").action((options) => init(options));
|
|
4643
5065
|
cli.command("logs [function-name-or-invocation-id]", "View invocation history, fetch log output, or follow live").usage(`logs [target] [options]
|
|
4644
5066
|
|