lakebed 0.0.8 → 0.0.10

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
@@ -1021,7 +1021,7 @@ function metadataFields() {
1021
1021
  return new Set(["id", "createdAt", "updatedAt"]);
1022
1022
  }
1023
1023
 
1024
- function assertFieldValue(tableName, fieldName, field, value) {
1024
+ function assertFieldValue(tableName, fieldName, field, value, limits = DEFAULT_ANONYMOUS_LIMITS) {
1025
1025
  if (value === undefined) {
1026
1026
  throw new Error(`Missing value for ${tableName}.${fieldName}`);
1027
1027
  }
@@ -1034,12 +1034,13 @@ function assertFieldValue(tableName, fieldName, field, value) {
1034
1034
  throw new Error(`Expected ${tableName}.${fieldName} to be a boolean.`);
1035
1035
  }
1036
1036
 
1037
- if (byteLength(value) > DEFAULT_ANONYMOUS_LIMITS.maxValueBytes) {
1038
- throw new Error(`Value for ${tableName}.${fieldName} exceeds ${DEFAULT_ANONYMOUS_LIMITS.maxValueBytes} bytes.`);
1037
+ const maxValueBytes = limits.maxValueBytes ?? DEFAULT_ANONYMOUS_LIMITS.maxValueBytes;
1038
+ if (byteLength(value) > maxValueBytes) {
1039
+ throw new Error(`Value for ${tableName}.${fieldName} exceeds ${maxValueBytes} bytes.`);
1039
1040
  }
1040
1041
  }
1041
1042
 
1042
- function prepareInsert(schema, tableName, value) {
1043
+ function prepareInsert(schema, tableName, value, limits = DEFAULT_ANONYMOUS_LIMITS) {
1043
1044
  const table = schema[tableName];
1044
1045
  if (!table) {
1045
1046
  throw new Error(`Unknown table: ${tableName}`);
@@ -1065,14 +1066,14 @@ function prepareInsert(schema, tableName, value) {
1065
1066
 
1066
1067
  for (const [fieldName, field] of Object.entries(fields)) {
1067
1068
  const valueOrDefault = value[fieldName] ?? field.defaultValue;
1068
- assertFieldValue(tableName, fieldName, field, valueOrDefault);
1069
+ assertFieldValue(tableName, fieldName, field, valueOrDefault, limits);
1069
1070
  row[fieldName] = valueOrDefault;
1070
1071
  }
1071
1072
 
1072
1073
  return row;
1073
1074
  }
1074
1075
 
1075
- function preparePatch(schema, tableName, patch) {
1076
+ function preparePatch(schema, tableName, patch, limits = DEFAULT_ANONYMOUS_LIMITS) {
1076
1077
  const table = schema[tableName];
1077
1078
  if (!table) {
1078
1079
  throw new Error(`Unknown table: ${tableName}`);
@@ -1089,7 +1090,7 @@ function preparePatch(schema, tableName, patch) {
1089
1090
  throw new Error(`Lakebed manages ${tableName}.${key}; app code cannot update it directly.`);
1090
1091
  }
1091
1092
 
1092
- assertFieldValue(tableName, key, fields[key], value);
1093
+ assertFieldValue(tableName, key, fields[key], value, limits);
1093
1094
  cleanPatch[key] = value;
1094
1095
  }
1095
1096
 
@@ -1097,6 +1098,14 @@ function preparePatch(schema, tableName, patch) {
1097
1098
  return cleanPatch;
1098
1099
  }
1099
1100
 
1101
+ export function prepareAnonymousInsert(schema, tableName, value, limits = DEFAULT_ANONYMOUS_LIMITS) {
1102
+ return prepareInsert(schema, tableName, value, limits);
1103
+ }
1104
+
1105
+ export function prepareAnonymousPatch(schema, tableName, patch, limits = DEFAULT_ANONYMOUS_LIMITS) {
1106
+ return preparePatch(schema, tableName, patch, limits);
1107
+ }
1108
+
1100
1109
  function compareValues(left, right) {
1101
1110
  if (left === right) {
1102
1111
  return 0;
@@ -1132,6 +1141,10 @@ async function loadSourceApp(artifact) {
1132
1141
  return null;
1133
1142
  }
1134
1143
 
1144
+ if (process.env.LAKEBED_UNSAFE_IN_PROCESS_SOURCE !== "1") {
1145
+ throw new Error("Claimed-source execution requires a configured source runtime. Set LAKEBED_UNSAFE_IN_PROCESS_SOURCE=1 only for local unsafe debugging.");
1146
+ }
1147
+
1135
1148
  if (!sourceModuleCache.has(source.bundleHash)) {
1136
1149
  const url = `data:text/javascript;base64,${source.bundle}`;
1137
1150
  sourceModuleCache.set(source.bundleHash, import(url).then((module) => module.default));
@@ -1234,9 +1247,13 @@ async function createSourceContext({ artifact, auth, deployId, state }) {
1234
1247
  };
1235
1248
  }
1236
1249
 
1237
- export async function executeAnonymousQuery({ args = [], artifact, auth, deployId, name, state }) {
1238
- const sourceApp = await loadSourceApp(artifact);
1239
- if (sourceApp) {
1250
+ export async function executeAnonymousQuery({ args = [], artifact, auth, deployId, name, sourceRuntime, state }) {
1251
+ if (artifact.server?.source) {
1252
+ if (sourceRuntime) {
1253
+ return sourceRuntime.executeQuery({ args, artifact, auth, deployId, name, state });
1254
+ }
1255
+
1256
+ const sourceApp = await loadSourceApp(artifact);
1240
1257
  const handler = sourceApp.queries?.[name];
1241
1258
  if (!handler) {
1242
1259
  throw new Error(`Unknown query: ${name}`);
@@ -1262,9 +1279,13 @@ async function checkUpdateGuards({ auth, guards = [], row }) {
1262
1279
  return true;
1263
1280
  }
1264
1281
 
1265
- export async function executeAnonymousMutation({ args = [], artifact, auth, deployId, name, state }) {
1266
- const sourceApp = await loadSourceApp(artifact);
1267
- if (sourceApp) {
1282
+ export async function executeAnonymousMutation({ args = [], artifact, auth, deployId, limits = DEFAULT_ANONYMOUS_LIMITS, name, sourceRuntime, state }) {
1283
+ if (artifact.server?.source) {
1284
+ if (sourceRuntime) {
1285
+ return sourceRuntime.executeMutation({ args, artifact, auth, deployId, limits, name, state });
1286
+ }
1287
+
1288
+ const sourceApp = await loadSourceApp(artifact);
1268
1289
  const handler = sourceApp.mutations?.[name];
1269
1290
  if (!handler) {
1270
1291
  throw new Error(`Unknown mutation: ${name}`);
package/src/cli.js CHANGED
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env node
2
+ import { execFile } from "node:child_process";
2
3
  import { createServer } from "node:http";
3
4
  import { existsSync, realpathSync } from "node:fs";
4
5
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
5
6
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
6
7
  import { createInterface } from "node:readline/promises";
8
+ import { promisify } from "node:util";
7
9
  import { fileURLToPath, pathToFileURL } from "node:url";
8
10
  import * as esbuild from "esbuild";
9
11
  import { WebSocketServer } from "ws";
@@ -28,12 +30,14 @@ const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
28
30
  const packageNodeModules = resolve(packageDir, "node_modules");
29
31
  const sourceNamespace = "lakebed-source";
30
32
  const defaultDeployApiUrl = "https://api.lakebed.app";
33
+ const execFileAsync = promisify(execFile);
31
34
 
32
35
  function usage() {
33
36
  console.log(`lakebed
34
37
 
35
38
  Usage:
36
- lakebed new [name] [--template todo]
39
+ lakebed new [name] [--template todo] [--no-git]
40
+ lakebed create [name] [--template todo] [--no-git]
37
41
  lakebed dev <capsule-dir> [--port 3000]
38
42
  lakebed build <capsule-dir> --target anonymous [--out .lakebed/artifacts/app.json] [--json]
39
43
  lakebed deploy [capsule-dir] [--ttl 7d] [--api <url>] [--json]
@@ -1238,6 +1242,7 @@ export function App() {
1238
1242
  const todos = useQuery<Todo[]>("todos");
1239
1243
  const addTodo = useMutation<[text: string], void>("addTodo");
1240
1244
  const authLabel = auth.email ?? auth.displayName;
1245
+ const authStatus = auth.isLoading && auth.isGuest ? "checking session" : "signed in as " + authLabel;
1241
1246
 
1242
1247
  async function onSubmit(event: SubmitEvent) {
1243
1248
  event.preventDefault();
@@ -1256,14 +1261,14 @@ export function App() {
1256
1261
  <main className="min-h-screen bg-black px-6 py-10 text-white">
1257
1262
  <section className="mx-auto max-w-2xl">
1258
1263
  <div className="mb-3 flex items-center justify-between gap-3">
1259
- <p className="font-mono text-sm text-neutral-500">signed in as {authLabel}</p>
1260
- {auth.isGuest ? (
1264
+ <p className="font-mono text-sm text-neutral-500">{authStatus}</p>
1265
+ {!auth.isLoading && auth.isGuest ? (
1261
1266
  <SignInWithGoogle className="border border-neutral-700 px-3 py-1.5 text-sm font-medium text-neutral-200 hover:border-white hover:text-white" />
1262
- ) : (
1267
+ ) : !auth.isLoading ? (
1263
1268
  <button className="text-sm text-neutral-400 hover:text-white" type="button" onClick={() => signOut()}>
1264
1269
  Sign out
1265
1270
  </button>
1266
- )}
1271
+ ) : null}
1267
1272
  </div>
1268
1273
  <h1 className="mb-8 text-5xl font-bold tracking-tight">${title}</h1>
1269
1274
  <form className="mb-8 flex gap-3" onSubmit={(event) => void onSubmit(event)}>
@@ -1310,6 +1315,7 @@ pnpm lakebed dev ${name}
1310
1315
  async function newCommand(args) {
1311
1316
  const [nameArg] = positionals(args);
1312
1317
  const template = readArg(args, "--template", "todo");
1318
+ const shouldInitGit = !hasFlag(args, "--no-git");
1313
1319
 
1314
1320
  if (template !== "todo") {
1315
1321
  throw new Error(`Unknown template: ${template}`);
@@ -1333,6 +1339,51 @@ async function newCommand(args) {
1333
1339
  }
1334
1340
 
1335
1341
  console.log(`Created Lakebed capsule at ${targetDir}`);
1342
+ const gitStatus = shouldInitGit ? await initializeGitRepository(targetDir) : "Skipped git setup (--no-git).";
1343
+ console.log(gitStatus);
1344
+ console.log(`
1345
+ Next:
1346
+ cd ${shellQuote(name)}
1347
+ npx lakebed dev
1348
+
1349
+ deploy instantly for free with:
1350
+ npx lakebed deploy`);
1351
+ }
1352
+
1353
+ async function initializeGitRepository(targetDir) {
1354
+ if (await isInsideGitWorkTree(dirname(targetDir))) {
1355
+ return "Skipped git setup because the capsule is inside an existing git repository.";
1356
+ }
1357
+
1358
+ try {
1359
+ await execFileAsync("git", ["init"], { cwd: targetDir });
1360
+ await execFileAsync("git", ["add", "."], { cwd: targetDir });
1361
+ await execFileAsync(
1362
+ "git",
1363
+ ["-c", "user.name=Lakebed", "-c", "user.email=lakebed@example.invalid", "commit", "-m", "Initial Lakebed capsule"],
1364
+ { cwd: targetDir }
1365
+ );
1366
+ return "Initialized git repository and created initial commit.";
1367
+ } catch (error) {
1368
+ return `Skipped git setup: ${error instanceof Error ? error.message : String(error)}`;
1369
+ }
1370
+ }
1371
+
1372
+ async function isInsideGitWorkTree(cwd) {
1373
+ try {
1374
+ const { stdout } = await execFileAsync("git", ["rev-parse", "--is-inside-work-tree"], { cwd });
1375
+ return stdout.trim() === "true";
1376
+ } catch {
1377
+ return false;
1378
+ }
1379
+ }
1380
+
1381
+ function shellQuote(value) {
1382
+ if (/^[A-Za-z0-9_./:-]+$/.test(value)) {
1383
+ return value;
1384
+ }
1385
+
1386
+ return `'${value.replaceAll("'", "'\\''")}'`;
1336
1387
  }
1337
1388
 
1338
1389
  async function promptForCapsuleName() {
@@ -1395,7 +1446,7 @@ async function runMany(args) {
1395
1446
  async function main() {
1396
1447
  const [command, ...args] = process.argv.slice(2);
1397
1448
 
1398
- if (command === "new") {
1449
+ if (command === "new" || command === "create") {
1399
1450
  await newCommand(args);
1400
1451
  return;
1401
1452
  }
package/src/client.d.ts CHANGED
@@ -6,6 +6,7 @@ export type Auth = {
6
6
  provider: "guest" | "google";
7
7
  isGuest: boolean;
8
8
  isAuthenticated: boolean;
9
+ isLoading?: boolean;
9
10
  email?: string;
10
11
  emailVerified?: boolean;
11
12
  name?: string;
package/src/client.js CHANGED
@@ -11,7 +11,7 @@ const encoder = new TextEncoder();
11
11
 
12
12
  let socket = null;
13
13
  let nextRequestId = 1;
14
- let auth = createGuestAuth("local");
14
+ let auth = createInitialAuth();
15
15
  const authListeners = new Set();
16
16
  const queryValues = new Map();
17
17
  const queryListeners = new Map();
@@ -51,6 +51,40 @@ function createGuestAuth(name) {
51
51
  };
52
52
  }
53
53
 
54
+ function withAuthLoading(value, isLoading) {
55
+ return { ...value, isLoading };
56
+ }
57
+
58
+ function browserStorage() {
59
+ if (typeof window === "undefined") {
60
+ return null;
61
+ }
62
+
63
+ try {
64
+ return window.localStorage;
65
+ } catch {
66
+ return null;
67
+ }
68
+ }
69
+
70
+ function currentGuestName() {
71
+ if (typeof window === "undefined") {
72
+ return "local";
73
+ }
74
+
75
+ return new URLSearchParams(window.location.search).get("lakebed_guest") ?? "local";
76
+ }
77
+
78
+ function createInitialAuth() {
79
+ const token = storedAuthToken();
80
+ const googleAuth = createGoogleAuthFromToken(token);
81
+ if (googleAuth) {
82
+ return withAuthLoading(googleAuth, true);
83
+ }
84
+
85
+ return withAuthLoading(createGuestAuth(currentGuestName()), typeof window !== "undefined");
86
+ }
87
+
54
88
  function emitAuth() {
55
89
  for (const listener of authListeners) {
56
90
  listener(auth);
@@ -203,7 +237,18 @@ export function decodeIdentityClaims(idToken) {
203
237
  }
204
238
 
205
239
  function readStoredIdentity() {
206
- const raw = window.localStorage.getItem(AUTH_STORAGE_KEY) ?? window.localStorage.getItem(LEGACY_SHOO_STORAGE_KEY);
240
+ const storage = browserStorage();
241
+ if (!storage) {
242
+ return { userId: null };
243
+ }
244
+
245
+ let raw = null;
246
+ try {
247
+ raw = storage.getItem(AUTH_STORAGE_KEY) ?? storage.getItem(LEGACY_SHOO_STORAGE_KEY);
248
+ } catch {
249
+ return { userId: null };
250
+ }
251
+
207
252
  if (!raw) {
208
253
  return { userId: null };
209
254
  }
@@ -227,20 +272,38 @@ function readStoredIdentity() {
227
272
  }
228
273
 
229
274
  function persistIdentity(userId, token, expiresIn) {
230
- window.localStorage.setItem(
231
- AUTH_STORAGE_KEY,
232
- JSON.stringify({
233
- expiresIn,
234
- receivedAt: Date.now(),
235
- token,
236
- userId
237
- })
238
- );
275
+ const storage = browserStorage();
276
+ if (!storage) {
277
+ return;
278
+ }
279
+
280
+ try {
281
+ storage.setItem(
282
+ AUTH_STORAGE_KEY,
283
+ JSON.stringify({
284
+ expiresIn,
285
+ receivedAt: Date.now(),
286
+ token,
287
+ userId
288
+ })
289
+ );
290
+ } catch {
291
+ // Storage persistence is best effort; server auth remains authoritative.
292
+ }
239
293
  }
240
294
 
241
295
  function clearStoredIdentity() {
242
- window.localStorage.removeItem(AUTH_STORAGE_KEY);
243
- window.localStorage.removeItem(LEGACY_SHOO_STORAGE_KEY);
296
+ const storage = browserStorage();
297
+ if (!storage) {
298
+ return;
299
+ }
300
+
301
+ try {
302
+ storage.removeItem(AUTH_STORAGE_KEY);
303
+ storage.removeItem(LEGACY_SHOO_STORAGE_KEY);
304
+ } catch {
305
+ // Ignore storage failures; reconnecting as guest is still safe.
306
+ }
244
307
  }
245
308
 
246
309
  function storedAuthToken() {
@@ -372,7 +435,7 @@ async function handleGoogleCallback() {
372
435
 
373
436
  const localAuth = createGoogleAuthFromToken(token.id_token);
374
437
  if (localAuth) {
375
- auth = localAuth;
438
+ auth = withAuthLoading(localAuth, true);
376
439
  emitAuth();
377
440
  }
378
441
 
@@ -438,7 +501,7 @@ function connect() {
438
501
  const message = JSON.parse(String(event.data));
439
502
 
440
503
  if (message.op === "auth.result") {
441
- auth = message.auth;
504
+ auth = withAuthLoading(message.auth, false);
442
505
  emitAuth();
443
506
  return;
444
507
  }
@@ -529,7 +592,7 @@ export async function signInWithGoogle(options = {}) {
529
592
 
530
593
  export function signOut() {
531
594
  clearStoredIdentity();
532
- auth = createGuestAuth(new URLSearchParams(window.location.search).get("lakebed_guest") ?? "local");
595
+ auth = withAuthLoading(createGuestAuth(currentGuestName()), true);
533
596
  emitAuth();
534
597
  reconnect();
535
598
  }
@@ -0,0 +1,31 @@
1
+ function isBareSpecifier(specifier) {
2
+ return !specifier.startsWith(".") && !specifier.startsWith("/") && !/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(specifier);
3
+ }
4
+
5
+ function isWorkerInternalImport(context) {
6
+ return !context.parentURL || context.parentURL.startsWith("file:");
7
+ }
8
+
9
+ export async function resolve(specifier, context, nextResolve) {
10
+ if (specifier.startsWith("node:")) {
11
+ if (isWorkerInternalImport(context)) {
12
+ return nextResolve(specifier, context);
13
+ }
14
+
15
+ throw new Error(`Source runtime imports are not available: ${specifier}`);
16
+ }
17
+
18
+ if (isBareSpecifier(specifier)) {
19
+ throw new Error(`Source runtime imports are not available: ${specifier}`);
20
+ }
21
+
22
+ if (specifier.startsWith("file:") && context.parentURL) {
23
+ throw new Error(`Source runtime file imports are not available: ${specifier}`);
24
+ }
25
+
26
+ if (specifier.startsWith("/") || specifier.startsWith("./") || specifier.startsWith("../")) {
27
+ throw new Error(`Source runtime relative imports are not available: ${specifier}`);
28
+ }
29
+
30
+ return nextResolve(specifier, context);
31
+ }