@rimelight/cms 0.0.3 → 0.0.4

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.
@@ -0,0 +1,137 @@
1
+ import { sql } from "drizzle-orm";
2
+ import { index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
3
+ //#region src/schema/sqlite.ts
4
+ const pageTemplates = sqliteTable("page_templates", {
5
+ id: text("id").$defaultFn(() => crypto.randomUUID()).notNull().primaryKey(),
6
+ slug: text("slug").notNull().unique(),
7
+ title: text("title", { mode: "json" }).$type().notNull(),
8
+ description: text("description", { mode: "json" }).$type(),
9
+ pageType: text("page_type").$type().notNull(),
10
+ version: integer("version").default(1).notNull(),
11
+ allowedRoles: text("allowed_roles", { mode: "json" }).$type().default(sql`'[]'`).notNull(),
12
+ rolePermissions: text("role_permissions", { mode: "json" }).$type().default(sql`'{"whoCanCreate":[],"whoCanEdit":[],"whoCanReview":[],"whoCanView":[]}'`).notNull(),
13
+ requiredPermission: text("required_permission"),
14
+ approvalRules: text("approval_rules", { mode: "json" }).$type().default(sql`'{"allowSelfApproval":true,"minApprovals":1}'`).notNull(),
15
+ defaultProperties: text("default_properties", { mode: "json" }).default(sql`'{}'`).notNull(),
16
+ initialBlocks: text("initial_blocks", { mode: "json" }).$type().default(sql`'[]'`).notNull(),
17
+ createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => /* @__PURE__ */ new Date()).notNull(),
18
+ updatedAt: integer("updated_at", { mode: "timestamp" }).$onUpdate(() => /* @__PURE__ */ new Date())
19
+ });
20
+ const pageVersions = sqliteTable("page_versions", {
21
+ id: text("id").$defaultFn(() => crypto.randomUUID()).notNull().primaryKey(),
22
+ pageId: text("page_id").notNull(),
23
+ versionNumber: integer("version_number").notNull(),
24
+ status: text("status").$type().default("pending").notNull(),
25
+ slug: text("slug").notNull(),
26
+ type: text("type").$type().notNull(),
27
+ title: text("title", { mode: "json" }).$type().notNull(),
28
+ description: text("description", { mode: "json" }).$type(),
29
+ tags: text("tags", { mode: "json" }).$type().default(sql`'[]'`).notNull(),
30
+ authorIds: text("author_ids", { mode: "json" }).$type().default(sql`'[]'`),
31
+ bylines: text("bylines", { mode: "json" }).$type().default(sql`'[]'`),
32
+ content: text("content", { mode: "json" }).$type().notNull(),
33
+ createdBy: text("created_by").notNull(),
34
+ approvedBy: text("approved_by", { mode: "json" }).$type().default(sql`'[]'`).notNull(),
35
+ approvedAt: integer("approved_at", { mode: "timestamp" }),
36
+ changeSummary: text("change_summary"),
37
+ createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => /* @__PURE__ */ new Date()).notNull()
38
+ }, (table) => [uniqueIndex("page_versions_page_id_version_idx").on(table.pageId, table.versionNumber)]);
39
+ const pages = sqliteTable("pages", {
40
+ id: text("id").$defaultFn(() => crypto.randomUUID()).notNull().primaryKey(),
41
+ slug: text("slug").notNull(),
42
+ type: text("type").$type().notNull(),
43
+ templateId: text("template_id").references(() => pageTemplates.id, { onDelete: "set null" }),
44
+ templateVersion: integer("template_version").default(1).notNull(),
45
+ title: text("title", { mode: "json" }).$type().notNull(),
46
+ description: text("description", { mode: "json" }).$type(),
47
+ tags: text("tags", { mode: "json" }).$type().default(sql`'[]'`).notNull(),
48
+ authorIds: text("author_ids", { mode: "json" }).$type().default(sql`'[]'`),
49
+ bylines: text("bylines", { mode: "json" }).$type().default(sql`'[]'`),
50
+ content: text("content", { mode: "json" }).$type().notNull(),
51
+ publishedVersionId: text("published_version_id").references(() => pageVersions.id, { onDelete: "set null" }),
52
+ postedAt: integer("posted_at", { mode: "timestamp" }),
53
+ createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => /* @__PURE__ */ new Date()).notNull(),
54
+ updatedAt: integer("updated_at", { mode: "timestamp" }).$onUpdate(() => /* @__PURE__ */ new Date()),
55
+ deletedAt: integer("deleted_at", { mode: "timestamp" })
56
+ }, (table) => [uniqueIndex("pages_slug_active_unique_idx").on(table.slug).where(sql`deleted_at IS NULL`)]);
57
+ const pageDrafts = sqliteTable("page_drafts", {
58
+ pageId: text("page_id").notNull().primaryKey().references(() => pages.id, { onDelete: "cascade" }),
59
+ content: text("content", { mode: "json" }).$type().notNull(),
60
+ updatedBy: text("updated_by").notNull(),
61
+ updatedAt: integer("updated_at", { mode: "timestamp" }).$defaultFn(() => /* @__PURE__ */ new Date()).notNull()
62
+ });
63
+ const pageDraftLocks = sqliteTable("page_draft_locks", {
64
+ pageId: text("page_id").notNull().primaryKey().references(() => pages.id, { onDelete: "cascade" }),
65
+ lockedByUserId: text("locked_by_user_id").notNull(),
66
+ lockedByUserName: text("locked_by_user_name").notNull(),
67
+ acquiredAt: integer("acquired_at", { mode: "timestamp" }).$defaultFn(() => /* @__PURE__ */ new Date()).notNull(),
68
+ expiresAt: integer("expires_at", { mode: "timestamp" }).notNull()
69
+ });
70
+ const pageVersionApprovals = sqliteTable("page_version_approvals", {
71
+ id: text("id").$defaultFn(() => crypto.randomUUID()).notNull().primaryKey(),
72
+ versionId: text("version_id").notNull().references(() => pageVersions.id, { onDelete: "cascade" }),
73
+ userId: text("user_id").notNull(),
74
+ userRole: text("user_role").notNull(),
75
+ approvedAt: integer("approved_at", { mode: "timestamp" }).$defaultFn(() => /* @__PURE__ */ new Date()).notNull()
76
+ });
77
+ const pageVersionComments = sqliteTable("page_version_comments", {
78
+ id: text("id").$defaultFn(() => crypto.randomUUID()).notNull().primaryKey(),
79
+ versionId: text("version_id").notNull().references(() => pageVersions.id, { onDelete: "cascade" }),
80
+ userId: text("user_id").notNull(),
81
+ userRole: text("user_role").notNull(),
82
+ content: text("content").notNull(),
83
+ blockId: text("block_id"),
84
+ createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => /* @__PURE__ */ new Date()).notNull()
85
+ });
86
+ const contentSearchIndex = sqliteTable("content_search_index", {
87
+ id: text("id").$defaultFn(() => crypto.randomUUID()).notNull().primaryKey(),
88
+ pageId: text("page_id").notNull().references(() => pages.id, { onDelete: "cascade" }),
89
+ locale: text("locale").notNull(),
90
+ titleText: text("title_text").notNull(),
91
+ contentText: text("content_text").notNull()
92
+ }, (table) => [index("content_search_index_page_id_idx").on(table.pageId)]);
93
+ const siteSettings = sqliteTable("site_settings", {
94
+ id: text("id").primaryKey().default("default"),
95
+ name: text("name").notNull(),
96
+ description: text("description").notNull(),
97
+ url: text("url").notNull(),
98
+ ogImage: text("og_image").notNull(),
99
+ author: text("author").notNull(),
100
+ email: text("email").notNull().default(""),
101
+ branding: text("branding", { mode: "json" }).$type().notNull(),
102
+ seo: text("seo", { mode: "json" }).$type().notNull(),
103
+ createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => /* @__PURE__ */ new Date()).notNull(),
104
+ updatedAt: integer("updated_at", { mode: "timestamp" }).$defaultFn(() => /* @__PURE__ */ new Date()).$onUpdate(() => /* @__PURE__ */ new Date()).notNull()
105
+ });
106
+ const taxonomyTerms = sqliteTable("taxonomy_terms", {
107
+ id: text("id").$defaultFn(() => crypto.randomUUID()).notNull().primaryKey(),
108
+ taxonomy: text("taxonomy").notNull(),
109
+ slug: text("slug").notNull(),
110
+ label: text("label", { mode: "json" }).$type().notNull(),
111
+ description: text("description", { mode: "json" }).$type(),
112
+ parentId: text("parent_id").references(() => taxonomyTerms.id, { onDelete: "cascade" }),
113
+ displayOrder: integer("display_order").default(0).notNull(),
114
+ createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => /* @__PURE__ */ new Date()).notNull(),
115
+ updatedAt: integer("updated_at", { mode: "timestamp" }).$onUpdate(() => /* @__PURE__ */ new Date())
116
+ }, (table) => [uniqueIndex("taxonomy_terms_tax_slug_idx").on(table.taxonomy, table.slug)]);
117
+ const pageTaxonomyTerms = sqliteTable("page_taxonomy_terms", {
118
+ pageId: text("page_id").notNull().references(() => pages.id, { onDelete: "cascade" }),
119
+ termId: text("term_id").notNull().references(() => taxonomyTerms.id, { onDelete: "cascade" }),
120
+ assignedAt: integer("assigned_at", { mode: "timestamp" }).$defaultFn(() => /* @__PURE__ */ new Date()).notNull()
121
+ }, (table) => [uniqueIndex("page_taxonomy_terms_page_term_idx").on(table.pageId, table.termId)]);
122
+ const bylines = sqliteTable("bylines", {
123
+ id: text("id").$defaultFn(() => crypto.randomUUID()).primaryKey(),
124
+ name: text("name").notNull(),
125
+ slug: text("slug").notNull(),
126
+ websiteUrl: text("website_url"),
127
+ bio: text("bio"),
128
+ avatar: text("avatar"),
129
+ userId: text("user_id"),
130
+ socials: text("socials", { mode: "json" }).$type().default(sql`'{}'`),
131
+ metadata: text("metadata", { mode: "json" }).$type().default(sql`'{}'`),
132
+ createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => /* @__PURE__ */ new Date()).notNull(),
133
+ updatedAt: integer("updated_at", { mode: "timestamp" }).$defaultFn(() => /* @__PURE__ */ new Date()).notNull(),
134
+ deletedAt: integer("deleted_at", { mode: "timestamp" })
135
+ }, (table) => [uniqueIndex("bylines_slug_idx").on(table.slug)]);
136
+ //#endregion
137
+ export { bylines, contentSearchIndex, pageDraftLocks, pageDrafts, pageTaxonomyTerms, pageTemplates, pageVersionApprovals, pageVersionComments, pageVersions, pages, siteSettings, taxonomyTerms };
@@ -0,0 +1,59 @@
1
+ //#region src/storage/index.d.ts
2
+ interface StorageFile {
3
+ key: string;
4
+ name: string;
5
+ size: number;
6
+ mimeType: string;
7
+ uploadedAt: string;
8
+ url: string;
9
+ etag?: string;
10
+ customMetadata?: Record<string, string>;
11
+ }
12
+ interface StorageListResult {
13
+ files: StorageFile[];
14
+ truncated: boolean;
15
+ cursor?: string;
16
+ }
17
+ interface R2StorageOptions {
18
+ binding?: string | undefined;
19
+ publicUrl?: string | undefined;
20
+ prefix?: string | undefined;
21
+ }
22
+ interface StorageConfig {
23
+ provider: "r2" | "local" | "s3";
24
+ binding?: string | undefined;
25
+ publicUrl?: string | undefined;
26
+ prefix?: string | undefined;
27
+ }
28
+ interface StorageListOptions {
29
+ prefix?: string | undefined;
30
+ cursor?: string | undefined;
31
+ limit?: number | undefined;
32
+ publicUrl?: string | undefined;
33
+ apiBasePath?: string | undefined;
34
+ }
35
+ interface StorageUploadOptions {
36
+ key?: string | undefined;
37
+ prefix?: string | undefined;
38
+ publicUrl?: string | undefined;
39
+ apiBasePath?: string | undefined;
40
+ }
41
+ declare function r2(options?: R2StorageOptions): StorageConfig;
42
+ /**
43
+ * Resolves Cloudflare R2 bucket instance from Cloudflare env or Astro locals.
44
+ */
45
+ declare function getR2Bucket(locals?: any, bindingName?: string): Promise<any>;
46
+ /**
47
+ * Lists files from R2 Bucket under configured prefix.
48
+ */
49
+ declare function listR2Files(bucket: any, options?: StorageListOptions): Promise<StorageListResult>;
50
+ /**
51
+ * Uploads a file buffer/stream into R2 Bucket.
52
+ */
53
+ declare function uploadR2File(bucket: any, file: File, options?: StorageUploadOptions): Promise<StorageFile>;
54
+ /**
55
+ * Deletes a file from R2 Bucket by key.
56
+ */
57
+ declare function deleteR2File(bucket: any, key: string): Promise<boolean>;
58
+ //#endregion
59
+ export { R2StorageOptions, StorageConfig, StorageFile, StorageListOptions, StorageListResult, StorageUploadOptions, deleteR2File, getR2Bucket, listR2Files, r2, uploadR2File };
@@ -0,0 +1,133 @@
1
+ //#region src/storage/index.ts
2
+ function r2(options = {}) {
3
+ return {
4
+ provider: "r2",
5
+ binding: options.binding ?? "BLOB",
6
+ publicUrl: options.publicUrl,
7
+ prefix: options.prefix ?? "media/"
8
+ };
9
+ }
10
+ /**
11
+ * Resolves Cloudflare R2 bucket instance from Cloudflare env or Astro locals.
12
+ */
13
+ async function getR2Bucket(locals, bindingName = "BLOB") {
14
+ try {
15
+ const cf = await import("cloudflare:workers");
16
+ if (cf?.env && cf.env[bindingName]) return cf.env[bindingName];
17
+ } catch {}
18
+ if (locals) {
19
+ if (locals.runtime?.env && locals.runtime.env[bindingName]) return locals.runtime.env[bindingName];
20
+ if (locals.env && locals.env[bindingName]) return locals.env[bindingName];
21
+ if (locals[bindingName]) return locals[bindingName];
22
+ }
23
+ if (typeof process !== "undefined" && process.env && process.env[bindingName]) return process.env[bindingName];
24
+ return null;
25
+ }
26
+ /**
27
+ * Lists files from R2 Bucket under configured prefix.
28
+ */
29
+ async function listR2Files(bucket, options = {}) {
30
+ if (!bucket || typeof bucket.list !== "function") return {
31
+ files: [],
32
+ truncated: false
33
+ };
34
+ const prefix = options.prefix ?? "media/";
35
+ const limit = options.limit ?? 50;
36
+ const result = await bucket.list({
37
+ prefix,
38
+ cursor: options.cursor,
39
+ limit
40
+ });
41
+ return {
42
+ files: (result.objects || []).map((obj) => {
43
+ const rawKey = obj.key;
44
+ const name = rawKey.startsWith(prefix) ? rawKey.slice(prefix.length) : rawKey;
45
+ const cleanedName = name.includes("/") ? name.split("/").pop() : name;
46
+ const mimeType = obj.httpMetadata?.contentType || guessMimeType(rawKey);
47
+ let url = "";
48
+ if (options.publicUrl) url = `${options.publicUrl.endsWith("/") ? options.publicUrl.slice(0, -1) : options.publicUrl}/${rawKey}`;
49
+ else if (options.apiBasePath) url = `${options.apiBasePath.endsWith("/") ? options.apiBasePath.slice(0, -1) : options.apiBasePath}/${rawKey}`;
50
+ else url = `/cms/api/media/${rawKey}`;
51
+ return {
52
+ key: rawKey,
53
+ name: cleanedName,
54
+ size: obj.size,
55
+ mimeType,
56
+ uploadedAt: obj.uploaded ? new Date(obj.uploaded).toISOString() : (/* @__PURE__ */ new Date()).toISOString(),
57
+ url,
58
+ etag: obj.etag,
59
+ customMetadata: obj.customMetadata
60
+ };
61
+ }),
62
+ truncated: result.truncated,
63
+ cursor: result.cursor
64
+ };
65
+ }
66
+ /**
67
+ * Uploads a file buffer/stream into R2 Bucket.
68
+ */
69
+ async function uploadR2File(bucket, file, options = {}) {
70
+ if (!bucket || typeof bucket.put !== "function") throw new Error("R2 bucket binding is not available in runtime environment.");
71
+ const prefix = options.prefix ?? "media/";
72
+ const timestamp = Date.now();
73
+ const safeName = file.name.replace(/[^a-zA-Z0-9._-]/g, "_");
74
+ const randomSuffix = Math.random().toString(36).substring(2, 8);
75
+ const key = options.key ?? `${prefix}${timestamp}-${randomSuffix}/${safeName}`;
76
+ const arrayBuffer = await file.arrayBuffer();
77
+ const r2Object = await bucket.put(key, arrayBuffer, {
78
+ httpMetadata: {
79
+ contentType: file.type || guessMimeType(safeName),
80
+ contentDisposition: `inline; filename="${safeName}"`
81
+ },
82
+ customMetadata: {
83
+ originalName: file.name,
84
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
85
+ }
86
+ });
87
+ let url = "";
88
+ if (options.publicUrl) url = `${options.publicUrl.endsWith("/") ? options.publicUrl.slice(0, -1) : options.publicUrl}/${key}`;
89
+ else if (options.apiBasePath) url = `${options.apiBasePath.endsWith("/") ? options.apiBasePath.slice(0, -1) : options.apiBasePath}/${key}`;
90
+ else url = `/cms/api/media/${key}`;
91
+ return {
92
+ key,
93
+ name: safeName,
94
+ size: file.size,
95
+ mimeType: file.type || guessMimeType(safeName),
96
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
97
+ url,
98
+ etag: r2Object?.etag
99
+ };
100
+ }
101
+ /**
102
+ * Deletes a file from R2 Bucket by key.
103
+ */
104
+ async function deleteR2File(bucket, key) {
105
+ if (!bucket || typeof bucket.delete !== "function") throw new Error("R2 bucket binding is not available in runtime environment.");
106
+ await bucket.delete(key);
107
+ return true;
108
+ }
109
+ /**
110
+ * MIME type guesser fallback.
111
+ */
112
+ function guessMimeType(filename) {
113
+ switch (filename.split(".").pop()?.toLowerCase()) {
114
+ case "jpg":
115
+ case "jpeg": return "image/jpeg";
116
+ case "png": return "image/png";
117
+ case "webp": return "image/webp";
118
+ case "gif": return "image/gif";
119
+ case "svg": return "image/svg+xml";
120
+ case "avif": return "image/avif";
121
+ case "mp4": return "video/mp4";
122
+ case "webm": return "video/webm";
123
+ case "mp3": return "audio/mpeg";
124
+ case "wav": return "audio/wav";
125
+ case "pdf": return "application/pdf";
126
+ case "json": return "application/json";
127
+ case "txt": return "text/plain";
128
+ case "zip": return "application/zip";
129
+ default: return "application/octet-stream";
130
+ }
131
+ }
132
+ //#endregion
133
+ export { deleteR2File, getR2Bucket, listR2Files, r2, uploadR2File };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rimelight/cms",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "private": false,
5
5
  "description": "Rimelight Entertainment's Content Management System Package",
