lakebed 0.0.12 → 0.0.14
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 +1 -1
- package/package.json +1 -1
- package/src/anonymous-server.js +75 -28
- package/src/cli.js +94 -4
- package/src/client.js +108 -4
- package/src/runtime.js +3 -1
- package/src/version.js +1 -1
package/README.md
CHANGED
|
@@ -176,7 +176,7 @@ LAKEBED_SESSION_SECRET=...
|
|
|
176
176
|
LAKEBED_SERVER_ENV_SECRET=...
|
|
177
177
|
```
|
|
178
178
|
|
|
179
|
-
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.
|
|
179
|
+
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 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.
|
|
180
180
|
|
|
181
181
|
## Admin Dashboard
|
|
182
182
|
|
package/package.json
CHANGED
package/src/anonymous-server.js
CHANGED
|
@@ -190,11 +190,16 @@ function responseForDeploy({ deploy, token }) {
|
|
|
190
190
|
expiresAt: deploy.expiresAt,
|
|
191
191
|
inspect: inspectUrls(deploy.url),
|
|
192
192
|
limits: deploy.limits,
|
|
193
|
+
updatedAt: deploy.updatedAt,
|
|
193
194
|
url: deploy.url
|
|
194
195
|
};
|
|
195
196
|
}
|
|
196
197
|
|
|
197
198
|
function isExpired(deploy) {
|
|
199
|
+
if (deploy?.ownerId) {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
|
|
198
203
|
return Boolean(deploy.expiresAt) && Date.parse(deploy.expiresAt) <= Date.now();
|
|
199
204
|
}
|
|
200
205
|
|
|
@@ -705,6 +710,7 @@ function adminDeploySummary({ artifact, artifactBytes = 0, deploy, logBytes = 0,
|
|
|
705
710
|
stateRows,
|
|
706
711
|
status: isExpired(deploy) ? "expired" : deploy.status,
|
|
707
712
|
tableCount: Object.keys(artifact?.server?.schema ?? {}).length,
|
|
713
|
+
updatedAt: deploy.updatedAt,
|
|
708
714
|
url: deploy.url,
|
|
709
715
|
...usageCounts(usage)
|
|
710
716
|
};
|
|
@@ -1286,6 +1292,7 @@ function adminHtml() {
|
|
|
1286
1292
|
<th>User</th>
|
|
1287
1293
|
<th>Status</th>
|
|
1288
1294
|
<th>Created</th>
|
|
1295
|
+
<th>Updated</th>
|
|
1289
1296
|
<th>Expires</th>
|
|
1290
1297
|
<th>Usage</th>
|
|
1291
1298
|
<th>Storage</th>
|
|
@@ -1389,6 +1396,7 @@ function adminHtml() {
|
|
|
1389
1396
|
<th>User</th>
|
|
1390
1397
|
<th>Status</th>
|
|
1391
1398
|
<th>Created</th>
|
|
1399
|
+
<th>Updated</th>
|
|
1392
1400
|
<th>Expires</th>
|
|
1393
1401
|
<th>Usage</th>
|
|
1394
1402
|
<th>Storage</th>
|
|
@@ -1464,6 +1472,10 @@ function adminHtml() {
|
|
|
1464
1472
|
}).format(new Date(value));
|
|
1465
1473
|
}
|
|
1466
1474
|
|
|
1475
|
+
function formatExpiry(value) {
|
|
1476
|
+
return value ? formatTime(value) : "never";
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1467
1479
|
function plural(count, label) {
|
|
1468
1480
|
return formatNumber(count) + " " + label + (count === 1 ? "" : "s");
|
|
1469
1481
|
}
|
|
@@ -1715,7 +1727,7 @@ function adminHtml() {
|
|
|
1715
1727
|
if (!deploys.length) {
|
|
1716
1728
|
const tr = document.createElement("tr");
|
|
1717
1729
|
const td = textCell(emptyText, "mono");
|
|
1718
|
-
td.colSpan =
|
|
1730
|
+
td.colSpan = 10;
|
|
1719
1731
|
tr.appendChild(td);
|
|
1720
1732
|
tbody.appendChild(tr);
|
|
1721
1733
|
return;
|
|
@@ -1727,7 +1739,8 @@ function adminHtml() {
|
|
|
1727
1739
|
tr.appendChild(deployUserCell(deploy));
|
|
1728
1740
|
tr.appendChild(statusCell(deploy));
|
|
1729
1741
|
tr.appendChild(textCell(formatTime(deploy.createdAt), "mono"));
|
|
1730
|
-
tr.appendChild(textCell(formatTime(deploy.
|
|
1742
|
+
tr.appendChild(textCell(formatTime(deploy.updatedAt), "mono"));
|
|
1743
|
+
tr.appendChild(textCell(formatExpiry(deploy.expiresAt), "mono"));
|
|
1731
1744
|
tr.appendChild(usageCell(deploy));
|
|
1732
1745
|
tr.appendChild(storageCell(deploy));
|
|
1733
1746
|
tr.appendChild(textCell(formatNumber(deploy.connections), "mono"));
|
|
@@ -2207,6 +2220,14 @@ function escapeHtml(value) {
|
|
|
2207
2220
|
.replace(/"/g, """);
|
|
2208
2221
|
}
|
|
2209
2222
|
|
|
2223
|
+
function formatHtmlTime(value) {
|
|
2224
|
+
return value ? new Date(value).toLocaleString() : "unknown";
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
function formatHtmlExpiry(value) {
|
|
2228
|
+
return value ? formatHtmlTime(value) : "never";
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2210
2231
|
function developerDeploySummary({ artifact, deploy, usage }) {
|
|
2211
2232
|
return {
|
|
2212
2233
|
artifactHash: deploy.artifactHash,
|
|
@@ -2221,6 +2242,7 @@ function developerDeploySummary({ artifact, deploy, usage }) {
|
|
|
2221
2242
|
ownerId: deploy.ownerId,
|
|
2222
2243
|
slug: deploy.slug,
|
|
2223
2244
|
status: isExpired(deploy) ? "expired" : deploy.status,
|
|
2245
|
+
updatedAt: deploy.updatedAt,
|
|
2224
2246
|
url: deploy.url,
|
|
2225
2247
|
usage: usageCounts(usage)
|
|
2226
2248
|
};
|
|
@@ -2233,8 +2255,9 @@ function developerHtml({ authConfigured, deploys = [], user }) {
|
|
|
2233
2255
|
(deploy) => `<tr>
|
|
2234
2256
|
<td><a href="${escapeHtml(deploy.url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(deploy.name)}</a><small>${escapeHtml(deploy.deployId)} / ${escapeHtml(deploy.slug)}</small></td>
|
|
2235
2257
|
<td><span class="pill ${escapeHtml(deploy.status)}">${escapeHtml(deploy.status)}</span></td>
|
|
2236
|
-
<td>${escapeHtml(
|
|
2237
|
-
<td>${escapeHtml(
|
|
2258
|
+
<td>${escapeHtml(formatHtmlTime(deploy.createdAt))}</td>
|
|
2259
|
+
<td>${escapeHtml(formatHtmlTime(deploy.updatedAt))}</td>
|
|
2260
|
+
<td>${escapeHtml(formatHtmlExpiry(deploy.expiresAt))}</td>
|
|
2238
2261
|
<td>${escapeHtml(deploy.usage.requestsToday)} / ${escapeHtml(deploy.limits.requestsPerDay)}</td>
|
|
2239
2262
|
<td>${escapeHtml(deploy.usage.mutationsToday)} / ${escapeHtml(deploy.limits.mutationsPerDay)}</td>
|
|
2240
2263
|
</tr>`
|
|
@@ -2434,6 +2457,7 @@ function developerHtml({ authConfigured, deploys = [], user }) {
|
|
|
2434
2457
|
<th>Deploy</th>
|
|
2435
2458
|
<th>Status</th>
|
|
2436
2459
|
<th>Created</th>
|
|
2460
|
+
<th>Updated</th>
|
|
2437
2461
|
<th>Expires</th>
|
|
2438
2462
|
<th>Requests</th>
|
|
2439
2463
|
<th>Mutations</th>
|
|
@@ -2622,6 +2646,7 @@ export class MemoryAnonymousStore {
|
|
|
2622
2646
|
publicRootUrl,
|
|
2623
2647
|
slug,
|
|
2624
2648
|
status: "active",
|
|
2649
|
+
updatedAt: createdAt,
|
|
2625
2650
|
url
|
|
2626
2651
|
};
|
|
2627
2652
|
this.deploys.set(deployId, deploy);
|
|
@@ -2648,7 +2673,7 @@ export class MemoryAnonymousStore {
|
|
|
2648
2673
|
return null;
|
|
2649
2674
|
}
|
|
2650
2675
|
|
|
2651
|
-
const
|
|
2676
|
+
const updatedAt = now();
|
|
2652
2677
|
const ttlSeconds = parseTtlSeconds(requestedTtlSeconds);
|
|
2653
2678
|
const nextAppBaseDomain = normalizeAppBaseDomain(appBaseDomain ?? currentDeploy.appBaseDomain);
|
|
2654
2679
|
const nextPublicRootUrl = publicRootUrl ?? currentDeploy.publicRootUrl;
|
|
@@ -2657,10 +2682,11 @@ export class MemoryAnonymousStore {
|
|
|
2657
2682
|
appBaseDomain: nextAppBaseDomain,
|
|
2658
2683
|
artifactHash,
|
|
2659
2684
|
clientBundleHash,
|
|
2660
|
-
expiresAt: new Date(Date.now() + ttlSeconds * 1000).toISOString(),
|
|
2685
|
+
expiresAt: currentDeploy.ownerId ? null : new Date(Date.now() + ttlSeconds * 1000).toISOString(),
|
|
2661
2686
|
publicRootUrl: nextPublicRootUrl,
|
|
2662
2687
|
url: appUrlForSlug({ appBaseDomain: nextAppBaseDomain, publicRootUrl: nextPublicRootUrl, slug: currentDeploy.slug }),
|
|
2663
|
-
status: "active"
|
|
2688
|
+
status: "active",
|
|
2689
|
+
updatedAt
|
|
2664
2690
|
};
|
|
2665
2691
|
|
|
2666
2692
|
this.storeArtifact({
|
|
@@ -2668,7 +2694,7 @@ export class MemoryAnonymousStore {
|
|
|
2668
2694
|
artifactHash,
|
|
2669
2695
|
clientBundleBase64,
|
|
2670
2696
|
clientBundleHash,
|
|
2671
|
-
createdAt
|
|
2697
|
+
createdAt: updatedAt
|
|
2672
2698
|
});
|
|
2673
2699
|
this.deploys.set(deployId, deploy);
|
|
2674
2700
|
if (serverEnv !== undefined) {
|
|
@@ -2691,11 +2717,14 @@ export class MemoryAnonymousStore {
|
|
|
2691
2717
|
return { deploy: currentDeploy, status: "conflict" };
|
|
2692
2718
|
}
|
|
2693
2719
|
|
|
2720
|
+
const updatedAt = now();
|
|
2694
2721
|
const deploy = {
|
|
2695
2722
|
...currentDeploy,
|
|
2696
|
-
claimedAt: currentDeploy.claimedAt ??
|
|
2723
|
+
claimedAt: currentDeploy.claimedAt ?? updatedAt,
|
|
2724
|
+
expiresAt: null,
|
|
2697
2725
|
owner,
|
|
2698
|
-
ownerId: owner.id
|
|
2726
|
+
ownerId: owner.id,
|
|
2727
|
+
updatedAt
|
|
2699
2728
|
};
|
|
2700
2729
|
await this.rememberUser(owner);
|
|
2701
2730
|
this.deploys.set(deployId, deploy);
|
|
@@ -2710,7 +2739,8 @@ export class MemoryAnonymousStore {
|
|
|
2710
2739
|
|
|
2711
2740
|
const deploy = {
|
|
2712
2741
|
...currentDeploy,
|
|
2713
|
-
status: "terminated"
|
|
2742
|
+
status: "terminated",
|
|
2743
|
+
updatedAt: now()
|
|
2714
2744
|
};
|
|
2715
2745
|
this.deploys.set(deployId, deploy);
|
|
2716
2746
|
return this.deployWithUserLimitOverrides(deploy);
|
|
@@ -2938,7 +2968,8 @@ export class PostgresAnonymousStore {
|
|
|
2938
2968
|
artifact_hash text not null,
|
|
2939
2969
|
client_bundle_hash text not null,
|
|
2940
2970
|
created_at timestamptz not null,
|
|
2941
|
-
|
|
2971
|
+
updated_at timestamptz not null,
|
|
2972
|
+
expires_at timestamptz,
|
|
2942
2973
|
claimed_at timestamptz,
|
|
2943
2974
|
owner_id text,
|
|
2944
2975
|
owner_json jsonb,
|
|
@@ -2953,6 +2984,11 @@ export class PostgresAnonymousStore {
|
|
|
2953
2984
|
await this.query("alter table deploys add column if not exists claimed_at timestamptz");
|
|
2954
2985
|
await this.query("alter table deploys add column if not exists owner_id text");
|
|
2955
2986
|
await this.query("alter table deploys add column if not exists owner_json jsonb");
|
|
2987
|
+
await this.query("alter table deploys add column if not exists updated_at timestamptz");
|
|
2988
|
+
await this.query("update deploys set updated_at = created_at where updated_at is null");
|
|
2989
|
+
await this.query("alter table deploys alter column updated_at set not null");
|
|
2990
|
+
await this.query("alter table deploys alter column expires_at drop not null");
|
|
2991
|
+
await this.query("update deploys set expires_at = null where owner_id is not null and expires_at is not null");
|
|
2956
2992
|
await this.query(`
|
|
2957
2993
|
create table if not exists artifacts(
|
|
2958
2994
|
hash text primary key,
|
|
@@ -3188,7 +3224,7 @@ export class PostgresAnonymousStore {
|
|
|
3188
3224
|
select
|
|
3189
3225
|
owner_id,
|
|
3190
3226
|
count(*)::int as deploy_count,
|
|
3191
|
-
count(*) filter (where status = 'active'
|
|
3227
|
+
count(*) filter (where status = 'active')::int as active_deploy_count
|
|
3192
3228
|
from deploys
|
|
3193
3229
|
where owner_id is not null
|
|
3194
3230
|
group by owner_id
|
|
@@ -3238,7 +3274,7 @@ export class PostgresAnonymousStore {
|
|
|
3238
3274
|
select
|
|
3239
3275
|
owner_id,
|
|
3240
3276
|
count(*)::int as deploy_count,
|
|
3241
|
-
count(*) filter (where status = 'active'
|
|
3277
|
+
count(*) filter (where status = 'active')::int as active_deploy_count
|
|
3242
3278
|
from deploys
|
|
3243
3279
|
where owner_id is not null
|
|
3244
3280
|
group by owner_id
|
|
@@ -3310,6 +3346,7 @@ export class PostgresAnonymousStore {
|
|
|
3310
3346
|
publicRootUrl,
|
|
3311
3347
|
slug,
|
|
3312
3348
|
status: "active",
|
|
3349
|
+
updatedAt: createdAt,
|
|
3313
3350
|
url
|
|
3314
3351
|
};
|
|
3315
3352
|
|
|
@@ -3317,10 +3354,10 @@ export class PostgresAnonymousStore {
|
|
|
3317
3354
|
await this.query(
|
|
3318
3355
|
`
|
|
3319
3356
|
insert into deploys(
|
|
3320
|
-
id, slug, status, artifact_hash, client_bundle_hash, created_at, expires_at,
|
|
3357
|
+
id, slug, status, artifact_hash, client_bundle_hash, created_at, updated_at, expires_at,
|
|
3321
3358
|
claim_token_hash, limits_json, counters_json, public_root_url, app_base_domain, url
|
|
3322
3359
|
)
|
|
3323
|
-
values($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, '{}'::jsonb, $
|
|
3360
|
+
values($1, $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb, '{}'::jsonb, $11, $12, $13)
|
|
3324
3361
|
`,
|
|
3325
3362
|
[
|
|
3326
3363
|
deploy.id,
|
|
@@ -3329,6 +3366,7 @@ export class PostgresAnonymousStore {
|
|
|
3329
3366
|
deploy.artifactHash,
|
|
3330
3367
|
deploy.clientBundleHash,
|
|
3331
3368
|
deploy.createdAt,
|
|
3369
|
+
deploy.updatedAt,
|
|
3332
3370
|
deploy.expiresAt,
|
|
3333
3371
|
deploy.claimTokenHash,
|
|
3334
3372
|
JSON.stringify(deploy.limits),
|
|
@@ -3367,13 +3405,13 @@ export class PostgresAnonymousStore {
|
|
|
3367
3405
|
return null;
|
|
3368
3406
|
}
|
|
3369
3407
|
|
|
3370
|
-
const
|
|
3408
|
+
const updatedAt = now();
|
|
3371
3409
|
const ttlSeconds = parseTtlSeconds(requestedTtlSeconds);
|
|
3372
|
-
const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
3410
|
+
const expiresAt = currentDeploy.ownerId ? null : new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
3373
3411
|
const nextAppBaseDomain = normalizeAppBaseDomain(appBaseDomain ?? currentDeploy.appBaseDomain);
|
|
3374
3412
|
const nextPublicRootUrl = publicRootUrl ?? currentDeploy.publicRootUrl;
|
|
3375
3413
|
const url = appUrlForSlug({ appBaseDomain: nextAppBaseDomain, publicRootUrl: nextPublicRootUrl, slug: currentDeploy.slug });
|
|
3376
|
-
await this.storeArtifact({ artifact, artifactHash, clientBundleBase64, clientBundleHash, createdAt });
|
|
3414
|
+
await this.storeArtifact({ artifact, artifactHash, clientBundleBase64, clientBundleHash, createdAt: updatedAt });
|
|
3377
3415
|
|
|
3378
3416
|
const result = await this.query(
|
|
3379
3417
|
`
|
|
@@ -3384,14 +3422,15 @@ export class PostgresAnonymousStore {
|
|
|
3384
3422
|
expires_at = $4,
|
|
3385
3423
|
public_root_url = $5,
|
|
3386
3424
|
app_base_domain = $6,
|
|
3387
|
-
url = $7
|
|
3425
|
+
url = $7,
|
|
3426
|
+
updated_at = $8
|
|
3388
3427
|
where id = $1
|
|
3389
3428
|
returning *
|
|
3390
3429
|
`,
|
|
3391
|
-
[deployId, artifactHash, clientBundleHash, expiresAt, nextPublicRootUrl, nextAppBaseDomain || null, url]
|
|
3430
|
+
[deployId, artifactHash, clientBundleHash, expiresAt, nextPublicRootUrl, nextAppBaseDomain || null, url, updatedAt]
|
|
3392
3431
|
);
|
|
3393
3432
|
if (serverEnv !== undefined) {
|
|
3394
|
-
await this.replaceServerEnv(deployId, serverEnv,
|
|
3433
|
+
await this.replaceServerEnv(deployId, serverEnv, updatedAt);
|
|
3395
3434
|
}
|
|
3396
3435
|
return this.deployWithUserLimitOverrides(this.rowToDeploy(result.rows[0]));
|
|
3397
3436
|
}
|
|
@@ -3410,17 +3449,20 @@ export class PostgresAnonymousStore {
|
|
|
3410
3449
|
return { deploy: currentDeploy, status: "conflict" };
|
|
3411
3450
|
}
|
|
3412
3451
|
|
|
3413
|
-
const
|
|
3452
|
+
const updatedAt = now();
|
|
3453
|
+
const claimedAt = currentDeploy.claimedAt ?? updatedAt;
|
|
3414
3454
|
const result = await this.query(
|
|
3415
3455
|
`
|
|
3416
3456
|
update deploys
|
|
3417
3457
|
set claimed_at = coalesce(claimed_at, $3),
|
|
3418
3458
|
owner_id = $2,
|
|
3419
|
-
owner_json = $4::jsonb
|
|
3459
|
+
owner_json = $4::jsonb,
|
|
3460
|
+
expires_at = null,
|
|
3461
|
+
updated_at = $5
|
|
3420
3462
|
where id = $1
|
|
3421
3463
|
returning *
|
|
3422
3464
|
`,
|
|
3423
|
-
[deployId, owner.id, claimedAt, JSON.stringify(owner)]
|
|
3465
|
+
[deployId, owner.id, claimedAt, JSON.stringify(owner), updatedAt]
|
|
3424
3466
|
);
|
|
3425
3467
|
await this.rememberUser(owner);
|
|
3426
3468
|
return {
|
|
@@ -3430,14 +3472,16 @@ export class PostgresAnonymousStore {
|
|
|
3430
3472
|
}
|
|
3431
3473
|
|
|
3432
3474
|
async terminateDeploy(deployId) {
|
|
3475
|
+
const updatedAt = now();
|
|
3433
3476
|
const result = await this.query(
|
|
3434
3477
|
`
|
|
3435
3478
|
update deploys
|
|
3436
|
-
set status = 'terminated'
|
|
3479
|
+
set status = 'terminated',
|
|
3480
|
+
updated_at = $2
|
|
3437
3481
|
where id = $1
|
|
3438
3482
|
returning *
|
|
3439
3483
|
`,
|
|
3440
|
-
[deployId]
|
|
3484
|
+
[deployId, updatedAt]
|
|
3441
3485
|
);
|
|
3442
3486
|
return this.deployWithUserLimitOverrides(this.rowToDeploy(result.rows[0]));
|
|
3443
3487
|
}
|
|
@@ -3454,7 +3498,7 @@ export class PostgresAnonymousStore {
|
|
|
3454
3498
|
claimTokenHash: row.claim_token_hash,
|
|
3455
3499
|
clientBundleHash: row.client_bundle_hash,
|
|
3456
3500
|
createdAt: new Date(row.created_at).toISOString(),
|
|
3457
|
-
expiresAt: new Date(row.expires_at).toISOString(),
|
|
3501
|
+
expiresAt: row.expires_at ? new Date(row.expires_at).toISOString() : null,
|
|
3458
3502
|
id: row.id,
|
|
3459
3503
|
limits: row.limits_json,
|
|
3460
3504
|
owner: row.owner_json ?? null,
|
|
@@ -3462,6 +3506,7 @@ export class PostgresAnonymousStore {
|
|
|
3462
3506
|
publicRootUrl: row.public_root_url,
|
|
3463
3507
|
slug: row.slug,
|
|
3464
3508
|
status: row.status,
|
|
3509
|
+
updatedAt: new Date(row.updated_at).toISOString(),
|
|
3465
3510
|
url: row.url
|
|
3466
3511
|
};
|
|
3467
3512
|
}
|
|
@@ -3804,9 +3849,11 @@ async function serveInspect({ artifact, deploy, route, store, systemPath }, res)
|
|
|
3804
3849
|
expiresAt: deploy.expiresAt,
|
|
3805
3850
|
limits: deploy.limits,
|
|
3806
3851
|
mutations: Object.keys(artifact.server.mutations ?? {}),
|
|
3852
|
+
name: artifact.name ?? "Lakebed Capsule",
|
|
3807
3853
|
queries: Object.keys(artifact.server.queries ?? {}),
|
|
3808
3854
|
schema: artifact.server.schema,
|
|
3809
3855
|
slug: deploy.slug,
|
|
3856
|
+
updatedAt: deploy.updatedAt,
|
|
3810
3857
|
url: deploy.url
|
|
3811
3858
|
});
|
|
3812
3859
|
return true;
|
package/src/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import { createServer } from "node:http";
|
|
|
4
4
|
import { existsSync, realpathSync } from "node:fs";
|
|
5
5
|
import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
6
6
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
|
+
import { clearLine, cursorTo } from "node:readline";
|
|
7
8
|
import { createInterface } from "node:readline/promises";
|
|
8
9
|
import { promisify } from "node:util";
|
|
9
10
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -105,6 +106,89 @@ function hasFlag(args, name) {
|
|
|
105
106
|
return args.includes(name);
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
function formatDevUpdateAge(updatedAt, now = new Date()) {
|
|
110
|
+
const elapsedSeconds = Math.max(0, Math.floor((now.getTime() - updatedAt.getTime()) / 1000));
|
|
111
|
+
if (elapsedSeconds < 30) {
|
|
112
|
+
return `${elapsedSeconds} ${elapsedSeconds === 1 ? "second" : "seconds"} ago`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (elapsedSeconds < 60) {
|
|
116
|
+
return "under 1 minute ago";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const elapsedMinutes = Math.floor(elapsedSeconds / 60);
|
|
120
|
+
return `${elapsedMinutes} ${elapsedMinutes === 1 ? "minute" : "minutes"} ago`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function formatOptionalTimestamp(value, fallback = "never") {
|
|
124
|
+
return value || fallback;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function devUpdateRefreshDelay(updatedAt, now = new Date()) {
|
|
128
|
+
const elapsedSeconds = Math.max(0, Math.floor((now.getTime() - updatedAt.getTime()) / 1000));
|
|
129
|
+
return elapsedSeconds < 30 ? 1_000 : 30_000;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function createDevStatusWriter({ quiet = false } = {}) {
|
|
133
|
+
let wroteStatusLine = false;
|
|
134
|
+
let lastUpdatedAt = null;
|
|
135
|
+
let refreshTimer = null;
|
|
136
|
+
|
|
137
|
+
function clearRefreshTimer() {
|
|
138
|
+
if (refreshTimer) {
|
|
139
|
+
clearTimeout(refreshTimer);
|
|
140
|
+
refreshTimer = null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function render() {
|
|
145
|
+
if (quiet || !process.stdout.isTTY || !lastUpdatedAt) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const message = `Live updated with your changes ${formatDevUpdateAge(lastUpdatedAt)}`;
|
|
150
|
+
clearLine(process.stdout, 0);
|
|
151
|
+
cursorTo(process.stdout, 0);
|
|
152
|
+
process.stdout.write(message);
|
|
153
|
+
wroteStatusLine = true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function scheduleRefresh() {
|
|
157
|
+
clearRefreshTimer();
|
|
158
|
+
if (quiet || !process.stdout.isTTY || !lastUpdatedAt) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
refreshTimer = setTimeout(() => {
|
|
163
|
+
refreshTimer = null;
|
|
164
|
+
render();
|
|
165
|
+
scheduleRefresh();
|
|
166
|
+
}, devUpdateRefreshDelay(lastUpdatedAt));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
close() {
|
|
171
|
+
clearRefreshTimer();
|
|
172
|
+
this.finish();
|
|
173
|
+
},
|
|
174
|
+
finish() {
|
|
175
|
+
if (wroteStatusLine && process.stdout.isTTY) {
|
|
176
|
+
process.stdout.write("\n");
|
|
177
|
+
}
|
|
178
|
+
wroteStatusLine = false;
|
|
179
|
+
},
|
|
180
|
+
update(date = new Date()) {
|
|
181
|
+
if (quiet || !process.stdout.isTTY) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
lastUpdatedAt = date;
|
|
186
|
+
render();
|
|
187
|
+
scheduleRefresh();
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
108
192
|
function resolveCapsuleDir(value) {
|
|
109
193
|
if (!value) {
|
|
110
194
|
return root;
|
|
@@ -446,7 +530,8 @@ export async function startDevServer({
|
|
|
446
530
|
let currentBuild = await buildCapsule({ capsuleDir: resolvedCapsuleDir, sourceStore, capsuleId });
|
|
447
531
|
const defaultAuth = await readAuth();
|
|
448
532
|
const stateCell = new StateCell(currentBuild.app.schema);
|
|
449
|
-
const
|
|
533
|
+
const devStatus = createDevStatusWriter({ quiet });
|
|
534
|
+
const logs = new LogBuffer({ beforeConsoleWrite: () => devStatus.finish() });
|
|
450
535
|
const subscriptions = new Map();
|
|
451
536
|
let fileFingerprint = sourceStore ? "" : await capsuleFileFingerprint(resolvedCapsuleDir);
|
|
452
537
|
let rebuildTimer = null;
|
|
@@ -457,7 +542,7 @@ export async function startDevServer({
|
|
|
457
542
|
const nextBuild = await buildCapsule({ capsuleDir: resolvedCapsuleDir, sourceStore, capsuleId });
|
|
458
543
|
currentBuild = nextBuild;
|
|
459
544
|
stateCell.updateSchema(nextBuild.app.schema);
|
|
460
|
-
|
|
545
|
+
devStatus.update();
|
|
461
546
|
for (const client of wss.clients) {
|
|
462
547
|
sendJson(client, { op: "refresh" });
|
|
463
548
|
}
|
|
@@ -675,6 +760,7 @@ export async function startDevServer({
|
|
|
675
760
|
clearTimeout(rebuildTimer);
|
|
676
761
|
}
|
|
677
762
|
await rebuildPromise.catch(() => {});
|
|
763
|
+
devStatus.close();
|
|
678
764
|
for (const client of wss.clients) {
|
|
679
765
|
client.close();
|
|
680
766
|
}
|
|
@@ -1044,7 +1130,8 @@ async function deployCommand(args) {
|
|
|
1044
1130
|
console.log(`${mode === "updated" ? "Updated" : "Created"} anonymous preview.\n`);
|
|
1045
1131
|
}
|
|
1046
1132
|
console.log(`App: ${deployed.url}`);
|
|
1047
|
-
console.log(`
|
|
1133
|
+
console.log(`Updated: ${formatOptionalTimestamp(deployed.updatedAt, "unknown")}`);
|
|
1134
|
+
console.log(`Expires: ${formatOptionalTimestamp(deployed.expiresAt)}`);
|
|
1048
1135
|
if (deployed.claimUrl) {
|
|
1049
1136
|
console.log(`Claim: ${deployed.claimUrl}`);
|
|
1050
1137
|
}
|
|
@@ -1163,7 +1250,8 @@ async function inspectCommand(args) {
|
|
|
1163
1250
|
|
|
1164
1251
|
console.log(`Deploy: ${manifest.deployId}`);
|
|
1165
1252
|
console.log(`URL: ${manifest.url}`);
|
|
1166
|
-
console.log(`
|
|
1253
|
+
console.log(`Updated: ${formatOptionalTimestamp(manifest.updatedAt, "unknown")}`);
|
|
1254
|
+
console.log(`Expires: ${formatOptionalTimestamp(manifest.expiresAt)}`);
|
|
1167
1255
|
console.log(`Artifact: ${manifest.artifactHash}`);
|
|
1168
1256
|
console.log(`Queries: ${manifest.queries.join(", ") || "(none)"}`);
|
|
1169
1257
|
console.log(`Mutations: ${manifest.mutations.join(", ") || "(none)"}`);
|
|
@@ -1301,6 +1389,8 @@ function todoTemplate(name) {
|
|
|
1301
1389
|
import { cleanTodoText } from "../shared/todo";
|
|
1302
1390
|
|
|
1303
1391
|
export default capsule({
|
|
1392
|
+
name: ${JSON.stringify(title)},
|
|
1393
|
+
|
|
1304
1394
|
schema: {
|
|
1305
1395
|
todos: table({
|
|
1306
1396
|
text: string(),
|
package/src/client.js
CHANGED
|
@@ -6,6 +6,7 @@ const AUTH_STORAGE_KEY = "lakebed_identity";
|
|
|
6
6
|
const LEGACY_SHOO_STORAGE_KEY = "shoo_identity";
|
|
7
7
|
const PKCE_STORAGE_KEY = "lakebed_google_pkce";
|
|
8
8
|
const RETURN_TO_STORAGE_KEY = "lakebed_google_return_to";
|
|
9
|
+
const AUTH_RESUME_STORAGE_KEY = "lakebed_google_resume_attempt";
|
|
9
10
|
const PKCE_MAX_AGE_MS = 10 * 60 * 1000;
|
|
10
11
|
const encoder = new TextEncoder();
|
|
11
12
|
|
|
@@ -19,6 +20,7 @@ const pending = new Map();
|
|
|
19
20
|
const activeSubscriptions = new Set();
|
|
20
21
|
let authInitPromise = null;
|
|
21
22
|
let authInitialized = false;
|
|
23
|
+
let authResumeStarted = false;
|
|
22
24
|
let refreshRequested = false;
|
|
23
25
|
|
|
24
26
|
function toGuestName(name) {
|
|
@@ -67,6 +69,18 @@ function browserStorage() {
|
|
|
67
69
|
}
|
|
68
70
|
}
|
|
69
71
|
|
|
72
|
+
function browserSessionStorage() {
|
|
73
|
+
if (typeof window === "undefined") {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
return window.sessionStorage;
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
70
84
|
function currentGuestName() {
|
|
71
85
|
if (typeof window === "undefined") {
|
|
72
86
|
return "local";
|
|
@@ -236,7 +250,11 @@ export function decodeIdentityClaims(idToken) {
|
|
|
236
250
|
return parseJson(decodeBase64Url(parts[1]));
|
|
237
251
|
}
|
|
238
252
|
|
|
239
|
-
function
|
|
253
|
+
function isExpiredClaims(claims) {
|
|
254
|
+
return typeof claims?.exp === "number" && claims.exp * 1000 <= Date.now();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function readStoredIdentity({ allowExpired = false } = {}) {
|
|
240
258
|
const storage = browserStorage();
|
|
241
259
|
if (!storage) {
|
|
242
260
|
return { userId: null };
|
|
@@ -260,12 +278,13 @@ function readStoredIdentity() {
|
|
|
260
278
|
|
|
261
279
|
const token = typeof parsed.token === "string" ? parsed.token : undefined;
|
|
262
280
|
const claims = decodeIdentityClaims(token);
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
return { userId: null };
|
|
281
|
+
const expired = isExpiredClaims(claims);
|
|
282
|
+
if (expired && !allowExpired) {
|
|
283
|
+
return { expired, userId: null };
|
|
266
284
|
}
|
|
267
285
|
|
|
268
286
|
return {
|
|
287
|
+
expired,
|
|
269
288
|
token,
|
|
270
289
|
userId: typeof parsed.userId === "string" ? parsed.userId : (typeof parsed.pairwiseSub === "string" ? parsed.pairwiseSub : null)
|
|
271
290
|
};
|
|
@@ -306,6 +325,19 @@ function clearStoredIdentity() {
|
|
|
306
325
|
}
|
|
307
326
|
}
|
|
308
327
|
|
|
328
|
+
function clearAuthResumeAttempt() {
|
|
329
|
+
const storage = browserSessionStorage();
|
|
330
|
+
if (!storage) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
storage.removeItem(AUTH_RESUME_STORAGE_KEY);
|
|
336
|
+
} catch {
|
|
337
|
+
// Ignore storage failures; resume attempts are best effort.
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
309
341
|
function storedAuthToken() {
|
|
310
342
|
return readStoredIdentity().token ?? "";
|
|
311
343
|
}
|
|
@@ -431,6 +463,7 @@ async function handleGoogleCallback() {
|
|
|
431
463
|
throw new Error("Google sign-in token response was missing identity claims.");
|
|
432
464
|
}
|
|
433
465
|
persistIdentity(token.pairwise_sub, token.id_token, token.expires_in);
|
|
466
|
+
clearAuthResumeAttempt();
|
|
434
467
|
window.sessionStorage.removeItem(PKCE_STORAGE_KEY);
|
|
435
468
|
|
|
436
469
|
const localAuth = createGoogleAuthFromToken(token.id_token);
|
|
@@ -445,11 +478,78 @@ async function handleGoogleCallback() {
|
|
|
445
478
|
return token;
|
|
446
479
|
}
|
|
447
480
|
|
|
481
|
+
function authResumeAttemptKey(identity) {
|
|
482
|
+
const claims = decodeIdentityClaims(identity.token);
|
|
483
|
+
return [identity.userId, claims?.jti, claims?.exp].filter(Boolean).join(":") || identity.token;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function beginStoredGoogleSessionResume() {
|
|
487
|
+
if (typeof window === "undefined" || authResumeStarted) {
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (parseCallback()) {
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const search = new URLSearchParams(window.location.search);
|
|
496
|
+
if (search.has("error") || window.location.pathname === callbackPath()) {
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (search.has("lakebed_guest") || search.has("guest")) {
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const identity = readStoredIdentity({ allowExpired: true });
|
|
505
|
+
if (!identity.token || !identity.expired) {
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const claims = decodeIdentityClaims(identity.token);
|
|
510
|
+
if (!claims?.pairwise_sub && !claims?.sub) {
|
|
511
|
+
clearStoredIdentity();
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const storage = browserSessionStorage();
|
|
516
|
+
const attemptKey = authResumeAttemptKey(identity);
|
|
517
|
+
try {
|
|
518
|
+
if (storage?.getItem(AUTH_RESUME_STORAGE_KEY) === attemptKey) {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
storage?.setItem(AUTH_RESUME_STORAGE_KEY, attemptKey);
|
|
522
|
+
} catch {
|
|
523
|
+
// Without sessionStorage, the page navigation below is still safe.
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
authResumeStarted = true;
|
|
527
|
+
auth = withAuthLoading(createGuestAuth(currentGuestName()), true);
|
|
528
|
+
emitAuth();
|
|
529
|
+
|
|
530
|
+
void signInWithGoogle({ returnTo: currentRoute() }).catch((error) => {
|
|
531
|
+
console.error("[lakebed] Google session resume failed", error);
|
|
532
|
+
authResumeStarted = false;
|
|
533
|
+
clearStoredIdentity();
|
|
534
|
+
clearAuthResumeAttempt();
|
|
535
|
+
auth = withAuthLoading(createGuestAuth(currentGuestName()), false);
|
|
536
|
+
emitAuth();
|
|
537
|
+
reconnect();
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
return true;
|
|
541
|
+
}
|
|
542
|
+
|
|
448
543
|
function ensureAuthInitialized() {
|
|
449
544
|
if (authInitialized) {
|
|
450
545
|
return Promise.resolve();
|
|
451
546
|
}
|
|
452
547
|
|
|
548
|
+
if (beginStoredGoogleSessionResume()) {
|
|
549
|
+
authInitialized = true;
|
|
550
|
+
return Promise.resolve();
|
|
551
|
+
}
|
|
552
|
+
|
|
453
553
|
authInitPromise ??= handleGoogleCallback()
|
|
454
554
|
.catch((error) => {
|
|
455
555
|
console.error("[lakebed] Google sign-in failed", error);
|
|
@@ -501,6 +601,9 @@ function connect() {
|
|
|
501
601
|
const message = JSON.parse(String(event.data));
|
|
502
602
|
|
|
503
603
|
if (message.op === "auth.result") {
|
|
604
|
+
if (authResumeStarted) {
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
504
607
|
auth = withAuthLoading(message.auth, false);
|
|
505
608
|
emitAuth();
|
|
506
609
|
return;
|
|
@@ -592,6 +695,7 @@ export async function signInWithGoogle(options = {}) {
|
|
|
592
695
|
|
|
593
696
|
export function signOut() {
|
|
594
697
|
clearStoredIdentity();
|
|
698
|
+
clearAuthResumeAttempt();
|
|
595
699
|
auth = withAuthLoading(createGuestAuth(currentGuestName()), true);
|
|
596
700
|
emitAuth();
|
|
597
701
|
reconnect();
|
package/src/runtime.js
CHANGED
|
@@ -243,8 +243,9 @@ export class StateCell {
|
|
|
243
243
|
}
|
|
244
244
|
|
|
245
245
|
export class LogBuffer {
|
|
246
|
-
constructor() {
|
|
246
|
+
constructor({ beforeConsoleWrite } = {}) {
|
|
247
247
|
this.entries = [];
|
|
248
|
+
this.beforeConsoleWrite = beforeConsoleWrite;
|
|
248
249
|
}
|
|
249
250
|
|
|
250
251
|
append(level, message, data) {
|
|
@@ -255,6 +256,7 @@ export class LogBuffer {
|
|
|
255
256
|
at: now()
|
|
256
257
|
};
|
|
257
258
|
this.entries.push(entry);
|
|
259
|
+
this.beforeConsoleWrite?.();
|
|
258
260
|
console[level === "error" ? "error" : level === "warn" ? "warn" : "log"](`[lakebed:${level}] ${message}`, data ?? "");
|
|
259
261
|
}
|
|
260
262
|
|
package/src/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const LAKEBED_VERSION = "0.0.
|
|
1
|
+
export const LAKEBED_VERSION = "0.0.14";
|