lakebed 0.0.3 → 0.0.4
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 +1 -1
- package/src/anonymous-server.js +1 -1
- package/src/anonymous.js +251 -4
- package/src/cli.js +115 -4
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.
|
|
147
148
|
|
|
148
149
|
## Admin Dashboard
|
|
149
150
|
|
package/package.json
CHANGED
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
|
@@ -17,6 +17,7 @@ export const DEFAULT_ANONYMOUS_LIMITS = {
|
|
|
17
17
|
|
|
18
18
|
const expressionOps = new Set(["arg", "auth", "call", "row"]);
|
|
19
19
|
const authFields = new Set(["displayName", "email", "emailVerified", "isAuthenticated", "isGuest", "name", "picture", "provider", "userId"]);
|
|
20
|
+
const sourceModuleCache = new Map();
|
|
20
21
|
|
|
21
22
|
export class AnonymousCompilerError extends Error {
|
|
22
23
|
constructor(diagnostics) {
|
|
@@ -555,6 +556,77 @@ export async function createAnonymousArtifact({ app, clientOut, sourceStore, ver
|
|
|
555
556
|
};
|
|
556
557
|
}
|
|
557
558
|
|
|
559
|
+
export async function createClaimedArtifact({ app, clientOut, serverOut, sourceStore, version = "0.0.3" }) {
|
|
560
|
+
if (!serverOut) {
|
|
561
|
+
throw new AnonymousCompilerError([diagnostic("server/index.ts", "Claimed deploys require a bundled server module.")]);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const sourceFiles = await readSourceFiles(sourceStore);
|
|
565
|
+
const diagnostics = forbiddenSourceDiagnostics(sourceFiles).filter(
|
|
566
|
+
(entry) =>
|
|
567
|
+
entry.message !== "Outbound fetch is disabled for anonymous deploys." &&
|
|
568
|
+
entry.message !== "Async server handlers are not part of the anonymous IR yet. Use synchronous Lakebed database operations."
|
|
569
|
+
);
|
|
570
|
+
const { diagnostics: schemaDiagnostics, schema } = serializeSchema(app.schema);
|
|
571
|
+
diagnostics.push(...schemaDiagnostics);
|
|
572
|
+
if (diagnostics.length > 0) {
|
|
573
|
+
throw new AnonymousCompilerError(diagnostics);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const clientBundle = await readFile(clientOut);
|
|
577
|
+
const serverBundle = await readFile(serverOut);
|
|
578
|
+
const clientBundleBase64 = clientBundle.toString("base64");
|
|
579
|
+
const clientBundleHash = sha256(clientBundle);
|
|
580
|
+
const serverBundleBase64 = serverBundle.toString("base64");
|
|
581
|
+
const serverBundleHash = sha256(serverBundle);
|
|
582
|
+
const sourceManifest = sourceFiles.map(({ bytes, hash, path }) => ({ bytes, hash, path }));
|
|
583
|
+
const sourceSnapshotHash = sha256(stableStringify(sourceManifest));
|
|
584
|
+
const artifact = {
|
|
585
|
+
name: app.name ?? "Lakebed Capsule",
|
|
586
|
+
client: {
|
|
587
|
+
bundleHash: clientBundleHash,
|
|
588
|
+
bytes: clientBundle.byteLength,
|
|
589
|
+
entry: "/client.js"
|
|
590
|
+
},
|
|
591
|
+
createdWith: {
|
|
592
|
+
compiler: "0.1.0",
|
|
593
|
+
lakebed: version
|
|
594
|
+
},
|
|
595
|
+
deployTarget: "claimed-source",
|
|
596
|
+
format: ANONYMOUS_ARTIFACT_FORMAT,
|
|
597
|
+
limits: {
|
|
598
|
+
instructionBudget: DEFAULT_ANONYMOUS_LIMITS.instructionBudget,
|
|
599
|
+
maxRowsReturned: DEFAULT_ANONYMOUS_LIMITS.rowsReturned,
|
|
600
|
+
maxValueBytes: DEFAULT_ANONYMOUS_LIMITS.maxValueBytes
|
|
601
|
+
},
|
|
602
|
+
server: {
|
|
603
|
+
helpers: {},
|
|
604
|
+
imports: ["lakebed/server"],
|
|
605
|
+
mutations: Object.fromEntries(Object.keys(app.mutations ?? {}).map((name) => [name, { op: "source" }])),
|
|
606
|
+
queries: Object.fromEntries(Object.keys(app.queries ?? {}).map((name) => [name, { op: "source" }])),
|
|
607
|
+
schema,
|
|
608
|
+
source: {
|
|
609
|
+
bytes: serverBundle.byteLength,
|
|
610
|
+
bundle: serverBundleBase64,
|
|
611
|
+
bundleHash: serverBundleHash,
|
|
612
|
+
entry: "/server.mjs"
|
|
613
|
+
}
|
|
614
|
+
},
|
|
615
|
+
source: {
|
|
616
|
+
files: sourceManifest,
|
|
617
|
+
snapshotHash: sourceSnapshotHash
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
const artifactHash = sha256(stableStringify(artifact));
|
|
621
|
+
|
|
622
|
+
return {
|
|
623
|
+
artifact,
|
|
624
|
+
artifactHash,
|
|
625
|
+
clientBundle: clientBundleBase64,
|
|
626
|
+
clientBundleHash
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
558
630
|
function validateExpression(expr, path, diagnostics) {
|
|
559
631
|
if (!isExpression(expr)) {
|
|
560
632
|
diagnostics.push(diagnostic(path, "Expected an anonymous IR expression."));
|
|
@@ -614,6 +686,10 @@ function validateValue(value, path, diagnostics) {
|
|
|
614
686
|
}
|
|
615
687
|
|
|
616
688
|
function validateQuery(query, path, schema, diagnostics) {
|
|
689
|
+
if (query?.op === "source") {
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
|
|
617
693
|
if (!isPlainObject(query) || query.op !== "table.all" || !schema[query.table]) {
|
|
618
694
|
diagnostics.push(diagnostic(path, "Invalid anonymous query IR."));
|
|
619
695
|
return;
|
|
@@ -636,7 +712,7 @@ function validateQuery(query, path, schema, diagnostics) {
|
|
|
636
712
|
}
|
|
637
713
|
}
|
|
638
714
|
|
|
639
|
-
export function validateAnonymousArtifact(artifact) {
|
|
715
|
+
export function validateAnonymousArtifact(artifact, { allowClaimedSource = false } = {}) {
|
|
640
716
|
const diagnostics = [];
|
|
641
717
|
|
|
642
718
|
if (!isPlainObject(artifact)) {
|
|
@@ -647,7 +723,7 @@ export function validateAnonymousArtifact(artifact) {
|
|
|
647
723
|
diagnostics.push(diagnostic("artifact.format", `Expected ${ANONYMOUS_ARTIFACT_FORMAT}.`));
|
|
648
724
|
}
|
|
649
725
|
|
|
650
|
-
if (artifact.deployTarget !== "anonymous-interpreter") {
|
|
726
|
+
if (artifact.deployTarget !== "anonymous-interpreter" && !(allowClaimedSource && artifact.deployTarget === "claimed-source")) {
|
|
651
727
|
diagnostics.push(diagnostic("artifact.deployTarget", "Anonymous deploys require deployTarget anonymous-interpreter."));
|
|
652
728
|
}
|
|
653
729
|
|
|
@@ -669,9 +745,49 @@ export function validateAnonymousArtifact(artifact) {
|
|
|
669
745
|
|
|
670
746
|
for (const [name, query] of Object.entries(artifact.server?.queries ?? {})) {
|
|
671
747
|
validateQuery(query, `artifact.server.queries.${name}`, schema ?? {}, diagnostics);
|
|
748
|
+
if (query?.op === "source" && artifact.server?.source === undefined) {
|
|
749
|
+
diagnostics.push(diagnostic(`artifact.server.queries.${name}`, "Source query requires artifact.server.source."));
|
|
750
|
+
}
|
|
751
|
+
if (query?.op === "source" && artifact.deployTarget !== "claimed-source") {
|
|
752
|
+
diagnostics.push(diagnostic(`artifact.server.queries.${name}`, "Source query requires deployTarget claimed-source."));
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
if (artifact.server?.source !== undefined) {
|
|
757
|
+
if (artifact.deployTarget !== "claimed-source") {
|
|
758
|
+
diagnostics.push(diagnostic("artifact.server.source", "Server source bundles require deployTarget claimed-source."));
|
|
759
|
+
}
|
|
760
|
+
const source = artifact.server.source;
|
|
761
|
+
if (
|
|
762
|
+
!isPlainObject(source) ||
|
|
763
|
+
typeof source.bundle !== "string" ||
|
|
764
|
+
typeof source.bundleHash !== "string" ||
|
|
765
|
+
!Number.isInteger(source.bytes) ||
|
|
766
|
+
source.bytes <= 0
|
|
767
|
+
) {
|
|
768
|
+
diagnostics.push(diagnostic("artifact.server.source", "Invalid anonymous server source bundle."));
|
|
769
|
+
} else {
|
|
770
|
+
const bundle = Buffer.from(source.bundle, "base64");
|
|
771
|
+
if (bundle.byteLength !== source.bytes) {
|
|
772
|
+
diagnostics.push(diagnostic("artifact.server.source.bytes", "Server bundle byte count does not match artifact.server.source.bytes."));
|
|
773
|
+
}
|
|
774
|
+
if (sha256(bundle) !== source.bundleHash) {
|
|
775
|
+
diagnostics.push(diagnostic("artifact.server.source.bundleHash", "Server bundle hash does not match artifact.server.source.bundleHash."));
|
|
776
|
+
}
|
|
777
|
+
}
|
|
672
778
|
}
|
|
673
779
|
|
|
674
780
|
for (const [name, mutation] of Object.entries(artifact.server?.mutations ?? {})) {
|
|
781
|
+
if (mutation?.op === "source") {
|
|
782
|
+
if (artifact.server?.source === undefined) {
|
|
783
|
+
diagnostics.push(diagnostic(`artifact.server.mutations.${name}`, "Source mutation requires artifact.server.source."));
|
|
784
|
+
}
|
|
785
|
+
if (artifact.deployTarget !== "claimed-source") {
|
|
786
|
+
diagnostics.push(diagnostic(`artifact.server.mutations.${name}`, "Source mutation requires deployTarget claimed-source."));
|
|
787
|
+
}
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
|
|
675
791
|
if (mutation?.op !== "mutation" || !Array.isArray(mutation.body)) {
|
|
676
792
|
diagnostics.push(diagnostic(`artifact.server.mutations.${name}`, "Invalid mutation IR."));
|
|
677
793
|
continue;
|
|
@@ -704,12 +820,12 @@ export function validateAnonymousArtifact(artifact) {
|
|
|
704
820
|
return diagnostics;
|
|
705
821
|
}
|
|
706
822
|
|
|
707
|
-
export function validateAnonymousDeployPayload(payload) {
|
|
823
|
+
export function validateAnonymousDeployPayload(payload, options = {}) {
|
|
708
824
|
if (!payload || typeof payload !== "object") {
|
|
709
825
|
throw new Error("Deploy payload must be a JSON object.");
|
|
710
826
|
}
|
|
711
827
|
|
|
712
|
-
const diagnostics = validateAnonymousArtifact(payload.artifact);
|
|
828
|
+
const diagnostics = validateAnonymousArtifact(payload.artifact, options);
|
|
713
829
|
if (diagnostics.length > 0) {
|
|
714
830
|
throw new AnonymousCompilerError(diagnostics);
|
|
715
831
|
}
|
|
@@ -939,7 +1055,124 @@ async function executeQuerySpec({ args = [], artifact, auth, query, state, deplo
|
|
|
939
1055
|
return results.slice(0, limit);
|
|
940
1056
|
}
|
|
941
1057
|
|
|
1058
|
+
async function loadSourceApp(artifact) {
|
|
1059
|
+
const source = artifact.server?.source;
|
|
1060
|
+
if (!source) {
|
|
1061
|
+
return null;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
if (!sourceModuleCache.has(source.bundleHash)) {
|
|
1065
|
+
const url = `data:text/javascript;base64,${source.bundle}`;
|
|
1066
|
+
sourceModuleCache.set(source.bundleHash, import(url).then((module) => module.default));
|
|
1067
|
+
}
|
|
1068
|
+
return sourceModuleCache.get(source.bundleHash);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function createSourceQuery(rows, tableName) {
|
|
1072
|
+
return {
|
|
1073
|
+
filters: [],
|
|
1074
|
+
limitValue: null,
|
|
1075
|
+
orderByValue: null,
|
|
1076
|
+
tableName,
|
|
1077
|
+
where(field, value) {
|
|
1078
|
+
return { ...this, filters: [...this.filters, { field, value }] };
|
|
1079
|
+
},
|
|
1080
|
+
orderBy(field, direction = "asc") {
|
|
1081
|
+
return { ...this, orderByValue: { field, direction: direction === "desc" ? "desc" : "asc" } };
|
|
1082
|
+
},
|
|
1083
|
+
limit(count) {
|
|
1084
|
+
return { ...this, limitValue: Number(count) };
|
|
1085
|
+
},
|
|
1086
|
+
all() {
|
|
1087
|
+
let results = [...rows[this.tableName]].map((row) => ({ ...row }));
|
|
1088
|
+
for (const filter of this.filters) {
|
|
1089
|
+
results = results.filter((row) => row[filter.field] === filter.value);
|
|
1090
|
+
}
|
|
1091
|
+
if (this.orderByValue) {
|
|
1092
|
+
const direction = this.orderByValue.direction === "desc" ? -1 : 1;
|
|
1093
|
+
results.sort((left, right) => compareValues(left[this.orderByValue.field], right[this.orderByValue.field]) * direction);
|
|
1094
|
+
}
|
|
1095
|
+
return results.slice(0, this.limitValue ?? results.length);
|
|
1096
|
+
}
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
async function createSourceContext({ artifact, auth, deployId, state }) {
|
|
1101
|
+
const rows = {};
|
|
1102
|
+
const operations = [];
|
|
1103
|
+
for (const tableName of Object.keys(artifact.server.schema ?? {})) {
|
|
1104
|
+
rows[tableName] = await state.listRows(deployId, tableName);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
const db = {};
|
|
1108
|
+
for (const tableName of Object.keys(artifact.server.schema ?? {})) {
|
|
1109
|
+
db[tableName] = {
|
|
1110
|
+
...createSourceQuery(rows, tableName),
|
|
1111
|
+
get(id) {
|
|
1112
|
+
return rows[tableName].find((row) => row.id === id) ?? null;
|
|
1113
|
+
},
|
|
1114
|
+
insert(value) {
|
|
1115
|
+
const row = prepareInsert(artifact.server.schema, tableName, value);
|
|
1116
|
+
rows[tableName].push(row);
|
|
1117
|
+
operations.push({ op: "insert", row, table: tableName });
|
|
1118
|
+
return { ...row };
|
|
1119
|
+
},
|
|
1120
|
+
update(id, patch) {
|
|
1121
|
+
const index = rows[tableName].findIndex((row) => row.id === id);
|
|
1122
|
+
if (index === -1) {
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
const cleanPatch = preparePatch(artifact.server.schema, tableName, patch);
|
|
1126
|
+
rows[tableName][index] = { ...rows[tableName][index], ...cleanPatch };
|
|
1127
|
+
operations.push({ id, op: "update", patch: cleanPatch, table: tableName });
|
|
1128
|
+
},
|
|
1129
|
+
delete(id) {
|
|
1130
|
+
const index = rows[tableName].findIndex((row) => row.id === id);
|
|
1131
|
+
if (index === -1) {
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
rows[tableName].splice(index, 1);
|
|
1135
|
+
operations.push({ id, op: "delete", table: tableName });
|
|
1136
|
+
}
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
return {
|
|
1141
|
+
ctx: {
|
|
1142
|
+
auth,
|
|
1143
|
+
db,
|
|
1144
|
+
env: {},
|
|
1145
|
+
log: {
|
|
1146
|
+
error() {},
|
|
1147
|
+
info() {},
|
|
1148
|
+
warn() {}
|
|
1149
|
+
}
|
|
1150
|
+
},
|
|
1151
|
+
async flush(tx) {
|
|
1152
|
+
for (const operation of operations) {
|
|
1153
|
+
if (operation.op === "insert") {
|
|
1154
|
+
await tx.insertRow(deployId, operation.table, operation.row);
|
|
1155
|
+
} else if (operation.op === "update") {
|
|
1156
|
+
await tx.updateRow(deployId, operation.table, operation.id, operation.patch);
|
|
1157
|
+
} else if (operation.op === "delete") {
|
|
1158
|
+
await tx.deleteRow(deployId, operation.table, operation.id);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
|
|
942
1165
|
export async function executeAnonymousQuery({ args = [], artifact, auth, deployId, name, state }) {
|
|
1166
|
+
const sourceApp = await loadSourceApp(artifact);
|
|
1167
|
+
if (sourceApp) {
|
|
1168
|
+
const handler = sourceApp.queries?.[name];
|
|
1169
|
+
if (!handler) {
|
|
1170
|
+
throw new Error(`Unknown query: ${name}`);
|
|
1171
|
+
}
|
|
1172
|
+
const { ctx } = await createSourceContext({ artifact, auth, deployId, state });
|
|
1173
|
+
return handler(ctx, ...args);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
943
1176
|
const query = artifact.server.queries?.[name];
|
|
944
1177
|
if (!query) {
|
|
945
1178
|
throw new Error(`Unknown query: ${name}`);
|
|
@@ -958,6 +1191,20 @@ async function checkUpdateGuards({ auth, guards = [], row }) {
|
|
|
958
1191
|
}
|
|
959
1192
|
|
|
960
1193
|
export async function executeAnonymousMutation({ args = [], artifact, auth, deployId, name, state }) {
|
|
1194
|
+
const sourceApp = await loadSourceApp(artifact);
|
|
1195
|
+
if (sourceApp) {
|
|
1196
|
+
const handler = sourceApp.mutations?.[name];
|
|
1197
|
+
if (!handler) {
|
|
1198
|
+
throw new Error(`Unknown mutation: ${name}`);
|
|
1199
|
+
}
|
|
1200
|
+
return state.transaction(deployId, async (tx) => {
|
|
1201
|
+
const source = await createSourceContext({ artifact, auth, deployId, state: tx });
|
|
1202
|
+
const result = await handler(source.ctx, ...args);
|
|
1203
|
+
await source.flush(tx);
|
|
1204
|
+
return result ?? null;
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
|
|
961
1208
|
const mutation = artifact.server.mutations?.[name];
|
|
962
1209
|
if (!mutation) {
|
|
963
1210
|
throw new Error(`Unknown mutation: ${name}`);
|
package/src/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
ANONYMOUS_ARTIFACT_MEDIA_TYPE,
|
|
11
11
|
AnonymousCompilerError,
|
|
12
12
|
createAnonymousArtifact,
|
|
13
|
+
createClaimedArtifact,
|
|
13
14
|
parseTtlSeconds,
|
|
14
15
|
stableStringify
|
|
15
16
|
} from "./anonymous.js";
|
|
@@ -32,6 +33,7 @@ Usage:
|
|
|
32
33
|
lakebed dev <capsule-dir> [--port 3000]
|
|
33
34
|
lakebed build <capsule-dir> --target anonymous [--out .lakebed/artifacts/app.json] [--json]
|
|
34
35
|
lakebed deploy [capsule-dir] [--ttl 7d] [--api <url>] [--json]
|
|
36
|
+
lakebed claim [capsule-dir] [--api <url>] [--json]
|
|
35
37
|
lakebed anonymous-server [--port 8787] [--public-root-url <url>] [--app-base-domain <domain>]
|
|
36
38
|
lakebed inspect <deploy-id-or-url> [--api <url>] [--json]
|
|
37
39
|
lakebed run-many <capsule-dir> [--count 20] [--base-port 4000]
|
|
@@ -299,6 +301,7 @@ export async function buildCapsule({ capsuleDir, sourceStore, capsuleId = "dev"
|
|
|
299
301
|
buildDir,
|
|
300
302
|
clientOut,
|
|
301
303
|
env: await readServerEnv(workingStore),
|
|
304
|
+
serverOut,
|
|
302
305
|
sourceStore: workingStore
|
|
303
306
|
};
|
|
304
307
|
}
|
|
@@ -598,6 +601,30 @@ async function buildAnonymousEnvelope(capsuleArg) {
|
|
|
598
601
|
};
|
|
599
602
|
}
|
|
600
603
|
|
|
604
|
+
async function buildClaimedEnvelope(capsuleArg) {
|
|
605
|
+
const capsuleDir = resolveCapsuleDir(capsuleArg);
|
|
606
|
+
const sourceStore = await createMemorySourceStoreFromDirectory(capsuleDir);
|
|
607
|
+
const built = await buildCapsule({
|
|
608
|
+
capsuleDir,
|
|
609
|
+
capsuleId: `claimed-${Date.now()}`,
|
|
610
|
+
sourceStore
|
|
611
|
+
});
|
|
612
|
+
const artifact = await createClaimedArtifact({
|
|
613
|
+
app: built.app,
|
|
614
|
+
clientOut: built.clientOut,
|
|
615
|
+
serverOut: built.serverOut,
|
|
616
|
+
sourceStore
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
return {
|
|
620
|
+
artifact: artifact.artifact,
|
|
621
|
+
artifactHash: artifact.artifactHash,
|
|
622
|
+
clientBundle: artifact.clientBundle,
|
|
623
|
+
clientBundleHash: artifact.clientBundleHash,
|
|
624
|
+
mediaType: ANONYMOUS_ARTIFACT_MEDIA_TYPE
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
601
628
|
function defaultArtifactPath(capsuleDir) {
|
|
602
629
|
return resolve(root, ".lakebed/artifacts", `${basename(capsuleDir)}.anonymous.json`);
|
|
603
630
|
}
|
|
@@ -642,6 +669,14 @@ function claimTokenFromDeployResponse(deployed) {
|
|
|
642
669
|
return null;
|
|
643
670
|
}
|
|
644
671
|
|
|
672
|
+
function claimUrlFromDeployMetadata(metadata) {
|
|
673
|
+
if (!metadata?.api || !metadata?.deployId || !metadata?.claimToken) {
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return `${String(metadata.api).replace(/\/+$/g, "")}/claim/${encodeURIComponent(metadata.deployId)}/${encodeURIComponent(metadata.claimToken)}`;
|
|
678
|
+
}
|
|
679
|
+
|
|
645
680
|
function deployRequestBody(envelope, ttl) {
|
|
646
681
|
return JSON.stringify({
|
|
647
682
|
artifact: envelope.artifact,
|
|
@@ -700,13 +735,31 @@ async function readResponseJson(response) {
|
|
|
700
735
|
async function deployCommand(args) {
|
|
701
736
|
const [capsuleArg] = positionals(args);
|
|
702
737
|
const capsuleDir = capsuleArg ? resolveCapsuleDir(capsuleArg) : root;
|
|
703
|
-
const envelope = await buildAnonymousEnvelope(capsuleDir);
|
|
704
738
|
const ttl = parseTtlSeconds(readArg(args, "--ttl", "7d"));
|
|
705
739
|
const api = deployApiUrl(args);
|
|
706
|
-
const body = deployRequestBody(envelope, ttl);
|
|
707
740
|
const metadata = await readDeployMetadata(capsuleDir);
|
|
708
741
|
const canUpdate =
|
|
709
742
|
metadata?.api === api && typeof metadata?.deployId === "string" && typeof metadata?.claimToken === "string";
|
|
743
|
+
let currentDeploy = null;
|
|
744
|
+
if (canUpdate) {
|
|
745
|
+
const currentResponse = await fetch(`${api}/v1/deploys/${encodeURIComponent(metadata.deployId)}`);
|
|
746
|
+
if (currentResponse.ok) {
|
|
747
|
+
currentDeploy = await currentResponse.json();
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
let envelope;
|
|
752
|
+
try {
|
|
753
|
+
envelope = currentDeploy?.claimed ? await buildClaimedEnvelope(capsuleDir) : await buildAnonymousEnvelope(capsuleDir);
|
|
754
|
+
} catch (error) {
|
|
755
|
+
if (error instanceof AnonymousCompilerError && canUpdate && !currentDeploy?.claimed) {
|
|
756
|
+
throw new Error(
|
|
757
|
+
`${error.message}\n\nThis deploy is still anonymous. Claim it first, then run lakebed deploy again to use server-side fetch.`
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
throw error;
|
|
761
|
+
}
|
|
762
|
+
const body = deployRequestBody(envelope, ttl);
|
|
710
763
|
let mode = "created";
|
|
711
764
|
let response;
|
|
712
765
|
|
|
@@ -764,7 +817,60 @@ async function deployCommand(args) {
|
|
|
764
817
|
console.log(` state: ${deployed.limits.stateBytes} bytes`);
|
|
765
818
|
console.log(` requests: ${deployed.limits.requestsPerDay} / day`);
|
|
766
819
|
console.log(` mutations: ${deployed.limits.mutationsPerDay} / day`);
|
|
767
|
-
console.log(
|
|
820
|
+
console.log(` outbound fetch: ${envelope.artifact.deployTarget === "claimed-source" ? "enabled" : "disabled"}`);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
async function claimCommand(args) {
|
|
824
|
+
const [capsuleArg] = positionals(args);
|
|
825
|
+
const capsuleDir = capsuleArg ? resolveCapsuleDir(capsuleArg) : root;
|
|
826
|
+
const api = deployApiUrl(args);
|
|
827
|
+
const metadata = await readDeployMetadata(capsuleDir);
|
|
828
|
+
|
|
829
|
+
if (!metadata) {
|
|
830
|
+
throw new Error(`No Lakebed deploy metadata found at ${deployMetadataPath(capsuleDir)}. Run lakebed deploy from this project first.`);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
if (metadata.api !== api) {
|
|
834
|
+
throw new Error(`Saved deploy metadata is for ${metadata.api}, but this command is using ${api}. Pass --api ${metadata.api} to claim it.`);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const claimUrl = claimUrlFromDeployMetadata(metadata);
|
|
838
|
+
if (!claimUrl) {
|
|
839
|
+
throw new Error("This project does not have a saved claim token. Redeploy to create a new claim URL.");
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
let deploy = null;
|
|
843
|
+
try {
|
|
844
|
+
const response = await fetch(`${api}/v1/deploys/${encodeURIComponent(metadata.deployId)}`);
|
|
845
|
+
if (response.ok) {
|
|
846
|
+
deploy = await response.json();
|
|
847
|
+
}
|
|
848
|
+
} catch {
|
|
849
|
+
deploy = null;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const result = {
|
|
853
|
+
claimed: Boolean(deploy?.claimed),
|
|
854
|
+
claimUrl,
|
|
855
|
+
deployId: metadata.deployId,
|
|
856
|
+
url: deploy?.url ?? metadata.url
|
|
857
|
+
};
|
|
858
|
+
|
|
859
|
+
if (hasFlag(args, "--json")) {
|
|
860
|
+
console.log(JSON.stringify(result, null, 2));
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
if (result.claimed) {
|
|
865
|
+
console.log(`Deploy ${result.deployId} is already claimed.`);
|
|
866
|
+
if (result.url) {
|
|
867
|
+
console.log(`App: ${result.url}`);
|
|
868
|
+
}
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
console.log("Open this URL to claim the current project's deploy:");
|
|
873
|
+
console.log(claimUrl);
|
|
768
874
|
}
|
|
769
875
|
|
|
770
876
|
async function anonymousServerCommand(args) {
|
|
@@ -931,7 +1037,7 @@ lakebed logs --port 3000
|
|
|
931
1037
|
- One client entry.
|
|
932
1038
|
- Guest auth locally, with built-in Google sign-in through Shoo.
|
|
933
1039
|
- No file storage.
|
|
934
|
-
- No outbound fetch in anonymous deploys.
|
|
1040
|
+
- No outbound fetch in anonymous deploys. Claim the deploy before using server-side fetch.
|
|
935
1041
|
- Local state resets when \`lakebed dev\` restarts.
|
|
936
1042
|
`;
|
|
937
1043
|
}
|
|
@@ -1146,6 +1252,11 @@ async function main() {
|
|
|
1146
1252
|
return;
|
|
1147
1253
|
}
|
|
1148
1254
|
|
|
1255
|
+
if (command === "claim") {
|
|
1256
|
+
await claimCommand(args);
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1149
1260
|
if (command === "anonymous-server") {
|
|
1150
1261
|
await anonymousServerCommand(args);
|
|
1151
1262
|
return;
|