lakebed 0.0.4 → 0.0.6
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 +23 -1
- package/package.json +7 -6
- package/src/anonymous-server.js +113 -9
- package/src/anonymous.js +77 -5
- package/src/cli.js +181 -32
- package/src/version.js +1 -0
package/README.md
CHANGED
|
@@ -86,6 +86,26 @@ mutations: {
|
|
|
86
86
|
}
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
## Server Env
|
|
90
|
+
|
|
91
|
+
Server handlers can read capsule-specific environment variables through `ctx.env`.
|
|
92
|
+
|
|
93
|
+
```txt
|
|
94
|
+
# .env.lakebed.server
|
|
95
|
+
OPENAI_API_KEY=sk-...
|
|
96
|
+
STRIPE_WEBHOOK_SECRET=whsec_...
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
queries: {
|
|
101
|
+
settings: query((ctx) => ({
|
|
102
|
+
hasOpenAiKey: Boolean(ctx.env.OPENAI_API_KEY)
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`lakebed dev` loads `.env.lakebed.server` locally. For hosted apps, claim the deploy, then run `lakebed deploy` to replace the deploy's server env with the file contents. Env values are not included in anonymous artifacts or source manifests.
|
|
108
|
+
|
|
89
109
|
## Commands
|
|
90
110
|
|
|
91
111
|
```sh
|
|
@@ -109,6 +129,7 @@ lakebed logs [deploy-id-or-url] [--port 3000]
|
|
|
109
129
|
- Tailwind classes in JSX are the only styling path in v0.
|
|
110
130
|
- Guest auth works by default. Google sign-in is built in through Shoo and exposes verified identity on `ctx.auth`.
|
|
111
131
|
- Anonymous deploys disable outbound `fetch`.
|
|
132
|
+
- Non-empty `.env.lakebed.server` files require a claimed deploy before they can sync to hosted server code.
|
|
112
133
|
|
|
113
134
|
## Hosted Deploys
|
|
114
135
|
|
|
@@ -142,9 +163,10 @@ Deploy responses include a claim URL. Configure GitHub OAuth on the runner, then
|
|
|
142
163
|
LAKEBED_GITHUB_CLIENT_ID=...
|
|
143
164
|
LAKEBED_GITHUB_CLIENT_SECRET=...
|
|
144
165
|
LAKEBED_SESSION_SECRET=...
|
|
166
|
+
LAKEBED_SERVER_ENV_SECRET=...
|
|
145
167
|
```
|
|
146
168
|
|
|
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
|
|
169
|
+
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` or hosted server env; after a deploy is claimed, `lakebed deploy` can update it with a source-backed server artifact that supports async handlers, server-side fetch, and `.env.lakebed.server` sync. If the first deploy already needs server-side `fetch` or server env, `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. Set `LAKEBED_SERVER_ENV_SECRET` on Postgres-backed runners to encrypt stored server env values.
|
|
148
170
|
|
|
149
171
|
## Admin Dashboard
|
|
150
172
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lakebed",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
1
|
+
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
3
|
import {
|
|
4
4
|
createClaimToken,
|
|
@@ -22,6 +22,46 @@ function dayWindowStart() {
|
|
|
22
22
|
return `${new Date().toISOString().slice(0, 10)}T00:00:00.000Z`;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
function serverEnvSecretFromEnv(env = process.env) {
|
|
26
|
+
return env.LAKEBED_SERVER_ENV_SECRET ?? env.LAKEBED_SESSION_SECRET ?? env.SPAN_SESSION_SECRET ?? "";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function serverEnvEncryptionKey(secret) {
|
|
30
|
+
return createHash("sha256").update(String(secret)).digest();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function encryptServerEnvValue(value, secret) {
|
|
34
|
+
if (!secret) {
|
|
35
|
+
return `plain:${Buffer.from(value, "utf8").toString("base64")}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const iv = randomBytes(12);
|
|
39
|
+
const cipher = createCipheriv("aes-256-gcm", serverEnvEncryptionKey(secret), iv);
|
|
40
|
+
const ciphertext = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
|
|
41
|
+
const tag = cipher.getAuthTag();
|
|
42
|
+
return `v1:${iv.toString("base64url")}:${tag.toString("base64url")}:${ciphertext.toString("base64url")}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function decryptServerEnvValue(value, secret) {
|
|
46
|
+
const stored = String(value ?? "");
|
|
47
|
+
if (stored.startsWith("plain:")) {
|
|
48
|
+
return Buffer.from(stored.slice("plain:".length), "base64").toString("utf8");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!stored.startsWith("v1:")) {
|
|
52
|
+
return stored;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!secret) {
|
|
56
|
+
throw new Error("LAKEBED_SERVER_ENV_SECRET is required to decrypt hosted server env.");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const [, ivBase64, tagBase64, ciphertextBase64] = stored.split(":");
|
|
60
|
+
const decipher = createDecipheriv("aes-256-gcm", serverEnvEncryptionKey(secret), Buffer.from(ivBase64, "base64url"));
|
|
61
|
+
decipher.setAuthTag(Buffer.from(tagBase64, "base64url"));
|
|
62
|
+
return Buffer.concat([decipher.update(Buffer.from(ciphertextBase64, "base64url")), decipher.final()]).toString("utf8");
|
|
63
|
+
}
|
|
64
|
+
|
|
25
65
|
function html(title, basePath, { shooBaseUrl } = {}) {
|
|
26
66
|
return `<!doctype html>
|
|
27
67
|
<html lang="en">
|
|
@@ -1434,6 +1474,7 @@ export class MemoryAnonymousStore {
|
|
|
1434
1474
|
this.quotaEvents = new Map();
|
|
1435
1475
|
this.queues = new Map();
|
|
1436
1476
|
this.rows = new Map();
|
|
1477
|
+
this.serverEnv = new Map();
|
|
1437
1478
|
}
|
|
1438
1479
|
|
|
1439
1480
|
async initialize() {}
|
|
@@ -1451,7 +1492,7 @@ export class MemoryAnonymousStore {
|
|
|
1451
1492
|
});
|
|
1452
1493
|
}
|
|
1453
1494
|
|
|
1454
|
-
async createDeploy({ appBaseDomain, artifact, artifactHash, clientBundleBase64, clientBundleHash, publicRootUrl, requestedTtlSeconds }) {
|
|
1495
|
+
async createDeploy({ appBaseDomain, artifact, artifactHash, clientBundleBase64, clientBundleHash, publicRootUrl, requestedTtlSeconds, serverEnv }) {
|
|
1455
1496
|
const deployId = createDeployId();
|
|
1456
1497
|
const normalizedAppBaseDomain = normalizeAppBaseDomain(appBaseDomain);
|
|
1457
1498
|
let slug = createSlug();
|
|
@@ -1492,6 +1533,9 @@ export class MemoryAnonymousStore {
|
|
|
1492
1533
|
};
|
|
1493
1534
|
this.deploys.set(deployId, deploy);
|
|
1494
1535
|
this.deploysBySlug.set(slug, deployId);
|
|
1536
|
+
if (serverEnv !== undefined) {
|
|
1537
|
+
this.serverEnv.set(deployId, { ...serverEnv });
|
|
1538
|
+
}
|
|
1495
1539
|
return { deploy, token };
|
|
1496
1540
|
}
|
|
1497
1541
|
|
|
@@ -1503,7 +1547,8 @@ export class MemoryAnonymousStore {
|
|
|
1503
1547
|
clientBundleHash,
|
|
1504
1548
|
deployId,
|
|
1505
1549
|
publicRootUrl,
|
|
1506
|
-
requestedTtlSeconds
|
|
1550
|
+
requestedTtlSeconds,
|
|
1551
|
+
serverEnv
|
|
1507
1552
|
}) {
|
|
1508
1553
|
const currentDeploy = await this.getDeployById(deployId);
|
|
1509
1554
|
if (!currentDeploy) {
|
|
@@ -1533,6 +1578,9 @@ export class MemoryAnonymousStore {
|
|
|
1533
1578
|
createdAt
|
|
1534
1579
|
});
|
|
1535
1580
|
this.deploys.set(deployId, deploy);
|
|
1581
|
+
if (serverEnv !== undefined) {
|
|
1582
|
+
this.serverEnv.set(deployId, { ...serverEnv });
|
|
1583
|
+
}
|
|
1536
1584
|
return deploy;
|
|
1537
1585
|
}
|
|
1538
1586
|
|
|
@@ -1624,6 +1672,14 @@ export class MemoryAnonymousStore {
|
|
|
1624
1672
|
this.tableRows(deployId, tableName).delete(rowId);
|
|
1625
1673
|
}
|
|
1626
1674
|
|
|
1675
|
+
async replaceServerEnv(deployId, serverEnv) {
|
|
1676
|
+
this.serverEnv.set(deployId, { ...serverEnv });
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
async getServerEnv(deployId) {
|
|
1680
|
+
return { ...(this.serverEnv.get(deployId) ?? {}) };
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1627
1683
|
async transaction(deployId, handler) {
|
|
1628
1684
|
const run = () => handler(this);
|
|
1629
1685
|
const next = (this.queues.get(deployId) ?? Promise.resolve()).then(run, run);
|
|
@@ -1768,10 +1824,11 @@ export class MemoryAnonymousStore {
|
|
|
1768
1824
|
}
|
|
1769
1825
|
|
|
1770
1826
|
export class PostgresAnonymousStore {
|
|
1771
|
-
constructor({ connectionString }) {
|
|
1827
|
+
constructor({ connectionString, serverEnvSecret = "" }) {
|
|
1772
1828
|
this.connectionString = connectionString;
|
|
1773
1829
|
this.pool = null;
|
|
1774
1830
|
this.queues = new Map();
|
|
1831
|
+
this.serverEnvSecret = serverEnvSecret;
|
|
1775
1832
|
}
|
|
1776
1833
|
|
|
1777
1834
|
async initialize() {
|
|
@@ -1841,6 +1898,15 @@ export class PostgresAnonymousStore {
|
|
|
1841
1898
|
primary key (deploy_id, bucket, window_start)
|
|
1842
1899
|
)
|
|
1843
1900
|
`);
|
|
1901
|
+
await this.query(`
|
|
1902
|
+
create table if not exists deploy_server_env(
|
|
1903
|
+
deploy_id text not null references deploys(id) on delete cascade,
|
|
1904
|
+
env_key text not null,
|
|
1905
|
+
env_value text not null,
|
|
1906
|
+
updated_at timestamptz not null,
|
|
1907
|
+
primary key (deploy_id, env_key)
|
|
1908
|
+
)
|
|
1909
|
+
`);
|
|
1844
1910
|
}
|
|
1845
1911
|
|
|
1846
1912
|
async query(sql, params = []) {
|
|
@@ -1862,7 +1928,7 @@ export class PostgresAnonymousStore {
|
|
|
1862
1928
|
);
|
|
1863
1929
|
}
|
|
1864
1930
|
|
|
1865
|
-
async createDeploy({ appBaseDomain, artifact, artifactHash, clientBundleBase64, clientBundleHash, publicRootUrl, requestedTtlSeconds }) {
|
|
1931
|
+
async createDeploy({ appBaseDomain, artifact, artifactHash, clientBundleBase64, clientBundleHash, publicRootUrl, requestedTtlSeconds, serverEnv }) {
|
|
1866
1932
|
const createdAt = now();
|
|
1867
1933
|
const normalizedAppBaseDomain = normalizeAppBaseDomain(appBaseDomain);
|
|
1868
1934
|
const ttlSeconds = parseTtlSeconds(requestedTtlSeconds);
|
|
@@ -1917,6 +1983,9 @@ export class PostgresAnonymousStore {
|
|
|
1917
1983
|
deploy.url
|
|
1918
1984
|
]
|
|
1919
1985
|
);
|
|
1986
|
+
if (serverEnv !== undefined) {
|
|
1987
|
+
await this.replaceServerEnv(deploy.id, serverEnv, createdAt);
|
|
1988
|
+
}
|
|
1920
1989
|
return { deploy, token };
|
|
1921
1990
|
} catch (error) {
|
|
1922
1991
|
if (error?.code !== "23505" || attempt === 7) {
|
|
@@ -1936,7 +2005,8 @@ export class PostgresAnonymousStore {
|
|
|
1936
2005
|
clientBundleHash,
|
|
1937
2006
|
deployId,
|
|
1938
2007
|
publicRootUrl,
|
|
1939
|
-
requestedTtlSeconds
|
|
2008
|
+
requestedTtlSeconds,
|
|
2009
|
+
serverEnv
|
|
1940
2010
|
}) {
|
|
1941
2011
|
const currentDeploy = await this.getDeployById(deployId);
|
|
1942
2012
|
if (!currentDeploy) {
|
|
@@ -1966,6 +2036,9 @@ export class PostgresAnonymousStore {
|
|
|
1966
2036
|
`,
|
|
1967
2037
|
[deployId, artifactHash, clientBundleHash, expiresAt, nextPublicRootUrl, nextAppBaseDomain || null, url]
|
|
1968
2038
|
);
|
|
2039
|
+
if (serverEnv !== undefined) {
|
|
2040
|
+
await this.replaceServerEnv(deployId, serverEnv, createdAt);
|
|
2041
|
+
}
|
|
1969
2042
|
return this.rowToDeploy(result.rows[0]);
|
|
1970
2043
|
}
|
|
1971
2044
|
|
|
@@ -2107,6 +2180,24 @@ export class PostgresAnonymousStore {
|
|
|
2107
2180
|
await this.query("delete from state_rows where deploy_id = $1 and table_name = $2 and row_id = $3", [deployId, tableName, rowId]);
|
|
2108
2181
|
}
|
|
2109
2182
|
|
|
2183
|
+
async replaceServerEnv(deployId, serverEnv, updatedAt = now()) {
|
|
2184
|
+
await this.query("delete from deploy_server_env where deploy_id = $1", [deployId]);
|
|
2185
|
+
for (const [key, value] of Object.entries(serverEnv)) {
|
|
2186
|
+
await this.query(
|
|
2187
|
+
`
|
|
2188
|
+
insert into deploy_server_env(deploy_id, env_key, env_value, updated_at)
|
|
2189
|
+
values($1, $2, $3, $4)
|
|
2190
|
+
`,
|
|
2191
|
+
[deployId, key, encryptServerEnvValue(value, this.serverEnvSecret), updatedAt]
|
|
2192
|
+
);
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
async getServerEnv(deployId) {
|
|
2197
|
+
const result = await this.query("select env_key, env_value from deploy_server_env where deploy_id = $1", [deployId]);
|
|
2198
|
+
return Object.fromEntries(result.rows.map((row) => [row.env_key, decryptServerEnvValue(row.env_value, this.serverEnvSecret)]));
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2110
2201
|
async transaction(deployId, handler) {
|
|
2111
2202
|
const run = () => handler(this);
|
|
2112
2203
|
const next = (this.queues.get(deployId) ?? Promise.resolve()).then(run, run);
|
|
@@ -2301,7 +2392,10 @@ export class PostgresAnonymousStore {
|
|
|
2301
2392
|
|
|
2302
2393
|
export async function createAnonymousStoreFromEnv(env = process.env) {
|
|
2303
2394
|
if (env.DATABASE_URL) {
|
|
2304
|
-
const store = new PostgresAnonymousStore({
|
|
2395
|
+
const store = new PostgresAnonymousStore({
|
|
2396
|
+
connectionString: env.DATABASE_URL,
|
|
2397
|
+
serverEnvSecret: serverEnvSecretFromEnv(env)
|
|
2398
|
+
});
|
|
2305
2399
|
await store.initialize();
|
|
2306
2400
|
return store;
|
|
2307
2401
|
}
|
|
@@ -2766,6 +2860,10 @@ export async function startAnonymousServer({
|
|
|
2766
2860
|
if (req.method === "POST" && requestUrl.pathname === "/v1/anonymous-deploys") {
|
|
2767
2861
|
const body = await readJsonBody(req);
|
|
2768
2862
|
const payload = validateAnonymousDeployPayload(body);
|
|
2863
|
+
if (payload.serverEnv !== undefined && Object.keys(payload.serverEnv).length > 0) {
|
|
2864
|
+
sendJson(res, 400, { error: "Server env requires a claimed deploy." });
|
|
2865
|
+
return;
|
|
2866
|
+
}
|
|
2769
2867
|
const { deploy, token } = await resolvedStore.createDeploy({
|
|
2770
2868
|
appBaseDomain: resolvedAppBaseDomain,
|
|
2771
2869
|
artifact: payload.artifact,
|
|
@@ -2773,7 +2871,8 @@ export async function startAnonymousServer({
|
|
|
2773
2871
|
clientBundleBase64: payload.clientBundleBase64,
|
|
2774
2872
|
clientBundleHash: payload.clientBundleHash,
|
|
2775
2873
|
publicRootUrl: resolvedPublicRootUrl,
|
|
2776
|
-
requestedTtlSeconds: body.requestedTtlSeconds ?? requestUrl.searchParams.get("ttl") ?? undefined
|
|
2874
|
+
requestedTtlSeconds: body.requestedTtlSeconds ?? requestUrl.searchParams.get("ttl") ?? undefined,
|
|
2875
|
+
serverEnv: payload.serverEnv
|
|
2777
2876
|
});
|
|
2778
2877
|
await resolvedStore.appendLog(deploy.id, "info", "anonymous deploy created", { artifactHash: deploy.artifactHash });
|
|
2779
2878
|
sendJson(res, 201, responseForDeploy({ deploy, token }));
|
|
@@ -2795,6 +2894,10 @@ export async function startAnonymousServer({
|
|
|
2795
2894
|
|
|
2796
2895
|
const body = await readJsonBody(req);
|
|
2797
2896
|
const payload = validateAnonymousDeployPayload(body, { allowClaimedSource: Boolean(currentDeploy.ownerId) });
|
|
2897
|
+
if (payload.serverEnv !== undefined && !currentDeploy.ownerId) {
|
|
2898
|
+
sendJson(res, 400, { error: "Server env requires a claimed deploy." });
|
|
2899
|
+
return;
|
|
2900
|
+
}
|
|
2798
2901
|
const deploy = await resolvedStore.updateDeploy({
|
|
2799
2902
|
appBaseDomain: resolvedAppBaseDomain,
|
|
2800
2903
|
artifact: payload.artifact,
|
|
@@ -2803,7 +2906,8 @@ export async function startAnonymousServer({
|
|
|
2803
2906
|
clientBundleHash: payload.clientBundleHash,
|
|
2804
2907
|
deployId,
|
|
2805
2908
|
publicRootUrl: resolvedPublicRootUrl,
|
|
2806
|
-
requestedTtlSeconds: body.requestedTtlSeconds ?? requestUrl.searchParams.get("ttl") ?? undefined
|
|
2909
|
+
requestedTtlSeconds: body.requestedTtlSeconds ?? requestUrl.searchParams.get("ttl") ?? undefined,
|
|
2910
|
+
serverEnv: payload.serverEnv
|
|
2807
2911
|
});
|
|
2808
2912
|
if (!deploy) {
|
|
2809
2913
|
sendJson(res, 404, { error: "Unknown deploy." });
|
package/src/anonymous.js
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
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 };
|
|
8
|
+
|
|
9
|
+
export const SERVER_ENV_FILE = ".env.lakebed.server";
|
|
10
|
+
export const SERVER_ENV_LIMITS = {
|
|
11
|
+
maxKeyBytes: 128,
|
|
12
|
+
maxKeys: 64,
|
|
13
|
+
maxTotalBytes: 64 * 1024,
|
|
14
|
+
maxValueBytes: 16 * 1024
|
|
15
|
+
};
|
|
6
16
|
|
|
7
17
|
export const DEFAULT_ANONYMOUS_LIMITS = {
|
|
8
18
|
artifactBytes: 1024 * 1024,
|
|
@@ -65,6 +75,65 @@ function isPlainObject(value) {
|
|
|
65
75
|
return Boolean(value) && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype;
|
|
66
76
|
}
|
|
67
77
|
|
|
78
|
+
export function validateServerEnvValues(values, path = "serverEnv.values") {
|
|
79
|
+
if (!isPlainObject(values)) {
|
|
80
|
+
throw new Error(`${path} must be a JSON object.`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const entries = Object.entries(values).sort(([left], [right]) => left.localeCompare(right));
|
|
84
|
+
if (entries.length > SERVER_ENV_LIMITS.maxKeys) {
|
|
85
|
+
throw new Error(`${path} may include at most ${SERVER_ENV_LIMITS.maxKeys} keys.`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const env = {};
|
|
89
|
+
let totalBytes = 0;
|
|
90
|
+
for (const [key, value] of entries) {
|
|
91
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
92
|
+
throw new Error(`${path}.${key} is not a valid server env key.`);
|
|
93
|
+
}
|
|
94
|
+
if (key.startsWith("LAKEBED_")) {
|
|
95
|
+
throw new Error(`${path}.${key} uses the reserved LAKEBED_ prefix.`);
|
|
96
|
+
}
|
|
97
|
+
if (typeof value !== "string") {
|
|
98
|
+
throw new Error(`${path}.${key} must be a string.`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const keyBytes = byteLength(key);
|
|
102
|
+
const valueBytes = byteLength(value);
|
|
103
|
+
if (keyBytes > SERVER_ENV_LIMITS.maxKeyBytes) {
|
|
104
|
+
throw new Error(`${path}.${key} exceeds ${SERVER_ENV_LIMITS.maxKeyBytes} bytes.`);
|
|
105
|
+
}
|
|
106
|
+
if (valueBytes > SERVER_ENV_LIMITS.maxValueBytes) {
|
|
107
|
+
throw new Error(`${path}.${key} exceeds ${SERVER_ENV_LIMITS.maxValueBytes} bytes.`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
totalBytes += keyBytes + valueBytes;
|
|
111
|
+
env[key] = value;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (totalBytes > SERVER_ENV_LIMITS.maxTotalBytes) {
|
|
115
|
+
throw new Error(`${path} exceeds ${SERVER_ENV_LIMITS.maxTotalBytes} bytes.`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return env;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function validateServerEnvPayload(payload) {
|
|
122
|
+
if (payload === undefined) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!isPlainObject(payload)) {
|
|
127
|
+
throw new Error("serverEnv must be a JSON object.");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (payload.mode !== "replace") {
|
|
131
|
+
throw new Error("serverEnv.mode must be replace.");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return validateServerEnvValues(payload.values);
|
|
135
|
+
}
|
|
136
|
+
|
|
68
137
|
function isExpression(value) {
|
|
69
138
|
return Array.isArray(value) && expressionOps.has(value[0]);
|
|
70
139
|
}
|
|
@@ -315,7 +384,7 @@ function diagnostic(file, message) {
|
|
|
315
384
|
}
|
|
316
385
|
|
|
317
386
|
async function readSourceFiles(sourceStore) {
|
|
318
|
-
const paths = (await sourceStore.listFiles()).filter((path) => !path.startsWith("__lakebed/"));
|
|
387
|
+
const paths = (await sourceStore.listFiles()).filter((path) => !path.startsWith("__lakebed/") && path !== SERVER_ENV_FILE);
|
|
319
388
|
const files = [];
|
|
320
389
|
|
|
321
390
|
for (const path of paths) {
|
|
@@ -494,7 +563,7 @@ function compileServerToIr(app, schema) {
|
|
|
494
563
|
return { diagnostics, mutations, queries };
|
|
495
564
|
}
|
|
496
565
|
|
|
497
|
-
export async function createAnonymousArtifact({ app, clientOut, sourceStore, version =
|
|
566
|
+
export async function createAnonymousArtifact({ app, clientOut, sourceStore, version = LAKEBED_VERSION }) {
|
|
498
567
|
const sourceFiles = await readSourceFiles(sourceStore);
|
|
499
568
|
const diagnostics = forbiddenSourceDiagnostics(sourceFiles);
|
|
500
569
|
const { diagnostics: schemaDiagnostics, schema } = serializeSchema(app.schema);
|
|
@@ -556,7 +625,7 @@ export async function createAnonymousArtifact({ app, clientOut, sourceStore, ver
|
|
|
556
625
|
};
|
|
557
626
|
}
|
|
558
627
|
|
|
559
|
-
export async function createClaimedArtifact({ app, clientOut, serverOut, sourceStore, version =
|
|
628
|
+
export async function createClaimedArtifact({ app, clientOut, serverOut, sourceStore, version = LAKEBED_VERSION }) {
|
|
560
629
|
if (!serverOut) {
|
|
561
630
|
throw new AnonymousCompilerError([diagnostic("server/index.ts", "Claimed deploys require a bundled server module.")]);
|
|
562
631
|
}
|
|
@@ -841,12 +910,14 @@ export function validateAnonymousDeployPayload(payload, options = {}) {
|
|
|
841
910
|
}
|
|
842
911
|
|
|
843
912
|
const artifactHash = sha256(stableStringify(payload.artifact));
|
|
913
|
+
const serverEnv = validateServerEnvPayload(payload.serverEnv);
|
|
844
914
|
return {
|
|
845
915
|
artifact: cloneJson(payload.artifact),
|
|
846
916
|
artifactHash,
|
|
847
917
|
clientBundle,
|
|
848
918
|
clientBundleBase64: clientBundle.toString("base64"),
|
|
849
|
-
clientBundleHash
|
|
919
|
+
clientBundleHash,
|
|
920
|
+
serverEnv
|
|
850
921
|
};
|
|
851
922
|
}
|
|
852
923
|
|
|
@@ -1100,6 +1171,7 @@ function createSourceQuery(rows, tableName) {
|
|
|
1100
1171
|
async function createSourceContext({ artifact, auth, deployId, state }) {
|
|
1101
1172
|
const rows = {};
|
|
1102
1173
|
const operations = [];
|
|
1174
|
+
const env = typeof state.getServerEnv === "function" ? await state.getServerEnv(deployId) : {};
|
|
1103
1175
|
for (const tableName of Object.keys(artifact.server.schema ?? {})) {
|
|
1104
1176
|
rows[tableName] = await state.listRows(deployId, tableName);
|
|
1105
1177
|
}
|
|
@@ -1141,7 +1213,7 @@ async function createSourceContext({ artifact, auth, deployId, state }) {
|
|
|
1141
1213
|
ctx: {
|
|
1142
1214
|
auth,
|
|
1143
1215
|
db,
|
|
1144
|
-
env
|
|
1216
|
+
env,
|
|
1145
1217
|
log: {
|
|
1146
1218
|
error() {},
|
|
1147
1219
|
info() {},
|
package/src/cli.js
CHANGED
|
@@ -9,15 +9,18 @@ import { WebSocketServer } from "ws";
|
|
|
9
9
|
import {
|
|
10
10
|
ANONYMOUS_ARTIFACT_MEDIA_TYPE,
|
|
11
11
|
AnonymousCompilerError,
|
|
12
|
+
LAKEBED_VERSION,
|
|
13
|
+
SERVER_ENV_FILE,
|
|
12
14
|
createAnonymousArtifact,
|
|
13
15
|
createClaimedArtifact,
|
|
14
16
|
parseTtlSeconds,
|
|
15
|
-
stableStringify
|
|
17
|
+
stableStringify,
|
|
18
|
+
validateServerEnvValues
|
|
16
19
|
} from "./anonymous.js";
|
|
17
20
|
import { startAnonymousServer } from "./anonymous-server.js";
|
|
18
21
|
import { authFromUrl as resolveAuthFromUrl, createGuestAuth, requestOrigin, shooBaseUrlFromEnv } from "./auth.js";
|
|
19
22
|
import { LogBuffer, StateCell } from "./runtime.js";
|
|
20
|
-
import { createMemorySourceStoreFromDirectory, sourcePathDirname, sourcePathJoin } from "./source-store.js";
|
|
23
|
+
import { MemorySourceStore, createMemorySourceStoreFromDirectory, sourcePathDirname, sourcePathJoin } from "./source-store.js";
|
|
21
24
|
|
|
22
25
|
const root = process.cwd();
|
|
23
26
|
const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -220,29 +223,45 @@ function createSourcePlugin(sourceStore, target) {
|
|
|
220
223
|
};
|
|
221
224
|
}
|
|
222
225
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
return env;
|
|
226
|
+
function unquoteServerEnvValue(value) {
|
|
227
|
+
if (value.length < 2) {
|
|
228
|
+
return value;
|
|
227
229
|
}
|
|
228
230
|
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
+
const quote = value[0];
|
|
232
|
+
if ((quote !== `"` && quote !== `'`) || value[value.length - 1] !== quote) {
|
|
233
|
+
return value;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return value.slice(1, -1);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function parseServerEnvFile(contents) {
|
|
240
|
+
const env = {};
|
|
241
|
+
for (const [index, rawLine] of contents.split(/\r?\n/).entries()) {
|
|
231
242
|
const line = rawLine.trim();
|
|
232
243
|
if (!line || line.startsWith("#")) {
|
|
233
244
|
continue;
|
|
234
245
|
}
|
|
235
246
|
|
|
236
|
-
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
247
|
+
const match = line.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
237
248
|
if (!match) {
|
|
238
|
-
|
|
249
|
+
throw new Error(`Invalid ${SERVER_ENV_FILE} line ${index + 1}. Use KEY=value.`);
|
|
239
250
|
}
|
|
240
251
|
|
|
241
252
|
const [, key, rawValue] = match;
|
|
242
|
-
env[key] = rawValue.
|
|
253
|
+
env[key] = unquoteServerEnvValue(rawValue.trim());
|
|
243
254
|
}
|
|
244
255
|
|
|
245
|
-
return env;
|
|
256
|
+
return validateServerEnvValues(env, SERVER_ENV_FILE);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function readCapsuleServerEnv(sourceStore) {
|
|
260
|
+
if (!sourceStore.hasFile(SERVER_ENV_FILE)) {
|
|
261
|
+
return {};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return parseServerEnvFile(await sourceStore.readFile(SERVER_ENV_FILE));
|
|
246
265
|
}
|
|
247
266
|
|
|
248
267
|
export async function buildCapsule({ capsuleDir, sourceStore, capsuleId = "dev" } = {}) {
|
|
@@ -300,7 +319,7 @@ export async function buildCapsule({ capsuleDir, sourceStore, capsuleId = "dev"
|
|
|
300
319
|
app: capsuleModule.default,
|
|
301
320
|
buildDir,
|
|
302
321
|
clientOut,
|
|
303
|
-
env: await
|
|
322
|
+
env: await readCapsuleServerEnv(workingStore),
|
|
304
323
|
serverOut,
|
|
305
324
|
sourceStore: workingStore
|
|
306
325
|
};
|
|
@@ -578,9 +597,9 @@ async function dev(args) {
|
|
|
578
597
|
});
|
|
579
598
|
}
|
|
580
599
|
|
|
581
|
-
async function buildAnonymousEnvelope(capsuleArg) {
|
|
600
|
+
async function buildAnonymousEnvelope(capsuleArg, sourceStore) {
|
|
582
601
|
const capsuleDir = resolveCapsuleDir(capsuleArg);
|
|
583
|
-
|
|
602
|
+
sourceStore ??= await createMemorySourceStoreFromDirectory(capsuleDir);
|
|
584
603
|
const built = await buildCapsule({
|
|
585
604
|
capsuleDir,
|
|
586
605
|
capsuleId: `anonymous-${Date.now()}`,
|
|
@@ -601,9 +620,74 @@ async function buildAnonymousEnvelope(capsuleArg) {
|
|
|
601
620
|
};
|
|
602
621
|
}
|
|
603
622
|
|
|
604
|
-
|
|
623
|
+
const claimRequiredDiagnosticMessages = new Set([
|
|
624
|
+
"Outbound fetch is disabled for anonymous deploys.",
|
|
625
|
+
"Async server handlers are not part of the anonymous IR yet. Use synchronous Lakebed database operations."
|
|
626
|
+
]);
|
|
627
|
+
|
|
628
|
+
function canDeployAfterClaim(error) {
|
|
629
|
+
return (
|
|
630
|
+
error instanceof AnonymousCompilerError &&
|
|
631
|
+
error.diagnostics.length > 0 &&
|
|
632
|
+
error.diagnostics.every((entry) => claimRequiredDiagnosticMessages.has(entry.message))
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
async function buildClaimRequiredEnvelope({ capsuleDir, feature = "claimed server features" }) {
|
|
637
|
+
const sourceStore = new MemorySourceStore();
|
|
638
|
+
await sourceStore.writeFile(
|
|
639
|
+
"server/index.ts",
|
|
640
|
+
`import { capsule } from "lakebed/server";
|
|
641
|
+
|
|
642
|
+
export default capsule({
|
|
643
|
+
name: "Claim Required",
|
|
644
|
+
schema: {},
|
|
645
|
+
queries: {},
|
|
646
|
+
mutations: {}
|
|
647
|
+
});
|
|
648
|
+
`
|
|
649
|
+
);
|
|
650
|
+
await sourceStore.writeFile(
|
|
651
|
+
"client/index.tsx",
|
|
652
|
+
`export function App() {
|
|
653
|
+
return (
|
|
654
|
+
<main className="min-h-screen bg-neutral-950 px-6 py-12 text-neutral-100">
|
|
655
|
+
<section className="mx-auto max-w-2xl rounded-lg border border-neutral-800 bg-neutral-900 p-8 shadow-2xl">
|
|
656
|
+
<p className="text-sm font-semibold uppercase tracking-wide text-cyan-300">Lakebed deploy</p>
|
|
657
|
+
<h1 className="mt-3 text-3xl font-semibold">Claim required</h1>
|
|
658
|
+
<p className="mt-4 text-neutral-300">
|
|
659
|
+
This capsule uses ${feature}. Claim this deploy, then run lakebed deploy again to publish the app.
|
|
660
|
+
</p>
|
|
661
|
+
</section>
|
|
662
|
+
</main>
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
`
|
|
666
|
+
);
|
|
667
|
+
const built = await buildCapsule({
|
|
668
|
+
capsuleDir,
|
|
669
|
+
capsuleId: `claim-required-${Date.now()}`,
|
|
670
|
+
sourceStore
|
|
671
|
+
});
|
|
672
|
+
const artifact = await createAnonymousArtifact({
|
|
673
|
+
app: built.app,
|
|
674
|
+
clientOut: built.clientOut,
|
|
675
|
+
sourceStore
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
return {
|
|
679
|
+
artifact: artifact.artifact,
|
|
680
|
+
artifactHash: artifact.artifactHash,
|
|
681
|
+
clientBundle: artifact.clientBundle,
|
|
682
|
+
clientBundleHash: artifact.clientBundleHash,
|
|
683
|
+
claimRequired: true,
|
|
684
|
+
mediaType: ANONYMOUS_ARTIFACT_MEDIA_TYPE
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
async function buildClaimedEnvelope(capsuleArg, sourceStore) {
|
|
605
689
|
const capsuleDir = resolveCapsuleDir(capsuleArg);
|
|
606
|
-
|
|
690
|
+
sourceStore ??= await createMemorySourceStoreFromDirectory(capsuleDir);
|
|
607
691
|
const built = await buildCapsule({
|
|
608
692
|
capsuleDir,
|
|
609
693
|
capsuleId: `claimed-${Date.now()}`,
|
|
@@ -677,13 +761,21 @@ function claimUrlFromDeployMetadata(metadata) {
|
|
|
677
761
|
return `${String(metadata.api).replace(/\/+$/g, "")}/claim/${encodeURIComponent(metadata.deployId)}/${encodeURIComponent(metadata.claimToken)}`;
|
|
678
762
|
}
|
|
679
763
|
|
|
680
|
-
function deployRequestBody(envelope, ttl) {
|
|
681
|
-
|
|
764
|
+
function deployRequestBody(envelope, ttl, { serverEnv } = {}) {
|
|
765
|
+
const body = {
|
|
682
766
|
artifact: envelope.artifact,
|
|
683
767
|
clientBundle: envelope.clientBundle,
|
|
684
|
-
clientVersion:
|
|
768
|
+
clientVersion: LAKEBED_VERSION,
|
|
685
769
|
requestedTtlSeconds: ttl
|
|
686
|
-
}
|
|
770
|
+
};
|
|
771
|
+
if (serverEnv !== undefined) {
|
|
772
|
+
body.serverEnv = {
|
|
773
|
+
mode: "replace",
|
|
774
|
+
values: serverEnv
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
return JSON.stringify(body);
|
|
687
779
|
}
|
|
688
780
|
|
|
689
781
|
async function buildCommand(args) {
|
|
@@ -735,6 +827,11 @@ async function readResponseJson(response) {
|
|
|
735
827
|
async function deployCommand(args) {
|
|
736
828
|
const [capsuleArg] = positionals(args);
|
|
737
829
|
const capsuleDir = capsuleArg ? resolveCapsuleDir(capsuleArg) : root;
|
|
830
|
+
const sourceStore = await createMemorySourceStoreFromDirectory(capsuleDir);
|
|
831
|
+
const serverEnvFileExists = sourceStore.hasFile(SERVER_ENV_FILE);
|
|
832
|
+
const serverEnv = await readCapsuleServerEnv(sourceStore);
|
|
833
|
+
const serverEnvKeys = Object.keys(serverEnv).sort();
|
|
834
|
+
const hasServerEnvValues = serverEnvKeys.length > 0;
|
|
738
835
|
const ttl = parseTtlSeconds(readArg(args, "--ttl", "7d"));
|
|
739
836
|
const api = deployApiUrl(args);
|
|
740
837
|
const metadata = await readDeployMetadata(capsuleDir);
|
|
@@ -749,23 +846,45 @@ async function deployCommand(args) {
|
|
|
749
846
|
}
|
|
750
847
|
|
|
751
848
|
let envelope;
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
} catch (error) {
|
|
755
|
-
if (error instanceof AnonymousCompilerError && canUpdate && !currentDeploy?.claimed) {
|
|
849
|
+
if (!currentDeploy?.claimed && hasServerEnvValues) {
|
|
850
|
+
if (canUpdate && currentDeploy) {
|
|
756
851
|
throw new Error(
|
|
757
|
-
|
|
852
|
+
`This capsule defines server env in ${SERVER_ENV_FILE}.\n\nThis deploy is still anonymous. Claim it first, then run lakebed deploy again to sync server env.`
|
|
758
853
|
);
|
|
759
854
|
}
|
|
760
|
-
|
|
855
|
+
try {
|
|
856
|
+
await buildAnonymousEnvelope(capsuleDir, sourceStore);
|
|
857
|
+
} catch (error) {
|
|
858
|
+
if (!canDeployAfterClaim(error)) {
|
|
859
|
+
throw error;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
envelope = await buildClaimRequiredEnvelope({ capsuleDir, feature: "server env" });
|
|
863
|
+
} else {
|
|
864
|
+
try {
|
|
865
|
+
envelope = currentDeploy?.claimed ? await buildClaimedEnvelope(capsuleDir, sourceStore) : await buildAnonymousEnvelope(capsuleDir, sourceStore);
|
|
866
|
+
} catch (error) {
|
|
867
|
+
if (error instanceof AnonymousCompilerError && canUpdate && currentDeploy && !currentDeploy.claimed) {
|
|
868
|
+
throw new Error(
|
|
869
|
+
`${error.message}\n\nThis deploy is still anonymous. Claim it first, then run lakebed deploy again to use server-side fetch.`
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
if ((!canUpdate || !currentDeploy) && canDeployAfterClaim(error)) {
|
|
873
|
+
envelope = await buildClaimRequiredEnvelope({ capsuleDir, feature: "server-side fetch" });
|
|
874
|
+
} else {
|
|
875
|
+
throw error;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
761
878
|
}
|
|
762
|
-
const
|
|
879
|
+
const syncServerEnv = Boolean(currentDeploy?.claimed && serverEnvFileExists);
|
|
880
|
+
const serverEnvForUpdate = syncServerEnv ? serverEnv : undefined;
|
|
881
|
+
let serverEnvSynced = false;
|
|
763
882
|
let mode = "created";
|
|
764
883
|
let response;
|
|
765
884
|
|
|
766
885
|
if (canUpdate) {
|
|
767
886
|
response = await fetch(`${api}/v1/deploys/${encodeURIComponent(metadata.deployId)}`, {
|
|
768
|
-
body,
|
|
887
|
+
body: deployRequestBody(envelope, ttl, { serverEnv: serverEnvForUpdate }),
|
|
769
888
|
headers: {
|
|
770
889
|
"Authorization": `Bearer ${metadata.claimToken}`,
|
|
771
890
|
"Content-Type": "application/json"
|
|
@@ -776,13 +895,17 @@ async function deployCommand(args) {
|
|
|
776
895
|
if (response.status === 404 || response.status === 410) {
|
|
777
896
|
mode = "created";
|
|
778
897
|
response = null;
|
|
898
|
+
if (serverEnvForUpdate !== undefined && hasServerEnvValues) {
|
|
899
|
+
envelope = await buildClaimRequiredEnvelope({ capsuleDir, feature: "server env" });
|
|
900
|
+
}
|
|
779
901
|
} else {
|
|
780
902
|
mode = "updated";
|
|
903
|
+
serverEnvSynced = serverEnvForUpdate !== undefined;
|
|
781
904
|
}
|
|
782
905
|
}
|
|
783
906
|
|
|
784
907
|
response ??= await fetch(`${api}/v1/anonymous-deploys`, {
|
|
785
|
-
body,
|
|
908
|
+
body: deployRequestBody(envelope, ttl),
|
|
786
909
|
headers: {
|
|
787
910
|
"Content-Type": "application/json"
|
|
788
911
|
},
|
|
@@ -801,11 +924,24 @@ async function deployCommand(args) {
|
|
|
801
924
|
}
|
|
802
925
|
|
|
803
926
|
if (hasFlag(args, "--json")) {
|
|
804
|
-
console.log(
|
|
927
|
+
console.log(
|
|
928
|
+
JSON.stringify(
|
|
929
|
+
{
|
|
930
|
+
...(envelope.claimRequired ? { ...deployed, claimRequired: true } : deployed),
|
|
931
|
+
...(serverEnvSynced ? { serverEnv: { keys: serverEnvKeys, mode: "replace", synced: true } } : {})
|
|
932
|
+
},
|
|
933
|
+
null,
|
|
934
|
+
2
|
|
935
|
+
)
|
|
936
|
+
);
|
|
805
937
|
return;
|
|
806
938
|
}
|
|
807
939
|
|
|
808
|
-
|
|
940
|
+
if (envelope.claimRequired && mode !== "updated") {
|
|
941
|
+
console.log("Created claim-required preview.\n");
|
|
942
|
+
} else {
|
|
943
|
+
console.log(`${mode === "updated" ? "Updated" : "Created"} anonymous preview.\n`);
|
|
944
|
+
}
|
|
809
945
|
console.log(`App: ${deployed.url}`);
|
|
810
946
|
console.log(`Expires: ${deployed.expiresAt}`);
|
|
811
947
|
if (deployed.claimUrl) {
|
|
@@ -818,6 +954,16 @@ async function deployCommand(args) {
|
|
|
818
954
|
console.log(` requests: ${deployed.limits.requestsPerDay} / day`);
|
|
819
955
|
console.log(` mutations: ${deployed.limits.mutationsPerDay} / day`);
|
|
820
956
|
console.log(` outbound fetch: ${envelope.artifact.deployTarget === "claimed-source" ? "enabled" : "disabled"}`);
|
|
957
|
+
if (serverEnvSynced) {
|
|
958
|
+
console.log(`\nServer env: ${serverEnvKeys.length} synced`);
|
|
959
|
+
for (const key of serverEnvKeys) {
|
|
960
|
+
console.log(` ${key}`);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
if (envelope.claimRequired) {
|
|
964
|
+
console.log("\nThis app needs a claimed deploy before server-side fetch or server env can run.");
|
|
965
|
+
console.log("Open the claim URL, then run lakebed deploy again.");
|
|
966
|
+
}
|
|
821
967
|
}
|
|
822
968
|
|
|
823
969
|
async function claimCommand(args) {
|
|
@@ -1028,6 +1174,7 @@ lakebed logs --port 3000
|
|
|
1028
1174
|
- Use Tailwind classes directly in JSX.
|
|
1029
1175
|
- Do not add a CSS, PostCSS, or Tailwind build pipeline.
|
|
1030
1176
|
- Use auth through \`ctx.auth\` on the server and \`useAuth()\` on the client.
|
|
1177
|
+
- Read server-only environment variables through \`ctx.env\`; define them in \`.env.lakebed.server\`.
|
|
1031
1178
|
- Add Google sign-in with \`<SignInWithGoogle />\` or \`signInWithGoogle()\` from \`lakebed/client\`.
|
|
1032
1179
|
- Keep \`shared/\` free of DOM, Node, env, and Lakebed runtime imports.
|
|
1033
1180
|
|
|
@@ -1038,6 +1185,7 @@ lakebed logs --port 3000
|
|
|
1038
1185
|
- Guest auth locally, with built-in Google sign-in through Shoo.
|
|
1039
1186
|
- No file storage.
|
|
1040
1187
|
- No outbound fetch in anonymous deploys. Claim the deploy before using server-side fetch.
|
|
1188
|
+
- Non-empty \`.env.lakebed.server\` files sync only after a deploy is claimed.
|
|
1041
1189
|
- Local state resets when \`lakebed dev\` restarts.
|
|
1042
1190
|
`;
|
|
1043
1191
|
}
|
|
@@ -1145,6 +1293,7 @@ export function cleanTodoText(value: string): string {
|
|
|
1145
1293
|
}
|
|
1146
1294
|
`,
|
|
1147
1295
|
".gitignore": `.lakebed/
|
|
1296
|
+
.env.lakebed.server
|
|
1148
1297
|
`,
|
|
1149
1298
|
"README.md": `# ${title}
|
|
1150
1299
|
|
package/src/version.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const LAKEBED_VERSION = "0.0.6";
|