lakebed 0.0.8 → 0.0.9

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 CHANGED
@@ -60,6 +60,7 @@ export default capsule({
60
60
  ## Auth
61
61
 
62
62
  Every app starts with local guest auth. To let users sign in with Google, render the built-in button. Lakebed always asks Shoo for profile fields so the app can show a useful identifier like `auth.email` or `auth.displayName`.
63
+ Check `auth.isLoading` before showing signed-out UI, because Lakebed may still be confirming a stored session.
63
64
 
64
65
  ```tsx
65
66
  import { SignInWithGoogle, signOut, useAuth } from "lakebed/client";
@@ -68,6 +69,10 @@ export function App() {
68
69
  const auth = useAuth();
69
70
  const authLabel = auth.email ?? auth.displayName;
70
71
 
72
+ if (auth.isLoading) {
73
+ return <p>Checking session</p>;
74
+ }
75
+
71
76
  return auth.isGuest ? (
72
77
  <SignInWithGoogle />
73
78
  ) : (
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lakebed",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "Agent-native CLI and runtime for building and deploying Lakebed capsules.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -29,6 +29,9 @@
29
29
  "src/runtime.js",
30
30
  "src/server.d.ts",
31
31
  "src/server.js",
32
+ "src/source-runtime-loader.mjs",
33
+ "src/source-runtime-worker.js",
34
+ "src/source-runtime.js",
32
35
  "src/source-store.js",
33
36
  "src/version.js"
34
37
  ],
@@ -49,9 +52,6 @@
49
52
  "publishConfig": {
50
53
  "access": "public"
51
54
  },
52
- "scripts": {
53
- "check": "node --check src/cli.js && node --check src/runtime.js && node --check src/server.js && node --check src/client.js && node --check src/source-store.js && node --check src/anonymous.js && node --check src/anonymous-server.js && node --check src/auth.js && node --check src/version.js"
54
- },
55
55
  "dependencies": {
56
56
  "esbuild": "^0.27.1",
57
57
  "pg": "^8.16.3",
@@ -60,5 +60,8 @@
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/ws": "^8.18.1"
63
+ },
64
+ "scripts": {
65
+ "check": "node --check src/cli.js && node --check src/runtime.js && node --check src/server.js && node --check src/client.js && node --check src/source-store.js && node --check src/source-runtime.js && node --check src/source-runtime-worker.js && node --check src/source-runtime-loader.mjs && node --check src/anonymous.js && node --check src/anonymous-server.js && node --check src/auth.js && node --check src/version.js"
63
66
  }
64
- }
67
+ }
@@ -12,6 +12,7 @@ import {
12
12
  validateAnonymousDeployPayload
13
13
  } from "./anonymous.js";
14
14
  import { authFromUrl as resolveAuthFromUrl, createGuestAuth, requestOrigin, shooBaseUrlFromEnv } from "./auth.js";
15
+ import { createSourceRuntimeFromEnv } from "./source-runtime.js";
15
16
  import { WebSocketServer } from "ws";
16
17
 
17
18
  function now() {
@@ -3185,6 +3186,7 @@ export async function startAnonymousServer({
3185
3186
  publicRootUrl,
3186
3187
  quiet = false,
3187
3188
  shooBaseUrl = shooBaseUrlFromEnv(),
3189
+ sourceRuntime,
3188
3190
  store
3189
3191
  } = {}) {
3190
3192
  const resolvedAppBaseDomain = normalizeAppBaseDomain(appBaseDomain);
@@ -3193,6 +3195,7 @@ export async function startAnonymousServer({
3193
3195
  const resolvedDeveloperSessionSecret =
3194
3196
  developerSessionSecret || resolvedGithubOAuth?.sessionSecret || resolvedGithubOAuth?.clientSecret || adminPassword || "";
3195
3197
  const resolvedStore = store ?? (await createAnonymousStoreFromEnv());
3198
+ const resolvedSourceRuntime = sourceRuntime === undefined ? createSourceRuntimeFromEnv() : sourceRuntime;
3196
3199
  await resolvedStore.initialize();
3197
3200
  const subscriptions = new Map();
3198
3201
 
@@ -3304,6 +3307,7 @@ export async function startAnonymousServer({
3304
3307
  auth: subscription.auth,
3305
3308
  deployId,
3306
3309
  name,
3310
+ sourceRuntime: resolvedSourceRuntime,
3307
3311
  state: resolvedStore
3308
3312
  });
3309
3313
  websocketSend(ws, { data, name, op: "query.result" });
@@ -3762,6 +3766,7 @@ export async function startAnonymousServer({
3762
3766
  auth: subscription.auth,
3763
3767
  deployId: subscription.deploy.id,
3764
3768
  name: message.name,
3769
+ sourceRuntime: resolvedSourceRuntime,
3765
3770
  state: resolvedStore
3766
3771
  });
3767
3772
  websocketSend(ws, { data, id: message.id, name: message.name, ok: true, op: "query.result" });
@@ -3779,7 +3784,9 @@ export async function startAnonymousServer({
3779
3784
  artifact: subscription.artifact,
3780
3785
  auth: subscription.auth,
3781
3786
  deployId: subscription.deploy.id,
3787
+ limits: subscription.deploy.limits,
3782
3788
  name: message.name,
3789
+ sourceRuntime: resolvedSourceRuntime,
3783
3790
  state: resolvedStore
3784
3791
  });
3785
3792
  websocketSend(ws, { id: message.id, ok: true, op: "mutation.result", result });
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
@@ -1238,6 +1238,7 @@ export function App() {
1238
1238
  const todos = useQuery<Todo[]>("todos");
1239
1239
  const addTodo = useMutation<[text: string], void>("addTodo");
1240
1240
  const authLabel = auth.email ?? auth.displayName;
1241
+ const authStatus = auth.isLoading && auth.isGuest ? "checking session" : "signed in as " + authLabel;
1241
1242
 
1242
1243
  async function onSubmit(event: SubmitEvent) {
1243
1244
  event.preventDefault();
@@ -1256,14 +1257,14 @@ export function App() {
1256
1257
  <main className="min-h-screen bg-black px-6 py-10 text-white">
1257
1258
  <section className="mx-auto max-w-2xl">
1258
1259
  <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 ? (
1260
+ <p className="font-mono text-sm text-neutral-500">{authStatus}</p>
1261
+ {!auth.isLoading && auth.isGuest ? (
1261
1262
  <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
- ) : (
1263
+ ) : !auth.isLoading ? (
1263
1264
  <button className="text-sm text-neutral-400 hover:text-white" type="button" onClick={() => signOut()}>
1264
1265
  Sign out
1265
1266
  </button>
1266
- )}
1267
+ ) : null}
1267
1268
  </div>
1268
1269
  <h1 className="mb-8 text-5xl font-bold tracking-tight">${title}</h1>
1269
1270
  <form className="mb-8 flex gap-3" onSubmit={(event) => void onSubmit(event)}>
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
+ }