wawesome 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +296 -73
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -895,7 +895,7 @@ async function buildHandler(entryInput, config, options) {
|
|
|
895
895
|
* that has to name this version — `--version`, the dependency a scaffolded
|
|
896
896
|
* project pins — reads it here, so a release bumps one file.
|
|
897
897
|
*/
|
|
898
|
-
const CLI_VERSION = "0.
|
|
898
|
+
const CLI_VERSION = "0.21.0";
|
|
899
899
|
//#endregion
|
|
900
900
|
//#region src/cli-login.ts
|
|
901
901
|
const START_PATH = "/v1/cli/login/start";
|
|
@@ -1207,7 +1207,7 @@ async function renewedSession(creds, tenantId) {
|
|
|
1207
1207
|
if (res.status === 429) throw await asGatewayError(res, "The platform is rate limiting this session's renewals.");
|
|
1208
1208
|
if (res.status >= 400 && res.status < 500) {
|
|
1209
1209
|
const refused = await asGatewayError(res, "Your session could not be renewed.");
|
|
1210
|
-
if (tenantId && refused.reason === "not-a-member") throw refused;
|
|
1210
|
+
if (tenantId && (refused.reason === "not-a-member" || refused.reason === "session-ended")) throw refused;
|
|
1211
1211
|
endSession(refused.message);
|
|
1212
1212
|
}
|
|
1213
1213
|
if (!res.ok) throw new Error(`Could not renew your session (HTTP ${res.status}).`);
|
|
@@ -2734,7 +2734,22 @@ async function fetchAppFunctions(gatewayUrl, tenantJwt, appSlug, isVerbose) {
|
|
|
2734
2734
|
headers: { Authorization: `Bearer ${tenantJwt}` }
|
|
2735
2735
|
});
|
|
2736
2736
|
if (!res.ok) return null;
|
|
2737
|
-
|
|
2737
|
+
const data = await res.json();
|
|
2738
|
+
return (Array.isArray(data) ? data : Array.isArray(data?.functions) ? data.functions : []).map((item) => ({
|
|
2739
|
+
id: item.function?.id || item.id,
|
|
2740
|
+
name: item.function?.name || item.name || "",
|
|
2741
|
+
slug: item.function?.slug || item.slug || "",
|
|
2742
|
+
visibility: item.function?.visibility || item.visibility || "public",
|
|
2743
|
+
function: {
|
|
2744
|
+
id: item.function?.id || item.id || "",
|
|
2745
|
+
name: item.function?.name || item.name || "",
|
|
2746
|
+
slug: item.function?.slug || item.slug || "",
|
|
2747
|
+
app_id: item.function?.app_id || item.app_id || "",
|
|
2748
|
+
visibility: item.function?.visibility || item.visibility || "public",
|
|
2749
|
+
created_at: item.function?.created_at || item.created_at || "",
|
|
2750
|
+
updated_at: item.function?.updated_at || item.updated_at || ""
|
|
2751
|
+
}
|
|
2752
|
+
}));
|
|
2738
2753
|
}
|
|
2739
2754
|
async function pauseScheduleApi(gatewayUrl, tenantJwt, funcName, scheduleName, appSlug, reason, isVerbose) {
|
|
2740
2755
|
const scopedUrl = appSlug ? `${gatewayUrl}/v1/apps/${encodeURIComponent(appSlug)}/functions/${encodeURIComponent(funcName)}/schedules/${encodeURIComponent(scheduleName)}/pause` : `${gatewayUrl}/v1/functions/${encodeURIComponent(funcName)}/schedules/${encodeURIComponent(scheduleName)}/pause`;
|
|
@@ -2833,9 +2848,11 @@ async function listSchedulesCommand(target, options = {}) {
|
|
|
2833
2848
|
if (appFuncs !== null) {
|
|
2834
2849
|
const functionSchedules = [];
|
|
2835
2850
|
for (const item of appFuncs) {
|
|
2836
|
-
const
|
|
2851
|
+
const name = item.function?.name || item.name;
|
|
2852
|
+
if (!name) continue;
|
|
2853
|
+
const scheds = await fetchFunctionSchedules(creds.gateway_url, creds.tenant_jwt, name, target, isVerbose);
|
|
2837
2854
|
functionSchedules.push({
|
|
2838
|
-
functionName:
|
|
2855
|
+
functionName: name,
|
|
2839
2856
|
schedules: scheds || []
|
|
2840
2857
|
});
|
|
2841
2858
|
}
|
|
@@ -2859,9 +2876,11 @@ async function listSchedulesCommand(target, options = {}) {
|
|
|
2859
2876
|
}
|
|
2860
2877
|
const functionSchedules = [];
|
|
2861
2878
|
for (const item of appFuncs) {
|
|
2862
|
-
const
|
|
2879
|
+
const name = item.function?.name || item.name;
|
|
2880
|
+
if (!name) continue;
|
|
2881
|
+
const scheds = await fetchFunctionSchedules(creds.gateway_url, creds.tenant_jwt, name, appSlug, isVerbose);
|
|
2863
2882
|
functionSchedules.push({
|
|
2864
|
-
functionName:
|
|
2883
|
+
functionName: name,
|
|
2865
2884
|
schedules: scheds || []
|
|
2866
2885
|
});
|
|
2867
2886
|
}
|
|
@@ -3543,6 +3562,97 @@ async function uploadAssets(creds, app, funcName, bundle, assets, declared, isVe
|
|
|
3543
3562
|
console.log(`[wawesome] ✅ ${toUpload.length} asset(s) uploaded.`);
|
|
3544
3563
|
}
|
|
3545
3564
|
//#endregion
|
|
3565
|
+
//#region src/opens-host.ts
|
|
3566
|
+
/**
|
|
3567
|
+
* The host a variable declared with `opens_host` names, for the App's allowlist.
|
|
3568
|
+
* Null for a blank value. A refusal names the variable and never repeats the
|
|
3569
|
+
* value, since a connection string carries a password.
|
|
3570
|
+
*/
|
|
3571
|
+
function hostOpenedBy(key, value) {
|
|
3572
|
+
const trimmed = value.trim();
|
|
3573
|
+
if (!trimmed) return null;
|
|
3574
|
+
const refuse = (why) => {
|
|
3575
|
+
throw new Error(`${key} ${why}`);
|
|
3576
|
+
};
|
|
3577
|
+
let url;
|
|
3578
|
+
try {
|
|
3579
|
+
url = new URL(trimmed);
|
|
3580
|
+
} catch {
|
|
3581
|
+
return refuse("is not an https:// address, so there is no host to open.");
|
|
3582
|
+
}
|
|
3583
|
+
if (url.protocol !== "https:") return refuse(`must be an https:// address, not ${url.protocol}//. Outbound calls are HTTPS only.`);
|
|
3584
|
+
if (url.hostname.startsWith("[")) return refuse("names an IPv6 address. The allowlist takes a host name, not an IPv6 literal.");
|
|
3585
|
+
if (url.port) return refuse(`names port ${url.port}. Only the default HTTPS port is opened from a variable; add the host and port under Egress in the dashboard.`);
|
|
3586
|
+
const host = url.hostname.toLowerCase().replace(/\.$/, "");
|
|
3587
|
+
if (!host) return refuse("names no host.");
|
|
3588
|
+
return host;
|
|
3589
|
+
}
|
|
3590
|
+
//#endregion
|
|
3591
|
+
//#region src/provision.ts
|
|
3592
|
+
/** The gateway's own words for a failure, or a plain HTTP status if it gave none. */
|
|
3593
|
+
async function rejected(res, fallback) {
|
|
3594
|
+
if (res.status === 401 || res.status === 403) return new GatewayError(unauthorizedMessage(res.status, await res.text().catch(() => "")), res.status);
|
|
3595
|
+
return asGatewayError(res, `${fallback} (HTTP ${res.status}).`);
|
|
3596
|
+
}
|
|
3597
|
+
function authHeaders(creds) {
|
|
3598
|
+
return {
|
|
3599
|
+
Authorization: `Bearer ${creds.tenant_jwt}`,
|
|
3600
|
+
"Content-Type": "application/json"
|
|
3601
|
+
};
|
|
3602
|
+
}
|
|
3603
|
+
/**
|
|
3604
|
+
* Make sure the App exists before anything is attached to it.
|
|
3605
|
+
*
|
|
3606
|
+
* Environment variables and subscriptions are both App-scoped, and neither
|
|
3607
|
+
* endpoint creates one — deploy would, but it runs last, and a Function that
|
|
3608
|
+
* goes live before its secrets do is a Function that fails its first request.
|
|
3609
|
+
*
|
|
3610
|
+
* Listing first rather than creating and reading the error keeps the common
|
|
3611
|
+
* case ("I already have this App") off the failure path; an App that appears in
|
|
3612
|
+
* between is still treated as the success it is.
|
|
3613
|
+
*/
|
|
3614
|
+
async function ensureApp(creds, appSlug) {
|
|
3615
|
+
const listRes = await authorizedFetch(`${creds.gateway_url}/v1/apps`, { headers: { Authorization: `Bearer ${creds.tenant_jwt}` } });
|
|
3616
|
+
if (!listRes.ok) throw await rejected(listRes, "Could not list the Tenant's apps");
|
|
3617
|
+
if ((await listRes.json()).applications?.some((app) => app.slug === appSlug)) return;
|
|
3618
|
+
const createRes = await authorizedFetch(`${creds.gateway_url}/v1/apps`, {
|
|
3619
|
+
method: "POST",
|
|
3620
|
+
headers: authHeaders(creds),
|
|
3621
|
+
body: JSON.stringify({
|
|
3622
|
+
name: appSlug,
|
|
3623
|
+
slug: appSlug
|
|
3624
|
+
})
|
|
3625
|
+
});
|
|
3626
|
+
if (createRes.ok) return;
|
|
3627
|
+
const error = await rejected(createRes, `Could not create the app '${appSlug}'`);
|
|
3628
|
+
if (/already exists/i.test(error.message)) return;
|
|
3629
|
+
throw error;
|
|
3630
|
+
}
|
|
3631
|
+
/**
|
|
3632
|
+
* Enable one catalogue provider for the App.
|
|
3633
|
+
*
|
|
3634
|
+
* A template declares the providers it calls; without the subscription the
|
|
3635
|
+
* platform's egress policy denies those calls, so this is what stands between a
|
|
3636
|
+
* scaffolded template and an outbound request that silently fails at runtime.
|
|
3637
|
+
*/
|
|
3638
|
+
async function enableEgressProvider(creds, appSlug, providerKey) {
|
|
3639
|
+
const res = await authorizedFetch(`${creds.gateway_url}/v1/apps/${encodeURIComponent(appSlug)}/egress/subscriptions`, {
|
|
3640
|
+
method: "POST",
|
|
3641
|
+
headers: authHeaders(creds),
|
|
3642
|
+
body: JSON.stringify({ provider_key: providerKey })
|
|
3643
|
+
});
|
|
3644
|
+
if (!res.ok) throw await rejected(res, `Could not enable outbound provider '${providerKey}'`);
|
|
3645
|
+
}
|
|
3646
|
+
/** Add a bare host to the App's allowlist. The gateway upserts, so a repeat is harmless. */
|
|
3647
|
+
async function allowHost(creds, appSlug, host) {
|
|
3648
|
+
const res = await authorizedFetch(`${creds.gateway_url}/v1/apps/${encodeURIComponent(appSlug)}/egress`, {
|
|
3649
|
+
method: "POST",
|
|
3650
|
+
headers: authHeaders(creds),
|
|
3651
|
+
body: JSON.stringify({ host_pattern: host })
|
|
3652
|
+
});
|
|
3653
|
+
if (!res.ok) throw await rejected(res, `Could not open outbound calls to '${host}'`);
|
|
3654
|
+
}
|
|
3655
|
+
//#endregion
|
|
3546
3656
|
//#region src/env.ts
|
|
3547
3657
|
const STANDARD_SECRET_MESSAGES = [
|
|
3548
3658
|
"Encrypted at rest using AES-256",
|
|
@@ -3566,7 +3676,8 @@ function loadClientContext() {
|
|
|
3566
3676
|
}
|
|
3567
3677
|
return {
|
|
3568
3678
|
creds,
|
|
3569
|
-
app: config.app.trim()
|
|
3679
|
+
app: config.app.trim(),
|
|
3680
|
+
opensHost: config.opens_host ?? []
|
|
3570
3681
|
};
|
|
3571
3682
|
}
|
|
3572
3683
|
/**
|
|
@@ -3604,8 +3715,18 @@ async function setEnvVar(key, value, options) {
|
|
|
3604
3715
|
console.error("[wawesome] Error: Missing key or value. Usage: wawesome env set <KEY> <VALUE> [--secret]");
|
|
3605
3716
|
process.exit(1);
|
|
3606
3717
|
}
|
|
3607
|
-
const { creds, app } = loadClientContext();
|
|
3718
|
+
const { creds, app, opensHost } = loadClientContext();
|
|
3608
3719
|
const isSecret = Boolean(options.secret);
|
|
3720
|
+
const opensAHost = opensHost.includes(key);
|
|
3721
|
+
let host = null;
|
|
3722
|
+
if (opensAHost) try {
|
|
3723
|
+
host = hostOpenedBy(key, value);
|
|
3724
|
+
} catch (err) {
|
|
3725
|
+
console.error(`[wawesome] Error: ${errorText(err)}`);
|
|
3726
|
+
process.exit(1);
|
|
3727
|
+
return;
|
|
3728
|
+
}
|
|
3729
|
+
const previous = opensAHost ? await previousValue(creds, app, key) : void 0;
|
|
3609
3730
|
if (isVerbose) {
|
|
3610
3731
|
console.log(`[wawesome:verbose] POST ${creds.gateway_url}/v1/apps/${encodeURIComponent(app)}/env`);
|
|
3611
3732
|
console.log(`[wawesome:verbose] Payload: key=${key}, is_secret=${isSecret}`);
|
|
@@ -3626,6 +3747,41 @@ async function setEnvVar(key, value, options) {
|
|
|
3626
3747
|
for (const msg of messages) console.log(` • ${msg}`);
|
|
3627
3748
|
console.log("");
|
|
3628
3749
|
} else console.log(`[wawesome] ✅ Set variable '${key}' on app '${app}'.`);
|
|
3750
|
+
if (host) {
|
|
3751
|
+
try {
|
|
3752
|
+
await allowHost(creds, app, host);
|
|
3753
|
+
} catch (err) {
|
|
3754
|
+
console.error(`[wawesome] Error: ${errorText(err)}`);
|
|
3755
|
+
console.error(`[wawesome] '${key}' is set, but its host is not open. Run the same 'wawesome env set' again.`);
|
|
3756
|
+
process.exit(1);
|
|
3757
|
+
return;
|
|
3758
|
+
}
|
|
3759
|
+
console.log(`[wawesome] ✅ Opened outbound calls to '${host}', from ${key}.`);
|
|
3760
|
+
}
|
|
3761
|
+
if (previous?.is_secret) console.log(`[wawesome] ${key} was a secret, so the host its previous value opened cannot be read back. If that host changed, its entry stays on the allowlist; remove it under Egress in the dashboard if nothing else uses it.`);
|
|
3762
|
+
else if (previous) {
|
|
3763
|
+
const previousHost = derivedOrNull(key, previous.value);
|
|
3764
|
+
if (previousHost && previousHost !== host) console.log(`[wawesome] The allowlist entry '${previousHost}' no longer matches ${key}. It stays; remove it under Egress in the dashboard if nothing else uses it.`);
|
|
3765
|
+
}
|
|
3766
|
+
}
|
|
3767
|
+
async function previousValue(creds, app, key) {
|
|
3768
|
+
try {
|
|
3769
|
+
const res = await authorizedFetch(`${creds.gateway_url}/v1/apps/${encodeURIComponent(app)}/env`, {
|
|
3770
|
+
method: "GET",
|
|
3771
|
+
headers: { Authorization: `Bearer ${creds.tenant_jwt}` }
|
|
3772
|
+
});
|
|
3773
|
+
if (!res.ok) return void 0;
|
|
3774
|
+
return (await res.json()).variables?.find((variable) => variable.key === key);
|
|
3775
|
+
} catch {
|
|
3776
|
+
return;
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
function derivedOrNull(key, value) {
|
|
3780
|
+
try {
|
|
3781
|
+
return hostOpenedBy(key, value);
|
|
3782
|
+
} catch {
|
|
3783
|
+
return null;
|
|
3784
|
+
}
|
|
3629
3785
|
}
|
|
3630
3786
|
/**
|
|
3631
3787
|
* List environment variables on the current app.
|
|
@@ -3768,62 +3924,6 @@ function installDependencies(dir, options = {}) {
|
|
|
3768
3924
|
};
|
|
3769
3925
|
}
|
|
3770
3926
|
//#endregion
|
|
3771
|
-
//#region src/provision.ts
|
|
3772
|
-
/** The gateway's own words for a failure, or a plain HTTP status if it gave none. */
|
|
3773
|
-
async function rejected(res, fallback) {
|
|
3774
|
-
if (res.status === 401 || res.status === 403) return new GatewayError(unauthorizedMessage(res.status, await res.text().catch(() => "")), res.status);
|
|
3775
|
-
return asGatewayError(res, `${fallback} (HTTP ${res.status}).`);
|
|
3776
|
-
}
|
|
3777
|
-
function authHeaders(creds) {
|
|
3778
|
-
return {
|
|
3779
|
-
Authorization: `Bearer ${creds.tenant_jwt}`,
|
|
3780
|
-
"Content-Type": "application/json"
|
|
3781
|
-
};
|
|
3782
|
-
}
|
|
3783
|
-
/**
|
|
3784
|
-
* Make sure the App exists before anything is attached to it.
|
|
3785
|
-
*
|
|
3786
|
-
* Environment variables and subscriptions are both App-scoped, and neither
|
|
3787
|
-
* endpoint creates one — deploy would, but it runs last, and a Function that
|
|
3788
|
-
* goes live before its secrets do is a Function that fails its first request.
|
|
3789
|
-
*
|
|
3790
|
-
* Listing first rather than creating and reading the error keeps the common
|
|
3791
|
-
* case ("I already have this App") off the failure path; an App that appears in
|
|
3792
|
-
* between is still treated as the success it is.
|
|
3793
|
-
*/
|
|
3794
|
-
async function ensureApp(creds, appSlug) {
|
|
3795
|
-
const listRes = await authorizedFetch(`${creds.gateway_url}/v1/apps`, { headers: { Authorization: `Bearer ${creds.tenant_jwt}` } });
|
|
3796
|
-
if (!listRes.ok) throw await rejected(listRes, "Could not list the Tenant's apps");
|
|
3797
|
-
if ((await listRes.json()).applications?.some((app) => app.slug === appSlug)) return;
|
|
3798
|
-
const createRes = await authorizedFetch(`${creds.gateway_url}/v1/apps`, {
|
|
3799
|
-
method: "POST",
|
|
3800
|
-
headers: authHeaders(creds),
|
|
3801
|
-
body: JSON.stringify({
|
|
3802
|
-
name: appSlug,
|
|
3803
|
-
slug: appSlug
|
|
3804
|
-
})
|
|
3805
|
-
});
|
|
3806
|
-
if (createRes.ok) return;
|
|
3807
|
-
const error = await rejected(createRes, `Could not create the app '${appSlug}'`);
|
|
3808
|
-
if (/already exists/i.test(error.message)) return;
|
|
3809
|
-
throw error;
|
|
3810
|
-
}
|
|
3811
|
-
/**
|
|
3812
|
-
* Enable one catalogue provider for the App.
|
|
3813
|
-
*
|
|
3814
|
-
* A template declares the providers it calls; without the subscription the
|
|
3815
|
-
* platform's egress policy denies those calls, so this is what stands between a
|
|
3816
|
-
* scaffolded template and an outbound request that silently fails at runtime.
|
|
3817
|
-
*/
|
|
3818
|
-
async function enableEgressProvider(creds, appSlug, providerKey) {
|
|
3819
|
-
const res = await authorizedFetch(`${creds.gateway_url}/v1/apps/${encodeURIComponent(appSlug)}/egress/subscriptions`, {
|
|
3820
|
-
method: "POST",
|
|
3821
|
-
headers: authHeaders(creds),
|
|
3822
|
-
body: JSON.stringify({ provider_key: providerKey })
|
|
3823
|
-
});
|
|
3824
|
-
if (!res.ok) throw await rejected(res, `Could not enable outbound provider '${providerKey}'`);
|
|
3825
|
-
}
|
|
3826
|
-
//#endregion
|
|
3827
3927
|
//#region src/scaffold.ts
|
|
3828
3928
|
const MANIFEST_FILE = "template.json";
|
|
3829
3929
|
const PACKAGE_FILE = "package.json";
|
|
@@ -3863,6 +3963,18 @@ function applyTemplateIdentity(dir, identity) {
|
|
|
3863
3963
|
function: identity.functionName
|
|
3864
3964
|
}));
|
|
3865
3965
|
}
|
|
3966
|
+
/**
|
|
3967
|
+
* Write down which variables open a host, so a later `env set` knows. Nothing is
|
|
3968
|
+
* written for a template that declares none.
|
|
3969
|
+
*/
|
|
3970
|
+
function recordHostVariables(dir, env) {
|
|
3971
|
+
const keys = env.filter((variable) => variable.opens_host === true).map((variable) => variable.key);
|
|
3972
|
+
if (keys.length === 0) return;
|
|
3973
|
+
patchJson(path.join(dir, FUNCTION_CONFIG_FILE), (config) => ({
|
|
3974
|
+
...config,
|
|
3975
|
+
opens_host: keys
|
|
3976
|
+
}));
|
|
3977
|
+
}
|
|
3866
3978
|
/** The package the CLI publishes itself as, and which a template depends on. */
|
|
3867
3979
|
const CLI_PACKAGE = "wawesome";
|
|
3868
3980
|
/**
|
|
@@ -4462,6 +4574,19 @@ async function offerWorkspaceAddress(session, creds, tenant, appSlug, functionNa
|
|
|
4462
4574
|
*/
|
|
4463
4575
|
async function wireUp(creds, appSlug, manifest, answers) {
|
|
4464
4576
|
const scaffolded = "Your project is scaffolded. Fix this, then run 'wawesome deploy'.";
|
|
4577
|
+
const opened = [];
|
|
4578
|
+
for (const { declared, value } of answers) {
|
|
4579
|
+
if (declared.opens_host !== true) continue;
|
|
4580
|
+
try {
|
|
4581
|
+
const host = hostOpenedBy(declared.key, value);
|
|
4582
|
+
if (host) opened.push({
|
|
4583
|
+
key: declared.key,
|
|
4584
|
+
host
|
|
4585
|
+
});
|
|
4586
|
+
} catch (err) {
|
|
4587
|
+
fail$1(errorText(err), "Your project is scaffolded, and nothing was stored. Run 'wawesome init' again in an empty directory.");
|
|
4588
|
+
}
|
|
4589
|
+
}
|
|
4465
4590
|
try {
|
|
4466
4591
|
await ensureApp(creds, appSlug);
|
|
4467
4592
|
} catch (err) {
|
|
@@ -4488,6 +4613,14 @@ async function wireUp(creds, appSlug, manifest, answers) {
|
|
|
4488
4613
|
}
|
|
4489
4614
|
console.log(`[wawesome] ✅ Enabled outbound calls to '${provider}'.`);
|
|
4490
4615
|
}
|
|
4616
|
+
for (const { key, host } of opened) {
|
|
4617
|
+
try {
|
|
4618
|
+
await allowHost(creds, appSlug, host);
|
|
4619
|
+
} catch (err) {
|
|
4620
|
+
fail$1(errorText(err), scaffolded);
|
|
4621
|
+
}
|
|
4622
|
+
console.log(`[wawesome] ✅ Opened outbound calls to '${host}', from ${key}.`);
|
|
4623
|
+
}
|
|
4491
4624
|
}
|
|
4492
4625
|
/**
|
|
4493
4626
|
* Scaffold from a catalogue template, wire up what it declared, and deploy it.
|
|
@@ -4553,6 +4686,7 @@ async function initFromTemplate(templateName, options) {
|
|
|
4553
4686
|
app: appSlug,
|
|
4554
4687
|
functionName
|
|
4555
4688
|
});
|
|
4689
|
+
recordHostVariables(cwd, manifest.env);
|
|
4556
4690
|
repinned = alignCliDependency(cwd, CLI_VERSION);
|
|
4557
4691
|
} catch (err) {
|
|
4558
4692
|
fail$1(errorText(err), `Some of '${template.name}' may have been written to ${cwd}.`);
|
|
@@ -5690,7 +5824,7 @@ async function renameWorkspace(slug, options) {
|
|
|
5690
5824
|
* hearing about it.
|
|
5691
5825
|
*/
|
|
5692
5826
|
async function listWorkspaces(options) {
|
|
5693
|
-
const read = await requireWorkspaces(requireSession("Your workspaces would be read for that credential's Tenant alone."));
|
|
5827
|
+
const read = await requireWorkspaces(requireSession$1("Your workspaces would be read for that credential's Tenant alone."));
|
|
5694
5828
|
console.log("\n[wawesome] Your workspaces:\n");
|
|
5695
5829
|
const width = Math.max(...read.workspaces.map((workspace) => workspace.name.length));
|
|
5696
5830
|
for (const workspace of read.workspaces) {
|
|
@@ -5710,7 +5844,7 @@ async function listWorkspaces(options) {
|
|
|
5710
5844
|
* so this is where somebody is told rather than where it is decided.
|
|
5711
5845
|
*/
|
|
5712
5846
|
async function switchWorkspace(target, options) {
|
|
5713
|
-
const creds = requireSession("A switch would move a session that credential does not have.");
|
|
5847
|
+
const creds = requireSession$1("A switch would move a session that credential does not have.");
|
|
5714
5848
|
if (!target || !target.trim()) {
|
|
5715
5849
|
console.error("[wawesome] Error: No workspace given. Usage: wawesome workspace switch <name>");
|
|
5716
5850
|
process.exit(1);
|
|
@@ -5757,7 +5891,7 @@ async function workspaceCommand(action, target, options) {
|
|
|
5757
5891
|
* **Deploy credential**: a credential names one Tenant and holds no
|
|
5758
5892
|
* **Membership** anywhere, so there is nothing for it to list or move.
|
|
5759
5893
|
*/
|
|
5760
|
-
function requireSession(consequence) {
|
|
5894
|
+
function requireSession$1(consequence) {
|
|
5761
5895
|
const credential = credentialFromEnvironment();
|
|
5762
5896
|
if (credential) {
|
|
5763
5897
|
reportSessionCommandRefused(credential, consequence);
|
|
@@ -5795,6 +5929,93 @@ function refuse(headline, next, lines) {
|
|
|
5795
5929
|
process.exit(1);
|
|
5796
5930
|
}
|
|
5797
5931
|
//#endregion
|
|
5932
|
+
//#region src/export.ts
|
|
5933
|
+
const POLL_MS = 3e3;
|
|
5934
|
+
async function exportCall(creds, method) {
|
|
5935
|
+
const res = await authorizedFetch(`${creds.gateway_url}/v1/tenant/export`, {
|
|
5936
|
+
method,
|
|
5937
|
+
headers: { Authorization: `Bearer ${creds.tenant_jwt}` }
|
|
5938
|
+
});
|
|
5939
|
+
if (!res.ok) throw await asGatewayError(res, `Failed to ${method === "POST" ? "ask for" : "read"} the export (HTTP ${res.status}).`);
|
|
5940
|
+
return await res.json();
|
|
5941
|
+
}
|
|
5942
|
+
function requestExport(creds) {
|
|
5943
|
+
return exportCall(creds, "POST");
|
|
5944
|
+
}
|
|
5945
|
+
function readExport(creds) {
|
|
5946
|
+
return exportCall(creds, "GET");
|
|
5947
|
+
}
|
|
5948
|
+
function when(instant) {
|
|
5949
|
+
return new Date(instant).toUTCString();
|
|
5950
|
+
}
|
|
5951
|
+
function megabytes(bytes) {
|
|
5952
|
+
return `${(bytes / 1048576).toFixed(1)} MB`;
|
|
5953
|
+
}
|
|
5954
|
+
function describeExport(view) {
|
|
5955
|
+
const current = view.export;
|
|
5956
|
+
if (!current) return ["This workspace has not asked for an export. Run 'wawesome export' to ask for one."];
|
|
5957
|
+
switch (current.status) {
|
|
5958
|
+
case "queued": return [`Export asked for at ${when(current.requested_at)}. It is waiting to be built.`];
|
|
5959
|
+
case "building": return current.last_failed_at ? [`The last attempt to build it failed at ${when(current.last_failed_at)}. It is being tried again.`, "Run 'wawesome export status' later to see where it got to."] : ["The archive is being built. A large workspace takes a while."];
|
|
5960
|
+
case "expired": return [`The last export was deleted on ${when(current.expires_at)}, ${view.retention_days} days after it was built.`, "Run 'wawesome export' to ask for a new one."];
|
|
5961
|
+
case "ready": return [
|
|
5962
|
+
`Ready${current.size_bytes !== null ? ` (${megabytes(current.size_bytes)})` : ""}. Download it here:`,
|
|
5963
|
+
"",
|
|
5964
|
+
` ${current.download.url}`,
|
|
5965
|
+
"",
|
|
5966
|
+
`This link works until ${when(current.download.expires_at)}. Run 'wawesome export status' for a new one.`,
|
|
5967
|
+
`The archive is deleted on ${when(current.expires_at)}.`,
|
|
5968
|
+
"Secret values are not in it. Rotate any secret that mattered."
|
|
5969
|
+
];
|
|
5970
|
+
}
|
|
5971
|
+
}
|
|
5972
|
+
function requireSession() {
|
|
5973
|
+
const credential = credentialFromEnvironment();
|
|
5974
|
+
if (credential) {
|
|
5975
|
+
reportSessionCommandRefused(credential, "Only the workspace's owner, signed in, can export it.");
|
|
5976
|
+
process.exit(1);
|
|
5977
|
+
}
|
|
5978
|
+
const creds = readCredentials();
|
|
5979
|
+
if (!creds) {
|
|
5980
|
+
console.error("[wawesome] Error: Not logged in. Run 'wawesome login' first.");
|
|
5981
|
+
process.exit(1);
|
|
5982
|
+
}
|
|
5983
|
+
return creds;
|
|
5984
|
+
}
|
|
5985
|
+
function print(view) {
|
|
5986
|
+
for (const line of describeExport(view)) console.log(line ? `[wawesome] ${line}` : "");
|
|
5987
|
+
}
|
|
5988
|
+
const settled = (view) => view.export?.status === "ready" || view.export?.status === "expired";
|
|
5989
|
+
async function exportCommand(action, options) {
|
|
5990
|
+
if (action && action !== "status") {
|
|
5991
|
+
console.error(`[wawesome] Error: Unknown export action '${action}'.`);
|
|
5992
|
+
console.error("[wawesome] Usage: wawesome export [status] [--no-wait]");
|
|
5993
|
+
process.exit(1);
|
|
5994
|
+
}
|
|
5995
|
+
const creds = requireSession();
|
|
5996
|
+
try {
|
|
5997
|
+
if (action === "status") {
|
|
5998
|
+
print(await readExport(creds));
|
|
5999
|
+
return;
|
|
6000
|
+
}
|
|
6001
|
+
let view = await requestExport(creds);
|
|
6002
|
+
print(view);
|
|
6003
|
+
if (options.wait === false) return;
|
|
6004
|
+
let said = view.export?.status;
|
|
6005
|
+
while (!settled(view) && !view.export?.last_failed_at) {
|
|
6006
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_MS));
|
|
6007
|
+
view = await readExport(creds);
|
|
6008
|
+
if (view.export?.status !== said) {
|
|
6009
|
+
said = view.export?.status;
|
|
6010
|
+
print(view);
|
|
6011
|
+
}
|
|
6012
|
+
}
|
|
6013
|
+
} catch (err) {
|
|
6014
|
+
console.error(`[wawesome] Error: ${errorText(err)}`);
|
|
6015
|
+
process.exit(1);
|
|
6016
|
+
}
|
|
6017
|
+
}
|
|
6018
|
+
//#endregion
|
|
5798
6019
|
//#region ../shared/mint-surface.generated.ts
|
|
5799
6020
|
const MINT_PRESETS = [
|
|
5800
6021
|
{
|
|
@@ -5890,9 +6111,10 @@ function mintPreset(name) {
|
|
|
5890
6111
|
"write:egress",
|
|
5891
6112
|
"write:billing",
|
|
5892
6113
|
"write:credentials",
|
|
5893
|
-
"write:members"
|
|
6114
|
+
"write:members",
|
|
6115
|
+
"write:exports"
|
|
5894
6116
|
],
|
|
5895
|
-
summary: "Reads the workspace, Apps, Functions, environment variables, domains, invocations, runs, schedules, previews and the egress allowlist, and can rename the workspace, create Apps, deploy Functions, set environment variables, attach domains, detach domains, run Functions, pause and resume schedules, create previews, widen the egress allowlist, change billing, manage credentials and
|
|
6117
|
+
summary: "Reads the workspace, Apps, Functions, environment variables, domains, invocations, runs, schedules, previews and the egress allowlist, and can rename the workspace, create Apps, deploy Functions, set environment variables, attach domains, detach domains, run Functions, pause and resume schedules, create previews, widen the egress allowlist, change billing, manage credentials, manage members and export the workspace.",
|
|
5896
6118
|
withheldFromAPerson: "A workspace has one owner, so 'owner' is not a standing anybody is put at."
|
|
5897
6119
|
},
|
|
5898
6120
|
{
|
|
@@ -5921,7 +6143,7 @@ function mintPreset(name) {
|
|
|
5921
6143
|
"write:credentials",
|
|
5922
6144
|
"write:members"
|
|
5923
6145
|
],
|
|
5924
|
-
summary: "Reads the workspace, Apps, Functions, environment variables, domains, invocations, runs, schedules, previews and the egress allowlist, and can create Apps, deploy Functions, set environment variables, attach domains, detach domains, run Functions, pause and resume schedules, create previews, widen the egress allowlist, manage credentials and manage members. It does not rename the workspace or
|
|
6146
|
+
summary: "Reads the workspace, Apps, Functions, environment variables, domains, invocations, runs, schedules, previews and the egress allowlist, and can create Apps, deploy Functions, set environment variables, attach domains, detach domains, run Functions, pause and resume schedules, create previews, widen the egress allowlist, manage credentials and manage members. It does not rename the workspace, change billing or export the workspace.",
|
|
5925
6147
|
withheldFromAPerson: null
|
|
5926
6148
|
},
|
|
5927
6149
|
{
|
|
@@ -5946,7 +6168,7 @@ function mintPreset(name) {
|
|
|
5946
6168
|
"write:schedules",
|
|
5947
6169
|
"write:previews"
|
|
5948
6170
|
],
|
|
5949
|
-
summary: "Reads the workspace, Apps, Functions, environment variables, domains, invocations, runs, schedules, previews and the egress allowlist, and can create Apps, deploy Functions, set environment variables, attach domains, run Functions, pause and resume schedules and create previews. It does not rename the workspace, detach domains, widen the egress allowlist, change billing, manage credentials or
|
|
6171
|
+
summary: "Reads the workspace, Apps, Functions, environment variables, domains, invocations, runs, schedules, previews and the egress allowlist, and can create Apps, deploy Functions, set environment variables, attach domains, run Functions, pause and resume schedules and create previews. It does not rename the workspace, detach domains, widen the egress allowlist, change billing, manage credentials, manage members or export the workspace.",
|
|
5950
6172
|
withheldFromAPerson: null
|
|
5951
6173
|
},
|
|
5952
6174
|
{
|
|
@@ -5968,7 +6190,7 @@ function mintPreset(name) {
|
|
|
5968
6190
|
"write:env",
|
|
5969
6191
|
"write:runs"
|
|
5970
6192
|
],
|
|
5971
|
-
summary: "Reads the workspace, Apps, Functions, environment variables, domains, invocations, runs, schedules, previews and the egress allowlist, and can create Apps, deploy Functions, set environment variables and run Functions. It does not rename the workspace, attach domains, detach domains, pause and resume schedules, create previews, widen the egress allowlist, change billing, manage credentials or
|
|
6193
|
+
summary: "Reads the workspace, Apps, Functions, environment variables, domains, invocations, runs, schedules, previews and the egress allowlist, and can create Apps, deploy Functions, set environment variables and run Functions. It does not rename the workspace, attach domains, detach domains, pause and resume schedules, create previews, widen the egress allowlist, change billing, manage credentials, manage members or export the workspace.",
|
|
5972
6194
|
withheldFromAPerson: "'deployer' is the recipe a Deploy credential is cut from. A pipeline gets a credential of its own rather than somebody's account."
|
|
5973
6195
|
},
|
|
5974
6196
|
{
|
|
@@ -6454,6 +6676,7 @@ cli.command("login", "Authenticate with the wawesome.io platform").option("--api
|
|
|
6454
6676
|
cli.command("logout", "Clear stored authentication credentials").action(async () => await logout());
|
|
6455
6677
|
cli.command("whoami", "Show current login session info").action(() => whoami());
|
|
6456
6678
|
cli.command("workspace [action] [name]", "Show the workspace, move between workspaces, or rename the public address").usage("workspace <action> [name]\n\nActions:\n show Show the workspace name, address, and whether it can still change\n list (ls) Show the workspaces you belong to, and which one you are in\n switch <name> Move this terminal to another workspace you belong to\n rename <name> Change the public address, while nothing live depends on it").example("wawesome workspace").example("wawesome workspace list").example("wawesome workspace switch northwind").example("wawesome workspace rename northwind").option("-v, --verbose", "Enable verbose debug output").action((action, name, options) => workspaceCommand(action, name, options));
|
|
6679
|
+
cli.command("export [action]", "Export the whole workspace as one archive, and print the link to it").usage("export [status]\n\nWith no action, asks for an export and waits until it is ready.\n status Show the latest export, and a new link if it is ready").example("wawesome export").example("wawesome export status").option("--no-wait", "Ask for the export and return without waiting for it").option("-v, --verbose", "Enable verbose debug output").action((action, options) => exportCommand(action, options));
|
|
6457
6680
|
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));
|
|
6458
6681
|
cli.command("credentials [action] [target]", "Mint, list, regenerate, revoke and delete deploy credentials").alias("credential").usage(`credentials <action> [options]
|
|
6459
6682
|
|