lakebed 0.0.3 → 0.0.5
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 +2 -1
- package/package.json +7 -6
- package/src/anonymous-server.js +1 -1
- package/src/anonymous.js +254 -5
- package/src/cli.js +197 -8
- package/src/version.js +1 -0
package/README.md
CHANGED
|
@@ -93,6 +93,7 @@ lakebed new <name> [--template todo]
|
|
|
93
93
|
lakebed dev <capsule-dir> [--port 3000]
|
|
94
94
|
lakebed build <capsule-dir> --target anonymous [--out .lakebed/artifacts/app.json] [--json]
|
|
95
95
|
lakebed deploy [capsule-dir] [--ttl 7d] [--api <url>] [--json]
|
|
96
|
+
lakebed claim [capsule-dir] [--api <url>] [--json]
|
|
96
97
|
lakebed anonymous-server [--port 8787] [--public-root-url <url>] [--app-base-domain <domain>]
|
|
97
98
|
lakebed inspect <deploy-id-or-url> [--api <url>] [--json]
|
|
98
99
|
lakebed db list [deploy-id-or-url] [--port 3000]
|
|
@@ -143,7 +144,7 @@ LAKEBED_GITHUB_CLIENT_SECRET=...
|
|
|
143
144
|
LAKEBED_SESSION_SECRET=...
|
|
144
145
|
```
|
|
145
146
|
|
|
146
|
-
Claimed deploys are listed at `/deploys` on the deploy API origin. They keep the same resource limits as anonymous deploys.
|
|
147
|
+
Claimed deploys are listed at `/deploys` on the deploy API origin. They keep the same resource limits as anonymous deploys. Anonymous deploys cannot use outbound `fetch`; after a deploy is claimed, `lakebed deploy` can update it with a source-backed server artifact that supports async handlers and server-side fetch. If the first deploy already needs server-side `fetch`, `lakebed deploy` creates a claim-required preview, saves its claim metadata, and prints the claim URL. Open that URL, then run `lakebed deploy` again to publish the real source-backed app.
|
|
147
148
|
|
|
148
149
|
## Admin Dashboard
|
|
149
150
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lakebed",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Agent-native CLI and runtime for building and deploying Lakebed capsules.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"src/runtime.js",
|
|
30
30
|
"src/server.d.ts",
|
|
31
31
|
"src/server.js",
|
|
32
|
-
"src/source-store.js"
|
|
32
|
+
"src/source-store.js",
|
|
33
|
+
"src/version.js"
|
|
33
34
|
],
|
|
34
35
|
"exports": {
|
|
35
36
|
"./package.json": "./package.json",
|
|
@@ -48,6 +49,9 @@
|
|
|
48
49
|
"publishConfig": {
|
|
49
50
|
"access": "public"
|
|
50
51
|
},
|
|
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
|
+
},
|
|
51
55
|
"dependencies": {
|
|
52
56
|
"esbuild": "^0.27.1",
|
|
53
57
|
"pg": "^8.16.3",
|
|
@@ -56,8 +60,5 @@
|
|
|
56
60
|
},
|
|
57
61
|
"devDependencies": {
|
|
58
62
|
"@types/ws": "^8.18.1"
|
|
59
|
-
},
|
|
60
|
-
"scripts": {
|
|
61
|
-
"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"
|
|
62
63
|
}
|
|
63
|
-
}
|
|
64
|
+
}
|
package/src/anonymous-server.js
CHANGED
|
@@ -2794,7 +2794,7 @@ export async function startAnonymousServer({
|
|
|
2794
2794
|
}
|
|
2795
2795
|
|
|
2796
2796
|
const body = await readJsonBody(req);
|
|
2797
|
-
const payload = validateAnonymousDeployPayload(body);
|
|
2797
|
+
const payload = validateAnonymousDeployPayload(body, { allowClaimedSource: Boolean(currentDeploy.ownerId) });
|
|
2798
2798
|
const deploy = await resolvedStore.updateDeploy({
|
|
2799
2799
|
appBaseDomain: resolvedAppBaseDomain,
|
|
2800
2800
|
artifact: payload.artifact,
|
package/src/anonymous.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { LAKEBED_VERSION } from "./version.js";
|
|
3
4
|
|
|
4
5
|
export const ANONYMOUS_ARTIFACT_FORMAT = "lakebed.capsule.artifact.v1";
|
|
5
6
|
export const ANONYMOUS_ARTIFACT_MEDIA_TYPE = "application/vnd.lakebed.artifact+json";
|
|
7
|
+
export { LAKEBED_VERSION };
|
|
6
8
|
|
|
7
9
|
export const DEFAULT_ANONYMOUS_LIMITS = {
|
|
8
10
|
artifactBytes: 1024 * 1024,
|
|
@@ -17,6 +19,7 @@ export const DEFAULT_ANONYMOUS_LIMITS = {
|
|
|
17
19
|
|
|
18
20
|
const expressionOps = new Set(["arg", "auth", "call", "row"]);
|
|
19
21
|
const authFields = new Set(["displayName", "email", "emailVerified", "isAuthenticated", "isGuest", "name", "picture", "provider", "userId"]);
|
|
22
|
+
const sourceModuleCache = new Map();
|
|
20
23
|
|
|
21
24
|
export class AnonymousCompilerError extends Error {
|
|
22
25
|
constructor(diagnostics) {
|
|
@@ -493,7 +496,7 @@ function compileServerToIr(app, schema) {
|
|
|
493
496
|
return { diagnostics, mutations, queries };
|
|
494
497
|
}
|
|
495
498
|
|
|
496
|
-
export async function createAnonymousArtifact({ app, clientOut, sourceStore, version =
|
|
499
|
+
export async function createAnonymousArtifact({ app, clientOut, sourceStore, version = LAKEBED_VERSION }) {
|
|
497
500
|
const sourceFiles = await readSourceFiles(sourceStore);
|
|
498
501
|
const diagnostics = forbiddenSourceDiagnostics(sourceFiles);
|
|
499
502
|
const { diagnostics: schemaDiagnostics, schema } = serializeSchema(app.schema);
|
|
@@ -555,6 +558,77 @@ export async function createAnonymousArtifact({ app, clientOut, sourceStore, ver
|
|
|
555
558
|
};
|
|
556
559
|
}
|
|
557
560
|
|
|
561
|
+
export async function createClaimedArtifact({ app, clientOut, serverOut, sourceStore, version = LAKEBED_VERSION }) {
|
|
562
|
+
if (!serverOut) {
|
|
563
|
+
throw new AnonymousCompilerError([diagnostic("server/index.ts", "Claimed deploys require a bundled server module.")]);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const sourceFiles = await readSourceFiles(sourceStore);
|
|
567
|
+
const diagnostics = forbiddenSourceDiagnostics(sourceFiles).filter(
|
|
568
|
+
(entry) =>
|
|
569
|
+
entry.message !== "Outbound fetch is disabled for anonymous deploys." &&
|
|
570
|
+
entry.message !== "Async server handlers are not part of the anonymous IR yet. Use synchronous Lakebed database operations."
|
|
571
|
+
);
|
|
572
|
+
const { diagnostics: schemaDiagnostics, schema } = serializeSchema(app.schema);
|
|
573
|
+
diagnostics.push(...schemaDiagnostics);
|
|
574
|
+
if (diagnostics.length > 0) {
|
|
575
|
+
throw new AnonymousCompilerError(diagnostics);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const clientBundle = await readFile(clientOut);
|
|
579
|
+
const serverBundle = await readFile(serverOut);
|
|
580
|
+
const clientBundleBase64 = clientBundle.toString("base64");
|
|
581
|
+
const clientBundleHash = sha256(clientBundle);
|
|
582
|
+
const serverBundleBase64 = serverBundle.toString("base64");
|
|
583
|
+
const serverBundleHash = sha256(serverBundle);
|
|
584
|
+
const sourceManifest = sourceFiles.map(({ bytes, hash, path }) => ({ bytes, hash, path }));
|
|
585
|
+
const sourceSnapshotHash = sha256(stableStringify(sourceManifest));
|
|
586
|
+
const artifact = {
|
|
587
|
+
name: app.name ?? "Lakebed Capsule",
|
|
588
|
+
client: {
|
|
589
|
+
bundleHash: clientBundleHash,
|
|
590
|
+
bytes: clientBundle.byteLength,
|
|
591
|
+
entry: "/client.js"
|
|
592
|
+
},
|
|
593
|
+
createdWith: {
|
|
594
|
+
compiler: "0.1.0",
|
|
595
|
+
lakebed: version
|
|
596
|
+
},
|
|
597
|
+
deployTarget: "claimed-source",
|
|
598
|
+
format: ANONYMOUS_ARTIFACT_FORMAT,
|
|
599
|
+
limits: {
|
|
600
|
+
instructionBudget: DEFAULT_ANONYMOUS_LIMITS.instructionBudget,
|
|
601
|
+
maxRowsReturned: DEFAULT_ANONYMOUS_LIMITS.rowsReturned,
|
|
602
|
+
maxValueBytes: DEFAULT_ANONYMOUS_LIMITS.maxValueBytes
|
|
603
|
+
},
|
|
604
|
+
server: {
|
|
605
|
+
helpers: {},
|
|
606
|
+
imports: ["lakebed/server"],
|
|
607
|
+
mutations: Object.fromEntries(Object.keys(app.mutations ?? {}).map((name) => [name, { op: "source" }])),
|
|
608
|
+
queries: Object.fromEntries(Object.keys(app.queries ?? {}).map((name) => [name, { op: "source" }])),
|
|
609
|
+
schema,
|
|
610
|
+
source: {
|
|
611
|
+
bytes: serverBundle.byteLength,
|
|
612
|
+
bundle: serverBundleBase64,
|
|
613
|
+
bundleHash: serverBundleHash,
|
|
614
|
+
entry: "/server.mjs"
|
|
615
|
+
}
|
|
616
|
+
},
|
|
617
|
+
source: {
|
|
618
|
+
files: sourceManifest,
|
|
619
|
+
snapshotHash: sourceSnapshotHash
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
const artifactHash = sha256(stableStringify(artifact));
|
|
623
|
+
|
|
624
|
+
return {
|
|
625
|
+
artifact,
|
|
626
|
+
artifactHash,
|
|
627
|
+
clientBundle: clientBundleBase64,
|
|
628
|
+
clientBundleHash
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
|
|
558
632
|
function validateExpression(expr, path, diagnostics) {
|
|
559
633
|
if (!isExpression(expr)) {
|
|
560
634
|
diagnostics.push(diagnostic(path, "Expected an anonymous IR expression."));
|
|
@@ -614,6 +688,10 @@ function validateValue(value, path, diagnostics) {
|
|
|
614
688
|
}
|
|
615
689
|
|
|
616
690
|
function validateQuery(query, path, schema, diagnostics) {
|
|
691
|
+
if (query?.op === "source") {
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
|
|
617
695
|
if (!isPlainObject(query) || query.op !== "table.all" || !schema[query.table]) {
|
|
618
696
|
diagnostics.push(diagnostic(path, "Invalid anonymous query IR."));
|
|
619
697
|
return;
|
|
@@ -636,7 +714,7 @@ function validateQuery(query, path, schema, diagnostics) {
|
|
|
636
714
|
}
|
|
637
715
|
}
|
|
638
716
|
|
|
639
|
-
export function validateAnonymousArtifact(artifact) {
|
|
717
|
+
export function validateAnonymousArtifact(artifact, { allowClaimedSource = false } = {}) {
|
|
640
718
|
const diagnostics = [];
|
|
641
719
|
|
|
642
720
|
if (!isPlainObject(artifact)) {
|
|
@@ -647,7 +725,7 @@ export function validateAnonymousArtifact(artifact) {
|
|
|
647
725
|
diagnostics.push(diagnostic("artifact.format", `Expected ${ANONYMOUS_ARTIFACT_FORMAT}.`));
|
|
648
726
|
}
|
|
649
727
|
|
|
650
|
-
if (artifact.deployTarget !== "anonymous-interpreter") {
|
|
728
|
+
if (artifact.deployTarget !== "anonymous-interpreter" && !(allowClaimedSource && artifact.deployTarget === "claimed-source")) {
|
|
651
729
|
diagnostics.push(diagnostic("artifact.deployTarget", "Anonymous deploys require deployTarget anonymous-interpreter."));
|
|
652
730
|
}
|
|
653
731
|
|
|
@@ -669,9 +747,49 @@ export function validateAnonymousArtifact(artifact) {
|
|
|
669
747
|
|
|
670
748
|
for (const [name, query] of Object.entries(artifact.server?.queries ?? {})) {
|
|
671
749
|
validateQuery(query, `artifact.server.queries.${name}`, schema ?? {}, diagnostics);
|
|
750
|
+
if (query?.op === "source" && artifact.server?.source === undefined) {
|
|
751
|
+
diagnostics.push(diagnostic(`artifact.server.queries.${name}`, "Source query requires artifact.server.source."));
|
|
752
|
+
}
|
|
753
|
+
if (query?.op === "source" && artifact.deployTarget !== "claimed-source") {
|
|
754
|
+
diagnostics.push(diagnostic(`artifact.server.queries.${name}`, "Source query requires deployTarget claimed-source."));
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (artifact.server?.source !== undefined) {
|
|
759
|
+
if (artifact.deployTarget !== "claimed-source") {
|
|
760
|
+
diagnostics.push(diagnostic("artifact.server.source", "Server source bundles require deployTarget claimed-source."));
|
|
761
|
+
}
|
|
762
|
+
const source = artifact.server.source;
|
|
763
|
+
if (
|
|
764
|
+
!isPlainObject(source) ||
|
|
765
|
+
typeof source.bundle !== "string" ||
|
|
766
|
+
typeof source.bundleHash !== "string" ||
|
|
767
|
+
!Number.isInteger(source.bytes) ||
|
|
768
|
+
source.bytes <= 0
|
|
769
|
+
) {
|
|
770
|
+
diagnostics.push(diagnostic("artifact.server.source", "Invalid anonymous server source bundle."));
|
|
771
|
+
} else {
|
|
772
|
+
const bundle = Buffer.from(source.bundle, "base64");
|
|
773
|
+
if (bundle.byteLength !== source.bytes) {
|
|
774
|
+
diagnostics.push(diagnostic("artifact.server.source.bytes", "Server bundle byte count does not match artifact.server.source.bytes."));
|
|
775
|
+
}
|
|
776
|
+
if (sha256(bundle) !== source.bundleHash) {
|
|
777
|
+
diagnostics.push(diagnostic("artifact.server.source.bundleHash", "Server bundle hash does not match artifact.server.source.bundleHash."));
|
|
778
|
+
}
|
|
779
|
+
}
|
|
672
780
|
}
|
|
673
781
|
|
|
674
782
|
for (const [name, mutation] of Object.entries(artifact.server?.mutations ?? {})) {
|
|
783
|
+
if (mutation?.op === "source") {
|
|
784
|
+
if (artifact.server?.source === undefined) {
|
|
785
|
+
diagnostics.push(diagnostic(`artifact.server.mutations.${name}`, "Source mutation requires artifact.server.source."));
|
|
786
|
+
}
|
|
787
|
+
if (artifact.deployTarget !== "claimed-source") {
|
|
788
|
+
diagnostics.push(diagnostic(`artifact.server.mutations.${name}`, "Source mutation requires deployTarget claimed-source."));
|
|
789
|
+
}
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
|
|
675
793
|
if (mutation?.op !== "mutation" || !Array.isArray(mutation.body)) {
|
|
676
794
|
diagnostics.push(diagnostic(`artifact.server.mutations.${name}`, "Invalid mutation IR."));
|
|
677
795
|
continue;
|
|
@@ -704,12 +822,12 @@ export function validateAnonymousArtifact(artifact) {
|
|
|
704
822
|
return diagnostics;
|
|
705
823
|
}
|
|
706
824
|
|
|
707
|
-
export function validateAnonymousDeployPayload(payload) {
|
|
825
|
+
export function validateAnonymousDeployPayload(payload, options = {}) {
|
|
708
826
|
if (!payload || typeof payload !== "object") {
|
|
709
827
|
throw new Error("Deploy payload must be a JSON object.");
|
|
710
828
|
}
|
|
711
829
|
|
|
712
|
-
const diagnostics = validateAnonymousArtifact(payload.artifact);
|
|
830
|
+
const diagnostics = validateAnonymousArtifact(payload.artifact, options);
|
|
713
831
|
if (diagnostics.length > 0) {
|
|
714
832
|
throw new AnonymousCompilerError(diagnostics);
|
|
715
833
|
}
|
|
@@ -939,7 +1057,124 @@ async function executeQuerySpec({ args = [], artifact, auth, query, state, deplo
|
|
|
939
1057
|
return results.slice(0, limit);
|
|
940
1058
|
}
|
|
941
1059
|
|
|
1060
|
+
async function loadSourceApp(artifact) {
|
|
1061
|
+
const source = artifact.server?.source;
|
|
1062
|
+
if (!source) {
|
|
1063
|
+
return null;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
if (!sourceModuleCache.has(source.bundleHash)) {
|
|
1067
|
+
const url = `data:text/javascript;base64,${source.bundle}`;
|
|
1068
|
+
sourceModuleCache.set(source.bundleHash, import(url).then((module) => module.default));
|
|
1069
|
+
}
|
|
1070
|
+
return sourceModuleCache.get(source.bundleHash);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function createSourceQuery(rows, tableName) {
|
|
1074
|
+
return {
|
|
1075
|
+
filters: [],
|
|
1076
|
+
limitValue: null,
|
|
1077
|
+
orderByValue: null,
|
|
1078
|
+
tableName,
|
|
1079
|
+
where(field, value) {
|
|
1080
|
+
return { ...this, filters: [...this.filters, { field, value }] };
|
|
1081
|
+
},
|
|
1082
|
+
orderBy(field, direction = "asc") {
|
|
1083
|
+
return { ...this, orderByValue: { field, direction: direction === "desc" ? "desc" : "asc" } };
|
|
1084
|
+
},
|
|
1085
|
+
limit(count) {
|
|
1086
|
+
return { ...this, limitValue: Number(count) };
|
|
1087
|
+
},
|
|
1088
|
+
all() {
|
|
1089
|
+
let results = [...rows[this.tableName]].map((row) => ({ ...row }));
|
|
1090
|
+
for (const filter of this.filters) {
|
|
1091
|
+
results = results.filter((row) => row[filter.field] === filter.value);
|
|
1092
|
+
}
|
|
1093
|
+
if (this.orderByValue) {
|
|
1094
|
+
const direction = this.orderByValue.direction === "desc" ? -1 : 1;
|
|
1095
|
+
results.sort((left, right) => compareValues(left[this.orderByValue.field], right[this.orderByValue.field]) * direction);
|
|
1096
|
+
}
|
|
1097
|
+
return results.slice(0, this.limitValue ?? results.length);
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
async function createSourceContext({ artifact, auth, deployId, state }) {
|
|
1103
|
+
const rows = {};
|
|
1104
|
+
const operations = [];
|
|
1105
|
+
for (const tableName of Object.keys(artifact.server.schema ?? {})) {
|
|
1106
|
+
rows[tableName] = await state.listRows(deployId, tableName);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
const db = {};
|
|
1110
|
+
for (const tableName of Object.keys(artifact.server.schema ?? {})) {
|
|
1111
|
+
db[tableName] = {
|
|
1112
|
+
...createSourceQuery(rows, tableName),
|
|
1113
|
+
get(id) {
|
|
1114
|
+
return rows[tableName].find((row) => row.id === id) ?? null;
|
|
1115
|
+
},
|
|
1116
|
+
insert(value) {
|
|
1117
|
+
const row = prepareInsert(artifact.server.schema, tableName, value);
|
|
1118
|
+
rows[tableName].push(row);
|
|
1119
|
+
operations.push({ op: "insert", row, table: tableName });
|
|
1120
|
+
return { ...row };
|
|
1121
|
+
},
|
|
1122
|
+
update(id, patch) {
|
|
1123
|
+
const index = rows[tableName].findIndex((row) => row.id === id);
|
|
1124
|
+
if (index === -1) {
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
const cleanPatch = preparePatch(artifact.server.schema, tableName, patch);
|
|
1128
|
+
rows[tableName][index] = { ...rows[tableName][index], ...cleanPatch };
|
|
1129
|
+
operations.push({ id, op: "update", patch: cleanPatch, table: tableName });
|
|
1130
|
+
},
|
|
1131
|
+
delete(id) {
|
|
1132
|
+
const index = rows[tableName].findIndex((row) => row.id === id);
|
|
1133
|
+
if (index === -1) {
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
rows[tableName].splice(index, 1);
|
|
1137
|
+
operations.push({ id, op: "delete", table: tableName });
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
return {
|
|
1143
|
+
ctx: {
|
|
1144
|
+
auth,
|
|
1145
|
+
db,
|
|
1146
|
+
env: {},
|
|
1147
|
+
log: {
|
|
1148
|
+
error() {},
|
|
1149
|
+
info() {},
|
|
1150
|
+
warn() {}
|
|
1151
|
+
}
|
|
1152
|
+
},
|
|
1153
|
+
async flush(tx) {
|
|
1154
|
+
for (const operation of operations) {
|
|
1155
|
+
if (operation.op === "insert") {
|
|
1156
|
+
await tx.insertRow(deployId, operation.table, operation.row);
|
|
1157
|
+
} else if (operation.op === "update") {
|
|
1158
|
+
await tx.updateRow(deployId, operation.table, operation.id, operation.patch);
|
|
1159
|
+
} else if (operation.op === "delete") {
|
|
1160
|
+
await tx.deleteRow(deployId, operation.table, operation.id);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
|
|
942
1167
|
export async function executeAnonymousQuery({ args = [], artifact, auth, deployId, name, state }) {
|
|
1168
|
+
const sourceApp = await loadSourceApp(artifact);
|
|
1169
|
+
if (sourceApp) {
|
|
1170
|
+
const handler = sourceApp.queries?.[name];
|
|
1171
|
+
if (!handler) {
|
|
1172
|
+
throw new Error(`Unknown query: ${name}`);
|
|
1173
|
+
}
|
|
1174
|
+
const { ctx } = await createSourceContext({ artifact, auth, deployId, state });
|
|
1175
|
+
return handler(ctx, ...args);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
943
1178
|
const query = artifact.server.queries?.[name];
|
|
944
1179
|
if (!query) {
|
|
945
1180
|
throw new Error(`Unknown query: ${name}`);
|
|
@@ -958,6 +1193,20 @@ async function checkUpdateGuards({ auth, guards = [], row }) {
|
|
|
958
1193
|
}
|
|
959
1194
|
|
|
960
1195
|
export async function executeAnonymousMutation({ args = [], artifact, auth, deployId, name, state }) {
|
|
1196
|
+
const sourceApp = await loadSourceApp(artifact);
|
|
1197
|
+
if (sourceApp) {
|
|
1198
|
+
const handler = sourceApp.mutations?.[name];
|
|
1199
|
+
if (!handler) {
|
|
1200
|
+
throw new Error(`Unknown mutation: ${name}`);
|
|
1201
|
+
}
|
|
1202
|
+
return state.transaction(deployId, async (tx) => {
|
|
1203
|
+
const source = await createSourceContext({ artifact, auth, deployId, state: tx });
|
|
1204
|
+
const result = await handler(source.ctx, ...args);
|
|
1205
|
+
await source.flush(tx);
|
|
1206
|
+
return result ?? null;
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
|
|
961
1210
|
const mutation = artifact.server.mutations?.[name];
|
|
962
1211
|
if (!mutation) {
|
|
963
1212
|
throw new Error(`Unknown mutation: ${name}`);
|
package/src/cli.js
CHANGED
|
@@ -9,14 +9,16 @@ import { WebSocketServer } from "ws";
|
|
|
9
9
|
import {
|
|
10
10
|
ANONYMOUS_ARTIFACT_MEDIA_TYPE,
|
|
11
11
|
AnonymousCompilerError,
|
|
12
|
+
LAKEBED_VERSION,
|
|
12
13
|
createAnonymousArtifact,
|
|
14
|
+
createClaimedArtifact,
|
|
13
15
|
parseTtlSeconds,
|
|
14
16
|
stableStringify
|
|
15
17
|
} from "./anonymous.js";
|
|
16
18
|
import { startAnonymousServer } from "./anonymous-server.js";
|
|
17
19
|
import { authFromUrl as resolveAuthFromUrl, createGuestAuth, requestOrigin, shooBaseUrlFromEnv } from "./auth.js";
|
|
18
20
|
import { LogBuffer, StateCell } from "./runtime.js";
|
|
19
|
-
import { createMemorySourceStoreFromDirectory, sourcePathDirname, sourcePathJoin } from "./source-store.js";
|
|
21
|
+
import { MemorySourceStore, createMemorySourceStoreFromDirectory, sourcePathDirname, sourcePathJoin } from "./source-store.js";
|
|
20
22
|
|
|
21
23
|
const root = process.cwd();
|
|
22
24
|
const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -32,6 +34,7 @@ Usage:
|
|
|
32
34
|
lakebed dev <capsule-dir> [--port 3000]
|
|
33
35
|
lakebed build <capsule-dir> --target anonymous [--out .lakebed/artifacts/app.json] [--json]
|
|
34
36
|
lakebed deploy [capsule-dir] [--ttl 7d] [--api <url>] [--json]
|
|
37
|
+
lakebed claim [capsule-dir] [--api <url>] [--json]
|
|
35
38
|
lakebed anonymous-server [--port 8787] [--public-root-url <url>] [--app-base-domain <domain>]
|
|
36
39
|
lakebed inspect <deploy-id-or-url> [--api <url>] [--json]
|
|
37
40
|
lakebed run-many <capsule-dir> [--count 20] [--base-port 4000]
|
|
@@ -299,6 +302,7 @@ export async function buildCapsule({ capsuleDir, sourceStore, capsuleId = "dev"
|
|
|
299
302
|
buildDir,
|
|
300
303
|
clientOut,
|
|
301
304
|
env: await readServerEnv(workingStore),
|
|
305
|
+
serverOut,
|
|
302
306
|
sourceStore: workingStore
|
|
303
307
|
};
|
|
304
308
|
}
|
|
@@ -598,6 +602,95 @@ async function buildAnonymousEnvelope(capsuleArg) {
|
|
|
598
602
|
};
|
|
599
603
|
}
|
|
600
604
|
|
|
605
|
+
const claimRequiredDiagnosticMessages = new Set([
|
|
606
|
+
"Outbound fetch is disabled for anonymous deploys.",
|
|
607
|
+
"Async server handlers are not part of the anonymous IR yet. Use synchronous Lakebed database operations."
|
|
608
|
+
]);
|
|
609
|
+
|
|
610
|
+
function canDeployAfterClaim(error) {
|
|
611
|
+
return (
|
|
612
|
+
error instanceof AnonymousCompilerError &&
|
|
613
|
+
error.diagnostics.length > 0 &&
|
|
614
|
+
error.diagnostics.every((entry) => claimRequiredDiagnosticMessages.has(entry.message))
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
async function buildClaimRequiredEnvelope({ capsuleDir }) {
|
|
619
|
+
const sourceStore = new MemorySourceStore();
|
|
620
|
+
await sourceStore.writeFile(
|
|
621
|
+
"server/index.ts",
|
|
622
|
+
`import { capsule } from "lakebed/server";
|
|
623
|
+
|
|
624
|
+
export default capsule({
|
|
625
|
+
name: "Claim Required",
|
|
626
|
+
schema: {},
|
|
627
|
+
queries: {},
|
|
628
|
+
mutations: {}
|
|
629
|
+
});
|
|
630
|
+
`
|
|
631
|
+
);
|
|
632
|
+
await sourceStore.writeFile(
|
|
633
|
+
"client/index.tsx",
|
|
634
|
+
`export function App() {
|
|
635
|
+
return (
|
|
636
|
+
<main className="min-h-screen bg-neutral-950 px-6 py-12 text-neutral-100">
|
|
637
|
+
<section className="mx-auto max-w-2xl rounded-lg border border-neutral-800 bg-neutral-900 p-8 shadow-2xl">
|
|
638
|
+
<p className="text-sm font-semibold uppercase tracking-wide text-cyan-300">Lakebed deploy</p>
|
|
639
|
+
<h1 className="mt-3 text-3xl font-semibold">Claim required</h1>
|
|
640
|
+
<p className="mt-4 text-neutral-300">
|
|
641
|
+
This capsule uses server-side fetch. Claim this deploy, then run lakebed deploy again to publish the app.
|
|
642
|
+
</p>
|
|
643
|
+
</section>
|
|
644
|
+
</main>
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
`
|
|
648
|
+
);
|
|
649
|
+
const built = await buildCapsule({
|
|
650
|
+
capsuleDir,
|
|
651
|
+
capsuleId: `claim-required-${Date.now()}`,
|
|
652
|
+
sourceStore
|
|
653
|
+
});
|
|
654
|
+
const artifact = await createAnonymousArtifact({
|
|
655
|
+
app: built.app,
|
|
656
|
+
clientOut: built.clientOut,
|
|
657
|
+
sourceStore
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
return {
|
|
661
|
+
artifact: artifact.artifact,
|
|
662
|
+
artifactHash: artifact.artifactHash,
|
|
663
|
+
clientBundle: artifact.clientBundle,
|
|
664
|
+
clientBundleHash: artifact.clientBundleHash,
|
|
665
|
+
claimRequired: true,
|
|
666
|
+
mediaType: ANONYMOUS_ARTIFACT_MEDIA_TYPE
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
async function buildClaimedEnvelope(capsuleArg) {
|
|
671
|
+
const capsuleDir = resolveCapsuleDir(capsuleArg);
|
|
672
|
+
const sourceStore = await createMemorySourceStoreFromDirectory(capsuleDir);
|
|
673
|
+
const built = await buildCapsule({
|
|
674
|
+
capsuleDir,
|
|
675
|
+
capsuleId: `claimed-${Date.now()}`,
|
|
676
|
+
sourceStore
|
|
677
|
+
});
|
|
678
|
+
const artifact = await createClaimedArtifact({
|
|
679
|
+
app: built.app,
|
|
680
|
+
clientOut: built.clientOut,
|
|
681
|
+
serverOut: built.serverOut,
|
|
682
|
+
sourceStore
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
return {
|
|
686
|
+
artifact: artifact.artifact,
|
|
687
|
+
artifactHash: artifact.artifactHash,
|
|
688
|
+
clientBundle: artifact.clientBundle,
|
|
689
|
+
clientBundleHash: artifact.clientBundleHash,
|
|
690
|
+
mediaType: ANONYMOUS_ARTIFACT_MEDIA_TYPE
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
601
694
|
function defaultArtifactPath(capsuleDir) {
|
|
602
695
|
return resolve(root, ".lakebed/artifacts", `${basename(capsuleDir)}.anonymous.json`);
|
|
603
696
|
}
|
|
@@ -642,11 +735,19 @@ function claimTokenFromDeployResponse(deployed) {
|
|
|
642
735
|
return null;
|
|
643
736
|
}
|
|
644
737
|
|
|
738
|
+
function claimUrlFromDeployMetadata(metadata) {
|
|
739
|
+
if (!metadata?.api || !metadata?.deployId || !metadata?.claimToken) {
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
return `${String(metadata.api).replace(/\/+$/g, "")}/claim/${encodeURIComponent(metadata.deployId)}/${encodeURIComponent(metadata.claimToken)}`;
|
|
744
|
+
}
|
|
745
|
+
|
|
645
746
|
function deployRequestBody(envelope, ttl) {
|
|
646
747
|
return JSON.stringify({
|
|
647
748
|
artifact: envelope.artifact,
|
|
648
749
|
clientBundle: envelope.clientBundle,
|
|
649
|
-
clientVersion:
|
|
750
|
+
clientVersion: LAKEBED_VERSION,
|
|
650
751
|
requestedTtlSeconds: ttl
|
|
651
752
|
});
|
|
652
753
|
}
|
|
@@ -700,13 +801,35 @@ async function readResponseJson(response) {
|
|
|
700
801
|
async function deployCommand(args) {
|
|
701
802
|
const [capsuleArg] = positionals(args);
|
|
702
803
|
const capsuleDir = capsuleArg ? resolveCapsuleDir(capsuleArg) : root;
|
|
703
|
-
const envelope = await buildAnonymousEnvelope(capsuleDir);
|
|
704
804
|
const ttl = parseTtlSeconds(readArg(args, "--ttl", "7d"));
|
|
705
805
|
const api = deployApiUrl(args);
|
|
706
|
-
const body = deployRequestBody(envelope, ttl);
|
|
707
806
|
const metadata = await readDeployMetadata(capsuleDir);
|
|
708
807
|
const canUpdate =
|
|
709
808
|
metadata?.api === api && typeof metadata?.deployId === "string" && typeof metadata?.claimToken === "string";
|
|
809
|
+
let currentDeploy = null;
|
|
810
|
+
if (canUpdate) {
|
|
811
|
+
const currentResponse = await fetch(`${api}/v1/deploys/${encodeURIComponent(metadata.deployId)}`);
|
|
812
|
+
if (currentResponse.ok) {
|
|
813
|
+
currentDeploy = await currentResponse.json();
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
let envelope;
|
|
818
|
+
try {
|
|
819
|
+
envelope = currentDeploy?.claimed ? await buildClaimedEnvelope(capsuleDir) : await buildAnonymousEnvelope(capsuleDir);
|
|
820
|
+
} catch (error) {
|
|
821
|
+
if (error instanceof AnonymousCompilerError && canUpdate && currentDeploy && !currentDeploy.claimed) {
|
|
822
|
+
throw new Error(
|
|
823
|
+
`${error.message}\n\nThis deploy is still anonymous. Claim it first, then run lakebed deploy again to use server-side fetch.`
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
if ((!canUpdate || !currentDeploy) && canDeployAfterClaim(error)) {
|
|
827
|
+
envelope = await buildClaimRequiredEnvelope({ capsuleDir });
|
|
828
|
+
} else {
|
|
829
|
+
throw error;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
const body = deployRequestBody(envelope, ttl);
|
|
710
833
|
let mode = "created";
|
|
711
834
|
let response;
|
|
712
835
|
|
|
@@ -748,11 +871,15 @@ async function deployCommand(args) {
|
|
|
748
871
|
}
|
|
749
872
|
|
|
750
873
|
if (hasFlag(args, "--json")) {
|
|
751
|
-
console.log(JSON.stringify(deployed, null, 2));
|
|
874
|
+
console.log(JSON.stringify(envelope.claimRequired ? { ...deployed, claimRequired: true } : deployed, null, 2));
|
|
752
875
|
return;
|
|
753
876
|
}
|
|
754
877
|
|
|
755
|
-
|
|
878
|
+
if (envelope.claimRequired && mode !== "updated") {
|
|
879
|
+
console.log("Created claim-required preview.\n");
|
|
880
|
+
} else {
|
|
881
|
+
console.log(`${mode === "updated" ? "Updated" : "Created"} anonymous preview.\n`);
|
|
882
|
+
}
|
|
756
883
|
console.log(`App: ${deployed.url}`);
|
|
757
884
|
console.log(`Expires: ${deployed.expiresAt}`);
|
|
758
885
|
if (deployed.claimUrl) {
|
|
@@ -764,7 +891,64 @@ async function deployCommand(args) {
|
|
|
764
891
|
console.log(` state: ${deployed.limits.stateBytes} bytes`);
|
|
765
892
|
console.log(` requests: ${deployed.limits.requestsPerDay} / day`);
|
|
766
893
|
console.log(` mutations: ${deployed.limits.mutationsPerDay} / day`);
|
|
767
|
-
console.log(
|
|
894
|
+
console.log(` outbound fetch: ${envelope.artifact.deployTarget === "claimed-source" ? "enabled" : "disabled"}`);
|
|
895
|
+
if (envelope.claimRequired) {
|
|
896
|
+
console.log("\nThis app needs a claimed deploy before server-side fetch can run.");
|
|
897
|
+
console.log("Open the claim URL, then run lakebed deploy again.");
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
async function claimCommand(args) {
|
|
902
|
+
const [capsuleArg] = positionals(args);
|
|
903
|
+
const capsuleDir = capsuleArg ? resolveCapsuleDir(capsuleArg) : root;
|
|
904
|
+
const api = deployApiUrl(args);
|
|
905
|
+
const metadata = await readDeployMetadata(capsuleDir);
|
|
906
|
+
|
|
907
|
+
if (!metadata) {
|
|
908
|
+
throw new Error(`No Lakebed deploy metadata found at ${deployMetadataPath(capsuleDir)}. Run lakebed deploy from this project first.`);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
if (metadata.api !== api) {
|
|
912
|
+
throw new Error(`Saved deploy metadata is for ${metadata.api}, but this command is using ${api}. Pass --api ${metadata.api} to claim it.`);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const claimUrl = claimUrlFromDeployMetadata(metadata);
|
|
916
|
+
if (!claimUrl) {
|
|
917
|
+
throw new Error("This project does not have a saved claim token. Redeploy to create a new claim URL.");
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
let deploy = null;
|
|
921
|
+
try {
|
|
922
|
+
const response = await fetch(`${api}/v1/deploys/${encodeURIComponent(metadata.deployId)}`);
|
|
923
|
+
if (response.ok) {
|
|
924
|
+
deploy = await response.json();
|
|
925
|
+
}
|
|
926
|
+
} catch {
|
|
927
|
+
deploy = null;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const result = {
|
|
931
|
+
claimed: Boolean(deploy?.claimed),
|
|
932
|
+
claimUrl,
|
|
933
|
+
deployId: metadata.deployId,
|
|
934
|
+
url: deploy?.url ?? metadata.url
|
|
935
|
+
};
|
|
936
|
+
|
|
937
|
+
if (hasFlag(args, "--json")) {
|
|
938
|
+
console.log(JSON.stringify(result, null, 2));
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
if (result.claimed) {
|
|
943
|
+
console.log(`Deploy ${result.deployId} is already claimed.`);
|
|
944
|
+
if (result.url) {
|
|
945
|
+
console.log(`App: ${result.url}`);
|
|
946
|
+
}
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
console.log("Open this URL to claim the current project's deploy:");
|
|
951
|
+
console.log(claimUrl);
|
|
768
952
|
}
|
|
769
953
|
|
|
770
954
|
async function anonymousServerCommand(args) {
|
|
@@ -931,7 +1115,7 @@ lakebed logs --port 3000
|
|
|
931
1115
|
- One client entry.
|
|
932
1116
|
- Guest auth locally, with built-in Google sign-in through Shoo.
|
|
933
1117
|
- No file storage.
|
|
934
|
-
- No outbound fetch in anonymous deploys.
|
|
1118
|
+
- No outbound fetch in anonymous deploys. Claim the deploy before using server-side fetch.
|
|
935
1119
|
- Local state resets when \`lakebed dev\` restarts.
|
|
936
1120
|
`;
|
|
937
1121
|
}
|
|
@@ -1146,6 +1330,11 @@ async function main() {
|
|
|
1146
1330
|
return;
|
|
1147
1331
|
}
|
|
1148
1332
|
|
|
1333
|
+
if (command === "claim") {
|
|
1334
|
+
await claimCommand(args);
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1149
1338
|
if (command === "anonymous-server") {
|
|
1150
1339
|
await anonymousServerCommand(args);
|
|
1151
1340
|
return;
|
package/src/version.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const LAKEBED_VERSION = "0.0.5";
|