lakebed 0.0.7 → 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 +10 -3
- package/package.json +8 -5
- package/src/anonymous-server.js +7 -0
- package/src/anonymous.js +34 -13
- package/src/cli.js +26 -11
- 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/README.md
CHANGED
|
@@ -8,14 +8,16 @@ This package is an early public prototype. It includes the `lakebed` CLI, a loca
|
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
10
|
npm install -g lakebed
|
|
11
|
-
lakebed new
|
|
11
|
+
lakebed new
|
|
12
12
|
lakebed dev my-app
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
Enter `my-app` when `lakebed new` asks for a capsule name.
|
|
16
|
+
|
|
15
17
|
Or run it without a global install:
|
|
16
18
|
|
|
17
19
|
```sh
|
|
18
|
-
|
|
20
|
+
npx lakebed new
|
|
19
21
|
npm exec --package lakebed -- lakebed dev my-app
|
|
20
22
|
```
|
|
21
23
|
|
|
@@ -58,6 +60,7 @@ export default capsule({
|
|
|
58
60
|
## Auth
|
|
59
61
|
|
|
60
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.
|
|
61
64
|
|
|
62
65
|
```tsx
|
|
63
66
|
import { SignInWithGoogle, signOut, useAuth } from "lakebed/client";
|
|
@@ -66,6 +69,10 @@ export function App() {
|
|
|
66
69
|
const auth = useAuth();
|
|
67
70
|
const authLabel = auth.email ?? auth.displayName;
|
|
68
71
|
|
|
72
|
+
if (auth.isLoading) {
|
|
73
|
+
return <p>Checking session</p>;
|
|
74
|
+
}
|
|
75
|
+
|
|
69
76
|
return auth.isGuest ? (
|
|
70
77
|
<SignInWithGoogle />
|
|
71
78
|
) : (
|
|
@@ -109,7 +116,7 @@ queries: {
|
|
|
109
116
|
## Commands
|
|
110
117
|
|
|
111
118
|
```sh
|
|
112
|
-
lakebed new
|
|
119
|
+
lakebed new [name] [--template todo]
|
|
113
120
|
lakebed dev <capsule-dir> [--port 3000]
|
|
114
121
|
lakebed build <capsule-dir> --target anonymous [--out .lakebed/artifacts/app.json] [--json]
|
|
115
122
|
lakebed deploy [capsule-dir] [--ttl 7d] [--api <url>] [--json]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lakebed",
|
|
3
|
-
"version": "0.0.
|
|
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
|
+
}
|
package/src/anonymous-server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
@@ -3,6 +3,7 @@ import { createServer } from "node:http";
|
|
|
3
3
|
import { existsSync, realpathSync } from "node:fs";
|
|
4
4
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
5
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
6
7
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
8
|
import * as esbuild from "esbuild";
|
|
8
9
|
import { WebSocketServer } from "ws";
|
|
@@ -32,7 +33,7 @@ function usage() {
|
|
|
32
33
|
console.log(`lakebed
|
|
33
34
|
|
|
34
35
|
Usage:
|
|
35
|
-
lakebed new
|
|
36
|
+
lakebed new [name] [--template todo]
|
|
36
37
|
lakebed dev <capsule-dir> [--port 3000]
|
|
37
38
|
lakebed build <capsule-dir> --target anonymous [--out .lakebed/artifacts/app.json] [--json]
|
|
38
39
|
lakebed deploy [capsule-dir] [--ttl 7d] [--api <url>] [--json]
|
|
@@ -1237,6 +1238,7 @@ export function App() {
|
|
|
1237
1238
|
const todos = useQuery<Todo[]>("todos");
|
|
1238
1239
|
const addTodo = useMutation<[text: string], void>("addTodo");
|
|
1239
1240
|
const authLabel = auth.email ?? auth.displayName;
|
|
1241
|
+
const authStatus = auth.isLoading && auth.isGuest ? "checking session" : "signed in as " + authLabel;
|
|
1240
1242
|
|
|
1241
1243
|
async function onSubmit(event: SubmitEvent) {
|
|
1242
1244
|
event.preventDefault();
|
|
@@ -1255,14 +1257,14 @@ export function App() {
|
|
|
1255
1257
|
<main className="min-h-screen bg-black px-6 py-10 text-white">
|
|
1256
1258
|
<section className="mx-auto max-w-2xl">
|
|
1257
1259
|
<div className="mb-3 flex items-center justify-between gap-3">
|
|
1258
|
-
<p className="font-mono text-sm text-neutral-500">
|
|
1259
|
-
{auth.isGuest ? (
|
|
1260
|
+
<p className="font-mono text-sm text-neutral-500">{authStatus}</p>
|
|
1261
|
+
{!auth.isLoading && auth.isGuest ? (
|
|
1260
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" />
|
|
1261
|
-
) : (
|
|
1263
|
+
) : !auth.isLoading ? (
|
|
1262
1264
|
<button className="text-sm text-neutral-400 hover:text-white" type="button" onClick={() => signOut()}>
|
|
1263
1265
|
Sign out
|
|
1264
1266
|
</button>
|
|
1265
|
-
)}
|
|
1267
|
+
) : null}
|
|
1266
1268
|
</div>
|
|
1267
1269
|
<h1 className="mb-8 text-5xl font-bold tracking-tight">${title}</h1>
|
|
1268
1270
|
<form className="mb-8 flex gap-3" onSubmit={(event) => void onSubmit(event)}>
|
|
@@ -1307,18 +1309,18 @@ pnpm lakebed dev ${name}
|
|
|
1307
1309
|
}
|
|
1308
1310
|
|
|
1309
1311
|
async function newCommand(args) {
|
|
1310
|
-
const [
|
|
1312
|
+
const [nameArg] = positionals(args);
|
|
1311
1313
|
const template = readArg(args, "--template", "todo");
|
|
1312
1314
|
|
|
1313
|
-
if (!name) {
|
|
1314
|
-
usage();
|
|
1315
|
-
return;
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
1315
|
if (template !== "todo") {
|
|
1319
1316
|
throw new Error(`Unknown template: ${template}`);
|
|
1320
1317
|
}
|
|
1321
1318
|
|
|
1319
|
+
const name = nameArg ?? (await promptForCapsuleName());
|
|
1320
|
+
if (!name) {
|
|
1321
|
+
throw new Error("Capsule name is required.");
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1322
1324
|
const targetDir = resolveCapsuleDir(name);
|
|
1323
1325
|
if (existsSync(targetDir)) {
|
|
1324
1326
|
throw new Error(`Target already exists: ${targetDir}`);
|
|
@@ -1334,6 +1336,19 @@ async function newCommand(args) {
|
|
|
1334
1336
|
console.log(`Created Lakebed capsule at ${targetDir}`);
|
|
1335
1337
|
}
|
|
1336
1338
|
|
|
1339
|
+
async function promptForCapsuleName() {
|
|
1340
|
+
const rl = createInterface({
|
|
1341
|
+
input: process.stdin,
|
|
1342
|
+
output: process.stdout
|
|
1343
|
+
});
|
|
1344
|
+
|
|
1345
|
+
try {
|
|
1346
|
+
return (await rl.question("Capsule name: ")).trim();
|
|
1347
|
+
} finally {
|
|
1348
|
+
rl.close();
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1337
1352
|
async function runMany(args) {
|
|
1338
1353
|
const [capsuleArg] = positionals(args);
|
|
1339
1354
|
const capsuleDir = resolveCapsuleDir(capsuleArg);
|
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
|
+
}
|