@springbrand/space 0.1.0-alpha.9 → 0.2.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -7
- package/package.json +1 -1
- package/src/index.ts +11 -1
- package/src/space/deploy-engine.ts +87 -15
- package/src/space/durable-object.ts +54 -2
- package/src/space/workspace-port.ts +19 -1
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @springbrand/space
|
|
2
2
|
|
|
3
|
-
SpaceDO — a git-backed Durable Object workspace with
|
|
4
|
-
Facet database inspection.
|
|
3
|
+
SpaceDO — a git-backed Durable Object workspace with target-neutral deployment
|
|
4
|
+
bundles, CDN-backed Preview assets, App Facet execution and database inspection.
|
|
5
5
|
|
|
6
6
|
## Provenance
|
|
7
7
|
|
|
@@ -11,14 +11,19 @@ This package is a tracked fork of the VibeSDK `space/` package.
|
|
|
11
11
|
- Frozen at: `main@a318f08625db`
|
|
12
12
|
- Licence: MIT (see `LICENSE`)
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
path-level restore and `destroySpace`.
|
|
14
|
+
The fork retains VibeSDK's Workspace and App Facet model while replacing its
|
|
15
|
+
single-row Preview assets with per-file R2 objects. One `DeploymentBundle`
|
|
16
|
+
separates Worker modules, static asset metadata and deployment-level runtime
|
|
17
|
+
configuration so Preview and production publishers can consume the same build.
|
|
19
18
|
|
|
20
19
|
## Boundary
|
|
21
20
|
|
|
22
21
|
SpaceDO is infrastructure. It knows nothing about users, Chats or
|
|
23
22
|
authorisation — the host Worker decides which Space a request belongs to and
|
|
24
23
|
whether the caller may reach it.
|
|
24
|
+
|
|
25
|
+
Preview publication writes static assets below one immutable R2 deployment
|
|
26
|
+
prefix and returns that prefix to the host. Static requests can therefore go
|
|
27
|
+
straight to a Workspace CDN; `serveApp()` is the dynamic-only path into the App
|
|
28
|
+
Facet. `servePreview()` remains available for hosts that intentionally proxy
|
|
29
|
+
both asset and backend traffic through SpaceDO.
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -18,7 +18,17 @@ export type {
|
|
|
18
18
|
SpaceWorkspacePort,
|
|
19
19
|
} from "./space/durable-object";
|
|
20
20
|
export { SPACE_RESERVED_PREFIXES } from "./space/durable-object";
|
|
21
|
-
export
|
|
21
|
+
export {
|
|
22
|
+
deploymentRuntimeConfigSource,
|
|
23
|
+
RUNTIME_CONFIG_ASSET_PATH,
|
|
24
|
+
} from "./space/deploy-engine";
|
|
25
|
+
export type {
|
|
26
|
+
BranchDeploymentBundle,
|
|
27
|
+
DeploymentAsset,
|
|
28
|
+
DeploymentBundle,
|
|
29
|
+
DeploymentRuntimeConfig,
|
|
30
|
+
SpaceDeploymentResult,
|
|
31
|
+
} from "./space/deploy-engine";
|
|
22
32
|
|
|
23
33
|
// ── Preview hardening ─────────────────────────────────────────────
|
|
24
34
|
export { stripPreviewSecurityHeaders, STRIPPED_PREVIEW_HEADERS } from "./space/preview-headers";
|
|
@@ -32,18 +32,40 @@ export interface DeployContext {
|
|
|
32
32
|
assetPrefix?: string
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export interface
|
|
35
|
+
export interface DeploymentBundle {
|
|
36
36
|
branch: string
|
|
37
37
|
commitHash: string
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
workerModules: {
|
|
39
|
+
mainModule: string
|
|
40
|
+
modules: Record<string, string | Record<string, unknown>>
|
|
41
|
+
compatibilityDate: string
|
|
42
|
+
}
|
|
43
|
+
staticAssets: DeploymentAsset[]
|
|
41
44
|
assetConfig: AssetConfig | undefined
|
|
42
|
-
|
|
45
|
+
runtimeConfig: DeploymentRuntimeConfig | undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type BranchDeploymentBundle = DeploymentBundle
|
|
49
|
+
|
|
50
|
+
export interface DeploymentRuntimeConfig {
|
|
51
|
+
apiBaseUrl: string
|
|
43
52
|
}
|
|
44
53
|
|
|
54
|
+
export interface SpaceDeploymentResult {
|
|
55
|
+
branch: string
|
|
56
|
+
commit_hash: string
|
|
57
|
+
main_module: string
|
|
58
|
+
has_assets: boolean
|
|
59
|
+
compatibility_date: string
|
|
60
|
+
deployed_at: string
|
|
61
|
+
asset_root: string | null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const RUNTIME_CONFIG_ASSET_PATH = "/.springbrand/runtime-config.js"
|
|
65
|
+
|
|
45
66
|
export interface DeploymentAsset {
|
|
46
67
|
path: string
|
|
68
|
+
sourcePath: string | null
|
|
47
69
|
contentType: string | undefined
|
|
48
70
|
etag: string
|
|
49
71
|
size: number
|
|
@@ -173,6 +195,7 @@ async function readBranchFiles(
|
|
|
173
195
|
const bytes = await ctx.fs.readFileBytes(entry.sourcePath)
|
|
174
196
|
const asset: Omit<DeploymentAsset, "etag"> = {
|
|
175
197
|
path: toAssetPath(entry.path, assetsDirectory!),
|
|
198
|
+
sourcePath: entry.sourcePath,
|
|
176
199
|
contentType: inferContentType(entry.path),
|
|
177
200
|
size: bytes.byteLength,
|
|
178
201
|
}
|
|
@@ -219,6 +242,7 @@ async function deployBranch(
|
|
|
219
242
|
branch: string
|
|
220
243
|
appRoot?: string
|
|
221
244
|
revision?: string
|
|
245
|
+
runtimeConfig?: DeploymentRuntimeConfig
|
|
222
246
|
}
|
|
223
247
|
const branch = body.branch
|
|
224
248
|
if (!branch) {
|
|
@@ -257,6 +281,7 @@ async function deployBranch(
|
|
|
257
281
|
)
|
|
258
282
|
return uploaded.etag
|
|
259
283
|
},
|
|
284
|
+
body.runtimeConfig,
|
|
260
285
|
)
|
|
261
286
|
} catch (e) {
|
|
262
287
|
await discardAssetDeployment(ctx, assetDeploymentId, assetRoot).catch((cleanupError) => {
|
|
@@ -273,11 +298,13 @@ async function deployBranch(
|
|
|
273
298
|
const {
|
|
274
299
|
branch: deploymentId,
|
|
275
300
|
commitHash,
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
301
|
+
workerModules: {
|
|
302
|
+
mainModule,
|
|
303
|
+
modules: serializedModules,
|
|
304
|
+
compatibilityDate: compatDate,
|
|
305
|
+
},
|
|
306
|
+
staticAssets: assets,
|
|
279
307
|
assetConfig,
|
|
280
|
-
compatibilityDate: compatDate,
|
|
281
308
|
} = bundle
|
|
282
309
|
|
|
283
310
|
const now = Date.now()
|
|
@@ -333,6 +360,7 @@ async function deployBranch(
|
|
|
333
360
|
has_assets: assets.length > 0,
|
|
334
361
|
compatibility_date: compatDate,
|
|
335
362
|
deployed_at: new Date(now).toISOString(),
|
|
363
|
+
asset_root: assets.length > 0 ? assetRoot : null,
|
|
336
364
|
})
|
|
337
365
|
}
|
|
338
366
|
|
|
@@ -342,6 +370,7 @@ export async function buildBranchDeployment(
|
|
|
342
370
|
appRoot?: string | null,
|
|
343
371
|
revision?: string,
|
|
344
372
|
writeAsset?: AssetWriter,
|
|
373
|
+
runtimeConfig?: DeploymentRuntimeConfig,
|
|
345
374
|
): Promise<BranchDeploymentBundle> {
|
|
346
375
|
const root = normalizeAppRoot(appRoot)
|
|
347
376
|
const { commitHash, files, assets, config } = await readBranchFiles(
|
|
@@ -351,6 +380,22 @@ export async function buildBranchDeployment(
|
|
|
351
380
|
revision,
|
|
352
381
|
writeAsset,
|
|
353
382
|
)
|
|
383
|
+
const normalizedRuntimeConfig = normalizeRuntimeConfig(runtimeConfig)
|
|
384
|
+
if (normalizedRuntimeConfig) {
|
|
385
|
+
const bytes = new TextEncoder().encode(
|
|
386
|
+
deploymentRuntimeConfigSource(normalizedRuntimeConfig),
|
|
387
|
+
)
|
|
388
|
+
const asset: Omit<DeploymentAsset, "etag"> = {
|
|
389
|
+
path: RUNTIME_CONFIG_ASSET_PATH,
|
|
390
|
+
sourcePath: null,
|
|
391
|
+
contentType: "application/javascript; charset=utf-8",
|
|
392
|
+
size: bytes.byteLength,
|
|
393
|
+
}
|
|
394
|
+
const etag = writeAsset
|
|
395
|
+
? await writeAsset(asset, bytes)
|
|
396
|
+
: await computeAssetEtag(bytes)
|
|
397
|
+
assets.push({ ...asset, etag })
|
|
398
|
+
}
|
|
354
399
|
if (Object.keys(files).length === 0) {
|
|
355
400
|
throw new Error(
|
|
356
401
|
root
|
|
@@ -364,17 +409,40 @@ export async function buildBranchDeployment(
|
|
|
364
409
|
return {
|
|
365
410
|
branch: revision ?? branch,
|
|
366
411
|
commitHash,
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
412
|
+
workerModules: {
|
|
413
|
+
mainModule: result.mainModule,
|
|
414
|
+
modules: result.modules,
|
|
415
|
+
compatibilityDate: result.compatibilityDate,
|
|
416
|
+
},
|
|
417
|
+
staticAssets: assets,
|
|
370
418
|
assetConfig: result.assetConfig,
|
|
371
|
-
|
|
419
|
+
runtimeConfig: normalizedRuntimeConfig,
|
|
372
420
|
}
|
|
373
421
|
} catch (e) {
|
|
374
422
|
throw new Error(`Build failed: ${e instanceof Error ? e.message : String(e)}`)
|
|
375
423
|
}
|
|
376
424
|
}
|
|
377
425
|
|
|
426
|
+
function normalizeRuntimeConfig(
|
|
427
|
+
config: DeploymentRuntimeConfig | undefined,
|
|
428
|
+
): DeploymentRuntimeConfig | undefined {
|
|
429
|
+
if (!config) return undefined
|
|
430
|
+
let url: URL
|
|
431
|
+
try {
|
|
432
|
+
url = new URL(config.apiBaseUrl)
|
|
433
|
+
} catch {
|
|
434
|
+
throw new Error("runtimeConfig.apiBaseUrl must be an absolute HTTP(S) URL ending in /")
|
|
435
|
+
}
|
|
436
|
+
if (!["http:", "https:"].includes(url.protocol) || !url.pathname.endsWith("/")) {
|
|
437
|
+
throw new Error("runtimeConfig.apiBaseUrl must be an absolute HTTP(S) URL ending in /")
|
|
438
|
+
}
|
|
439
|
+
return { apiBaseUrl: url.toString() }
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export function deploymentRuntimeConfigSource(config: DeploymentRuntimeConfig): string {
|
|
443
|
+
return `globalThis.__APP_CONFIG__=Object.freeze(${JSON.stringify(config)});`
|
|
444
|
+
}
|
|
445
|
+
|
|
378
446
|
async function buildWebsite(
|
|
379
447
|
sourceFiles: Record<string, string>,
|
|
380
448
|
config: ParsedWranglerConfig,
|
|
@@ -463,7 +531,7 @@ function toAssetPath(path: string, directory: string): string {
|
|
|
463
531
|
|
|
464
532
|
async function computeAssetEtag(bytes: Uint8Array): Promise<string> {
|
|
465
533
|
const digest = await crypto.subtle.digest("SHA-256", exactBuffer(bytes))
|
|
466
|
-
return [...new Uint8Array(digest).slice(0,
|
|
534
|
+
return [...new Uint8Array(digest).slice(0, 16)]
|
|
467
535
|
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
468
536
|
.join("")
|
|
469
537
|
}
|
|
@@ -490,7 +558,7 @@ async function getDeployment(
|
|
|
490
558
|
|
|
491
559
|
const row = ctx.sql
|
|
492
560
|
.exec(
|
|
493
|
-
`SELECT branch, commit_hash, main_module, modules, assets, asset_count,
|
|
561
|
+
`SELECT branch, commit_hash, main_module, modules, assets, asset_deployment_id, asset_count,
|
|
494
562
|
asset_config, deployed_at
|
|
495
563
|
FROM deployments WHERE branch = ?`,
|
|
496
564
|
branch
|
|
@@ -503,6 +571,7 @@ async function getDeployment(
|
|
|
503
571
|
|
|
504
572
|
const r = row[0]
|
|
505
573
|
const legacyAssets = JSON.parse((r.assets as string) || "{}")
|
|
574
|
+
const assetDeploymentId = (r.asset_deployment_id as string) || ""
|
|
506
575
|
return jsonResponse({
|
|
507
576
|
branch: r.branch as string,
|
|
508
577
|
commit_hash: r.commit_hash as string,
|
|
@@ -510,6 +579,9 @@ async function getDeployment(
|
|
|
510
579
|
modules: JSON.parse(r.modules as string),
|
|
511
580
|
has_assets: Number(r.asset_count ?? 0) > 0 || Object.keys(legacyAssets).length > 0,
|
|
512
581
|
deployed_at: new Date(r.deployed_at as number).toISOString(),
|
|
582
|
+
asset_root: assetDeploymentId && ctx.assetPrefix
|
|
583
|
+
? `${ctx.assetPrefix}/deployments/${assetDeploymentId}`
|
|
584
|
+
: null,
|
|
513
585
|
})
|
|
514
586
|
}
|
|
515
587
|
|
|
@@ -6,7 +6,9 @@ import {
|
|
|
6
6
|
buildBranchDeployment,
|
|
7
7
|
handleDeployCommand,
|
|
8
8
|
type BranchDeploymentBundle,
|
|
9
|
+
type DeploymentRuntimeConfig,
|
|
9
10
|
type DeployContext,
|
|
11
|
+
type SpaceDeploymentResult,
|
|
10
12
|
} from "./deploy-engine"
|
|
11
13
|
import { globInfos, readDirInfos, toFileInfo } from "./fileinfo"
|
|
12
14
|
import {
|
|
@@ -1109,7 +1111,12 @@ export class SpaceDO extends DurableObject<Env>
|
|
|
1109
1111
|
* analysis Space with a spreadsheet and a script deploys nothing until
|
|
1110
1112
|
* someone names the directory that holds a site.
|
|
1111
1113
|
*/
|
|
1112
|
-
async deploy(
|
|
1114
|
+
async deploy(
|
|
1115
|
+
branch: string,
|
|
1116
|
+
appRoot?: string,
|
|
1117
|
+
revision?: string,
|
|
1118
|
+
runtimeConfig?: DeploymentRuntimeConfig,
|
|
1119
|
+
): Promise<SpaceDeploymentResult> {
|
|
1113
1120
|
await this.ensureInit()
|
|
1114
1121
|
return this.lane(async () => {
|
|
1115
1122
|
// The deploy engine reads the full branch tree, so ensure the Artifacts
|
|
@@ -1123,6 +1130,7 @@ export class SpaceDO extends DurableObject<Env>
|
|
|
1123
1130
|
branch,
|
|
1124
1131
|
...(appRoot ? { appRoot } : {}),
|
|
1125
1132
|
...(revision ? { revision } : {}),
|
|
1133
|
+
...(runtimeConfig ? { runtimeConfig } : {}),
|
|
1126
1134
|
}),
|
|
1127
1135
|
})
|
|
1128
1136
|
const ctx: DeployContext = {
|
|
@@ -1142,7 +1150,7 @@ export class SpaceDO extends DurableObject<Env>
|
|
|
1142
1150
|
|
|
1143
1151
|
if (!revision) await this.backend.push(branch)
|
|
1144
1152
|
|
|
1145
|
-
return data
|
|
1153
|
+
return spaceDeploymentResult(data)
|
|
1146
1154
|
})
|
|
1147
1155
|
}
|
|
1148
1156
|
|
|
@@ -1150,6 +1158,7 @@ export class SpaceDO extends DurableObject<Env>
|
|
|
1150
1158
|
branch: string,
|
|
1151
1159
|
appRoot?: string,
|
|
1152
1160
|
revision?: string,
|
|
1161
|
+
runtimeConfig?: DeploymentRuntimeConfig,
|
|
1153
1162
|
): Promise<BranchDeploymentBundle> {
|
|
1154
1163
|
await this.ensureInit()
|
|
1155
1164
|
await this.materializeAll()
|
|
@@ -1162,6 +1171,8 @@ export class SpaceDO extends DurableObject<Env>
|
|
|
1162
1171
|
branch,
|
|
1163
1172
|
appRoot,
|
|
1164
1173
|
revision,
|
|
1174
|
+
undefined,
|
|
1175
|
+
runtimeConfig,
|
|
1165
1176
|
)
|
|
1166
1177
|
}
|
|
1167
1178
|
|
|
@@ -1203,6 +1214,7 @@ export class SpaceDO extends DurableObject<Env>
|
|
|
1203
1214
|
sql: this.ctx.storage.sql,
|
|
1204
1215
|
git: this.git,
|
|
1205
1216
|
fs: this.overlay,
|
|
1217
|
+
assetPrefix: spaceR2Prefix(this.spaceName),
|
|
1206
1218
|
}
|
|
1207
1219
|
const res = await handleDeployCommand(ctx, "get_deployment", fakeRequest)
|
|
1208
1220
|
return res.json()
|
|
@@ -1294,6 +1306,23 @@ export class SpaceDO extends DurableObject<Env>
|
|
|
1294
1306
|
if (assetResponse) return assetResponse
|
|
1295
1307
|
}
|
|
1296
1308
|
|
|
1309
|
+
return this.serveDeploymentApp(branch, dep, request)
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
async serveApp(branch: string, request: Request): Promise<Response> {
|
|
1313
|
+
await this.ensureInit()
|
|
1314
|
+
const dep = this.readDeployment(branch)
|
|
1315
|
+
if (!dep) {
|
|
1316
|
+
return new Response(`No deployment found for branch "${branch}"`, { status: 404 })
|
|
1317
|
+
}
|
|
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> {
|
|
1297
1326
|
let appClass: DurableObjectClass
|
|
1298
1327
|
try {
|
|
1299
1328
|
appClass = this.loadAppClass(dep)
|
|
@@ -1610,6 +1639,29 @@ function byteLength(content: string): number {
|
|
|
1610
1639
|
return new TextEncoder().encode(content).byteLength
|
|
1611
1640
|
}
|
|
1612
1641
|
|
|
1642
|
+
function spaceDeploymentResult(data: Record<string, unknown>): SpaceDeploymentResult {
|
|
1643
|
+
if (
|
|
1644
|
+
typeof data.branch !== "string" ||
|
|
1645
|
+
typeof data.commit_hash !== "string" ||
|
|
1646
|
+
typeof data.main_module !== "string" ||
|
|
1647
|
+
typeof data.has_assets !== "boolean" ||
|
|
1648
|
+
typeof data.compatibility_date !== "string" ||
|
|
1649
|
+
typeof data.deployed_at !== "string" ||
|
|
1650
|
+
!(typeof data.asset_root === "string" || data.asset_root === null)
|
|
1651
|
+
) {
|
|
1652
|
+
throw new Error("Invalid deployment result")
|
|
1653
|
+
}
|
|
1654
|
+
return {
|
|
1655
|
+
branch: data.branch,
|
|
1656
|
+
commit_hash: data.commit_hash,
|
|
1657
|
+
main_module: data.main_module,
|
|
1658
|
+
has_assets: data.has_assets,
|
|
1659
|
+
compatibility_date: data.compatibility_date,
|
|
1660
|
+
deployed_at: data.deployed_at,
|
|
1661
|
+
asset_root: data.asset_root,
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1613
1665
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
1614
1666
|
|
|
1615
1667
|
interface PatchEdit {
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BranchDeploymentBundle,
|
|
3
|
+
DeploymentRuntimeConfig,
|
|
4
|
+
SpaceDeploymentResult,
|
|
5
|
+
} from "./deploy-engine";
|
|
6
|
+
|
|
1
7
|
/**
|
|
2
8
|
* The production Workspace contract a Space must satisfy.
|
|
3
9
|
*
|
|
@@ -234,11 +240,23 @@ export interface SpaceControlPort {
|
|
|
234
240
|
* and only an explicit deploy brings an app into existence.
|
|
235
241
|
*/
|
|
236
242
|
export interface SpaceAppPort {
|
|
237
|
-
deploy(
|
|
243
|
+
deploy(
|
|
244
|
+
branch: string,
|
|
245
|
+
appRoot?: string,
|
|
246
|
+
revision?: string,
|
|
247
|
+
runtimeConfig?: DeploymentRuntimeConfig,
|
|
248
|
+
): Promise<SpaceDeploymentResult>;
|
|
238
249
|
undeploy(branch: string): Promise<unknown>;
|
|
239
250
|
listDeployments(): Promise<unknown>;
|
|
240
251
|
getDeployment(branch: string): Promise<unknown>;
|
|
252
|
+
getDeploymentBundle(
|
|
253
|
+
branch: string,
|
|
254
|
+
appRoot?: string,
|
|
255
|
+
revision?: string,
|
|
256
|
+
runtimeConfig?: DeploymentRuntimeConfig,
|
|
257
|
+
): Promise<BranchDeploymentBundle>;
|
|
241
258
|
servePreview(branch: string, request: Request): Promise<Response>;
|
|
259
|
+
serveApp(branch: string, request: Request): Promise<Response>;
|
|
242
260
|
listAppTables(branch: string): Promise<unknown>;
|
|
243
261
|
queryAppTable(branch: string, table: string, opts?: unknown): Promise<unknown>;
|
|
244
262
|
wipeAppDatabase(branch: string): Promise<{ ok: true }>;
|