@treeseed/sdk 0.4.8 → 0.4.10

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.
Files changed (73) hide show
  1. package/README.md +1 -1
  2. package/dist/control-plane-client.d.ts +45 -0
  3. package/dist/control-plane-client.js +229 -0
  4. package/dist/control-plane.d.ts +94 -0
  5. package/dist/control-plane.js +125 -0
  6. package/dist/d1-store.d.ts +56 -1
  7. package/dist/d1-store.js +132 -0
  8. package/dist/dispatch.d.ts +4 -0
  9. package/dist/dispatch.js +180 -0
  10. package/dist/index.d.ts +14 -2
  11. package/dist/index.js +94 -4
  12. package/dist/operations/services/config-runtime.d.ts +10 -0
  13. package/dist/operations/services/config-runtime.js +62 -4
  14. package/dist/operations/services/deploy.d.ts +95 -3
  15. package/dist/operations/services/deploy.js +351 -10
  16. package/dist/operations/services/github-automation.d.ts +37 -1
  17. package/dist/operations/services/github-automation.js +71 -14
  18. package/dist/operations/services/project-platform.d.ts +835 -0
  19. package/dist/operations/services/project-platform.js +782 -0
  20. package/dist/operations/services/railway-deploy.d.ts +113 -18
  21. package/dist/operations/services/railway-deploy.js +357 -8
  22. package/dist/operations/services/runtime-tools.d.ts +25 -1
  23. package/dist/operations/services/runtime-tools.js +66 -5
  24. package/dist/operations/services/template-registry.d.ts +1 -1
  25. package/dist/operations/services/template-registry.js +17 -3
  26. package/dist/platform/books-data.d.ts +3 -4
  27. package/dist/platform/books-data.js +30 -4
  28. package/dist/platform/contracts.d.ts +56 -4
  29. package/dist/platform/deploy-config.js +109 -4
  30. package/dist/platform/deploy-runtime.d.ts +2 -0
  31. package/dist/platform/deploy-runtime.js +9 -1
  32. package/dist/platform/env.yaml +677 -0
  33. package/dist/platform/environment.js +57 -2
  34. package/dist/platform/plugin.d.ts +8 -0
  35. package/dist/platform/plugins/constants.d.ts +2 -0
  36. package/dist/platform/plugins/constants.js +2 -0
  37. package/dist/platform/plugins/runtime.d.ts +2 -0
  38. package/dist/platform/plugins/runtime.js +9 -1
  39. package/dist/platform/plugins.d.ts +1 -1
  40. package/dist/platform/plugins.js +4 -0
  41. package/dist/platform/published-content-pipeline.d.ts +84 -0
  42. package/dist/platform/published-content-pipeline.js +543 -0
  43. package/dist/platform/published-content.d.ts +223 -0
  44. package/dist/platform/published-content.js +588 -0
  45. package/dist/platform/tenant/runtime-config.d.ts +1 -1
  46. package/dist/platform/tenant/runtime-config.js +34 -1
  47. package/dist/platform/tenant-config.d.ts +2 -1
  48. package/dist/platform/tenant-config.js +17 -1
  49. package/dist/platform/utils/site-config-schema.js +104 -0
  50. package/dist/plugin-default.d.ts +2 -0
  51. package/dist/plugin-default.js +2 -0
  52. package/dist/remote.d.ts +65 -9
  53. package/dist/remote.js +104 -28
  54. package/dist/scripts/check-build-warnings.js +50 -0
  55. package/dist/scripts/config-treeseed.js +7 -0
  56. package/dist/scripts/tenant-workflow-action.js +71 -0
  57. package/dist/sdk-dispatch.d.ts +12 -0
  58. package/dist/sdk-dispatch.js +142 -0
  59. package/dist/sdk-types.d.ts +579 -7
  60. package/dist/sdk-types.js +53 -1
  61. package/dist/sdk.d.ts +17 -1
  62. package/dist/sdk.js +109 -0
  63. package/dist/stores/operational-store.d.ts +22 -2
  64. package/dist/stores/operational-store.js +235 -0
  65. package/dist/template-catalog.js +8 -1
  66. package/dist/treeseed/template-catalog/templates/starter-basic/template/treeseed.site.yaml +20 -0
  67. package/dist/types/cloudflare.d.ts +23 -0
  68. package/dist/workflow/operations.d.ts +12 -3
  69. package/dist/workflow/policy.d.ts +1 -1
  70. package/dist/workflow-state.js +2 -1
  71. package/package.json +7 -2
  72. package/templates/github/deploy.workflow.yml +442 -0
  73. package/templates/github/hosted-project.workflow.yml +77 -0
