lakebed 0.0.2 → 0.0.3-alpha.0
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 +5 -3
- package/package.json +1 -1
- package/src/anonymous-server.js +139 -12
- package/src/anonymous.js +1 -1
- package/src/cli.js +96 -12
package/README.md
CHANGED
|
@@ -61,7 +61,7 @@ export default capsule({
|
|
|
61
61
|
lakebed new <name> [--template todo]
|
|
62
62
|
lakebed dev <capsule-dir> [--port 3000]
|
|
63
63
|
lakebed build <capsule-dir> --target anonymous [--out .lakebed/artifacts/app.json] [--json]
|
|
64
|
-
lakebed deploy
|
|
64
|
+
lakebed deploy [capsule-dir] [--ttl 7d] [--api <url>] [--json]
|
|
65
65
|
lakebed anonymous-server [--port 8787] [--public-root-url <url>]
|
|
66
66
|
lakebed inspect <deploy-id-or-url> [--api <url>] [--json]
|
|
67
67
|
lakebed db list [deploy-id-or-url] [--port 3000]
|
|
@@ -83,12 +83,14 @@ lakebed logs [deploy-id-or-url] [--port 3000]
|
|
|
83
83
|
Once a Lakebed deploy runner is available, deploy a capsule with:
|
|
84
84
|
|
|
85
85
|
```sh
|
|
86
|
-
|
|
86
|
+
cd my-app
|
|
87
|
+
lakebed deploy
|
|
87
88
|
```
|
|
88
89
|
|
|
89
90
|
For local deploy-runner testing:
|
|
90
91
|
|
|
91
92
|
```sh
|
|
92
93
|
lakebed anonymous-server --port 8787
|
|
93
|
-
|
|
94
|
+
cd my-app
|
|
95
|
+
lakebed deploy --api http://localhost:8787
|
|
94
96
|
```
|
package/package.json
CHANGED
package/src/anonymous-server.js
CHANGED
|
@@ -108,6 +108,20 @@ async function readJsonBody(req, maxBytes = 2 * 1024 * 1024) {
|
|
|
108
108
|
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
function bearerToken(req) {
|
|
112
|
+
const header = req.headers.authorization;
|
|
113
|
+
if (!header) {
|
|
114
|
+
return "";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const match = String(header).match(/^Bearer\s+(.+)$/i);
|
|
118
|
+
return match?.[1]?.trim() ?? "";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isDeployTokenValid(deploy, token) {
|
|
122
|
+
return Boolean(token && deploy?.claimTokenHash && hashClaimToken(token) === deploy.claimTokenHash);
|
|
123
|
+
}
|
|
124
|
+
|
|
111
125
|
function normalizePublicRootUrl(value, port) {
|
|
112
126
|
const fallback = `http://localhost:${port}`;
|
|
113
127
|
return String(value || fallback).replace(/\/+$/g, "");
|
|
@@ -220,6 +234,19 @@ export class MemoryAnonymousStore {
|
|
|
220
234
|
|
|
221
235
|
async initialize() {}
|
|
222
236
|
|
|
237
|
+
storeArtifact({ artifact, artifactHash, clientBundleBase64, clientBundleHash, createdAt }) {
|
|
238
|
+
const currentArtifact = this.artifacts.get(artifactHash);
|
|
239
|
+
this.artifacts.set(artifactHash, {
|
|
240
|
+
artifact,
|
|
241
|
+
bytes: Buffer.byteLength(clientBundleBase64, "base64"),
|
|
242
|
+
clientBundleBase64,
|
|
243
|
+
clientBundleHash,
|
|
244
|
+
createdAt,
|
|
245
|
+
hash: artifactHash,
|
|
246
|
+
refCount: (currentArtifact?.refCount ?? 0) + 1
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
223
250
|
async createDeploy({ appBaseDomain, artifact, artifactHash, clientBundleBase64, clientBundleHash, publicRootUrl, requestedTtlSeconds }) {
|
|
224
251
|
const deployId = createDeployId();
|
|
225
252
|
let slug = createSlug();
|
|
@@ -233,15 +260,12 @@ export class MemoryAnonymousStore {
|
|
|
233
260
|
const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
234
261
|
const url = appUrlForSlug({ appBaseDomain, publicRootUrl, slug });
|
|
235
262
|
|
|
236
|
-
|
|
237
|
-
this.artifacts.set(artifactHash, {
|
|
263
|
+
this.storeArtifact({
|
|
238
264
|
artifact,
|
|
239
|
-
bytes: Buffer.byteLength(clientBundleBase64, "base64"),
|
|
240
265
|
clientBundleBase64,
|
|
241
266
|
clientBundleHash,
|
|
242
267
|
createdAt,
|
|
243
|
-
|
|
244
|
-
refCount: (currentArtifact?.refCount ?? 0) + 1
|
|
268
|
+
artifactHash
|
|
245
269
|
});
|
|
246
270
|
|
|
247
271
|
const deploy = {
|
|
@@ -263,6 +287,33 @@ export class MemoryAnonymousStore {
|
|
|
263
287
|
return { deploy, token };
|
|
264
288
|
}
|
|
265
289
|
|
|
290
|
+
async updateDeploy({ artifact, artifactHash, clientBundleBase64, clientBundleHash, deployId, requestedTtlSeconds }) {
|
|
291
|
+
const currentDeploy = await this.getDeployById(deployId);
|
|
292
|
+
if (!currentDeploy) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const createdAt = now();
|
|
297
|
+
const ttlSeconds = parseTtlSeconds(requestedTtlSeconds);
|
|
298
|
+
const deploy = {
|
|
299
|
+
...currentDeploy,
|
|
300
|
+
artifactHash,
|
|
301
|
+
clientBundleHash,
|
|
302
|
+
expiresAt: new Date(Date.now() + ttlSeconds * 1000).toISOString(),
|
|
303
|
+
status: "active"
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
this.storeArtifact({
|
|
307
|
+
artifact,
|
|
308
|
+
artifactHash,
|
|
309
|
+
clientBundleBase64,
|
|
310
|
+
clientBundleHash,
|
|
311
|
+
createdAt
|
|
312
|
+
});
|
|
313
|
+
this.deploys.set(deployId, deploy);
|
|
314
|
+
return deploy;
|
|
315
|
+
}
|
|
316
|
+
|
|
266
317
|
async getDeployById(id) {
|
|
267
318
|
return this.deploys.get(id) ?? null;
|
|
268
319
|
}
|
|
@@ -467,13 +518,7 @@ export class PostgresAnonymousStore {
|
|
|
467
518
|
return this.pool.query(sql, params);
|
|
468
519
|
}
|
|
469
520
|
|
|
470
|
-
async
|
|
471
|
-
const createdAt = now();
|
|
472
|
-
const ttlSeconds = parseTtlSeconds(requestedTtlSeconds);
|
|
473
|
-
const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
474
|
-
const token = createClaimToken();
|
|
475
|
-
const deployId = createDeployId();
|
|
476
|
-
|
|
521
|
+
async storeArtifact({ artifact, artifactHash, clientBundleBase64, clientBundleHash, createdAt }) {
|
|
477
522
|
await this.query(
|
|
478
523
|
`
|
|
479
524
|
insert into artifacts(hash, artifact_json, client_bundle_base64, client_bundle_hash, bytes, created_at, ref_count)
|
|
@@ -482,6 +527,16 @@ export class PostgresAnonymousStore {
|
|
|
482
527
|
`,
|
|
483
528
|
[artifactHash, JSON.stringify(artifact), clientBundleBase64, clientBundleHash, Buffer.byteLength(clientBundleBase64, "base64"), createdAt]
|
|
484
529
|
);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async createDeploy({ appBaseDomain, artifact, artifactHash, clientBundleBase64, clientBundleHash, publicRootUrl, requestedTtlSeconds }) {
|
|
533
|
+
const createdAt = now();
|
|
534
|
+
const ttlSeconds = parseTtlSeconds(requestedTtlSeconds);
|
|
535
|
+
const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
536
|
+
const token = createClaimToken();
|
|
537
|
+
const deployId = createDeployId();
|
|
538
|
+
|
|
539
|
+
await this.storeArtifact({ artifact, artifactHash, clientBundleBase64, clientBundleHash, createdAt });
|
|
485
540
|
|
|
486
541
|
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
487
542
|
const slug = createSlug();
|
|
@@ -536,6 +591,29 @@ export class PostgresAnonymousStore {
|
|
|
536
591
|
throw new Error("Unable to allocate anonymous deploy slug.");
|
|
537
592
|
}
|
|
538
593
|
|
|
594
|
+
async updateDeploy({ artifact, artifactHash, clientBundleBase64, clientBundleHash, deployId, requestedTtlSeconds }) {
|
|
595
|
+
const currentDeploy = await this.getDeployById(deployId);
|
|
596
|
+
if (!currentDeploy) {
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const createdAt = now();
|
|
601
|
+
const ttlSeconds = parseTtlSeconds(requestedTtlSeconds);
|
|
602
|
+
const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
603
|
+
await this.storeArtifact({ artifact, artifactHash, clientBundleBase64, clientBundleHash, createdAt });
|
|
604
|
+
|
|
605
|
+
const result = await this.query(
|
|
606
|
+
`
|
|
607
|
+
update deploys
|
|
608
|
+
set status = 'active', artifact_hash = $2, client_bundle_hash = $3, expires_at = $4
|
|
609
|
+
where id = $1
|
|
610
|
+
returning *
|
|
611
|
+
`,
|
|
612
|
+
[deployId, artifactHash, clientBundleHash, expiresAt]
|
|
613
|
+
);
|
|
614
|
+
return this.rowToDeploy(result.rows[0]);
|
|
615
|
+
}
|
|
616
|
+
|
|
539
617
|
rowToDeploy(row) {
|
|
540
618
|
if (!row) {
|
|
541
619
|
return null;
|
|
@@ -834,6 +912,20 @@ export async function startAnonymousServer({
|
|
|
834
912
|
await resolvedStore.initialize();
|
|
835
913
|
const subscriptions = new Map();
|
|
836
914
|
|
|
915
|
+
async function refreshDeploySubscriptions(deploy) {
|
|
916
|
+
const storedArtifact = await resolvedStore.getArtifact(deploy.artifactHash);
|
|
917
|
+
if (!storedArtifact) {
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
for (const subscription of subscriptions.values()) {
|
|
922
|
+
if (subscription.deploy.id === deploy.id) {
|
|
923
|
+
subscription.artifact = storedArtifact.artifact;
|
|
924
|
+
subscription.deploy = deploy;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
837
929
|
async function publishDeploy(deployId) {
|
|
838
930
|
for (const [ws, subscription] of subscriptions) {
|
|
839
931
|
if (subscription.deploy.id !== deployId) {
|
|
@@ -884,6 +976,41 @@ export async function startAnonymousServer({
|
|
|
884
976
|
return;
|
|
885
977
|
}
|
|
886
978
|
|
|
979
|
+
if ((req.method === "PUT" || req.method === "PATCH") && requestUrl.pathname.startsWith("/v1/deploys/")) {
|
|
980
|
+
const deployId = requestUrl.pathname.slice("/v1/deploys/".length);
|
|
981
|
+
const currentDeploy = await resolvedStore.getDeployById(deployId);
|
|
982
|
+
if (!currentDeploy) {
|
|
983
|
+
sendJson(res, 404, { error: "Unknown deploy." });
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
if (!isDeployTokenValid(currentDeploy, bearerToken(req))) {
|
|
988
|
+
sendJson(res, 401, { error: "Invalid deploy token." });
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const body = await readJsonBody(req);
|
|
993
|
+
const payload = validateAnonymousDeployPayload(body);
|
|
994
|
+
const deploy = await resolvedStore.updateDeploy({
|
|
995
|
+
artifact: payload.artifact,
|
|
996
|
+
artifactHash: payload.artifactHash,
|
|
997
|
+
clientBundleBase64: payload.clientBundleBase64,
|
|
998
|
+
clientBundleHash: payload.clientBundleHash,
|
|
999
|
+
deployId,
|
|
1000
|
+
requestedTtlSeconds: body.requestedTtlSeconds ?? requestUrl.searchParams.get("ttl") ?? undefined
|
|
1001
|
+
});
|
|
1002
|
+
if (!deploy) {
|
|
1003
|
+
sendJson(res, 404, { error: "Unknown deploy." });
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
await resolvedStore.appendLog(deploy.id, "info", "anonymous deploy updated", { artifactHash: deploy.artifactHash });
|
|
1008
|
+
await refreshDeploySubscriptions(deploy);
|
|
1009
|
+
await publishDeploy(deploy.id);
|
|
1010
|
+
sendJson(res, 200, responseForDeploy({ deploy }));
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
887
1014
|
if (req.method === "GET" && requestUrl.pathname.startsWith("/v1/deploys/")) {
|
|
888
1015
|
const id = requestUrl.pathname.slice("/v1/deploys/".length);
|
|
889
1016
|
const deploy = (await resolvedStore.getDeployById(id)) ?? (await resolvedStore.getDeployBySlug(id));
|
package/src/anonymous.js
CHANGED
|
@@ -484,7 +484,7 @@ function compileServerToIr(app, schema) {
|
|
|
484
484
|
return { diagnostics, mutations, queries };
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
-
export async function createAnonymousArtifact({ app, clientOut, sourceStore, version = "0.0.
|
|
487
|
+
export async function createAnonymousArtifact({ app, clientOut, sourceStore, version = "0.0.3-alpha.0" }) {
|
|
488
488
|
const sourceFiles = await readSourceFiles(sourceStore);
|
|
489
489
|
const diagnostics = forbiddenSourceDiagnostics(sourceFiles);
|
|
490
490
|
const { diagnostics: schemaDiagnostics, schema } = serializeSchema(app.schema);
|
package/src/cli.js
CHANGED
|
@@ -30,7 +30,7 @@ Usage:
|
|
|
30
30
|
lakebed new <name> [--template todo]
|
|
31
31
|
lakebed dev <capsule-dir> [--port 3000]
|
|
32
32
|
lakebed build <capsule-dir> --target anonymous [--out .lakebed/artifacts/app.json] [--json]
|
|
33
|
-
lakebed deploy
|
|
33
|
+
lakebed deploy [capsule-dir] [--ttl 7d] [--api <url>] [--json]
|
|
34
34
|
lakebed anonymous-server [--port 8787] [--public-root-url <url>]
|
|
35
35
|
lakebed inspect <deploy-id-or-url> [--api <url>] [--json]
|
|
36
36
|
lakebed run-many <capsule-dir> [--count 20] [--base-port 4000]
|
|
@@ -615,6 +615,55 @@ function defaultArtifactPath(capsuleDir) {
|
|
|
615
615
|
return resolve(root, ".lakebed/artifacts", `${basename(capsuleDir)}.anonymous.json`);
|
|
616
616
|
}
|
|
617
617
|
|
|
618
|
+
function deployMetadataPath(capsuleDir) {
|
|
619
|
+
return resolve(capsuleDir, ".lakebed/deploy.json");
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
async function readDeployMetadata(capsuleDir) {
|
|
623
|
+
try {
|
|
624
|
+
return JSON.parse(await readFile(deployMetadataPath(capsuleDir), "utf8"));
|
|
625
|
+
} catch (error) {
|
|
626
|
+
if (error?.code === "ENOENT") {
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
throw new Error(`Unable to read Lakebed deploy metadata: ${error instanceof Error ? error.message : String(error)}`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async function writeDeployMetadata(capsuleDir, metadata) {
|
|
635
|
+
const path = deployMetadataPath(capsuleDir);
|
|
636
|
+
await mkdir(dirname(path), { recursive: true });
|
|
637
|
+
await writeFile(path, `${JSON.stringify(metadata, null, 2)}\n`);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function claimTokenFromDeployResponse(deployed) {
|
|
641
|
+
if (!deployed?.claimUrl || !deployed?.deployId) {
|
|
642
|
+
return null;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
try {
|
|
646
|
+
const url = new URL(deployed.claimUrl);
|
|
647
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
648
|
+
if (segments[0] === "claim" && segments[1] === deployed.deployId) {
|
|
649
|
+
return segments[2] ?? null;
|
|
650
|
+
}
|
|
651
|
+
} catch {
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function deployRequestBody(envelope, ttl) {
|
|
659
|
+
return JSON.stringify({
|
|
660
|
+
artifact: envelope.artifact,
|
|
661
|
+
clientBundle: envelope.clientBundle,
|
|
662
|
+
clientVersion: "0.0.3-alpha.0",
|
|
663
|
+
requestedTtlSeconds: ttl
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
|
|
618
667
|
async function buildCommand(args) {
|
|
619
668
|
const [capsuleArg] = positionals(args);
|
|
620
669
|
const target = readArg(args, "--target", "anonymous");
|
|
@@ -663,32 +712,65 @@ async function readResponseJson(response) {
|
|
|
663
712
|
|
|
664
713
|
async function deployCommand(args) {
|
|
665
714
|
const [capsuleArg] = positionals(args);
|
|
666
|
-
const
|
|
715
|
+
const capsuleDir = capsuleArg ? resolveCapsuleDir(capsuleArg) : root;
|
|
716
|
+
const envelope = await buildAnonymousEnvelope(capsuleDir);
|
|
667
717
|
const ttl = parseTtlSeconds(readArg(args, "--ttl", "7d"));
|
|
668
718
|
const api = deployApiUrl(args);
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
719
|
+
const body = deployRequestBody(envelope, ttl);
|
|
720
|
+
const metadata = await readDeployMetadata(capsuleDir);
|
|
721
|
+
const canUpdate =
|
|
722
|
+
metadata?.api === api && typeof metadata?.deployId === "string" && typeof metadata?.claimToken === "string";
|
|
723
|
+
let mode = "created";
|
|
724
|
+
let response;
|
|
725
|
+
|
|
726
|
+
if (canUpdate) {
|
|
727
|
+
response = await fetch(`${api}/v1/deploys/${encodeURIComponent(metadata.deployId)}`, {
|
|
728
|
+
body,
|
|
729
|
+
headers: {
|
|
730
|
+
"Authorization": `Bearer ${metadata.claimToken}`,
|
|
731
|
+
"Content-Type": "application/json"
|
|
732
|
+
},
|
|
733
|
+
method: "PUT"
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
if (response.status === 404 || response.status === 410) {
|
|
737
|
+
mode = "created";
|
|
738
|
+
response = null;
|
|
739
|
+
} else {
|
|
740
|
+
mode = "updated";
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
response ??= await fetch(`${api}/v1/anonymous-deploys`, {
|
|
745
|
+
body,
|
|
676
746
|
headers: {
|
|
677
747
|
"Content-Type": "application/json"
|
|
678
748
|
},
|
|
679
749
|
method: "POST"
|
|
680
750
|
});
|
|
681
751
|
const deployed = await readResponseJson(response);
|
|
752
|
+
const claimToken = claimTokenFromDeployResponse(deployed) ?? metadata?.claimToken;
|
|
753
|
+
if (claimToken) {
|
|
754
|
+
await writeDeployMetadata(capsuleDir, {
|
|
755
|
+
api,
|
|
756
|
+
claimToken,
|
|
757
|
+
deployId: deployed.deployId,
|
|
758
|
+
updatedAt: new Date().toISOString(),
|
|
759
|
+
url: deployed.url
|
|
760
|
+
});
|
|
761
|
+
}
|
|
682
762
|
|
|
683
763
|
if (hasFlag(args, "--json")) {
|
|
684
764
|
console.log(JSON.stringify(deployed, null, 2));
|
|
685
765
|
return;
|
|
686
766
|
}
|
|
687
767
|
|
|
688
|
-
console.log("
|
|
768
|
+
console.log(`${mode === "updated" ? "Updated" : "Created"} anonymous preview.\n`);
|
|
689
769
|
console.log(`App: ${deployed.url}`);
|
|
690
770
|
console.log(`Expires: ${deployed.expiresAt}`);
|
|
691
|
-
|
|
771
|
+
if (deployed.claimUrl) {
|
|
772
|
+
console.log(`Claim: ${deployed.claimUrl}`);
|
|
773
|
+
}
|
|
692
774
|
console.log(`Inspect: lakebed inspect ${deployed.deployId}`);
|
|
693
775
|
console.log("\nLimits:");
|
|
694
776
|
console.log(` source/artifact: ${deployed.limits.artifactBytes} bytes`);
|
|
@@ -833,7 +915,7 @@ lakebed dev .
|
|
|
833
915
|
Deploy:
|
|
834
916
|
|
|
835
917
|
\`\`\`sh
|
|
836
|
-
lakebed deploy
|
|
918
|
+
lakebed deploy
|
|
837
919
|
\`\`\`
|
|
838
920
|
|
|
839
921
|
Inspect local state while \`lakebed dev\` is running:
|
|
@@ -957,6 +1039,8 @@ export function App() {
|
|
|
957
1039
|
export function cleanTodoText(value: string): string {
|
|
958
1040
|
return value.trim().slice(0, 160);
|
|
959
1041
|
}
|
|
1042
|
+
`,
|
|
1043
|
+
".gitignore": `.lakebed/
|
|
960
1044
|
`,
|
|
961
1045
|
"README.md": `# ${title}
|
|
962
1046
|
|