wawesome 0.1.0 → 0.3.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.
Files changed (2) hide show
  1. package/dist/index.mjs +101 -19
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -720,7 +720,7 @@ async function buildJs(entryInput, options) {
720
720
  * that has to name this version — `--version`, the dependency a scaffolded
721
721
  * project pins — reads it here, so a release bumps one file.
722
722
  */
723
- const CLI_VERSION = "0.1.0";
723
+ const CLI_VERSION = "0.3.0";
724
724
  //#endregion
725
725
  //#region src/prompt.ts
726
726
  /**
@@ -1326,18 +1326,85 @@ function manifestOf(assets) {
1326
1326
  //#endregion
1327
1327
  //#region ../shared/public-address.ts
1328
1328
  const INVOCATION_PREFIX = "/x";
1329
+ const SLUG_SEPARATOR = "--";
1330
+ const SURFACE_ROUTE = "/v1/invocation-surface";
1329
1331
  const SUBTREE_NOTE = "Every path beneath this address reaches the Function.";
1330
- function mountBase(origin, tenantSlug) {
1331
- return `${origin.replace(/\/+$/, "")}${INVOCATION_PREFIX}/${encodeURIComponent(tenantSlug)}`;
1332
+ /**
1333
+ * A gateway that serves no form of the address — and the value a client uses for
1334
+ * one that did not answer at all, deliberately the same: both name no address,
1335
+ * and there is nothing else either could honestly do.
1336
+ */
1337
+ const NO_INVOCATION_SURFACE = {
1338
+ contentOrigin: null,
1339
+ pathFormOrigin: null
1340
+ };
1341
+ async function fetchInvocationSurface(gatewayUrl) {
1342
+ const origin = trimTrailingSlashes(gatewayUrl);
1343
+ const res = await fetch(`${origin}${SURFACE_ROUTE}`);
1344
+ if (!res.ok) throw new Error(`Failed to read the gateway's address forms (HTTP ${res.status}).`);
1345
+ const body = await res.json();
1346
+ return {
1347
+ contentOrigin: body.content_origin ?? null,
1348
+ pathFormOrigin: body.path_form ? origin : null
1349
+ };
1332
1350
  }
1333
- function publicAddress(origin, tenantSlug, appSlug, functionSlug) {
1334
- const base = `${mountBase(origin, tenantSlug)}/${encodeURIComponent(appSlug)}`;
1351
+ /** The one address the Function answers at, `null` where it has none. */
1352
+ function publicAddress(surface, tenantSlug, appSlug, functionSlug) {
1353
+ const base = appBase(surface, tenantSlug, appSlug);
1354
+ if (base === null) return null;
1335
1355
  if (functionSlug === "root") return base;
1336
1356
  return `${base}/${encodeURIComponent(functionSlug)}`;
1337
1357
  }
1338
1358
  /** The same address with the app and function still to be chosen. */
1339
- function publicAddressTemplate(origin, tenantSlug) {
1340
- return `${mountBase(origin, tenantSlug)}/<app>/<function>`;
1359
+ function publicAddressTemplate(surface, tenantSlug) {
1360
+ if (surface.contentOrigin) {
1361
+ if (!isLegalDnsLabel(tenantSlug)) return null;
1362
+ const hostname = appHostname(surface.contentOrigin, `${tenantSlug}${SLUG_SEPARATOR}<app>`);
1363
+ return hostname === null ? null : `${hostname}/<function>`;
1364
+ }
1365
+ if (surface.pathFormOrigin) return `${pathFormBase(surface.pathFormOrigin, tenantSlug)}/<app>/<function>`;
1366
+ return null;
1367
+ }
1368
+ function appBase(surface, tenantSlug, appSlug) {
1369
+ if (surface.contentOrigin) {
1370
+ if (!isLegalDnsLabel(tenantSlug) || !isLegalDnsLabel(appSlug)) return null;
1371
+ return appHostname(surface.contentOrigin, `${tenantSlug}${SLUG_SEPARATOR}${appSlug}`);
1372
+ }
1373
+ if (surface.pathFormOrigin) return `${pathFormBase(surface.pathFormOrigin, tenantSlug)}/${encodeURIComponent(appSlug)}`;
1374
+ return null;
1375
+ }
1376
+ /**
1377
+ * The App's own hostname, built the way the gateway rebuilds it from its own
1378
+ * configuration: the label one level beneath the content domain. Callers check
1379
+ * the slugs the label is made of, since a slug the gateway would refuse to read
1380
+ * a `Host` for names no App there.
1381
+ */
1382
+ function appHostname(contentOrigin, label) {
1383
+ let origin;
1384
+ try {
1385
+ origin = new URL(contentOrigin);
1386
+ } catch {
1387
+ return null;
1388
+ }
1389
+ return `${origin.protocol}//${label}.${origin.host}`;
1390
+ }
1391
+ /** The surface, or none where the gateway did not answer. */
1392
+ async function invocationSurfaceOrNone(gatewayUrl) {
1393
+ try {
1394
+ return await fetchInvocationSurface(gatewayUrl);
1395
+ } catch {
1396
+ return NO_INVOCATION_SURFACE;
1397
+ }
1398
+ }
1399
+ function pathFormBase(origin, tenantSlug) {
1400
+ return `${trimTrailingSlashes(origin)}${INVOCATION_PREFIX}/${encodeURIComponent(tenantSlug)}`;
1401
+ }
1402
+ /** Must match `dns_label::is_legal` in `server/crates/core/src/value_objects.rs`. */
1403
+ function isLegalDnsLabel(candidate) {
1404
+ return candidate.length > 0 && candidate.length <= 63 && !candidate.startsWith("-") && !candidate.endsWith("-") && !candidate.includes("--") && /^[a-z0-9-]+$/.test(candidate);
1405
+ }
1406
+ function trimTrailingSlashes(origin) {
1407
+ return origin.replace(/\/+$/, "");
1341
1408
  }
1342
1409
  //#endregion
1343
1410
  //#region src/deploy.ts
@@ -1451,9 +1518,11 @@ async function deploy(entryInput, options) {
1451
1518
  process.exit(1);
1452
1519
  }
1453
1520
  let address = null;
1521
+ let surface = null;
1454
1522
  try {
1455
1523
  const { slug } = await resolveWorkspace(creds);
1456
- address = publicAddress(creds.gateway_url, slug, app, funcName);
1524
+ surface = await fetchInvocationSurface(creds.gateway_url);
1525
+ address = publicAddress(surface, slug, app, funcName);
1457
1526
  } catch (err) {
1458
1527
  if (isVerbose) console.log(`[wawesome:verbose] Could not resolve the workspace address: ${err instanceof Error ? err.message : err}`);
1459
1528
  }
@@ -1473,6 +1542,10 @@ async function deploy(entryInput, options) {
1473
1542
  if (address) {
1474
1543
  console.log(`\n URL: \x1b[36m${address}\x1b[0m`);
1475
1544
  console.log(` ${SUBTREE_NOTE}`);
1545
+ } else if (surface) {
1546
+ console.log("\n URL: none — this gateway serves no public address form.");
1547
+ console.log(" Set CONTENT_ORIGIN on it, or ALLOW_PATH_INVOCATION_FORM");
1548
+ console.log(" for local development.");
1476
1549
  }
1477
1550
  if (headroom) {
1478
1551
  console.log("");
@@ -2413,9 +2486,11 @@ async function ensureSession(options) {
2413
2486
  /** Bound on the rename loop, so a name the gateway keeps refusing ends the offer. */
2414
2487
  const MAX_RENAME_ATTEMPTS = 3;
2415
2488
  /** Show the address the Function will answer on, once it is deployed. */
2416
- function announceUrl(creds, slug, appSlug, functionName) {
2489
+ function announceUrl(surface, slug, appSlug, functionName) {
2490
+ const address = publicAddress(surface, slug, appSlug, functionName);
2491
+ if (!address) return;
2417
2492
  console.log("\n[wawesome] Your Function will answer on:\n");
2418
- console.log(` \x1b[36m${publicAddress(creds.gateway_url, slug, appSlug, functionName)}\x1b[0m`);
2493
+ console.log(` \x1b[36m${address}\x1b[0m`);
2419
2494
  console.log(` ${SUBTREE_NOTE}\n`);
2420
2495
  }
2421
2496
  /**
@@ -2435,7 +2510,8 @@ function announceUrl(creds, slug, appSlug, functionName) {
2435
2510
  async function offerWorkspaceAddress(session, creds, tenant, appSlug, functionName) {
2436
2511
  const current = tenant?.tenant_slug ?? creds.tenant_slug;
2437
2512
  if (!current) return;
2438
- announceUrl(creds, current, appSlug, functionName);
2513
+ const surface = await invocationSurfaceOrNone(creds.gateway_url);
2514
+ announceUrl(surface, current, appSlug, functionName);
2439
2515
  if (!tenant) {
2440
2516
  console.log(` '${current}' is your workspace address. Whether it can still be changed`);
2441
2517
  console.log(" is a question for the gateway, which did not answer.\n");
@@ -2459,7 +2535,7 @@ async function offerWorkspaceAddress(session, creds, tenant, appSlug, functionNa
2459
2535
  try {
2460
2536
  const renamed = await renameTenantSlug(creds, answer);
2461
2537
  console.log(`\n[wawesome] ✅ Workspace renamed to '${renamed.tenant_slug}'.`);
2462
- announceUrl(creds, renamed.tenant_slug, appSlug, functionName);
2538
+ announceUrl(surface, renamed.tenant_slug, appSlug, functionName);
2463
2539
  return;
2464
2540
  } catch (err) {
2465
2541
  console.log(`[wawesome] ${errorText(err)}`);
@@ -2739,6 +2815,8 @@ export default {
2739
2815
  }
2740
2816
  //#endregion
2741
2817
  //#region src/version.ts
2818
+ /** How both surfaces name a version nobody can switch to. */
2819
+ const RECLAIMED = "⚠ [assets reclaimed]";
2742
2820
  async function fetchVersions(gatewayUrl, tenantJwt, app, funcName, env, isVerbose) {
2743
2821
  const url = `${gatewayUrl}/v1/apps/${encodeURIComponent(app)}/functions/${encodeURIComponent(funcName)}/versions?environment=${encodeURIComponent(env)}`;
2744
2822
  if (isVerbose) console.log(`[wawesome:verbose] GET ${url}`);
@@ -2788,7 +2866,8 @@ async function listVersions(options) {
2788
2866
  const hashStr = v.cwasm_hash.slice(0, 8).padEnd(8);
2789
2867
  const dateStr = new Date(v.created_at).toISOString().replace("T", " ").slice(0, 19).padEnd(20);
2790
2868
  const statusStr = v.status.padEnd(9);
2791
- console.log(`${verStr} | ${hashStr} | ${dateStr} | ${statusStr} | ${isActive ? "\x1B[32m★ [active]\x1B[0m" : ""}`);
2869
+ const badge = isActive ? "\x1B[32m★ [active]\x1B[0m" : v.rollbackable === false ? `\x1b[33m${RECLAIMED}\x1b[0m` : "";
2870
+ console.log(`${verStr} | ${hashStr} | ${dateStr} | ${statusStr} | ${badge}`);
2792
2871
  }
2793
2872
  console.log("");
2794
2873
  }
@@ -2836,7 +2915,7 @@ async function switchVersion(targetInput, options) {
2836
2915
  const isActive = v.version_number === data.active_version_number;
2837
2916
  const hashShort = v.cwasm_hash.slice(0, 8);
2838
2917
  const dateStr = new Date(v.created_at).toISOString().replace("T", " ").slice(0, 19);
2839
- const badge = isActive ? " ★ [active]" : "";
2918
+ const badge = isActive ? " ★ [active]" : v.rollbackable === false ? ` ${RECLAIMED}` : "";
2840
2919
  return {
2841
2920
  name: `v${v.version_number} (${hashShort}) • ${dateStr}${badge}`,
2842
2921
  value: v.version_number
@@ -2867,9 +2946,9 @@ async function switchVersion(targetInput, options) {
2867
2946
  })
2868
2947
  });
2869
2948
  if (!deployRes.ok) {
2870
- const errorBody = await deployRes.text();
2871
- console.error(`[wawesome] Error: Failed to set active version (HTTP ${deployRes.status}).`);
2872
- if (isVerbose) console.error(`[wawesome:verbose] Response: ${errorBody}`);
2949
+ const rejection = await asGatewayError(deployRes, `Failed to set active version (HTTP ${deployRes.status}).`);
2950
+ console.error(`[wawesome] Error: ${rejection.message}`);
2951
+ if (isVerbose) console.error(`[wawesome:verbose] Response: ${rejection.body}`);
2873
2952
  process.exit(1);
2874
2953
  }
2875
2954
  console.log(`\n======================================================`);
@@ -3395,8 +3474,11 @@ async function showWorkspace(options) {
3395
3474
  console.log(` Address: ${tenant.tenant_slug}`);
3396
3475
  console.log(` Tenant: ${tenant.id}`);
3397
3476
  if (options.verbose) {
3398
- console.log(` URLs: ${publicAddressTemplate(creds.gateway_url, tenant.tenant_slug)}`);
3399
- console.log(` ${SUBTREE_NOTE}`);
3477
+ const template = publicAddressTemplate(await invocationSurfaceOrNone(creds.gateway_url), tenant.tenant_slug);
3478
+ if (template) {
3479
+ console.log(` URLs: ${template}`);
3480
+ console.log(` ${SUBTREE_NOTE}`);
3481
+ }
3400
3482
  }
3401
3483
  if (tenant.slug_locked) console.log("\n 🔒 The address is fixed — a Function version has been promoted and live URLs carry it.");
3402
3484
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wawesome",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "CLI tool for building and deploying serverless functions on wawesome.io platform",
5
5
  "type": "module",
6
6
  "bin": {