@rimelight/cms 0.0.3 → 0.0.5

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 (66) hide show
  1. package/dist/bylines-Csggkl-g.d.mts +405 -0
  2. package/dist/index.d.mts +724 -0
  3. package/dist/index.mjs +2382 -0
  4. package/dist/integration.d.mts +33 -0
  5. package/dist/integration.mjs +161 -0
  6. package/dist/schema/index.d.mts +2063 -0
  7. package/dist/schema/index.mjs +176 -0
  8. package/dist/schema/sqlite.d.mts +2132 -0
  9. package/dist/schema/sqlite.mjs +137 -0
  10. package/dist/storage/index.d.mts +59 -0
  11. package/dist/storage/index.mjs +133 -0
  12. package/package.json +35 -14
  13. package/src/admin/api/media-file.ts +1 -1
  14. package/src/admin/api/media.ts +28 -3
  15. package/src/admin/layouts/CMSDashboardLayout.astro +16 -3
  16. package/src/admin/pages/assets.astro +11 -11
  17. package/src/admin/pages/pages-edit.astro +3 -3
  18. package/src/admin/pages/pages-index.astro +2 -2
  19. package/src/admin/pages/pages-preview.astro +3 -3
  20. package/src/admin/pages/pages-review.astro +2 -2
  21. package/src/admin/pages/templates-edit.astro +1 -1
  22. package/src/admin/pages/templates-index.astro +1 -1
  23. package/src/astro/BlockRenderer.astro +1 -1
  24. package/src/astro/PageRenderer.astro +4 -4
  25. package/src/astro/blocks/CalloutBlock.astro +1 -1
  26. package/src/astro/blocks/OrderedListBlock.astro +2 -2
  27. package/src/astro/blocks/ParagraphBlock.astro +1 -1
  28. package/src/astro/blocks/TabsBlock.astro +5 -5
  29. package/src/astro/blocks/UnorderedListBlock.astro +2 -2
  30. package/src/auth/editor-guards.ts +1 -1
  31. package/src/cache/purge.ts +2 -2
  32. package/src/client/components/RimelightBlock.tsx +1 -1
  33. package/src/client/components/RimelightBlockPicker.tsx +1 -1
  34. package/src/client/components/RimelightPreview.tsx +27 -27
  35. package/src/client/components/RimelightPropertiesPanel.tsx +4 -4
  36. package/src/client/components/RimelightTemplateEditor.tsx +9 -5
  37. package/src/client/components/editors/CalloutEditor.tsx +10 -10
  38. package/src/client/components/editors/CardEditor.tsx +5 -5
  39. package/src/client/components/editors/CardsEditor.tsx +2 -2
  40. package/src/client/components/editors/CodeEditor.tsx +5 -5
  41. package/src/client/components/editors/DialogueEditor.tsx +6 -6
  42. package/src/client/components/editors/FileTreeEditor.tsx +1 -1
  43. package/src/client/components/editors/ImageEditor.tsx +9 -9
  44. package/src/client/components/editors/ParagraphEditor.tsx +1 -1
  45. package/src/client/components/editors/SceneEditor.tsx +9 -9
  46. package/src/client/components/editors/ScriptEditor.tsx +8 -8
  47. package/src/client/components/editors/SectionEditor.tsx +3 -3
  48. package/src/client/components/editors/StepItemEditor.tsx +6 -5
  49. package/src/client/components/editors/StepsEditor.tsx +1 -1
  50. package/src/client/components/editors/TabItemEditor.tsx +3 -3
  51. package/src/client/components/editors/TableEditor.tsx +4 -4
  52. package/src/client/components/editors/TabsEditor.tsx +1 -1
  53. package/src/client/state/editor-store.ts +23 -23
  54. package/src/client/utils/sortable.ts +13 -12
  55. package/src/client/utils/tree-sanitizer.ts +1 -1
  56. package/src/core/excerpt.ts +16 -16
  57. package/src/core/page-definitions.ts +2 -2
  58. package/src/docs/agent.ts +1 -1
  59. package/src/docs/routing.ts +2 -2
  60. package/src/env.d.ts +6 -0
  61. package/src/index.ts +17 -4
  62. package/src/integration.ts +54 -0
  63. package/src/loader/live.ts +1 -1
  64. package/src/markdown/serializer.ts +26 -26
  65. package/src/schema/sqlite.ts +344 -0
  66. package/src/storage/index.ts +4 -1