@@ -0,0 +1,588 @@
1
+ import { createHash, createHmac } from "node:crypto";
2
+ const PUBLISHED_CONTENT_MANIFEST_SCHEMA_VERSION = 2;
3
+ const EDITORIAL_PREVIEW_COOKIE = "treeseed-content-preview";
4
+ function isRecord(value) {
5
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6
+ }
7
+ function expectRecord(value, label) {
8
+ if (!isRecord(value)) {
9
+ throw new Error(`Invalid published content payload: expected ${label} to be an object.`);
10
+ }
11
+ return value;
12
+ }
13
+ function expectString(value, label) {
14
+ if (typeof value !== "string" || !value.trim()) {
15
+ throw new Error(`Invalid published content payload: expected ${label} to be a non-empty string.`);
16
+ }
17
+ return value.trim();
18
+ }
19
+ function optionalString(value) {
20
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
21
+ }
22
+ function optionalNumber(value) {
23
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
24
+ }
25
+ function optionalBoolean(value) {
26
+ return typeof value === "boolean" ? value : void 0;
27
+ }
28
+ function stableHash(value) {
29
+ return createHash("sha256").update(value).digest("hex");
30
+ }
31
+ function base64UrlEncode(value) {
32
+ return Buffer.from(value, "utf8").toString("base64url");
33
+ }
34
+ function base64UrlDecode(value) {
35
+ return Buffer.from(value, "base64url").toString("utf8");
36
+ }
37
+ function canonicalEntryPath(entry) {
38
+ return `${entry.model}/${entry.slug || entry.id}`.replace(/^\/+|\/+$/g, "");
39
+ }
40
+ function normalizeObjectPointer(value, label) {
41
+ const record = expectRecord(value, label);
42
+ return {
43
+ objectKey: expectString(record.objectKey ?? record.key, `${label}.objectKey`),
44
+ sha256: expectString(record.sha256, `${label}.sha256`),
45
+ size: optionalNumber(record.size),
46
+ contentType: optionalString(record.contentType),
47
+ publicUrl: optionalString(record.publicUrl)
48
+ };
49
+ }
50
+ function normalizeRuntimePointers(value, label) {
51
+ const record = isRecord(value) ? value : void 0;
52
+ if (!record) {
53
+ return void 0;
54
+ }
55
+ return {
56
+ docsHomePath: optionalString(record.docsHomePath),
57
+ booksRuntime: record.booksRuntime ? normalizeObjectPointer(record.booksRuntime, `${label}.booksRuntime`) : void 0,
58
+ docsTree: record.docsTree ? normalizeObjectPointer(record.docsTree, `${label}.docsTree`) : void 0,
59
+ searchIndex: record.searchIndex ? normalizeObjectPointer(record.searchIndex, `${label}.searchIndex`) : void 0
60
+ };
61
+ }
62
+ function normalizeLocator(value, label) {
63
+ const record = isRecord(value) ? value : void 0;
64
+ if (!record) {
65
+ return void 0;
66
+ }
67
+ return {
68
+ teamId: expectString(record.teamId, `${label}.teamId`),
69
+ manifestKey: expectString(record.manifestKey, `${label}.manifestKey`),
70
+ previewRoot: expectString(record.previewRoot, `${label}.previewRoot`),
71
+ overlayKey: optionalString(record.overlayKey),
72
+ previewId: optionalString(record.previewId),
73
+ mode: optionalString(record.mode)
74
+ };
75
+ }
76
+ function normalizeContentEntry(value, label) {
77
+ const record = expectRecord(value, label);
78
+ return {
79
+ id: expectString(record.id, `${label}.id`),
80
+ model: expectString(record.model, `${label}.model`),
81
+ slug: expectString(record.slug, `${label}.slug`),
82
+ title: optionalString(record.title),
83
+ summary: optionalString(record.summary),
84
+ status: optionalString(record.status),
85
+ visibility: optionalString(record.visibility),
86
+ teamId: optionalString(record.teamId),
87
+ publishedAt: optionalString(record.publishedAt),
88
+ updatedAt: optionalString(record.updatedAt),
89
+ content: normalizeObjectPointer(record.content, `${label}.content`),
90
+ rendered: record.rendered ? normalizeObjectPointer(record.rendered, `${label}.rendered`) : void 0,
91
+ search: record.search ? normalizeObjectPointer(record.search, `${label}.search`) : void 0,
92
+ metadata: isRecord(record.metadata) ? record.metadata : void 0
93
+ };
94
+ }
95
+ function normalizeArtifactVersion(value, label) {
96
+ const record = expectRecord(value, label);
97
+ return {
98
+ id: expectString(record.id, `${label}.id`),
99
+ itemId: expectString(record.itemId, `${label}.itemId`),
100
+ kind: expectString(record.kind, `${label}.kind`),
101
+ version: expectString(record.version, `${label}.version`),
102
+ label: optionalString(record.label),
103
+ visibility: optionalString(record.visibility),
104
+ teamId: optionalString(record.teamId),
105
+ publishedAt: expectString(record.publishedAt, `${label}.publishedAt`),
106
+ content: normalizeObjectPointer(record.content, `${label}.content`),
107
+ metadata: isRecord(record.metadata) ? record.metadata : void 0
108
+ };
109
+ }
110
+ function normalizeTombstones(value) {
111
+ return Array.isArray(value) ? value.map((item, index) => {
112
+ const tombstone = expectRecord(item, `tombstones[${index}]`);
113
+ return {
114
+ path: expectString(tombstone.path, `tombstones[${index}].path`),
115
+ removedAt: expectString(tombstone.removedAt, `tombstones[${index}].removedAt`),
116
+ previousSha256: optionalString(tombstone.previousSha256)
117
+ };
118
+ }) : [];
119
+ }
120
+ function parsePublishedContentManifest(value) {
121
+ const record = expectRecord(value, "manifest");
122
+ const collectionsRecord = isRecord(record.collections) ? record.collections : {};
123
+ return {
124
+ schemaVersion: optionalNumber(record.schemaVersion) ?? PUBLISHED_CONTENT_MANIFEST_SCHEMA_VERSION,
125
+ siteSlug: expectString(record.siteSlug, "manifest.siteSlug"),
126
+ teamId: optionalString(record.teamId) ?? expectString(record.siteSlug, "manifest.siteSlug"),
127
+ revision: expectString(record.revision, "manifest.revision"),
128
+ generatedAt: expectString(record.generatedAt, "manifest.generatedAt"),
129
+ mode: optionalString(record.mode) ?? "production",
130
+ sourceCommit: optionalString(record.sourceCommit),
131
+ appRevision: optionalString(record.appRevision),
132
+ appCompatibility: isRecord(record.appCompatibility) ? {
133
+ min: optionalString(record.appCompatibility.min),
134
+ max: optionalString(record.appCompatibility.max),
135
+ lastKnownCompatibleRevision: optionalString(record.appCompatibility.lastKnownCompatibleRevision)
136
+ } : void 0,
137
+ locator: normalizeLocator(record.locator, "manifest.locator"),
138
+ collections: Object.fromEntries(
139
+ Object.entries(collectionsRecord).map(([model, pointer]) => [model, normalizeObjectPointer(pointer, `manifest.collections.${model}`)])
140
+ ),
141
+ entries: Array.isArray(record.entries) ? record.entries.map((entry, index) => normalizeContentEntry(entry, `manifest.entries[${index}]`)) : [],
142
+ artifacts: Array.isArray(record.artifacts) ? record.artifacts.map((artifact, index) => normalizeArtifactVersion(artifact, `manifest.artifacts[${index}]`)) : [],
143
+ runtime: normalizeRuntimePointers(record.runtime, "manifest.runtime"),
144
+ tombstones: normalizeTombstones(record.tombstones),
145
+ metadata: isRecord(record.metadata) ? record.metadata : void 0
146
+ };
147
+ }
148
+ function parsePublishedOverlayManifest(value) {
149
+ const record = expectRecord(value, "overlay");
150
+ const collectionsRecord = isRecord(record.collections) ? record.collections : {};
151
+ return {
152
+ schemaVersion: optionalNumber(record.schemaVersion) ?? PUBLISHED_CONTENT_MANIFEST_SCHEMA_VERSION,
153
+ siteSlug: expectString(record.siteSlug, "overlay.siteSlug"),
154
+ teamId: optionalString(record.teamId) ?? expectString(record.siteSlug, "overlay.siteSlug"),
155
+ previewId: expectString(record.previewId, "overlay.previewId"),
156
+ generatedAt: expectString(record.generatedAt, "overlay.generatedAt"),
157
+ mode: optionalString(record.mode) ?? "editorial_overlay",
158
+ baseManifestKey: expectString(record.baseManifestKey, "overlay.baseManifestKey"),
159
+ baseRevision: optionalString(record.baseRevision),
160
+ sourceCommit: optionalString(record.sourceCommit),
161
+ expiresAt: optionalString(record.expiresAt),
162
+ locator: normalizeLocator(record.locator, "overlay.locator"),
163
+ collections: Object.fromEntries(
164
+ Object.entries(collectionsRecord).map(([model, pointer]) => [model, normalizeObjectPointer(pointer, `overlay.collections.${model}`)])
165
+ ),
166
+ entries: Array.isArray(record.entries) ? record.entries.map((entry, index) => normalizeContentEntry(entry, `overlay.entries[${index}]`)) : [],
167
+ artifacts: Array.isArray(record.artifacts) ? record.artifacts.map((artifact, index) => normalizeArtifactVersion(artifact, `overlay.artifacts[${index}]`)) : [],
168
+ runtime: normalizeRuntimePointers(record.runtime, "overlay.runtime"),
169
+ tombstones: normalizeTombstones(record.tombstones),
170
+ metadata: isRecord(record.metadata) ? record.metadata : void 0
171
+ };
172
+ }
173
+ function parsePublishedCollectionIndex(value) {
174
+ const record = expectRecord(value, "collection index");
175
+ return {
176
+ model: expectString(record.model, "collectionIndex.model"),
177
+ generatedAt: expectString(record.generatedAt, "collectionIndex.generatedAt"),
178
+ count: optionalNumber(record.count),
179
+ entries: Array.isArray(record.entries) ? record.entries.map((entry, index) => normalizeContentEntry(entry, `collectionIndex.entries[${index}]`)) : []
180
+ };
181
+ }
182
+ function resolvePublishedContentBucketBinding(config) {
183
+ return config.cloudflare.r2?.binding ?? "TREESEED_CONTENT_BUCKET";
184
+ }
185
+ function fillTeamTemplate(template, teamId) {
186
+ return template.replaceAll("{teamId}", teamId);
187
+ }
188
+ function resolvePublishedContentManifestKey(config, teamId) {
189
+ const resolvedTeamId = typeof teamId === "string" && teamId.trim() ? teamId.trim() : typeof config.slug === "string" && config.slug.trim() ? config.slug.trim() : "default";
190
+ return fillTeamTemplate(
191
+ config.cloudflare.r2?.manifestKeyTemplate ?? "teams/{teamId}/published/common.json",
192
+ resolvedTeamId
193
+ );
194
+ }
195
+ function resolvePublishedContentPreviewRoot(config, teamId) {
196
+ const resolvedTeamId = typeof teamId === "string" && teamId.trim() ? teamId.trim() : typeof config.slug === "string" && config.slug.trim() ? config.slug.trim() : "default";
197
+ return fillTeamTemplate(
198
+ config.cloudflare.r2?.previewRootTemplate ?? "teams/{teamId}/previews",
199
+ resolvedTeamId
200
+ );
201
+ }
202
+ function resolvePublishedContentPreviewTtlHours(config) {
203
+ return config.cloudflare.r2?.previewTtlHours ?? 168;
204
+ }
205
+ function resolveTeamScopedContentLocator(config, teamId, previewId) {
206
+ const previewRoot = resolvePublishedContentPreviewRoot(config, teamId);
207
+ return {
208
+ teamId,
209
+ manifestKey: resolvePublishedContentManifestKey(config, teamId),
210
+ previewRoot,
211
+ previewId: previewId || void 0,
212
+ overlayKey: previewId ? `${previewRoot}/${previewId}/overlay.json` : void 0,
213
+ mode: previewId ? "editorial_overlay" : "production"
214
+ };
215
+ }
216
+ function isTeamScopedR2ContentEnabled(config) {
217
+ return config.providers?.content?.runtime === "team_scoped_r2_overlay" && Boolean(config.cloudflare.r2?.binding);
218
+ }
219
+ function signEditorialPreviewToken(payload, secret) {
220
+ const normalized = {
221
+ teamId: expectString(payload.teamId, "previewToken.teamId"),
222
+ previewId: expectString(payload.previewId, "previewToken.previewId"),
223
+ expiresAt: expectString(payload.expiresAt, "previewToken.expiresAt")
224
+ };
225
+ const encodedPayload = base64UrlEncode(JSON.stringify(normalized));
226
+ const signature = createHmac("sha256", secret).update(encodedPayload).digest("base64url");
227
+ return `${encodedPayload}.${signature}`;
228
+ }
229
+ function verifyEditorialPreviewToken(token, secret) {
230
+ const [encodedPayload, signature] = String(token ?? "").split(".");
231
+ if (!encodedPayload || !signature) {
232
+ return null;
233
+ }
234
+ const expected = createHmac("sha256", secret).update(encodedPayload).digest("base64url");
235
+ if (expected !== signature) {
236
+ return null;
237
+ }
238
+ const payload = JSON.parse(base64UrlDecode(encodedPayload));
239
+ const normalized = {
240
+ teamId: expectString(payload.teamId, "previewToken.teamId"),
241
+ previewId: expectString(payload.previewId, "previewToken.previewId"),
242
+ expiresAt: expectString(payload.expiresAt, "previewToken.expiresAt")
243
+ };
244
+ if (Date.parse(normalized.expiresAt) <= Date.now()) {
245
+ return null;
246
+ }
247
+ return normalized;
248
+ }
249
+ function resolveCloudflareR2Bucket(runtime, binding) {
250
+ if (!runtime?.env || !binding) {
251
+ return null;
252
+ }
253
+ const candidate = runtime.env[binding];
254
+ return candidate && typeof candidate === "object" ? candidate : null;
255
+ }
256
+ async function readJsonObject(bucket, key) {
257
+ const object = await bucket.get(key);
258
+ return object ? object.json() : null;
259
+ }
260
+ async function readPublishedContentManifest(bucket, manifestKey) {
261
+ const payload = await readJsonObject(bucket, manifestKey);
262
+ return payload ? parsePublishedContentManifest(payload) : null;
263
+ }
264
+ async function readPublishedOverlayManifest(bucket, overlayKey) {
265
+ const payload = await readJsonObject(bucket, overlayKey);
266
+ return payload ? parsePublishedOverlayManifest(payload) : null;
267
+ }
268
+ function mergeEntries(baseEntries, overlayEntries = [], tombstones = []) {
269
+ const removed = new Set(tombstones.map((entry) => entry.path));
270
+ const merged = /* @__PURE__ */ new Map();
271
+ for (const entry of baseEntries) {
272
+ const key = canonicalEntryPath(entry);
273
+ if (!removed.has(key)) {
274
+ merged.set(key, entry);
275
+ }
276
+ }
277
+ for (const entry of overlayEntries) {
278
+ merged.set(canonicalEntryPath(entry), entry);
279
+ }
280
+ return [...merged.values()];
281
+ }
282
+ function mergeArtifacts(baseArtifacts = [], overlayArtifacts = []) {
283
+ const merged = /* @__PURE__ */ new Map();
284
+ for (const artifact of baseArtifacts) {
285
+ merged.set(`${artifact.itemId}:${artifact.version}`, artifact);
286
+ }
287
+ for (const artifact of overlayArtifacts) {
288
+ merged.set(`${artifact.itemId}:${artifact.version}`, artifact);
289
+ }
290
+ return [...merged.values()];
291
+ }
292
+ function mergeRuntimePointers(baseRuntime, overlayRuntime) {
293
+ return overlayRuntime ? { ...baseRuntime ?? {}, ...overlayRuntime } : baseRuntime;
294
+ }
295
+ function mergeManifests(production, overlay) {
296
+ if (!overlay) {
297
+ return production;
298
+ }
299
+ return {
300
+ ...production,
301
+ mode: "editorial_overlay",
302
+ generatedAt: overlay.generatedAt,
303
+ sourceCommit: overlay.sourceCommit ?? production.sourceCommit,
304
+ locator: overlay.locator ?? production.locator,
305
+ collections: {
306
+ ...production.collections ?? {},
307
+ ...overlay.collections ?? {}
308
+ },
309
+ entries: mergeEntries(production.entries, overlay.entries ?? [], overlay.tombstones ?? []),
310
+ artifacts: mergeArtifacts(production.artifacts ?? [], overlay.artifacts ?? []),
311
+ runtime: mergeRuntimePointers(production.runtime, overlay.runtime),
312
+ tombstones: [...production.tombstones ?? [], ...overlay.tombstones ?? []],
313
+ metadata: {
314
+ ...production.metadata ?? {},
315
+ ...overlay.metadata ?? {},
316
+ overlayPreviewId: overlay.previewId,
317
+ overlayExpiresAt: overlay.expiresAt
318
+ }
319
+ };
320
+ }
321
+ function collectManifestPointers(manifest) {
322
+ const pointers = [];
323
+ for (const pointer of Object.values(manifest.collections ?? {})) {
324
+ pointers.push(pointer);
325
+ }
326
+ for (const entry of manifest.entries ?? []) {
327
+ pointers.push(entry.content);
328
+ if (entry.rendered) pointers.push(entry.rendered);
329
+ if (entry.search) pointers.push(entry.search);
330
+ }
331
+ for (const artifact of manifest.artifacts ?? []) {
332
+ pointers.push(artifact.content);
333
+ }
334
+ if (manifest.runtime?.booksRuntime) pointers.push(manifest.runtime.booksRuntime);
335
+ if (manifest.runtime?.docsTree) pointers.push(manifest.runtime.docsTree);
336
+ if (manifest.runtime?.searchIndex) pointers.push(manifest.runtime.searchIndex);
337
+ return pointers;
338
+ }
339
+ async function putContentObjects(bucket, objects) {
340
+ for (const object of objects) {
341
+ await bucket.put(object.pointer.objectKey, object.body, {
342
+ httpMetadata: object.httpMetadata,
343
+ customMetadata: object.customMetadata
344
+ });
345
+ }
346
+ }
347
+ class TeamScopedR2OverlayContentRuntimeProvider {
348
+ constructor(bucket, locator) {
349
+ this.bucket = bucket;
350
+ this.locator = locator;
351
+ }
352
+ bucket;
353
+ locator;
354
+ productionManifestPromise = null;
355
+ overlayManifestPromise = null;
356
+ manifestPromise = null;
357
+ collectionCache = /* @__PURE__ */ new Map();
358
+ async getProductionManifest() {
359
+ if (!this.productionManifestPromise) {
360
+ this.productionManifestPromise = (async () => {
361
+ const manifest = await readPublishedContentManifest(this.bucket, this.locator.manifestKey);
362
+ if (!manifest) {
363
+ const overlay = this.locator.overlayKey ? await readPublishedOverlayManifest(this.bucket, this.locator.overlayKey) : null;
364
+ if (overlay) {
365
+ return {
366
+ schemaVersion: overlay.schemaVersion,
367
+ siteSlug: overlay.siteSlug,
368
+ teamId: overlay.teamId,
369
+ revision: overlay.baseRevision ?? "unpublished",
370
+ generatedAt: overlay.generatedAt,
371
+ mode: "production",
372
+ sourceCommit: overlay.sourceCommit,
373
+ locator: {
374
+ ...this.locator,
375
+ mode: "production",
376
+ overlayKey: void 0,
377
+ previewId: void 0
378
+ },
379
+ collections: {},
380
+ entries: [],
381
+ artifacts: [],
382
+ runtime: {},
383
+ tombstones: [],
384
+ metadata: {
385
+ overlayOnly: true
386
+ }
387
+ };
388
+ }
389
+ throw new Error(`Published content manifest "${this.locator.manifestKey}" was not found in R2.`);
390
+ }
391
+ return manifest;
392
+ })();
393
+ }
394
+ return this.productionManifestPromise;
395
+ }
396
+ async getOverlayManifest() {
397
+ if (!this.locator.overlayKey) {
398
+ return null;
399
+ }
400
+ if (!this.overlayManifestPromise) {
401
+ this.overlayManifestPromise = readPublishedOverlayManifest(this.bucket, this.locator.overlayKey);
402
+ }
403
+ return this.overlayManifestPromise;
404
+ }
405
+ async getManifest() {
406
+ if (!this.manifestPromise) {
407
+ this.manifestPromise = (async () => mergeManifests(await this.getProductionManifest(), await this.getOverlayManifest()))();
408
+ }
409
+ return this.manifestPromise;
410
+ }
411
+ async getCollectionIndex(model) {
412
+ if (!this.collectionCache.has(model)) {
413
+ this.collectionCache.set(model, (async () => {
414
+ const [manifest, overlay, production] = await Promise.all([
415
+ this.getManifest(),
416
+ this.getOverlayManifest(),
417
+ this.getProductionManifest()
418
+ ]);
419
+ const pointer = manifest.collections?.[model];
420
+ if (!pointer) {
421
+ return {
422
+ model,
423
+ generatedAt: manifest.generatedAt,
424
+ count: 0,
425
+ entries: manifest.entries.filter((entry) => entry.model === model)
426
+ };
427
+ }
428
+ const payload = await readJsonObject(this.bucket, pointer.objectKey);
429
+ if (!payload) {
430
+ throw new Error(`Published collection index "${pointer.objectKey}" for model "${model}" was not found.`);
431
+ }
432
+ const parsed = parsePublishedCollectionIndex(payload);
433
+ if (!overlay) {
434
+ return parsed;
435
+ }
436
+ const baseIndexPointer = production.collections?.[model];
437
+ const basePayload = baseIndexPointer && baseIndexPointer.objectKey !== pointer.objectKey ? await readJsonObject(this.bucket, baseIndexPointer.objectKey) : null;
438
+ const baseEntries = basePayload ? parsePublishedCollectionIndex(basePayload).entries : parsed.entries;
439
+ return {
440
+ model,
441
+ generatedAt: manifest.generatedAt,
442
+ count: void 0,
443
+ entries: mergeEntries(
444
+ baseEntries,
445
+ (overlay.entries ?? []).filter((entry) => entry.model === model),
446
+ (overlay.tombstones ?? []).filter((entry) => entry.path.startsWith(`${model}/`))
447
+ )
448
+ };
449
+ })());
450
+ }
451
+ return this.collectionCache.get(model);
452
+ }
453
+ async listCollection(model) {
454
+ return (await this.getCollectionIndex(model)).entries;
455
+ }
456
+ async getEntry(model, slugOrId) {
457
+ const normalized = String(slugOrId).trim().replace(/^\/+|\/+$/g, "");
458
+ const manifest = await this.getManifest();
459
+ const fromManifest = manifest.entries.find((entry) => entry.model === model && (entry.slug === normalized || entry.id === normalized));
460
+ if (fromManifest) {
461
+ return fromManifest;
462
+ }
463
+ const index = await this.getCollectionIndex(model);
464
+ return index.entries.find((entry) => entry.slug === normalized || entry.id === normalized) ?? null;
465
+ }
466
+ async getArtifactVersion(itemId, version) {
467
+ const manifest = await this.getManifest();
468
+ const candidates = (manifest.artifacts ?? []).filter((artifact) => artifact.itemId === itemId);
469
+ if (!candidates.length) {
470
+ return null;
471
+ }
472
+ if (version) {
473
+ return candidates.find((artifact) => artifact.version === version || artifact.id === version) ?? null;
474
+ }
475
+ return [...candidates].sort((left, right) => right.publishedAt.localeCompare(left.publishedAt))[0] ?? null;
476
+ }
477
+ async getObject(pointer) {
478
+ const objectKey = typeof pointer === "string" ? pointer : pointer.objectKey;
479
+ return readJsonObject(this.bucket, objectKey);
480
+ }
481
+ }
482
+ class TeamScopedR2OverlayContentPublishProvider {
483
+ constructor(bucket, locator) {
484
+ this.bucket = bucket;
485
+ this.locator = locator;
486
+ }
487
+ bucket;
488
+ locator;
489
+ async publishRevision(input) {
490
+ await putContentObjects(this.bucket, input.objects);
491
+ const snapshotKey = this.locator.manifestKey.replace(/\/common\.json$/u, `/manifests/${input.manifest.revision}.json`);
492
+ await this.bucket.put(snapshotKey, JSON.stringify(input.manifest, null, 2));
493
+ await this.bucket.put(this.locator.manifestKey, JSON.stringify(input.manifest, null, 2));
494
+ return {
495
+ revision: input.manifest.revision,
496
+ manifestKey: this.locator.manifestKey
497
+ };
498
+ }
499
+ async publishOverlay(input) {
500
+ await putContentObjects(this.bucket, input.objects);
501
+ const overlayKey = input.overlay.locator?.overlayKey ?? `${this.locator.previewRoot}/${input.overlay.previewId}/overlay.json`;
502
+ await this.bucket.put(overlayKey, JSON.stringify(input.overlay, null, 2));
503
+ return {
504
+ previewId: input.overlay.previewId,
505
+ overlayKey
506
+ };
507
+ }
508
+ async promoteOverlay(input) {
509
+ const production = await readPublishedContentManifest(this.bucket, this.locator.manifestKey);
510
+ if (!production) {
511
+ throw new Error(`Published content manifest "${this.locator.manifestKey}" was not found in R2.`);
512
+ }
513
+ const overlayKey = `${this.locator.previewRoot}/${input.previewId}/overlay.json`;
514
+ const overlay = await readPublishedOverlayManifest(this.bucket, overlayKey);
515
+ if (!overlay) {
516
+ throw new Error(`Editorial overlay "${overlayKey}" was not found in R2.`);
517
+ }
518
+ const merged = mergeManifests(production, overlay);
519
+ const nextManifest = {
520
+ ...merged,
521
+ mode: "production",
522
+ revision: input.revision ?? `${overlay.previewId}-${stableHash(`${overlay.generatedAt}:${overlay.previewId}`).slice(0, 12)}`,
523
+ generatedAt: input.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
524
+ sourceCommit: input.sourceCommit ?? overlay.sourceCommit ?? merged.sourceCommit,
525
+ appRevision: input.appRevision ?? merged.appRevision,
526
+ locator: {
527
+ ...this.locator,
528
+ mode: "production",
529
+ overlayKey: void 0,
530
+ previewId: void 0
531
+ }
532
+ };
533
+ return this.publishRevision({
534
+ manifest: nextManifest,
535
+ objects: []
536
+ });
537
+ }
538
+ async rollbackRevision(snapshotKey) {
539
+ const manifest = await readPublishedContentManifest(this.bucket, snapshotKey);
540
+ if (!manifest) {
541
+ throw new Error(`Published content snapshot "${snapshotKey}" was not found in R2.`);
542
+ }
543
+ await this.bucket.put(this.locator.manifestKey, JSON.stringify(manifest, null, 2));
544
+ return {
545
+ revision: manifest.revision,
546
+ manifestKey: this.locator.manifestKey
547
+ };
548
+ }
549
+ async deleteOverlay(previewId, overlay = null) {
550
+ const overlayKey = `${this.locator.previewRoot}/${previewId}/overlay.json`;
551
+ if (this.bucket.delete) {
552
+ const manifest = overlay ?? await readPublishedOverlayManifest(this.bucket, overlayKey);
553
+ const keys = /* @__PURE__ */ new Set([overlayKey]);
554
+ for (const pointer of manifest ? collectManifestPointers(manifest) : []) {
555
+ keys.add(pointer.objectKey);
556
+ }
557
+ await this.bucket.delete([...keys]);
558
+ }
559
+ }
560
+ }
561
+ function createTeamScopedR2OverlayContentRuntimeProvider(options) {
562
+ return new TeamScopedR2OverlayContentRuntimeProvider(options.bucket, options.locator);
563
+ }
564
+ function createTeamScopedR2OverlayContentPublishProvider(options) {
565
+ return new TeamScopedR2OverlayContentPublishProvider(options.bucket, options.locator);
566
+ }
567
+ export {
568
+ EDITORIAL_PREVIEW_COOKIE,
569
+ PUBLISHED_CONTENT_MANIFEST_SCHEMA_VERSION,
570
+ TeamScopedR2OverlayContentPublishProvider,
571
+ TeamScopedR2OverlayContentRuntimeProvider,
572
+ createTeamScopedR2OverlayContentPublishProvider,
573
+ createTeamScopedR2OverlayContentRuntimeProvider,
574
+ isTeamScopedR2ContentEnabled,
575
+ parsePublishedCollectionIndex,
576
+ parsePublishedContentManifest,
577
+ parsePublishedOverlayManifest,
578
+ readPublishedContentManifest,
579
+ readPublishedOverlayManifest,
580
+ resolveCloudflareR2Bucket,
581
+ resolvePublishedContentBucketBinding,
582
+ resolvePublishedContentManifestKey,
583
+ resolvePublishedContentPreviewRoot,
584
+ resolvePublishedContentPreviewTtlHours,
585
+ resolveTeamScopedContentLocator,
586
+ signEditorialPreviewToken,
587
+ verifyEditorialPreviewToken
588
+ };
@@ -1,4 +1,4 @@
1
1
  import type { TreeseedTenantConfig } from '../contracts.ts';