6
6
  "homepage": "https://rimelight.com/docs",
@@ -16,12 +16,31 @@
16
16
  "url": "git+https://github.com/Rimelight-Entertainment/rimelight.git"
17
17
  },
18
18
  "files": [
19
+ "dist",
19
20
  "src"
20
21
  ],
21
22
  "type": "module",
22
23
  "exports": {
23
- ".": "./src/index.ts",
24
- "./schema": "./src/schema/index.ts",
24
+ ".": {
25
+ "types": "./dist/index.d.mts",
26
+ "import": "./dist/index.mjs"
27
+ },
28
+ "./schema": {
29
+ "types": "./dist/schema/index.d.mts",
30
+ "import": "./dist/schema/index.mjs"
31
+ },
32
+ "./schema/sqlite": {
33
+ "types": "./dist/schema/sqlite.d.mts",
34
+ "import": "./dist/schema/sqlite.mjs"
35
+ },
36
+ "./integration": {
37
+ "types": "./dist/integration.d.mts",
38
+ "import": "./dist/integration.mjs"
39
+ },
40
+ "./storage": {
41
+ "types": "./dist/storage/index.d.mts",
42
+ "import": "./dist/storage/index.mjs"
43
+ },
25
44
  "./core/*": "./src/core/*",
26
45
  "./astro/*": "./src/astro/*",
27
46
  "./client/styles/*": "./src/client/styles/*",
@@ -34,38 +53,35 @@
34
53
  "./auth/*": "./src/auth/*",
35
54
  "./cache/*": "./src/cache/*",
36
55
  "./search/*": "./src/search/*",
37
- "./integration": "./src/integration.ts",
38
- "./storage": "./src/storage/index.ts",
39
56
  "./admin/*": "./src/admin/*"
40
57
  },