@@ -22,8 +22,8 @@ export function getDocsVersions(entries: (DocEntry | string)[], locale?: string)
22
22
  continue
23
23
  }
24
24
 
25
- if (entry.properties?.version && String(entry.properties.version).startsWith("v")) {
26
- versions.add(String(entry.properties.version))
25
+ if (entry.properties?.["version"] && String(entry.properties["version"]).startsWith("v")) {
26
+ versions.add(String(entry.properties["version"]))
27
27
  continue
28
28
  }
29
29
 
package/src/env.d.ts CHANGED
@@ -15,3 +15,9 @@ declare module "virtual:rimelight-cms/db" {
15
15
  declare module "virtual:rimelight-cms/storage" {
16
16
  export const storageConfig: any
17
17
  }
18
+
19
+ declare module "virtual:rimelight-cms/auth" {
20
+ import type { AuthAdapter } from "@rimelight/auth"
21
+ export const authAdapter: AuthAdapter
22
+ export default authAdapter
23
+ }
package/src/index.ts CHANGED
@@ -23,11 +23,25 @@ export type {
23
23
  export * from "./core/registry.ts"
24
24
  export * from "./core/validator.ts"
25
25
 
26
- // Auth & Security Guards
26
+ // Universal Auth & Pluggable Adapters (RFC-0004 & RFC-0006)
27
27
  export * from "./auth/permissions.ts"
28
28
  export * from "./auth/filter-blocks.ts"
29
29
  export * from "./auth/editor-guards.ts"
30
30
  export * from "./auth/filter-templates.ts"
31
+ export {
32
+ cfAccessAuth,
33
+ auth0Auth,
34
+ mockAuth,
35
+ hasRole,
36
+ hasPermission,
37
+ evaluateAccess
38
+ } from "@rimelight/auth"
39
+ export type {
40
+ AuthAdapter as CmsAuthAdapter,
41
+ AuthAdapter,
42
+ UserSessionContext,
43
+ UserType
44
+ } from "@rimelight/auth"
31
45
 
32
46
  // Edge Cache & Search
33
47
  export * from "./cache/purge.ts"
@@ -36,9 +50,8 @@ export * from "./search/query.ts"
36
50
  // Database Schemas & Migration
37
51
  export * from "./schema/index.ts"
38
52
 
39
- // Astro Components
40
- export { default as PageRenderer } from "./astro/PageRenderer.astro"
41
- export { default as BlockRenderer } from "./astro/BlockRenderer.astro"
53
+ // Astro Integration & Components
54
+ export { rimelightCms, type RimelightCmsOptions } from "./integration.ts"
42
55
 
43
56
  // Toast API
44
57
  export * from "./client/toast.ts"
@@ -3,6 +3,7 @@ import path from "node:path"
3
3
  import fs from "node:fs"
4
4
  import { fileURLToPath } from "node:url"
5
5
  import type { StorageConfig } from "./storage/index"
6
+ import type { AuthAdapter } from "@rimelight/auth"
6
7
 
7
8
  export interface RimelightCmsOptions {
8
9
  /**
@@ -27,6 +28,12 @@ export interface RimelightCmsOptions {
27
28
  * "https://media.example.com" })).
28
29
  */
29
30
  storage?: StorageConfig | undefined
31
+
32
+ /**
33
+ * Pluggable Auth Adapter instance or module path (RFC-0004 / RFC-0006). E.g. cfAccessAuth({ ...
34
+ * }), auth0Auth({ ... }), or mockAuth().
35
+ */
36
+ auth?: AuthAdapter | string | undefined
30
37
  }
31
38
 
32
39
  export function rimelightCms(options: RimelightCmsOptions = {}): AstroIntegration {
@@ -66,6 +73,47 @@ export function rimelightCms(options: RimelightCmsOptions = {}): AstroIntegratio
66
73
  }
67
74
  }
68
75
 
76
+ // Resolve auth module / adapter code
77
+ let resolvedAuthCode: string
78
+ if (typeof options.auth === "string") {
79
+ const resolvedAuthPath = path.isAbsolute(options.auth)
80
+ ? options.auth
81
+ : path.resolve(rootDir, options.auth)
82
+ resolvedAuthCode = `export { authAdapter, authAdapter as default } from ${JSON.stringify(resolvedAuthPath)};`
83
+ } else if (options.auth && typeof options.auth === "object") {
84
+ const authObj = options.auth as any
85
+ const adapterName = authObj.name || "mock"
86
+ const adapterOpts = authObj.options || {}
87
+
88
+ if (adapterName === "cf-access") {
89
+ resolvedAuthCode = `import { cfAccessAuth } from "@rimelight/auth/cf-access";
90
+ export const authAdapter = cfAccessAuth(${JSON.stringify(adapterOpts)});
91
+ export default authAdapter;`
92
+ } else if (adapterName === "auth0") {
93
+ resolvedAuthCode = `import { auth0Auth } from "@rimelight/auth/auth0";
94
+ export const authAdapter = auth0Auth(${JSON.stringify(adapterOpts)});
95
+ export default authAdapter;`
96
+ } else {
97
+ resolvedAuthCode = `import { mockAuth } from "@rimelight/auth/mock";
98
+ export const authAdapter = mockAuth(${JSON.stringify(adapterOpts)});
99
+ export default authAdapter;`
100
+ }
101
+ } else {
102
+ // Default fallback mock admin adapter for development
103
+ resolvedAuthCode = `import { mockAuth } from "@rimelight/auth/mock";
104
+ export const authAdapter = mockAuth({
105
+ session: {
106
+ userId: "cms-admin",
107
+ name: "CMS Administrator",
108
+ email: "admin@rimelight.com",
109
+ roles: ["admin", "editor"],
110
+ permissions: ["*"],
111
+ userType: "employee"
112
+ }
113
+ });
114
+ export default authAdapter;`
115
+ }
116
+
69
117
  // 1. Inject CMS admin routes
70
118
  injectRoute({
71
119
  pattern: `${routePrefix}`,
@@ -166,6 +214,9 @@ export function rimelightCms(options: RimelightCmsOptions = {}): AstroIntegratio
166
214
  if (id === "virtual:rimelight-cms/storage") {
167
215
  return "\0virtual:rimelight-cms/storage"
168
216
  }
217
+ if (id === "virtual:rimelight-cms/auth") {
218
+ return "\0virtual:rimelight-cms/auth"
219
+ }
169
220
  return undefined
170
221
  },
171
222
  load(id) {
@@ -184,6 +235,9 @@ export function rimelightCms(options: RimelightCmsOptions = {}): AstroIntegratio
184
235
  if (id === "\0virtual:rimelight-cms/storage") {
185
236
  return `export const storageConfig = ${JSON.stringify(storageConfig)};`
186
237
  }
238
+ if (id === "\0virtual:rimelight-cms/auth") {
239
+ return resolvedAuthCode
240
+ }
187
241
  return undefined
188
242
  }
189
243
  }
@@ -56,7 +56,7 @@ function resolveLocalized(val: unknown, locale: string = "en"): string {
56
56
  if (typeof val === "string") return val
57
57
  if (typeof val === "object" && val !== null) {
58
58
  const rec = val as Record<string, string>
59
- return rec[locale] || rec.en || Object.values(rec)[0] || ""
59
+ return rec[locale] || rec["en"] || Object.values(rec)[0] || ""
60
60
  }
61
61
  return typeof val === "number" ? String(val) : ""
62
62
  }
@@ -46,18 +46,18 @@ export function blocksToMarkdown(
46
46
 
47
47
  for (const block of blocks) {
48
48
  if (!block) continue
49
- const children = block.children || block.props?.children || []
49
+ const children = block.children || block.props?.["children"] || []
50
50
 
51
51
  switch (block.type) {
52
52
  case "SectionBlock": {
53
- const level = (block.props?.level as HeadingLevel) || (baseLevel as HeadingLevel)
53
+ const level = (block.props?.["level"] as HeadingLevel) || (baseLevel as HeadingLevel)
54
54
  const hashes = "#".repeat(Math.min(6, Math.max(1, level)))
55
- const title = block.props?.title || ""
55
+ const title = block.props?.["title"] || ""
56
56
  if (title) {
57
57
  lines.push(`${hashes} ${title}`)
58
58
  }
59
- if (block.props?.description) {
60
- lines.push(`_${block.props.description}_`)
59
+ if (block.props?.["description"]) {
60
+ lines.push(`_${block.props["description"]}_`)
61
61
  }
62
62
  if (children.length > 0) {
63
63
  lines.push(blocksToMarkdown(children, { sectionLevel: Math.min(6, level + 1) }))
@@ -66,13 +66,13 @@ export function blocksToMarkdown(
66
66
  }
67
67
 
68
68
  case "ParagraphBlock": {
69
- const text = inlinesToMarkdown(block.props?.text)
69
+ const text = inlinesToMarkdown(block.props?.["text"])
70
70
  if (text) lines.push(text)
71
71
  break
72
72
  }
73
73
 
74
74
  case "CalloutBlock": {
75
- const variant = (block.props?.variant || "info").toUpperCase()
75
+ const variant = (block.props?.["variant"] || "info").toUpperCase()
76
76
  const childMd = blocksToMarkdown(children, options)
77
77
  const calloutHeader = `> [!${variant}]`
78
78
  if (childMd) {
@@ -88,16 +88,16 @@ export function blocksToMarkdown(
88
88
  }
89
89
 
90
90
  case "CodeBlock": {
91
- const lang = block.props?.language || ""
92
- const code = block.props?.code || ""
93
- const caption = block.props?.caption ? `\n_${block.props.caption}_` : ""
91
+ const lang = block.props?.["language"] || ""
92
+ const code = block.props?.["code"] || ""
93
+ const caption = block.props?.["caption"] ? `\n_${block.props["caption"]}_` : ""
94
94
  lines.push(`\`\`\`${lang}\n${code}\n\`\`\`${caption}`)
95
95
  break
96
96
  }
97
97
 
98
98
  case "TableBlock": {
99
- const columns = block.props?.columns || []
100
- const rows = block.props?.rows || []
99
+ const columns = block.props?.["columns"] || []
100
+ const rows = block.props?.["rows"] || []
101
101
  if (columns.length > 0) {
102
102
  const headerRow = `| ${columns.map((c: any) => c.header || c.key).join(" | ")} |`
103
103
  const alignRow = `| ${columns
@@ -111,16 +111,16 @@ export function blocksToMarkdown(
111
111
  (r: any) => `| ${columns.map((c: any) => r[c.key] || "").join(" | ")} |`
112
112
  )
113
113
  const tableMd = [headerRow, alignRow, ...bodyRows].join("\n")
114
- const caption = block.props?.caption ? `\n_${block.props.caption}_` : ""
114
+ const caption = block.props?.["caption"] ? `\n_${block.props["caption"]}_` : ""
115
115
  lines.push(`${tableMd}${caption}`)
116
116
  }
117
117
  break
118
118
  }
119
119
 
120
120
  case "ImageBlock": {
121
- const alt = block.props?.alt || ""
122
- const src = block.props?.src || ""
123
- const caption = block.props?.caption ? `\n_${block.props.caption}_` : ""
121
+ const alt = block.props?.["alt"] || ""
122
+ const src = block.props?.["src"] || ""
123
+ const caption = block.props?.["caption"] ? `\n_${block.props["caption"]}_` : ""
124
124
  lines.push(`![${alt}](${src})${caption}`)
125
125
  break
126
126
  }
@@ -135,7 +135,7 @@ export function blocksToMarkdown(
135
135
  }
136
136
 
137
137
  case "TabItemBlock": {
138
- const label = block.props?.label || "Tab"
138
+ const label = block.props?.["label"] || "Tab"
139
139
  lines.push(`#### ${label}\n\n${blocksToMarkdown(children, options)}`)
140
140
  break
141
141
  }
@@ -156,8 +156,8 @@ export function blocksToMarkdown(
156
156
  }
157
157
 
158
158
  case "StepItemBlock": {
159
- const title = block.props?.title || "Step"
160
- const desc = block.props?.description ? ` - ${block.props.description}` : ""
159
+ const title = block.props?.["title"] || "Step"
160
+ const desc = block.props?.["description"] ? ` - ${block.props["description"]}` : ""
161
161
  lines.push(`- **${title}**${desc}`)
162
162
  if (children.length > 0) {
163
163
  lines.push(blocksToMarkdown(children, options))
@@ -176,9 +176,9 @@ export function blocksToMarkdown(
176
176
  }
177
177
 
178
178
  case "CardBlock": {
179
- const title = block.props?.title || ""
180
- const href = block.props?.href || "#"
181
- const desc = block.props?.description ? `: ${block.props.description}` : ""
179
+ const title = block.props?.["title"] || ""
180
+ const href = block.props?.["href"] || "#"
181
+ const desc = block.props?.["description"] ? `: ${block.props["description"]}` : ""
182
182
  lines.push(`- [${title}](${href})${desc}`)
183
183
  break
184
184
  }
@@ -196,19 +196,19 @@ export function blocksToMarkdown(
196
196
  }
197
197
  return treeLines
198
198
  }
199
- const treeNodes = block.props?.tree || []
199
+ const treeNodes = block.props?.["tree"] || []
200
200
  lines.push(`\`\`\`text\n${renderTree(treeNodes).join("\n")}\n\`\`\``)
201
201
  break
202
202
  }
203
203
 
204
204
  case "DialogueBlock": {
205
- lines.push(`**${block.props?.character}:** "${block.props?.line || ""}"`)
205
+ lines.push(`**${block.props?.["character"]}:** "${block.props?.["line"] || ""}"`)
206
206
  break
207
207
  }
208
208
 
209
209
  default: {
210
- if (block.props?.text) {
211
- lines.push(inlinesToMarkdown(block.props.text))
210
+ if (block.props?.["text"]) {
211
+ lines.push(inlinesToMarkdown(block.props["text"]))
212
212
  }
213
213
  break
214
214
  }
@@ -0,0 +1,344 @@
1
+ import { sql } from "drizzle-orm"
2
+ import {
3
+ sqliteTable,
4
+ text,
5
+ integer,
6
+ uniqueIndex,
7
+ index,
8
+ type AnySQLiteColumn
9
+ } from "drizzle-orm/sqlite-core"
10
+ import type { BaseBlock } from "../core/types/blocks"
11
+ import type { Localized, PageType } from "../core/types/pages"
12
+ import type { BylineCredit } from "../core/types/bylines"
13
+ import type { PageVersionStatus } from "../core/types/versioning"
14
+ import type { TemplateApprovalRules, TemplateRolePermissions } from "../core/types/templates"
15
+
16
+ export interface SiteSettingsBranding {
17
+ logo: {
18
+ alt: string
19
+ }
20
+ favicon: {
21
+ svg: string
22
+ }
23
+ colors: {
24
+ themeColor: string
25
+ backgroundColor: string
26
+ }
27
+ }
28
+
29
+ export interface SiteSettingsSeo {
30
+ titleTemplate: string
31
+ ogImageFallback: string
32
+ maxDescriptionLength: number
33
+ authorHandle?: string
34
+ }
35
+
36
+ // 1. Page Templates
37
+ export const pageTemplates = sqliteTable("page_templates", {
38
+ id: text("id")
39
+ .$defaultFn(() => crypto.randomUUID())
40
+ .notNull()
41
+ .primaryKey(),
42
+ slug: text("slug").notNull().unique(),
43
+ title: text("title", { mode: "json" }).$type<Localized>().notNull(),
44
+ description: text("description", { mode: "json" }).$type<Localized>(),
45
+ pageType: text("page_type").$type<PageType>().notNull(),
46
+ version: integer("version").default(1).notNull(),
47
+ allowedRoles: text("allowed_roles", { mode: "json" })
48
+ .$type<string[]>()
49
+ .default(sql`'[]'`)
50
+ .notNull(),
51
+ rolePermissions: text("role_permissions", { mode: "json" })
52
+ .$type<TemplateRolePermissions>()
53
+ .default(sql`'{"whoCanCreate":[],"whoCanEdit":[],"whoCanReview":[],"whoCanView":[]}'`)
54
+ .notNull(),
55
+ requiredPermission: text("required_permission"),
56
+ approvalRules: text("approval_rules", { mode: "json" })
57
+ .$type<TemplateApprovalRules>()
58
+ .default(sql`'{"allowSelfApproval":true,"minApprovals":1}'`)
59
+ .notNull(),
60
+ defaultProperties: text("default_properties", { mode: "json" })
61
+ .default(sql`'{}'`)
62
+ .notNull(),
63
+ initialBlocks: text("initial_blocks", { mode: "json" })
64
+ .$type<BaseBlock[]>()
65
+ .default(sql`'[]'`)
66
+ .notNull(),
67
+ createdAt: integer("created_at", { mode: "timestamp" })
68
+ .$defaultFn(() => new Date())
69
+ .notNull(),
70
+ updatedAt: integer("updated_at", { mode: "timestamp" }).$onUpdate(() => new Date())
71
+ })
72
+
73
+ // 2. Page Versions
74
+ export const pageVersions = sqliteTable(
75
+ "page_versions",
76
+ {
77
+ id: text("id")
78
+ .$defaultFn(() => crypto.randomUUID())
79
+ .notNull()
80
+ .primaryKey(),
81
+ pageId: text("page_id").notNull(),
82
+ versionNumber: integer("version_number").notNull(),
83
+ status: text("status").$type<PageVersionStatus>().default("pending").notNull(),
84
+ slug: text("slug").notNull(),
85
+ type: text("type").$type<PageType>().notNull(),
86
+ title: text("title", { mode: "json" }).$type<Localized>().notNull(),
87
+ description: text("description", { mode: "json" }).$type<Localized>(),
88
+ tags: text("tags", { mode: "json" })
89
+ .$type<Localized[]>()
90
+ .default(sql`'[]'`)
91
+ .notNull(),
92
+ authorIds: text("author_ids", { mode: "json" })
93
+ .$type<string[]>()
94
+ .default(sql`'[]'`),
95
+ bylines: text("bylines", { mode: "json" })
96
+ .$type<BylineCredit[]>()
97
+ .default(sql`'[]'`),
98
+ content: text("content", { mode: "json" })
99
+ .$type<{
100
+ blocks: BaseBlock[]
101
+ properties: Record<string, any>
102
+ }>()
103
+ .notNull(),
104
+ createdBy: text("created_by").notNull(),
105
+ approvedBy: text("approved_by", { mode: "json" })
106
+ .$type<string[]>()
107
+ .default(sql`'[]'`)
108
+ .notNull(),
109
+ approvedAt: integer("approved_at", { mode: "timestamp" }),
110
+ changeSummary: text("change_summary"),
111
+ createdAt: integer("created_at", { mode: "timestamp" })
112
+ .$defaultFn(() => new Date())
113
+ .notNull()
114
+ },
115
+ (table) => [
116
+ uniqueIndex("page_versions_page_id_version_idx").on(table.pageId, table.versionNumber)
117
+ ]
118
+ )
119
+
120
+ // 3. Pages
121
+ export const pages = sqliteTable(
122
+ "pages",
123
+ {
124
+ id: text("id")
125
+ .$defaultFn(() => crypto.randomUUID())
126
+ .notNull()
127
+ .primaryKey(),
128
+ slug: text("slug").notNull(),
129
+ type: text("type").$type<PageType>().notNull(),
130
+ templateId: text("template_id").references(() => pageTemplates.id, { onDelete: "set null" }),
131
+ templateVersion: integer("template_version").default(1).notNull(),
132
+ title: text("title", { mode: "json" }).$type<Localized>().notNull(),
133
+ description: text("description", { mode: "json" }).$type<Localized>(),
134
+ tags: text("tags", { mode: "json" })
135
+ .$type<Localized[]>()
136
+ .default(sql`'[]'`)
137
+ .notNull(),
138
+ authorIds: text("author_ids", { mode: "json" })
139
+ .$type<string[]>()
140
+ .default(sql`'[]'`),
141
+ bylines: text("bylines", { mode: "json" })
142
+ .$type<BylineCredit[]>()
143
+ .default(sql`'[]'`),
144
+ content: text("content", { mode: "json" })
145
+ .$type<{
146
+ blocks: BaseBlock[]
147
+ properties: Record<string, any>
148
+ }>()
149
+ .notNull(),
150
+ publishedVersionId: text("published_version_id").references(() => pageVersions.id, {
151
+ onDelete: "set null"
152
+ }),
153
+ postedAt: integer("posted_at", { mode: "timestamp" }),
154
+ createdAt: integer("created_at", { mode: "timestamp" })
155
+ .$defaultFn(() => new Date())
156
+ .notNull(),
157
+ updatedAt: integer("updated_at", { mode: "timestamp" }).$onUpdate(() => new Date()),
158
+ deletedAt: integer("deleted_at", { mode: "timestamp" })
159
+ },
160
+ (table) => [
161
+ uniqueIndex("pages_slug_active_unique_idx")
162
+ .on(table.slug)
163
+ .where(sql`deleted_at IS NULL`)
164
+ ]
165
+ )
166
+
167
+ // 4. Page Drafts
168
+ export const pageDrafts = sqliteTable("page_drafts", {
169
+ pageId: text("page_id")
170
+ .notNull()
171
+ .primaryKey()
172
+ .references(() => pages.id, { onDelete: "cascade" }),
173
+ content: text("content", { mode: "json" })
174
+ .$type<{
175
+ blocks: BaseBlock[]
176
+ properties: Record<string, any>
177
+ }>()
178
+ .notNull(),
179
+ updatedBy: text("updated_by").notNull(),
180
+ updatedAt: integer("updated_at", { mode: "timestamp" })
181
+ .$defaultFn(() => new Date())
182
+ .notNull()
183
+ })
184
+
185
+ // 5. Page Draft Locks
186
+ export const pageDraftLocks = sqliteTable("page_draft_locks", {
187
+ pageId: text("page_id")
188
+ .notNull()
189
+ .primaryKey()
190
+ .references(() => pages.id, { onDelete: "cascade" }),
191
+ lockedByUserId: text("locked_by_user_id").notNull(),
192
+ lockedByUserName: text("locked_by_user_name").notNull(),
193
+ acquiredAt: integer("acquired_at", { mode: "timestamp" })
194
+ .$defaultFn(() => new Date())
195
+ .notNull(),
196
+ expiresAt: integer("expires_at", { mode: "timestamp" }).notNull()
197
+ })
198
+
199
+ // 6. Page Version Approvals
200
+ export const pageVersionApprovals = sqliteTable("page_version_approvals", {
201
+ id: text("id")
202
+ .$defaultFn(() => crypto.randomUUID())
203
+ .notNull()
204
+ .primaryKey(),
205
+ versionId: text("version_id")
206
+ .notNull()
207
+ .references(() => pageVersions.id, { onDelete: "cascade" }),
208
+ userId: text("user_id").notNull(),
209
+ userRole: text("user_role").notNull(),
210
+ approvedAt: integer("approved_at", { mode: "timestamp" })
211
+ .$defaultFn(() => new Date())
212
+ .notNull()
213
+ })
214
+
215
+ // 7. Page Version Comments
216
+ export const pageVersionComments = sqliteTable("page_version_comments", {
217
+ id: text("id")
218
+ .$defaultFn(() => crypto.randomUUID())
219
+ .notNull()
220
+ .primaryKey(),
221
+ versionId: text("version_id")
222
+ .notNull()
223
+ .references(() => pageVersions.id, { onDelete: "cascade" }),
224
+ userId: text("user_id").notNull(),
225
+ userRole: text("user_role").notNull(),
226
+ content: text("content").notNull(),
227
+ blockId: text("block_id"),
228
+ createdAt: integer("created_at", { mode: "timestamp" })
229
+ .$defaultFn(() => new Date())
230
+ .notNull()
231
+ })
232
+
233
+ // 8. Search
234
+ export const contentSearchIndex = sqliteTable(
235
+ "content_search_index",
236
+ {
237
+ id: text("id")
238
+ .$defaultFn(() => crypto.randomUUID())
239
+ .notNull()
240
+ .primaryKey(),
241
+ pageId: text("page_id")
242
+ .notNull()
243
+ .references(() => pages.id, { onDelete: "cascade" }),
244
+ locale: text("locale").notNull(),
245
+ titleText: text("title_text").notNull(),
246
+ contentText: text("content_text").notNull()
247
+ },
248
+ (table) => [index("content_search_index_page_id_idx").on(table.pageId)]
249
+ )
250
+
251
+ // 9. Site Settings
252
+ export const siteSettings = sqliteTable("site_settings", {
253
+ id: text("id").primaryKey().default("default"),
254
+ name: text("name").notNull(),
255
+ description: text("description").notNull(),
256
+ url: text("url").notNull(),
257
+ ogImage: text("og_image").notNull(),
258
+ author: text("author").notNull(),
259
+ email: text("email").notNull().default(""),
260
+ branding: text("branding", { mode: "json" }).$type<SiteSettingsBranding>().notNull(),
261
+ seo: text("seo", { mode: "json" }).$type<SiteSettingsSeo>().notNull(),
262
+ createdAt: integer("created_at", { mode: "timestamp" })
263
+ .$defaultFn(() => new Date())
264
+ .notNull(),
265
+ updatedAt: integer("updated_at", { mode: "timestamp" })
266
+ .$defaultFn(() => new Date())
267
+ .$onUpdate(() => new Date())
268
+ .notNull()
269
+ })
270
+
271
+ // 10. Taxonomies
272
+ export const taxonomyTerms = sqliteTable(
273
+ "taxonomy_terms",
274
+ {
275
+ id: text("id")
276
+ .$defaultFn(() => crypto.randomUUID())
277
+ .notNull()
278
+ .primaryKey(),
279
+ taxonomy: text("taxonomy").notNull(),
280
+ slug: text("slug").notNull(),
281
+ label: text("label", { mode: "json" }).$type<Localized | string>().notNull(),
282
+ description: text("description", { mode: "json" }).$type<Localized | string>(),
283
+ parentId: text("parent_id").references((): AnySQLiteColumn => taxonomyTerms.id, {
284
+ onDelete: "cascade"
285
+ }),
286
+ displayOrder: integer("display_order").default(0).notNull(),
287
+ createdAt: integer("created_at", { mode: "timestamp" })
288
+ .$defaultFn(() => new Date())
289
+ .notNull(),
290
+ updatedAt: integer("updated_at", { mode: "timestamp" }).$onUpdate(() => new Date())
291
+ },
292
+ (table) => [uniqueIndex("taxonomy_terms_tax_slug_idx").on(table.taxonomy, table.slug)]
293
+ )
294
+
295
+ export const pageTaxonomyTerms = sqliteTable(
296
+ "page_taxonomy_terms",
297
+ {
298
+ pageId: text("page_id")
299
+ .notNull()
300
+ .references(() => pages.id, { onDelete: "cascade" }),
301
+ termId: text("term_id")
302
+ .notNull()
303
+ .references(() => taxonomyTerms.id, { onDelete: "cascade" }),
304
+ assignedAt: integer("assigned_at", { mode: "timestamp" })
305
+ .$defaultFn(() => new Date())
306
+ .notNull()
307
+ },
308
+ (table) => [uniqueIndex("page_taxonomy_terms_page_term_idx").on(table.pageId, table.termId)]
309
+ )
310
+
311
+ // 11. Bylines
312
+ export const bylines = sqliteTable(
313
+ "bylines",
314
+ {
315
+ id: text("id")
316
+ .$defaultFn(() => crypto.randomUUID())
317
+ .primaryKey(),
318
+ name: text("name").notNull(),
319
+ slug: text("slug").notNull(),
320
+ websiteUrl: text("website_url"),
321
+ bio: text("bio"),
322
+ avatar: text("avatar"),
323
+ userId: text("user_id"),
324
+ socials: text("socials", { mode: "json" })
325
+ .$type<Record<string, string>>()
326
+ .default(sql`'{}'`),
327
+ metadata: text("metadata", { mode: "json" })
328
+ .$type<Record<string, any>>()
329
+ .default(sql`'{}'`),
330
+ createdAt: integer("created_at", { mode: "timestamp" })
331
+ .$defaultFn(() => new Date())
332
+ .notNull(),
333
+ updatedAt: integer("updated_at", { mode: "timestamp" })
334
+ .$defaultFn(() => new Date())
335
+ .notNull(),
336
+ deletedAt: integer("deleted_at", { mode: "timestamp" })
337
+ },
338
+ (table) => [uniqueIndex("bylines_slug_idx").on(table.slug)]
339
+ )
340
+
341
+ export type Byline = typeof bylines.$inferSelect
342
+ export type NewByline = typeof bylines.$inferInsert
343
+ export type SiteSettings = typeof siteSettings.$inferSelect
344
+ export type InsertSiteSettings = typeof siteSettings.$inferInsert
@@ -65,8 +65,11 @@ export async function getR2Bucket(locals?: any, bindingName = "BLOB"): Promise<a
65
65
  }
66
66
  } catch {}
67
67
 
68
- // 2. Direct locals property check (locals.env or locals[bindingName])
68
+ // 2. Direct locals property check (locals.runtime.env, locals.env or locals[bindingName])
69
69
  if (locals) {
70
+ if (locals.runtime?.env && locals.runtime.env[bindingName]) {
71
+ return locals.runtime.env[bindingName]
72
+ }
70
73
  if (locals.env && locals.env[bindingName]) {
71
74
  return locals.env[bindingName]
72
75
  }