@structcms/api 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1124 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ ALLOWED_MIME_TYPES: () => ALLOWED_MIME_TYPES,
34
+ AuthError: () => AuthError,
35
+ AuthValidationError: () => AuthValidationError,
36
+ MediaError: () => MediaError,
37
+ MediaValidationError: () => MediaValidationError,
38
+ StorageError: () => StorageError,
39
+ StorageValidationError: () => StorageValidationError,
40
+ SupabaseAuthAdapter: () => SupabaseAuthAdapter,
41
+ SupabaseMediaAdapter: () => SupabaseMediaAdapter,
42
+ SupabaseStorageAdapter: () => SupabaseStorageAdapter,
43
+ createAuthAdapter: () => createAuthAdapter,
44
+ createAuthMiddleware: () => createAuthMiddleware,
45
+ createMediaAdapter: () => createMediaAdapter,
46
+ createStorageAdapter: () => createStorageAdapter,
47
+ createSupabaseAdapters: () => createSupabaseAdapters,
48
+ ensureUniqueSlug: () => ensureUniqueSlug,
49
+ generateSlug: () => generateSlug,
50
+ handleCreateNavigation: () => handleCreateNavigation,
51
+ handleCreatePage: () => handleCreatePage,
52
+ handleDeleteMedia: () => handleDeleteMedia,
53
+ handleDeleteNavigation: () => handleDeleteNavigation,
54
+ handleDeletePage: () => handleDeletePage,
55
+ handleExportAllPages: () => handleExportAllPages,
56
+ handleExportNavigations: () => handleExportNavigations,
57
+ handleExportPage: () => handleExportPage,
58
+ handleExportSite: () => handleExportSite,
59
+ handleGetCurrentUser: () => handleGetCurrentUser,
60
+ handleGetMedia: () => handleGetMedia,
61
+ handleGetNavigation: () => handleGetNavigation,
62
+ handleGetPageBySlug: () => handleGetPageBySlug,
63
+ handleListMedia: () => handleListMedia,
64
+ handleListPages: () => handleListPages,
65
+ handleRefreshSession: () => handleRefreshSession,
66
+ handleSignInWithOAuth: () => handleSignInWithOAuth,
67
+ handleSignInWithPassword: () => handleSignInWithPassword,
68
+ handleSignOut: () => handleSignOut,
69
+ handleUpdateNavigation: () => handleUpdateNavigation,
70
+ handleUpdatePage: () => handleUpdatePage,
71
+ handleUploadMedia: () => handleUploadMedia,
72
+ handleVerifySession: () => handleVerifySession,
73
+ resolveMediaReferences: () => resolveMediaReferences
74
+ });
75
+ module.exports = __toCommonJS(index_exports);
76
+
77
+ // src/storage/supabase-adapter.ts
78
+ function mapPageRowToPage(row) {
79
+ return {
80
+ id: row.id,
81
+ slug: row.slug,
82
+ pageType: row.page_type,
83
+ title: row.title,
84
+ sections: row.sections,
85
+ createdAt: new Date(row.created_at),
86
+ updatedAt: new Date(row.updated_at)
87
+ };
88
+ }
89
+ function mapNavigationRowToNavigation(row) {
90
+ return {
91
+ id: row.id,
92
+ name: row.name,
93
+ items: row.items,
94
+ updatedAt: new Date(row.updated_at)
95
+ };
96
+ }
97
+ var StorageError = class extends Error {
98
+ constructor(message, code, details) {
99
+ super(message);
100
+ this.code = code;
101
+ this.details = details;
102
+ this.name = "StorageError";
103
+ }
104
+ };
105
+ var SupabaseStorageAdapter = class {
106
+ client;
107
+ constructor(config) {
108
+ this.client = config.client;
109
+ }
110
+ async getPage(slug) {
111
+ const { data, error } = await this.client.from("pages").select("*").eq("slug", slug).single();
112
+ if (error) {
113
+ if (error.code === "PGRST116") {
114
+ return null;
115
+ }
116
+ throw new StorageError(error.message, error.code, error.details);
117
+ }
118
+ return mapPageRowToPage(data);
119
+ }
120
+ async getPageById(id) {
121
+ const { data, error } = await this.client.from("pages").select("*").eq("id", id).single();
122
+ if (error) {
123
+ if (error.code === "PGRST116") {
124
+ return null;
125
+ }
126
+ throw new StorageError(error.message, error.code, error.details);
127
+ }
128
+ return mapPageRowToPage(data);
129
+ }
130
+ async createPage(input) {
131
+ const { data, error } = await this.client.from("pages").insert({
132
+ slug: input.slug,
133
+ page_type: input.pageType,
134
+ title: input.title,
135
+ sections: input.sections ?? []
136
+ }).select().single();
137
+ if (error) {
138
+ throw new StorageError(error.message, error.code, error.details);
139
+ }
140
+ return mapPageRowToPage(data);
141
+ }
142
+ async updatePage(input) {
143
+ const updateData = {};
144
+ if (input.slug !== void 0) {
145
+ updateData.slug = input.slug;
146
+ }
147
+ if (input.pageType !== void 0) {
148
+ updateData.page_type = input.pageType;
149
+ }
150
+ if (input.title !== void 0) {
151
+ updateData.title = input.title;
152
+ }
153
+ if (input.sections !== void 0) {
154
+ updateData.sections = input.sections;
155
+ }
156
+ const { data, error } = await this.client.from("pages").update(updateData).eq("id", input.id).select().single();
157
+ if (error) {
158
+ if (error.code === "PGRST116") {
159
+ throw new StorageError(`Page not found: ${input.id}`, "NOT_FOUND");
160
+ }
161
+ throw new StorageError(error.message, error.code, error.details);
162
+ }
163
+ return mapPageRowToPage(data);
164
+ }
165
+ async deletePage(id) {
166
+ const { error } = await this.client.from("pages").delete().eq("id", id);
167
+ if (error) {
168
+ throw new StorageError(error.message, error.code, error.details);
169
+ }
170
+ }
171
+ async listPages(filter) {
172
+ let query = this.client.from("pages").select("*");
173
+ if (filter?.pageType) {
174
+ query = query.eq("page_type", filter.pageType);
175
+ }
176
+ query = query.order("created_at", { ascending: false });
177
+ if (filter?.limit) {
178
+ query = query.limit(filter.limit);
179
+ }
180
+ if (filter?.offset) {
181
+ query = query.range(filter.offset, filter.offset + (filter.limit ?? 100) - 1);
182
+ }
183
+ const { data, error } = await query;
184
+ if (error) {
185
+ throw new StorageError(error.message, error.code, error.details);
186
+ }
187
+ return data.map(mapPageRowToPage);
188
+ }
189
+ async getNavigation(name) {
190
+ const { data, error } = await this.client.from("navigation").select("*").eq("name", name).single();
191
+ if (error) {
192
+ if (error.code === "PGRST116") {
193
+ return null;
194
+ }
195
+ throw new StorageError(error.message, error.code, error.details);
196
+ }
197
+ return mapNavigationRowToNavigation(data);
198
+ }
199
+ async getNavigationById(id) {
200
+ const { data, error } = await this.client.from("navigation").select("*").eq("id", id).single();
201
+ if (error) {
202
+ if (error.code === "PGRST116") {
203
+ return null;
204
+ }
205
+ throw new StorageError(error.message, error.code, error.details);
206
+ }
207
+ return mapNavigationRowToNavigation(data);
208
+ }
209
+ async createNavigation(input) {
210
+ const { data, error } = await this.client.from("navigation").insert({
211
+ name: input.name,
212
+ items: input.items
213
+ }).select().single();
214
+ if (error) {
215
+ if (error.code === "23505") {
216
+ throw new StorageError(
217
+ `Navigation with name "${input.name}" already exists`,
218
+ "DUPLICATE_NAME"
219
+ );
220
+ }
221
+ throw new StorageError(error.message, error.code, error.details);
222
+ }
223
+ return mapNavigationRowToNavigation(data);
224
+ }
225
+ async updateNavigation(input) {
226
+ const updateData = {};
227
+ if (input.name !== void 0) {
228
+ updateData.name = input.name;
229
+ }
230
+ if (input.items !== void 0) {
231
+ updateData.items = input.items;
232
+ }
233
+ const { data, error } = await this.client.from("navigation").update(updateData).eq("id", input.id).select().single();
234
+ if (error) {
235
+ if (error.code === "PGRST116") {
236
+ throw new StorageError(`Navigation not found: ${input.id}`, "NOT_FOUND");
237
+ }
238
+ throw new StorageError(error.message, error.code, error.details);
239
+ }
240
+ return mapNavigationRowToNavigation(data);
241
+ }
242
+ async deleteNavigation(id) {
243
+ const { error } = await this.client.from("navigation").delete().eq("id", id);
244
+ if (error) {
245
+ throw new StorageError(error.message, error.code, error.details);
246
+ }
247
+ }
248
+ async listNavigations() {
249
+ const { data, error } = await this.client.from("navigation").select("*").order("name", { ascending: true });
250
+ if (error) {
251
+ throw new StorageError(error.message, error.code, error.details);
252
+ }
253
+ return data.map(mapNavigationRowToNavigation);
254
+ }
255
+ };
256
+ function createStorageAdapter(config) {
257
+ return new SupabaseStorageAdapter(config);
258
+ }
259
+
260
+ // src/utils/sanitize.ts
261
+ var import_sanitize_html = __toESM(require("sanitize-html"), 1);
262
+ var ALLOWED_TAGS = [
263
+ "p",
264
+ "h1",
265
+ "h2",
266
+ "h3",
267
+ "h4",
268
+ "h5",
269
+ "h6",
270
+ "ul",
271
+ "ol",
272
+ "li",
273
+ "a",
274
+ "strong",
275
+ "em",
276
+ "br",
277
+ "blockquote",
278
+ "code",
279
+ "pre",
280
+ "img"
281
+ ];
282
+ var ALLOWED_ATTRIBUTES = {
283
+ a: ["href"],
284
+ img: ["src", "alt"]
285
+ };
286
+ var SANITIZE_OPTIONS = {
287
+ allowedTags: ALLOWED_TAGS,
288
+ allowedAttributes: ALLOWED_ATTRIBUTES,
289
+ disallowedTagsMode: "discard"
290
+ };
291
+ function sanitizeString(value) {
292
+ return (0, import_sanitize_html.default)(value, SANITIZE_OPTIONS);
293
+ }
294
+ function sanitizeValue(value) {
295
+ if (typeof value === "string") {
296
+ return sanitizeString(value);
297
+ }
298
+ if (Array.isArray(value)) {
299
+ return value.map(sanitizeValue);
300
+ }
301
+ if (value !== null && typeof value === "object") {
302
+ const result = {};
303
+ for (const [key, val] of Object.entries(value)) {
304
+ result[key] = sanitizeValue(val);
305
+ }
306
+ return result;
307
+ }
308
+ return value;
309
+ }
310
+ function sanitizeSectionData(sections) {
311
+ return sections.map((section) => ({
312
+ ...section,
313
+ data: sanitizeValue(section.data)
314
+ }));
315
+ }
316
+
317
+ // src/utils/slug.ts
318
+ var UMLAUT_MAP = {
319
+ \u00E4: "ae",
320
+ \u00F6: "oe",
321
+ \u00FC: "ue",
322
+ \u00C4: "Ae",
323
+ \u00D6: "Oe",
324
+ \u00DC: "Ue",
325
+ \u00DF: "ss"
326
+ };
327
+ function generateSlug(title) {
328
+ let slug = title.toLowerCase().trim();
329
+ for (const [umlaut, replacement] of Object.entries(UMLAUT_MAP)) {
330
+ slug = slug.replace(new RegExp(umlaut, "g"), replacement.toLowerCase());
331
+ }
332
+ slug = slug.replace(/[\s_]+/g, "-");
333
+ slug = slug.replace(/[^a-z0-9-]/g, "");
334
+ slug = slug.replace(/-+/g, "-");
335
+ slug = slug.replace(/^-+|-+$/g, "");
336
+ return slug;
337
+ }
338
+ function ensureUniqueSlug(slug, existingSlugs) {
339
+ if (!existingSlugs.includes(slug)) {
340
+ return slug;
341
+ }
342
+ let counter = 1;
343
+ let uniqueSlug = `${slug}-${counter}`;
344
+ while (existingSlugs.includes(uniqueSlug)) {
345
+ counter++;
346
+ uniqueSlug = `${slug}-${counter}`;
347
+ }
348
+ return uniqueSlug;
349
+ }
350
+
351
+ // src/storage/handlers.ts
352
+ var StorageValidationError = class extends Error {
353
+ constructor(message, code) {
354
+ super(message);
355
+ this.code = code;
356
+ this.name = "StorageValidationError";
357
+ }
358
+ };
359
+ async function handleCreatePage(adapter, input) {
360
+ if (!input.title.trim()) {
361
+ throw new StorageValidationError("Page title must not be empty", "EMPTY_TITLE");
362
+ }
363
+ const slug = input.slug?.trim() || generateSlug(input.title);
364
+ if (!slug) {
365
+ throw new StorageValidationError(
366
+ "Could not generate a valid slug from the provided title",
367
+ "INVALID_SLUG"
368
+ );
369
+ }
370
+ const existingPages = await adapter.listPages();
371
+ const existingSlugs = existingPages.map((p) => p.slug);
372
+ const uniqueSlug = ensureUniqueSlug(slug, existingSlugs);
373
+ const sanitizedSections = input.sections ? sanitizeSectionData(input.sections) : void 0;
374
+ return adapter.createPage({
375
+ ...input,
376
+ slug: uniqueSlug,
377
+ sections: sanitizedSections
378
+ });
379
+ }
380
+ async function handleUpdatePage(adapter, input) {
381
+ if (!input.id.trim()) {
382
+ throw new StorageValidationError("Page ID must not be empty", "EMPTY_ID");
383
+ }
384
+ if (input.title !== void 0 && !input.title.trim()) {
385
+ throw new StorageValidationError("Page title must not be empty", "EMPTY_TITLE");
386
+ }
387
+ if (input.slug !== void 0) {
388
+ const slug = input.slug.trim();
389
+ if (!slug) {
390
+ throw new StorageValidationError("Page slug must not be empty", "EMPTY_SLUG");
391
+ }
392
+ const existingPages = await adapter.listPages();
393
+ const existingSlugs = existingPages.filter((p) => p.id !== input.id).map((p) => p.slug);
394
+ if (existingSlugs.includes(slug)) {
395
+ throw new StorageValidationError(`Slug "${slug}" is already in use`, "DUPLICATE_SLUG");
396
+ }
397
+ }
398
+ const sanitizedInput = input.sections ? { ...input, sections: sanitizeSectionData(input.sections) } : input;
399
+ return adapter.updatePage(sanitizedInput);
400
+ }
401
+ async function handleDeletePage(adapter, id) {
402
+ if (!id.trim()) {
403
+ throw new StorageValidationError("Page ID must not be empty", "EMPTY_ID");
404
+ }
405
+ return adapter.deletePage(id);
406
+ }
407
+ async function handleCreateNavigation(adapter, input) {
408
+ if (!input.name.trim()) {
409
+ throw new StorageValidationError("Navigation name must not be empty", "EMPTY_NAME");
410
+ }
411
+ const existingNavigations = await adapter.listNavigations();
412
+ const existingNames = existingNavigations.map((n) => n.name);
413
+ if (existingNames.includes(input.name.trim())) {
414
+ throw new StorageValidationError(
415
+ `Navigation name "${input.name.trim()}" is already in use`,
416
+ "DUPLICATE_NAME"
417
+ );
418
+ }
419
+ return adapter.createNavigation(input);
420
+ }
421
+ async function handleUpdateNavigation(adapter, input) {
422
+ if (!input.id.trim()) {
423
+ throw new StorageValidationError("Navigation ID must not be empty", "EMPTY_ID");
424
+ }
425
+ if (input.name !== void 0) {
426
+ const name = input.name.trim();
427
+ if (!name) {
428
+ throw new StorageValidationError("Navigation name must not be empty", "EMPTY_NAME");
429
+ }
430
+ const existingNavigations = await adapter.listNavigations();
431
+ const existingNames = existingNavigations.filter((n) => n.id !== input.id).map((n) => n.name);
432
+ if (existingNames.includes(name)) {
433
+ throw new StorageValidationError(
434
+ `Navigation name "${name}" is already in use`,
435
+ "DUPLICATE_NAME"
436
+ );
437
+ }
438
+ }
439
+ return adapter.updateNavigation(input);
440
+ }
441
+ async function handleDeleteNavigation(adapter, id) {
442
+ if (!id.trim()) {
443
+ throw new StorageValidationError("Navigation ID must not be empty", "EMPTY_ID");
444
+ }
445
+ return adapter.deleteNavigation(id);
446
+ }
447
+
448
+ // src/media/resolve.ts
449
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
450
+ var MEDIA_FIELD_SUFFIXES = ["_image", "_media", "_photo", "_thumbnail", "_avatar", "_icon"];
451
+ function isMediaField(fieldName) {
452
+ const lower = fieldName.toLowerCase();
453
+ if (lower === "image" || lower === "media" || lower === "photo" || lower === "thumbnail" || lower === "avatar" || lower === "icon") {
454
+ return true;
455
+ }
456
+ return MEDIA_FIELD_SUFFIXES.some((suffix) => lower.endsWith(suffix));
457
+ }
458
+ function isMediaId(value) {
459
+ return typeof value === "string" && UUID_PATTERN.test(value);
460
+ }
461
+ async function resolveDataObject(data, adapter) {
462
+ const resolved = {};
463
+ for (const [key, value] of Object.entries(data)) {
464
+ if (isMediaField(key) && isMediaId(value)) {
465
+ const media = await adapter.getMedia(value);
466
+ resolved[key] = media ? media.url : null;
467
+ } else if (value !== null && typeof value === "object" && !Array.isArray(value)) {
468
+ resolved[key] = await resolveDataObject(value, adapter);
469
+ } else {
470
+ resolved[key] = value;
471
+ }
472
+ }
473
+ return resolved;
474
+ }
475
+ async function resolveMediaReferences(sections, adapter) {
476
+ const resolved = [];
477
+ for (const section of sections) {
478
+ const resolvedData = await resolveDataObject(section.data, adapter);
479
+ resolved.push({
480
+ id: section.id,
481
+ type: section.type,
482
+ data: resolvedData
483
+ });
484
+ }
485
+ return resolved;
486
+ }
487
+
488
+ // src/delivery/handlers.ts
489
+ async function toPageResponse(page, mediaAdapter) {
490
+ const resolvedSections = await resolveMediaReferences(page.sections, mediaAdapter);
491
+ return {
492
+ id: page.id,
493
+ slug: page.slug,
494
+ pageType: page.pageType,
495
+ title: page.title,
496
+ sections: resolvedSections,
497
+ meta: {
498
+ createdAt: page.createdAt.toISOString(),
499
+ updatedAt: page.updatedAt.toISOString()
500
+ }
501
+ };
502
+ }
503
+ function toNavigationResponse(navigation) {
504
+ return {
505
+ id: navigation.id,
506
+ name: navigation.name,
507
+ items: navigation.items,
508
+ meta: {
509
+ updatedAt: navigation.updatedAt.toISOString()
510
+ }
511
+ };
512
+ }
513
+ async function handleListPages(adapter, mediaAdapter, options) {
514
+ const pages = await adapter.listPages({
515
+ pageType: options?.pageType,
516
+ limit: options?.limit,
517
+ offset: options?.offset
518
+ });
519
+ const results = [];
520
+ for (const page of pages) {
521
+ results.push(await toPageResponse(page, mediaAdapter));
522
+ }
523
+ return results;
524
+ }
525
+ async function handleGetPageBySlug(adapter, mediaAdapter, slug) {
526
+ const page = await adapter.getPage(slug);
527
+ if (!page) {
528
+ return null;
529
+ }
530
+ return toPageResponse(page, mediaAdapter);
531
+ }
532
+ async function handleGetNavigation(adapter, name) {
533
+ const navigation = await adapter.getNavigation(name);
534
+ if (!navigation) {
535
+ return null;
536
+ }
537
+ return toNavigationResponse(navigation);
538
+ }
539
+
540
+ // src/media/types.ts
541
+ var ALLOWED_MIME_TYPES = [
542
+ "image/jpeg",
543
+ "image/png",
544
+ "image/gif",
545
+ "image/webp",
546
+ "image/svg+xml"
547
+ ];
548
+
549
+ // src/media/supabase-adapter.ts
550
+ var MediaError = class extends Error {
551
+ constructor(message, code, details) {
552
+ super(message);
553
+ this.code = code;
554
+ this.details = details;
555
+ this.name = "MediaError";
556
+ }
557
+ };
558
+ var SupabaseMediaAdapter = class {
559
+ client;
560
+ bucketName;
561
+ constructor(config) {
562
+ this.client = config.client;
563
+ this.bucketName = config.bucketName ?? "media";
564
+ }
565
+ /**
566
+ * Generates a unique storage path for a file
567
+ */
568
+ generateStoragePath(filename) {
569
+ const timestamp = Date.now();
570
+ const randomSuffix = Math.random().toString(36).substring(2, 8);
571
+ const sanitizedFilename = filename.replace(/[^a-zA-Z0-9.-]/g, "_");
572
+ return `${timestamp}-${randomSuffix}-${sanitizedFilename}`;
573
+ }
574
+ /**
575
+ * Constructs the public URL for a stored file
576
+ */
577
+ getPublicUrl(storagePath) {
578
+ const { data } = this.client.storage.from(this.bucketName).getPublicUrl(storagePath);
579
+ return data.publicUrl;
580
+ }
581
+ /**
582
+ * Maps a database row to MediaFile
583
+ */
584
+ mapRowToMediaFile(row) {
585
+ return {
586
+ id: row.id,
587
+ filename: row.filename,
588
+ url: this.getPublicUrl(row.storage_path),
589
+ mimeType: row.mime_type,
590
+ size: row.size,
591
+ createdAt: new Date(row.created_at)
592
+ };
593
+ }
594
+ async upload(input) {
595
+ const storagePath = this.generateStoragePath(input.filename);
596
+ const { error: uploadError } = await this.client.storage.from(this.bucketName).upload(storagePath, input.data, {
597
+ contentType: input.mimeType,
598
+ upsert: false
599
+ });
600
+ if (uploadError) {
601
+ throw new MediaError(
602
+ `Failed to upload file: ${uploadError.message}`,
603
+ "UPLOAD_FAILED",
604
+ uploadError.message
605
+ );
606
+ }
607
+ const { data, error: dbError } = await this.client.from("media").insert({
608
+ filename: input.filename,
609
+ storage_path: storagePath,
610
+ mime_type: input.mimeType,
611
+ size: input.size
612
+ }).select().single();
613
+ if (dbError) {
614
+ await this.client.storage.from(this.bucketName).remove([storagePath]);
615
+ throw new MediaError(
616
+ `Failed to create media record: ${dbError.message}`,
617
+ dbError.code,
618
+ dbError.details
619
+ );
620
+ }
621
+ return this.mapRowToMediaFile(data);
622
+ }
623
+ async getMedia(id) {
624
+ const { data, error } = await this.client.from("media").select("*").eq("id", id).single();
625
+ if (error) {
626
+ if (error.code === "PGRST116") {
627
+ return null;
628
+ }
629
+ throw new MediaError(error.message, error.code, error.details);
630
+ }
631
+ return this.mapRowToMediaFile(data);
632
+ }
633
+ async listMedia(filter) {
634
+ let query = this.client.from("media").select("*");
635
+ if (filter?.mimeType) {
636
+ query = query.eq("mime_type", filter.mimeType);
637
+ }
638
+ query = query.order("created_at", { ascending: false });
639
+ if (filter?.limit) {
640
+ query = query.limit(filter.limit);
641
+ }
642
+ if (filter?.offset) {
643
+ query = query.range(filter.offset, filter.offset + (filter.limit ?? 100) - 1);
644
+ }
645
+ const { data, error } = await query;
646
+ if (error) {
647
+ throw new MediaError(error.message, error.code, error.details);
648
+ }
649
+ return data.map((row) => this.mapRowToMediaFile(row));
650
+ }
651
+ async deleteMedia(id) {
652
+ const { data: mediaRecord, error: fetchError } = await this.client.from("media").select("storage_path").eq("id", id).single();
653
+ if (fetchError) {
654
+ if (fetchError.code === "PGRST116") {
655
+ throw new MediaError(`Media not found: ${id}`, "NOT_FOUND");
656
+ }
657
+ throw new MediaError(fetchError.message, fetchError.code, fetchError.details);
658
+ }
659
+ const storagePath = mediaRecord.storage_path;
660
+ const { error: storageError } = await this.client.storage.from(this.bucketName).remove([storagePath]);
661
+ if (storageError) {
662
+ throw new MediaError(
663
+ `Failed to delete file from storage: ${storageError.message}`,
664
+ "STORAGE_DELETE_FAILED",
665
+ storageError.message
666
+ );
667
+ }
668
+ const { error: dbError } = await this.client.from("media").delete().eq("id", id);
669
+ if (dbError) {
670
+ throw new MediaError(dbError.message, dbError.code, dbError.details);
671
+ }
672
+ }
673
+ };
674
+ function createMediaAdapter(config) {
675
+ return new SupabaseMediaAdapter(config);
676
+ }
677
+
678
+ // src/media/handlers.ts
679
+ var MediaValidationError = class extends Error {
680
+ constructor(message, code) {
681
+ super(message);
682
+ this.code = code;
683
+ this.name = "MediaValidationError";
684
+ }
685
+ };
686
+ function validateMimeType(mimeType) {
687
+ const allowed = ALLOWED_MIME_TYPES;
688
+ if (!allowed.includes(mimeType)) {
689
+ throw new MediaValidationError(
690
+ `Invalid file type: ${mimeType}. Allowed types: ${ALLOWED_MIME_TYPES.join(", ")}`,
691
+ "INVALID_MIME_TYPE"
692
+ );
693
+ }
694
+ }
695
+ async function handleUploadMedia(adapter, input) {
696
+ validateMimeType(input.mimeType);
697
+ return adapter.upload(input);
698
+ }
699
+ async function handleGetMedia(adapter, id) {
700
+ return adapter.getMedia(id);
701
+ }
702
+ async function handleListMedia(adapter, filter) {
703
+ return adapter.listMedia(filter);
704
+ }
705
+ async function handleDeleteMedia(adapter, id) {
706
+ return adapter.deleteMedia(id);
707
+ }
708
+
709
+ // src/supabase/factory.ts
710
+ var import_supabase_js = require("@supabase/supabase-js");
711
+
712
+ // src/auth/supabase-adapter.ts
713
+ var AuthError = class extends Error {
714
+ constructor(message, code, details) {
715
+ super(message);
716
+ this.code = code;
717
+ this.details = details;
718
+ this.name = "AuthError";
719
+ }
720
+ };
721
+ var SupabaseAuthAdapter = class {
722
+ client;
723
+ constructor(config) {
724
+ this.client = config.client;
725
+ }
726
+ mapSupabaseUser(user) {
727
+ if (!user.email) {
728
+ throw new AuthError("User email is required", "INVALID_USER");
729
+ }
730
+ return {
731
+ id: user.id,
732
+ email: user.email,
733
+ metadata: user.user_metadata
734
+ };
735
+ }
736
+ async signInWithOAuth(input) {
737
+ const { data, error } = await this.client.auth.signInWithOAuth({
738
+ provider: input.provider,
739
+ options: {
740
+ redirectTo: input.redirectTo
741
+ }
742
+ });
743
+ if (error) {
744
+ throw new AuthError(error.message, error.status?.toString(), error.message);
745
+ }
746
+ if (!data.url) {
747
+ throw new AuthError("OAuth URL not provided", "OAUTH_URL_MISSING");
748
+ }
749
+ return {
750
+ url: data.url,
751
+ provider: input.provider
752
+ };
753
+ }
754
+ async signInWithPassword(input) {
755
+ const { data, error } = await this.client.auth.signInWithPassword({
756
+ email: input.email,
757
+ password: input.password
758
+ });
759
+ if (error) {
760
+ throw new AuthError(error.message, error.status?.toString(), error.message);
761
+ }
762
+ if (!data.session || !data.user) {
763
+ throw new AuthError("Session or user not returned", "AUTH_FAILED");
764
+ }
765
+ return {
766
+ accessToken: data.session.access_token,
767
+ refreshToken: data.session.refresh_token,
768
+ expiresAt: data.session.expires_at ? new Date(data.session.expires_at * 1e3) : void 0,
769
+ user: this.mapSupabaseUser(data.user)
770
+ };
771
+ }
772
+ async signOut(_accessToken) {
773
+ const { error } = await this.client.auth.signOut();
774
+ if (error) {
775
+ throw new AuthError(error.message, error.status?.toString(), error.message);
776
+ }
777
+ }
778
+ async verifySession(input) {
779
+ const { data, error } = await this.client.auth.getUser(input.accessToken);
780
+ if (error) {
781
+ return null;
782
+ }
783
+ if (!data.user) {
784
+ return null;
785
+ }
786
+ return this.mapSupabaseUser(data.user);
787
+ }
788
+ async refreshSession(refreshToken) {
789
+ const { data, error } = await this.client.auth.refreshSession({
790
+ refresh_token: refreshToken
791
+ });
792
+ if (error) {
793
+ throw new AuthError(error.message, error.status?.toString(), error.message);
794
+ }
795
+ if (!data.session || !data.user) {
796
+ throw new AuthError("Session refresh failed", "REFRESH_FAILED");
797
+ }
798
+ return {
799
+ accessToken: data.session.access_token,
800
+ refreshToken: data.session.refresh_token,
801
+ expiresAt: data.session.expires_at ? new Date(data.session.expires_at * 1e3) : void 0,
802
+ user: this.mapSupabaseUser(data.user)
803
+ };
804
+ }
805
+ async getCurrentUser(accessToken) {
806
+ const { data, error } = await this.client.auth.getUser(accessToken);
807
+ if (error) {
808
+ return null;
809
+ }
810
+ if (!data.user) {
811
+ return null;
812
+ }
813
+ return this.mapSupabaseUser(data.user);
814
+ }
815
+ };
816
+ function createAuthAdapter(config) {
817
+ return new SupabaseAuthAdapter(config);
818
+ }
819
+
820
+ // src/auth/handlers.ts
821
+ var AuthValidationError = class extends Error {
822
+ constructor(message) {
823
+ super(message);
824
+ this.name = "AuthValidationError";
825
+ }
826
+ };
827
+ async function handleSignInWithOAuth(adapter, input) {
828
+ if (!input.provider) {
829
+ throw new AuthValidationError("Provider is required");
830
+ }
831
+ const validProviders = ["google", "github", "gitlab", "azure", "bitbucket"];
832
+ if (!validProviders.includes(input.provider)) {
833
+ throw new AuthValidationError(`Invalid provider. Must be one of: ${validProviders.join(", ")}`);
834
+ }
835
+ return await adapter.signInWithOAuth(input);
836
+ }
837
+ function validatePassword(password) {
838
+ if (password.length < 8) {
839
+ throw new AuthValidationError("Password must be at least 8 characters");
840
+ }
841
+ const hasUpperCase = /[A-Z]/.test(password);
842
+ const hasLowerCase = /[a-z]/.test(password);
843
+ const hasNumbers = /\d/.test(password);
844
+ const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>_\-+=\[\]\\/'`~]/.test(password);
845
+ const complexityCount = [hasUpperCase, hasLowerCase, hasNumbers, hasSpecialChar].filter(
846
+ Boolean
847
+ ).length;
848
+ if (complexityCount < 3) {
849
+ throw new AuthValidationError(
850
+ "Password must contain at least 3 of: uppercase letters, lowercase letters, numbers, special characters"
851
+ );
852
+ }
853
+ const commonPasswords = [
854
+ "password",
855
+ "password1",
856
+ "password123",
857
+ "12345678",
858
+ "123456789",
859
+ "qwerty",
860
+ "abc123",
861
+ "monkey",
862
+ "1234567890",
863
+ "letmein",
864
+ "trustno1",
865
+ "dragon",
866
+ "baseball",
867
+ "iloveyou",
868
+ "master",
869
+ "sunshine",
870
+ "ashley",
871
+ "bailey",
872
+ "shadow",
873
+ "123123"
874
+ ];
875
+ if (commonPasswords.includes(password.toLowerCase())) {
876
+ throw new AuthValidationError("Password is too common. Please choose a stronger password.");
877
+ }
878
+ }
879
+ async function handleSignInWithPassword(adapter, input) {
880
+ if (!input.email || !input.password) {
881
+ throw new AuthValidationError("Email and password are required");
882
+ }
883
+ if (!input.email.includes("@")) {
884
+ throw new AuthValidationError("Invalid email format");
885
+ }
886
+ validatePassword(input.password);
887
+ return await adapter.signInWithPassword(input);
888
+ }
889
+ async function handleSignOut(adapter, accessToken) {
890
+ if (!accessToken) {
891
+ throw new AuthValidationError("Access token is required");
892
+ }
893
+ return await adapter.signOut(accessToken);
894
+ }
895
+ async function handleVerifySession(adapter, input) {
896
+ if (!input.accessToken) {
897
+ throw new AuthValidationError("Access token is required");
898
+ }
899
+ if (input.expiresAt) {
900
+ const now = /* @__PURE__ */ new Date();
901
+ const expiresAt = input.expiresAt instanceof Date ? input.expiresAt : new Date(input.expiresAt);
902
+ if (now > expiresAt) {
903
+ throw new AuthValidationError("Token has expired");
904
+ }
905
+ }
906
+ const user = await adapter.verifySession(input);
907
+ if (!user) {
908
+ throw new AuthValidationError("Invalid or expired token");
909
+ }
910
+ return user;
911
+ }
912
+ async function handleRefreshSession(adapter, refreshToken) {
913
+ if (!refreshToken) {
914
+ throw new AuthValidationError("Refresh token is required");
915
+ }
916
+ return await adapter.refreshSession(refreshToken);
917
+ }
918
+ async function handleGetCurrentUser(adapter, accessToken) {
919
+ if (!accessToken) {
920
+ throw new AuthValidationError("Access token is required");
921
+ }
922
+ return await adapter.getCurrentUser(accessToken);
923
+ }
924
+
925
+ // src/auth/middleware.ts
926
+ function createAuthMiddleware(config) {
927
+ const extractToken = config.extractToken || ((headers) => {
928
+ const authHeader = headers.authorization || headers.Authorization;
929
+ if (!authHeader) return null;
930
+ const parts = authHeader.split(" ");
931
+ if (parts.length !== 2 || parts[0] !== "Bearer") return null;
932
+ return parts[1];
933
+ });
934
+ return async function authenticate(headers) {
935
+ const token = extractToken(headers);
936
+ if (!token) {
937
+ throw new Error("No authentication token provided");
938
+ }
939
+ const user = await config.adapter.verifySession({ accessToken: token });
940
+ if (!user) {
941
+ throw new Error("Invalid or expired token");
942
+ }
943
+ return {
944
+ user,
945
+ accessToken: token
946
+ };
947
+ };
948
+ }
949
+
950
+ // src/supabase/factory.ts
951
+ function readEnv(name) {
952
+ const processLike = globalThis.process;
953
+ return processLike?.env[name];
954
+ }
955
+ function resolveRequiredConfig(explicitValue, envName) {
956
+ const explicit = explicitValue?.trim();
957
+ if (explicit) {
958
+ return explicit;
959
+ }
960
+ const fromEnv = readEnv(envName)?.trim();
961
+ if (fromEnv) {
962
+ return fromEnv;
963
+ }
964
+ throw new Error(
965
+ `Missing Supabase configuration: provide ${envName} in factory config or environment variable`
966
+ );
967
+ }
968
+ function resolveBucketName(config) {
969
+ const explicitBucket = config.storage?.bucket?.trim();
970
+ if (explicitBucket) {
971
+ return explicitBucket;
972
+ }
973
+ const envBucket = readEnv("SUPABASE_STORAGE_BUCKET")?.trim();
974
+ if (envBucket) {
975
+ return envBucket;
976
+ }
977
+ return "media";
978
+ }
979
+ function createSupabaseAdapters(config = {}) {
980
+ const url = resolveRequiredConfig(config.url, "SUPABASE_URL");
981
+ const key = resolveRequiredConfig(config.key, "SUPABASE_SECRET_KEY");
982
+ const bucketName = resolveBucketName(config);
983
+ const client = (0, import_supabase_js.createClient)(url, key);
984
+ return {
985
+ storageAdapter: createStorageAdapter({ client }),
986
+ mediaAdapter: createMediaAdapter({ client, bucketName }),
987
+ authAdapter: createAuthAdapter({ client })
988
+ };
989
+ }
990
+
991
+ // src/export/handlers.ts
992
+ function contentDisposition(filename) {
993
+ return `attachment; filename="${filename}"`;
994
+ }
995
+ async function toPageExport(page, mediaAdapter) {
996
+ const resolvedSections = await resolveMediaReferences(page.sections, mediaAdapter);
997
+ return {
998
+ id: page.id,
999
+ slug: page.slug,
1000
+ pageType: page.pageType,
1001
+ title: page.title,
1002
+ sections: resolvedSections,
1003
+ createdAt: page.createdAt.toISOString(),
1004
+ updatedAt: page.updatedAt.toISOString()
1005
+ };
1006
+ }
1007
+ async function handleExportPage(storageAdapter, mediaAdapter, slug) {
1008
+ const page = await storageAdapter.getPage(slug);
1009
+ if (!page) {
1010
+ return null;
1011
+ }
1012
+ const data = await toPageExport(page, mediaAdapter);
1013
+ return {
1014
+ data,
1015
+ contentDisposition: contentDisposition(`${page.slug}.json`)
1016
+ };
1017
+ }
1018
+ async function handleExportAllPages(storageAdapter, mediaAdapter) {
1019
+ const pages = await storageAdapter.listPages();
1020
+ const exportedPages = [];
1021
+ for (const page of pages) {
1022
+ exportedPages.push(await toPageExport(page, mediaAdapter));
1023
+ }
1024
+ const data = {
1025
+ pages: exportedPages,
1026
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString()
1027
+ };
1028
+ return {
1029
+ data,
1030
+ contentDisposition: contentDisposition("pages-export.json")
1031
+ };
1032
+ }
1033
+ function toNavigationExport(nav) {
1034
+ return {
1035
+ id: nav.id,
1036
+ name: nav.name,
1037
+ items: nav.items,
1038
+ updatedAt: nav.updatedAt.toISOString()
1039
+ };
1040
+ }
1041
+ async function handleExportNavigations(storageAdapter) {
1042
+ const navigations = await storageAdapter.listNavigations();
1043
+ const data = {
1044
+ navigations: navigations.map(toNavigationExport),
1045
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString()
1046
+ };
1047
+ return {
1048
+ data,
1049
+ contentDisposition: contentDisposition("navigation-export.json")
1050
+ };
1051
+ }
1052
+ async function handleExportSite(storageAdapter, mediaAdapter) {
1053
+ const pages = await storageAdapter.listPages();
1054
+ const exportedPages = [];
1055
+ for (const page of pages) {
1056
+ exportedPages.push(await toPageExport(page, mediaAdapter));
1057
+ }
1058
+ const navigations = await storageAdapter.listNavigations();
1059
+ const exportedNavigations = navigations.map(toNavigationExport);
1060
+ const mediaFiles = await mediaAdapter.listMedia();
1061
+ const exportedMedia = mediaFiles.map((file) => ({
1062
+ id: file.id,
1063
+ filename: file.filename,
1064
+ url: file.url,
1065
+ mimeType: file.mimeType,
1066
+ size: file.size,
1067
+ createdAt: file.createdAt.toISOString()
1068
+ }));
1069
+ const data = {
1070
+ pages: exportedPages,
1071
+ navigations: exportedNavigations,
1072
+ media: exportedMedia,
1073
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString()
1074
+ };
1075
+ return {
1076
+ data,
1077
+ contentDisposition: contentDisposition("site-export.json")
1078
+ };
1079
+ }
1080
+ // Annotate the CommonJS export names for ESM import in node:
1081
+ 0 && (module.exports = {
1082
+ ALLOWED_MIME_TYPES,
1083
+ AuthError,
1084
+ AuthValidationError,
1085
+ MediaError,
1086
+ MediaValidationError,
1087
+ StorageError,
1088
+ StorageValidationError,
1089
+ SupabaseAuthAdapter,
1090
+ SupabaseMediaAdapter,
1091
+ SupabaseStorageAdapter,
1092
+ createAuthAdapter,
1093
+ createAuthMiddleware,
1094
+ createMediaAdapter,
1095
+ createStorageAdapter,
1096
+ createSupabaseAdapters,
1097
+ ensureUniqueSlug,
1098
+ generateSlug,
1099
+ handleCreateNavigation,
1100
+ handleCreatePage,
1101
+ handleDeleteMedia,
1102
+ handleDeleteNavigation,
1103
+ handleDeletePage,
1104
+ handleExportAllPages,
1105
+ handleExportNavigations,
1106
+ handleExportPage,
1107
+ handleExportSite,
1108
+ handleGetCurrentUser,
1109
+ handleGetMedia,
1110
+ handleGetNavigation,
1111
+ handleGetPageBySlug,
1112
+ handleListMedia,
1113
+ handleListPages,
1114
+ handleRefreshSession,
1115
+ handleSignInWithOAuth,
1116
+ handleSignInWithPassword,
1117
+ handleSignOut,
1118
+ handleUpdateNavigation,
1119
+ handleUpdatePage,
1120
+ handleUploadMedia,
1121
+ handleVerifySession,
1122
+ resolveMediaReferences
1123
+ });
1124
+ //# sourceMappingURL=index.cjs.map