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 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 <capsule-dir> [--ttl 7d] [--api <url>] [--json]
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
- lakebed deploy my-app
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
- lakebed deploy my-app --api http://localhost:8787
94
+ cd my-app
95
+ lakebed deploy --api http://localhost:8787
94
96
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lakebed",
3
- "version": "0.0.2",
3
+ "version": "0.0.3-alpha.0",
4
4
  "description": "Agent-native CLI and runtime for building and deploying Lakebed capsules.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -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
- const currentArtifact = this.artifacts.get(artifactHash);
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
- hash: artifactHash,
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 createDeploy({ appBaseDomain, artifact, artifactHash, clientBundleBase64, clientBundleHash, publicRootUrl, requestedTtlSeconds }) {
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.2" }) {
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 <capsule-dir> [--ttl 7d] [--api <url>] [--json]
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 envelope = await buildAnonymousEnvelope(capsuleArg);
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 response = await fetch(`${api}/v1/anonymous-deploys`, {
670
- body: JSON.stringify({
671
- artifact: envelope.artifact,
672
- clientBundle: envelope.clientBundle,
673
- clientVersion: "0.0.2",
674
- requestedTtlSeconds: ttl
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("Deploying anonymous preview...\n");
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
- console.log(`Claim: ${deployed.claimUrl}`);
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