@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
@@ -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.5",
4
4
  "private": false,
5
5
  "description": "Rimelight Entertainment's Content Management System Package",
6
6
  "homepage": "https://rimelight.com/docs",
@@ -16,14 +16,38 @@
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/*",
46
+ "./cron/scheduled": {
47
+ "types": "./src/cron/scheduled.ts",
48
+ "import": "./src/cron/scheduled.ts"
49
+ },
50
+ "./cron/*": "./src/cron/*",
27
51
  "./client/styles/*": "./src/client/styles/*",
28
52
  "./client/components": "./src/client/components/index.ts",
29
53
  "./client/components/*.tsx": "./src/client/components/*.tsx",
@@ -34,38 +58,35 @@
34
58
  "./auth/*": "./src/auth/*",
35
59
  "./cache/*": "./src/cache/*",
36
60
  "./search/*": "./src/search/*",
37
- "./integration": "./src/integration.ts",
38
- "./storage": "./src/storage/index.ts",
39
61
  "./admin/*": "./src/admin/*"
40
62
  },
41
63
  "publishConfig": {
42
64
  "access": "public"
43
65
  },
44
66
  "dependencies": {
45
- "@rimelight/i18n": "0.0.6",
46
- "@rimelight/ui": "0.0.44",
67
+ "@rimelight/auth": "0.0.3",
68
+ "@rimelight/i18n": "0.0.9",
69
+ "@rimelight/ui": "0.0.47",
47
70
  "drizzle-orm": "0.45.2",
48
71
  "solid-js": "1.9.15",
49
72
  "sortablejs": "1.15.7"
50
73
  },
51
74
  "devDependencies": {
52
75
  "@astrojs/check": "0.9.10",
53
- "@rimelight/config": "0.0.3",
76
+ "@rimelight/config": "0.0.5",
54
77
  "@types/sortablejs": "1.15.9",
55
- "astro": "7.2.7",
56
- "better-auth": "1.7.2",
78
+ "astro": "7.3.1",
57
79
  "typescript": "6.0.3",
58
- "unocss": "66.8.1",
59
- "vitest": "4.1.11"
80
+ "unocss": "66.10.0"
60
81
  },
61
82
  "peerDependencies": {
62
- "astro": ">=7.0.0",
63
- "better-auth": ">=1.0.0"
83
+ "astro": ">=7.0.0"
64
84
  },
65
85
  "engines": {
66
86
  "node": ">=26.7.0"
67
87
  },
68
88
  "scripts": {
89
+ "build": "vp pack",
69
90
  "check": "vp check --fix && astro check"
70
91
  }
71
92
  }
@@ -14,7 +14,7 @@ export const GET: APIRoute = async ({ params, locals }) => {
14
14
  return new Response("Storage binding not found.", { status: 500 })
15
15
  }
16
16
 
17
- const key = params.key as string
17
+ const key = params["key"] as string
18
18
  if (!key) {
19
19
  return new Response("Missing file key.", { status: 400 })
20
20
  }
@@ -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">
@@ -199,7 +199,7 @@ function formatDate(dateStr?: string): string {
199
199
  const root = document.getElementById("assets-page-root");
200
200
  if (!root) return;
201
201
 
202
- const apiEndpoint = root.dataset.apiEndpoint || "/cms/api/media";
202
+ const apiEndpoint = root.dataset["apiEndpoint"] || "/cms/api/media";
203
203
  const uploadInput = document.getElementById("media-upload-input") as HTMLInputElement | null;
204
204
  const dropzone = document.getElementById("media-dropzone");
205
205
  const indicator = document.getElementById("upload-status-indicator");
@@ -280,10 +280,10 @@ function formatDate(dateStr?: string): string {
280
280
 
281
281
  const card = document.createElement("div");
282
282
  card.className = "border border-default rounded-lg bg-default overflow-hidden flex flex-col hover:border-primary/50 transition-all group asset-card animate-in fade-in";
283
- card.dataset.key = asset.key;
284
- card.dataset.name = asset.name;
285
- card.dataset.type = asset.mimeType;
286
- card.dataset.url = asset.url;
283
+ card.dataset["key"] = asset.key;
284
+ card.dataset["name"] = asset.name;
285
+ card.dataset["type"] = asset.mimeType;
286
+ card.dataset["url"] = asset.url;
287
287
 
288
288
  card.innerHTML = `
289
289
  <div class="h-40 bg-muted/30 flex items-center justify-center relative overflow-hidden">
@@ -320,7 +320,7 @@ function formatDate(dateStr?: string): string {
320
320
  parent.querySelectorAll(".copy-url-btn").forEach((btn) => {
321
321
  btn.addEventListener("click", async (e) => {
322
322
  e.stopPropagation();
323
- const url = (btn as HTMLElement).dataset.url;
323
+ const url = (btn as HTMLElement).dataset["url"];
324
324
  if (url) {
325
325
  const fullUrl = url.startsWith("http") ? url : window.location.origin + url;
326
326
  await navigator.clipboard.writeText(fullUrl);
@@ -337,7 +337,7 @@ function formatDate(dateStr?: string): string {
337
337
  parent.querySelectorAll(".delete-asset-btn").forEach((btn) => {
338
338
  btn.addEventListener("click", async (e) => {
339
339
  e.stopPropagation();
340
- const key = (btn as HTMLElement).dataset.key;
340
+ const key = (btn as HTMLElement).dataset["key"];
341
341
  if (!key) return;
342
342
  if (!confirm(`Are you sure you want to delete this file from R2 storage?\nKey: ${key}`)) return;
343
343
 
@@ -368,9 +368,9 @@ function formatDate(dateStr?: string): string {
368
368
  let visibleCount = 0;
369
369
 
370
370
  cards.forEach((card) => {
371
- const name = (card.dataset.name || "").toLowerCase();
372
- const key = (card.dataset.key || "").toLowerCase();
373
- const type = (card.dataset.type || "").toLowerCase();
371
+ const name = (card.dataset["name"] || "").toLowerCase();
372
+ const key = (card.dataset["key"] || "").toLowerCase();
373
+ const type = (card.dataset["type"] || "").toLowerCase();
374
374
 
375
375
  const matchesSearch = !searchQuery || name.includes(searchQuery) || key.includes(searchQuery);
376
376
  let matchesFilter = true;
@@ -408,7 +408,7 @@ function formatDate(dateStr?: string): string {
408
408
  btn.classList.remove("bg-muted", "text-muted-foreground");
409
409
  btn.classList.add("bg-primary", "text-white");
410
410
 
411
- currentFilter = (btn as HTMLElement).dataset.filter || "all";
411
+ currentFilter = (btn as HTMLElement).dataset["filter"] || "all";
412
412
  filterCards();
413
413
  });
414
414
  });
@@ -15,7 +15,7 @@ import "../../client/styles/cms-editor.css";
15
15
  export const prerender = false;
16
16
 
17
17
  const currentLocale = Astro.currentLocale ?? "en";
18
- const id = Astro.params.id;
18
+ const id = Astro.params["id"];
19
19
 
20
20
  let page: any = null;
21
21
  let draft: any = null;
@@ -57,12 +57,12 @@ const extractedBlocks = Array.isArray(rawContent?.blocks)
57
57
 
58
58
  const titleVal =
59
59
  typeof page.title === "object" && page.title !== null
60
- ? (page.title as Record<string, string>)[currentLocale] || (page.title as Record<string, string>).en || ""
60
+ ? (page.title as Record<string, string>)[currentLocale] || (page.title as Record<string, string>)["en"] || ""
61
61
  : String(page.title || "");
62
62
 
63
63
  const descVal =
64
64
  typeof page.description === "object" && page.description !== null
65
- ? (page.description as Record<string, string>)[currentLocale] || (page.description as Record<string, string>).en || ""
65
+ ? (page.description as Record<string, string>)[currentLocale] || (page.description as Record<string, string>)["en"] || ""
66
66
  : String(page.description || "");
67
67
 
68
68
  const pageType = (page as any).type || "blog";
@@ -29,7 +29,7 @@ if (db) {
29
29
  const formattedPages = pagesList.map((page) => {
30
30
  const titleVal =
31
31
  typeof page.title === "object" && page.title !== null
32
- ? (page.title as Record<string, string>).en || ""
32
+ ? (page.title as Record<string, string>)["en"] || ""
33
33
  : String(page.title || "");
34
34
  return {
35
35
  id: page.id,
@@ -124,7 +124,7 @@ const columns: TableColumn[] = [
124
124
  <script>
125
125
  document.querySelectorAll(".delete-page-btn").forEach((btn) => {
126
126
  btn.addEventListener("click", async () => {
127
- const pageId = (btn as HTMLElement).dataset.pageId;
127
+ const pageId = (btn as HTMLElement).dataset["pageId"];
128
128
  if (!pageId) return;
129
129
  if (!confirm("Are you sure you want to delete this page?")) return;
130
130