2
- export declare const RUNTIME_TENANT: TreeseedTenantConfig;
3
2
  export declare const RUNTIME_PROJECT_ROOT: string;
3
+ export declare const RUNTIME_TENANT: TreeseedTenantConfig;
4
4
  export declare const RUNTIME_SITE_CONFIG: any;
@@ -1,11 +1,44 @@
1
1
  import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
2
3
  import { loadTreeseedManifest } from "../tenant-config.js";
3
4
  import { parseSiteConfig } from "../utils/site-config-schema.js";
4
5
  const injectedTenantConfig = typeof __TREESEED_TENANT_CONFIG__ !== "undefined" ? __TREESEED_TENANT_CONFIG__ : null;
5
6
  const injectedProjectRoot = typeof __TREESEED_PROJECT_ROOT__ !== "undefined" ? __TREESEED_PROJECT_ROOT__ : null;
6
7
  const injectedSiteConfig = typeof __TREESEED_SITE_CONFIG__ !== "undefined" ? __TREESEED_SITE_CONFIG__ : null;
7
- const RUNTIME_TENANT = injectedTenantConfig ?? loadTreeseedManifest();
8
+ function fallbackTenantConfig(projectRoot) {
9
+ return {
10
+ id: "treeseed-runtime",
11
+ siteConfigPath: resolve(projectRoot, "treeseed.site.yaml"),
12
+ content: {
13
+ pages: resolve(projectRoot, "src/content/pages"),
14
+ notes: resolve(projectRoot, "src/content/notes"),
15
+ questions: resolve(projectRoot, "src/content/questions"),
16
+ objectives: resolve(projectRoot, "src/content/objectives"),
17
+ people: resolve(projectRoot, "src/content/people"),
18
+ agents: resolve(projectRoot, "src/content/agents"),
19
+ books: resolve(projectRoot, "src/content/books"),
20
+ docs: resolve(projectRoot, "src/content/knowledge"),
21
+ templates: resolve(projectRoot, "src/content/templates"),
22
+ knowledge_packs: resolve(projectRoot, "src/content/knowledge-packs"),
23
+ workdays: resolve(projectRoot, "src/content/workdays")
24
+ },
25
+ features: {
26
+ docs: true,
27
+ books: true
28
+ }
29
+ };
30
+ }
8
31
  const RUNTIME_PROJECT_ROOT = injectedProjectRoot ?? process.cwd();
