lakebed 0.0.16 → 0.0.17
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 +7 -5
- package/package.json +1 -1
- package/src/anonymous-server.js +266 -36
- package/src/anonymous.js +124 -39
- package/src/cli.js +94 -16
- package/src/source-runtime-worker.js +5 -0
- package/src/source-runtime.js +2 -0
- package/src/version.js +1 -1
package/README.md
CHANGED
|
@@ -122,14 +122,14 @@ lakebed new [name] [--template todo] [--no-git]
|
|
|
122
122
|
lakebed create [name] [--template todo] [--no-git]
|
|
123
123
|
lakebed dev [capsule-dir] [--port 3000]
|
|
124
124
|
lakebed build [capsule-dir] --target anonymous [--out .lakebed/artifacts/app.json] [--json]
|
|
125
|
-
lakebed deploy [capsule-dir] [--api <url>] [--json]
|
|
125
|
+
lakebed deploy [capsule-dir] [--api <url>] [--public-inspect] [--json]
|
|
126
126
|
lakebed claim [capsule-dir] [--api <url>] [--json]
|
|
127
127
|
lakebed domains add <subdomain.lakebed.app> [--api <url>] [--json]
|
|
128
128
|
lakebed anonymous-server [--port 8787] [--public-root-url <url>] [--app-base-domain <domain>]
|
|
129
|
-
lakebed inspect <deploy-id-or-url> [--api <url>] [--json]
|
|
130
|
-
lakebed db list [deploy-id-or-url] [--port 3000]
|
|
131
|
-
lakebed db dump [deploy-id-or-url] [--port 3000]
|
|
132
|
-
lakebed logs [deploy-id-or-url] [--port 3000]
|
|
129
|
+
lakebed inspect <deploy-id-or-url> [--api <url>] [--inspect-token <token>] [--json]
|
|
130
|
+
lakebed db list [deploy-id-or-url] [--port 3000] [--inspect-token <token>]
|
|
131
|
+
lakebed db dump [deploy-id-or-url] [--port 3000] [--inspect-token <token>]
|
|
132
|
+
lakebed logs [deploy-id-or-url] [--port 3000] [--inspect-token <token>]
|
|
133
133
|
```
|
|
134
134
|
|
|
135
135
|
## Current Constraints
|
|
@@ -199,6 +199,8 @@ LAKEBED_SERVER_ENV_SECRET=...
|
|
|
199
199
|
|
|
200
200
|
Claimed deploys are listed at `/deploys` on the deploy API origin. They keep the same resource limits as anonymous deploys and do not expire. 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 `lakebed claim` command. Run that command, 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.
|
|
201
201
|
|
|
202
|
+
Hosted inspection is private by default. `lakebed inspect`, `lakebed db list`, `lakebed db dump`, and `lakebed logs` send the saved claim token automatically when run from the capsule directory.
|
|
203
|
+
|
|
202
204
|
After a deploy is claimed, reserve a Lakebed-owned app subdomain from the capsule directory:
|
|
203
205
|
|
|
204
206
|
```sh
|
package/package.json
CHANGED
package/src/anonymous-server.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
createDeployId,
|
|
8
8
|
createSlug,
|
|
9
9
|
DEFAULT_ANONYMOUS_LIMITS,
|
|
10
|
+
LAKEBED_VERSION,
|
|
10
11
|
executeAnonymousMutation,
|
|
11
12
|
executeAnonymousQuery,
|
|
12
13
|
hashClaimToken,
|
|
@@ -151,6 +152,79 @@ function isDeployTokenValid(deploy, token) {
|
|
|
151
152
|
return Boolean(token && deploy?.claimTokenHash && hashClaimToken(token) === deploy.claimTokenHash);
|
|
152
153
|
}
|
|
153
154
|
|
|
155
|
+
const inspectPolicies = new Set(["private", "redacted", "public"]);
|
|
156
|
+
|
|
157
|
+
function normalizeInspectPolicy(value, fallback = "private") {
|
|
158
|
+
if (value === undefined || value === null || value === "") {
|
|
159
|
+
return fallback;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const policy = String(value).trim().toLowerCase();
|
|
163
|
+
if (!inspectPolicies.has(policy)) {
|
|
164
|
+
throw new Error("inspectPolicy must be private, redacted, or public.");
|
|
165
|
+
}
|
|
166
|
+
return policy;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function inspectPolicyForDeploy(deploy) {
|
|
170
|
+
return normalizeInspectPolicy(deploy?.inspectPolicy, "private");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const sensitiveKeyPattern =
|
|
174
|
+
/(^|[_-])(authorization|bearer|cookie|jwt|password|secret|session|token|api[_-]?key|access[_-]?key|private[_-]?key|refresh[_-]?token)([_-]|$)/i;
|
|
175
|
+
const secretValuePatterns = [
|
|
176
|
+
/\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi,
|
|
177
|
+
/\b(sk|pk|rk|ghp|gho|github_pat)_[A-Za-z0-9_]{12,}\b/g,
|
|
178
|
+
/\b[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
function redactSensitiveString(value) {
|
|
182
|
+
return secretValuePatterns.reduce((text, pattern) => text.replace(pattern, "[redacted]"), String(value));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function isSensitiveKey(key) {
|
|
186
|
+
const normalized = String(key).replace(/([a-z0-9])([A-Z])/g, "$1_$2");
|
|
187
|
+
return sensitiveKeyPattern.test(normalized);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function redactInspectValue(value, seen = new WeakSet()) {
|
|
191
|
+
if (typeof value === "string") {
|
|
192
|
+
return redactSensitiveString(value);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!value || typeof value !== "object") {
|
|
196
|
+
return value;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (seen.has(value)) {
|
|
200
|
+
return "[redacted circular]";
|
|
201
|
+
}
|
|
202
|
+
seen.add(value);
|
|
203
|
+
|
|
204
|
+
if (Array.isArray(value)) {
|
|
205
|
+
return value.map((entry) => redactInspectValue(entry, seen));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return Object.fromEntries(
|
|
209
|
+
Object.entries(value).map(([key, entryValue]) => [
|
|
210
|
+
key,
|
|
211
|
+
isSensitiveKey(key) ? "[redacted]" : redactInspectValue(entryValue, seen)
|
|
212
|
+
])
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function redactLogEntry(entry) {
|
|
217
|
+
return {
|
|
218
|
+
...entry,
|
|
219
|
+
data: redactInspectValue(entry.data),
|
|
220
|
+
message: redactSensitiveString(entry.message)
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function redactLogData(data) {
|
|
225
|
+
return redactInspectValue(data);
|
|
226
|
+
}
|
|
227
|
+
|
|
154
228
|
function normalizePublicRootUrl(value, port) {
|
|
155
229
|
const fallback = `http://localhost:${port}`;
|
|
156
230
|
return String(value || fallback).replace(/\/+$/g, "");
|
|
@@ -296,6 +370,7 @@ function responseForDeploy({ deploy, token }) {
|
|
|
296
370
|
deployId: deploy.id,
|
|
297
371
|
expiresAt: deploy.expiresAt,
|
|
298
372
|
inspect: inspectUrls(deploy.url),
|
|
373
|
+
inspectPolicy: inspectPolicyForDeploy(deploy),
|
|
299
374
|
limits: deploy.limits,
|
|
300
375
|
updatedAt: deploy.updatedAt,
|
|
301
376
|
url: deploy.url
|
|
@@ -698,7 +773,7 @@ function isSecureRequest(req) {
|
|
|
698
773
|
}
|
|
699
774
|
|
|
700
775
|
function adminCookie(value, maxAge = adminCookieMaxAgeSeconds, secure = false) {
|
|
701
|
-
return `${adminCookieName}=${value}; HttpOnly; SameSite=Lax; Path
|
|
776
|
+
return `${adminCookieName}=${value}; HttpOnly; SameSite=Lax; Path=/; Max-Age=${maxAge}${secure ? "; Secure" : ""}`;
|
|
702
777
|
}
|
|
703
778
|
|
|
704
779
|
function cookie(name, value, { httpOnly = true, maxAge, path = "/", sameSite = "Lax", secure = false } = {}) {
|
|
@@ -1130,6 +1205,7 @@ function adminDeploySummary({ artifact, artifactBytes = 0, deploy, logBytes = 0,
|
|
|
1130
1205
|
createdAt: deploy.createdAt,
|
|
1131
1206
|
expiresAt: deploy.expiresAt,
|
|
1132
1207
|
id: deploy.id,
|
|
1208
|
+
inspectPolicy: inspectPolicyForDeploy(deploy),
|
|
1133
1209
|
limits: deploy.limits,
|
|
1134
1210
|
logBytes,
|
|
1135
1211
|
logEntries,
|
|
@@ -2777,6 +2853,7 @@ function developerDeploySummary({ artifact, deploy, usage }) {
|
|
|
2777
2853
|
deployId: deploy.id,
|
|
2778
2854
|
expiresAt: deploy.expiresAt,
|
|
2779
2855
|
inspect: inspectUrls(deploy.url),
|
|
2856
|
+
inspectPolicy: inspectPolicyForDeploy(deploy),
|
|
2780
2857
|
limits: deploy.limits,
|
|
2781
2858
|
name: artifact?.name ?? "Lakebed Capsule",
|
|
2782
2859
|
ownerId: deploy.ownerId,
|
|
@@ -3389,7 +3466,7 @@ export class MemoryAnonymousStore {
|
|
|
3389
3466
|
return false;
|
|
3390
3467
|
}
|
|
3391
3468
|
|
|
3392
|
-
async createDeploy({ appBaseDomain, artifact, artifactHash, clientBundleBase64, clientBundleHash, publicRootUrl, serverEnv }) {
|
|
3469
|
+
async createDeploy({ appBaseDomain, artifact, artifactHash, clientBundleBase64, clientBundleHash, inspectPolicy, publicRootUrl, serverEnv }) {
|
|
3393
3470
|
const deployId = createDeployId();
|
|
3394
3471
|
const normalizedAppBaseDomain = normalizeAppBaseDomain(appBaseDomain);
|
|
3395
3472
|
let slug = createSlug();
|
|
@@ -3422,6 +3499,7 @@ export class MemoryAnonymousStore {
|
|
|
3422
3499
|
createdAt,
|
|
3423
3500
|
expiresAt,
|
|
3424
3501
|
id: deployId,
|
|
3502
|
+
inspectPolicy: normalizeInspectPolicy(inspectPolicy),
|
|
3425
3503
|
limits: { ...DEFAULT_ANONYMOUS_LIMITS },
|
|
3426
3504
|
owner: null,
|
|
3427
3505
|
ownerId: null,
|
|
@@ -3446,6 +3524,7 @@ export class MemoryAnonymousStore {
|
|
|
3446
3524
|
clientBundleBase64,
|
|
3447
3525
|
clientBundleHash,
|
|
3448
3526
|
deployId,
|
|
3527
|
+
inspectPolicy,
|
|
3449
3528
|
publicRootUrl,
|
|
3450
3529
|
serverEnv
|
|
3451
3530
|
}) {
|
|
@@ -3463,6 +3542,7 @@ export class MemoryAnonymousStore {
|
|
|
3463
3542
|
artifactHash,
|
|
3464
3543
|
clientBundleHash,
|
|
3465
3544
|
expiresAt: currentDeploy.ownerId ? null : anonymousDeployExpiresAt(),
|
|
3545
|
+
inspectPolicy: inspectPolicy === undefined ? inspectPolicyForDeploy(currentDeploy) : normalizeInspectPolicy(inspectPolicy),
|
|
3466
3546
|
publicRootUrl: nextPublicRootUrl,
|
|
3467
3547
|
url: appUrlForSlug({ appBaseDomain: nextAppBaseDomain, publicRootUrl: nextPublicRootUrl, slug: currentDeploy.slug }),
|
|
3468
3548
|
status: "active",
|
|
@@ -3692,7 +3772,7 @@ export class MemoryAnonymousStore {
|
|
|
3692
3772
|
}
|
|
3693
3773
|
|
|
3694
3774
|
async appendLog(deployId, level, message, data) {
|
|
3695
|
-
const entries = [...(this.logs.get(deployId) ?? []), normalizeLogEntry(level, message, data)];
|
|
3775
|
+
const entries = [...(this.logs.get(deployId) ?? []), redactLogEntry(normalizeLogEntry(level, message, data))];
|
|
3696
3776
|
const maxEntries = DEFAULT_ANONYMOUS_LIMITS.logEntries;
|
|
3697
3777
|
const maxBytes = DEFAULT_ANONYMOUS_LIMITS.logBytes;
|
|
3698
3778
|
while (entries.length > maxEntries || bytesOfJson(entries) > maxBytes) {
|
|
@@ -3702,7 +3782,7 @@ export class MemoryAnonymousStore {
|
|
|
3702
3782
|
}
|
|
3703
3783
|
|
|
3704
3784
|
async readLogs(deployId, limit = 100) {
|
|
3705
|
-
return (this.logs.get(deployId) ?? []).slice(-limit);
|
|
3785
|
+
return (this.logs.get(deployId) ?? []).slice(-limit).map(redactLogEntry);
|
|
3706
3786
|
}
|
|
3707
3787
|
|
|
3708
3788
|
async tableCounts(deployId, schema) {
|
|
@@ -4061,6 +4141,7 @@ export class PostgresAnonymousStore {
|
|
|
4061
4141
|
owner_id text,
|
|
4062
4142
|
owner_json jsonb,
|
|
4063
4143
|
claim_token_hash text not null,
|
|
4144
|
+
inspect_policy text not null default 'private',
|
|
4064
4145
|
limits_json jsonb not null,
|
|
4065
4146
|
counters_json jsonb not null default '{}',
|
|
4066
4147
|
public_root_url text not null,
|
|
@@ -4071,6 +4152,7 @@ export class PostgresAnonymousStore {
|
|
|
4071
4152
|
await this.query("alter table deploys add column if not exists claimed_at timestamptz");
|
|
4072
4153
|
await this.query("alter table deploys add column if not exists owner_id text");
|
|
4073
4154
|
await this.query("alter table deploys add column if not exists owner_json jsonb");
|
|
4155
|
+
await this.query("alter table deploys add column if not exists inspect_policy text not null default 'private'");
|
|
4074
4156
|
await this.query("alter table deploys add column if not exists updated_at timestamptz");
|
|
4075
4157
|
await this.query("update deploys set updated_at = created_at where updated_at is null");
|
|
4076
4158
|
await this.query("alter table deploys alter column updated_at set not null");
|
|
@@ -4491,10 +4573,11 @@ export class PostgresAnonymousStore {
|
|
|
4491
4573
|
return true;
|
|
4492
4574
|
}
|
|
4493
4575
|
|
|
4494
|
-
async createDeploy({ appBaseDomain, artifact, artifactHash, clientBundleBase64, clientBundleHash, publicRootUrl, serverEnv }) {
|
|
4576
|
+
async createDeploy({ appBaseDomain, artifact, artifactHash, clientBundleBase64, clientBundleHash, inspectPolicy, publicRootUrl, serverEnv }) {
|
|
4495
4577
|
const createdAt = now();
|
|
4496
4578
|
const normalizedAppBaseDomain = normalizeAppBaseDomain(appBaseDomain);
|
|
4497
4579
|
const expiresAt = anonymousDeployExpiresAt();
|
|
4580
|
+
const normalizedInspectPolicy = normalizeInspectPolicy(inspectPolicy);
|
|
4498
4581
|
const token = createClaimToken();
|
|
4499
4582
|
const deployId = createDeployId();
|
|
4500
4583
|
|
|
@@ -4514,6 +4597,7 @@ export class PostgresAnonymousStore {
|
|
|
4514
4597
|
createdAt,
|
|
4515
4598
|
expiresAt,
|
|
4516
4599
|
id: deployId,
|
|
4600
|
+
inspectPolicy: normalizedInspectPolicy,
|
|
4517
4601
|
limits: { ...DEFAULT_ANONYMOUS_LIMITS },
|
|
4518
4602
|
owner: null,
|
|
4519
4603
|
ownerId: null,
|
|
@@ -4538,9 +4622,9 @@ export class PostgresAnonymousStore {
|
|
|
4538
4622
|
`
|
|
4539
4623
|
insert into deploys(
|
|
4540
4624
|
id, slug, status, artifact_hash, client_bundle_hash, created_at, updated_at, expires_at,
|
|
4541
|
-
claim_token_hash, limits_json, counters_json, public_root_url, app_base_domain, url
|
|
4625
|
+
claim_token_hash, inspect_policy, limits_json, counters_json, public_root_url, app_base_domain, url
|
|
4542
4626
|
)
|
|
4543
|
-
values($1, $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb, '{}'::jsonb, $
|
|
4627
|
+
values($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11::jsonb, '{}'::jsonb, $12, $13, $14)
|
|
4544
4628
|
`,
|
|
4545
4629
|
[
|
|
4546
4630
|
deploy.id,
|
|
@@ -4552,6 +4636,7 @@ export class PostgresAnonymousStore {
|
|
|
4552
4636
|
deploy.updatedAt,
|
|
4553
4637
|
deploy.expiresAt,
|
|
4554
4638
|
deploy.claimTokenHash,
|
|
4639
|
+
deploy.inspectPolicy,
|
|
4555
4640
|
JSON.stringify(deploy.limits),
|
|
4556
4641
|
deploy.publicRootUrl,
|
|
4557
4642
|
deploy.appBaseDomain,
|
|
@@ -4584,6 +4669,7 @@ export class PostgresAnonymousStore {
|
|
|
4584
4669
|
clientBundleBase64,
|
|
4585
4670
|
clientBundleHash,
|
|
4586
4671
|
deployId,
|
|
4672
|
+
inspectPolicy,
|
|
4587
4673
|
publicRootUrl,
|
|
4588
4674
|
serverEnv
|
|
4589
4675
|
}) {
|
|
@@ -4596,6 +4682,7 @@ export class PostgresAnonymousStore {
|
|
|
4596
4682
|
const expiresAt = currentDeploy.ownerId ? null : anonymousDeployExpiresAt();
|
|
4597
4683
|
const nextAppBaseDomain = normalizeAppBaseDomain(appBaseDomain ?? currentDeploy.appBaseDomain);
|
|
4598
4684
|
const nextPublicRootUrl = publicRootUrl ?? currentDeploy.publicRootUrl;
|
|
4685
|
+
const nextInspectPolicy = inspectPolicy === undefined ? inspectPolicyForDeploy(currentDeploy) : normalizeInspectPolicy(inspectPolicy);
|
|
4599
4686
|
const url = appUrlForSlug({ appBaseDomain: nextAppBaseDomain, publicRootUrl: nextPublicRootUrl, slug: currentDeploy.slug });
|
|
4600
4687
|
await this.storeArtifact({ artifact, artifactHash, clientBundleBase64, clientBundleHash, createdAt: updatedAt });
|
|
4601
4688
|
|
|
@@ -4609,11 +4696,12 @@ export class PostgresAnonymousStore {
|
|
|
4609
4696
|
public_root_url = $5,
|
|
4610
4697
|
app_base_domain = $6,
|
|
4611
4698
|
url = $7,
|
|
4612
|
-
|
|
4699
|
+
inspect_policy = $8,
|
|
4700
|
+
updated_at = $9
|
|
4613
4701
|
where id = $1
|
|
4614
4702
|
returning *
|
|
4615
4703
|
`,
|
|
4616
|
-
[deployId, artifactHash, clientBundleHash, expiresAt, nextPublicRootUrl, nextAppBaseDomain || null, url, updatedAt]
|
|
4704
|
+
[deployId, artifactHash, clientBundleHash, expiresAt, nextPublicRootUrl, nextAppBaseDomain || null, url, nextInspectPolicy, updatedAt]
|
|
4617
4705
|
);
|
|
4618
4706
|
if (serverEnv !== undefined) {
|
|
4619
4707
|
await this.replaceServerEnv(deployId, serverEnv, updatedAt);
|
|
@@ -4687,6 +4775,7 @@ export class PostgresAnonymousStore {
|
|
|
4687
4775
|
createdAt: new Date(row.created_at).toISOString(),
|
|
4688
4776
|
expiresAt: row.expires_at ? new Date(row.expires_at).toISOString() : null,
|
|
4689
4777
|
id: row.id,
|
|
4778
|
+
inspectPolicy: normalizeInspectPolicy(row.inspect_policy),
|
|
4690
4779
|
limits: { ...DEFAULT_ANONYMOUS_LIMITS, ...(row.limits_json ?? {}) },
|
|
4691
4780
|
owner: row.owner_json ?? null,
|
|
4692
4781
|
ownerId: row.owner_id ?? null,
|
|
@@ -4931,7 +5020,7 @@ export class PostgresAnonymousStore {
|
|
|
4931
5020
|
}
|
|
4932
5021
|
|
|
4933
5022
|
async appendLog(deployId, level, message, data) {
|
|
4934
|
-
const entry = normalizeLogEntry(level, message, data);
|
|
5023
|
+
const entry = redactLogEntry(normalizeLogEntry(level, message, data));
|
|
4935
5024
|
await this.query(
|
|
4936
5025
|
"insert into logs(deploy_id, level, message, data_json, created_at) values($1, $2, $3, $4::jsonb, $5)",
|
|
4937
5026
|
[deployId, entry.level, entry.message, JSON.stringify(entry.data ?? null), entry.at]
|
|
@@ -4962,7 +5051,7 @@ export class PostgresAnonymousStore {
|
|
|
4962
5051
|
"select level, message, data_json, created_at from logs where deploy_id = $1 order by sequence desc limit $2",
|
|
4963
5052
|
[deployId, limit]
|
|
4964
5053
|
);
|
|
4965
|
-
return result.rows.reverse().map((row) => ({
|
|
5054
|
+
return result.rows.reverse().map((row) => redactLogEntry({
|
|
4966
5055
|
at: new Date(row.created_at).toISOString(),
|
|
4967
5056
|
data: row.data_json,
|
|
4968
5057
|
level: row.level,
|
|
@@ -5369,49 +5458,164 @@ async function loadDeployByRoute({ appBaseDomain, host, store, url }) {
|
|
|
5369
5458
|
return { artifact: storedArtifact.artifact, basePath: route.basePath, deploy, route, storedArtifact };
|
|
5370
5459
|
}
|
|
5371
5460
|
|
|
5372
|
-
|
|
5461
|
+
function mutationInspectDetails(artifact) {
|
|
5462
|
+
return Object.entries(artifact.server?.mutations ?? {}).map(([name, mutation]) => {
|
|
5463
|
+
if (mutation?.op === "source") {
|
|
5464
|
+
return { guards: [], mode: "source-backed", name };
|
|
5465
|
+
}
|
|
5466
|
+
|
|
5467
|
+
const guards = [];
|
|
5468
|
+
for (const operation of mutation?.body ?? []) {
|
|
5469
|
+
for (const guard of operation.guards ?? []) {
|
|
5470
|
+
guards.push({
|
|
5471
|
+
equalsAuth: guard.equalsAuth,
|
|
5472
|
+
field: guard.field,
|
|
5473
|
+
operation: operation.op,
|
|
5474
|
+
table: operation.table
|
|
5475
|
+
});
|
|
5476
|
+
}
|
|
5477
|
+
}
|
|
5478
|
+
|
|
5479
|
+
return {
|
|
5480
|
+
guards,
|
|
5481
|
+
mode: guards.length > 0 ? "guarded-ir" : "interpreted-ir",
|
|
5482
|
+
name
|
|
5483
|
+
};
|
|
5484
|
+
});
|
|
5485
|
+
}
|
|
5486
|
+
|
|
5487
|
+
function publicManifestForDeploy({ artifact, deploy }) {
|
|
5488
|
+
return {
|
|
5489
|
+
clientBundleHash: deploy.clientBundleHash,
|
|
5490
|
+
deployId: deploy.id,
|
|
5491
|
+
name: artifact.name ?? "Lakebed Capsule",
|
|
5492
|
+
runtimeVersion: LAKEBED_VERSION
|
|
5493
|
+
};
|
|
5494
|
+
}
|
|
5495
|
+
|
|
5496
|
+
function fullManifestForDeploy({ artifact, deploy, domains = [] }) {
|
|
5497
|
+
return {
|
|
5498
|
+
artifactHash: deploy.artifactHash,
|
|
5499
|
+
clientBundleHash: deploy.clientBundleHash,
|
|
5500
|
+
deployId: deploy.id,
|
|
5501
|
+
domains,
|
|
5502
|
+
expiresAt: deploy.expiresAt,
|
|
5503
|
+
inspectPolicy: inspectPolicyForDeploy(deploy),
|
|
5504
|
+
limits: deploy.limits,
|
|
5505
|
+
mutationDetails: mutationInspectDetails(artifact),
|
|
5506
|
+
mutations: Object.keys(artifact.server.mutations ?? {}),
|
|
5507
|
+
name: artifact.name ?? "Lakebed Capsule",
|
|
5508
|
+
queries: Object.keys(artifact.server.queries ?? {}),
|
|
5509
|
+
runtimeVersion: LAKEBED_VERSION,
|
|
5510
|
+
schema: artifact.server.schema,
|
|
5511
|
+
slug: deploy.slug,
|
|
5512
|
+
updatedAt: deploy.updatedAt,
|
|
5513
|
+
url: deploy.url
|
|
5514
|
+
};
|
|
5515
|
+
}
|
|
5516
|
+
|
|
5517
|
+
function inspectCommandForPath(deploy, systemPath) {
|
|
5518
|
+
if (systemPath === "/__lakebed/db") {
|
|
5519
|
+
return `lakebed db dump ${deploy.id}`;
|
|
5520
|
+
}
|
|
5521
|
+
if (systemPath === "/__lakebed/db/tables") {
|
|
5522
|
+
return `lakebed db list ${deploy.id}`;
|
|
5523
|
+
}
|
|
5524
|
+
if (systemPath === "/__lakebed/logs") {
|
|
5525
|
+
return `lakebed logs ${deploy.id}`;
|
|
5526
|
+
}
|
|
5527
|
+
return `lakebed inspect ${deploy.id}`;
|
|
5528
|
+
}
|
|
5529
|
+
|
|
5530
|
+
function inspectAuthFailure(deploy, systemPath) {
|
|
5531
|
+
return {
|
|
5532
|
+
command: inspectCommandForPath(deploy, systemPath),
|
|
5533
|
+
error: "Lakebed hosted inspection requires authorization.",
|
|
5534
|
+
hint: "Run this command from the capsule directory so Lakebed can read .lakebed/deploy.json, or send Authorization: Bearer <claim-token>.",
|
|
5535
|
+
inspectPolicy: inspectPolicyForDeploy(deploy),
|
|
5536
|
+
path: systemPath
|
|
5537
|
+
};
|
|
5538
|
+
}
|
|
5539
|
+
|
|
5540
|
+
function inspectAuthorized({ adminPassword, currentDeveloper, deploy, req }) {
|
|
5541
|
+
if (inspectPolicyForDeploy(deploy) === "public") {
|
|
5542
|
+
return true;
|
|
5543
|
+
}
|
|
5544
|
+
|
|
5545
|
+
if (isDeployTokenValid(deploy, bearerToken(req))) {
|
|
5546
|
+
return true;
|
|
5547
|
+
}
|
|
5548
|
+
|
|
5549
|
+
if (isAdminAuthenticated(req, adminPassword)) {
|
|
5550
|
+
return true;
|
|
5551
|
+
}
|
|
5552
|
+
|
|
5553
|
+
const user = currentDeveloper(req);
|
|
5554
|
+
return Boolean(user?.id && deploy.ownerId && user.id === deploy.ownerId);
|
|
5555
|
+
}
|
|
5556
|
+
|
|
5557
|
+
async function serveInspect({ adminPassword, artifact, currentDeveloper, deploy, req, route, store, systemPath }, res) {
|
|
5558
|
+
const policy = inspectPolicyForDeploy(deploy);
|
|
5559
|
+
const authorized = inspectAuthorized({ adminPassword, currentDeveloper, deploy, req });
|
|
5560
|
+
|
|
5373
5561
|
if (systemPath === "/__lakebed/manifest") {
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
mutations: Object.keys(artifact.server.mutations ?? {}),
|
|
5385
|
-
name: artifact.name ?? "Lakebed Capsule",
|
|
5386
|
-
queries: Object.keys(artifact.server.queries ?? {}),
|
|
5387
|
-
schema: artifact.server.schema,
|
|
5388
|
-
slug: deploy.slug,
|
|
5389
|
-
updatedAt: deploy.updatedAt,
|
|
5390
|
-
url: deploy.url
|
|
5391
|
-
});
|
|
5562
|
+
if (!authorized && policy === "private") {
|
|
5563
|
+
sendJson(res, 401, inspectAuthFailure(deploy, systemPath));
|
|
5564
|
+
return true;
|
|
5565
|
+
}
|
|
5566
|
+
|
|
5567
|
+
const domains =
|
|
5568
|
+
typeof store.listDeployDomainsForDeploy === "function"
|
|
5569
|
+
? (await store.listDeployDomainsForDeploy(deploy.id)).map(responseForDeployDomain)
|
|
5570
|
+
: [];
|
|
5571
|
+
sendJson(res, 200, authorized ? fullManifestForDeploy({ artifact, deploy, domains }) : publicManifestForDeploy({ artifact, deploy }));
|
|
5392
5572
|
return true;
|
|
5393
5573
|
}
|
|
5394
5574
|
|
|
5395
5575
|
if (systemPath === "/__lakebed/db/tables") {
|
|
5396
5576
|
const counts = await store.tableCounts(deploy.id, artifact.server.schema);
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
}
|
|
5577
|
+
if (!authorized && policy !== "redacted") {
|
|
5578
|
+
sendJson(res, 401, inspectAuthFailure(deploy, systemPath));
|
|
5579
|
+
return true;
|
|
5580
|
+
}
|
|
5581
|
+
|
|
5582
|
+
sendJson(res, 200, authorized ? { tables: Object.keys(artifact.server.schema ?? {}), counts } : { counts, redacted: true });
|
|
5401
5583
|
return true;
|
|
5402
5584
|
}
|
|
5403
5585
|
|
|
5404
5586
|
if (systemPath === "/__lakebed/db") {
|
|
5587
|
+
if (!authorized && policy !== "redacted") {
|
|
5588
|
+
sendJson(res, 401, inspectAuthFailure(deploy, systemPath));
|
|
5589
|
+
return true;
|
|
5590
|
+
}
|
|
5591
|
+
|
|
5592
|
+
if (policy === "redacted" && !authorized) {
|
|
5593
|
+
const counts = await store.tableCounts(deploy.id, artifact.server.schema);
|
|
5594
|
+
sendJson(res, 200, { counts, redacted: true });
|
|
5595
|
+
return true;
|
|
5596
|
+
}
|
|
5597
|
+
|
|
5405
5598
|
sendJson(res, 200, await store.dumpState(deploy.id, artifact.server.schema, deploy.limits.rowsReturned));
|
|
5406
5599
|
return true;
|
|
5407
5600
|
}
|
|
5408
5601
|
|
|
5409
5602
|
if (systemPath === "/__lakebed/logs") {
|
|
5410
|
-
|
|
5603
|
+
if (!authorized && policy !== "redacted") {
|
|
5604
|
+
sendJson(res, 401, inspectAuthFailure(deploy, systemPath));
|
|
5605
|
+
return true;
|
|
5606
|
+
}
|
|
5607
|
+
|
|
5608
|
+
const logs = await store.readLogs(deploy.id, 100);
|
|
5609
|
+
sendJson(res, 200, policy === "redacted" && !authorized ? logs.map(redactLogEntry) : logs);
|
|
5411
5610
|
return true;
|
|
5412
5611
|
}
|
|
5413
5612
|
|
|
5414
5613
|
if (systemPath === "/__lakebed/usage") {
|
|
5614
|
+
if (!authorized && policy !== "redacted") {
|
|
5615
|
+
sendJson(res, 401, inspectAuthFailure(deploy, systemPath));
|
|
5616
|
+
return true;
|
|
5617
|
+
}
|
|
5618
|
+
|
|
5415
5619
|
sendJson(res, 200, {
|
|
5416
5620
|
limits: deploy.limits,
|
|
5417
5621
|
usage: await store.readUsage(deploy.id)
|
|
@@ -6073,6 +6277,13 @@ export async function startAnonymousServer({
|
|
|
6073
6277
|
await enforceAnonymousDeployCreation(req);
|
|
6074
6278
|
const body = await readJsonBody(req);
|
|
6075
6279
|
const payload = validateAnonymousDeployPayload(body);
|
|
6280
|
+
let inspectPolicy;
|
|
6281
|
+
try {
|
|
6282
|
+
inspectPolicy = normalizeInspectPolicy(body.inspectPolicy);
|
|
6283
|
+
} catch (error) {
|
|
6284
|
+
sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
6285
|
+
return;
|
|
6286
|
+
}
|
|
6076
6287
|
if (payload.serverEnv !== undefined && Object.keys(payload.serverEnv).length > 0) {
|
|
6077
6288
|
sendJson(res, 400, { error: "Server env requires a claimed deploy." });
|
|
6078
6289
|
return;
|
|
@@ -6083,6 +6294,7 @@ export async function startAnonymousServer({
|
|
|
6083
6294
|
artifactHash: payload.artifactHash,
|
|
6084
6295
|
clientBundleBase64: payload.clientBundleBase64,
|
|
6085
6296
|
clientBundleHash: payload.clientBundleHash,
|
|
6297
|
+
inspectPolicy,
|
|
6086
6298
|
publicRootUrl: resolvedPublicRootUrl,
|
|
6087
6299
|
serverEnv: payload.serverEnv
|
|
6088
6300
|
});
|
|
@@ -6106,6 +6318,13 @@ export async function startAnonymousServer({
|
|
|
6106
6318
|
|
|
6107
6319
|
const body = await readJsonBody(req);
|
|
6108
6320
|
const payload = validateAnonymousDeployPayload(body, { allowClaimedSource: Boolean(currentDeploy.ownerId) });
|
|
6321
|
+
let inspectPolicy;
|
|
6322
|
+
try {
|
|
6323
|
+
inspectPolicy = Object.hasOwn(body, "inspectPolicy") ? normalizeInspectPolicy(body.inspectPolicy) : undefined;
|
|
6324
|
+
} catch (error) {
|
|
6325
|
+
sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
6326
|
+
return;
|
|
6327
|
+
}
|
|
6109
6328
|
if (payload.serverEnv !== undefined && !currentDeploy.ownerId) {
|
|
6110
6329
|
sendJson(res, 400, { error: "Server env requires a claimed deploy." });
|
|
6111
6330
|
return;
|
|
@@ -6117,6 +6336,7 @@ export async function startAnonymousServer({
|
|
|
6117
6336
|
clientBundleBase64: payload.clientBundleBase64,
|
|
6118
6337
|
clientBundleHash: payload.clientBundleHash,
|
|
6119
6338
|
deployId,
|
|
6339
|
+
inspectPolicy,
|
|
6120
6340
|
publicRootUrl: resolvedPublicRootUrl,
|
|
6121
6341
|
serverEnv: payload.serverEnv
|
|
6122
6342
|
});
|
|
@@ -6180,7 +6400,17 @@ export async function startAnonymousServer({
|
|
|
6180
6400
|
return;
|
|
6181
6401
|
}
|
|
6182
6402
|
|
|
6183
|
-
if (
|
|
6403
|
+
if (
|
|
6404
|
+
req.method === "GET" &&
|
|
6405
|
+
(await serveInspect({
|
|
6406
|
+
...loaded,
|
|
6407
|
+
adminPassword,
|
|
6408
|
+
currentDeveloper,
|
|
6409
|
+
req,
|
|
6410
|
+
store: resolvedStore,
|
|
6411
|
+
systemPath: appPath
|
|
6412
|
+
}, res))
|
|
6413
|
+
) {
|
|
6184
6414
|
return;
|
|
6185
6415
|
}
|
|
6186
6416
|
|
package/src/anonymous.js
CHANGED
|
@@ -420,13 +420,13 @@ async function readSourceFiles(sourceStore) {
|
|
|
420
420
|
return files.sort((left, right) => left.path.localeCompare(right.path));
|
|
421
421
|
}
|
|
422
422
|
|
|
423
|
-
function forbiddenSourceDiagnostics(files) {
|
|
423
|
+
function forbiddenSourceDiagnostics(files, { allowAsync = false } = {}) {
|
|
424
424
|
const checks = [
|
|
425
425
|
[/\beval\s*\(/, "eval is not available in anonymous server code."],
|
|
426
426
|
[/\bFunction\s*\(/, "Function constructors are not available in anonymous server code."],
|
|
427
427
|
[/\bimport\s*\(/, "Dynamic import is not available in anonymous server code."],
|
|
428
|
-
[/\bfetch\
|
|
429
|
-
[/\basync\b/, "Async server handlers are not part of the anonymous IR yet. Use synchronous Lakebed database operations."],
|
|
428
|
+
[/\bfetch\b/, "Outbound fetch is disabled for anonymous deploys."],
|
|
429
|
+
...(allowAsync ? [] : [[/\basync\b/, "Async server handlers are not part of the anonymous IR yet. Use synchronous Lakebed database operations."]]),
|
|
430
430
|
[/\bwhile\s*\(/, "while loops are not available in anonymous server code."],
|
|
431
431
|
[/\bfor\s*\(\s*;/, "Unbounded for loops are not available in anonymous server code."],
|
|
432
432
|
[/\bprocess\b/, "process is not available in anonymous server code."],
|
|
@@ -486,18 +486,6 @@ function serializeSchema(schema) {
|
|
|
486
486
|
return { diagnostics, schema: cleanSchema };
|
|
487
487
|
}
|
|
488
488
|
|
|
489
|
-
function inferMutationGuards(handler) {
|
|
490
|
-
const source = Function.prototype.toString.call(handler);
|
|
491
|
-
const guards = [];
|
|
492
|
-
if (source.includes("ownerId") && source.includes("auth.userId")) {
|
|
493
|
-
guards.push({ field: "ownerId", equalsAuth: "userId", op: "rowFieldEqualsAuth" });
|
|
494
|
-
}
|
|
495
|
-
if (source.includes("authorId") && source.includes("auth.userId")) {
|
|
496
|
-
guards.push({ field: "authorId", equalsAuth: "userId", op: "rowFieldEqualsAuth" });
|
|
497
|
-
}
|
|
498
|
-
return guards;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
489
|
function compileQueryHandler({ handler, name, schema }) {
|
|
502
490
|
const { ctx, recorder } = createTraceContext({ mode: "query", schema });
|
|
503
491
|
try {
|
|
@@ -513,6 +501,40 @@ function compileQueryHandler({ handler, name, schema }) {
|
|
|
513
501
|
return recorder.query;
|
|
514
502
|
}
|
|
515
503
|
|
|
504
|
+
function isArgExpression(expr) {
|
|
505
|
+
return Array.isArray(expr) && expr[0] === "arg" && Number.isInteger(expr[1]);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function expressionContainsOp(expr, targetOp) {
|
|
509
|
+
if (!isExpression(expr)) {
|
|
510
|
+
if (Array.isArray(expr)) {
|
|
511
|
+
return expr.some((item) => expressionContainsOp(item, targetOp));
|
|
512
|
+
}
|
|
513
|
+
if (isPlainObject(expr)) {
|
|
514
|
+
return Object.values(expr).some((value) => expressionContainsOp(value, targetOp));
|
|
515
|
+
}
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (expr[0] === targetOp) {
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return expr.slice(1).some((item) => expressionContainsOp(item, targetOp));
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function expressionContainsArg(expr) {
|
|
527
|
+
return isArgExpression(expr) || expressionContainsOp(expr, "arg");
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function expressionContainsRow(expr) {
|
|
531
|
+
return expressionContainsOp(expr, "row");
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function directWriteGuardDiagnostic(name, operation) {
|
|
535
|
+
return `Unable to compile mutation "${name}" to anonymous IR: Direct ${operation.op}(id) from an argument-derived value cannot be proven safe by the IR compiler. Use the anonymous source runtime, claim the deploy, or rewrite this mutation to an owner-filtered query shape.`;
|
|
536
|
+
}
|
|
537
|
+
|
|
516
538
|
function compileMutationHandler({ handler, name, schema }) {
|
|
517
539
|
const { ctx, recorder } = createTraceContext({ mode: "mutation", schema });
|
|
518
540
|
const args = Array.from({ length: Math.max(0, handler.length - 1) }, (_, index) => createSymbolicArg(index));
|
|
@@ -523,7 +545,6 @@ function compileMutationHandler({ handler, name, schema }) {
|
|
|
523
545
|
throw new Error(`Unable to compile mutation "${name}" to anonymous IR: ${error instanceof Error ? error.message : String(error)}`);
|
|
524
546
|
}
|
|
525
547
|
|
|
526
|
-
const guards = inferMutationGuards(handler);
|
|
527
548
|
const operations = [];
|
|
528
549
|
|
|
529
550
|
for (const operation of recorder.operations) {
|
|
@@ -540,8 +561,14 @@ function compileMutationHandler({ handler, name, schema }) {
|
|
|
540
561
|
continue;
|
|
541
562
|
}
|
|
542
563
|
|
|
543
|
-
if (operation.op === "update") {
|
|
544
|
-
|
|
564
|
+
if (operation.op === "update" || operation.op === "delete") {
|
|
565
|
+
if (expressionContainsArg(operation.id)) {
|
|
566
|
+
throw new Error(directWriteGuardDiagnostic(name, operation));
|
|
567
|
+
}
|
|
568
|
+
if (expressionContainsRow(operation.id)) {
|
|
569
|
+
throw new Error(`Unable to compile mutation "${name}" to anonymous IR: ${operation.op} uses an unsupported symbolic row id.`);
|
|
570
|
+
}
|
|
571
|
+
operations.push(operation);
|
|
545
572
|
continue;
|
|
546
573
|
}
|
|
547
574
|
|
|
@@ -583,9 +610,9 @@ function compileServerToIr(app, schema) {
|
|
|
583
610
|
return { diagnostics, mutations, queries };
|
|
584
611
|
}
|
|
585
612
|
|
|
586
|
-
export async function createAnonymousArtifact({ app, clientOut, sourceStore, version = LAKEBED_VERSION }) {
|
|
613
|
+
export async function createAnonymousArtifact({ app, clientOut, serverOut, sourceStore, version = LAKEBED_VERSION }) {
|
|
587
614
|
const sourceFiles = await readSourceFiles(sourceStore);
|
|
588
|
-
const diagnostics = forbiddenSourceDiagnostics(sourceFiles);
|
|
615
|
+
const diagnostics = forbiddenSourceDiagnostics(sourceFiles, { allowAsync: Boolean(serverOut) });
|
|
589
616
|
const { diagnostics: schemaDiagnostics, schema } = serializeSchema(app.schema);
|
|
590
617
|
diagnostics.push(...schemaDiagnostics);
|
|
591
618
|
|
|
@@ -593,18 +620,40 @@ export async function createAnonymousArtifact({ app, clientOut, sourceStore, ver
|
|
|
593
620
|
throw new AnonymousCompilerError(diagnostics);
|
|
594
621
|
}
|
|
595
622
|
|
|
596
|
-
const compiled = compileServerToIr(app, schema);
|
|
597
|
-
diagnostics.push(...compiled.diagnostics);
|
|
598
|
-
|
|
599
|
-
if (diagnostics.length > 0) {
|
|
600
|
-
throw new AnonymousCompilerError(diagnostics);
|
|
601
|
-
}
|
|
602
|
-
|
|
603
623
|
const clientBundle = await readFile(clientOut);
|
|
604
624
|
const clientBundleBase64 = clientBundle.toString("base64");
|
|
605
625
|
const clientBundleHash = sha256(clientBundle);
|
|
626
|
+
const serverBundle = serverOut ? await readFile(serverOut) : null;
|
|
627
|
+
const serverBundleBase64 = serverBundle?.toString("base64");
|
|
628
|
+
const serverBundleHash = serverBundle ? sha256(serverBundle) : null;
|
|
606
629
|
const sourceManifest = sourceFiles.map(({ bytes, hash, path }) => ({ bytes, hash, path }));
|
|
607
630
|
const sourceSnapshotHash = sha256(stableStringify(sourceManifest));
|
|
631
|
+
const server = serverBundle
|
|
632
|
+
? {
|
|
633
|
+
helpers: {},
|
|
634
|
+
imports: ["lakebed/server"],
|
|
635
|
+
mutations: Object.fromEntries(Object.keys(app.mutations ?? {}).map((name) => [name, { op: "source" }])),
|
|
636
|
+
queries: Object.fromEntries(Object.keys(app.queries ?? {}).map((name) => [name, { op: "source" }])),
|
|
637
|
+
schema,
|
|
638
|
+
source: {
|
|
639
|
+
bytes: serverBundle.byteLength,
|
|
640
|
+
bundle: serverBundleBase64,
|
|
641
|
+
bundleHash: serverBundleHash,
|
|
642
|
+
entry: "/server.mjs"
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
: null;
|
|
646
|
+
let compiled = null;
|
|
647
|
+
|
|
648
|
+
if (!server) {
|
|
649
|
+
compiled = compileServerToIr(app, schema);
|
|
650
|
+
diagnostics.push(...compiled.diagnostics);
|
|
651
|
+
|
|
652
|
+
if (diagnostics.length > 0) {
|
|
653
|
+
throw new AnonymousCompilerError(diagnostics);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
608
657
|
const artifact = {
|
|
609
658
|
name: app.name ?? "Lakebed Capsule",
|
|
610
659
|
client: {
|
|
@@ -616,14 +665,14 @@ export async function createAnonymousArtifact({ app, clientOut, sourceStore, ver
|
|
|
616
665
|
compiler: "0.1.0",
|
|
617
666
|
lakebed: version
|
|
618
667
|
},
|
|
619
|
-
deployTarget: "anonymous-interpreter",
|
|
668
|
+
deployTarget: server ? "anonymous-source" : "anonymous-interpreter",
|
|
620
669
|
format: ANONYMOUS_ARTIFACT_FORMAT,
|
|
621
670
|
limits: {
|
|
622
671
|
instructionBudget: DEFAULT_ANONYMOUS_LIMITS.instructionBudget,
|
|
623
672
|
maxRowsReturned: DEFAULT_ANONYMOUS_LIMITS.rowsReturned,
|
|
624
673
|
maxValueBytes: DEFAULT_ANONYMOUS_LIMITS.maxValueBytes
|
|
625
674
|
},
|
|
626
|
-
server: {
|
|
675
|
+
server: server ?? {
|
|
627
676
|
helpers: {},
|
|
628
677
|
imports: ["lakebed/server"],
|
|
629
678
|
mutations: compiled.mutations,
|
|
@@ -801,6 +850,31 @@ function validateQuery(query, path, schema, diagnostics) {
|
|
|
801
850
|
}
|
|
802
851
|
}
|
|
803
852
|
|
|
853
|
+
function validateGuards(guards, path, schema, tableName, diagnostics) {
|
|
854
|
+
if (guards === undefined) {
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
if (!Array.isArray(guards)) {
|
|
859
|
+
diagnostics.push(diagnostic(path, "Mutation guards must be an array."));
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
for (const [index, guard] of guards.entries()) {
|
|
864
|
+
const guardPath = `${path}.guards.${index}`;
|
|
865
|
+
if (!isPlainObject(guard) || guard.op !== "rowFieldEqualsAuth") {
|
|
866
|
+
diagnostics.push(diagnostic(guardPath, "Unsupported mutation guard."));
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
if (typeof guard.field !== "string" || !schema?.[tableName]?.fields?.[guard.field]) {
|
|
870
|
+
diagnostics.push(diagnostic(guardPath, "Mutation guard field must exist in the table schema."));
|
|
871
|
+
}
|
|
872
|
+
if (guard.equalsAuth !== "userId") {
|
|
873
|
+
diagnostics.push(diagnostic(guardPath, "Mutation guard auth field must be userId."));
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
804
878
|
export function validateAnonymousArtifact(artifact, { allowClaimedSource = false } = {}) {
|
|
805
879
|
const diagnostics = [];
|
|
806
880
|
|
|
@@ -812,8 +886,13 @@ export function validateAnonymousArtifact(artifact, { allowClaimedSource = false
|
|
|
812
886
|
diagnostics.push(diagnostic("artifact.format", `Expected ${ANONYMOUS_ARTIFACT_FORMAT}.`));
|
|
813
887
|
}
|
|
814
888
|
|
|
815
|
-
|
|
816
|
-
|
|
889
|
+
const sourceDeployTargets = new Set(["anonymous-source", "claimed-source"]);
|
|
890
|
+
if (
|
|
891
|
+
artifact.deployTarget !== "anonymous-interpreter" &&
|
|
892
|
+
artifact.deployTarget !== "anonymous-source" &&
|
|
893
|
+
!(allowClaimedSource && artifact.deployTarget === "claimed-source")
|
|
894
|
+
) {
|
|
895
|
+
diagnostics.push(diagnostic("artifact.deployTarget", "Anonymous deploys require deployTarget anonymous-interpreter or anonymous-source."));
|
|
817
896
|
}
|
|
818
897
|
|
|
819
898
|
const schema = artifact.server?.schema;
|
|
@@ -837,14 +916,14 @@ export function validateAnonymousArtifact(artifact, { allowClaimedSource = false
|
|
|
837
916
|
if (query?.op === "source" && artifact.server?.source === undefined) {
|
|
838
917
|
diagnostics.push(diagnostic(`artifact.server.queries.${name}`, "Source query requires artifact.server.source."));
|
|
839
918
|
}
|
|
840
|
-
if (query?.op === "source" && artifact.deployTarget
|
|
841
|
-
diagnostics.push(diagnostic(`artifact.server.queries.${name}`, "Source query requires deployTarget claimed-source."));
|
|
919
|
+
if (query?.op === "source" && !sourceDeployTargets.has(artifact.deployTarget)) {
|
|
920
|
+
diagnostics.push(diagnostic(`artifact.server.queries.${name}`, "Source query requires deployTarget anonymous-source or claimed-source."));
|
|
842
921
|
}
|
|
843
922
|
}
|
|
844
923
|
|
|
845
924
|
if (artifact.server?.source !== undefined) {
|
|
846
|
-
if (artifact.deployTarget
|
|
847
|
-
diagnostics.push(diagnostic("artifact.server.source", "Server source bundles require deployTarget claimed-source."));
|
|
925
|
+
if (!sourceDeployTargets.has(artifact.deployTarget)) {
|
|
926
|
+
diagnostics.push(diagnostic("artifact.server.source", "Server source bundles require deployTarget anonymous-source or claimed-source."));
|
|
848
927
|
}
|
|
849
928
|
const source = artifact.server.source;
|
|
850
929
|
if (
|
|
@@ -871,8 +950,8 @@ export function validateAnonymousArtifact(artifact, { allowClaimedSource = false
|
|
|
871
950
|
if (artifact.server?.source === undefined) {
|
|
872
951
|
diagnostics.push(diagnostic(`artifact.server.mutations.${name}`, "Source mutation requires artifact.server.source."));
|
|
873
952
|
}
|
|
874
|
-
if (artifact.deployTarget
|
|
875
|
-
diagnostics.push(diagnostic(`artifact.server.mutations.${name}`, "Source mutation requires deployTarget claimed-source."));
|
|
953
|
+
if (!sourceDeployTargets.has(artifact.deployTarget)) {
|
|
954
|
+
diagnostics.push(diagnostic(`artifact.server.mutations.${name}`, "Source mutation requires deployTarget anonymous-source or claimed-source."));
|
|
876
955
|
}
|
|
877
956
|
continue;
|
|
878
957
|
}
|
|
@@ -894,8 +973,10 @@ export function validateAnonymousArtifact(artifact, { allowClaimedSource = false
|
|
|
894
973
|
} else if (operation.op === "update") {
|
|
895
974
|
validateValue(operation.id, path, diagnostics);
|
|
896
975
|
validateValue(operation.patch, path, diagnostics);
|
|
976
|
+
validateGuards(operation.guards, path, schema, operation.table, diagnostics);
|
|
897
977
|
} else if (operation.op === "delete") {
|
|
898
978
|
validateValue(operation.id, path, diagnostics);
|
|
979
|
+
validateGuards(operation.guards, path, schema, operation.table, diagnostics);
|
|
899
980
|
} else if (operation.op === "deleteWhere") {
|
|
900
981
|
validateQuery(operation.query, path, schema, diagnostics);
|
|
901
982
|
}
|
|
@@ -1290,7 +1371,7 @@ export async function executeAnonymousQuery({ args = [], artifact, auth, deployI
|
|
|
1290
1371
|
return executeQuerySpec({ args, artifact, auth, deployId, query, state });
|
|
1291
1372
|
}
|
|
1292
1373
|
|
|
1293
|
-
async function
|
|
1374
|
+
async function checkRowGuards({ auth, guards = [], row }) {
|
|
1294
1375
|
for (const guard of guards) {
|
|
1295
1376
|
if (guard.op === "rowFieldEqualsAuth" && row?.[guard.field] !== auth[guard.equalsAuth]) {
|
|
1296
1377
|
return false;
|
|
@@ -1335,7 +1416,7 @@ export async function executeAnonymousMutation({ args = [], artifact, auth, depl
|
|
|
1335
1416
|
if (operation.op === "update") {
|
|
1336
1417
|
const id = evaluateValue(operation.id, { args, auth });
|
|
1337
1418
|
const row = await tx.getRow(deployId, operation.table, id);
|
|
1338
|
-
if (!row || !(await
|
|
1419
|
+
if (!row || !(await checkRowGuards({ auth, guards: operation.guards, row }))) {
|
|
1339
1420
|
continue;
|
|
1340
1421
|
}
|
|
1341
1422
|
const patch = preparePatch(artifact.server.schema, operation.table, evaluateValue(operation.patch, { args, auth, row }));
|
|
@@ -1345,6 +1426,10 @@ export async function executeAnonymousMutation({ args = [], artifact, auth, depl
|
|
|
1345
1426
|
|
|
1346
1427
|
if (operation.op === "delete") {
|
|
1347
1428
|
const id = evaluateValue(operation.id, { args, auth });
|
|
1429
|
+
const row = await tx.getRow(deployId, operation.table, id);
|
|
1430
|
+
if (!row || !(await checkRowGuards({ auth, guards: operation.guards, row }))) {
|
|
1431
|
+
continue;
|
|
1432
|
+
}
|
|
1348
1433
|
await tx.deleteRow(deployId, operation.table, id);
|
|
1349
1434
|
continue;
|
|
1350
1435
|
}
|
package/src/cli.js
CHANGED
|
@@ -40,17 +40,17 @@ Usage:
|
|
|
40
40
|
lakebed create [name] [--template todo] [--no-git]
|
|
41
41
|
lakebed dev [capsule-dir] [--port 3000]
|
|
42
42
|
lakebed build [capsule-dir] --target anonymous [--out .lakebed/artifacts/app.json] [--json]
|
|
43
|
-
lakebed deploy [capsule-dir] [--api <url>] [--json]
|
|
43
|
+
lakebed deploy [capsule-dir] [--api <url>] [--public-inspect] [--json]
|
|
44
44
|
lakebed claim [capsule-dir] [--api <url>] [--json]
|
|
45
45
|
lakebed domains add <subdomain.lakebed.app> [--api <url>] [--json]
|
|
46
46
|
lakebed anonymous-server [--port 8787] [--public-root-url <url>] [--app-base-domain <domain>]
|
|
47
|
-
lakebed inspect <deploy-id-or-url> [--api <url>] [--json]
|
|
47
|
+
lakebed inspect <deploy-id-or-url> [--api <url>] [--inspect-token <token>] [--json]
|
|
48
48
|
lakebed run-many [capsule-dir] [--count 20] [--base-port 4000]
|
|
49
49
|
lakebed auth as <name>
|
|
50
50
|
lakebed auth reset
|
|
51
|
-
lakebed db list [deploy-id-or-url] [--port 3000]
|
|
52
|
-
lakebed db dump [deploy-id-or-url] [--port 3000]
|
|
53
|
-
lakebed logs [deploy-id-or-url] [--port 3000]
|
|
51
|
+
lakebed db list [deploy-id-or-url] [--port 3000] [--inspect-token <token>]
|
|
52
|
+
lakebed db dump [deploy-id-or-url] [--port 3000] [--inspect-token <token>]
|
|
53
|
+
lakebed logs [deploy-id-or-url] [--port 3000] [--inspect-token <token>]
|
|
54
54
|
`);
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -68,6 +68,7 @@ const optionsWithValues = new Set([
|
|
|
68
68
|
"--app-base-domain",
|
|
69
69
|
"--base-port",
|
|
70
70
|
"--count",
|
|
71
|
+
"--inspect-token",
|
|
71
72
|
"--out",
|
|
72
73
|
"--port",
|
|
73
74
|
"--public-root-url",
|
|
@@ -794,6 +795,7 @@ async function buildAnonymousEnvelope(capsuleArg, sourceStore) {
|
|
|
794
795
|
const artifact = await createAnonymousArtifact({
|
|
795
796
|
app: built.app,
|
|
796
797
|
clientOut: built.clientOut,
|
|
798
|
+
serverOut: built.serverOut,
|
|
797
799
|
sourceStore
|
|
798
800
|
});
|
|
799
801
|
|
|
@@ -1003,12 +1005,15 @@ async function openUrlInBrowser(url) {
|
|
|
1003
1005
|
await execFileAsync(invocation.command, invocation.args, { windowsHide: true });
|
|
1004
1006
|
}
|
|
1005
1007
|
|
|
1006
|
-
function deployRequestBody(envelope, { serverEnv } = {}) {
|
|
1008
|
+
function deployRequestBody(envelope, { inspectPolicy, serverEnv } = {}) {
|
|
1007
1009
|
const body = {
|
|
1008
1010
|
artifact: envelope.artifact,
|
|
1009
1011
|
clientBundle: envelope.clientBundle,
|
|
1010
1012
|
clientVersion: LAKEBED_VERSION
|
|
1011
1013
|
};
|
|
1014
|
+
if (inspectPolicy !== undefined) {
|
|
1015
|
+
body.inspectPolicy = inspectPolicy;
|
|
1016
|
+
}
|
|
1012
1017
|
if (serverEnv !== undefined) {
|
|
1013
1018
|
body.serverEnv = {
|
|
1014
1019
|
mode: "replace",
|
|
@@ -1057,6 +1062,22 @@ function deployApiUrl(args) {
|
|
|
1057
1062
|
return String(readArg(args, "--api", process.env.LAKEBED_DEPLOY_API ?? process.env.SPAN_DEPLOY_API ?? defaultDeployApiUrl)).replace(/\/+$/g, "");
|
|
1058
1063
|
}
|
|
1059
1064
|
|
|
1065
|
+
function hasExplicitOption(args, name) {
|
|
1066
|
+
return args.some((arg) => arg === name || arg.startsWith(`${name}=`));
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
function normalizeHostedUrl(value) {
|
|
1070
|
+
if (!value) {
|
|
1071
|
+
return "";
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
try {
|
|
1075
|
+
return new URL(value).href.replace(/\/+$/g, "");
|
|
1076
|
+
} catch {
|
|
1077
|
+
return String(value).replace(/\/+$/g, "");
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1060
1081
|
async function readResponseJson(response) {
|
|
1061
1082
|
const body = await response.text();
|
|
1062
1083
|
if (!response.ok) {
|
|
@@ -1078,6 +1099,7 @@ async function deployCommand(args) {
|
|
|
1078
1099
|
const serverEnvKeys = Object.keys(serverEnv).sort();
|
|
1079
1100
|
const hasServerEnvValues = serverEnvKeys.length > 0;
|
|
1080
1101
|
const api = deployApiUrl(args);
|
|
1102
|
+
const inspectPolicy = hasFlag(args, "--public-inspect") ? "public" : undefined;
|
|
1081
1103
|
const metadata = await readDeployMetadata(capsuleDir);
|
|
1082
1104
|
const canUpdate =
|
|
1083
1105
|
metadata?.api === api && typeof metadata?.deployId === "string" && typeof metadata?.claimToken === "string";
|
|
@@ -1128,7 +1150,7 @@ async function deployCommand(args) {
|
|
|
1128
1150
|
|
|
1129
1151
|
if (canUpdate) {
|
|
1130
1152
|
response = await fetch(`${api}/v1/deploys/${encodeURIComponent(metadata.deployId)}`, {
|
|
1131
|
-
body: deployRequestBody(envelope, { serverEnv: serverEnvForUpdate }),
|
|
1153
|
+
body: deployRequestBody(envelope, { inspectPolicy, serverEnv: serverEnvForUpdate }),
|
|
1132
1154
|
headers: {
|
|
1133
1155
|
"Authorization": `Bearer ${metadata.claimToken}`,
|
|
1134
1156
|
"Content-Type": "application/json"
|
|
@@ -1149,7 +1171,7 @@ async function deployCommand(args) {
|
|
|
1149
1171
|
}
|
|
1150
1172
|
|
|
1151
1173
|
response ??= await fetch(`${api}/v1/anonymous-deploys`, {
|
|
1152
|
-
body: deployRequestBody(envelope),
|
|
1174
|
+
body: deployRequestBody(envelope, { inspectPolicy }),
|
|
1153
1175
|
headers: {
|
|
1154
1176
|
"Content-Type": "application/json"
|
|
1155
1177
|
},
|
|
@@ -1193,6 +1215,9 @@ async function deployCommand(args) {
|
|
|
1193
1215
|
console.log(`Claim: ${claimCommandText({ api, capsuleArg })}`);
|
|
1194
1216
|
}
|
|
1195
1217
|
console.log(`Inspect: lakebed inspect ${deployed.deployId}`);
|
|
1218
|
+
if (deployed.inspectPolicy === "public") {
|
|
1219
|
+
console.log("Inspect policy: public - data and logs are readable by anyone with the app URL.");
|
|
1220
|
+
}
|
|
1196
1221
|
console.log("\nLimits:");
|
|
1197
1222
|
console.log(` source/artifact: ${deployed.limits.artifactBytes} bytes`);
|
|
1198
1223
|
console.log(` state: ${deployed.limits.stateBytes} bytes`);
|
|
@@ -1322,7 +1347,15 @@ async function anonymousServerCommand(args) {
|
|
|
1322
1347
|
await new Promise(() => {});
|
|
1323
1348
|
}
|
|
1324
1349
|
|
|
1325
|
-
|
|
1350
|
+
function deployLookupApiUrl(target, args, metadata) {
|
|
1351
|
+
if (!hasExplicitOption(args, "--api") && metadata?.api && metadata.deployId === target) {
|
|
1352
|
+
return String(metadata.api).replace(/\/+$/g, "");
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
return deployApiUrl(args);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
async function resolveDeployUrl(target, args, metadata) {
|
|
1326
1359
|
if (!target) {
|
|
1327
1360
|
throw new Error("Expected a deploy ID or URL.");
|
|
1328
1361
|
}
|
|
@@ -1331,16 +1364,40 @@ async function resolveDeployUrl(target, args) {
|
|
|
1331
1364
|
const url = new URL(target);
|
|
1332
1365
|
return url.href.replace(/\/+$/g, "");
|
|
1333
1366
|
} catch {
|
|
1334
|
-
const api =
|
|
1367
|
+
const api = deployLookupApiUrl(target, args, metadata);
|
|
1335
1368
|
const response = await fetch(`${api}/v1/deploys/${encodeURIComponent(target)}`);
|
|
1336
1369
|
const deploy = await readResponseJson(response);
|
|
1337
1370
|
return deploy.url.replace(/\/+$/g, "");
|
|
1338
1371
|
}
|
|
1339
1372
|
}
|
|
1340
1373
|
|
|
1374
|
+
function inspectTokenForHostedTarget({ args, metadata, target, url }) {
|
|
1375
|
+
const explicitToken = readArg(args, "--inspect-token", process.env.LAKEBED_INSPECT_TOKEN ?? "");
|
|
1376
|
+
if (explicitToken) {
|
|
1377
|
+
return explicitToken;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
if (!metadata?.claimToken) {
|
|
1381
|
+
return "";
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
const normalizedTarget = normalizeHostedUrl(target);
|
|
1385
|
+
const normalizedUrl = normalizeHostedUrl(url);
|
|
1386
|
+
const metadataUrl = normalizeHostedUrl(metadata.url);
|
|
1387
|
+
if (metadata.deployId === target || (metadataUrl && (metadataUrl === normalizedTarget || metadataUrl === normalizedUrl))) {
|
|
1388
|
+
return metadata.claimToken;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
return "";
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1341
1394
|
async function hostedJson(target, path, args) {
|
|
1342
|
-
const
|
|
1343
|
-
const
|
|
1395
|
+
const metadata = await readDeployMetadata(root);
|
|
1396
|
+
const url = await resolveDeployUrl(target, args, metadata);
|
|
1397
|
+
const inspectToken = inspectTokenForHostedTarget({ args, metadata, target, url });
|
|
1398
|
+
const response = await fetch(`${url}${path}`, {
|
|
1399
|
+
headers: inspectToken ? { Authorization: `Bearer ${inspectToken}` } : {}
|
|
1400
|
+
});
|
|
1344
1401
|
return readResponseJson(response);
|
|
1345
1402
|
}
|
|
1346
1403
|
|
|
@@ -1354,15 +1411,36 @@ async function inspectCommand(args) {
|
|
|
1354
1411
|
}
|
|
1355
1412
|
|
|
1356
1413
|
console.log(`Deploy: ${manifest.deployId}`);
|
|
1357
|
-
|
|
1414
|
+
if (manifest.url) {
|
|
1415
|
+
console.log(`URL: ${manifest.url}`);
|
|
1416
|
+
}
|
|
1358
1417
|
console.log(`Updated: ${formatOptionalTimestamp(manifest.updatedAt, "unknown")}`);
|
|
1359
1418
|
console.log(`Expires: ${formatOptionalTimestamp(manifest.expiresAt)}`);
|
|
1419
|
+
console.log(`Runtime: ${manifest.runtimeVersion ?? "unknown"}`);
|
|
1420
|
+
if (manifest.inspectPolicy) {
|
|
1421
|
+
console.log(`Policy: ${manifest.inspectPolicy}`);
|
|
1422
|
+
}
|
|
1360
1423
|
if (Array.isArray(manifest.domains) && manifest.domains.length > 0) {
|
|
1361
1424
|
console.log(`Domains: ${manifest.domains.map((domain) => domain.hostname).join(", ")}`);
|
|
1362
1425
|
}
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1426
|
+
if (manifest.artifactHash) {
|
|
1427
|
+
console.log(`Artifact: ${manifest.artifactHash}`);
|
|
1428
|
+
}
|
|
1429
|
+
if (Array.isArray(manifest.queries)) {
|
|
1430
|
+
console.log(`Queries: ${manifest.queries.join(", ") || "(none)"}`);
|
|
1431
|
+
}
|
|
1432
|
+
if (Array.isArray(manifest.mutations)) {
|
|
1433
|
+
console.log(`Mutations: ${manifest.mutations.join(", ") || "(none)"}`);
|
|
1434
|
+
}
|
|
1435
|
+
if (Array.isArray(manifest.mutationDetails) && manifest.mutationDetails.length > 0) {
|
|
1436
|
+
console.log("Mutation runtime:");
|
|
1437
|
+
for (const detail of manifest.mutationDetails) {
|
|
1438
|
+
const guardSummary = (detail.guards ?? [])
|
|
1439
|
+
.map((guard) => `${guard.table}.${guard.operation}:${guard.field}=auth.${guard.equalsAuth}`)
|
|
1440
|
+
.join(", ");
|
|
1441
|
+
console.log(` ${detail.name}: ${detail.mode}${guardSummary ? ` (${guardSummary})` : ""}`);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1366
1444
|
}
|
|
1367
1445
|
|
|
1368
1446
|
async function authCommand(args) {
|
|
@@ -13,6 +13,7 @@ const DEFAULT_LIMITS = {
|
|
|
13
13
|
|
|
14
14
|
let nextFetchId = 1;
|
|
15
15
|
const pendingFetches = new Map();
|
|
16
|
+
let allowBrokeredFetch = true;
|
|
16
17
|
|
|
17
18
|
function stableStringify(value) {
|
|
18
19
|
if (value === undefined) {
|
|
@@ -312,6 +313,9 @@ function createBrokeredResponse(response) {
|
|
|
312
313
|
}
|
|
313
314
|
|
|
314
315
|
function brokeredFetch(input, init = {}) {
|
|
316
|
+
if (!allowBrokeredFetch) {
|
|
317
|
+
return Promise.reject(new Error("Outbound fetch is disabled for anonymous deploys."));
|
|
318
|
+
}
|
|
315
319
|
if (!sendToParent) {
|
|
316
320
|
return Promise.reject(new Error("Source fetch broker is not available."));
|
|
317
321
|
}
|
|
@@ -393,6 +397,7 @@ function sendError(error) {
|
|
|
393
397
|
}
|
|
394
398
|
|
|
395
399
|
async function runSource(request) {
|
|
400
|
+
allowBrokeredFetch = request.allowFetch !== false;
|
|
396
401
|
const app = await loadSourceApp(request.artifact);
|
|
397
402
|
const source = createSourceContext({
|
|
398
403
|
artifact: request.artifact,
|
package/src/source-runtime.js
CHANGED
|
@@ -522,6 +522,7 @@ export class ChildProcessSourceRuntime {
|
|
|
522
522
|
async executeQuery({ args = [], artifact, auth, deployId, name, state }) {
|
|
523
523
|
const snapshot = await snapshotSourceState({ artifact, deployId, state });
|
|
524
524
|
const response = await this.runWorker({
|
|
525
|
+
allowFetch: artifact.deployTarget === "claimed-source",
|
|
525
526
|
args,
|
|
526
527
|
artifact,
|
|
527
528
|
auth,
|
|
@@ -537,6 +538,7 @@ export class ChildProcessSourceRuntime {
|
|
|
537
538
|
return state.transaction(deployId, async (tx) => {
|
|
538
539
|
const snapshot = await snapshotSourceState({ artifact, deployId, state: tx });
|
|
539
540
|
const response = await this.runWorker({
|
|
541
|
+
allowFetch: artifact.deployTarget === "claimed-source",
|
|
540
542
|
args,
|
|
541
543
|
artifact,
|
|
542
544
|
auth,
|
package/src/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const LAKEBED_VERSION = "0.0.
|
|
1
|
+
export const LAKEBED_VERSION = "0.0.17";
|