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/README.md +9 -1
- package/package.json +5 -2
- package/src/anonymous-server.js +1066 -329
- package/src/anonymous.js +34 -13
- package/src/cli.js +57 -6
- package/src/client.d.ts +1 -0
- package/src/client.js +79 -16
- package/src/source-runtime-loader.mjs +31 -0
- package/src/source-runtime-worker.js +454 -0
- package/src/source-runtime.js +668 -0
- package/src/version.js +1 -1
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
|
-
|
|
1038
|
-
|
|
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
|
-
|
|
1239
|
-
|
|
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
|
-
|
|
1267
|
-
|
|
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">
|
|
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
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 =
|
|
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
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
243
|
-
|
|
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(
|
|
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
|
+
}
|