32
+ const RUNTIME_TENANT = (() => {
33
+ if (injectedTenantConfig) {
34
+ return injectedTenantConfig;
35
+ }
36
+ try {
37
+ return loadTreeseedManifest();
38
+ } catch {
39
+ return fallbackTenantConfig(RUNTIME_PROJECT_ROOT);
40
+ }
41
+ })();
9
42
  const RUNTIME_SITE_CONFIG = injectedSiteConfig ?? (() => {
10
43
  try {
11
44
  return parseSiteConfig(readFileSync(RUNTIME_TENANT.siteConfigPath, "utf8"));
@@ -1,7 +1,8 @@
1
- import type { TreeseedTenantConfig } from './contracts.ts';
1
+ import type { TreeseedContentCollection, TreeseedTenantConfig } from './contracts.ts';
2
2
  export declare function resolveTreeseedTenantRoot(): string;
3
3
  export declare function defineTreeseedTenant<T>(tenantConfig: T): T;
4
4
  export declare function loadTreeseedManifest(manifestPath?: string): TreeseedTenantConfig;
5
5
  export declare const loadTreeseedTenantManifest: typeof loadTreeseedManifest;
6
6
  export declare function getTenantContentRoot(tenantConfig: Pick<TreeseedTenantConfig, 'content'>, collectionName: string): string;
7
7
  export declare function tenantFeatureEnabled(tenantConfig: Pick<TreeseedTenantConfig, 'features'>, featureName: string): boolean;
8
+ export declare function tenantModelRendered(tenantConfig: Pick<TreeseedTenantConfig, 'features' | 'site'>, modelName: TreeseedContentCollection): boolean;
@@ -168,11 +168,27 @@ function getTenantContentRoot(tenantConfig, collectionName) {
168
168
  function tenantFeatureEnabled(tenantConfig, featureName) {
169
169
  return tenantConfig.features?.[featureName] !== false;
170
170
  }
171
+ const MODEL_FEATURE_MAP = {
172
+ docs: "docs",
173
+ books: "books",
174
+ notes: "notes",
175
+ questions: "questions",
176
+ objectives: "objectives",
177
+ agents: "agents"
178
+ };
179
+ function tenantModelRendered(tenantConfig, modelName) {
180
+ const featureName = MODEL_FEATURE_MAP[modelName];
181
+ if (featureName && !tenantFeatureEnabled(tenantConfig, featureName)) {
182
+ return false;
183
+ }
184
+ return tenantConfig.site?.models?.[modelName]?.rendered !== false;
185
+ }
171
186
  export {
172
187
  defineTreeseedTenant,
173
188
  getTenantContentRoot,
174
189
  loadTreeseedManifest,
175
190
  loadTreeseedTenantManifest,
176
191
  resolveTreeseedTenantRoot,
177
- tenantFeatureEnabled
192
+ tenantFeatureEnabled,
193
+ tenantModelRendered
178
194
  };