@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  # @springbrand/space
2
2
 
3
- SpaceDO — a git-backed Durable Object workspace with deploy, preview and App
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
- Files under `src/space/` are the upstream implementation. Local additions are
15
- confined to the `WorkspacePort` compatibility surface on `SpaceDO`
16
- (`durable-object.ts`), which Agent Runtime hosts require: binary
17
- writes, append, conditional write, quota, full file operations, R2 spill,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@springbrand/space",
3
- "version": "0.1.0-alpha.9",
3
+ "version": "0.2.0-alpha.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
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 type { BranchDeploymentBundle } from "./space/deploy-engine";
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 BranchDeploymentBundle {
35
+ export interface DeploymentBundle {
36
36
  branch: string
37
37
  commitHash: string
38
- mainModule: string
39
- modules: Record<string, string | Record<string, unknown>>
40
- assets: DeploymentAsset[]
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
- compatibilityDate: string
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
- mainModule,
277
- modules: serializedModules,
278
- assets,
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
- mainModule: result.mainModule,
368
- modules: result.modules,
369
- assets,
412
+ workerModules: {
413
+ mainModule: result.mainModule,
414
+ modules: result.modules,
415
+ compatibilityDate: result.compatibilityDate,
416
+ },
417
+ staticAssets: assets,
370
418
  assetConfig: result.assetConfig,
371
- compatibilityDate: result.compatibilityDate,
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, 8)]
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(branch: string, appRoot?: string, revision?: string): Promise<unknown> {
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(branch: string, appRoot?: string, revision?: string): Promise<unknown>;
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 }>;