@springbrand/space 0.2.0-alpha.0 → 0.2.0-alpha.3
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 -2
- package/package.json +1 -1
- package/src/space/deploy-engine.ts +13 -22
- package/src/space/durable-object.ts +10 -247
- package/src/space/workspace-port.ts +0 -1
package/README.md
CHANGED
|
@@ -25,5 +25,4 @@ whether the caller may reach it.
|
|
|
25
25
|
Preview publication writes static assets below one immutable R2 deployment
|
|
26
26
|
prefix and returns that prefix to the host. Static requests can therefore go
|
|
27
27
|
straight to a Workspace CDN; `serveApp()` is the dynamic-only path into the App
|
|
28
|
-
Facet.
|
|
29
|
-
both asset and backend traffic through SpaceDO.
|
|
28
|
+
Facet. SpaceDO never reads static assets for browser requests.
|
package/package.json
CHANGED
|
@@ -304,7 +304,6 @@ async function deployBranch(
|
|
|
304
304
|
compatibilityDate: compatDate,
|
|
305
305
|
},
|
|
306
306
|
staticAssets: assets,
|
|
307
|
-
assetConfig,
|
|
308
307
|
} = bundle
|
|
309
308
|
|
|
310
309
|
const now = Date.now()
|
|
@@ -318,16 +317,14 @@ async function deployBranch(
|
|
|
318
317
|
try {
|
|
319
318
|
ctx.sql.exec(
|
|
320
319
|
`INSERT OR REPLACE INTO deployments
|
|
321
|
-
(branch, commit_hash, main_module, modules,
|
|
322
|
-
|
|
323
|
-
VALUES (?, ?, ?, ?,
|
|
320
|
+
(branch, commit_hash, main_module, modules, asset_deployment_id,
|
|
321
|
+
compatibility_date, deployed_at)
|
|
322
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
324
323
|
deploymentId,
|
|
325
324
|
commitHash,
|
|
326
325
|
mainModule,
|
|
327
326
|
JSON.stringify(serializedModules),
|
|
328
327
|
assets.length > 0 ? assetDeploymentId : "",
|
|
329
|
-
assets.length,
|
|
330
|
-
assetConfig ? JSON.stringify(assetConfig) : "{}",
|
|
331
328
|
compatDate,
|
|
332
329
|
now
|
|
333
330
|
)
|
|
@@ -558,8 +555,7 @@ async function getDeployment(
|
|
|
558
555
|
|
|
559
556
|
const row = ctx.sql
|
|
560
557
|
.exec(
|
|
561
|
-
`SELECT branch, commit_hash, main_module, modules,
|
|
562
|
-
asset_config, deployed_at
|
|
558
|
+
`SELECT branch, commit_hash, main_module, modules, asset_deployment_id, deployed_at
|
|
563
559
|
FROM deployments WHERE branch = ?`,
|
|
564
560
|
branch
|
|
565
561
|
)
|
|
@@ -570,14 +566,13 @@ async function getDeployment(
|
|
|
570
566
|
}
|
|
571
567
|
|
|
572
568
|
const r = row[0]
|
|
573
|
-
const legacyAssets = JSON.parse((r.assets as string) || "{}")
|
|
574
569
|
const assetDeploymentId = (r.asset_deployment_id as string) || ""
|
|
575
570
|
return jsonResponse({
|
|
576
571
|
branch: r.branch as string,
|
|
577
572
|
commit_hash: r.commit_hash as string,
|
|
578
573
|
main_module: r.main_module as string,
|
|
579
574
|
modules: JSON.parse(r.modules as string),
|
|
580
|
-
has_assets:
|
|
575
|
+
has_assets: Boolean(assetDeploymentId),
|
|
581
576
|
deployed_at: new Date(r.deployed_at as number).toISOString(),
|
|
582
577
|
asset_root: assetDeploymentId && ctx.assetPrefix
|
|
583
578
|
? `${ctx.assetPrefix}/deployments/${assetDeploymentId}`
|
|
@@ -590,21 +585,17 @@ async function getDeployment(
|
|
|
590
585
|
async function listDeployments(ctx: DeployContext): Promise<Response> {
|
|
591
586
|
const rows = ctx.sql
|
|
592
587
|
.exec(
|
|
593
|
-
`SELECT branch, commit_hash, main_module,
|
|
588
|
+
`SELECT branch, commit_hash, main_module, asset_deployment_id, deployed_at
|
|
594
589
|
FROM deployments ORDER BY deployed_at DESC`,
|
|
595
590
|
)
|
|
596
591
|
.toArray()
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
has_assets: Number(r.asset_count ?? 0) > 0 || Object.keys(legacyAssets).length > 0,
|
|
605
|
-
deployed_at: new Date(r.deployed_at as number).toISOString(),
|
|
606
|
-
}
|
|
607
|
-
})
|
|
592
|
+
const deployments = rows.map((r) => ({
|
|
593
|
+
branch: r.branch as string,
|
|
594
|
+
commit_hash: r.commit_hash as string,
|
|
595
|
+
main_module: r.main_module as string,
|
|
596
|
+
has_assets: Boolean(r.asset_deployment_id),
|
|
597
|
+
deployed_at: new Date(r.deployed_at as number).toISOString(),
|
|
598
|
+
}))
|
|
608
599
|
|
|
609
600
|
return jsonResponse(deployments)
|
|
610
601
|
}
|
|
@@ -11,21 +11,12 @@ import {
|
|
|
11
11
|
type SpaceDeploymentResult,
|
|
12
12
|
} from "./deploy-engine"
|
|
13
13
|
import { globInfos, readDirInfos, toFileInfo } from "./fileinfo"
|
|
14
|
-
import {
|
|
15
|
-
handleAssetRequest,
|
|
16
|
-
buildAssetManifest,
|
|
17
|
-
createMemoryStorage,
|
|
18
|
-
type AssetConfig,
|
|
19
|
-
type AssetManifest,
|
|
20
|
-
type AssetStorage,
|
|
21
|
-
} from "@cloudflare/worker-bundler"
|
|
22
14
|
import {
|
|
23
15
|
buildInspectorWrapperSource,
|
|
24
16
|
VIBE_APP_MODULE,
|
|
25
17
|
} from "./inspector-wrapper"
|
|
26
18
|
import { ArtifactsBackend, resolveSpaceFsBackendMode, SqlBackend, spaceR2Prefix, type SpaceFsBackend } from "./fs-backend"
|
|
27
19
|
import { createGitFs, stageWorkdir } from "./git-objects"
|
|
28
|
-
import { stripPreviewSecurityHeaders } from "./preview-headers"
|
|
29
20
|
import * as git from "isomorphic-git"
|
|
30
21
|
import {
|
|
31
22
|
defaultSpaceQuota,
|
|
@@ -102,41 +93,9 @@ interface DeploymentRow {
|
|
|
102
93
|
commitHash: string
|
|
103
94
|
mainModule: string
|
|
104
95
|
modules: Record<string, string | Record<string, unknown>>
|
|
105
|
-
legacyAssets: Record<string, string | ArrayBuffer>
|
|
106
|
-
assetDeploymentId: string
|
|
107
|
-
assetCount: number
|
|
108
|
-
assetConfig: AssetConfig
|
|
109
96
|
compatibilityDate: string
|
|
110
97
|
}
|
|
111
98
|
|
|
112
|
-
type SerializedAsset = string | { base64: string }
|
|
113
|
-
|
|
114
|
-
function deserializeAssets(
|
|
115
|
-
assets: Record<string, SerializedAsset>,
|
|
116
|
-
): Record<string, string | ArrayBuffer> {
|
|
117
|
-
return Object.fromEntries(
|
|
118
|
-
Object.entries(assets).map(([path, content]) => [
|
|
119
|
-
path,
|
|
120
|
-
typeof content === "string"
|
|
121
|
-
? content
|
|
122
|
-
: exactArrayBuffer(base64ToBytes(content.base64)),
|
|
123
|
-
]),
|
|
124
|
-
)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function exactArrayBuffer(bytes: Uint8Array): ArrayBuffer {
|
|
128
|
-
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function base64ToBytes(value: string): Uint8Array {
|
|
132
|
-
const binary = atob(value)
|
|
133
|
-
const bytes = new Uint8Array(binary.length)
|
|
134
|
-
for (let index = 0; index < binary.length; index++) {
|
|
135
|
-
bytes[index] = binary.charCodeAt(index)
|
|
136
|
-
}
|
|
137
|
-
return bytes
|
|
138
|
-
}
|
|
139
|
-
|
|
140
99
|
// Overlay-only paths that must never leak into a deploy, rollback tree, or any
|
|
141
100
|
// file listing: git's object store and the ArtifactsFileSystem bookkeeping dir.
|
|
142
101
|
const isReservedPath = isReservedSpacePath
|
|
@@ -149,22 +108,10 @@ const isReservedPath = isReservedSpacePath
|
|
|
149
108
|
// Commits/deploys are mirrored to a per-app Cloudflare Artifacts repo
|
|
150
109
|
// (see artifacts-sync.ts), which is the durable source of truth for history.
|
|
151
110
|
|
|
152
|
-
// Built asset manifest + in-memory storage for a single deployment. Rebuilding
|
|
153
|
-
// these on every request is wasteful (CWE-770 amplification under a preview
|
|
154
|
-
// flood), so we cache them per `branch:commitHash` with an LRU + TTL bound.
|
|
155
|
-
type CachedAssets = {
|
|
156
|
-
manifest: AssetManifest
|
|
157
|
-
storage: AssetStorage
|
|
158
|
-
expiresAt: number
|
|
159
|
-
}
|
|
160
|
-
const ASSET_CACHE_MAX_ENTRIES = 8
|
|
161
|
-
const ASSET_CACHE_TTL_MS = 5 * 60 * 1000 // 5 minutes
|
|
162
|
-
|
|
163
111
|
export class SpaceDO extends DurableObject<Env>
|
|
164
112
|
implements SpaceWorkspacePort, SpaceControlPort, SpaceAppPort {
|
|
165
113
|
private backend!: SpaceFsBackend
|
|
166
114
|
private initializationPromise: Promise<void> | null = null
|
|
167
|
-
private assetCache = new Map<string, CachedAssets>()
|
|
168
115
|
/**
|
|
169
116
|
* One lane for every operation that reads or rewrites the whole tree.
|
|
170
117
|
*
|
|
@@ -220,11 +167,8 @@ export class SpaceDO extends DurableObject<Env>
|
|
|
220
167
|
commit_hash TEXT NOT NULL,
|
|
221
168
|
main_module TEXT NOT NULL,
|
|
222
169
|
modules TEXT NOT NULL,
|
|
223
|
-
assets TEXT NOT NULL DEFAULT '{}',
|
|
224
170
|
asset_deployment_id TEXT NOT NULL DEFAULT '',
|
|
225
|
-
|
|
226
|
-
asset_config TEXT NOT NULL DEFAULT '{}',
|
|
227
|
-
compatibility_date TEXT NOT NULL DEFAULT '',
|
|
171
|
+
compatibility_date TEXT NOT NULL,
|
|
228
172
|
deployed_at INTEGER NOT NULL
|
|
229
173
|
);
|
|
230
174
|
CREATE TABLE IF NOT EXISTS deployment_assets (
|
|
@@ -238,13 +182,6 @@ export class SpaceDO extends DurableObject<Env>
|
|
|
238
182
|
);
|
|
239
183
|
`)
|
|
240
184
|
|
|
241
|
-
// Migrate existing deployments tables that lack new columns
|
|
242
|
-
try { this.ctx.storage.sql.exec(`ALTER TABLE deployments ADD COLUMN assets TEXT NOT NULL DEFAULT '{}'`) } catch {}
|
|
243
|
-
try { this.ctx.storage.sql.exec(`ALTER TABLE deployments ADD COLUMN asset_deployment_id TEXT NOT NULL DEFAULT ''`) } catch {}
|
|
244
|
-
try { this.ctx.storage.sql.exec(`ALTER TABLE deployments ADD COLUMN asset_count INTEGER NOT NULL DEFAULT 0`) } catch {}
|
|
245
|
-
try { this.ctx.storage.sql.exec(`ALTER TABLE deployments ADD COLUMN asset_config TEXT NOT NULL DEFAULT '{}'`) } catch {}
|
|
246
|
-
try { this.ctx.storage.sql.exec(`ALTER TABLE deployments ADD COLUMN compatibility_date TEXT NOT NULL DEFAULT ''`) } catch {}
|
|
247
|
-
|
|
248
185
|
// Initialize git repo if not already done
|
|
249
186
|
try {
|
|
250
187
|
await this.git.init({ defaultBranch: "main" })
|
|
@@ -636,7 +573,6 @@ export class SpaceDO extends DurableObject<Env>
|
|
|
636
573
|
}
|
|
637
574
|
}
|
|
638
575
|
await this.deleteSpilledObjects()
|
|
639
|
-
this.assetCache.clear()
|
|
640
576
|
await this.ctx.storage.deleteAll()
|
|
641
577
|
this.initializationPromise = null
|
|
642
578
|
})
|
|
@@ -1143,10 +1079,6 @@ export class SpaceDO extends DurableObject<Env>
|
|
|
1143
1079
|
const res = await handleDeployCommand(ctx, "deploy", fakeRequest)
|
|
1144
1080
|
const data = await res.json() as Record<string, unknown>
|
|
1145
1081
|
if (typeof data.error === "string") throw new Error(data.error)
|
|
1146
|
-
this.assetCache.clear()
|
|
1147
|
-
const deploymentId = data.branch as string
|
|
1148
|
-
data.preview_url =
|
|
1149
|
-
`/space/${this.spaceName}/preview/${encodeURIComponent(deploymentId)}/`
|
|
1150
1082
|
|
|
1151
1083
|
if (!revision) await this.backend.push(branch)
|
|
1152
1084
|
|
|
@@ -1191,7 +1123,6 @@ export class SpaceDO extends DurableObject<Env>
|
|
|
1191
1123
|
assetPrefix: spaceR2Prefix(this.spaceName),
|
|
1192
1124
|
}
|
|
1193
1125
|
const res = await handleDeployCommand(ctx, "undeploy", fakeRequest)
|
|
1194
|
-
this.assetCache.clear()
|
|
1195
1126
|
return res.json()
|
|
1196
1127
|
}
|
|
1197
1128
|
|
|
@@ -1240,13 +1171,12 @@ export class SpaceDO extends DurableObject<Env>
|
|
|
1240
1171
|
return { fileCount, directoryCount, totalBytes }
|
|
1241
1172
|
}
|
|
1242
1173
|
|
|
1243
|
-
// ── Deployment row reader
|
|
1174
|
+
// ── Deployment row reader ──
|
|
1244
1175
|
|
|
1245
1176
|
private readDeployment(branch: string): DeploymentRow | null {
|
|
1246
1177
|
const rows = this.ctx.storage.sql
|
|
1247
1178
|
.exec(
|
|
1248
|
-
`SELECT branch, commit_hash, main_module, modules,
|
|
1249
|
-
asset_deployment_id, asset_count, asset_config, compatibility_date
|
|
1179
|
+
`SELECT branch, commit_hash, main_module, modules, compatibility_date
|
|
1250
1180
|
FROM deployments WHERE branch = ?`,
|
|
1251
1181
|
branch,
|
|
1252
1182
|
)
|
|
@@ -1258,17 +1188,11 @@ export class SpaceDO extends DurableObject<Env>
|
|
|
1258
1188
|
commitHash: r.commit_hash as string,
|
|
1259
1189
|
mainModule: r.main_module as string,
|
|
1260
1190
|
modules: JSON.parse(r.modules as string) as Record<string, string | Record<string, unknown>>,
|
|
1261
|
-
|
|
1262
|
-
JSON.parse((r.assets as string) || "{}") as Record<string, SerializedAsset>,
|
|
1263
|
-
),
|
|
1264
|
-
assetDeploymentId: (r.asset_deployment_id as string) || "",
|
|
1265
|
-
assetCount: Number(r.asset_count ?? 0),
|
|
1266
|
-
assetConfig: JSON.parse((r.asset_config as string) || "{}") as AssetConfig,
|
|
1267
|
-
compatibilityDate: (r.compatibility_date as string) || FALLBACK_COMPATIBILITY_DATE,
|
|
1191
|
+
compatibilityDate: r.compatibility_date as string,
|
|
1268
1192
|
}
|
|
1269
1193
|
}
|
|
1270
1194
|
|
|
1271
|
-
// ──
|
|
1195
|
+
// ── App serving via Dynamic Workers ─────────────────────────────
|
|
1272
1196
|
//
|
|
1273
1197
|
// Architecture (matches Cloudflare's Durable Object Facets docs example):
|
|
1274
1198
|
//
|
|
@@ -1276,10 +1200,8 @@ export class SpaceDO extends DurableObject<Env>
|
|
|
1276
1200
|
// module. `App.fetch(request)` is the entire backend (Hono /
|
|
1277
1201
|
// itty-router / vanilla — the LLM decides).
|
|
1278
1202
|
// - SpaceDO acts as the supervisor ("AppRunner" in the docs).
|
|
1279
|
-
// `
|
|
1280
|
-
//
|
|
1281
|
-
// `app:<branch>`. Static assets are served host-side; everything
|
|
1282
|
-
// else (including WebSocket upgrades) is forwarded into the Facet.
|
|
1203
|
+
// `serveApp` loads the user's worker via the Worker Loader, extracts the
|
|
1204
|
+
// App class, and hosts it as a Facet keyed `app:<branch>`.
|
|
1283
1205
|
// - State is the Facet's own `ctx.storage` (SQLite + KV). No env.DB
|
|
1284
1206
|
// binding is injected.
|
|
1285
1207
|
// - To make the DB-viewer work without forcing the LLM to write
|
|
@@ -1289,40 +1211,12 @@ export class SpaceDO extends DurableObject<Env>
|
|
|
1289
1211
|
// `App` that adds `__vibeInspectListTables` / `__vibeInspectRead`
|
|
1290
1212
|
// / `__vibeWipe`. The subclass shares the same `ctx.storage`.
|
|
1291
1213
|
|
|
1292
|
-
async servePreview(branch: string, request: Request): Promise<Response> {
|
|
1293
|
-
await this.ensureInit()
|
|
1294
|
-
|
|
1295
|
-
const dep = this.readDeployment(branch)
|
|
1296
|
-
if (!dep) {
|
|
1297
|
-
return new Response(`No deployment found for branch "${branch}"`, { status: 404 })
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
|
-
// Serve static assets host-side before forwarding to the Facet. The built
|
|
1301
|
-
// manifest/storage are cached per deployment so repeat asset reads don't
|
|
1302
|
-
// re-spin the build on every request.
|
|
1303
|
-
if (dep.assetCount > 0 || Object.keys(dep.legacyAssets).length > 0) {
|
|
1304
|
-
const { manifest, storage } = await this.getCachedAssets(dep)
|
|
1305
|
-
const assetResponse = await handleAssetRequest(request, manifest, storage, dep.assetConfig)
|
|
1306
|
-
if (assetResponse) return assetResponse
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
return this.serveDeploymentApp(branch, dep, request)
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
1214
|
async serveApp(branch: string, request: Request): Promise<Response> {
|
|
1313
1215
|
await this.ensureInit()
|
|
1314
1216
|
const dep = this.readDeployment(branch)
|
|
1315
1217
|
if (!dep) {
|
|
1316
1218
|
return new Response(`No deployment found for branch "${branch}"`, { status: 404 })
|
|
1317
1219
|
}
|
|
1318
|
-
return this.serveDeploymentApp(branch, dep, request)
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
private serveDeploymentApp(
|
|
1322
|
-
branch: string,
|
|
1323
|
-
dep: DeploymentRow,
|
|
1324
|
-
request: Request,
|
|
1325
|
-
): Response | Promise<Response> {
|
|
1326
1220
|
let appClass: DurableObjectClass
|
|
1327
1221
|
try {
|
|
1328
1222
|
appClass = this.loadAppClass(dep)
|
|
@@ -1337,73 +1231,6 @@ export class SpaceDO extends DurableObject<Env>
|
|
|
1337
1231
|
return facet.fetch(request)
|
|
1338
1232
|
}
|
|
1339
1233
|
|
|
1340
|
-
/**
|
|
1341
|
-
* Return the built asset manifest + in-memory storage for a deployment,
|
|
1342
|
-
* reusing a cached build when available. Keyed by `branch:commitHash` so a
|
|
1343
|
-
* redeploy (new commit) transparently rebuilds. Bounded by an LRU cap + TTL.
|
|
1344
|
-
*/
|
|
1345
|
-
private async getCachedAssets(dep: DeploymentRow): Promise<CachedAssets> {
|
|
1346
|
-
const key = `${dep.branch}:${dep.assetDeploymentId || dep.commitHash}`
|
|
1347
|
-
const now = Date.now()
|
|
1348
|
-
|
|
1349
|
-
const cached = this.assetCache.get(key)
|
|
1350
|
-
if (cached && cached.expiresAt > now) {
|
|
1351
|
-
// Refresh LRU recency.
|
|
1352
|
-
this.assetCache.delete(key)
|
|
1353
|
-
this.assetCache.set(key, cached)
|
|
1354
|
-
return cached
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
let manifest: AssetManifest
|
|
1358
|
-
let storage: AssetStorage
|
|
1359
|
-
if (dep.assetDeploymentId) {
|
|
1360
|
-
const bucket = this.env.WORKSPACE_R2
|
|
1361
|
-
if (!bucket) throw new Error("Static preview requires the WORKSPACE_R2 binding")
|
|
1362
|
-
const rows = this.ctx.storage.sql
|
|
1363
|
-
.exec(
|
|
1364
|
-
`SELECT path, r2_key, content_type, etag
|
|
1365
|
-
FROM deployment_assets WHERE deployment_id = ?`,
|
|
1366
|
-
dep.assetDeploymentId,
|
|
1367
|
-
)
|
|
1368
|
-
.toArray()
|
|
1369
|
-
const keys = new Map<string, string>()
|
|
1370
|
-
manifest = new Map()
|
|
1371
|
-
for (const row of rows) {
|
|
1372
|
-
const path = row.path as string
|
|
1373
|
-
keys.set(path, row.r2_key as string)
|
|
1374
|
-
manifest.set(path, {
|
|
1375
|
-
contentType: (row.content_type as string | null) ?? undefined,
|
|
1376
|
-
etag: row.etag as string,
|
|
1377
|
-
})
|
|
1378
|
-
}
|
|
1379
|
-
storage = {
|
|
1380
|
-
async get(pathname) {
|
|
1381
|
-
const r2Key = keys.get(pathname)
|
|
1382
|
-
if (!r2Key) return null
|
|
1383
|
-
return (await bucket.get(r2Key))?.body ?? null
|
|
1384
|
-
},
|
|
1385
|
-
}
|
|
1386
|
-
} else {
|
|
1387
|
-
manifest = await buildAssetManifest(dep.legacyAssets)
|
|
1388
|
-
storage = createMemoryStorage(dep.legacyAssets)
|
|
1389
|
-
}
|
|
1390
|
-
const entry: CachedAssets = { manifest, storage, expiresAt: now + ASSET_CACHE_TTL_MS }
|
|
1391
|
-
|
|
1392
|
-
this.assetCache.set(key, entry)
|
|
1393
|
-
|
|
1394
|
-
// Evict expired / oldest entries to keep the cache bounded.
|
|
1395
|
-
for (const [k, v] of this.assetCache) {
|
|
1396
|
-
if (v.expiresAt <= now) this.assetCache.delete(k)
|
|
1397
|
-
}
|
|
1398
|
-
while (this.assetCache.size > ASSET_CACHE_MAX_ENTRIES) {
|
|
1399
|
-
const oldest = this.assetCache.keys().next().value
|
|
1400
|
-
if (oldest === undefined) break
|
|
1401
|
-
this.assetCache.delete(oldest)
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
return entry
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
1234
|
// ── App-class loader ────────────────────────────────────────────
|
|
1408
1235
|
//
|
|
1409
1236
|
// Loads the dynamic worker for `branch`'s latest deployment with the
|
|
@@ -1428,7 +1255,7 @@ export class SpaceDO extends DurableObject<Env>
|
|
|
1428
1255
|
// The date the app declared, not a date frozen into this file. A worker
|
|
1429
1256
|
// built against one runtime and run under another is a bug the author
|
|
1430
1257
|
// cannot see or fix.
|
|
1431
|
-
compatibilityDate: dep.compatibilityDate
|
|
1258
|
+
compatibilityDate: dep.compatibilityDate,
|
|
1432
1259
|
// Generated code gets no outbound network. Omitting this inherits the
|
|
1433
1260
|
// parent Worker's full internet access, which would let an app the model
|
|
1434
1261
|
// wrote reach anything this Worker can — including internal services.
|
|
@@ -1503,40 +1330,12 @@ export class SpaceDO extends DurableObject<Env>
|
|
|
1503
1330
|
return { ok: true }
|
|
1504
1331
|
}
|
|
1505
1332
|
|
|
1506
|
-
// ──
|
|
1333
|
+
// ── Internal deploy commands ──
|
|
1507
1334
|
|
|
1508
1335
|
async fetch(request: Request): Promise<Response> {
|
|
1509
1336
|
await this.ensureInit()
|
|
1510
1337
|
|
|
1511
|
-
const
|
|
1512
|
-
const path = url.pathname
|
|
1513
|
-
|
|
1514
|
-
// Preview routes: /space/:name/preview/:branch/*
|
|
1515
|
-
const previewMatch = path.match(/\/preview\/([^/]+)(\/.*)?$/)
|
|
1516
|
-
if (previewMatch) {
|
|
1517
|
-
const branch = decodeURIComponent(previewMatch[1])
|
|
1518
|
-
const spaceName = this.ctx.id.name ?? "space"
|
|
1519
|
-
const basePath = `/space/${spaceName}/preview/${encodeURIComponent(branch)}`
|
|
1520
|
-
|
|
1521
|
-
// Rewrite the URL so the dynamic worker sees a clean path
|
|
1522
|
-
const subPath = previewMatch[2] || "/"
|
|
1523
|
-
const previewUrl = new URL(subPath, url.origin)
|
|
1524
|
-
previewUrl.search = url.search
|
|
1525
|
-
const previewRequest = new Request(previewUrl.toString(), request)
|
|
1526
|
-
const response = await this.servePreview(branch, previewRequest)
|
|
1527
|
-
|
|
1528
|
-
// Strip headers a generated app must not be able to set on the shared
|
|
1529
|
-
// preview origin (e.g. Service-Worker-Allowed scope expansion) before
|
|
1530
|
-
// any further rewriting.
|
|
1531
|
-
const safeResponse = stripPreviewSecurityHeaders(response)
|
|
1532
|
-
|
|
1533
|
-
// Rewrite root-relative paths in HTML responses so they resolve
|
|
1534
|
-
// correctly when the preview is mounted on a sub-path
|
|
1535
|
-
return rewritePreviewResponse(safeResponse, basePath)
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
// Deploy command routes
|
|
1539
|
-
const cmd = url.searchParams.get("cmd")
|
|
1338
|
+
const cmd = new URL(request.url).searchParams.get("cmd")
|
|
1540
1339
|
if (cmd && ["deploy", "get_deployment", "list_deployments", "undeploy"].includes(cmd)) {
|
|
1541
1340
|
const deployCtx: DeployContext = {
|
|
1542
1341
|
sql: this.ctx.storage.sql,
|
|
@@ -1552,39 +1351,6 @@ export class SpaceDO extends DurableObject<Env>
|
|
|
1552
1351
|
}
|
|
1553
1352
|
}
|
|
1554
1353
|
|
|
1555
|
-
// ─── Preview Response Rewriting ──────────────────────────────────────────────
|
|
1556
|
-
// When a preview is served on /space/:name/preview/:branch/, root-relative
|
|
1557
|
-
// paths like /style.css in HTML would resolve to the domain root instead of
|
|
1558
|
-
// the preview path. We rewrite them so the browser fetches the correct URL.
|
|
1559
|
-
|
|
1560
|
-
function rewritePreviewResponse(response: Response, basePath: string): Response {
|
|
1561
|
-
// Rewrite Location header on redirects
|
|
1562
|
-
const location = response.headers.get("location")
|
|
1563
|
-
if (location?.startsWith("/")) {
|
|
1564
|
-
const rewritten = new Response(response.body, response)
|
|
1565
|
-
rewritten.headers.set("location", basePath + location)
|
|
1566
|
-
return rewritten
|
|
1567
|
-
}
|
|
1568
|
-
|
|
1569
|
-
// Only rewrite HTML responses
|
|
1570
|
-
const ct = response.headers.get("content-type") ?? ""
|
|
1571
|
-
if (!ct.includes("text/html")) return response
|
|
1572
|
-
|
|
1573
|
-
// Use HTMLRewriter to prefix root-relative src/href/action attributes
|
|
1574
|
-
return new HTMLRewriter()
|
|
1575
|
-
.on("[src],[href],[action]", {
|
|
1576
|
-
element(el) {
|
|
1577
|
-
for (const attr of ["src", "href", "action"] as const) {
|
|
1578
|
-
const val = el.getAttribute(attr)
|
|
1579
|
-
if (val?.startsWith("/") && !val.startsWith("//")) {
|
|
1580
|
-
el.setAttribute(attr, basePath + val)
|
|
1581
|
-
}
|
|
1582
|
-
}
|
|
1583
|
-
},
|
|
1584
|
-
})
|
|
1585
|
-
.transform(response)
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
1354
|
// ─── Facet naming ───────────────────────────────────────────────────────────
|
|
1589
1355
|
|
|
1590
1356
|
function facetNameForApp(branch: string): string {
|
|
@@ -1596,9 +1362,6 @@ function facetNameForApp(branch: string): string {
|
|
|
1596
1362
|
/** Git's oid for an empty tree — what a repository with no content writes. */
|
|
1597
1363
|
const EMPTY_TREE_OID = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
|
|
1598
1364
|
|
|
1599
|
-
/** Used only for deployments recorded before the date was persisted. */
|
|
1600
|
-
const FALLBACK_COMPATIBILITY_DATE = "2025-04-01"
|
|
1601
|
-
|
|
1602
1365
|
const SPACE_AUTHOR = Object.freeze({
|
|
1603
1366
|
name: "Workspace",
|
|
1604
1367
|
email: "workspace@springbrand.local",
|
|
@@ -255,7 +255,6 @@ export interface SpaceAppPort {
|
|
|
255
255
|
revision?: string,
|
|
256
256
|
runtimeConfig?: DeploymentRuntimeConfig,
|
|
257
257
|
): Promise<BranchDeploymentBundle>;
|
|
258
|
-
servePreview(branch: string, request: Request): Promise<Response>;
|
|
259
258
|
serveApp(branch: string, request: Request): Promise<Response>;
|
|
260
259
|
listAppTables(branch: string): Promise<unknown>;
|
|
261
260
|
queryAppTable(branch: string, table: string, opts?: unknown): Promise<unknown>;
|