41
58
  "publishConfig": {
42
59
  "access": "public"
43
60
  },
44
61
  "dependencies": {
45
- "@rimelight/i18n": "0.0.6",
46
- "@rimelight/ui": "0.0.44",
62
+ "@rimelight/auth": "0.0.3",
63
+ "@rimelight/i18n": "0.0.7",
64
+ "@rimelight/ui": "0.0.46",
47
65
  "drizzle-orm": "0.45.2",
48
66
  "solid-js": "1.9.15",
49
67
  "sortablejs": "1.15.7"
50
68
  },
51
69
  "devDependencies": {
52
70
  "@astrojs/check": "0.9.10",
53
- "@rimelight/config": "0.0.3",
71
+ "@rimelight/config": "0.0.4",
54
72
  "@types/sortablejs": "1.15.9",
55
- "astro": "7.2.7",
56
- "better-auth": "1.7.2",
73
+ "astro": "7.3.1",
57
74
  "typescript": "6.0.3",
58
- "unocss": "66.8.1",
59
- "vitest": "4.1.11"
75
+ "unocss": "66.10.0"
60
76
  },
61
77
  "peerDependencies": {
62
- "astro": ">=7.0.0",
63
- "better-auth": ">=1.0.0"
78
+ "astro": ">=7.0.0"
64
79
  },
