wawesome 0.18.0 → 0.19.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 +12 -1004
- package/dist/{guest-parity-Df4rlLDW.mjs → guest-parity-B5GLO6LA.mjs} +1 -1
- package/dist/guest-parity.mjs +1 -1
- package/dist/{guest-surface-CXON0L5V.mjs → guest-surface-4bHyTMIy.mjs} +11 -0
- package/dist/index.mjs +229 -156
- package/dist/vite.mjs +1 -1
- package/dist/vitest-setup.mjs +1 -1
- package/package.json +4 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as unsupportedGlobals, i as shimmedGlobals, n as methodRemedy, r as polyfilledGlobals, t as declaredMethods } from "./guest-surface-
|
|
1
|
+
import { a as unsupportedGlobals, i as shimmedGlobals, n as methodRemedy, r as polyfilledGlobals, t as declaredMethods } from "./guest-surface-4bHyTMIy.mjs";
|
|
2
2
|
//#region ../../server/guest-surface/message-channel.js
|
|
3
3
|
const ENTANGLED = Symbol("entangled");
|
|
4
4
|
const QUEUE = Symbol("queue");
|
package/dist/guest-parity.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as applyGuestParity } from "./guest-parity-
|
|
1
|
+
import { t as applyGuestParity } from "./guest-parity-B5GLO6LA.mjs";
|
|
2
2
|
export { applyGuestParity };
|
|
@@ -27,6 +27,17 @@ var globals = [
|
|
|
27
27
|
"intl-locales-supported",
|
|
28
28
|
"intl-segmenter-polyfill"
|
|
29
29
|
]
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"name": "URLPattern",
|
|
33
|
+
"status": "unsupported",
|
|
34
|
+
"remedy": "bundle a URLPattern polyfill, for example urlpattern-polyfill",
|
|
35
|
+
"providedBy": ["urlpattern-polyfill"]
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "WebSocket",
|
|
39
|
+
"status": "unsupported",
|
|
40
|
+
"remedy": "the sandbox has no sockets, so nothing can supply this one; open the connection from the browser instead, or reach a realtime service over HTTPS"
|
|
30
41
|
}
|
|
31
42
|
];
|
|
32
43
|
var methods = [
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as unsupportedGlobals, n as methodRemedy, r as polyfilledGlobals, t as declaredMethods } from "./guest-surface-
|
|
1
|
+
import { a as unsupportedGlobals, n as methodRemedy, r as polyfilledGlobals, t as declaredMethods } from "./guest-surface-4bHyTMIy.mjs";
|
|
2
2
|
import cac from "cac";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
@@ -905,7 +905,7 @@ async function buildHandler(entryInput, config, options) {
|
|
|
905
905
|
* that has to name this version — `--version`, the dependency a scaffolded
|
|
906
906
|
* project pins — reads it here, so a release bumps one file.
|
|
907
907
|
*/
|
|
908
|
-
const CLI_VERSION = "0.
|
|
908
|
+
const CLI_VERSION = "0.19.0";
|
|
909
909
|
//#endregion
|
|
910
910
|
//#region src/prompt.ts
|
|
911
911
|
/**
|
|
@@ -962,6 +962,144 @@ function isInteractive() {
|
|
|
962
962
|
return Boolean(process.stdin.isTTY);
|
|
963
963
|
}
|
|
964
964
|
//#endregion
|
|
965
|
+
//#region ../shared/public-address.ts
|
|
966
|
+
const INVOCATION_PREFIX = "/x";
|
|
967
|
+
const SLUG_SEPARATOR = "--";
|
|
968
|
+
const SURFACE_ROUTE = "/v1/invocation-surface";
|
|
969
|
+
const SUBTREE_NOTE = "Every path beneath this address reaches the Function.";
|
|
970
|
+
const ROOT_FUNCTION_SLUG = "root";
|
|
971
|
+
/**
|
|
972
|
+
* A gateway that serves no form of the address — and the value a client uses for
|
|
973
|
+
* one that did not answer at all, deliberately the same: both name no address,
|
|
974
|
+
* and there is nothing else either could honestly do.
|
|
975
|
+
*/
|
|
976
|
+
const NO_INVOCATION_SURFACE = {
|
|
977
|
+
contentOrigin: null,
|
|
978
|
+
pathFormOrigin: null
|
|
979
|
+
};
|
|
980
|
+
async function fetchInvocationSurface(gatewayUrl) {
|
|
981
|
+
const origin = trimTrailingSlashes(gatewayUrl);
|
|
982
|
+
const res = await fetch(`${origin}${SURFACE_ROUTE}`);
|
|
983
|
+
if (!res.ok) throw new Error(`Failed to read the gateway's address forms (HTTP ${res.status}).`);
|
|
984
|
+
const body = await res.json();
|
|
985
|
+
return {
|
|
986
|
+
contentOrigin: body.content_origin ?? null,
|
|
987
|
+
pathFormOrigin: body.path_form ? origin : null
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
/** The one address the Function answers at, `null` where it has none. */
|
|
991
|
+
function publicAddress(surface, tenantSlug, appSlug, functionSlug) {
|
|
992
|
+
const base = appBase(surface, tenantSlug, appSlug);
|
|
993
|
+
if (base === null) return null;
|
|
994
|
+
if (functionSlug === "root") return base;
|
|
995
|
+
return `${base}/${encodeURIComponent(functionSlug)}`;
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Where one of a Function's static files answers, `null` where none of them do.
|
|
999
|
+
*
|
|
1000
|
+
* A **Static asset** is served on the App's own hostname and on no other
|
|
1001
|
+
* address, so a gateway serving only the path form has no address to give for
|
|
1002
|
+
* one — see **Static route** in `CONTEXT.md`.
|
|
1003
|
+
*/
|
|
1004
|
+
function assetAddress(surface, tenantSlug, appSlug, functionSlug, assetPath) {
|
|
1005
|
+
if (!surface.contentOrigin) return null;
|
|
1006
|
+
const mount = publicAddress(surface, tenantSlug, appSlug, functionSlug);
|
|
1007
|
+
if (mount === null) return null;
|
|
1008
|
+
return `${mount}/${assetPath.split("/").map(encodeURIComponent).join("/")}`;
|
|
1009
|
+
}
|
|
1010
|
+
/** The same address with the app and function still to be chosen. */
|
|
1011
|
+
function publicAddressTemplate(surface, tenantSlug) {
|
|
1012
|
+
if (surface.contentOrigin) {
|
|
1013
|
+
if (!isLegalDnsLabel(tenantSlug)) return null;
|
|
1014
|
+
const hostname = appHostname(surface.contentOrigin, `${tenantSlug}${SLUG_SEPARATOR}<app>`);
|
|
1015
|
+
return hostname === null ? null : `${hostname}/<function>`;
|
|
1016
|
+
}
|
|
1017
|
+
if (surface.pathFormOrigin) return `${pathFormBase(surface.pathFormOrigin, tenantSlug)}/<app>/<function>`;
|
|
1018
|
+
return null;
|
|
1019
|
+
}
|
|
1020
|
+
function appBase(surface, tenantSlug, appSlug) {
|
|
1021
|
+
if (surface.contentOrigin) {
|
|
1022
|
+
if (!isLegalDnsLabel(tenantSlug) || !isLegalDnsLabel(appSlug)) return null;
|
|
1023
|
+
return appHostname(surface.contentOrigin, `${tenantSlug}${SLUG_SEPARATOR}${appSlug}`);
|
|
1024
|
+
}
|
|
1025
|
+
if (surface.pathFormOrigin) return `${pathFormBase(surface.pathFormOrigin, tenantSlug)}/${encodeURIComponent(appSlug)}`;
|
|
1026
|
+
return null;
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* The App's own hostname, built the way the gateway rebuilds it from its own
|
|
1030
|
+
* configuration: the label one level beneath the content domain. Callers check
|
|
1031
|
+
* the slugs the label is made of, since a slug the gateway would refuse to read
|
|
1032
|
+
* a `Host` for names no App there.
|
|
1033
|
+
*/
|
|
1034
|
+
function appHostname(contentOrigin, label) {
|
|
1035
|
+
let origin;
|
|
1036
|
+
try {
|
|
1037
|
+
origin = new URL(contentOrigin);
|
|
1038
|
+
} catch {
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
return `${origin.protocol}//${label}.${origin.host}`;
|
|
1042
|
+
}
|
|
1043
|
+
/** The surface, or none where the gateway did not answer. */
|
|
1044
|
+
async function invocationSurfaceOrNone(gatewayUrl) {
|
|
1045
|
+
try {
|
|
1046
|
+
return await fetchInvocationSurface(gatewayUrl);
|
|
1047
|
+
} catch {
|
|
1048
|
+
return NO_INVOCATION_SURFACE;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
function pathFormBase(origin, tenantSlug) {
|
|
1052
|
+
return `${trimTrailingSlashes(origin)}${INVOCATION_PREFIX}/${encodeURIComponent(tenantSlug)}`;
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* The legal DNS label closest to what somebody typed, empty where nothing legal
|
|
1056
|
+
* survives. What a workspace name suggests as an address, and what the CLI
|
|
1057
|
+
* offers as a default — never applied to a typed address, which is refused
|
|
1058
|
+
* rather than rewritten into a name nobody chose.
|
|
1059
|
+
*/
|
|
1060
|
+
function slugifyLabel(raw) {
|
|
1061
|
+
let label = "";
|
|
1062
|
+
for (const c of raw.trim().toLowerCase()) if (c >= "a" && c <= "z" || c >= "0" && c <= "9") label += c;
|
|
1063
|
+
else if (label.length > 0 && !label.endsWith("-")) label += "-";
|
|
1064
|
+
label = label.slice(0, 63);
|
|
1065
|
+
while (label.endsWith("-")) label = label.slice(0, -1);
|
|
1066
|
+
return label;
|
|
1067
|
+
}
|
|
1068
|
+
/** Must match `dns_label::is_legal` in `server/crates/core/src/value_objects.rs`. */
|
|
1069
|
+
function isLegalDnsLabel(candidate) {
|
|
1070
|
+
return candidate.length > 0 && candidate.length <= 63 && !candidate.startsWith("-") && !candidate.endsWith("-") && !candidate.includes("--") && /^[a-z0-9-]+$/.test(candidate);
|
|
1071
|
+
}
|
|
1072
|
+
function trimTrailingSlashes(origin) {
|
|
1073
|
+
return origin.replace(/\/+$/, "");
|
|
1074
|
+
}
|
|
1075
|
+
//#endregion
|
|
1076
|
+
//#region src/slug.ts
|
|
1077
|
+
/**
|
|
1078
|
+
* The character rules every slug on the platform obeys, mirrored from the
|
|
1079
|
+
* `dns_label` module in `server/crates/core/src/value_objects.rs`.
|
|
1080
|
+
*
|
|
1081
|
+
* A typed name is checked while the user is still standing there, rather than
|
|
1082
|
+
* accepted, sent, and rewritten by the gateway into something else that ends up
|
|
1083
|
+
* in a client's dashboard. The rules live beside the address they build, so the
|
|
1084
|
+
* CLI and the dashboard cannot come to disagree about what a legal name is.
|
|
1085
|
+
*/
|
|
1086
|
+
/** Maximum length of a single DNS label (RFC 1035 §2.3.4). */
|
|
1087
|
+
const MAX_SLUG_LENGTH = 63;
|
|
1088
|
+
/**
|
|
1089
|
+
* Normalise arbitrary input into a legal DNS label, returning an empty string
|
|
1090
|
+
* when nothing legal survives — callers decide whether that is an error.
|
|
1091
|
+
*/
|
|
1092
|
+
function sanitizeSlug(raw) {
|
|
1093
|
+
return slugifyLabel(raw);
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Whether `candidate` is already a legal DNS label — i.e. whether
|
|
1097
|
+
* {@link sanitizeSlug} would leave it untouched.
|
|
1098
|
+
*/
|
|
1099
|
+
function isLegalSlug(candidate) {
|
|
1100
|
+
return candidate.length > 0 && candidate.length <= MAX_SLUG_LENGTH && !candidate.startsWith("-") && !candidate.endsWith("-") && !candidate.includes("--") && /^[a-z0-9-]+$/.test(candidate);
|
|
1101
|
+
}
|
|
1102
|
+
//#endregion
|
|
965
1103
|
//#region src/session.ts
|
|
966
1104
|
/**
|
|
967
1105
|
* A gateway request, retried once against a renewed session if it comes back 401.
|
|
@@ -1170,24 +1308,36 @@ async function renameTenantSlug(creds, slug) {
|
|
|
1170
1308
|
//#endregion
|
|
1171
1309
|
//#region src/auth.ts
|
|
1172
1310
|
/**
|
|
1173
|
-
* Ask what to call the workspace being created.
|
|
1311
|
+
* Ask what to call the workspace being created, and what it should answer at.
|
|
1174
1312
|
*
|
|
1175
|
-
* There is no default worth offering: a name nobody typed is exactly what
|
|
1176
|
-
* replaced.
|
|
1177
|
-
*
|
|
1178
|
-
|
|
1179
|
-
|
|
1313
|
+
* There is no default name worth offering: a name nobody typed is exactly what
|
|
1314
|
+
* this replaced. The address has one — the name, slugified — because it is a
|
|
1315
|
+
* DNS label and typing it a second time is work the name already did. A
|
|
1316
|
+
* non-interactive login has nobody to ask, so it says what flag to pass rather
|
|
1317
|
+
* than inventing a name and minting a workspace under it; an address it is not
|
|
1318
|
+
* given is still minted, which is what a pipeline that says nothing gets.
|
|
1319
|
+
*/
|
|
1320
|
+
async function promptForWorkspace(options) {
|
|
1180
1321
|
const supplied = options.workspace?.trim();
|
|
1181
|
-
if (supplied) return
|
|
1322
|
+
if (supplied) return {
|
|
1323
|
+
name: supplied,
|
|
1324
|
+
address: options.address?.trim() || ""
|
|
1325
|
+
};
|
|
1182
1326
|
if (!isInteractive()) throw new Error("No workspace found, and no name to create one with. Re-run with --workspace <name>.");
|
|
1183
1327
|
console.log("\n[wawesome] You don't have a workspace yet. Let's create one.");
|
|
1184
|
-
console.log(" (
|
|
1328
|
+
console.log(" (the name is what you'll see in the dashboard; the address is the first half of every URL you deploy)");
|
|
1185
1329
|
const session = openPromptSession();
|
|
1186
1330
|
try {
|
|
1187
1331
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
1188
|
-
const
|
|
1189
|
-
if (
|
|
1190
|
-
|
|
1332
|
+
const name = (await session.ask(" Workspace name?", "")).trim();
|
|
1333
|
+
if (!name) {
|
|
1334
|
+
console.log("[wawesome] A workspace needs a name.");
|
|
1335
|
+
continue;
|
|
1336
|
+
}
|
|
1337
|
+
return {
|
|
1338
|
+
name,
|
|
1339
|
+
address: (await session.ask(" Public address?", options.address?.trim() || sanitizeSlug(name))).trim()
|
|
1340
|
+
};
|
|
1191
1341
|
}
|
|
1192
1342
|
} finally {
|
|
1193
1343
|
session.close();
|
|
@@ -1235,9 +1385,9 @@ async function completeLogin({ gatewayUrl, dashboardUrl, accessToken, refreshTok
|
|
|
1235
1385
|
let workspaceName;
|
|
1236
1386
|
let workspaceSlug;
|
|
1237
1387
|
if (tenants.length === 0) {
|
|
1238
|
-
const
|
|
1388
|
+
const chosen = await promptForWorkspace(options);
|
|
1239
1389
|
await confirmTermsAccepted(options);
|
|
1240
|
-
console.log(`[wawesome] Creating workspace "${
|
|
1390
|
+
console.log(`[wawesome] Creating workspace "${chosen.name}"...`);
|
|
1241
1391
|
const initRes = await fetch(`${gatewayUrl}/api/v1/onboarding/init`, {
|
|
1242
1392
|
method: "POST",
|
|
1243
1393
|
headers: {
|
|
@@ -1245,7 +1395,8 @@ async function completeLogin({ gatewayUrl, dashboardUrl, accessToken, refreshTok
|
|
|
1245
1395
|
"Content-Type": "application/json"
|
|
1246
1396
|
},
|
|
1247
1397
|
body: JSON.stringify({
|
|
1248
|
-
tenant_name:
|
|
1398
|
+
tenant_name: chosen.name,
|
|
1399
|
+
tenant_slug: chosen.address || void 0,
|
|
1249
1400
|
accepted_terms: true
|
|
1250
1401
|
})
|
|
1251
1402
|
});
|
|
@@ -1253,7 +1404,7 @@ async function completeLogin({ gatewayUrl, dashboardUrl, accessToken, refreshTok
|
|
|
1253
1404
|
const initData = await initRes.json();
|
|
1254
1405
|
primaryTenantId = initData.tenant_id;
|
|
1255
1406
|
workspaceSlug = initData.tenant_slug;
|
|
1256
|
-
workspaceName = initData.tenant_name ||
|
|
1407
|
+
workspaceName = initData.tenant_name || chosen.name;
|
|
1257
1408
|
} else primaryTenantId = tenants[0].tenant_id;
|
|
1258
1409
|
if (isVerbose) console.log(`[wawesome:verbose] Using tenant: ${primaryTenantId}`);
|
|
1259
1410
|
console.log("[wawesome] Exchanging for tenant-scoped credentials...");
|
|
@@ -1544,6 +1695,17 @@ function wrapped(said) {
|
|
|
1544
1695
|
function indented(said) {
|
|
1545
1696
|
return wrapped(said).map((line) => `${VALUE_COLUMN}${line}`);
|
|
1546
1697
|
}
|
|
1698
|
+
/**
|
|
1699
|
+
* What fits in `room` characters, with an ellipsis where it does not. Cut
|
|
1700
|
+
* rather than wrapped: a row that took two lines would push the row above it
|
|
1701
|
+
* out of sight. A room too small for even that shows nothing, so nothing ever
|
|
1702
|
+
* runs past the width it was given.
|
|
1703
|
+
*/
|
|
1704
|
+
function truncate(said, room) {
|
|
1705
|
+
if (said.length <= room) return said;
|
|
1706
|
+
if (room <= 1) return "";
|
|
1707
|
+
return `${said.slice(0, room - 1)}…`;
|
|
1708
|
+
}
|
|
1547
1709
|
//#endregion
|
|
1548
1710
|
//#region src/domains.ts
|
|
1549
1711
|
/**
|
|
@@ -1883,6 +2045,7 @@ function domainsUrl(creds, app) {
|
|
|
1883
2045
|
function whereItGotTo(domain) {
|
|
1884
2046
|
const { state } = domain;
|
|
1885
2047
|
if (state.serving) return "serving";
|
|
2048
|
+
if (domain.proxied_in_public_dns) return "the CNAME is in at Cloudflare with the proxy on, so set that record to DNS only";
|
|
1886
2049
|
if (state.certificate_active) return "certificate issued, so point the CNAME here to cut over";
|
|
1887
2050
|
if (state.ownership_verified) return domain.certificate_record_pending ? "name verified, and the certificate record is not ready" : "name verified, so add the certificate CNAME below";
|
|
1888
2051
|
if (domain.ownership_record_pending) return "not verified yet, and the TXT record is not ready";
|
|
@@ -2280,103 +2443,6 @@ function manifestOf(assets) {
|
|
|
2280
2443
|
}));
|
|
2281
2444
|
}
|
|
2282
2445
|
//#endregion
|
|
2283
|
-
//#region ../shared/public-address.ts
|
|
2284
|
-
const INVOCATION_PREFIX = "/x";
|
|
2285
|
-
const SLUG_SEPARATOR = "--";
|
|
2286
|
-
const SURFACE_ROUTE = "/v1/invocation-surface";
|
|
2287
|
-
const SUBTREE_NOTE = "Every path beneath this address reaches the Function.";
|
|
2288
|
-
const ROOT_FUNCTION_SLUG = "root";
|
|
2289
|
-
/**
|
|
2290
|
-
* A gateway that serves no form of the address — and the value a client uses for
|
|
2291
|
-
* one that did not answer at all, deliberately the same: both name no address,
|
|
2292
|
-
* and there is nothing else either could honestly do.
|
|
2293
|
-
*/
|
|
2294
|
-
const NO_INVOCATION_SURFACE = {
|
|
2295
|
-
contentOrigin: null,
|
|
2296
|
-
pathFormOrigin: null
|
|
2297
|
-
};
|
|
2298
|
-
async function fetchInvocationSurface(gatewayUrl) {
|
|
2299
|
-
const origin = trimTrailingSlashes(gatewayUrl);
|
|
2300
|
-
const res = await fetch(`${origin}${SURFACE_ROUTE}`);
|
|
2301
|
-
if (!res.ok) throw new Error(`Failed to read the gateway's address forms (HTTP ${res.status}).`);
|
|
2302
|
-
const body = await res.json();
|
|
2303
|
-
return {
|
|
2304
|
-
contentOrigin: body.content_origin ?? null,
|
|
2305
|
-
pathFormOrigin: body.path_form ? origin : null
|
|
2306
|
-
};
|
|
2307
|
-
}
|
|
2308
|
-
/** The one address the Function answers at, `null` where it has none. */
|
|
2309
|
-
function publicAddress(surface, tenantSlug, appSlug, functionSlug) {
|
|
2310
|
-
const base = appBase(surface, tenantSlug, appSlug);
|
|
2311
|
-
if (base === null) return null;
|
|
2312
|
-
if (functionSlug === "root") return base;
|
|
2313
|
-
return `${base}/${encodeURIComponent(functionSlug)}`;
|
|
2314
|
-
}
|
|
2315
|
-
/**
|
|
2316
|
-
* Where one of a Function's static files answers, `null` where none of them do.
|
|
2317
|
-
*
|
|
2318
|
-
* A **Static asset** is served on the App's own hostname and on no other
|
|
2319
|
-
* address, so a gateway serving only the path form has no address to give for
|
|
2320
|
-
* one — see **Static route** in `CONTEXT.md`.
|
|
2321
|
-
*/
|
|
2322
|
-
function assetAddress(surface, tenantSlug, appSlug, functionSlug, assetPath) {
|
|
2323
|
-
if (!surface.contentOrigin) return null;
|
|
2324
|
-
const mount = publicAddress(surface, tenantSlug, appSlug, functionSlug);
|
|
2325
|
-
if (mount === null) return null;
|
|
2326
|
-
return `${mount}/${assetPath.split("/").map(encodeURIComponent).join("/")}`;
|
|
2327
|
-
}
|
|
2328
|
-
/** The same address with the app and function still to be chosen. */
|
|
2329
|
-
function publicAddressTemplate(surface, tenantSlug) {
|
|
2330
|
-
if (surface.contentOrigin) {
|
|
2331
|
-
if (!isLegalDnsLabel(tenantSlug)) return null;
|
|
2332
|
-
const hostname = appHostname(surface.contentOrigin, `${tenantSlug}${SLUG_SEPARATOR}<app>`);
|
|
2333
|
-
return hostname === null ? null : `${hostname}/<function>`;
|
|
2334
|
-
}
|
|
2335
|
-
if (surface.pathFormOrigin) return `${pathFormBase(surface.pathFormOrigin, tenantSlug)}/<app>/<function>`;
|
|
2336
|
-
return null;
|
|
2337
|
-
}
|
|
2338
|
-
function appBase(surface, tenantSlug, appSlug) {
|
|
2339
|
-
if (surface.contentOrigin) {
|
|
2340
|
-
if (!isLegalDnsLabel(tenantSlug) || !isLegalDnsLabel(appSlug)) return null;
|
|
2341
|
-
return appHostname(surface.contentOrigin, `${tenantSlug}${SLUG_SEPARATOR}${appSlug}`);
|
|
2342
|
-
}
|
|
2343
|
-
if (surface.pathFormOrigin) return `${pathFormBase(surface.pathFormOrigin, tenantSlug)}/${encodeURIComponent(appSlug)}`;
|
|
2344
|
-
return null;
|
|
2345
|
-
}
|
|
2346
|
-
/**
|
|
2347
|
-
* The App's own hostname, built the way the gateway rebuilds it from its own
|
|
2348
|
-
* configuration: the label one level beneath the content domain. Callers check
|
|
2349
|
-
* the slugs the label is made of, since a slug the gateway would refuse to read
|
|
2350
|
-
* a `Host` for names no App there.
|
|
2351
|
-
*/
|
|
2352
|
-
function appHostname(contentOrigin, label) {
|
|
2353
|
-
let origin;
|
|
2354
|
-
try {
|
|
2355
|
-
origin = new URL(contentOrigin);
|
|
2356
|
-
} catch {
|
|
2357
|
-
return null;
|
|
2358
|
-
}
|
|
2359
|
-
return `${origin.protocol}//${label}.${origin.host}`;
|
|
2360
|
-
}
|
|
2361
|
-
/** The surface, or none where the gateway did not answer. */
|
|
2362
|
-
async function invocationSurfaceOrNone(gatewayUrl) {
|
|
2363
|
-
try {
|
|
2364
|
-
return await fetchInvocationSurface(gatewayUrl);
|
|
2365
|
-
} catch {
|
|
2366
|
-
return NO_INVOCATION_SURFACE;
|
|
2367
|
-
}
|
|
2368
|
-
}
|
|
2369
|
-
function pathFormBase(origin, tenantSlug) {
|
|
2370
|
-
return `${trimTrailingSlashes(origin)}${INVOCATION_PREFIX}/${encodeURIComponent(tenantSlug)}`;
|
|
2371
|
-
}
|
|
2372
|
-
/** Must match `dns_label::is_legal` in `server/crates/core/src/value_objects.rs`. */
|
|
2373
|
-
function isLegalDnsLabel(candidate) {
|
|
2374
|
-
return candidate.length > 0 && candidate.length <= 63 && !candidate.startsWith("-") && !candidate.endsWith("-") && !candidate.includes("--") && /^[a-z0-9-]+$/.test(candidate);
|
|
2375
|
-
}
|
|
2376
|
-
function trimTrailingSlashes(origin) {
|
|
2377
|
-
return origin.replace(/\/+$/, "");
|
|
2378
|
-
}
|
|
2379
|
-
//#endregion
|
|
2380
2446
|
//#region src/deploy-target.ts
|
|
2381
2447
|
const RECORD_PATH = path.join(".wawesome", "last-deploy.json");
|
|
2382
2448
|
function readLastDeployTarget(projectDir = process.cwd()) {
|
|
@@ -2909,7 +2975,8 @@ async function cronCommand(action, target, subtarget, options = {}) {
|
|
|
2909
2975
|
* The address a **Document** answers at, relative to its Function's mount: a
|
|
2910
2976
|
* request path whose last segment carries no extension resolves to that path,
|
|
2911
2977
|
* to it with `.html` appended and to it with `/index.html` appended, so both
|
|
2912
|
-
* `about.html` and `about/index.html` are the address `about`.
|
|
2978
|
+
* `about.html` and `about/index.html` are the address `about`. A last segment
|
|
2979
|
+
* spelled `index` skips the middle one, which is what this strips first.
|
|
2913
2980
|
*/
|
|
2914
2981
|
function documentAddressPath(assetPath) {
|
|
2915
2982
|
return assetPath.replace(/(^|\/)index\.html$/, "").replace(/\.html$/, "");
|
|
@@ -3036,6 +3103,17 @@ function declaredFields(declared) {
|
|
|
3036
3103
|
};
|
|
3037
3104
|
}
|
|
3038
3105
|
/**
|
|
3106
|
+
* What this deploy says it carries.
|
|
3107
|
+
*
|
|
3108
|
+
* Trimmed here so that a flag whose value came out of a shell variable nobody
|
|
3109
|
+
* set is a deploy that said nothing rather than one the gateway turns down. How
|
|
3110
|
+
* long a message may be and whether it is one line are the platform's rule, and
|
|
3111
|
+
* holding them here as well is two places for them to drift.
|
|
3112
|
+
*/
|
|
3113
|
+
function declaredMessage(stated) {
|
|
3114
|
+
return typeof stated === "string" ? stated.trim() || void 0 : void 0;
|
|
3115
|
+
}
|
|
3116
|
+
/**
|
|
3039
3117
|
* The fallback this project declares.
|
|
3040
3118
|
*
|
|
3041
3119
|
* A declaration missing either half is refused here rather than at the gateway,
|
|
@@ -3106,7 +3184,8 @@ async function deploy(entryInput, options) {
|
|
|
3106
3184
|
visibility: declaredVisibility(config.visibility),
|
|
3107
3185
|
confirmPublish: Boolean(options.publish),
|
|
3108
3186
|
confirmAssetDrop: Boolean(options.dropAssets),
|
|
3109
|
-
fallback: declaredFallback(config.fallback)
|
|
3187
|
+
fallback: declaredFallback(config.fallback),
|
|
3188
|
+
message: declaredMessage(options.message)
|
|
3110
3189
|
};
|
|
3111
3190
|
const domainName = declaredDomain(config.domain);
|
|
3112
3191
|
if (isVerbose) {
|
|
@@ -3165,7 +3244,10 @@ async function deploy(entryInput, options) {
|
|
|
3165
3244
|
console.log(`[wawesome] Uploading code for ${app}/${funcName}...`);
|
|
3166
3245
|
const uploadUrl = `${creds.gateway_url}/v1/apps/${encodeURIComponent(app)}/functions/${encodeURIComponent(funcName)}/code`;
|
|
3167
3246
|
if (isVerbose) console.log(`[wawesome:verbose] POST ${uploadUrl}`);
|
|
3168
|
-
const declaredOnTheWire =
|
|
3247
|
+
const declaredOnTheWire = {
|
|
3248
|
+
...declaredFields(declared),
|
|
3249
|
+
...declared.message ? { message: declared.message } : {}
|
|
3250
|
+
};
|
|
3169
3251
|
const uploadRes = assets.length > 0 || declaresSchedules || declaresFallback || Object.keys(declaredOnTheWire).length > 0 || jsCode === null ? await authorizedFetch(uploadUrl, {
|
|
3170
3252
|
method: "POST",
|
|
3171
3253
|
headers: {
|
|
@@ -3189,7 +3271,7 @@ async function deploy(entryInput, options) {
|
|
|
3189
3271
|
});
|
|
3190
3272
|
if (!uploadRes.ok) {
|
|
3191
3273
|
const errorBody = await uploadRes.text();
|
|
3192
|
-
const refusal = rejectionOf(errorBody, uploadRes.status, "Version with this code bundle already exists.");
|
|
3274
|
+
const refusal = rejectionOf(errorBody, uploadRes.status, uploadRes.status === 409 ? "Version with this code bundle already exists." : `Code upload failed (HTTP ${uploadRes.status}).`);
|
|
3193
3275
|
if (uploadRes.status === 401 || uploadRes.status === 403) console.error(`[wawesome] Error: ${unauthorizedMessage(uploadRes.status, errorBody)}`);
|
|
3194
3276
|
else if (refusal.reason === "paid-plan-required") {
|
|
3195
3277
|
console.error(`\n[wawesome] \x1b[31mError: ${refusal.message}\x1b[0m`);
|
|
@@ -3211,7 +3293,7 @@ async function deploy(entryInput, options) {
|
|
|
3211
3293
|
console.error("[wawesome] To switch active version, run: \x1B[36mwawesome version switch\x1B[0m\n");
|
|
3212
3294
|
}
|
|
3213
3295
|
} else {
|
|
3214
|
-
console.error(
|
|
3296
|
+
console.error(`\n[wawesome] \x1b[31mError: ${refusal.message}\x1b[0m\n`);
|
|
3215
3297
|
if (isVerbose) console.error(`[wawesome:verbose] Response: ${errorBody}`);
|
|
3216
3298
|
}
|
|
3217
3299
|
process.exit(1);
|
|
@@ -3482,11 +3564,6 @@ async function setEnvVar(key, value, options) {
|
|
|
3482
3564
|
console.log("");
|
|
3483
3565
|
} else console.log(`[wawesome] ✅ Set variable '${key}' on app '${app}'.`);
|
|
3484
3566
|
}
|
|
3485
|
-
function truncate(str, maxLen) {
|
|
3486
|
-
if (str.length <= maxLen) return str;
|
|
3487
|
-
if (maxLen <= 3) return str.slice(0, maxLen);
|
|
3488
|
-
return str.slice(0, maxLen - 1) + "…";
|
|
3489
|
-
}
|
|
3490
3567
|
/**
|
|
3491
3568
|
* List environment variables on the current app.
|
|
3492
3569
|
*/
|
|
@@ -3807,26 +3884,6 @@ function renderPostDeploy(postDeploy, address) {
|
|
|
3807
3884
|
if (!address) return postDeploy;
|
|
3808
3885
|
return postDeploy.split("{{url}}").join(address);
|
|
3809
3886
|
}
|
|
3810
|
-
/**
|
|
3811
|
-
* Normalise arbitrary input into a legal DNS label, returning an empty string
|
|
3812
|
-
* when nothing legal survives — callers decide whether that is an error.
|
|
3813
|
-
*/
|
|
3814
|
-
function sanitizeSlug(raw) {
|
|
3815
|
-
let label = "";
|
|
3816
|
-
for (const c of raw.trim().toLowerCase()) if (c >= "a" && c <= "z") label += c;
|
|
3817
|
-
else if (c >= "0" && c <= "9") label += c;
|
|
3818
|
-
else if (label.length > 0 && !label.endsWith("-")) label += "-";
|
|
3819
|
-
label = label.slice(0, 63);
|
|
3820
|
-
while (label.endsWith("-")) label = label.slice(0, -1);
|
|
3821
|
-
return label;
|
|
3822
|
-
}
|
|
3823
|
-
/**
|
|
3824
|
-
* Whether `candidate` is already a legal DNS label — i.e. whether
|
|
3825
|
-
* {@link sanitizeSlug} would leave it untouched.
|
|
3826
|
-
*/
|
|
3827
|
-
function isLegalSlug(candidate) {
|
|
3828
|
-
return candidate.length > 0 && candidate.length <= 63 && !candidate.startsWith("-") && !candidate.endsWith("-") && !candidate.includes("--") && /^[a-z0-9-]+$/.test(candidate);
|
|
3829
|
-
}
|
|
3830
3887
|
//#endregion
|
|
3831
3888
|
//#region src/tarball.ts
|
|
3832
3889
|
/**
|
|
@@ -4576,6 +4633,24 @@ export default {
|
|
|
4576
4633
|
//#region src/version.ts
|
|
4577
4634
|
/** How both surfaces name a version nobody can switch to. */
|
|
4578
4635
|
const RECLAIMED = "⚠ [assets reclaimed]";
|
|
4636
|
+
/** Where the message sits, under the row rather than in a column of its own. */
|
|
4637
|
+
const MESSAGE_INDENT = " ".repeat(10);
|
|
4638
|
+
const ASSUMED_WIDTH = 80;
|
|
4639
|
+
function terminalWidth() {
|
|
4640
|
+
const columns = process.stdout.columns;
|
|
4641
|
+
return columns && columns > 40 ? columns : ASSUMED_WIDTH;
|
|
4642
|
+
}
|
|
4643
|
+
/**
|
|
4644
|
+
* The lines one version takes in the listing: its row, and the words its deploy
|
|
4645
|
+
* stated beneath it where it stated any.
|
|
4646
|
+
*/
|
|
4647
|
+
function versionLines(v, activeNumber, width) {
|
|
4648
|
+
const isActive = v.version_number === activeNumber;
|
|
4649
|
+
const row = `${`#${v.version_number}`.padEnd(7)} | ${v.cwasm_hash.slice(0, 8).padEnd(8)} | ${new Date(v.created_at).toISOString().replace("T", " ").slice(0, 19).padEnd(20)} | ${v.status.padEnd(9)} | ${isActive ? "\x1B[32m★ [active]\x1B[0m" : v.rollbackable === false ? `\x1b[33m${RECLAIMED}\x1b[0m` : ""}`;
|
|
4650
|
+
const message = v.message?.trim();
|
|
4651
|
+
if (!message) return [row];
|
|
4652
|
+
return [row, `${MESSAGE_INDENT}\x1b[2m${truncate(message, width - MESSAGE_INDENT.length)}\x1b[0m`];
|
|
4653
|
+
}
|
|
4579
4654
|
async function fetchVersions(gatewayUrl, tenantJwt, app, funcName, env, isVerbose) {
|
|
4580
4655
|
const url = `${gatewayUrl}/v1/apps/${encodeURIComponent(app)}/functions/${encodeURIComponent(funcName)}/versions?environment=${encodeURIComponent(env)}`;
|
|
4581
4656
|
if (isVerbose) console.log(`[wawesome:verbose] GET ${url}`);
|
|
@@ -4619,15 +4694,8 @@ async function listVersions(options) {
|
|
|
4619
4694
|
console.log(`\n📦 \x1b[1mVersions for '${app}/${funcName}' (environment: ${env})\x1b[0m\n`);
|
|
4620
4695
|
console.log("VERSION | HASH | CREATED AT | STATUS | ACTIVE");
|
|
4621
4696
|
console.log("--------|----------|----------------------|-----------|-------");
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
const verStr = `#${v.version_number}`.padEnd(7);
|
|
4625
|
-
const hashStr = v.cwasm_hash.slice(0, 8).padEnd(8);
|
|
4626
|
-
const dateStr = new Date(v.created_at).toISOString().replace("T", " ").slice(0, 19).padEnd(20);
|
|
4627
|
-
const statusStr = v.status.padEnd(9);
|
|
4628
|
-
const badge = isActive ? "\x1B[32m★ [active]\x1B[0m" : v.rollbackable === false ? `\x1b[33m${RECLAIMED}\x1b[0m` : "";
|
|
4629
|
-
console.log(`${verStr} | ${hashStr} | ${dateStr} | ${statusStr} | ${badge}`);
|
|
4630
|
-
}
|
|
4697
|
+
const width = terminalWidth();
|
|
4698
|
+
for (const v of data.versions) for (const line of versionLines(v, data.active_version_number, width)) console.log(line);
|
|
4631
4699
|
console.log("");
|
|
4632
4700
|
}
|
|
4633
4701
|
/**
|
|
@@ -4675,8 +4743,11 @@ async function switchVersion(targetInput, options) {
|
|
|
4675
4743
|
const hashShort = v.cwasm_hash.slice(0, 8);
|
|
4676
4744
|
const dateStr = new Date(v.created_at).toISOString().replace("T", " ").slice(0, 19);
|
|
4677
4745
|
const badge = isActive ? " ★ [active]" : v.rollbackable === false ? ` ${RECLAIMED}` : "";
|
|
4746
|
+
const said = v.message?.trim();
|
|
4747
|
+
const row = `v${v.version_number} (${hashShort}) • ${dateStr}${badge}`;
|
|
4748
|
+
const room = terminalWidth() - row.length - 3 - 2;
|
|
4678
4749
|
return {
|
|
4679
|
-
name:
|
|
4750
|
+
name: said ? `${row} • ${truncate(said, room)}` : row,
|
|
4680
4751
|
value: v.version_number
|
|
4681
4752
|
};
|
|
4682
4753
|
});
|
|
@@ -4716,6 +4787,8 @@ async function switchVersion(targetInput, options) {
|
|
|
4716
4787
|
console.log(` Function: ${funcName}`);
|
|
4717
4788
|
console.log(` Environment: ${env}`);
|
|
4718
4789
|
console.log(` Active Ver: v${selectedVersionNum}`);
|
|
4790
|
+
const restored = data.versions.find((v) => v.version_number === selectedVersionNum)?.message?.trim();
|
|
4791
|
+
if (restored) console.log(` Message: ${truncate(restored, terminalWidth() - 15)}`);
|
|
4719
4792
|
console.log(`======================================================\n`);
|
|
4720
4793
|
}
|
|
4721
4794
|
//#endregion
|
|
@@ -6039,7 +6112,7 @@ async function credentialsCommand(action, target, options) {
|
|
|
6039
6112
|
//#region src/index.ts
|
|
6040
6113
|
const cli = cac("wawesome");
|
|
6041
6114
|
cli.command("build [entry]", "Bundle a serverless function to an optimized JS file").option("-o, --out <path>", "Output JS bundle path", { default: "dist/index.js" }).option("-v, --verbose", "Enable verbose debug output").action((entry, options) => buildJs(entry, options));
|
|
6042
|
-
cli.command("deploy [entry]", "Build, upload, and promote a serverless function").option("-o, --out <path>", "Output JS bundle path", { default: "dist/index.js" }).option("-v, --verbose", "Enable verbose debug output").option("--skip-build", "Skip the build step, deploy an already-built bundle").option("--publish", "Confirm putting a private function back on its public address").option("--drop-assets", "Confirm a deploy carrying no static files where the live version serves some").action((entry, options) => deploy(entry, options));
|
|
6115
|
+
cli.command("deploy [entry]", "Build, upload, and promote a serverless function").option("-o, --out <path>", "Output JS bundle path", { default: "dist/index.js" }).option("-v, --verbose", "Enable verbose debug output").option("--skip-build", "Skip the build step, deploy an already-built bundle").option("--publish", "Confirm putting a private function back on its public address").option("--drop-assets", "Confirm a deploy carrying no static files where the live version serves some").option("-m, --message <text>", "One line about what this deploy carries, kept on the version it makes").action((entry, options) => deploy(entry, options));
|
|
6043
6116
|
cli.command("version [action] [target]", "Manage versions (e.g. 'version list', 'version switch [version]')").option("-e, --env <environment>", "Target environment (default: production)").option("-v, --verbose", "Enable verbose debug output").action((action, target, options) => {
|
|
6044
6117
|
if (!action || action === "list" || action === "ls") return listVersions(options);
|
|
6045
6118
|
if (action === "switch" || action === "use" || action === "select") return switchVersion(target, options);
|
|
@@ -6054,7 +6127,7 @@ cli.command("env rm <key>", "Delete an environment variable from the current app
|
|
|
6054
6127
|
cli.command("domains [action] [hostname]", "Show the custom domain on the current app, its state and its DNS records").usage("domains [action] [hostname]\n\nWith no action, prints the app's domain, every half of it, where each got to, when the platform last checked, and the records to add.\n\nActions:\n adopt Write the attached domain into wawesome-function.json\n detach <hostname> Stop serving the app at a domain you attached\n\nA domain is attached by declaring it in wawesome-function.json and deploying, from the dashboard, or by an agent.").example("wawesome domains").example("wawesome domains adopt").example("wawesome domains detach shop.client.com").option("-a, --app <slug>", "App to act on (default: the app in wawesome-function.json)").action((action, hostname, options) => domainsCommand(action, hostname, options));
|
|
6055
6128
|
cli.command("domains adopt", "Write the domain the current app answers at into wawesome-function.json").option("-a, --app <slug>", "App to act on (default: the app in wawesome-function.json)").action((options) => adoptDomainCommand(void 0, options));
|
|
6056
6129
|
cli.command("domains detach <hostname>", "Stop serving the current app at a domain you attached").option("-a, --app <slug>", "App to act on (default: the app in wawesome-function.json)").action((hostname, options) => detachDomainCommand(hostname, options));
|
|
6057
|
-
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));
|
|
6130
|
+
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("--address <name>", "Public address for the workspace being created (default: one is minted)").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));
|
|
6058
6131
|
cli.command("logout", "Clear stored authentication credentials").action(() => logout());
|
|
6059
6132
|
cli.command("whoami", "Show current login session info").action(() => whoami());
|
|
6060
6133
|
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));
|
package/dist/vite.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as unsupportedGlobals, n as methodRemedy, r as polyfilledGlobals, t as declaredMethods } from "./guest-surface-
|
|
1
|
+
import { a as unsupportedGlobals, n as methodRemedy, r as polyfilledGlobals, t as declaredMethods } from "./guest-surface-4bHyTMIy.mjs";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
//#region src/vite.ts
|
package/dist/vitest-setup.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wawesome",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "CLI tool for building and deploying serverless functions on wawesome.io platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -35,6 +35,9 @@
|
|
|
35
35
|
"dist",
|
|
36
36
|
"vite-env.d.ts"
|
|
37
37
|
],
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=22.13.0"
|
|
40
|
+
},
|
|
38
41
|
"scripts": {
|
|
39
42
|
"build": "tsdown",
|
|
40
43
|
"dev": "tsdown --watch",
|