lakebed 0.0.14 → 0.0.16

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/src/anonymous.js CHANGED
@@ -17,12 +17,15 @@ export const SERVER_ENV_LIMITS = {
17
17
  export const DEFAULT_ANONYMOUS_LIMITS = {
18
18
  artifactBytes: 1024 * 1024,
19
19
  stateBytes: 1024 * 1024,
20
+ stateRows: 16384,
20
21
  requestsPerDay: 10000,
21
22
  mutationsPerDay: 1000,
22
23
  rowsReturned: 1000,
23
24
  instructionBudget: 50000,
24
25
  maxValueBytes: 65536,
25
- logEntries: 1000
26
+ logEntries: 1000,
27
+ logBytes: 256 * 1024,
28
+ logEntryBytes: 16 * 1024
26
29
  };
27
30
 
28
31
  const expressionOps = new Set(["arg", "auth", "call", "row"]);
@@ -63,10 +66,27 @@ export function stableStringify(value) {
63
66
  .join(",")}}`;
64
67
  }
65
68
 
66
- function byteLength(value) {
69
+ export function byteLength(value) {
67
70
  return Buffer.byteLength(typeof value === "string" ? value : stableStringify(value), "utf8");
68
71
  }
69
72
 
73
+ export function stateRowsLimitForLimits(limits = DEFAULT_ANONYMOUS_LIMITS) {
74
+ const explicit = limits.stateRows ?? DEFAULT_ANONYMOUS_LIMITS.stateRows;
75
+ if (Number.isSafeInteger(explicit) && explicit > 0) {
76
+ return explicit;
77
+ }
78
+
79
+ const stateBytes = limits.stateBytes ?? DEFAULT_ANONYMOUS_LIMITS.stateBytes;
80
+ return Math.max(1, Math.floor(stateBytes / 64));
81
+ }
82
+
83
+ export function mutationTransactionOptions(limits = DEFAULT_ANONYMOUS_LIMITS) {
84
+ return {
85
+ stateBytesLimit: limits.stateBytes ?? DEFAULT_ANONYMOUS_LIMITS.stateBytes,
86
+ stateRowsLimit: stateRowsLimitForLimits(limits)
87
+ };
88
+ }
89
+
70
90
  function cloneJson(value) {
71
91
  return JSON.parse(JSON.stringify(value));
72
92
  }
@@ -953,7 +973,7 @@ export function createSlug() {
953
973
  const adjectives = ["frosted", "quiet", "bright", "lucky", "silver", "rapid", "clear", "open"];
954
974
  const nouns = ["river", "field", "ridge", "harbor", "garden", "signal", "meadow", "orbit"];
955
975
  const bytes = randomBytes(4);
956
- return `${adjectives[bytes[0] % adjectives.length]}-${nouns[bytes[1] % nouns.length]}-${bytes.toString("base64url").slice(0, 6).toLowerCase()}`;
976
+ return `${adjectives[bytes[0] % adjectives.length]}-${nouns[bytes[1] % nouns.length]}-${bytes.toString("hex").slice(0, 6)}`;
957
977
  }
958
978
 
959
979
  export function hashClaimToken(token) {
@@ -1295,7 +1315,7 @@ export async function executeAnonymousMutation({ args = [], artifact, auth, depl
1295
1315
  const result = await handler(source.ctx, ...args);
1296
1316
  await source.flush(tx);
1297
1317
  return result ?? null;
1298
- });
1318
+ }, mutationTransactionOptions(limits));
1299
1319
  }
1300
1320
 
1301
1321
  const mutation = artifact.server.mutations?.[name];
@@ -1341,5 +1361,5 @@ export async function executeAnonymousMutation({ args = [], artifact, auth, depl
1341
1361
  }
1342
1362
 
1343
1363
  return null;
1344
- });
1364
+ }, mutationTransactionOptions(limits));
1345
1365
  }
package/src/cli.js CHANGED
@@ -7,7 +7,7 @@ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "nod
7
7
  import { clearLine, cursorTo } from "node:readline";
8
8
  import { createInterface } from "node:readline/promises";
9
9
  import { promisify } from "node:util";
10
- import { fileURLToPath, pathToFileURL } from "node:url";
10
+ import { domainToASCII, fileURLToPath, pathToFileURL } from "node:url";
11
11
  import * as esbuild from "esbuild";
12
12
  import { WebSocketServer } from "ws";
13
13
  import {
@@ -17,7 +17,6 @@ import {
17
17
  SERVER_ENV_FILE,
18
18
  createAnonymousArtifact,
19
19
  createClaimedArtifact,
20
- parseTtlSeconds,
21
20
  stableStringify,
22
21
  validateServerEnvValues
23
22
  } from "./anonymous.js";
@@ -41,8 +40,9 @@ Usage:
41
40
  lakebed create [name] [--template todo] [--no-git]
42
41
  lakebed dev [capsule-dir] [--port 3000]
43
42
  lakebed build [capsule-dir] --target anonymous [--out .lakebed/artifacts/app.json] [--json]
44
- lakebed deploy [capsule-dir] [--ttl 7d] [--api <url>] [--json]
43
+ lakebed deploy [capsule-dir] [--api <url>] [--json]
45
44
  lakebed claim [capsule-dir] [--api <url>] [--json]
45
+ lakebed domains add <subdomain.lakebed.app> [--api <url>] [--json]
46
46
  lakebed anonymous-server [--port 8787] [--public-root-url <url>] [--app-base-domain <domain>]
47
47
  lakebed inspect <deploy-id-or-url> [--api <url>] [--json]
48
48
  lakebed run-many [capsule-dir] [--count 20] [--base-port 4000]
@@ -72,8 +72,7 @@ const optionsWithValues = new Set([
72
72
  "--port",
73
73
  "--public-root-url",
74
74
  "--target",
75
- "--template",
76
- "--ttl"
75
+ "--template"
77
76
  ]);
78
77
 
79
78
  function positionals(args) {
@@ -948,12 +947,67 @@ function claimUrlFromDeployMetadata(metadata) {
948
947
  return `${String(metadata.api).replace(/\/+$/g, "")}/claim/${encodeURIComponent(metadata.deployId)}/${encodeURIComponent(metadata.claimToken)}`;
949
948
  }
950
949
 
951
- function deployRequestBody(envelope, ttl, { serverEnv } = {}) {
950
+ function claimCommandText({ api, capsuleArg }) {
951
+ const parts = ["lakebed", "claim"];
952
+ if (capsuleArg) {
953
+ parts.push(capsuleArg);
954
+ }
955
+ if (api !== defaultDeployApiUrl) {
956
+ parts.push("--api", api);
957
+ }
958
+ return parts.map(shellQuote).join(" ");
959
+ }
960
+
961
+ function normalizeDomainCommandHostname(value) {
962
+ const raw = String(value ?? "").trim();
963
+ if (!raw) {
964
+ throw new Error("Domain is required.");
965
+ }
966
+
967
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw) || raw.includes("/") || raw.includes("\\") || raw.includes(":")) {
968
+ throw new Error("Enter a domain like my-app.lakebed.app, without a scheme, port, or path.");
969
+ }
970
+
971
+ const hostname = domainToASCII(raw.replace(/\.$/, "").toLowerCase());
972
+ if (!hostname) {
973
+ throw new Error("Domain is not a valid hostname.");
974
+ }
975
+
976
+ if (hostname.length > 253) {
977
+ throw new Error("Domain must be 253 characters or fewer.");
978
+ }
979
+
980
+ for (const label of hostname.split(".")) {
981
+ if (!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label)) {
982
+ throw new Error("Domain labels must use letters, numbers, or hyphens.");
983
+ }
984
+ }
985
+
986
+ return hostname;
987
+ }
988
+
989
+ function browserOpenInvocation(url) {
990
+ if (process.platform === "darwin") {
991
+ return { command: "open", args: [url] };
992
+ }
993
+
994
+ if (process.platform === "win32") {
995
+ return { command: "cmd", args: ["/c", "start", "", url] };
996
+ }
997
+
998
+ return { command: "xdg-open", args: [url] };
999
+ }
1000
+
1001
+ async function openUrlInBrowser(url) {
1002
+ const invocation = browserOpenInvocation(url);
1003
+ await execFileAsync(invocation.command, invocation.args, { windowsHide: true });
1004
+ }
1005
+
1006
+ function deployRequestBody(envelope, { serverEnv } = {}) {
952
1007
  const body = {
953
1008
  artifact: envelope.artifact,
954
1009
  clientBundle: envelope.clientBundle,
955
- clientVersion: LAKEBED_VERSION,
956
- requestedTtlSeconds: ttl
1010
+ clientVersion: LAKEBED_VERSION
957
1011
  };
958
1012
  if (serverEnv !== undefined) {
959
1013
  body.serverEnv = {
@@ -1012,6 +1066,10 @@ async function readResponseJson(response) {
1012
1066
  }
1013
1067
 
1014
1068
  async function deployCommand(args) {
1069
+ if (args.some((arg) => arg === "--ttl" || arg.startsWith("--ttl="))) {
1070
+ throw new Error("lakebed deploy no longer accepts --ttl. Deploy expiry is set by the server.");
1071
+ }
1072
+
1015
1073
  const [capsuleArg] = positionals(args);
1016
1074
  const capsuleDir = resolveCapsuleDir(capsuleArg);
1017
1075
  const sourceStore = await createMemorySourceStoreFromDirectory(capsuleDir);
@@ -1019,7 +1077,6 @@ async function deployCommand(args) {
1019
1077
  const serverEnv = await readCapsuleServerEnv(sourceStore);
1020
1078
  const serverEnvKeys = Object.keys(serverEnv).sort();
1021
1079
  const hasServerEnvValues = serverEnvKeys.length > 0;
1022
- const ttl = parseTtlSeconds(readArg(args, "--ttl", "7d"));
1023
1080
  const api = deployApiUrl(args);
1024
1081
  const metadata = await readDeployMetadata(capsuleDir);
1025
1082
  const canUpdate =
@@ -1071,7 +1128,7 @@ async function deployCommand(args) {
1071
1128
 
1072
1129
  if (canUpdate) {
1073
1130
  response = await fetch(`${api}/v1/deploys/${encodeURIComponent(metadata.deployId)}`, {
1074
- body: deployRequestBody(envelope, ttl, { serverEnv: serverEnvForUpdate }),
1131
+ body: deployRequestBody(envelope, { serverEnv: serverEnvForUpdate }),
1075
1132
  headers: {
1076
1133
  "Authorization": `Bearer ${metadata.claimToken}`,
1077
1134
  "Content-Type": "application/json"
@@ -1092,7 +1149,7 @@ async function deployCommand(args) {
1092
1149
  }
1093
1150
 
1094
1151
  response ??= await fetch(`${api}/v1/anonymous-deploys`, {
1095
- body: deployRequestBody(envelope, ttl),
1152
+ body: deployRequestBody(envelope),
1096
1153
  headers: {
1097
1154
  "Content-Type": "application/json"
1098
1155
  },
@@ -1133,7 +1190,7 @@ async function deployCommand(args) {
1133
1190
  console.log(`Updated: ${formatOptionalTimestamp(deployed.updatedAt, "unknown")}`);
1134
1191
  console.log(`Expires: ${formatOptionalTimestamp(deployed.expiresAt)}`);
1135
1192
  if (deployed.claimUrl) {
1136
- console.log(`Claim: ${deployed.claimUrl}`);
1193
+ console.log(`Claim: ${claimCommandText({ api, capsuleArg })}`);
1137
1194
  }
1138
1195
  console.log(`Inspect: lakebed inspect ${deployed.deployId}`);
1139
1196
  console.log("\nLimits:");
@@ -1150,7 +1207,7 @@ async function deployCommand(args) {
1150
1207
  }
1151
1208
  if (envelope.claimRequired) {
1152
1209
  console.log("\nThis app needs a claimed deploy before server-side fetch or server env can run.");
1153
- console.log("Open the claim URL, then run lakebed deploy again.");
1210
+ console.log(`Run ${claimCommandText({ api, capsuleArg })}, then run lakebed deploy again.`);
1154
1211
  }
1155
1212
  }
1156
1213
 
@@ -1203,8 +1260,56 @@ async function claimCommand(args) {
1203
1260
  return;
1204
1261
  }
1205
1262
 
1206
- console.log("Open this URL to claim the current project's deploy:");
1207
- console.log(claimUrl);
1263
+ try {
1264
+ await openUrlInBrowser(claimUrl);
1265
+ } catch (error) {
1266
+ throw new Error(
1267
+ `Unable to open the claim page in your browser: ${error instanceof Error ? error.message : String(error)}\n\nRun ${claimCommandText({ api, capsuleArg })} --json to read the claim URL.`
1268
+ );
1269
+ }
1270
+
1271
+ console.log(`Opened claim page for deploy ${result.deployId} in your browser.`);
1272
+ }
1273
+
1274
+ async function domainsCommand(args) {
1275
+ const [subcommand, hostname] = positionals(args);
1276
+ if (subcommand !== "add" || !hostname) {
1277
+ usage();
1278
+ return;
1279
+ }
1280
+
1281
+ const capsuleDir = resolveCapsuleDir(undefined);
1282
+ const api = deployApiUrl(args);
1283
+ const metadata = await readDeployMetadata(capsuleDir);
1284
+ if (!metadata) {
1285
+ throw new Error(`No Lakebed deploy metadata found at ${deployMetadataPath(capsuleDir)}. Run lakebed deploy from this project first.`);
1286
+ }
1287
+ if (metadata.api !== api) {
1288
+ throw new Error(`Saved deploy metadata is for ${metadata.api}, but this command is using ${api}. Pass --api ${metadata.api} to use it.`);
1289
+ }
1290
+ if (!metadata.deployId || !metadata.claimToken) {
1291
+ throw new Error("This project does not have a saved deploy token. Redeploy to refresh Lakebed deploy metadata.");
1292
+ }
1293
+ const normalizedHostname = normalizeDomainCommandHostname(hostname);
1294
+
1295
+ const response = await fetch(`${api}/v1/deploys/${encodeURIComponent(metadata.deployId)}/domains`, {
1296
+ body: JSON.stringify({ hostname: normalizedHostname }),
1297
+ headers: {
1298
+ "Authorization": `Bearer ${metadata.claimToken}`,
1299
+ "Content-Type": "application/json"
1300
+ },
1301
+ method: "POST"
1302
+ });
1303
+ const domain = await readResponseJson(response);
1304
+
1305
+ if (hasFlag(args, "--json")) {
1306
+ console.log(JSON.stringify(domain, null, 2));
1307
+ return;
1308
+ }
1309
+
1310
+ console.log("Registered Lakebed subdomain.");
1311
+ console.log(`Domain: ${domain.url}`);
1312
+ console.log(`Deploy: ${domain.deployId}`);
1208
1313
  }
1209
1314
 
1210
1315
  async function anonymousServerCommand(args) {
@@ -1252,6 +1357,9 @@ async function inspectCommand(args) {
1252
1357
  console.log(`URL: ${manifest.url}`);
1253
1358
  console.log(`Updated: ${formatOptionalTimestamp(manifest.updatedAt, "unknown")}`);
1254
1359
  console.log(`Expires: ${formatOptionalTimestamp(manifest.expiresAt)}`);
1360
+ if (Array.isArray(manifest.domains) && manifest.domains.length > 0) {
1361
+ console.log(`Domains: ${manifest.domains.map((domain) => domain.hostname).join(", ")}`);
1362
+ }
1255
1363
  console.log(`Artifact: ${manifest.artifactHash}`);
1256
1364
  console.log(`Queries: ${manifest.queries.join(", ") || "(none)"}`);
1257
1365
  console.log(`Mutations: ${manifest.mutations.join(", ") || "(none)"}`);
@@ -1565,11 +1673,12 @@ async function isInsideGitWorkTree(cwd) {
1565
1673
  }
1566
1674
 
1567
1675
  function shellQuote(value) {
1568
- if (/^[A-Za-z0-9_./:-]+$/.test(value)) {
1569
- return value;
1676
+ const text = String(value);
1677
+ if (/^[A-Za-z0-9_./:=,@%+-]+$/.test(text)) {
1678
+ return text;
1570
1679
  }
1571
1680
 
1572
- return `'${value.replaceAll("'", "'\\''")}'`;
1681
+ return `'${text.replaceAll("'", "'\\''")}'`;
1573
1682
  }
1574
1683
 
1575
1684
  async function promptForCapsuleName() {
@@ -1657,6 +1766,11 @@ async function main() {
1657
1766
  return;
1658
1767
  }
1659
1768
 
1769
+ if (command === "domains") {
1770
+ await domainsCommand(args);
1771
+ return;
1772
+ }
1773
+
1660
1774
  if (command === "anonymous-server") {
1661
1775
  await anonymousServerCommand(args);
1662
1776
  return;
@@ -7,6 +7,7 @@ import { dirname, resolve } from "node:path";
7
7
  import { fileURLToPath, pathToFileURL } from "node:url";
8
8
  import {
9
9
  DEFAULT_ANONYMOUS_LIMITS,
10
+ mutationTransactionOptions,
10
11
  prepareAnonymousPatch,
11
12
  stableStringify
12
13
  } from "./anonymous.js";
@@ -551,7 +552,7 @@ export class ChildProcessSourceRuntime {
551
552
  tx
552
553
  });
553
554
  return response.result ?? null;
554
- }, { stateBytesLimit: limits.stateBytes ?? DEFAULT_ANONYMOUS_LIMITS.stateBytes });
555
+ }, mutationTransactionOptions(limits));
555
556
  }
556
557
 
557
558
  runWorker(request) {
package/src/version.js CHANGED
@@ -1 +1 @@
1
- export const LAKEBED_VERSION = "0.0.14";
1
+ export const LAKEBED_VERSION = "0.0.16";