65
80
  "engines": {
66
81
  "node": ">=26.7.0"
67
82
  },
68
83
  "scripts": {
84
+ "build": "vp pack",
69
85
  "check": "vp check --fix && astro check"
70
86
  }
71
87
  }
@@ -2,11 +2,20 @@ import type { APIRoute } from "astro"
2
2
  import { getR2Bucket, listR2Files, uploadR2File, deleteR2File } from "../../storage/index"
3
3
  // @ts-ignore
4
4
  import { storageConfig } from "virtual:rimelight-cms/storage"
5
+ // @ts-ignore
6
+ import authAdapter from "virtual:rimelight-cms/auth"
5
7
 
6
8
  export const prerender = false
7
9
 
8
10
  export const GET: APIRoute = async ({ request, locals }) => {
9
11
  try {
12
+ const session = await authAdapter.getSession(request)
13
+ if (!session) {
14
+ return new Response(JSON.stringify({ success: false, error: "Unauthorized" }), {
15
+ status: 401,
16
+ headers: { "Content-Type": "application/json" }
17
+ })
18
+ }
10
19
  const url = new URL(request.url)
11
20
  const prefix = (url.searchParams.get("prefix") ??
12
21
  (storageConfig?.prefix as string | undefined) ??
@@ -17,7 +26,7 @@ export const GET: APIRoute = async ({ request, locals }) => {
17
26
  const typeFilter = url.searchParams.get("type")?.toLowerCase()
18
27
 
19
28
  const bindingName = (storageConfig?.binding as string | undefined) ?? "BLOB"
20
- const bucket = getR2Bucket(locals, bindingName)
29
+ const bucket = await getR2Bucket(locals, bindingName)
21
30
 
22
31
  if (!bucket) {
23
32
  return new Response(
@@ -87,8 +96,16 @@ export const GET: APIRoute = async ({ request, locals }) => {
87
96
 
88
97
  export const POST: APIRoute = async ({ request, locals }) => {
89
98
  try {
99
+ const session = await authAdapter.getSession(request)
100
+ if (!session) {
101
+ return new Response(JSON.stringify({ success: false, error: "Unauthorized" }), {
102
+ status: 401,
103
+ headers: { "Content-Type": "application/json" }
104
+ })
105
+ }
106
+
90
107
  const bindingName = storageConfig?.binding ?? "BLOB"
91
- const bucket = getR2Bucket(locals, bindingName)
108
+ const bucket = await getR2Bucket(locals, bindingName)
92
109
 
93
110
  if (!bucket) {
94
111
  return new Response(
@@ -150,8 +167,16 @@ export const POST: APIRoute = async ({ request, locals }) => {
150
167
 
151
168
  export const DELETE: APIRoute = async ({ request, locals }) => {
152
169
  try {
170
+ const session = await authAdapter.getSession(request)
171
+ if (!session) {
172
+ return new Response(JSON.stringify({ success: false, error: "Unauthorized" }), {
173
+ status: 401,
174
+ headers: { "Content-Type": "application/json" }
175
+ })
176
+ }
177
+
153
178
  const bindingName = storageConfig?.binding ?? "BLOB"
154
- const bucket = getR2Bucket(locals, bindingName)
179
+ const bucket = await getR2Bucket(locals, bindingName)
155
180
 
156
181
  if (!bucket) {
157
182
  return new Response(
@@ -1,15 +1,26 @@
1
1
  ---
2
2
  import ParentLayout from "virtual:rimelight-cms/parent-layout";
3
+ import authAdapter from "virtual:rimelight-cms/auth";
3
4
  import RLADashboardGroup from "@rimelight/ui/components/dashboard-group/RLADashboardGroup.astro";
4
5
  import RLADashboardSidebar from "@rimelight/ui/components/dashboard-sidebar/RLADashboardSidebar.astro";
5
6
  import RLADashboardNavbar from "@rimelight/ui/components/dashboard-navbar/RLADashboardNavbar.astro";
6
7
  import RLADashboardPanel from "@rimelight/ui/components/dashboard-panel/RLADashboardPanel.astro";
7
8
  import RLADashboardToolbar from "@rimelight/ui/components/dashboard-toolbar/RLADashboardToolbar.astro";
8
9
  import RLADashboardSidebarCollapse from "@rimelight/ui/components/dashboard-sidebar-collapse/RLADashboardSidebarCollapse.astro";
10
+ import RLADashboardSidebarToggle from "@rimelight/ui/components/dashboard-sidebar-toggle/RLADashboardSidebarToggle.astro";
9
11
  import RLAIcon from "@rimelight/ui/components/icon/RLAIcon.astro";
10
12
  import { getRelativeLocaleUrl } from "@rimelight/i18n";
11
13
  import "../../client/styles/cms-editor.css";
12
14
 
15
+ const session = await authAdapter.getSession(Astro.request);
16
+ if (!session) {
17
+ if (authAdapter.handleUnauthorized) {
18
+ const res = await authAdapter.handleUnauthorized(Astro.request);
19
+ if (res instanceof Response) return res;
20
+ }
21
+ return Astro.redirect(getRelativeLocaleUrl(Astro.currentLocale ?? "en", "/auth/sign-in"));
22
+ }
23
+
13
24
  const currentLocale = Astro.currentLocale ?? "en";
14
25
 
15
26
  const dashboardItem = {
@@ -149,10 +160,12 @@ const {
149
160
  </RLADashboardSidebar>
150
161
 
151
162
  <RLADashboardPanel>
152
- <RLADashboardNavbar slot="header" title={title} icon="i-lucide-file-text" style="height: 4rem; min-height: 4rem;">
163
+ <RLADashboardNavbar slot="header" title={title} icon="i-lucide-file-text" class="h-16 min-h-16">
153
164
  <Fragment slot="left">
154
- <div class="h-16 min-h-16 flex items-center gap-3" style="height: 4rem; min-height: 4rem; display: flex; align-items: center;">
155
- <h1 class="text-xl font-semibold text-highlighted" data-dashboard-navbar-title>{title}</h1>
165
+ <div class="h-16 min-h-16 flex items-center gap-3">
166
+ <RLADashboardSidebarToggle class="flex lg:hidden size-9 rounded-lg items-center justify-center border border-default hover:bg-muted text-toned hover:text-highlighted transition-colors shrink-0" />
167
+ <RLADashboardSidebarCollapse side="left" class="hidden lg:flex size-9 rounded-lg" />
168
+ <h1 class="text-lg sm:text-xl font-semibold text-highlighted truncate" data-dashboard-navbar-title>{title}</h1>
156
169
  </div>
157
170
  </Fragment>
158
171
  <Fragment slot="right">
@@ -18,7 +18,7 @@ if (!id) {
18
18
  }
19
19
 
20
20
  const token = Astro.url.searchParams.get("token");
21
- const secret = process.env.BETTER_AUTH_SECRET || "rimelight-preview-secret-key";
21
+ const secret = process.env.CMS_PREVIEW_SECRET || "rimelight-preview-secret-key";
22
22
  if (token) {
23
23
  await verifyPreviewToken(id, token, secret);
24
24
  }
@@ -6,10 +6,10 @@ import BlockRenderer from "./BlockRenderer.astro"
6
6
 
7
7
  interface Props {
8
8
  blocks: BaseBlock[]
9
- session?: UserSessionContext
10
- locale?: string
11
- class?: string
12
- components?: Record<string, any>
9
+ session?: UserSessionContext | undefined
10
+ locale?: string | undefined
11
+ class?: string | undefined
12
+ components?: Record<string, any> | undefined
13
13
  }
14
14
 
15
15
  const {
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"