@distinctagency/cms-client 1.12.0 → 1.14.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.d.mts CHANGED
@@ -172,6 +172,7 @@ interface ContentQueryOptions {
172
172
  /** Member session token — if provided, checks access against membership tier */
173
173
  memberToken?: string;
174
174
  }
175
+ type ProductStatus = "draft" | "published" | "archived";
175
176
  interface Product {
176
177
  id: string;
177
178
  tenant_id: string;
@@ -179,14 +180,17 @@ interface Product {
179
180
  slug: string;
180
181
  description: string | null;
181
182
  short_description: string | null;
182
- status: "draft" | "published" | "archived";
183
+ status: ProductStatus;
183
184
  price_cents: number;
184
185
  compare_at_price_cents: number | null;
185
186
  sku: string | null;
186
187
  barcode: string | null;
187
188
  featured_image: string | null;
188
189
  gallery: string[];
190
+ /** Legacy free-text category. New code should prefer `category_id` joined to ProductCategory. */
189
191
  category: string | null;
192
+ /** FK to product_categories.id — null when uncategorised or only a legacy text category is set. */
193
+ category_id: string | null;
190
194
  tags: string[];
191
195
  weight_grams: number | null;
192
196
  is_digital: boolean;
@@ -202,6 +206,17 @@ interface Product {
202
206
  variants?: ProductVariant[];
203
207
  options?: ProductOption[];
204
208
  }
209
+ interface ProductCategory {
210
+ id: string;
211
+ tenant_id: string;
212
+ name: string;
213
+ slug: string;
214
+ description: string | null;
215
+ image_url: string | null;
216
+ sort_order: number;
217
+ created_at: string;
218
+ updated_at: string;
219
+ }
205
220
  interface ProductVariant {
206
221
  id: string;
207
222
  product_id: string;
@@ -231,6 +246,19 @@ interface OrderAddress {
231
246
  postcode: string;
232
247
  country: string;
233
248
  }
249
+ type OrderStatus = "pending" | "paid" | "processing" | "shipped" | "delivered" | "cancelled" | "refunded";
250
+ type PaymentStatus = "pending" | "succeeded" | "failed" | "refunded" | "partially_refunded";
251
+ interface OrderLineItem {
252
+ product_id: string;
253
+ variant_id: string | null;
254
+ name: string;
255
+ sku: string | null;
256
+ quantity: number;
257
+ unit_price_cents: number;
258
+ total_cents: number;
259
+ image_url: string | null;
260
+ }
261
+ type DiscountType = "percentage" | "fixed_amount";
234
262
  interface CreateOrderParams {
235
263
  customer_email: string;
236
264
  customer_name?: string;
@@ -248,11 +276,13 @@ interface CreateOrderParams {
248
276
  interface CreateOrderResult {
249
277
  order_id: string;
250
278
  order_number: string;
251
- client_secret: string;
279
+ /** Stripe PaymentIntent client_secret to confirm on the client. */
280
+ client_secret: string | null;
252
281
  total_cents: number;
253
282
  currency: string;
254
283
  }
255
284
  interface ProductQueryOptions {
285
+ /** Category slug (e.g. "electronics"). Matches the FK category, falling back to the legacy text column. */
256
286
  category?: string;
257
287
  tags?: string[];
258
288
  limit?: number;
@@ -435,13 +465,55 @@ interface TrackingConfig {
435
465
  */
436
466
  declare function getEmbedHtml(value: unknown): string;
437
467
 
438
- declare function createShopClient(supabase: SupabaseClient, options: {
468
+ interface ShopClientOptions {
469
+ /** Tenant API key (UUID). Sent as `x-cms-api-key` on every request. */
439
470
  apiKey: string;
471
+ /** CMS app base URL. Defaults to `https://cms.distinctstudio.co.nz`. */
440
472
  appUrl?: string;
441
- }): {
473
+ }
474
+ /**
475
+ * Creates a typed client for the eCommerce REST API (products, categories, orders).
476
+ *
477
+ * Unlike `createCmsClient`, the shop client talks to the CMS REST endpoints
478
+ * (`/api/products`, `/api/product-categories`, `/api/orders/create`) rather than
479
+ * Supabase directly — it doesn't need the `supabase` argument, but accepts it
480
+ * for API symmetry with the rest of the SDK.
481
+ *
482
+ * Usage:
483
+ * ```ts
484
+ * const shop = createShopClient(supabase, { apiKey: process.env.CMS_API_KEY! })
485
+ * const products = await shop.getProducts({ category: "electronics", limit: 20 })
486
+ * ```
487
+ */
488
+ declare function createShopClient(_supabase: SupabaseClient, options: ShopClientOptions): {
489
+ /**
490
+ * List published products. Includes nested `variants` and `options`.
491
+ *
492
+ * `category` filters by category slug (matches the FK `category_id`,
493
+ * with a fallback to the legacy `category` text column).
494
+ */
442
495
  getProducts(queryOptions?: ProductQueryOptions): Promise<Product[]>;
496
+ /**
497
+ * Get a single published product by slug. Returns null if not found.
498
+ * Includes nested `variants` and `options`.
499
+ */
443
500
  getProductBySlug(slug: string): Promise<Product | null>;
444
- getProductCategories(): Promise<string[]>;
501
+ /**
502
+ * List product categories for the tenant, ordered by sort_order then name.
503
+ */
504
+ getCategories(): Promise<ProductCategory[]>;
505
+ /**
506
+ * Get a single category by slug. Returns null if not found.
507
+ */
508
+ getCategoryBySlug(slug: string): Promise<ProductCategory | null>;
509
+ /**
510
+ * Create a pending order and return a Stripe PaymentIntent client_secret
511
+ * to confirm payment client-side.
512
+ *
513
+ * Stripe must be configured for the tenant; eCommerce must be enabled on
514
+ * their billing plan. Validates stock for tracked variants and applies
515
+ * any discount code before creating the PaymentIntent.
516
+ */
445
517
  createOrder(params: CreateOrderParams): Promise<CreateOrderResult>;
446
518
  };
447
519
 
@@ -603,4 +675,4 @@ declare const IMAGE_PRESETS: {
603
675
  };
604
676
  };
605
677
 
606
- export { type BookingStatus, type CmsClientOptions, type CmsEvent, type CmsTicketTier, type ContentItem, type ContentQueryOptions, type ContentType, type ContentTypeSeoConfig, type CreateBookingParams, type CreateBookingResult, type CreateOrderParams, type CreateOrderResult, type EmbedValue, type EventQueryOptions, type EventStatus, type EventWithTiers, type FieldDefinition, type FieldType, type FlipbookPagePublic, type FlipbookPublic, type FlipbookTocEntryPublic, type GoogleReview, IMAGE_PRESETS, type ImageConfig, type ImageTransformOptions, type MediaFolder, type MediaItem, type Member, type MembershipTier, type OrderAddress, type Product, type ProductOption, type ProductQueryOptions, type ProductVariant, type Profile, type ReviewQueryOptions, TRACKING_CONFIG_TAG, type Tenant, type TenantMembership, type TrackingConfig, type TrackingConfigOptions, createCmsClient, createEventsClient, createShopClient, getEmbedHtml, getSrcSet, getTransformUrl };
678
+ export { type BookingStatus, type CmsClientOptions, type CmsEvent, type CmsTicketTier, type ContentItem, type ContentQueryOptions, type ContentType, type ContentTypeSeoConfig, type CreateBookingParams, type CreateBookingResult, type CreateOrderParams, type CreateOrderResult, type DiscountType, type EmbedValue, type EventQueryOptions, type EventStatus, type EventWithTiers, type FieldDefinition, type FieldType, type FlipbookPagePublic, type FlipbookPublic, type FlipbookTocEntryPublic, type GoogleReview, IMAGE_PRESETS, type ImageConfig, type ImageTransformOptions, type MediaFolder, type MediaItem, type Member, type MembershipTier, type OrderAddress, type OrderLineItem, type OrderStatus, type PaymentStatus, type Product, type ProductCategory, type ProductOption, type ProductQueryOptions, type ProductStatus, type ProductVariant, type Profile, type ReviewQueryOptions, TRACKING_CONFIG_TAG, type Tenant, type TenantMembership, type TrackingConfig, type TrackingConfigOptions, createCmsClient, createEventsClient, createShopClient, getEmbedHtml, getSrcSet, getTransformUrl };
package/dist/index.d.ts CHANGED
@@ -172,6 +172,7 @@ interface ContentQueryOptions {
172
172
  /** Member session token — if provided, checks access against membership tier */
173
173
  memberToken?: string;
174
174
  }
175
+ type ProductStatus = "draft" | "published" | "archived";
175
176
  interface Product {
176
177
  id: string;
177
178
  tenant_id: string;
@@ -179,14 +180,17 @@ interface Product {
179
180
  slug: string;
180
181
  description: string | null;
181
182
  short_description: string | null;
182
- status: "draft" | "published" | "archived";
183
+ status: ProductStatus;
183
184
  price_cents: number;
184
185
  compare_at_price_cents: number | null;
185
186
  sku: string | null;
186
187
  barcode: string | null;
187
188
  featured_image: string | null;
188
189
  gallery: string[];
190
+ /** Legacy free-text category. New code should prefer `category_id` joined to ProductCategory. */
189
191
  category: string | null;
192
+ /** FK to product_categories.id — null when uncategorised or only a legacy text category is set. */
193
+ category_id: string | null;
190
194
  tags: string[];
191
195
  weight_grams: number | null;
192
196
  is_digital: boolean;
@@ -202,6 +206,17 @@ interface Product {
202
206
  variants?: ProductVariant[];
203
207
  options?: ProductOption[];
204
208
  }
209
+ interface ProductCategory {
210
+ id: string;
211
+ tenant_id: string;
212
+ name: string;
213
+ slug: string;
214
+ description: string | null;
215
+ image_url: string | null;
216
+ sort_order: number;
217
+ created_at: string;
218
+ updated_at: string;
219
+ }
205
220
  interface ProductVariant {
206
221
  id: string;
207
222
  product_id: string;
@@ -231,6 +246,19 @@ interface OrderAddress {
231
246
  postcode: string;
232
247
  country: string;
233
248
  }
249
+ type OrderStatus = "pending" | "paid" | "processing" | "shipped" | "delivered" | "cancelled" | "refunded";
250
+ type PaymentStatus = "pending" | "succeeded" | "failed" | "refunded" | "partially_refunded";
251
+ interface OrderLineItem {
252
+ product_id: string;
253
+ variant_id: string | null;
254
+ name: string;
255
+ sku: string | null;
256
+ quantity: number;
257
+ unit_price_cents: number;
258
+ total_cents: number;
259
+ image_url: string | null;
260
+ }
261
+ type DiscountType = "percentage" | "fixed_amount";
234
262
  interface CreateOrderParams {
235
263
  customer_email: string;
236
264
  customer_name?: string;
@@ -248,11 +276,13 @@ interface CreateOrderParams {
248
276
  interface CreateOrderResult {
249
277
  order_id: string;
250
278
  order_number: string;
251
- client_secret: string;
279
+ /** Stripe PaymentIntent client_secret to confirm on the client. */
280
+ client_secret: string | null;
252
281
  total_cents: number;
253
282
  currency: string;
254
283
  }
255
284
  interface ProductQueryOptions {
285
+ /** Category slug (e.g. "electronics"). Matches the FK category, falling back to the legacy text column. */
256
286
  category?: string;
257
287
  tags?: string[];
258
288
  limit?: number;
@@ -435,13 +465,55 @@ interface TrackingConfig {
435
465
  */
436
466
  declare function getEmbedHtml(value: unknown): string;
437
467
 
438
- declare function createShopClient(supabase: SupabaseClient, options: {
468
+ interface ShopClientOptions {
469
+ /** Tenant API key (UUID). Sent as `x-cms-api-key` on every request. */
439
470
  apiKey: string;
471
+ /** CMS app base URL. Defaults to `https://cms.distinctstudio.co.nz`. */
440
472
  appUrl?: string;
441
- }): {
473
+ }
474
+ /**
475
+ * Creates a typed client for the eCommerce REST API (products, categories, orders).
476
+ *
477
+ * Unlike `createCmsClient`, the shop client talks to the CMS REST endpoints
478
+ * (`/api/products`, `/api/product-categories`, `/api/orders/create`) rather than
479
+ * Supabase directly — it doesn't need the `supabase` argument, but accepts it
480
+ * for API symmetry with the rest of the SDK.
481
+ *
482
+ * Usage:
483
+ * ```ts
484
+ * const shop = createShopClient(supabase, { apiKey: process.env.CMS_API_KEY! })
485
+ * const products = await shop.getProducts({ category: "electronics", limit: 20 })
486
+ * ```
487
+ */
488
+ declare function createShopClient(_supabase: SupabaseClient, options: ShopClientOptions): {
489
+ /**
490
+ * List published products. Includes nested `variants` and `options`.
491
+ *
492
+ * `category` filters by category slug (matches the FK `category_id`,
493
+ * with a fallback to the legacy `category` text column).
494
+ */
442
495
  getProducts(queryOptions?: ProductQueryOptions): Promise<Product[]>;
496
+ /**
497
+ * Get a single published product by slug. Returns null if not found.
498
+ * Includes nested `variants` and `options`.
499
+ */
443
500
  getProductBySlug(slug: string): Promise<Product | null>;
444
- getProductCategories(): Promise<string[]>;
501
+ /**
502
+ * List product categories for the tenant, ordered by sort_order then name.
503
+ */
504
+ getCategories(): Promise<ProductCategory[]>;
505
+ /**
506
+ * Get a single category by slug. Returns null if not found.
507
+ */
508
+ getCategoryBySlug(slug: string): Promise<ProductCategory | null>;
509
+ /**
510
+ * Create a pending order and return a Stripe PaymentIntent client_secret
511
+ * to confirm payment client-side.
512
+ *
513
+ * Stripe must be configured for the tenant; eCommerce must be enabled on
514
+ * their billing plan. Validates stock for tracked variants and applies
515
+ * any discount code before creating the PaymentIntent.
516
+ */
445
517
  createOrder(params: CreateOrderParams): Promise<CreateOrderResult>;
446
518
  };
447
519
 
@@ -603,4 +675,4 @@ declare const IMAGE_PRESETS: {
603
675
  };
604
676
  };
605
677
 
606
- export { type BookingStatus, type CmsClientOptions, type CmsEvent, type CmsTicketTier, type ContentItem, type ContentQueryOptions, type ContentType, type ContentTypeSeoConfig, type CreateBookingParams, type CreateBookingResult, type CreateOrderParams, type CreateOrderResult, type EmbedValue, type EventQueryOptions, type EventStatus, type EventWithTiers, type FieldDefinition, type FieldType, type FlipbookPagePublic, type FlipbookPublic, type FlipbookTocEntryPublic, type GoogleReview, IMAGE_PRESETS, type ImageConfig, type ImageTransformOptions, type MediaFolder, type MediaItem, type Member, type MembershipTier, type OrderAddress, type Product, type ProductOption, type ProductQueryOptions, type ProductVariant, type Profile, type ReviewQueryOptions, TRACKING_CONFIG_TAG, type Tenant, type TenantMembership, type TrackingConfig, type TrackingConfigOptions, createCmsClient, createEventsClient, createShopClient, getEmbedHtml, getSrcSet, getTransformUrl };
678
+ export { type BookingStatus, type CmsClientOptions, type CmsEvent, type CmsTicketTier, type ContentItem, type ContentQueryOptions, type ContentType, type ContentTypeSeoConfig, type CreateBookingParams, type CreateBookingResult, type CreateOrderParams, type CreateOrderResult, type DiscountType, type EmbedValue, type EventQueryOptions, type EventStatus, type EventWithTiers, type FieldDefinition, type FieldType, type FlipbookPagePublic, type FlipbookPublic, type FlipbookTocEntryPublic, type GoogleReview, IMAGE_PRESETS, type ImageConfig, type ImageTransformOptions, type MediaFolder, type MediaItem, type Member, type MembershipTier, type OrderAddress, type OrderLineItem, type OrderStatus, type PaymentStatus, type Product, type ProductCategory, type ProductOption, type ProductQueryOptions, type ProductStatus, type ProductVariant, type Profile, type ReviewQueryOptions, TRACKING_CONFIG_TAG, type Tenant, type TenantMembership, type TrackingConfig, type TrackingConfigOptions, createCmsClient, createEventsClient, createShopClient, getEmbedHtml, getSrcSet, getTransformUrl };
package/dist/index.js CHANGED
@@ -33,9 +33,46 @@ module.exports = __toCommonJS(src_exports);
33
33
 
34
34
  // src/queries.ts
35
35
  var import_supabase_js = require("@supabase/supabase-js");
36
+
37
+ // src/version.ts
38
+ var CMS_CLIENT_VERSION = "1.14.0";
39
+
40
+ // src/telemetry.ts
41
+ var DEFAULT_APP_URL = "https://cms.distinctstudio.co.nz";
42
+ var reported = /* @__PURE__ */ new Map();
43
+ function todayKey() {
44
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
45
+ }
46
+ function reportClientVersion(apiKey, appUrl) {
47
+ if (typeof window !== "undefined") return;
48
+ if (!apiKey) return;
49
+ const today = todayKey();
50
+ const last = reported.get(apiKey);
51
+ if (last && last.date === today && last.version === CMS_CLIENT_VERSION) return;
52
+ reported.set(apiKey, { date: today, version: CMS_CLIENT_VERSION });
53
+ const base = (appUrl ?? DEFAULT_APP_URL).replace(/\/$/, "");
54
+ const url = `${base}/api/client-telemetry`;
55
+ void fetch(url, {
56
+ method: "POST",
57
+ headers: {
58
+ "Content-Type": "application/json",
59
+ "x-cms-api-key": apiKey,
60
+ "x-cms-client-version": CMS_CLIENT_VERSION
61
+ },
62
+ body: JSON.stringify({
63
+ api_key: apiKey,
64
+ version: CMS_CLIENT_VERSION
65
+ })
66
+ }).catch(() => {
67
+ reported.delete(apiKey);
68
+ });
69
+ }
70
+
71
+ // src/queries.ts
36
72
  function createCmsClient(supabase, options) {
37
73
  const { apiKey } = options;
38
74
  const client = withApiKey(supabase, apiKey);
75
+ reportClientVersion(apiKey, options.appUrl);
39
76
  let tenantIdCache = null;
40
77
  async function getTenantId() {
41
78
  if (tenantIdCache) return tenantIdCache;
@@ -311,6 +348,7 @@ function createCmsClient(supabase, options) {
311
348
  apikey: supabaseKey,
312
349
  Authorization: `Bearer ${supabaseKey}`,
313
350
  "x-cms-api-key": apiKey,
351
+ "x-cms-client-version": CMS_CLIENT_VERSION,
314
352
  Accept: "application/json"
315
353
  },
316
354
  next: { revalidate, tags }
@@ -344,7 +382,8 @@ function withApiKey(supabase, apiKey) {
344
382
  return (0, import_supabase_js.createClient)(supabaseUrl, supabaseKey, {
345
383
  global: {
346
384
  headers: {
347
- "x-cms-api-key": apiKey
385
+ "x-cms-api-key": apiKey,
386
+ "x-cms-client-version": CMS_CLIENT_VERSION
348
387
  }
349
388
  }
350
389
  });
@@ -369,45 +408,101 @@ function getEmbedHtml(value) {
369
408
  }
370
409
 
371
410
  // src/shop.ts
372
- function createShopClient(supabase, options) {
373
- const { apiKey, appUrl } = options;
411
+ var DEFAULT_APP_URL2 = "https://cms.distinctstudio.co.nz";
412
+ function createShopClient(_supabase, options) {
413
+ const { apiKey } = options;
414
+ const appUrl = (options.appUrl ?? DEFAULT_APP_URL2).replace(/\/$/, "");
415
+ reportClientVersion(apiKey, appUrl);
416
+ function authHeaders() {
417
+ return {
418
+ "Content-Type": "application/json",
419
+ "x-cms-api-key": apiKey,
420
+ "x-cms-client-version": CMS_CLIENT_VERSION
421
+ };
422
+ }
423
+ async function getJson(path) {
424
+ const res = await fetch(`${appUrl}${path}`, {
425
+ headers: authHeaders()
426
+ // Caller controls Next.js caching by wrapping the call site.
427
+ });
428
+ if (!res.ok) {
429
+ const body = await res.json().catch(() => ({}));
430
+ throw new Error(
431
+ body.error ?? `Shop request failed (${res.status}): ${path}`
432
+ );
433
+ }
434
+ return res.json();
435
+ }
374
436
  return {
375
- async getProducts(queryOptions) {
376
- let query = supabase.from("products").select("*, variants:product_variants(*), options:product_options(*)").eq("status", "published").order(queryOptions?.sort ?? "sort_order", { ascending: (queryOptions?.order ?? "asc") === "asc" });
377
- if (queryOptions?.category) {
378
- query = query.eq("category", queryOptions.category);
379
- }
380
- if (queryOptions?.tags?.length) {
381
- query = query.overlaps("tags", queryOptions.tags);
382
- }
383
- if (queryOptions?.limit) {
384
- query = query.limit(queryOptions.limit);
385
- }
386
- if (queryOptions?.offset) {
387
- query = query.range(queryOptions.offset, queryOptions.offset + (queryOptions.limit ?? 50) - 1);
388
- }
389
- const { data } = await query;
390
- return data ?? [];
437
+ /**
438
+ * List published products. Includes nested `variants` and `options`.
439
+ *
440
+ * `category` filters by category slug (matches the FK `category_id`,
441
+ * with a fallback to the legacy `category` text column).
442
+ */
443
+ async getProducts(queryOptions = {}) {
444
+ const params = new URLSearchParams();
445
+ if (queryOptions.category) params.set("category", queryOptions.category);
446
+ if (queryOptions.tags?.length) params.set("tags", queryOptions.tags.join(","));
447
+ if (queryOptions.limit) params.set("limit", String(queryOptions.limit));
448
+ if (queryOptions.offset) params.set("offset", String(queryOptions.offset));
449
+ if (queryOptions.sort) params.set("sort", queryOptions.sort);
450
+ if (queryOptions.order) params.set("order", queryOptions.order);
451
+ const qs = params.toString();
452
+ const { products } = await getJson(
453
+ `/api/products${qs ? `?${qs}` : ""}`
454
+ );
455
+ return products ?? [];
391
456
  },
457
+ /**
458
+ * Get a single published product by slug. Returns null if not found.
459
+ * Includes nested `variants` and `options`.
460
+ */
392
461
  async getProductBySlug(slug) {
393
- const { data } = await supabase.from("products").select("*, variants:product_variants(*), options:product_options(*)").eq("slug", slug).eq("status", "published").single();
394
- return data ?? null;
462
+ const res = await fetch(`${appUrl}/api/products/${encodeURIComponent(slug)}`, {
463
+ headers: authHeaders()
464
+ });
465
+ if (res.status === 404) return null;
466
+ if (!res.ok) {
467
+ const body = await res.json().catch(() => ({}));
468
+ throw new Error(body.error ?? `Failed to fetch product ${slug}`);
469
+ }
470
+ const { product } = await res.json();
471
+ return product ?? null;
395
472
  },
396
- async getProductCategories() {
397
- const { data } = await supabase.from("products").select("category").eq("status", "published").not("category", "is", null);
398
- const categories = [...new Set((data ?? []).map((d) => d.category))];
399
- return categories.sort();
473
+ /**
474
+ * List product categories for the tenant, ordered by sort_order then name.
475
+ */
476
+ async getCategories() {
477
+ const { categories } = await getJson(
478
+ `/api/product-categories`
479
+ );
480
+ return categories ?? [];
400
481
  },
482
+ /**
483
+ * Get a single category by slug. Returns null if not found.
484
+ */
485
+ async getCategoryBySlug(slug) {
486
+ const all = await this.getCategories();
487
+ return all.find((c) => c.slug === slug) ?? null;
488
+ },
489
+ /**
490
+ * Create a pending order and return a Stripe PaymentIntent client_secret
491
+ * to confirm payment client-side.
492
+ *
493
+ * Stripe must be configured for the tenant; eCommerce must be enabled on
494
+ * their billing plan. Validates stock for tracked variants and applies
495
+ * any discount code before creating the PaymentIntent.
496
+ */
401
497
  async createOrder(params) {
402
- const baseUrl = appUrl ?? "";
403
- const res = await fetch(`${baseUrl}/api/orders/create`, {
498
+ const res = await fetch(`${appUrl}/api/orders/create`, {
404
499
  method: "POST",
405
- headers: { "Content-Type": "application/json" },
500
+ headers: authHeaders(),
406
501
  body: JSON.stringify({ api_key: apiKey, ...params })
407
502
  });
408
503
  if (!res.ok) {
409
- const err = await res.json().catch(() => ({ error: "Order creation failed" }));
410
- throw new Error(err.error ?? "Order creation failed");
504
+ const body = await res.json().catch(() => ({}));
505
+ throw new Error(body.error ?? "Order creation failed");
411
506
  }
412
507
  return res.json();
413
508
  }
@@ -417,6 +512,7 @@ function createShopClient(supabase, options) {
417
512
  // src/events.ts
418
513
  function createEventsClient(supabase, options) {
419
514
  const { apiKey, appUrl } = options;
515
+ reportClientVersion(apiKey, appUrl);
420
516
  async function getEvents(queryOptions) {
421
517
  let query = supabase.from("events").select("*, tiers:ticket_tiers(*)").eq("status", "published").order(queryOptions?.sort ?? "start_at", {
422
518
  ascending: (queryOptions?.order ?? "asc") === "asc"
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/queries.ts","../src/embed.ts","../src/shop.ts","../src/events.ts","../src/cdn.ts"],"sourcesContent":["// Server-safe exports — no React, no \"use client\"\nexport { createCmsClient, TRACKING_CONFIG_TAG } from \"./queries\"\nexport type { TrackingConfig, TrackingConfigOptions } from \"./queries\"\nexport { getEmbedHtml } from \"./embed\"\nexport { createShopClient } from \"./shop\"\nexport { createEventsClient } from \"./events\"\nexport type {\n CmsEvent,\n CmsTicketTier,\n EventWithTiers,\n EventQueryOptions,\n CreateBookingParams,\n CreateBookingResult,\n EventStatus,\n BookingStatus,\n} from \"./events\"\nexport { getTransformUrl, getSrcSet, IMAGE_PRESETS } from \"./cdn\"\nexport type { ImageTransformOptions } from \"./cdn\"\nexport type {\n FieldType,\n FieldDefinition,\n Tenant,\n Profile,\n ContentType,\n ContentItem,\n MediaItem,\n MediaFolder,\n ContentQueryOptions,\n CmsClientOptions,\n TenantMembership,\n ImageConfig,\n ContentTypeSeoConfig,\n Product,\n ProductVariant,\n ProductOption,\n OrderAddress,\n CreateOrderParams,\n CreateOrderResult,\n ProductQueryOptions,\n MembershipTier,\n Member,\n GoogleReview,\n ReviewQueryOptions,\n EmbedValue,\n FlipbookPagePublic,\n FlipbookTocEntryPublic,\n FlipbookPublic,\n} from \"./types\"\n","import { createClient as createSupabaseClient } from \"@supabase/supabase-js\"\nimport type { SupabaseClient } from \"@supabase/supabase-js\"\nimport type {\n CmsClientOptions,\n ContentItem,\n ContentQueryOptions,\n ContentType,\n GoogleReview,\n ReviewQueryOptions,\n} from \"./types\"\n\n/**\n * Creates a CMS query client authenticated with a tenant API key.\n *\n * Usage:\n * ```ts\n * const cms = createCmsClient(supabase, { apiKey: process.env.CMS_API_KEY! })\n * const events = await cms.getContentItems('events', { status: 'published' })\n * ```\n *\n * The API key is sent as an `x-cms-api-key` header on every request.\n * Supabase RLS policies validate it against the tenant's stored key.\n */\nexport function createCmsClient(\n supabase: SupabaseClient,\n options: CmsClientOptions\n) {\n const { apiKey } = options\n\n // Create a new Supabase client with the API key header injected globally\n const client = withApiKey(supabase, apiKey) as SupabaseClient\n\n /** Resolve the tenant ID from the API key (cached per request) */\n let tenantIdCache: string | null = null\n\n async function getTenantId(): Promise<string> {\n if (tenantIdCache) return tenantIdCache\n\n const { data, error } = await client\n .from(\"tenants\")\n .select(\"id\")\n .single()\n\n if (error || !data) {\n throw new Error(\n \"Invalid CMS API key — no tenant found. Check your apiKey.\"\n )\n }\n\n tenantIdCache = data.id\n return data.id\n }\n\n /** Resolve a content type from its slug (cached) */\n const contentTypeCache = new Map<string, ContentType>()\n\n async function getContentTypeBySlug(contentTypeSlug: string): Promise<ContentType> {\n if (contentTypeCache.has(contentTypeSlug)) return contentTypeCache.get(contentTypeSlug)!\n const tenantId = await getTenantId()\n\n const { data, error } = await client\n .from(\"content_types\")\n .select(\"*\")\n .eq(\"tenant_id\", tenantId)\n .eq(\"slug\", contentTypeSlug)\n .single()\n\n if (error || !data) {\n throw new Error(`Content type not found: ${contentTypeSlug}`)\n }\n\n const ct = data as ContentType\n contentTypeCache.set(contentTypeSlug, ct)\n return ct\n }\n\n async function getContentTypeId(contentTypeSlug: string): Promise<string> {\n const ct = await getContentTypeBySlug(contentTypeSlug)\n return ct.id\n }\n\n /** Check if a member token has access to a gated content type */\n async function checkMemberAccess(\n contentType: ContentType,\n memberToken?: string\n ): Promise<{ allowed: boolean; memberTierId: string | null }> {\n const requiredTierId = contentType.required_membership_tier_id\n if (!requiredTierId) return { allowed: true, memberTierId: null } // Public\n\n if (!memberToken) return { allowed: false, memberTierId: null } // Gated but no token\n\n // Verify the member's token and check their tier\n const { data: session } = await client\n .from(\"member_sessions\")\n .select(\"member_id\")\n .eq(\"token_hash\", memberToken) // Note: caller should hash the token\n .single()\n\n if (!session) return { allowed: false, memberTierId: null }\n\n const { data: member } = await client\n .from(\"members\")\n .select(\"membership_tier_id, status\")\n .eq(\"id\", session.member_id)\n .single()\n\n if (!member || member.status !== \"active\") return { allowed: false, memberTierId: null }\n\n // Check if the member's tier matches (or exceeds) the required tier\n // For now: exact match or the member has the required tier\n return {\n allowed: member.membership_tier_id === requiredTierId,\n memberTierId: member.membership_tier_id as string | null,\n }\n }\n\n async function getFlipbook(id: string): Promise<import(\"./types\").FlipbookPublic | null> {\n const { data, error } = await client\n .from(\"flipbooks\")\n .select(\"id, title, page_count, status, manifest, download_enabled, tenant_id\")\n .eq(\"id\", id)\n .single()\n if (error || !data) return null\n\n const manifest = data.manifest as\n | { pages: Array<{ image: string; thumb: string; w: number; h: number }>; toc: Array<{ title: string; page: number }> }\n | null\n if (!manifest) {\n return {\n id: data.id as string,\n title: data.title as string,\n page_count: (data.page_count as number) ?? 0,\n status: data.status as \"pending_upload\" | \"pending\" | \"processing\" | \"ready\" | \"failed\",\n page_images: [],\n toc: [],\n download_url: null,\n }\n }\n\n // Browser-safe env lookup via globalThis. The client package builds without\n // @types/node so referring to `process` directly fails the dts build; reading\n // through globalThis sidesteps that and works in every JS environment that\n // matters (Node, browsers, edge runtimes).\n const proc = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process\n const cdnBase = proc?.env?.NEXT_PUBLIC_R2_PUBLIC_URL ?? \"https://cdn.distinctstudio.co.nz\"\n const prefix = `flipbooks/${data.tenant_id as string}/${data.id as string}/`\n return {\n id: data.id as string,\n title: data.title as string,\n page_count: (data.page_count as number) ?? manifest.pages.length,\n status: data.status as \"pending\" | \"processing\" | \"ready\" | \"failed\",\n page_images: manifest.pages.map((p) => ({\n url: `${cdnBase}/${prefix}${p.image}`,\n thumb_url: `${cdnBase}/${prefix}${p.thumb}`,\n w: p.w,\n h: p.h,\n })),\n toc: manifest.toc,\n download_url: data.download_enabled\n ? `${options.appUrl ?? \"\"}/api/flipbooks/${data.id as string}/download`\n : null,\n }\n }\n\n return {\n /**\n * List content items for a content type.\n * Defaults to published items ordered by most recent.\n */\n async getContentItems(\n contentTypeSlug: string,\n options: ContentQueryOptions = {}\n ): Promise<(ContentItem & { locked?: boolean })[]> {\n const contentType = await getContentTypeBySlug(contentTypeSlug)\n\n const {\n status = \"published\",\n orderBy = \"published_at\",\n orderDirection = \"desc\",\n limit = 100,\n offset = 0,\n memberToken,\n } = options\n\n // Check membership access\n const access = await checkMemberAccess(contentType, memberToken)\n\n // If gated and no access, check gating mode\n if (!access.allowed && contentType.required_membership_tier_id) {\n // Get tenant settings for gating mode\n const tenantId = await getTenantId()\n const { data: settings } = await client\n .from(\"tenant_settings\")\n .select(\"membership_gating_mode\")\n .eq(\"tenant_id\", tenantId)\n .single()\n\n const mode = (settings?.membership_gating_mode as string) ?? \"teaser\"\n\n if (mode === \"hide\") {\n return [] // Hide: return nothing\n }\n\n // Teaser mode: return items with locked flag, no body/data\n let query = client\n .from(\"content_items\")\n .select(\"id, title, slug, status, excerpt, seo_title, seo_description, featured_image, published_at, created_at, updated_at\")\n .eq(\"content_type_id\", contentType.id)\n\n if (status) query = query.eq(\"status\", status)\n query = query\n .order(orderBy, { ascending: orderDirection === \"asc\" })\n .range(offset, offset + limit - 1)\n\n const { data, error } = await query\n if (error) throw new Error(`Failed to fetch ${contentTypeSlug}: ${error.message}`)\n\n return (data ?? []).map((item) => ({\n ...item,\n data: {},\n locked: true,\n })) as (ContentItem & { locked: boolean })[]\n }\n\n // Full access\n let query = client\n .from(\"content_items\")\n .select(\"*\")\n .eq(\"content_type_id\", contentType.id)\n\n if (status) {\n query = query.eq(\"status\", status)\n }\n\n query = query\n .order(orderBy, { ascending: orderDirection === \"asc\" })\n .range(offset, offset + limit - 1)\n\n const { data, error } = await query\n\n if (error) {\n throw new Error(`Failed to fetch ${contentTypeSlug}: ${error.message}`)\n }\n\n return (data ?? []) as ContentItem[]\n },\n\n /**\n * Get a single content item by its slug.\n * Returns null if not found.\n */\n async getContentItemBySlug(\n contentTypeSlug: string,\n itemSlug: string\n ): Promise<ContentItem | null> {\n const contentTypeId = await getContentTypeId(contentTypeSlug)\n\n const { data, error } = await client\n .from(\"content_items\")\n .select(\"*\")\n .eq(\"content_type_id\", contentTypeId)\n .eq(\"slug\", itemSlug)\n .single()\n\n if (error) {\n if (error.code === \"PGRST116\") return null // not found\n throw new Error(\n `Failed to fetch ${contentTypeSlug}/${itemSlug}: ${error.message}`\n )\n }\n\n return data as ContentItem\n },\n\n /**\n * Get a content type definition (including its field schema).\n */\n async getContentType(contentTypeSlug: string): Promise<ContentType> {\n const tenantId = await getTenantId()\n\n const { data, error } = await client\n .from(\"content_types\")\n .select(\"*\")\n .eq(\"tenant_id\", tenantId)\n .eq(\"slug\", contentTypeSlug)\n .single()\n\n if (error || !data) {\n throw new Error(`Content type not found: ${contentTypeSlug}`)\n }\n\n return data as ContentType\n },\n\n /**\n * Get all slugs for a content type (for generateStaticParams).\n */\n async getAllSlugs(\n contentTypeSlug: string\n ): Promise<{ slug: string }[]> {\n const contentTypeId = await getContentTypeId(contentTypeSlug)\n\n const { data, error } = await client\n .from(\"content_items\")\n .select(\"slug\")\n .eq(\"content_type_id\", contentTypeId)\n .eq(\"status\", \"published\")\n\n if (error) {\n throw new Error(\n `Failed to fetch slugs for ${contentTypeSlug}: ${error.message}`\n )\n }\n\n return (data ?? []) as { slug: string }[]\n },\n\n /**\n * List all content types for this tenant.\n */\n async getContentTypes(): Promise<ContentType[]> {\n const tenantId = await getTenantId()\n\n const { data, error } = await client\n .from(\"content_types\")\n .select(\"*\")\n .eq(\"tenant_id\", tenantId)\n .order(\"name\")\n\n if (error) {\n throw new Error(`Failed to fetch content types: ${error.message}`)\n }\n\n return (data ?? []) as ContentType[]\n },\n\n /**\n * Get all slug redirects for a content type.\n * Use in next.config.js redirects() or middleware for 301s.\n * Returns: [{ old_slug, new_slug }, ...]\n */\n async getRedirects(\n contentTypeSlug: string\n ): Promise<{ old_slug: string; new_slug: string }[]> {\n const contentTypeId = await getContentTypeId(contentTypeSlug)\n\n const { data, error } = await client\n .from(\"slug_redirects\")\n .select(\"old_slug, new_slug\")\n .eq(\"content_type_id\", contentTypeId)\n\n if (error) {\n throw new Error(\n `Failed to fetch redirects for ${contentTypeSlug}: ${error.message}`\n )\n }\n\n return (data ?? []) as { old_slug: string; new_slug: string }[]\n },\n\n /**\n * Get all custom path redirects for this tenant.\n * Use alongside getRedirects() in next.config.ts for full redirect coverage.\n */\n async getCustomRedirects(): Promise<\n { source_path: string; destination_path: string; permanent: boolean }[]\n > {\n const tenantId = await getTenantId()\n\n const { data, error } = await client\n .from(\"custom_redirects\")\n .select(\"source_path, destination_path, permanent\")\n .eq(\"tenant_id\", tenantId)\n\n if (error) {\n throw new Error(`Failed to fetch custom redirects: ${error.message}`)\n }\n\n return (data ?? []) as {\n source_path: string\n destination_path: string\n permanent: boolean\n }[]\n },\n\n /**\n * Get form submissions for a specific form.\n * Useful for displaying testimonials, reviews, etc.\n */\n async getFormSubmissions(\n formSlug: string,\n options: { status?: string; limit?: number; offset?: number } = {}\n ): Promise<Record<string, unknown>[]> {\n const tenantId = await getTenantId()\n\n const { data: form } = await client\n .from(\"forms\")\n .select(\"id\")\n .eq(\"tenant_id\", tenantId)\n .eq(\"slug\", formSlug)\n .single()\n\n if (!form) return []\n\n const { limit = 50, offset = 0, status } = options\n\n let query = client\n .from(\"form_submissions\")\n .select(\"*\")\n .eq(\"form_id\", form.id)\n .eq(\"is_spam\", false)\n .order(\"created_at\", { ascending: false })\n .range(offset, offset + limit - 1)\n\n if (status) {\n query = query.eq(\"status\", status)\n }\n\n const { data } = await query\n return (data ?? []) as Record<string, unknown>[]\n },\n\n /**\n * Get a form configuration by slug.\n */\n async getForm(\n formSlug: string\n ): Promise<Record<string, unknown> | null> {\n const tenantId = await getTenantId()\n\n const { data } = await client\n .from(\"forms\")\n .select(\"id, name, slug, description, is_active\")\n .eq(\"tenant_id\", tenantId)\n .eq(\"slug\", formSlug)\n .single()\n\n return data as Record<string, unknown> | null\n },\n\n /**\n * Get a flipbook by ID, including CDN-resolved page image URLs.\n * Returns null if not found or manifest not yet ready.\n */\n getFlipbook,\n\n /**\n * Get Google Reviews for this tenant.\n * Defaults to approved reviews ordered by most recent.\n */\n async getReviews(\n options: ReviewQueryOptions = {}\n ): Promise<GoogleReview[]> {\n const tenantId = await getTenantId()\n\n const {\n status = \"approved\",\n minRating,\n limit = 50,\n offset = 0,\n orderBy = \"review_timestamp\",\n orderDirection = \"desc\",\n } = options\n\n let query = client\n .from(\"google_reviews\")\n .select(\"id, author_name, author_photo_url, rating, text, review_timestamp\")\n .eq(\"tenant_id\", tenantId)\n\n if (status) {\n query = query.eq(\"status\", status)\n }\n\n if (minRating) {\n query = query.gte(\"rating\", minRating)\n }\n\n query = query\n .order(orderBy, { ascending: orderDirection === \"asc\" })\n .range(offset, offset + limit - 1)\n\n const { data, error } = await query\n\n if (error) {\n throw new Error(`Failed to fetch reviews: ${error.message}`)\n }\n\n return (data ?? []) as GoogleReview[]\n },\n\n /**\n * Get the tenant's third-party tracking IDs (Google Analytics, Google Tag Manager, Meta Pixel).\n * Each value is null when not configured. These IDs are public and safe to render in HTML.\n *\n * On Next.js, the result is cached and revalidated every hour by default\n * (`revalidate: 3600`) and tagged with `TRACKING_CONFIG_TAG`. Tenant sites\n * that want zero-lag updates can call `revalidateTag(TRACKING_CONFIG_TAG)`\n * from a webhook. Pass `revalidate: 0` to disable caching, `revalidate: false`\n * to cache indefinitely (tag-only invalidation), or any positive integer\n * (seconds) to override the interval. Outside Next.js the cache hints are\n * silently ignored — every call hits the network.\n */\n async getTrackingConfig(options: TrackingConfigOptions = {}): Promise<TrackingConfig> {\n const { revalidate = 3600, tags = [TRACKING_CONFIG_TAG] } = options\n const supabaseUrl = (supabase as unknown as { supabaseUrl: string }).supabaseUrl\n const supabaseKey = (supabase as unknown as { supabaseKey: string }).supabaseKey\n\n // Direct PostgREST fetch (instead of going through supabase-js) so that\n // Next.js sees the request and applies its `next` cache options. RLS\n // restricts the result to the tenant matching `x-cms-api-key`, so no\n // explicit tenant_id filter is needed.\n const url =\n `${supabaseUrl}/rest/v1/tenant_settings` +\n `?select=google_analytics_id,google_tag_manager_id,meta_pixel_id&limit=1`\n\n const init: RequestInit & { next?: { revalidate?: number | false; tags?: string[] } } = {\n headers: {\n apikey: supabaseKey,\n Authorization: `Bearer ${supabaseKey}`,\n \"x-cms-api-key\": apiKey,\n Accept: \"application/json\",\n },\n next: { revalidate, tags },\n }\n\n let row: {\n google_analytics_id: string | null\n google_tag_manager_id: string | null\n meta_pixel_id: string | null\n } | undefined\n\n try {\n const res = await fetch(url, init as RequestInit)\n if (res.ok) {\n const rows = (await res.json()) as Array<typeof row>\n row = rows?.[0]\n }\n } catch {\n // Network errors fall through to the all-null result so a transient\n // outage on the CMS never breaks page renders on the tenant site.\n }\n\n return {\n googleAnalyticsId: nullify(row?.google_analytics_id),\n googleTagManagerId: nullify(row?.google_tag_manager_id),\n metaPixelId: nullify(row?.meta_pixel_id),\n }\n },\n }\n}\n\n/**\n * Cache tag applied to `getTrackingConfig()` fetches on Next.js. Call\n * `revalidateTag(TRACKING_CONFIG_TAG)` from a webhook handler on the tenant\n * site to make tracking-ID changes take effect immediately rather than\n * waiting for the next revalidation interval.\n */\nexport const TRACKING_CONFIG_TAG = \"cms:tracking-config\"\n\nexport interface TrackingConfigOptions {\n /**\n * Cache lifetime for the underlying fetch on Next.js, in seconds.\n * Defaults to 3600 (one hour). Set to `0` to disable caching, or `false`\n * to cache indefinitely until the tag is revalidated. Ignored outside\n * Next.js runtimes.\n */\n revalidate?: number | false\n /**\n * Cache tags for the underlying fetch on Next.js. Defaults to\n * `[TRACKING_CONFIG_TAG]`. Override to namespace by tenant if you share a\n * single Next.js process across multiple tenants (uncommon).\n */\n tags?: string[]\n}\n\nfunction nullify(v: string | null | undefined): string | null {\n if (!v) return null\n const trimmed = v.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nexport interface TrackingConfig {\n googleAnalyticsId: string | null\n googleTagManagerId: string | null\n metaPixelId: string | null\n}\n\n/**\n * Creates a new Supabase client that includes the `x-cms-api-key`\n * header in every request, using the same URL and key as the original.\n */\nfunction withApiKey(supabase: SupabaseClient, apiKey: string) {\n // Extract URL and key from the existing client\n const supabaseUrl = (supabase as unknown as { supabaseUrl: string }).supabaseUrl\n const supabaseKey = (supabase as unknown as { supabaseKey: string }).supabaseKey\n\n return createSupabaseClient(supabaseUrl, supabaseKey, {\n global: {\n headers: {\n \"x-cms-api-key\": apiKey,\n },\n },\n })\n}\n","import type { EmbedValue } from \"./types\"\n\nfunction toCssAspectRatio(ratio: string): string {\n const parts = ratio.split(\":\")\n if (parts.length !== 2) return \"16/9\"\n const w = parseFloat(parts[0])\n const h = parseFloat(parts[1])\n if (!w || !h || w <= 0 || h <= 0) return \"16/9\"\n return `${w}/${h}`\n}\n\n/**\n * Generate responsive iframe HTML for an embed field value.\n * Returns an empty string if the value is invalid or the URL is not HTTPS.\n */\nexport function getEmbedHtml(value: unknown): string {\n if (!value || typeof value !== \"object\") return \"\"\n const embed = value as EmbedValue\n if (!embed.url || !embed.url.startsWith(\"https://\")) return \"\"\n\n const width = embed.width || \"100%\"\n const aspectRatio = toCssAspectRatio(embed.aspect_ratio || \"16:9\")\n\n return `<div style=\"position:relative;width:${width};aspect-ratio:${aspectRatio}\"><iframe src=\"${embed.url}\" style=\"position:absolute;inset:0;width:100%;height:100%\" frameborder=\"0\" sandbox=\"allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox\" allowfullscreen loading=\"lazy\"></iframe></div>`\n}\n","import type { SupabaseClient } from \"@supabase/supabase-js\"\nimport type { Product, ProductQueryOptions, CreateOrderParams, CreateOrderResult } from \"./types\"\n\nexport function createShopClient(supabase: SupabaseClient, options: { apiKey: string; appUrl?: string }) {\n const { apiKey, appUrl } = options\n\n return {\n async getProducts(queryOptions?: ProductQueryOptions): Promise<Product[]> {\n let query = supabase\n .from(\"products\")\n .select(\"*, variants:product_variants(*), options:product_options(*)\")\n .eq(\"status\", \"published\")\n .order(queryOptions?.sort ?? \"sort_order\", { ascending: (queryOptions?.order ?? \"asc\") === \"asc\" })\n\n if (queryOptions?.category) {\n query = query.eq(\"category\", queryOptions.category)\n }\n if (queryOptions?.tags?.length) {\n query = query.overlaps(\"tags\", queryOptions.tags)\n }\n if (queryOptions?.limit) {\n query = query.limit(queryOptions.limit)\n }\n if (queryOptions?.offset) {\n query = query.range(queryOptions.offset, queryOptions.offset + (queryOptions.limit ?? 50) - 1)\n }\n\n const { data } = await query\n return (data ?? []) as Product[]\n },\n\n async getProductBySlug(slug: string): Promise<Product | null> {\n const { data } = await supabase\n .from(\"products\")\n .select(\"*, variants:product_variants(*), options:product_options(*)\")\n .eq(\"slug\", slug)\n .eq(\"status\", \"published\")\n .single()\n return (data as Product) ?? null\n },\n\n async getProductCategories(): Promise<string[]> {\n const { data } = await supabase\n .from(\"products\")\n .select(\"category\")\n .eq(\"status\", \"published\")\n .not(\"category\", \"is\", null)\n const categories = [...new Set((data ?? []).map((d: { category: string }) => d.category))]\n return categories.sort()\n },\n\n async createOrder(params: CreateOrderParams): Promise<CreateOrderResult> {\n // This must go through the API (not direct Supabase) because\n // order creation requires server-side Stripe PaymentIntent creation\n const baseUrl = appUrl ?? \"\"\n const res = await fetch(`${baseUrl}/api/orders/create`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ api_key: apiKey, ...params }),\n })\n if (!res.ok) {\n const err = await res.json().catch(() => ({ error: \"Order creation failed\" }))\n throw new Error(err.error ?? \"Order creation failed\")\n }\n return res.json()\n },\n }\n}\n","import type { SupabaseClient } from \"@supabase/supabase-js\"\n\nexport type EventStatus = \"draft\" | \"published\" | \"archived\"\nexport type BookingStatus = \"pending\" | \"confirmed\" | \"cancelled\" | \"refunded\"\n\nexport interface CmsEvent {\n id: string\n tenant_id: string\n title: string\n slug: string\n description: string | null\n short_description: string | null\n status: EventStatus\n start_at: string\n end_at: string | null\n venue_name: string | null\n venue_address: string | null\n hero_image: string | null\n gallery: string[]\n tags: string[]\n seo_title: string | null\n seo_description: string | null\n metadata: Record<string, unknown>\n sort_order: number\n published_at: string | null\n created_at: string\n updated_at: string\n}\n\nexport interface CmsTicketTier {\n id: string\n event_id: string\n tenant_id: string\n name: string\n description: string | null\n price_cents: number\n capacity: number | null\n sold_count: number\n sales_start_at: string | null\n sales_end_at: string | null\n is_active: boolean\n sort_order: number\n}\n\nexport interface EventWithTiers extends CmsEvent {\n tiers: CmsTicketTier[]\n}\n\nexport interface EventQueryOptions {\n /** Only include events with start_at >= now. Default false. */\n upcomingOnly?: boolean\n tag?: string\n limit?: number\n offset?: number\n sort?: \"start_at\" | \"sort_order\" | \"created_at\"\n order?: \"asc\" | \"desc\"\n}\n\nexport interface CreateBookingParams {\n event_slug: string\n ticket_tier_id?: string\n customer_email: string\n customer_name?: string\n customer_phone?: string\n quantity?: number\n success_url?: string\n cancel_url?: string\n metadata?: Record<string, unknown>\n}\n\nexport interface CreateBookingResult {\n booking_id: string\n booking_number: string\n status: \"pending\" | \"confirmed\"\n /** Redirect the browser here for paid bookings. */\n checkout_url?: string\n /** Present on free bookings — used for attendance QR display. */\n qr_token?: string\n}\n\nexport function createEventsClient(\n supabase: SupabaseClient,\n options: { apiKey: string; appUrl?: string }\n) {\n const { apiKey, appUrl } = options\n\n async function getEvents(\n queryOptions?: EventQueryOptions\n ): Promise<EventWithTiers[]> {\n let query = supabase\n .from(\"events\")\n .select(\"*, tiers:ticket_tiers(*)\")\n .eq(\"status\", \"published\")\n .order(queryOptions?.sort ?? \"start_at\", {\n ascending: (queryOptions?.order ?? \"asc\") === \"asc\",\n })\n\n if (queryOptions?.upcomingOnly) {\n query = query.gte(\"start_at\", new Date().toISOString())\n }\n if (queryOptions?.tag) {\n query = query.contains(\"tags\", [queryOptions.tag])\n }\n if (queryOptions?.limit) {\n query = query.limit(queryOptions.limit)\n }\n if (queryOptions?.offset) {\n query = query.range(\n queryOptions.offset,\n queryOptions.offset + (queryOptions.limit ?? 50) - 1\n )\n }\n\n const { data } = await query\n return (data ?? []) as EventWithTiers[]\n }\n\n async function getEventBySlug(slug: string): Promise<EventWithTiers | null> {\n const { data } = await supabase\n .from(\"events\")\n .select(\"*, tiers:ticket_tiers(*)\")\n .eq(\"slug\", slug)\n .eq(\"status\", \"published\")\n .single()\n return (data as EventWithTiers) ?? null\n }\n\n async function createBooking(\n params: CreateBookingParams\n ): Promise<CreateBookingResult> {\n const baseUrl = appUrl ?? \"\"\n const res = await fetch(`${baseUrl}/api/bookings/create`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ api_key: apiKey, ...params }),\n })\n if (!res.ok) {\n const err = await res\n .json()\n .catch(() => ({ error: \"Booking creation failed\" }))\n throw new Error(err.error ?? \"Booking creation failed\")\n }\n return res.json() as Promise<CreateBookingResult>\n }\n\n return { getEvents, getEventBySlug, createBooking }\n}\n","/**\n * Image helpers for CMS content.\n *\n * Images are already optimised (WebP, max 2400px) on upload.\n * No server-side transforms needed — serve originals directly.\n *\n * These functions are kept for backward compatibility but no longer\n * call Supabase image transformation endpoints.\n */\n\nexport interface ImageTransformOptions {\n width?: number\n height?: number\n quality?: number\n resize?: \"contain\" | \"cover\" | \"fill\"\n format?: \"origin\"\n}\n\n/**\n * Returns the image URL directly.\n *\n * Previously this converted URLs to use Supabase's /render/image/\n * transform endpoint, but images are now pre-optimised on upload\n * (WebP, max 2400px, quality 82) so transforms are unnecessary.\n *\n * The function is kept for backward compatibility — existing code\n * that calls getTransformUrl() will continue to work without changes.\n */\nexport function getTransformUrl(\n originalUrl: string,\n _options: ImageTransformOptions = {}\n): string {\n return originalUrl\n}\n\n/**\n * Returns a simple srcSet using the original image.\n *\n * Previously generated multiple transformed widths, but since images\n * are now pre-optimised WebP, a single source is sufficient.\n * Modern browsers handle responsive display efficiently with a\n * well-sized WebP source.\n */\nexport function getSrcSet(\n originalUrl: string,\n _widths: number[] = [],\n _quality = 80\n): string {\n return `${originalUrl} 2400w`\n}\n\n/**\n * Common image size presets — kept for backward compatibility.\n * Since images are pre-optimised, these are informational only.\n */\nexport const IMAGE_PRESETS = {\n thumbnail: { width: 150, height: 150, resize: \"cover\" as const, quality: 70 },\n card: { width: 400, height: 300, resize: \"cover\" as const, quality: 80 },\n hero: { width: 1200, height: 630, resize: \"cover\" as const, quality: 85 },\n og: { width: 1200, height: 630, resize: \"cover\" as const, quality: 90 },\n avatar: { width: 80, height: 80, resize: \"cover\" as const, quality: 75 },\n full: { width: 1920, resize: \"contain\" as const, quality: 85 },\n} as const\n"],"mappings":"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","names":["options","query","data","error","createSupabaseClient"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/queries.ts","../src/version.ts","../src/telemetry.ts","../src/embed.ts","../src/shop.ts","../src/events.ts","../src/cdn.ts"],"sourcesContent":["// Server-safe exports — no React, no \"use client\"\nexport { createCmsClient, TRACKING_CONFIG_TAG } from \"./queries\"\nexport type { TrackingConfig, TrackingConfigOptions } from \"./queries\"\nexport { getEmbedHtml } from \"./embed\"\nexport { createShopClient } from \"./shop\"\nexport { createEventsClient } from \"./events\"\nexport type {\n CmsEvent,\n CmsTicketTier,\n EventWithTiers,\n EventQueryOptions,\n CreateBookingParams,\n CreateBookingResult,\n EventStatus,\n BookingStatus,\n} from \"./events\"\nexport { getTransformUrl, getSrcSet, IMAGE_PRESETS } from \"./cdn\"\nexport type { ImageTransformOptions } from \"./cdn\"\nexport type {\n FieldType,\n FieldDefinition,\n Tenant,\n Profile,\n ContentType,\n ContentItem,\n MediaItem,\n MediaFolder,\n ContentQueryOptions,\n CmsClientOptions,\n TenantMembership,\n ImageConfig,\n ContentTypeSeoConfig,\n Product,\n ProductStatus,\n ProductCategory,\n ProductVariant,\n ProductOption,\n OrderAddress,\n OrderStatus,\n PaymentStatus,\n OrderLineItem,\n DiscountType,\n CreateOrderParams,\n CreateOrderResult,\n ProductQueryOptions,\n MembershipTier,\n Member,\n GoogleReview,\n ReviewQueryOptions,\n EmbedValue,\n FlipbookPagePublic,\n FlipbookTocEntryPublic,\n FlipbookPublic,\n} from \"./types\"\n","import { createClient as createSupabaseClient } from \"@supabase/supabase-js\"\nimport type { SupabaseClient } from \"@supabase/supabase-js\"\nimport type {\n CmsClientOptions,\n ContentItem,\n ContentQueryOptions,\n ContentType,\n GoogleReview,\n ReviewQueryOptions,\n} from \"./types\"\nimport { CMS_CLIENT_VERSION } from \"./version\"\nimport { reportClientVersion } from \"./telemetry\"\n\n/**\n * Creates a CMS query client authenticated with a tenant API key.\n *\n * Usage:\n * ```ts\n * const cms = createCmsClient(supabase, { apiKey: process.env.CMS_API_KEY! })\n * const events = await cms.getContentItems('events', { status: 'published' })\n * ```\n *\n * The API key is sent as an `x-cms-api-key` header on every request.\n * Supabase RLS policies validate it against the tenant's stored key.\n */\nexport function createCmsClient(\n supabase: SupabaseClient,\n options: CmsClientOptions\n) {\n const { apiKey } = options\n\n // Create a new Supabase client with the API key header injected globally\n const client = withApiKey(supabase, apiKey) as SupabaseClient\n\n // Day-granular, fire-and-forget version ping (server-side only).\n reportClientVersion(apiKey, options.appUrl)\n\n /** Resolve the tenant ID from the API key (cached per request) */\n let tenantIdCache: string | null = null\n\n async function getTenantId(): Promise<string> {\n if (tenantIdCache) return tenantIdCache\n\n const { data, error } = await client\n .from(\"tenants\")\n .select(\"id\")\n .single()\n\n if (error || !data) {\n throw new Error(\n \"Invalid CMS API key — no tenant found. Check your apiKey.\"\n )\n }\n\n tenantIdCache = data.id\n return data.id\n }\n\n /** Resolve a content type from its slug (cached) */\n const contentTypeCache = new Map<string, ContentType>()\n\n async function getContentTypeBySlug(contentTypeSlug: string): Promise<ContentType> {\n if (contentTypeCache.has(contentTypeSlug)) return contentTypeCache.get(contentTypeSlug)!\n const tenantId = await getTenantId()\n\n const { data, error } = await client\n .from(\"content_types\")\n .select(\"*\")\n .eq(\"tenant_id\", tenantId)\n .eq(\"slug\", contentTypeSlug)\n .single()\n\n if (error || !data) {\n throw new Error(`Content type not found: ${contentTypeSlug}`)\n }\n\n const ct = data as ContentType\n contentTypeCache.set(contentTypeSlug, ct)\n return ct\n }\n\n async function getContentTypeId(contentTypeSlug: string): Promise<string> {\n const ct = await getContentTypeBySlug(contentTypeSlug)\n return ct.id\n }\n\n /** Check if a member token has access to a gated content type */\n async function checkMemberAccess(\n contentType: ContentType,\n memberToken?: string\n ): Promise<{ allowed: boolean; memberTierId: string | null }> {\n const requiredTierId = contentType.required_membership_tier_id\n if (!requiredTierId) return { allowed: true, memberTierId: null } // Public\n\n if (!memberToken) return { allowed: false, memberTierId: null } // Gated but no token\n\n // Verify the member's token and check their tier\n const { data: session } = await client\n .from(\"member_sessions\")\n .select(\"member_id\")\n .eq(\"token_hash\", memberToken) // Note: caller should hash the token\n .single()\n\n if (!session) return { allowed: false, memberTierId: null }\n\n const { data: member } = await client\n .from(\"members\")\n .select(\"membership_tier_id, status\")\n .eq(\"id\", session.member_id)\n .single()\n\n if (!member || member.status !== \"active\") return { allowed: false, memberTierId: null }\n\n // Check if the member's tier matches (or exceeds) the required tier\n // For now: exact match or the member has the required tier\n return {\n allowed: member.membership_tier_id === requiredTierId,\n memberTierId: member.membership_tier_id as string | null,\n }\n }\n\n async function getFlipbook(id: string): Promise<import(\"./types\").FlipbookPublic | null> {\n const { data, error } = await client\n .from(\"flipbooks\")\n .select(\"id, title, page_count, status, manifest, download_enabled, tenant_id\")\n .eq(\"id\", id)\n .single()\n if (error || !data) return null\n\n const manifest = data.manifest as\n | { pages: Array<{ image: string; thumb: string; w: number; h: number }>; toc: Array<{ title: string; page: number }> }\n | null\n if (!manifest) {\n return {\n id: data.id as string,\n title: data.title as string,\n page_count: (data.page_count as number) ?? 0,\n status: data.status as \"pending_upload\" | \"pending\" | \"processing\" | \"ready\" | \"failed\",\n page_images: [],\n toc: [],\n download_url: null,\n }\n }\n\n // Browser-safe env lookup via globalThis. The client package builds without\n // @types/node so referring to `process` directly fails the dts build; reading\n // through globalThis sidesteps that and works in every JS environment that\n // matters (Node, browsers, edge runtimes).\n const proc = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process\n const cdnBase = proc?.env?.NEXT_PUBLIC_R2_PUBLIC_URL ?? \"https://cdn.distinctstudio.co.nz\"\n const prefix = `flipbooks/${data.tenant_id as string}/${data.id as string}/`\n return {\n id: data.id as string,\n title: data.title as string,\n page_count: (data.page_count as number) ?? manifest.pages.length,\n status: data.status as \"pending\" | \"processing\" | \"ready\" | \"failed\",\n page_images: manifest.pages.map((p) => ({\n url: `${cdnBase}/${prefix}${p.image}`,\n thumb_url: `${cdnBase}/${prefix}${p.thumb}`,\n w: p.w,\n h: p.h,\n })),\n toc: manifest.toc,\n download_url: data.download_enabled\n ? `${options.appUrl ?? \"\"}/api/flipbooks/${data.id as string}/download`\n : null,\n }\n }\n\n return {\n /**\n * List content items for a content type.\n * Defaults to published items ordered by most recent.\n */\n async getContentItems(\n contentTypeSlug: string,\n options: ContentQueryOptions = {}\n ): Promise<(ContentItem & { locked?: boolean })[]> {\n const contentType = await getContentTypeBySlug(contentTypeSlug)\n\n const {\n status = \"published\",\n orderBy = \"published_at\",\n orderDirection = \"desc\",\n limit = 100,\n offset = 0,\n memberToken,\n } = options\n\n // Check membership access\n const access = await checkMemberAccess(contentType, memberToken)\n\n // If gated and no access, check gating mode\n if (!access.allowed && contentType.required_membership_tier_id) {\n // Get tenant settings for gating mode\n const tenantId = await getTenantId()\n const { data: settings } = await client\n .from(\"tenant_settings\")\n .select(\"membership_gating_mode\")\n .eq(\"tenant_id\", tenantId)\n .single()\n\n const mode = (settings?.membership_gating_mode as string) ?? \"teaser\"\n\n if (mode === \"hide\") {\n return [] // Hide: return nothing\n }\n\n // Teaser mode: return items with locked flag, no body/data\n let query = client\n .from(\"content_items\")\n .select(\"id, title, slug, status, excerpt, seo_title, seo_description, featured_image, published_at, created_at, updated_at\")\n .eq(\"content_type_id\", contentType.id)\n\n if (status) query = query.eq(\"status\", status)\n query = query\n .order(orderBy, { ascending: orderDirection === \"asc\" })\n .range(offset, offset + limit - 1)\n\n const { data, error } = await query\n if (error) throw new Error(`Failed to fetch ${contentTypeSlug}: ${error.message}`)\n\n return (data ?? []).map((item) => ({\n ...item,\n data: {},\n locked: true,\n })) as (ContentItem & { locked: boolean })[]\n }\n\n // Full access\n let query = client\n .from(\"content_items\")\n .select(\"*\")\n .eq(\"content_type_id\", contentType.id)\n\n if (status) {\n query = query.eq(\"status\", status)\n }\n\n query = query\n .order(orderBy, { ascending: orderDirection === \"asc\" })\n .range(offset, offset + limit - 1)\n\n const { data, error } = await query\n\n if (error) {\n throw new Error(`Failed to fetch ${contentTypeSlug}: ${error.message}`)\n }\n\n return (data ?? []) as ContentItem[]\n },\n\n /**\n * Get a single content item by its slug.\n * Returns null if not found.\n */\n async getContentItemBySlug(\n contentTypeSlug: string,\n itemSlug: string\n ): Promise<ContentItem | null> {\n const contentTypeId = await getContentTypeId(contentTypeSlug)\n\n const { data, error } = await client\n .from(\"content_items\")\n .select(\"*\")\n .eq(\"content_type_id\", contentTypeId)\n .eq(\"slug\", itemSlug)\n .single()\n\n if (error) {\n if (error.code === \"PGRST116\") return null // not found\n throw new Error(\n `Failed to fetch ${contentTypeSlug}/${itemSlug}: ${error.message}`\n )\n }\n\n return data as ContentItem\n },\n\n /**\n * Get a content type definition (including its field schema).\n */\n async getContentType(contentTypeSlug: string): Promise<ContentType> {\n const tenantId = await getTenantId()\n\n const { data, error } = await client\n .from(\"content_types\")\n .select(\"*\")\n .eq(\"tenant_id\", tenantId)\n .eq(\"slug\", contentTypeSlug)\n .single()\n\n if (error || !data) {\n throw new Error(`Content type not found: ${contentTypeSlug}`)\n }\n\n return data as ContentType\n },\n\n /**\n * Get all slugs for a content type (for generateStaticParams).\n */\n async getAllSlugs(\n contentTypeSlug: string\n ): Promise<{ slug: string }[]> {\n const contentTypeId = await getContentTypeId(contentTypeSlug)\n\n const { data, error } = await client\n .from(\"content_items\")\n .select(\"slug\")\n .eq(\"content_type_id\", contentTypeId)\n .eq(\"status\", \"published\")\n\n if (error) {\n throw new Error(\n `Failed to fetch slugs for ${contentTypeSlug}: ${error.message}`\n )\n }\n\n return (data ?? []) as { slug: string }[]\n },\n\n /**\n * List all content types for this tenant.\n */\n async getContentTypes(): Promise<ContentType[]> {\n const tenantId = await getTenantId()\n\n const { data, error } = await client\n .from(\"content_types\")\n .select(\"*\")\n .eq(\"tenant_id\", tenantId)\n .order(\"name\")\n\n if (error) {\n throw new Error(`Failed to fetch content types: ${error.message}`)\n }\n\n return (data ?? []) as ContentType[]\n },\n\n /**\n * Get all slug redirects for a content type.\n * Use in next.config.js redirects() or middleware for 301s.\n * Returns: [{ old_slug, new_slug }, ...]\n */\n async getRedirects(\n contentTypeSlug: string\n ): Promise<{ old_slug: string; new_slug: string }[]> {\n const contentTypeId = await getContentTypeId(contentTypeSlug)\n\n const { data, error } = await client\n .from(\"slug_redirects\")\n .select(\"old_slug, new_slug\")\n .eq(\"content_type_id\", contentTypeId)\n\n if (error) {\n throw new Error(\n `Failed to fetch redirects for ${contentTypeSlug}: ${error.message}`\n )\n }\n\n return (data ?? []) as { old_slug: string; new_slug: string }[]\n },\n\n /**\n * Get all custom path redirects for this tenant.\n * Use alongside getRedirects() in next.config.ts for full redirect coverage.\n */\n async getCustomRedirects(): Promise<\n { source_path: string; destination_path: string; permanent: boolean }[]\n > {\n const tenantId = await getTenantId()\n\n const { data, error } = await client\n .from(\"custom_redirects\")\n .select(\"source_path, destination_path, permanent\")\n .eq(\"tenant_id\", tenantId)\n\n if (error) {\n throw new Error(`Failed to fetch custom redirects: ${error.message}`)\n }\n\n return (data ?? []) as {\n source_path: string\n destination_path: string\n permanent: boolean\n }[]\n },\n\n /**\n * Get form submissions for a specific form.\n * Useful for displaying testimonials, reviews, etc.\n */\n async getFormSubmissions(\n formSlug: string,\n options: { status?: string; limit?: number; offset?: number } = {}\n ): Promise<Record<string, unknown>[]> {\n const tenantId = await getTenantId()\n\n const { data: form } = await client\n .from(\"forms\")\n .select(\"id\")\n .eq(\"tenant_id\", tenantId)\n .eq(\"slug\", formSlug)\n .single()\n\n if (!form) return []\n\n const { limit = 50, offset = 0, status } = options\n\n let query = client\n .from(\"form_submissions\")\n .select(\"*\")\n .eq(\"form_id\", form.id)\n .eq(\"is_spam\", false)\n .order(\"created_at\", { ascending: false })\n .range(offset, offset + limit - 1)\n\n if (status) {\n query = query.eq(\"status\", status)\n }\n\n const { data } = await query\n return (data ?? []) as Record<string, unknown>[]\n },\n\n /**\n * Get a form configuration by slug.\n */\n async getForm(\n formSlug: string\n ): Promise<Record<string, unknown> | null> {\n const tenantId = await getTenantId()\n\n const { data } = await client\n .from(\"forms\")\n .select(\"id, name, slug, description, is_active\")\n .eq(\"tenant_id\", tenantId)\n .eq(\"slug\", formSlug)\n .single()\n\n return data as Record<string, unknown> | null\n },\n\n /**\n * Get a flipbook by ID, including CDN-resolved page image URLs.\n * Returns null if not found or manifest not yet ready.\n */\n getFlipbook,\n\n /**\n * Get Google Reviews for this tenant.\n * Defaults to approved reviews ordered by most recent.\n */\n async getReviews(\n options: ReviewQueryOptions = {}\n ): Promise<GoogleReview[]> {\n const tenantId = await getTenantId()\n\n const {\n status = \"approved\",\n minRating,\n limit = 50,\n offset = 0,\n orderBy = \"review_timestamp\",\n orderDirection = \"desc\",\n } = options\n\n let query = client\n .from(\"google_reviews\")\n .select(\"id, author_name, author_photo_url, rating, text, review_timestamp\")\n .eq(\"tenant_id\", tenantId)\n\n if (status) {\n query = query.eq(\"status\", status)\n }\n\n if (minRating) {\n query = query.gte(\"rating\", minRating)\n }\n\n query = query\n .order(orderBy, { ascending: orderDirection === \"asc\" })\n .range(offset, offset + limit - 1)\n\n const { data, error } = await query\n\n if (error) {\n throw new Error(`Failed to fetch reviews: ${error.message}`)\n }\n\n return (data ?? []) as GoogleReview[]\n },\n\n /**\n * Get the tenant's third-party tracking IDs (Google Analytics, Google Tag Manager, Meta Pixel).\n * Each value is null when not configured. These IDs are public and safe to render in HTML.\n *\n * On Next.js, the result is cached and revalidated every hour by default\n * (`revalidate: 3600`) and tagged with `TRACKING_CONFIG_TAG`. Tenant sites\n * that want zero-lag updates can call `revalidateTag(TRACKING_CONFIG_TAG)`\n * from a webhook. Pass `revalidate: 0` to disable caching, `revalidate: false`\n * to cache indefinitely (tag-only invalidation), or any positive integer\n * (seconds) to override the interval. Outside Next.js the cache hints are\n * silently ignored — every call hits the network.\n */\n async getTrackingConfig(options: TrackingConfigOptions = {}): Promise<TrackingConfig> {\n const { revalidate = 3600, tags = [TRACKING_CONFIG_TAG] } = options\n const supabaseUrl = (supabase as unknown as { supabaseUrl: string }).supabaseUrl\n const supabaseKey = (supabase as unknown as { supabaseKey: string }).supabaseKey\n\n // Direct PostgREST fetch (instead of going through supabase-js) so that\n // Next.js sees the request and applies its `next` cache options. RLS\n // restricts the result to the tenant matching `x-cms-api-key`, so no\n // explicit tenant_id filter is needed.\n const url =\n `${supabaseUrl}/rest/v1/tenant_settings` +\n `?select=google_analytics_id,google_tag_manager_id,meta_pixel_id&limit=1`\n\n const init: RequestInit & { next?: { revalidate?: number | false; tags?: string[] } } = {\n headers: {\n apikey: supabaseKey,\n Authorization: `Bearer ${supabaseKey}`,\n \"x-cms-api-key\": apiKey,\n \"x-cms-client-version\": CMS_CLIENT_VERSION,\n Accept: \"application/json\",\n },\n next: { revalidate, tags },\n }\n\n let row: {\n google_analytics_id: string | null\n google_tag_manager_id: string | null\n meta_pixel_id: string | null\n } | undefined\n\n try {\n const res = await fetch(url, init as RequestInit)\n if (res.ok) {\n const rows = (await res.json()) as Array<typeof row>\n row = rows?.[0]\n }\n } catch {\n // Network errors fall through to the all-null result so a transient\n // outage on the CMS never breaks page renders on the tenant site.\n }\n\n return {\n googleAnalyticsId: nullify(row?.google_analytics_id),\n googleTagManagerId: nullify(row?.google_tag_manager_id),\n metaPixelId: nullify(row?.meta_pixel_id),\n }\n },\n }\n}\n\n/**\n * Cache tag applied to `getTrackingConfig()` fetches on Next.js. Call\n * `revalidateTag(TRACKING_CONFIG_TAG)` from a webhook handler on the tenant\n * site to make tracking-ID changes take effect immediately rather than\n * waiting for the next revalidation interval.\n */\nexport const TRACKING_CONFIG_TAG = \"cms:tracking-config\"\n\nexport interface TrackingConfigOptions {\n /**\n * Cache lifetime for the underlying fetch on Next.js, in seconds.\n * Defaults to 3600 (one hour). Set to `0` to disable caching, or `false`\n * to cache indefinitely until the tag is revalidated. Ignored outside\n * Next.js runtimes.\n */\n revalidate?: number | false\n /**\n * Cache tags for the underlying fetch on Next.js. Defaults to\n * `[TRACKING_CONFIG_TAG]`. Override to namespace by tenant if you share a\n * single Next.js process across multiple tenants (uncommon).\n */\n tags?: string[]\n}\n\nfunction nullify(v: string | null | undefined): string | null {\n if (!v) return null\n const trimmed = v.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nexport interface TrackingConfig {\n googleAnalyticsId: string | null\n googleTagManagerId: string | null\n metaPixelId: string | null\n}\n\n/**\n * Creates a new Supabase client that includes the `x-cms-api-key`\n * header in every request, using the same URL and key as the original.\n */\nfunction withApiKey(supabase: SupabaseClient, apiKey: string) {\n // Extract URL and key from the existing client\n const supabaseUrl = (supabase as unknown as { supabaseUrl: string }).supabaseUrl\n const supabaseKey = (supabase as unknown as { supabaseKey: string }).supabaseKey\n\n return createSupabaseClient(supabaseUrl, supabaseKey, {\n global: {\n headers: {\n \"x-cms-api-key\": apiKey,\n \"x-cms-client-version\": CMS_CLIENT_VERSION,\n },\n },\n })\n}\n","/**\n * The version of @distinctagency/cms-client embedded in this build.\n *\n * Bump this in lock-step with package.json — the value is read by the SDK to\n * report installed-version telemetry and to send `x-cms-client-version` on\n * outgoing requests.\n */\nexport const CMS_CLIENT_VERSION = \"1.14.0\"\n","import { CMS_CLIENT_VERSION } from \"./version\"\n\n/**\n * Reports the running SDK version to the CMS so the Super Admin UI can show\n * which client version each tenant site is on.\n *\n * Day-granular and process-local: at most one POST per (api key + day) per\n * Node process. Calls from browser code are ignored — the API key would be\n * exposed and the data is redundant with server-side reports.\n *\n * Fire-and-forget. Failures are silent — telemetry must never break a render.\n */\n\nconst DEFAULT_APP_URL = \"https://cms.distinctstudio.co.nz\"\n\ninterface ReportRecord {\n date: string // YYYY-MM-DD\n version: string\n}\n\nconst reported = new Map<string, ReportRecord>()\n\nfunction todayKey(): string {\n return new Date().toISOString().slice(0, 10)\n}\n\nexport function reportClientVersion(apiKey: string, appUrl?: string): void {\n // Skip in browser contexts — server-side calls already cover the tenant.\n if (typeof window !== \"undefined\") return\n if (!apiKey) return\n\n const today = todayKey()\n const last = reported.get(apiKey)\n if (last && last.date === today && last.version === CMS_CLIENT_VERSION) return\n\n // Optimistically mark as reported so concurrent calls don't all fire. If the\n // request fails we'll retry tomorrow — that's acceptable for telemetry.\n reported.set(apiKey, { date: today, version: CMS_CLIENT_VERSION })\n\n const base = (appUrl ?? DEFAULT_APP_URL).replace(/\\/$/, \"\")\n const url = `${base}/api/client-telemetry`\n\n // Fire and forget. Use a hand-rolled then() chain rather than async/await so\n // we don't accidentally surface an unhandled rejection.\n void fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-cms-api-key\": apiKey,\n \"x-cms-client-version\": CMS_CLIENT_VERSION,\n },\n body: JSON.stringify({\n api_key: apiKey,\n version: CMS_CLIENT_VERSION,\n }),\n }).catch(() => {\n // Allow a retry on the next call by clearing the optimistic record.\n reported.delete(apiKey)\n })\n}\n","import type { EmbedValue } from \"./types\"\n\nfunction toCssAspectRatio(ratio: string): string {\n const parts = ratio.split(\":\")\n if (parts.length !== 2) return \"16/9\"\n const w = parseFloat(parts[0])\n const h = parseFloat(parts[1])\n if (!w || !h || w <= 0 || h <= 0) return \"16/9\"\n return `${w}/${h}`\n}\n\n/**\n * Generate responsive iframe HTML for an embed field value.\n * Returns an empty string if the value is invalid or the URL is not HTTPS.\n */\nexport function getEmbedHtml(value: unknown): string {\n if (!value || typeof value !== \"object\") return \"\"\n const embed = value as EmbedValue\n if (!embed.url || !embed.url.startsWith(\"https://\")) return \"\"\n\n const width = embed.width || \"100%\"\n const aspectRatio = toCssAspectRatio(embed.aspect_ratio || \"16:9\")\n\n return `<div style=\"position:relative;width:${width};aspect-ratio:${aspectRatio}\"><iframe src=\"${embed.url}\" style=\"position:absolute;inset:0;width:100%;height:100%\" frameborder=\"0\" sandbox=\"allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox\" allowfullscreen loading=\"lazy\"></iframe></div>`\n}\n","import type { SupabaseClient } from \"@supabase/supabase-js\"\nimport type {\n Product,\n ProductCategory,\n ProductQueryOptions,\n CreateOrderParams,\n CreateOrderResult,\n} from \"./types\"\nimport { CMS_CLIENT_VERSION } from \"./version\"\nimport { reportClientVersion } from \"./telemetry\"\n\nconst DEFAULT_APP_URL = \"https://cms.distinctstudio.co.nz\"\n\nexport interface ShopClientOptions {\n /** Tenant API key (UUID). Sent as `x-cms-api-key` on every request. */\n apiKey: string\n /** CMS app base URL. Defaults to `https://cms.distinctstudio.co.nz`. */\n appUrl?: string\n}\n\n/**\n * Creates a typed client for the eCommerce REST API (products, categories, orders).\n *\n * Unlike `createCmsClient`, the shop client talks to the CMS REST endpoints\n * (`/api/products`, `/api/product-categories`, `/api/orders/create`) rather than\n * Supabase directly — it doesn't need the `supabase` argument, but accepts it\n * for API symmetry with the rest of the SDK.\n *\n * Usage:\n * ```ts\n * const shop = createShopClient(supabase, { apiKey: process.env.CMS_API_KEY! })\n * const products = await shop.getProducts({ category: \"electronics\", limit: 20 })\n * ```\n */\nexport function createShopClient(\n _supabase: SupabaseClient,\n options: ShopClientOptions\n) {\n const { apiKey } = options\n const appUrl = (options.appUrl ?? DEFAULT_APP_URL).replace(/\\/$/, \"\")\n\n // Day-granular, fire-and-forget version ping (server-side only).\n reportClientVersion(apiKey, appUrl)\n\n function authHeaders(): HeadersInit {\n return {\n \"Content-Type\": \"application/json\",\n \"x-cms-api-key\": apiKey,\n \"x-cms-client-version\": CMS_CLIENT_VERSION,\n }\n }\n\n async function getJson<T>(path: string): Promise<T> {\n const res = await fetch(`${appUrl}${path}`, {\n headers: authHeaders(),\n // Caller controls Next.js caching by wrapping the call site.\n })\n if (!res.ok) {\n const body = await res.json().catch(() => ({}) as { error?: string })\n throw new Error(\n body.error ?? `Shop request failed (${res.status}): ${path}`\n )\n }\n return res.json() as Promise<T>\n }\n\n return {\n /**\n * List published products. Includes nested `variants` and `options`.\n *\n * `category` filters by category slug (matches the FK `category_id`,\n * with a fallback to the legacy `category` text column).\n */\n async getProducts(queryOptions: ProductQueryOptions = {}): Promise<Product[]> {\n const params = new URLSearchParams()\n if (queryOptions.category) params.set(\"category\", queryOptions.category)\n if (queryOptions.tags?.length) params.set(\"tags\", queryOptions.tags.join(\",\"))\n if (queryOptions.limit) params.set(\"limit\", String(queryOptions.limit))\n if (queryOptions.offset) params.set(\"offset\", String(queryOptions.offset))\n if (queryOptions.sort) params.set(\"sort\", queryOptions.sort)\n if (queryOptions.order) params.set(\"order\", queryOptions.order)\n\n const qs = params.toString()\n const { products } = await getJson<{ products: Product[] }>(\n `/api/products${qs ? `?${qs}` : \"\"}`\n )\n return products ?? []\n },\n\n /**\n * Get a single published product by slug. Returns null if not found.\n * Includes nested `variants` and `options`.\n */\n async getProductBySlug(slug: string): Promise<Product | null> {\n const res = await fetch(`${appUrl}/api/products/${encodeURIComponent(slug)}`, {\n headers: authHeaders(),\n })\n if (res.status === 404) return null\n if (!res.ok) {\n const body = await res.json().catch(() => ({}) as { error?: string })\n throw new Error(body.error ?? `Failed to fetch product ${slug}`)\n }\n const { product } = (await res.json()) as { product: Product }\n return product ?? null\n },\n\n /**\n * List product categories for the tenant, ordered by sort_order then name.\n */\n async getCategories(): Promise<ProductCategory[]> {\n const { categories } = await getJson<{ categories: ProductCategory[] }>(\n `/api/product-categories`\n )\n return categories ?? []\n },\n\n /**\n * Get a single category by slug. Returns null if not found.\n */\n async getCategoryBySlug(slug: string): Promise<ProductCategory | null> {\n const all = await this.getCategories()\n return all.find((c) => c.slug === slug) ?? null\n },\n\n /**\n * Create a pending order and return a Stripe PaymentIntent client_secret\n * to confirm payment client-side.\n *\n * Stripe must be configured for the tenant; eCommerce must be enabled on\n * their billing plan. Validates stock for tracked variants and applies\n * any discount code before creating the PaymentIntent.\n */\n async createOrder(params: CreateOrderParams): Promise<CreateOrderResult> {\n const res = await fetch(`${appUrl}/api/orders/create`, {\n method: \"POST\",\n headers: authHeaders(),\n body: JSON.stringify({ api_key: apiKey, ...params }),\n })\n if (!res.ok) {\n const body = await res.json().catch(() => ({}) as { error?: string })\n throw new Error(body.error ?? \"Order creation failed\")\n }\n return res.json() as Promise<CreateOrderResult>\n },\n }\n}\n","import type { SupabaseClient } from \"@supabase/supabase-js\"\nimport { reportClientVersion } from \"./telemetry\"\n\nexport type EventStatus = \"draft\" | \"published\" | \"archived\"\nexport type BookingStatus = \"pending\" | \"confirmed\" | \"cancelled\" | \"refunded\"\n\nexport interface CmsEvent {\n id: string\n tenant_id: string\n title: string\n slug: string\n description: string | null\n short_description: string | null\n status: EventStatus\n start_at: string\n end_at: string | null\n venue_name: string | null\n venue_address: string | null\n hero_image: string | null\n gallery: string[]\n tags: string[]\n seo_title: string | null\n seo_description: string | null\n metadata: Record<string, unknown>\n sort_order: number\n published_at: string | null\n created_at: string\n updated_at: string\n}\n\nexport interface CmsTicketTier {\n id: string\n event_id: string\n tenant_id: string\n name: string\n description: string | null\n price_cents: number\n capacity: number | null\n sold_count: number\n sales_start_at: string | null\n sales_end_at: string | null\n is_active: boolean\n sort_order: number\n}\n\nexport interface EventWithTiers extends CmsEvent {\n tiers: CmsTicketTier[]\n}\n\nexport interface EventQueryOptions {\n /** Only include events with start_at >= now. Default false. */\n upcomingOnly?: boolean\n tag?: string\n limit?: number\n offset?: number\n sort?: \"start_at\" | \"sort_order\" | \"created_at\"\n order?: \"asc\" | \"desc\"\n}\n\nexport interface CreateBookingParams {\n event_slug: string\n ticket_tier_id?: string\n customer_email: string\n customer_name?: string\n customer_phone?: string\n quantity?: number\n success_url?: string\n cancel_url?: string\n metadata?: Record<string, unknown>\n}\n\nexport interface CreateBookingResult {\n booking_id: string\n booking_number: string\n status: \"pending\" | \"confirmed\"\n /** Redirect the browser here for paid bookings. */\n checkout_url?: string\n /** Present on free bookings — used for attendance QR display. */\n qr_token?: string\n}\n\nexport function createEventsClient(\n supabase: SupabaseClient,\n options: { apiKey: string; appUrl?: string }\n) {\n const { apiKey, appUrl } = options\n\n // Day-granular, fire-and-forget version ping (server-side only).\n reportClientVersion(apiKey, appUrl)\n\n async function getEvents(\n queryOptions?: EventQueryOptions\n ): Promise<EventWithTiers[]> {\n let query = supabase\n .from(\"events\")\n .select(\"*, tiers:ticket_tiers(*)\")\n .eq(\"status\", \"published\")\n .order(queryOptions?.sort ?? \"start_at\", {\n ascending: (queryOptions?.order ?? \"asc\") === \"asc\",\n })\n\n if (queryOptions?.upcomingOnly) {\n query = query.gte(\"start_at\", new Date().toISOString())\n }\n if (queryOptions?.tag) {\n query = query.contains(\"tags\", [queryOptions.tag])\n }\n if (queryOptions?.limit) {\n query = query.limit(queryOptions.limit)\n }\n if (queryOptions?.offset) {\n query = query.range(\n queryOptions.offset,\n queryOptions.offset + (queryOptions.limit ?? 50) - 1\n )\n }\n\n const { data } = await query\n return (data ?? []) as EventWithTiers[]\n }\n\n async function getEventBySlug(slug: string): Promise<EventWithTiers | null> {\n const { data } = await supabase\n .from(\"events\")\n .select(\"*, tiers:ticket_tiers(*)\")\n .eq(\"slug\", slug)\n .eq(\"status\", \"published\")\n .single()\n return (data as EventWithTiers) ?? null\n }\n\n async function createBooking(\n params: CreateBookingParams\n ): Promise<CreateBookingResult> {\n const baseUrl = appUrl ?? \"\"\n const res = await fetch(`${baseUrl}/api/bookings/create`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ api_key: apiKey, ...params }),\n })\n if (!res.ok) {\n const err = await res\n .json()\n .catch(() => ({ error: \"Booking creation failed\" }))\n throw new Error(err.error ?? \"Booking creation failed\")\n }\n return res.json() as Promise<CreateBookingResult>\n }\n\n return { getEvents, getEventBySlug, createBooking }\n}\n","/**\n * Image helpers for CMS content.\n *\n * Images are already optimised (WebP, max 2400px) on upload.\n * No server-side transforms needed — serve originals directly.\n *\n * These functions are kept for backward compatibility but no longer\n * call Supabase image transformation endpoints.\n */\n\nexport interface ImageTransformOptions {\n width?: number\n height?: number\n quality?: number\n resize?: \"contain\" | \"cover\" | \"fill\"\n format?: \"origin\"\n}\n\n/**\n * Returns the image URL directly.\n *\n * Previously this converted URLs to use Supabase's /render/image/\n * transform endpoint, but images are now pre-optimised on upload\n * (WebP, max 2400px, quality 82) so transforms are unnecessary.\n *\n * The function is kept for backward compatibility — existing code\n * that calls getTransformUrl() will continue to work without changes.\n */\nexport function getTransformUrl(\n originalUrl: string,\n _options: ImageTransformOptions = {}\n): string {\n return originalUrl\n}\n\n/**\n * Returns a simple srcSet using the original image.\n *\n * Previously generated multiple transformed widths, but since images\n * are now pre-optimised WebP, a single source is sufficient.\n * Modern browsers handle responsive display efficiently with a\n * well-sized WebP source.\n */\nexport function getSrcSet(\n originalUrl: string,\n _widths: number[] = [],\n _quality = 80\n): string {\n return `${originalUrl} 2400w`\n}\n\n/**\n * Common image size presets — kept for backward compatibility.\n * Since images are pre-optimised, these are informational only.\n */\nexport const IMAGE_PRESETS = {\n thumbnail: { width: 150, height: 150, resize: \"cover\" as const, quality: 70 },\n card: { width: 400, height: 300, resize: \"cover\" as const, quality: 80 },\n hero: { width: 1200, height: 630, resize: \"cover\" as const, quality: 85 },\n og: { width: 1200, height: 630, resize: \"cover\" as const, quality: 90 },\n avatar: { width: 80, height: 80, resize: \"cover\" as const, quality: 75 },\n full: { width: 1920, resize: \"contain\" as const, quality: 85 },\n} as const\n"],"mappings":"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","names":["options","query","data","error","createSupabaseClient","DEFAULT_APP_URL"]}
package/dist/index.mjs CHANGED
@@ -1,8 +1,45 @@
1
1
  // src/queries.ts
2
2
  import { createClient as createSupabaseClient } from "@supabase/supabase-js";
3
+
4
+ // src/version.ts
5
+ var CMS_CLIENT_VERSION = "1.14.0";
6
+
7
+ // src/telemetry.ts
8
+ var DEFAULT_APP_URL = "https://cms.distinctstudio.co.nz";
9
+ var reported = /* @__PURE__ */ new Map();
10
+ function todayKey() {
11
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
12
+ }
13
+ function reportClientVersion(apiKey, appUrl) {
14
+ if (typeof window !== "undefined") return;
15
+ if (!apiKey) return;
16
+ const today = todayKey();
17
+ const last = reported.get(apiKey);
18
+ if (last && last.date === today && last.version === CMS_CLIENT_VERSION) return;
19
+ reported.set(apiKey, { date: today, version: CMS_CLIENT_VERSION });
20
+ const base = (appUrl ?? DEFAULT_APP_URL).replace(/\/$/, "");
21
+ const url = `${base}/api/client-telemetry`;
22
+ void fetch(url, {
23
+ method: "POST",
24
+ headers: {
25
+ "Content-Type": "application/json",
26
+ "x-cms-api-key": apiKey,
27
+ "x-cms-client-version": CMS_CLIENT_VERSION
28
+ },
29
+ body: JSON.stringify({
30
+ api_key: apiKey,
31
+ version: CMS_CLIENT_VERSION
32
+ })
33
+ }).catch(() => {
34
+ reported.delete(apiKey);
35
+ });
36
+ }
37
+
38
+ // src/queries.ts
3
39
  function createCmsClient(supabase, options) {
4
40
  const { apiKey } = options;
5
41
  const client = withApiKey(supabase, apiKey);
42
+ reportClientVersion(apiKey, options.appUrl);
6
43
  let tenantIdCache = null;
7
44
  async function getTenantId() {
8
45
  if (tenantIdCache) return tenantIdCache;
@@ -278,6 +315,7 @@ function createCmsClient(supabase, options) {
278
315
  apikey: supabaseKey,
279
316
  Authorization: `Bearer ${supabaseKey}`,
280
317
  "x-cms-api-key": apiKey,
318
+ "x-cms-client-version": CMS_CLIENT_VERSION,
281
319
  Accept: "application/json"
282
320
  },
283
321
  next: { revalidate, tags }
@@ -311,7 +349,8 @@ function withApiKey(supabase, apiKey) {
311
349
  return createSupabaseClient(supabaseUrl, supabaseKey, {
312
350
  global: {
313
351
  headers: {
314
- "x-cms-api-key": apiKey
352
+ "x-cms-api-key": apiKey,
353
+ "x-cms-client-version": CMS_CLIENT_VERSION
315
354
  }
316
355
  }
317
356
  });
@@ -336,45 +375,101 @@ function getEmbedHtml(value) {
336
375
  }
337
376
 
338
377
  // src/shop.ts
339
- function createShopClient(supabase, options) {
340
- const { apiKey, appUrl } = options;
378
+ var DEFAULT_APP_URL2 = "https://cms.distinctstudio.co.nz";
379
+ function createShopClient(_supabase, options) {
380
+ const { apiKey } = options;
381
+ const appUrl = (options.appUrl ?? DEFAULT_APP_URL2).replace(/\/$/, "");
382
+ reportClientVersion(apiKey, appUrl);
383
+ function authHeaders() {
384
+ return {
385
+ "Content-Type": "application/json",
386
+ "x-cms-api-key": apiKey,
387
+ "x-cms-client-version": CMS_CLIENT_VERSION
388
+ };
389
+ }
390
+ async function getJson(path) {
391
+ const res = await fetch(`${appUrl}${path}`, {
392
+ headers: authHeaders()
393
+ // Caller controls Next.js caching by wrapping the call site.
394
+ });
395
+ if (!res.ok) {
396
+ const body = await res.json().catch(() => ({}));
397
+ throw new Error(
398
+ body.error ?? `Shop request failed (${res.status}): ${path}`
399
+ );
400
+ }
401
+ return res.json();
402
+ }
341
403
  return {
342
- async getProducts(queryOptions) {
343
- let query = supabase.from("products").select("*, variants:product_variants(*), options:product_options(*)").eq("status", "published").order(queryOptions?.sort ?? "sort_order", { ascending: (queryOptions?.order ?? "asc") === "asc" });
344
- if (queryOptions?.category) {
345
- query = query.eq("category", queryOptions.category);
346
- }
347
- if (queryOptions?.tags?.length) {
348
- query = query.overlaps("tags", queryOptions.tags);
349
- }
350
- if (queryOptions?.limit) {
351
- query = query.limit(queryOptions.limit);
352
- }
353
- if (queryOptions?.offset) {
354
- query = query.range(queryOptions.offset, queryOptions.offset + (queryOptions.limit ?? 50) - 1);
355
- }
356
- const { data } = await query;
357
- return data ?? [];
404
+ /**
405
+ * List published products. Includes nested `variants` and `options`.
406
+ *
407
+ * `category` filters by category slug (matches the FK `category_id`,
408
+ * with a fallback to the legacy `category` text column).
409
+ */
410
+ async getProducts(queryOptions = {}) {
411
+ const params = new URLSearchParams();
412
+ if (queryOptions.category) params.set("category", queryOptions.category);
413
+ if (queryOptions.tags?.length) params.set("tags", queryOptions.tags.join(","));
414
+ if (queryOptions.limit) params.set("limit", String(queryOptions.limit));
415
+ if (queryOptions.offset) params.set("offset", String(queryOptions.offset));
416
+ if (queryOptions.sort) params.set("sort", queryOptions.sort);
417
+ if (queryOptions.order) params.set("order", queryOptions.order);
418
+ const qs = params.toString();
419
+ const { products } = await getJson(
420
+ `/api/products${qs ? `?${qs}` : ""}`
421
+ );
422
+ return products ?? [];
358
423
  },
424
+ /**
425
+ * Get a single published product by slug. Returns null if not found.
426
+ * Includes nested `variants` and `options`.
427
+ */
359
428
  async getProductBySlug(slug) {
360
- const { data } = await supabase.from("products").select("*, variants:product_variants(*), options:product_options(*)").eq("slug", slug).eq("status", "published").single();
361
- return data ?? null;
429
+ const res = await fetch(`${appUrl}/api/products/${encodeURIComponent(slug)}`, {
430
+ headers: authHeaders()
431
+ });
432
+ if (res.status === 404) return null;
433
+ if (!res.ok) {
434
+ const body = await res.json().catch(() => ({}));
435
+ throw new Error(body.error ?? `Failed to fetch product ${slug}`);
436
+ }
437
+ const { product } = await res.json();
438
+ return product ?? null;
362
439
  },
363
- async getProductCategories() {
364
- const { data } = await supabase.from("products").select("category").eq("status", "published").not("category", "is", null);
365
- const categories = [...new Set((data ?? []).map((d) => d.category))];
366
- return categories.sort();
440
+ /**
441
+ * List product categories for the tenant, ordered by sort_order then name.
442
+ */
443
+ async getCategories() {
444
+ const { categories } = await getJson(
445
+ `/api/product-categories`
446
+ );
447
+ return categories ?? [];
367
448
  },
449
+ /**
450
+ * Get a single category by slug. Returns null if not found.
451
+ */
452
+ async getCategoryBySlug(slug) {
453
+ const all = await this.getCategories();
454
+ return all.find((c) => c.slug === slug) ?? null;
455
+ },
456
+ /**
457
+ * Create a pending order and return a Stripe PaymentIntent client_secret
458
+ * to confirm payment client-side.
459
+ *
460
+ * Stripe must be configured for the tenant; eCommerce must be enabled on
461
+ * their billing plan. Validates stock for tracked variants and applies
462
+ * any discount code before creating the PaymentIntent.
463
+ */
368
464
  async createOrder(params) {
369
- const baseUrl = appUrl ?? "";
370
- const res = await fetch(`${baseUrl}/api/orders/create`, {
465
+ const res = await fetch(`${appUrl}/api/orders/create`, {
371
466
  method: "POST",
372
- headers: { "Content-Type": "application/json" },
467
+ headers: authHeaders(),
373
468
  body: JSON.stringify({ api_key: apiKey, ...params })
374
469
  });
375
470
  if (!res.ok) {
376
- const err = await res.json().catch(() => ({ error: "Order creation failed" }));
377
- throw new Error(err.error ?? "Order creation failed");
471
+ const body = await res.json().catch(() => ({}));
472
+ throw new Error(body.error ?? "Order creation failed");
378
473
  }
379
474
  return res.json();
380
475
  }
@@ -384,6 +479,7 @@ function createShopClient(supabase, options) {
384
479
  // src/events.ts
385
480
  function createEventsClient(supabase, options) {
386
481
  const { apiKey, appUrl } = options;
482
+ reportClientVersion(apiKey, appUrl);
387
483
  async function getEvents(queryOptions) {
388
484
  let query = supabase.from("events").select("*, tiers:ticket_tiers(*)").eq("status", "published").order(queryOptions?.sort ?? "start_at", {
389
485
  ascending: (queryOptions?.order ?? "asc") === "asc"
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/queries.ts","../src/embed.ts","../src/shop.ts","../src/events.ts","../src/cdn.ts"],"sourcesContent":["import { createClient as createSupabaseClient } from \"@supabase/supabase-js\"\nimport type { SupabaseClient } from \"@supabase/supabase-js\"\nimport type {\n CmsClientOptions,\n ContentItem,\n ContentQueryOptions,\n ContentType,\n GoogleReview,\n ReviewQueryOptions,\n} from \"./types\"\n\n/**\n * Creates a CMS query client authenticated with a tenant API key.\n *\n * Usage:\n * ```ts\n * const cms = createCmsClient(supabase, { apiKey: process.env.CMS_API_KEY! })\n * const events = await cms.getContentItems('events', { status: 'published' })\n * ```\n *\n * The API key is sent as an `x-cms-api-key` header on every request.\n * Supabase RLS policies validate it against the tenant's stored key.\n */\nexport function createCmsClient(\n supabase: SupabaseClient,\n options: CmsClientOptions\n) {\n const { apiKey } = options\n\n // Create a new Supabase client with the API key header injected globally\n const client = withApiKey(supabase, apiKey) as SupabaseClient\n\n /** Resolve the tenant ID from the API key (cached per request) */\n let tenantIdCache: string | null = null\n\n async function getTenantId(): Promise<string> {\n if (tenantIdCache) return tenantIdCache\n\n const { data, error } = await client\n .from(\"tenants\")\n .select(\"id\")\n .single()\n\n if (error || !data) {\n throw new Error(\n \"Invalid CMS API key — no tenant found. Check your apiKey.\"\n )\n }\n\n tenantIdCache = data.id\n return data.id\n }\n\n /** Resolve a content type from its slug (cached) */\n const contentTypeCache = new Map<string, ContentType>()\n\n async function getContentTypeBySlug(contentTypeSlug: string): Promise<ContentType> {\n if (contentTypeCache.has(contentTypeSlug)) return contentTypeCache.get(contentTypeSlug)!\n const tenantId = await getTenantId()\n\n const { data, error } = await client\n .from(\"content_types\")\n .select(\"*\")\n .eq(\"tenant_id\", tenantId)\n .eq(\"slug\", contentTypeSlug)\n .single()\n\n if (error || !data) {\n throw new Error(`Content type not found: ${contentTypeSlug}`)\n }\n\n const ct = data as ContentType\n contentTypeCache.set(contentTypeSlug, ct)\n return ct\n }\n\n async function getContentTypeId(contentTypeSlug: string): Promise<string> {\n const ct = await getContentTypeBySlug(contentTypeSlug)\n return ct.id\n }\n\n /** Check if a member token has access to a gated content type */\n async function checkMemberAccess(\n contentType: ContentType,\n memberToken?: string\n ): Promise<{ allowed: boolean; memberTierId: string | null }> {\n const requiredTierId = contentType.required_membership_tier_id\n if (!requiredTierId) return { allowed: true, memberTierId: null } // Public\n\n if (!memberToken) return { allowed: false, memberTierId: null } // Gated but no token\n\n // Verify the member's token and check their tier\n const { data: session } = await client\n .from(\"member_sessions\")\n .select(\"member_id\")\n .eq(\"token_hash\", memberToken) // Note: caller should hash the token\n .single()\n\n if (!session) return { allowed: false, memberTierId: null }\n\n const { data: member } = await client\n .from(\"members\")\n .select(\"membership_tier_id, status\")\n .eq(\"id\", session.member_id)\n .single()\n\n if (!member || member.status !== \"active\") return { allowed: false, memberTierId: null }\n\n // Check if the member's tier matches (or exceeds) the required tier\n // For now: exact match or the member has the required tier\n return {\n allowed: member.membership_tier_id === requiredTierId,\n memberTierId: member.membership_tier_id as string | null,\n }\n }\n\n async function getFlipbook(id: string): Promise<import(\"./types\").FlipbookPublic | null> {\n const { data, error } = await client\n .from(\"flipbooks\")\n .select(\"id, title, page_count, status, manifest, download_enabled, tenant_id\")\n .eq(\"id\", id)\n .single()\n if (error || !data) return null\n\n const manifest = data.manifest as\n | { pages: Array<{ image: string; thumb: string; w: number; h: number }>; toc: Array<{ title: string; page: number }> }\n | null\n if (!manifest) {\n return {\n id: data.id as string,\n title: data.title as string,\n page_count: (data.page_count as number) ?? 0,\n status: data.status as \"pending_upload\" | \"pending\" | \"processing\" | \"ready\" | \"failed\",\n page_images: [],\n toc: [],\n download_url: null,\n }\n }\n\n // Browser-safe env lookup via globalThis. The client package builds without\n // @types/node so referring to `process` directly fails the dts build; reading\n // through globalThis sidesteps that and works in every JS environment that\n // matters (Node, browsers, edge runtimes).\n const proc = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process\n const cdnBase = proc?.env?.NEXT_PUBLIC_R2_PUBLIC_URL ?? \"https://cdn.distinctstudio.co.nz\"\n const prefix = `flipbooks/${data.tenant_id as string}/${data.id as string}/`\n return {\n id: data.id as string,\n title: data.title as string,\n page_count: (data.page_count as number) ?? manifest.pages.length,\n status: data.status as \"pending\" | \"processing\" | \"ready\" | \"failed\",\n page_images: manifest.pages.map((p) => ({\n url: `${cdnBase}/${prefix}${p.image}`,\n thumb_url: `${cdnBase}/${prefix}${p.thumb}`,\n w: p.w,\n h: p.h,\n })),\n toc: manifest.toc,\n download_url: data.download_enabled\n ? `${options.appUrl ?? \"\"}/api/flipbooks/${data.id as string}/download`\n : null,\n }\n }\n\n return {\n /**\n * List content items for a content type.\n * Defaults to published items ordered by most recent.\n */\n async getContentItems(\n contentTypeSlug: string,\n options: ContentQueryOptions = {}\n ): Promise<(ContentItem & { locked?: boolean })[]> {\n const contentType = await getContentTypeBySlug(contentTypeSlug)\n\n const {\n status = \"published\",\n orderBy = \"published_at\",\n orderDirection = \"desc\",\n limit = 100,\n offset = 0,\n memberToken,\n } = options\n\n // Check membership access\n const access = await checkMemberAccess(contentType, memberToken)\n\n // If gated and no access, check gating mode\n if (!access.allowed && contentType.required_membership_tier_id) {\n // Get tenant settings for gating mode\n const tenantId = await getTenantId()\n const { data: settings } = await client\n .from(\"tenant_settings\")\n .select(\"membership_gating_mode\")\n .eq(\"tenant_id\", tenantId)\n .single()\n\n const mode = (settings?.membership_gating_mode as string) ?? \"teaser\"\n\n if (mode === \"hide\") {\n return [] // Hide: return nothing\n }\n\n // Teaser mode: return items with locked flag, no body/data\n let query = client\n .from(\"content_items\")\n .select(\"id, title, slug, status, excerpt, seo_title, seo_description, featured_image, published_at, created_at, updated_at\")\n .eq(\"content_type_id\", contentType.id)\n\n if (status) query = query.eq(\"status\", status)\n query = query\n .order(orderBy, { ascending: orderDirection === \"asc\" })\n .range(offset, offset + limit - 1)\n\n const { data, error } = await query\n if (error) throw new Error(`Failed to fetch ${contentTypeSlug}: ${error.message}`)\n\n return (data ?? []).map((item) => ({\n ...item,\n data: {},\n locked: true,\n })) as (ContentItem & { locked: boolean })[]\n }\n\n // Full access\n let query = client\n .from(\"content_items\")\n .select(\"*\")\n .eq(\"content_type_id\", contentType.id)\n\n if (status) {\n query = query.eq(\"status\", status)\n }\n\n query = query\n .order(orderBy, { ascending: orderDirection === \"asc\" })\n .range(offset, offset + limit - 1)\n\n const { data, error } = await query\n\n if (error) {\n throw new Error(`Failed to fetch ${contentTypeSlug}: ${error.message}`)\n }\n\n return (data ?? []) as ContentItem[]\n },\n\n /**\n * Get a single content item by its slug.\n * Returns null if not found.\n */\n async getContentItemBySlug(\n contentTypeSlug: string,\n itemSlug: string\n ): Promise<ContentItem | null> {\n const contentTypeId = await getContentTypeId(contentTypeSlug)\n\n const { data, error } = await client\n .from(\"content_items\")\n .select(\"*\")\n .eq(\"content_type_id\", contentTypeId)\n .eq(\"slug\", itemSlug)\n .single()\n\n if (error) {\n if (error.code === \"PGRST116\") return null // not found\n throw new Error(\n `Failed to fetch ${contentTypeSlug}/${itemSlug}: ${error.message}`\n )\n }\n\n return data as ContentItem\n },\n\n /**\n * Get a content type definition (including its field schema).\n */\n async getContentType(contentTypeSlug: string): Promise<ContentType> {\n const tenantId = await getTenantId()\n\n const { data, error } = await client\n .from(\"content_types\")\n .select(\"*\")\n .eq(\"tenant_id\", tenantId)\n .eq(\"slug\", contentTypeSlug)\n .single()\n\n if (error || !data) {\n throw new Error(`Content type not found: ${contentTypeSlug}`)\n }\n\n return data as ContentType\n },\n\n /**\n * Get all slugs for a content type (for generateStaticParams).\n */\n async getAllSlugs(\n contentTypeSlug: string\n ): Promise<{ slug: string }[]> {\n const contentTypeId = await getContentTypeId(contentTypeSlug)\n\n const { data, error } = await client\n .from(\"content_items\")\n .select(\"slug\")\n .eq(\"content_type_id\", contentTypeId)\n .eq(\"status\", \"published\")\n\n if (error) {\n throw new Error(\n `Failed to fetch slugs for ${contentTypeSlug}: ${error.message}`\n )\n }\n\n return (data ?? []) as { slug: string }[]\n },\n\n /**\n * List all content types for this tenant.\n */\n async getContentTypes(): Promise<ContentType[]> {\n const tenantId = await getTenantId()\n\n const { data, error } = await client\n .from(\"content_types\")\n .select(\"*\")\n .eq(\"tenant_id\", tenantId)\n .order(\"name\")\n\n if (error) {\n throw new Error(`Failed to fetch content types: ${error.message}`)\n }\n\n return (data ?? []) as ContentType[]\n },\n\n /**\n * Get all slug redirects for a content type.\n * Use in next.config.js redirects() or middleware for 301s.\n * Returns: [{ old_slug, new_slug }, ...]\n */\n async getRedirects(\n contentTypeSlug: string\n ): Promise<{ old_slug: string; new_slug: string }[]> {\n const contentTypeId = await getContentTypeId(contentTypeSlug)\n\n const { data, error } = await client\n .from(\"slug_redirects\")\n .select(\"old_slug, new_slug\")\n .eq(\"content_type_id\", contentTypeId)\n\n if (error) {\n throw new Error(\n `Failed to fetch redirects for ${contentTypeSlug}: ${error.message}`\n )\n }\n\n return (data ?? []) as { old_slug: string; new_slug: string }[]\n },\n\n /**\n * Get all custom path redirects for this tenant.\n * Use alongside getRedirects() in next.config.ts for full redirect coverage.\n */\n async getCustomRedirects(): Promise<\n { source_path: string; destination_path: string; permanent: boolean }[]\n > {\n const tenantId = await getTenantId()\n\n const { data, error } = await client\n .from(\"custom_redirects\")\n .select(\"source_path, destination_path, permanent\")\n .eq(\"tenant_id\", tenantId)\n\n if (error) {\n throw new Error(`Failed to fetch custom redirects: ${error.message}`)\n }\n\n return (data ?? []) as {\n source_path: string\n destination_path: string\n permanent: boolean\n }[]\n },\n\n /**\n * Get form submissions for a specific form.\n * Useful for displaying testimonials, reviews, etc.\n */\n async getFormSubmissions(\n formSlug: string,\n options: { status?: string; limit?: number; offset?: number } = {}\n ): Promise<Record<string, unknown>[]> {\n const tenantId = await getTenantId()\n\n const { data: form } = await client\n .from(\"forms\")\n .select(\"id\")\n .eq(\"tenant_id\", tenantId)\n .eq(\"slug\", formSlug)\n .single()\n\n if (!form) return []\n\n const { limit = 50, offset = 0, status } = options\n\n let query = client\n .from(\"form_submissions\")\n .select(\"*\")\n .eq(\"form_id\", form.id)\n .eq(\"is_spam\", false)\n .order(\"created_at\", { ascending: false })\n .range(offset, offset + limit - 1)\n\n if (status) {\n query = query.eq(\"status\", status)\n }\n\n const { data } = await query\n return (data ?? []) as Record<string, unknown>[]\n },\n\n /**\n * Get a form configuration by slug.\n */\n async getForm(\n formSlug: string\n ): Promise<Record<string, unknown> | null> {\n const tenantId = await getTenantId()\n\n const { data } = await client\n .from(\"forms\")\n .select(\"id, name, slug, description, is_active\")\n .eq(\"tenant_id\", tenantId)\n .eq(\"slug\", formSlug)\n .single()\n\n return data as Record<string, unknown> | null\n },\n\n /**\n * Get a flipbook by ID, including CDN-resolved page image URLs.\n * Returns null if not found or manifest not yet ready.\n */\n getFlipbook,\n\n /**\n * Get Google Reviews for this tenant.\n * Defaults to approved reviews ordered by most recent.\n */\n async getReviews(\n options: ReviewQueryOptions = {}\n ): Promise<GoogleReview[]> {\n const tenantId = await getTenantId()\n\n const {\n status = \"approved\",\n minRating,\n limit = 50,\n offset = 0,\n orderBy = \"review_timestamp\",\n orderDirection = \"desc\",\n } = options\n\n let query = client\n .from(\"google_reviews\")\n .select(\"id, author_name, author_photo_url, rating, text, review_timestamp\")\n .eq(\"tenant_id\", tenantId)\n\n if (status) {\n query = query.eq(\"status\", status)\n }\n\n if (minRating) {\n query = query.gte(\"rating\", minRating)\n }\n\n query = query\n .order(orderBy, { ascending: orderDirection === \"asc\" })\n .range(offset, offset + limit - 1)\n\n const { data, error } = await query\n\n if (error) {\n throw new Error(`Failed to fetch reviews: ${error.message}`)\n }\n\n return (data ?? []) as GoogleReview[]\n },\n\n /**\n * Get the tenant's third-party tracking IDs (Google Analytics, Google Tag Manager, Meta Pixel).\n * Each value is null when not configured. These IDs are public and safe to render in HTML.\n *\n * On Next.js, the result is cached and revalidated every hour by default\n * (`revalidate: 3600`) and tagged with `TRACKING_CONFIG_TAG`. Tenant sites\n * that want zero-lag updates can call `revalidateTag(TRACKING_CONFIG_TAG)`\n * from a webhook. Pass `revalidate: 0` to disable caching, `revalidate: false`\n * to cache indefinitely (tag-only invalidation), or any positive integer\n * (seconds) to override the interval. Outside Next.js the cache hints are\n * silently ignored — every call hits the network.\n */\n async getTrackingConfig(options: TrackingConfigOptions = {}): Promise<TrackingConfig> {\n const { revalidate = 3600, tags = [TRACKING_CONFIG_TAG] } = options\n const supabaseUrl = (supabase as unknown as { supabaseUrl: string }).supabaseUrl\n const supabaseKey = (supabase as unknown as { supabaseKey: string }).supabaseKey\n\n // Direct PostgREST fetch (instead of going through supabase-js) so that\n // Next.js sees the request and applies its `next` cache options. RLS\n // restricts the result to the tenant matching `x-cms-api-key`, so no\n // explicit tenant_id filter is needed.\n const url =\n `${supabaseUrl}/rest/v1/tenant_settings` +\n `?select=google_analytics_id,google_tag_manager_id,meta_pixel_id&limit=1`\n\n const init: RequestInit & { next?: { revalidate?: number | false; tags?: string[] } } = {\n headers: {\n apikey: supabaseKey,\n Authorization: `Bearer ${supabaseKey}`,\n \"x-cms-api-key\": apiKey,\n Accept: \"application/json\",\n },\n next: { revalidate, tags },\n }\n\n let row: {\n google_analytics_id: string | null\n google_tag_manager_id: string | null\n meta_pixel_id: string | null\n } | undefined\n\n try {\n const res = await fetch(url, init as RequestInit)\n if (res.ok) {\n const rows = (await res.json()) as Array<typeof row>\n row = rows?.[0]\n }\n } catch {\n // Network errors fall through to the all-null result so a transient\n // outage on the CMS never breaks page renders on the tenant site.\n }\n\n return {\n googleAnalyticsId: nullify(row?.google_analytics_id),\n googleTagManagerId: nullify(row?.google_tag_manager_id),\n metaPixelId: nullify(row?.meta_pixel_id),\n }\n },\n }\n}\n\n/**\n * Cache tag applied to `getTrackingConfig()` fetches on Next.js. Call\n * `revalidateTag(TRACKING_CONFIG_TAG)` from a webhook handler on the tenant\n * site to make tracking-ID changes take effect immediately rather than\n * waiting for the next revalidation interval.\n */\nexport const TRACKING_CONFIG_TAG = \"cms:tracking-config\"\n\nexport interface TrackingConfigOptions {\n /**\n * Cache lifetime for the underlying fetch on Next.js, in seconds.\n * Defaults to 3600 (one hour). Set to `0` to disable caching, or `false`\n * to cache indefinitely until the tag is revalidated. Ignored outside\n * Next.js runtimes.\n */\n revalidate?: number | false\n /**\n * Cache tags for the underlying fetch on Next.js. Defaults to\n * `[TRACKING_CONFIG_TAG]`. Override to namespace by tenant if you share a\n * single Next.js process across multiple tenants (uncommon).\n */\n tags?: string[]\n}\n\nfunction nullify(v: string | null | undefined): string | null {\n if (!v) return null\n const trimmed = v.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nexport interface TrackingConfig {\n googleAnalyticsId: string | null\n googleTagManagerId: string | null\n metaPixelId: string | null\n}\n\n/**\n * Creates a new Supabase client that includes the `x-cms-api-key`\n * header in every request, using the same URL and key as the original.\n */\nfunction withApiKey(supabase: SupabaseClient, apiKey: string) {\n // Extract URL and key from the existing client\n const supabaseUrl = (supabase as unknown as { supabaseUrl: string }).supabaseUrl\n const supabaseKey = (supabase as unknown as { supabaseKey: string }).supabaseKey\n\n return createSupabaseClient(supabaseUrl, supabaseKey, {\n global: {\n headers: {\n \"x-cms-api-key\": apiKey,\n },\n },\n })\n}\n","import type { EmbedValue } from \"./types\"\n\nfunction toCssAspectRatio(ratio: string): string {\n const parts = ratio.split(\":\")\n if (parts.length !== 2) return \"16/9\"\n const w = parseFloat(parts[0])\n const h = parseFloat(parts[1])\n if (!w || !h || w <= 0 || h <= 0) return \"16/9\"\n return `${w}/${h}`\n}\n\n/**\n * Generate responsive iframe HTML for an embed field value.\n * Returns an empty string if the value is invalid or the URL is not HTTPS.\n */\nexport function getEmbedHtml(value: unknown): string {\n if (!value || typeof value !== \"object\") return \"\"\n const embed = value as EmbedValue\n if (!embed.url || !embed.url.startsWith(\"https://\")) return \"\"\n\n const width = embed.width || \"100%\"\n const aspectRatio = toCssAspectRatio(embed.aspect_ratio || \"16:9\")\n\n return `<div style=\"position:relative;width:${width};aspect-ratio:${aspectRatio}\"><iframe src=\"${embed.url}\" style=\"position:absolute;inset:0;width:100%;height:100%\" frameborder=\"0\" sandbox=\"allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox\" allowfullscreen loading=\"lazy\"></iframe></div>`\n}\n","import type { SupabaseClient } from \"@supabase/supabase-js\"\nimport type { Product, ProductQueryOptions, CreateOrderParams, CreateOrderResult } from \"./types\"\n\nexport function createShopClient(supabase: SupabaseClient, options: { apiKey: string; appUrl?: string }) {\n const { apiKey, appUrl } = options\n\n return {\n async getProducts(queryOptions?: ProductQueryOptions): Promise<Product[]> {\n let query = supabase\n .from(\"products\")\n .select(\"*, variants:product_variants(*), options:product_options(*)\")\n .eq(\"status\", \"published\")\n .order(queryOptions?.sort ?? \"sort_order\", { ascending: (queryOptions?.order ?? \"asc\") === \"asc\" })\n\n if (queryOptions?.category) {\n query = query.eq(\"category\", queryOptions.category)\n }\n if (queryOptions?.tags?.length) {\n query = query.overlaps(\"tags\", queryOptions.tags)\n }\n if (queryOptions?.limit) {\n query = query.limit(queryOptions.limit)\n }\n if (queryOptions?.offset) {\n query = query.range(queryOptions.offset, queryOptions.offset + (queryOptions.limit ?? 50) - 1)\n }\n\n const { data } = await query\n return (data ?? []) as Product[]\n },\n\n async getProductBySlug(slug: string): Promise<Product | null> {\n const { data } = await supabase\n .from(\"products\")\n .select(\"*, variants:product_variants(*), options:product_options(*)\")\n .eq(\"slug\", slug)\n .eq(\"status\", \"published\")\n .single()\n return (data as Product) ?? null\n },\n\n async getProductCategories(): Promise<string[]> {\n const { data } = await supabase\n .from(\"products\")\n .select(\"category\")\n .eq(\"status\", \"published\")\n .not(\"category\", \"is\", null)\n const categories = [...new Set((data ?? []).map((d: { category: string }) => d.category))]\n return categories.sort()\n },\n\n async createOrder(params: CreateOrderParams): Promise<CreateOrderResult> {\n // This must go through the API (not direct Supabase) because\n // order creation requires server-side Stripe PaymentIntent creation\n const baseUrl = appUrl ?? \"\"\n const res = await fetch(`${baseUrl}/api/orders/create`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ api_key: apiKey, ...params }),\n })\n if (!res.ok) {\n const err = await res.json().catch(() => ({ error: \"Order creation failed\" }))\n throw new Error(err.error ?? \"Order creation failed\")\n }\n return res.json()\n },\n }\n}\n","import type { SupabaseClient } from \"@supabase/supabase-js\"\n\nexport type EventStatus = \"draft\" | \"published\" | \"archived\"\nexport type BookingStatus = \"pending\" | \"confirmed\" | \"cancelled\" | \"refunded\"\n\nexport interface CmsEvent {\n id: string\n tenant_id: string\n title: string\n slug: string\n description: string | null\n short_description: string | null\n status: EventStatus\n start_at: string\n end_at: string | null\n venue_name: string | null\n venue_address: string | null\n hero_image: string | null\n gallery: string[]\n tags: string[]\n seo_title: string | null\n seo_description: string | null\n metadata: Record<string, unknown>\n sort_order: number\n published_at: string | null\n created_at: string\n updated_at: string\n}\n\nexport interface CmsTicketTier {\n id: string\n event_id: string\n tenant_id: string\n name: string\n description: string | null\n price_cents: number\n capacity: number | null\n sold_count: number\n sales_start_at: string | null\n sales_end_at: string | null\n is_active: boolean\n sort_order: number\n}\n\nexport interface EventWithTiers extends CmsEvent {\n tiers: CmsTicketTier[]\n}\n\nexport interface EventQueryOptions {\n /** Only include events with start_at >= now. Default false. */\n upcomingOnly?: boolean\n tag?: string\n limit?: number\n offset?: number\n sort?: \"start_at\" | \"sort_order\" | \"created_at\"\n order?: \"asc\" | \"desc\"\n}\n\nexport interface CreateBookingParams {\n event_slug: string\n ticket_tier_id?: string\n customer_email: string\n customer_name?: string\n customer_phone?: string\n quantity?: number\n success_url?: string\n cancel_url?: string\n metadata?: Record<string, unknown>\n}\n\nexport interface CreateBookingResult {\n booking_id: string\n booking_number: string\n status: \"pending\" | \"confirmed\"\n /** Redirect the browser here for paid bookings. */\n checkout_url?: string\n /** Present on free bookings — used for attendance QR display. */\n qr_token?: string\n}\n\nexport function createEventsClient(\n supabase: SupabaseClient,\n options: { apiKey: string; appUrl?: string }\n) {\n const { apiKey, appUrl } = options\n\n async function getEvents(\n queryOptions?: EventQueryOptions\n ): Promise<EventWithTiers[]> {\n let query = supabase\n .from(\"events\")\n .select(\"*, tiers:ticket_tiers(*)\")\n .eq(\"status\", \"published\")\n .order(queryOptions?.sort ?? \"start_at\", {\n ascending: (queryOptions?.order ?? \"asc\") === \"asc\",\n })\n\n if (queryOptions?.upcomingOnly) {\n query = query.gte(\"start_at\", new Date().toISOString())\n }\n if (queryOptions?.tag) {\n query = query.contains(\"tags\", [queryOptions.tag])\n }\n if (queryOptions?.limit) {\n query = query.limit(queryOptions.limit)\n }\n if (queryOptions?.offset) {\n query = query.range(\n queryOptions.offset,\n queryOptions.offset + (queryOptions.limit ?? 50) - 1\n )\n }\n\n const { data } = await query\n return (data ?? []) as EventWithTiers[]\n }\n\n async function getEventBySlug(slug: string): Promise<EventWithTiers | null> {\n const { data } = await supabase\n .from(\"events\")\n .select(\"*, tiers:ticket_tiers(*)\")\n .eq(\"slug\", slug)\n .eq(\"status\", \"published\")\n .single()\n return (data as EventWithTiers) ?? null\n }\n\n async function createBooking(\n params: CreateBookingParams\n ): Promise<CreateBookingResult> {\n const baseUrl = appUrl ?? \"\"\n const res = await fetch(`${baseUrl}/api/bookings/create`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ api_key: apiKey, ...params }),\n })\n if (!res.ok) {\n const err = await res\n .json()\n .catch(() => ({ error: \"Booking creation failed\" }))\n throw new Error(err.error ?? \"Booking creation failed\")\n }\n return res.json() as Promise<CreateBookingResult>\n }\n\n return { getEvents, getEventBySlug, createBooking }\n}\n","/**\n * Image helpers for CMS content.\n *\n * Images are already optimised (WebP, max 2400px) on upload.\n * No server-side transforms needed — serve originals directly.\n *\n * These functions are kept for backward compatibility but no longer\n * call Supabase image transformation endpoints.\n */\n\nexport interface ImageTransformOptions {\n width?: number\n height?: number\n quality?: number\n resize?: \"contain\" | \"cover\" | \"fill\"\n format?: \"origin\"\n}\n\n/**\n * Returns the image URL directly.\n *\n * Previously this converted URLs to use Supabase's /render/image/\n * transform endpoint, but images are now pre-optimised on upload\n * (WebP, max 2400px, quality 82) so transforms are unnecessary.\n *\n * The function is kept for backward compatibility — existing code\n * that calls getTransformUrl() will continue to work without changes.\n */\nexport function getTransformUrl(\n originalUrl: string,\n _options: ImageTransformOptions = {}\n): string {\n return originalUrl\n}\n\n/**\n * Returns a simple srcSet using the original image.\n *\n * Previously generated multiple transformed widths, but since images\n * are now pre-optimised WebP, a single source is sufficient.\n * Modern browsers handle responsive display efficiently with a\n * well-sized WebP source.\n */\nexport function getSrcSet(\n originalUrl: string,\n _widths: number[] = [],\n _quality = 80\n): string {\n return `${originalUrl} 2400w`\n}\n\n/**\n * Common image size presets — kept for backward compatibility.\n * Since images are pre-optimised, these are informational only.\n */\nexport const IMAGE_PRESETS = {\n thumbnail: { width: 150, height: 150, resize: \"cover\" as const, quality: 70 },\n card: { width: 400, height: 300, resize: \"cover\" as const, quality: 80 },\n hero: { width: 1200, height: 630, resize: \"cover\" as const, quality: 85 },\n og: { width: 1200, height: 630, resize: \"cover\" as const, quality: 90 },\n avatar: { width: 80, height: 80, resize: \"cover\" as const, quality: 75 },\n full: { width: 1920, resize: \"contain\" as const, quality: 85 },\n} as const\n"],"mappings":"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","names":["options","query","data","error"]}
1
+ {"version":3,"sources":["../src/queries.ts","../src/version.ts","../src/telemetry.ts","../src/embed.ts","../src/shop.ts","../src/events.ts","../src/cdn.ts"],"sourcesContent":["import { createClient as createSupabaseClient } from \"@supabase/supabase-js\"\nimport type { SupabaseClient } from \"@supabase/supabase-js\"\nimport type {\n CmsClientOptions,\n ContentItem,\n ContentQueryOptions,\n ContentType,\n GoogleReview,\n ReviewQueryOptions,\n} from \"./types\"\nimport { CMS_CLIENT_VERSION } from \"./version\"\nimport { reportClientVersion } from \"./telemetry\"\n\n/**\n * Creates a CMS query client authenticated with a tenant API key.\n *\n * Usage:\n * ```ts\n * const cms = createCmsClient(supabase, { apiKey: process.env.CMS_API_KEY! })\n * const events = await cms.getContentItems('events', { status: 'published' })\n * ```\n *\n * The API key is sent as an `x-cms-api-key` header on every request.\n * Supabase RLS policies validate it against the tenant's stored key.\n */\nexport function createCmsClient(\n supabase: SupabaseClient,\n options: CmsClientOptions\n) {\n const { apiKey } = options\n\n // Create a new Supabase client with the API key header injected globally\n const client = withApiKey(supabase, apiKey) as SupabaseClient\n\n // Day-granular, fire-and-forget version ping (server-side only).\n reportClientVersion(apiKey, options.appUrl)\n\n /** Resolve the tenant ID from the API key (cached per request) */\n let tenantIdCache: string | null = null\n\n async function getTenantId(): Promise<string> {\n if (tenantIdCache) return tenantIdCache\n\n const { data, error } = await client\n .from(\"tenants\")\n .select(\"id\")\n .single()\n\n if (error || !data) {\n throw new Error(\n \"Invalid CMS API key — no tenant found. Check your apiKey.\"\n )\n }\n\n tenantIdCache = data.id\n return data.id\n }\n\n /** Resolve a content type from its slug (cached) */\n const contentTypeCache = new Map<string, ContentType>()\n\n async function getContentTypeBySlug(contentTypeSlug: string): Promise<ContentType> {\n if (contentTypeCache.has(contentTypeSlug)) return contentTypeCache.get(contentTypeSlug)!\n const tenantId = await getTenantId()\n\n const { data, error } = await client\n .from(\"content_types\")\n .select(\"*\")\n .eq(\"tenant_id\", tenantId)\n .eq(\"slug\", contentTypeSlug)\n .single()\n\n if (error || !data) {\n throw new Error(`Content type not found: ${contentTypeSlug}`)\n }\n\n const ct = data as ContentType\n contentTypeCache.set(contentTypeSlug, ct)\n return ct\n }\n\n async function getContentTypeId(contentTypeSlug: string): Promise<string> {\n const ct = await getContentTypeBySlug(contentTypeSlug)\n return ct.id\n }\n\n /** Check if a member token has access to a gated content type */\n async function checkMemberAccess(\n contentType: ContentType,\n memberToken?: string\n ): Promise<{ allowed: boolean; memberTierId: string | null }> {\n const requiredTierId = contentType.required_membership_tier_id\n if (!requiredTierId) return { allowed: true, memberTierId: null } // Public\n\n if (!memberToken) return { allowed: false, memberTierId: null } // Gated but no token\n\n // Verify the member's token and check their tier\n const { data: session } = await client\n .from(\"member_sessions\")\n .select(\"member_id\")\n .eq(\"token_hash\", memberToken) // Note: caller should hash the token\n .single()\n\n if (!session) return { allowed: false, memberTierId: null }\n\n const { data: member } = await client\n .from(\"members\")\n .select(\"membership_tier_id, status\")\n .eq(\"id\", session.member_id)\n .single()\n\n if (!member || member.status !== \"active\") return { allowed: false, memberTierId: null }\n\n // Check if the member's tier matches (or exceeds) the required tier\n // For now: exact match or the member has the required tier\n return {\n allowed: member.membership_tier_id === requiredTierId,\n memberTierId: member.membership_tier_id as string | null,\n }\n }\n\n async function getFlipbook(id: string): Promise<import(\"./types\").FlipbookPublic | null> {\n const { data, error } = await client\n .from(\"flipbooks\")\n .select(\"id, title, page_count, status, manifest, download_enabled, tenant_id\")\n .eq(\"id\", id)\n .single()\n if (error || !data) return null\n\n const manifest = data.manifest as\n | { pages: Array<{ image: string; thumb: string; w: number; h: number }>; toc: Array<{ title: string; page: number }> }\n | null\n if (!manifest) {\n return {\n id: data.id as string,\n title: data.title as string,\n page_count: (data.page_count as number) ?? 0,\n status: data.status as \"pending_upload\" | \"pending\" | \"processing\" | \"ready\" | \"failed\",\n page_images: [],\n toc: [],\n download_url: null,\n }\n }\n\n // Browser-safe env lookup via globalThis. The client package builds without\n // @types/node so referring to `process` directly fails the dts build; reading\n // through globalThis sidesteps that and works in every JS environment that\n // matters (Node, browsers, edge runtimes).\n const proc = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process\n const cdnBase = proc?.env?.NEXT_PUBLIC_R2_PUBLIC_URL ?? \"https://cdn.distinctstudio.co.nz\"\n const prefix = `flipbooks/${data.tenant_id as string}/${data.id as string}/`\n return {\n id: data.id as string,\n title: data.title as string,\n page_count: (data.page_count as number) ?? manifest.pages.length,\n status: data.status as \"pending\" | \"processing\" | \"ready\" | \"failed\",\n page_images: manifest.pages.map((p) => ({\n url: `${cdnBase}/${prefix}${p.image}`,\n thumb_url: `${cdnBase}/${prefix}${p.thumb}`,\n w: p.w,\n h: p.h,\n })),\n toc: manifest.toc,\n download_url: data.download_enabled\n ? `${options.appUrl ?? \"\"}/api/flipbooks/${data.id as string}/download`\n : null,\n }\n }\n\n return {\n /**\n * List content items for a content type.\n * Defaults to published items ordered by most recent.\n */\n async getContentItems(\n contentTypeSlug: string,\n options: ContentQueryOptions = {}\n ): Promise<(ContentItem & { locked?: boolean })[]> {\n const contentType = await getContentTypeBySlug(contentTypeSlug)\n\n const {\n status = \"published\",\n orderBy = \"published_at\",\n orderDirection = \"desc\",\n limit = 100,\n offset = 0,\n memberToken,\n } = options\n\n // Check membership access\n const access = await checkMemberAccess(contentType, memberToken)\n\n // If gated and no access, check gating mode\n if (!access.allowed && contentType.required_membership_tier_id) {\n // Get tenant settings for gating mode\n const tenantId = await getTenantId()\n const { data: settings } = await client\n .from(\"tenant_settings\")\n .select(\"membership_gating_mode\")\n .eq(\"tenant_id\", tenantId)\n .single()\n\n const mode = (settings?.membership_gating_mode as string) ?? \"teaser\"\n\n if (mode === \"hide\") {\n return [] // Hide: return nothing\n }\n\n // Teaser mode: return items with locked flag, no body/data\n let query = client\n .from(\"content_items\")\n .select(\"id, title, slug, status, excerpt, seo_title, seo_description, featured_image, published_at, created_at, updated_at\")\n .eq(\"content_type_id\", contentType.id)\n\n if (status) query = query.eq(\"status\", status)\n query = query\n .order(orderBy, { ascending: orderDirection === \"asc\" })\n .range(offset, offset + limit - 1)\n\n const { data, error } = await query\n if (error) throw new Error(`Failed to fetch ${contentTypeSlug}: ${error.message}`)\n\n return (data ?? []).map((item) => ({\n ...item,\n data: {},\n locked: true,\n })) as (ContentItem & { locked: boolean })[]\n }\n\n // Full access\n let query = client\n .from(\"content_items\")\n .select(\"*\")\n .eq(\"content_type_id\", contentType.id)\n\n if (status) {\n query = query.eq(\"status\", status)\n }\n\n query = query\n .order(orderBy, { ascending: orderDirection === \"asc\" })\n .range(offset, offset + limit - 1)\n\n const { data, error } = await query\n\n if (error) {\n throw new Error(`Failed to fetch ${contentTypeSlug}: ${error.message}`)\n }\n\n return (data ?? []) as ContentItem[]\n },\n\n /**\n * Get a single content item by its slug.\n * Returns null if not found.\n */\n async getContentItemBySlug(\n contentTypeSlug: string,\n itemSlug: string\n ): Promise<ContentItem | null> {\n const contentTypeId = await getContentTypeId(contentTypeSlug)\n\n const { data, error } = await client\n .from(\"content_items\")\n .select(\"*\")\n .eq(\"content_type_id\", contentTypeId)\n .eq(\"slug\", itemSlug)\n .single()\n\n if (error) {\n if (error.code === \"PGRST116\") return null // not found\n throw new Error(\n `Failed to fetch ${contentTypeSlug}/${itemSlug}: ${error.message}`\n )\n }\n\n return data as ContentItem\n },\n\n /**\n * Get a content type definition (including its field schema).\n */\n async getContentType(contentTypeSlug: string): Promise<ContentType> {\n const tenantId = await getTenantId()\n\n const { data, error } = await client\n .from(\"content_types\")\n .select(\"*\")\n .eq(\"tenant_id\", tenantId)\n .eq(\"slug\", contentTypeSlug)\n .single()\n\n if (error || !data) {\n throw new Error(`Content type not found: ${contentTypeSlug}`)\n }\n\n return data as ContentType\n },\n\n /**\n * Get all slugs for a content type (for generateStaticParams).\n */\n async getAllSlugs(\n contentTypeSlug: string\n ): Promise<{ slug: string }[]> {\n const contentTypeId = await getContentTypeId(contentTypeSlug)\n\n const { data, error } = await client\n .from(\"content_items\")\n .select(\"slug\")\n .eq(\"content_type_id\", contentTypeId)\n .eq(\"status\", \"published\")\n\n if (error) {\n throw new Error(\n `Failed to fetch slugs for ${contentTypeSlug}: ${error.message}`\n )\n }\n\n return (data ?? []) as { slug: string }[]\n },\n\n /**\n * List all content types for this tenant.\n */\n async getContentTypes(): Promise<ContentType[]> {\n const tenantId = await getTenantId()\n\n const { data, error } = await client\n .from(\"content_types\")\n .select(\"*\")\n .eq(\"tenant_id\", tenantId)\n .order(\"name\")\n\n if (error) {\n throw new Error(`Failed to fetch content types: ${error.message}`)\n }\n\n return (data ?? []) as ContentType[]\n },\n\n /**\n * Get all slug redirects for a content type.\n * Use in next.config.js redirects() or middleware for 301s.\n * Returns: [{ old_slug, new_slug }, ...]\n */\n async getRedirects(\n contentTypeSlug: string\n ): Promise<{ old_slug: string; new_slug: string }[]> {\n const contentTypeId = await getContentTypeId(contentTypeSlug)\n\n const { data, error } = await client\n .from(\"slug_redirects\")\n .select(\"old_slug, new_slug\")\n .eq(\"content_type_id\", contentTypeId)\n\n if (error) {\n throw new Error(\n `Failed to fetch redirects for ${contentTypeSlug}: ${error.message}`\n )\n }\n\n return (data ?? []) as { old_slug: string; new_slug: string }[]\n },\n\n /**\n * Get all custom path redirects for this tenant.\n * Use alongside getRedirects() in next.config.ts for full redirect coverage.\n */\n async getCustomRedirects(): Promise<\n { source_path: string; destination_path: string; permanent: boolean }[]\n > {\n const tenantId = await getTenantId()\n\n const { data, error } = await client\n .from(\"custom_redirects\")\n .select(\"source_path, destination_path, permanent\")\n .eq(\"tenant_id\", tenantId)\n\n if (error) {\n throw new Error(`Failed to fetch custom redirects: ${error.message}`)\n }\n\n return (data ?? []) as {\n source_path: string\n destination_path: string\n permanent: boolean\n }[]\n },\n\n /**\n * Get form submissions for a specific form.\n * Useful for displaying testimonials, reviews, etc.\n */\n async getFormSubmissions(\n formSlug: string,\n options: { status?: string; limit?: number; offset?: number } = {}\n ): Promise<Record<string, unknown>[]> {\n const tenantId = await getTenantId()\n\n const { data: form } = await client\n .from(\"forms\")\n .select(\"id\")\n .eq(\"tenant_id\", tenantId)\n .eq(\"slug\", formSlug)\n .single()\n\n if (!form) return []\n\n const { limit = 50, offset = 0, status } = options\n\n let query = client\n .from(\"form_submissions\")\n .select(\"*\")\n .eq(\"form_id\", form.id)\n .eq(\"is_spam\", false)\n .order(\"created_at\", { ascending: false })\n .range(offset, offset + limit - 1)\n\n if (status) {\n query = query.eq(\"status\", status)\n }\n\n const { data } = await query\n return (data ?? []) as Record<string, unknown>[]\n },\n\n /**\n * Get a form configuration by slug.\n */\n async getForm(\n formSlug: string\n ): Promise<Record<string, unknown> | null> {\n const tenantId = await getTenantId()\n\n const { data } = await client\n .from(\"forms\")\n .select(\"id, name, slug, description, is_active\")\n .eq(\"tenant_id\", tenantId)\n .eq(\"slug\", formSlug)\n .single()\n\n return data as Record<string, unknown> | null\n },\n\n /**\n * Get a flipbook by ID, including CDN-resolved page image URLs.\n * Returns null if not found or manifest not yet ready.\n */\n getFlipbook,\n\n /**\n * Get Google Reviews for this tenant.\n * Defaults to approved reviews ordered by most recent.\n */\n async getReviews(\n options: ReviewQueryOptions = {}\n ): Promise<GoogleReview[]> {\n const tenantId = await getTenantId()\n\n const {\n status = \"approved\",\n minRating,\n limit = 50,\n offset = 0,\n orderBy = \"review_timestamp\",\n orderDirection = \"desc\",\n } = options\n\n let query = client\n .from(\"google_reviews\")\n .select(\"id, author_name, author_photo_url, rating, text, review_timestamp\")\n .eq(\"tenant_id\", tenantId)\n\n if (status) {\n query = query.eq(\"status\", status)\n }\n\n if (minRating) {\n query = query.gte(\"rating\", minRating)\n }\n\n query = query\n .order(orderBy, { ascending: orderDirection === \"asc\" })\n .range(offset, offset + limit - 1)\n\n const { data, error } = await query\n\n if (error) {\n throw new Error(`Failed to fetch reviews: ${error.message}`)\n }\n\n return (data ?? []) as GoogleReview[]\n },\n\n /**\n * Get the tenant's third-party tracking IDs (Google Analytics, Google Tag Manager, Meta Pixel).\n * Each value is null when not configured. These IDs are public and safe to render in HTML.\n *\n * On Next.js, the result is cached and revalidated every hour by default\n * (`revalidate: 3600`) and tagged with `TRACKING_CONFIG_TAG`. Tenant sites\n * that want zero-lag updates can call `revalidateTag(TRACKING_CONFIG_TAG)`\n * from a webhook. Pass `revalidate: 0` to disable caching, `revalidate: false`\n * to cache indefinitely (tag-only invalidation), or any positive integer\n * (seconds) to override the interval. Outside Next.js the cache hints are\n * silently ignored — every call hits the network.\n */\n async getTrackingConfig(options: TrackingConfigOptions = {}): Promise<TrackingConfig> {\n const { revalidate = 3600, tags = [TRACKING_CONFIG_TAG] } = options\n const supabaseUrl = (supabase as unknown as { supabaseUrl: string }).supabaseUrl\n const supabaseKey = (supabase as unknown as { supabaseKey: string }).supabaseKey\n\n // Direct PostgREST fetch (instead of going through supabase-js) so that\n // Next.js sees the request and applies its `next` cache options. RLS\n // restricts the result to the tenant matching `x-cms-api-key`, so no\n // explicit tenant_id filter is needed.\n const url =\n `${supabaseUrl}/rest/v1/tenant_settings` +\n `?select=google_analytics_id,google_tag_manager_id,meta_pixel_id&limit=1`\n\n const init: RequestInit & { next?: { revalidate?: number | false; tags?: string[] } } = {\n headers: {\n apikey: supabaseKey,\n Authorization: `Bearer ${supabaseKey}`,\n \"x-cms-api-key\": apiKey,\n \"x-cms-client-version\": CMS_CLIENT_VERSION,\n Accept: \"application/json\",\n },\n next: { revalidate, tags },\n }\n\n let row: {\n google_analytics_id: string | null\n google_tag_manager_id: string | null\n meta_pixel_id: string | null\n } | undefined\n\n try {\n const res = await fetch(url, init as RequestInit)\n if (res.ok) {\n const rows = (await res.json()) as Array<typeof row>\n row = rows?.[0]\n }\n } catch {\n // Network errors fall through to the all-null result so a transient\n // outage on the CMS never breaks page renders on the tenant site.\n }\n\n return {\n googleAnalyticsId: nullify(row?.google_analytics_id),\n googleTagManagerId: nullify(row?.google_tag_manager_id),\n metaPixelId: nullify(row?.meta_pixel_id),\n }\n },\n }\n}\n\n/**\n * Cache tag applied to `getTrackingConfig()` fetches on Next.js. Call\n * `revalidateTag(TRACKING_CONFIG_TAG)` from a webhook handler on the tenant\n * site to make tracking-ID changes take effect immediately rather than\n * waiting for the next revalidation interval.\n */\nexport const TRACKING_CONFIG_TAG = \"cms:tracking-config\"\n\nexport interface TrackingConfigOptions {\n /**\n * Cache lifetime for the underlying fetch on Next.js, in seconds.\n * Defaults to 3600 (one hour). Set to `0` to disable caching, or `false`\n * to cache indefinitely until the tag is revalidated. Ignored outside\n * Next.js runtimes.\n */\n revalidate?: number | false\n /**\n * Cache tags for the underlying fetch on Next.js. Defaults to\n * `[TRACKING_CONFIG_TAG]`. Override to namespace by tenant if you share a\n * single Next.js process across multiple tenants (uncommon).\n */\n tags?: string[]\n}\n\nfunction nullify(v: string | null | undefined): string | null {\n if (!v) return null\n const trimmed = v.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nexport interface TrackingConfig {\n googleAnalyticsId: string | null\n googleTagManagerId: string | null\n metaPixelId: string | null\n}\n\n/**\n * Creates a new Supabase client that includes the `x-cms-api-key`\n * header in every request, using the same URL and key as the original.\n */\nfunction withApiKey(supabase: SupabaseClient, apiKey: string) {\n // Extract URL and key from the existing client\n const supabaseUrl = (supabase as unknown as { supabaseUrl: string }).supabaseUrl\n const supabaseKey = (supabase as unknown as { supabaseKey: string }).supabaseKey\n\n return createSupabaseClient(supabaseUrl, supabaseKey, {\n global: {\n headers: {\n \"x-cms-api-key\": apiKey,\n \"x-cms-client-version\": CMS_CLIENT_VERSION,\n },\n },\n })\n}\n","/**\n * The version of @distinctagency/cms-client embedded in this build.\n *\n * Bump this in lock-step with package.json — the value is read by the SDK to\n * report installed-version telemetry and to send `x-cms-client-version` on\n * outgoing requests.\n */\nexport const CMS_CLIENT_VERSION = \"1.14.0\"\n","import { CMS_CLIENT_VERSION } from \"./version\"\n\n/**\n * Reports the running SDK version to the CMS so the Super Admin UI can show\n * which client version each tenant site is on.\n *\n * Day-granular and process-local: at most one POST per (api key + day) per\n * Node process. Calls from browser code are ignored — the API key would be\n * exposed and the data is redundant with server-side reports.\n *\n * Fire-and-forget. Failures are silent — telemetry must never break a render.\n */\n\nconst DEFAULT_APP_URL = \"https://cms.distinctstudio.co.nz\"\n\ninterface ReportRecord {\n date: string // YYYY-MM-DD\n version: string\n}\n\nconst reported = new Map<string, ReportRecord>()\n\nfunction todayKey(): string {\n return new Date().toISOString().slice(0, 10)\n}\n\nexport function reportClientVersion(apiKey: string, appUrl?: string): void {\n // Skip in browser contexts — server-side calls already cover the tenant.\n if (typeof window !== \"undefined\") return\n if (!apiKey) return\n\n const today = todayKey()\n const last = reported.get(apiKey)\n if (last && last.date === today && last.version === CMS_CLIENT_VERSION) return\n\n // Optimistically mark as reported so concurrent calls don't all fire. If the\n // request fails we'll retry tomorrow — that's acceptable for telemetry.\n reported.set(apiKey, { date: today, version: CMS_CLIENT_VERSION })\n\n const base = (appUrl ?? DEFAULT_APP_URL).replace(/\\/$/, \"\")\n const url = `${base}/api/client-telemetry`\n\n // Fire and forget. Use a hand-rolled then() chain rather than async/await so\n // we don't accidentally surface an unhandled rejection.\n void fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-cms-api-key\": apiKey,\n \"x-cms-client-version\": CMS_CLIENT_VERSION,\n },\n body: JSON.stringify({\n api_key: apiKey,\n version: CMS_CLIENT_VERSION,\n }),\n }).catch(() => {\n // Allow a retry on the next call by clearing the optimistic record.\n reported.delete(apiKey)\n })\n}\n","import type { EmbedValue } from \"./types\"\n\nfunction toCssAspectRatio(ratio: string): string {\n const parts = ratio.split(\":\")\n if (parts.length !== 2) return \"16/9\"\n const w = parseFloat(parts[0])\n const h = parseFloat(parts[1])\n if (!w || !h || w <= 0 || h <= 0) return \"16/9\"\n return `${w}/${h}`\n}\n\n/**\n * Generate responsive iframe HTML for an embed field value.\n * Returns an empty string if the value is invalid or the URL is not HTTPS.\n */\nexport function getEmbedHtml(value: unknown): string {\n if (!value || typeof value !== \"object\") return \"\"\n const embed = value as EmbedValue\n if (!embed.url || !embed.url.startsWith(\"https://\")) return \"\"\n\n const width = embed.width || \"100%\"\n const aspectRatio = toCssAspectRatio(embed.aspect_ratio || \"16:9\")\n\n return `<div style=\"position:relative;width:${width};aspect-ratio:${aspectRatio}\"><iframe src=\"${embed.url}\" style=\"position:absolute;inset:0;width:100%;height:100%\" frameborder=\"0\" sandbox=\"allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox\" allowfullscreen loading=\"lazy\"></iframe></div>`\n}\n","import type { SupabaseClient } from \"@supabase/supabase-js\"\nimport type {\n Product,\n ProductCategory,\n ProductQueryOptions,\n CreateOrderParams,\n CreateOrderResult,\n} from \"./types\"\nimport { CMS_CLIENT_VERSION } from \"./version\"\nimport { reportClientVersion } from \"./telemetry\"\n\nconst DEFAULT_APP_URL = \"https://cms.distinctstudio.co.nz\"\n\nexport interface ShopClientOptions {\n /** Tenant API key (UUID). Sent as `x-cms-api-key` on every request. */\n apiKey: string\n /** CMS app base URL. Defaults to `https://cms.distinctstudio.co.nz`. */\n appUrl?: string\n}\n\n/**\n * Creates a typed client for the eCommerce REST API (products, categories, orders).\n *\n * Unlike `createCmsClient`, the shop client talks to the CMS REST endpoints\n * (`/api/products`, `/api/product-categories`, `/api/orders/create`) rather than\n * Supabase directly — it doesn't need the `supabase` argument, but accepts it\n * for API symmetry with the rest of the SDK.\n *\n * Usage:\n * ```ts\n * const shop = createShopClient(supabase, { apiKey: process.env.CMS_API_KEY! })\n * const products = await shop.getProducts({ category: \"electronics\", limit: 20 })\n * ```\n */\nexport function createShopClient(\n _supabase: SupabaseClient,\n options: ShopClientOptions\n) {\n const { apiKey } = options\n const appUrl = (options.appUrl ?? DEFAULT_APP_URL).replace(/\\/$/, \"\")\n\n // Day-granular, fire-and-forget version ping (server-side only).\n reportClientVersion(apiKey, appUrl)\n\n function authHeaders(): HeadersInit {\n return {\n \"Content-Type\": \"application/json\",\n \"x-cms-api-key\": apiKey,\n \"x-cms-client-version\": CMS_CLIENT_VERSION,\n }\n }\n\n async function getJson<T>(path: string): Promise<T> {\n const res = await fetch(`${appUrl}${path}`, {\n headers: authHeaders(),\n // Caller controls Next.js caching by wrapping the call site.\n })\n if (!res.ok) {\n const body = await res.json().catch(() => ({}) as { error?: string })\n throw new Error(\n body.error ?? `Shop request failed (${res.status}): ${path}`\n )\n }\n return res.json() as Promise<T>\n }\n\n return {\n /**\n * List published products. Includes nested `variants` and `options`.\n *\n * `category` filters by category slug (matches the FK `category_id`,\n * with a fallback to the legacy `category` text column).\n */\n async getProducts(queryOptions: ProductQueryOptions = {}): Promise<Product[]> {\n const params = new URLSearchParams()\n if (queryOptions.category) params.set(\"category\", queryOptions.category)\n if (queryOptions.tags?.length) params.set(\"tags\", queryOptions.tags.join(\",\"))\n if (queryOptions.limit) params.set(\"limit\", String(queryOptions.limit))\n if (queryOptions.offset) params.set(\"offset\", String(queryOptions.offset))\n if (queryOptions.sort) params.set(\"sort\", queryOptions.sort)\n if (queryOptions.order) params.set(\"order\", queryOptions.order)\n\n const qs = params.toString()\n const { products } = await getJson<{ products: Product[] }>(\n `/api/products${qs ? `?${qs}` : \"\"}`\n )\n return products ?? []\n },\n\n /**\n * Get a single published product by slug. Returns null if not found.\n * Includes nested `variants` and `options`.\n */\n async getProductBySlug(slug: string): Promise<Product | null> {\n const res = await fetch(`${appUrl}/api/products/${encodeURIComponent(slug)}`, {\n headers: authHeaders(),\n })\n if (res.status === 404) return null\n if (!res.ok) {\n const body = await res.json().catch(() => ({}) as { error?: string })\n throw new Error(body.error ?? `Failed to fetch product ${slug}`)\n }\n const { product } = (await res.json()) as { product: Product }\n return product ?? null\n },\n\n /**\n * List product categories for the tenant, ordered by sort_order then name.\n */\n async getCategories(): Promise<ProductCategory[]> {\n const { categories } = await getJson<{ categories: ProductCategory[] }>(\n `/api/product-categories`\n )\n return categories ?? []\n },\n\n /**\n * Get a single category by slug. Returns null if not found.\n */\n async getCategoryBySlug(slug: string): Promise<ProductCategory | null> {\n const all = await this.getCategories()\n return all.find((c) => c.slug === slug) ?? null\n },\n\n /**\n * Create a pending order and return a Stripe PaymentIntent client_secret\n * to confirm payment client-side.\n *\n * Stripe must be configured for the tenant; eCommerce must be enabled on\n * their billing plan. Validates stock for tracked variants and applies\n * any discount code before creating the PaymentIntent.\n */\n async createOrder(params: CreateOrderParams): Promise<CreateOrderResult> {\n const res = await fetch(`${appUrl}/api/orders/create`, {\n method: \"POST\",\n headers: authHeaders(),\n body: JSON.stringify({ api_key: apiKey, ...params }),\n })\n if (!res.ok) {\n const body = await res.json().catch(() => ({}) as { error?: string })\n throw new Error(body.error ?? \"Order creation failed\")\n }\n return res.json() as Promise<CreateOrderResult>\n },\n }\n}\n","import type { SupabaseClient } from \"@supabase/supabase-js\"\nimport { reportClientVersion } from \"./telemetry\"\n\nexport type EventStatus = \"draft\" | \"published\" | \"archived\"\nexport type BookingStatus = \"pending\" | \"confirmed\" | \"cancelled\" | \"refunded\"\n\nexport interface CmsEvent {\n id: string\n tenant_id: string\n title: string\n slug: string\n description: string | null\n short_description: string | null\n status: EventStatus\n start_at: string\n end_at: string | null\n venue_name: string | null\n venue_address: string | null\n hero_image: string | null\n gallery: string[]\n tags: string[]\n seo_title: string | null\n seo_description: string | null\n metadata: Record<string, unknown>\n sort_order: number\n published_at: string | null\n created_at: string\n updated_at: string\n}\n\nexport interface CmsTicketTier {\n id: string\n event_id: string\n tenant_id: string\n name: string\n description: string | null\n price_cents: number\n capacity: number | null\n sold_count: number\n sales_start_at: string | null\n sales_end_at: string | null\n is_active: boolean\n sort_order: number\n}\n\nexport interface EventWithTiers extends CmsEvent {\n tiers: CmsTicketTier[]\n}\n\nexport interface EventQueryOptions {\n /** Only include events with start_at >= now. Default false. */\n upcomingOnly?: boolean\n tag?: string\n limit?: number\n offset?: number\n sort?: \"start_at\" | \"sort_order\" | \"created_at\"\n order?: \"asc\" | \"desc\"\n}\n\nexport interface CreateBookingParams {\n event_slug: string\n ticket_tier_id?: string\n customer_email: string\n customer_name?: string\n customer_phone?: string\n quantity?: number\n success_url?: string\n cancel_url?: string\n metadata?: Record<string, unknown>\n}\n\nexport interface CreateBookingResult {\n booking_id: string\n booking_number: string\n status: \"pending\" | \"confirmed\"\n /** Redirect the browser here for paid bookings. */\n checkout_url?: string\n /** Present on free bookings — used for attendance QR display. */\n qr_token?: string\n}\n\nexport function createEventsClient(\n supabase: SupabaseClient,\n options: { apiKey: string; appUrl?: string }\n) {\n const { apiKey, appUrl } = options\n\n // Day-granular, fire-and-forget version ping (server-side only).\n reportClientVersion(apiKey, appUrl)\n\n async function getEvents(\n queryOptions?: EventQueryOptions\n ): Promise<EventWithTiers[]> {\n let query = supabase\n .from(\"events\")\n .select(\"*, tiers:ticket_tiers(*)\")\n .eq(\"status\", \"published\")\n .order(queryOptions?.sort ?? \"start_at\", {\n ascending: (queryOptions?.order ?? \"asc\") === \"asc\",\n })\n\n if (queryOptions?.upcomingOnly) {\n query = query.gte(\"start_at\", new Date().toISOString())\n }\n if (queryOptions?.tag) {\n query = query.contains(\"tags\", [queryOptions.tag])\n }\n if (queryOptions?.limit) {\n query = query.limit(queryOptions.limit)\n }\n if (queryOptions?.offset) {\n query = query.range(\n queryOptions.offset,\n queryOptions.offset + (queryOptions.limit ?? 50) - 1\n )\n }\n\n const { data } = await query\n return (data ?? []) as EventWithTiers[]\n }\n\n async function getEventBySlug(slug: string): Promise<EventWithTiers | null> {\n const { data } = await supabase\n .from(\"events\")\n .select(\"*, tiers:ticket_tiers(*)\")\n .eq(\"slug\", slug)\n .eq(\"status\", \"published\")\n .single()\n return (data as EventWithTiers) ?? null\n }\n\n async function createBooking(\n params: CreateBookingParams\n ): Promise<CreateBookingResult> {\n const baseUrl = appUrl ?? \"\"\n const res = await fetch(`${baseUrl}/api/bookings/create`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ api_key: apiKey, ...params }),\n })\n if (!res.ok) {\n const err = await res\n .json()\n .catch(() => ({ error: \"Booking creation failed\" }))\n throw new Error(err.error ?? \"Booking creation failed\")\n }\n return res.json() as Promise<CreateBookingResult>\n }\n\n return { getEvents, getEventBySlug, createBooking }\n}\n","/**\n * Image helpers for CMS content.\n *\n * Images are already optimised (WebP, max 2400px) on upload.\n * No server-side transforms needed — serve originals directly.\n *\n * These functions are kept for backward compatibility but no longer\n * call Supabase image transformation endpoints.\n */\n\nexport interface ImageTransformOptions {\n width?: number\n height?: number\n quality?: number\n resize?: \"contain\" | \"cover\" | \"fill\"\n format?: \"origin\"\n}\n\n/**\n * Returns the image URL directly.\n *\n * Previously this converted URLs to use Supabase's /render/image/\n * transform endpoint, but images are now pre-optimised on upload\n * (WebP, max 2400px, quality 82) so transforms are unnecessary.\n *\n * The function is kept for backward compatibility — existing code\n * that calls getTransformUrl() will continue to work without changes.\n */\nexport function getTransformUrl(\n originalUrl: string,\n _options: ImageTransformOptions = {}\n): string {\n return originalUrl\n}\n\n/**\n * Returns a simple srcSet using the original image.\n *\n * Previously generated multiple transformed widths, but since images\n * are now pre-optimised WebP, a single source is sufficient.\n * Modern browsers handle responsive display efficiently with a\n * well-sized WebP source.\n */\nexport function getSrcSet(\n originalUrl: string,\n _widths: number[] = [],\n _quality = 80\n): string {\n return `${originalUrl} 2400w`\n}\n\n/**\n * Common image size presets — kept for backward compatibility.\n * Since images are pre-optimised, these are informational only.\n */\nexport const IMAGE_PRESETS = {\n thumbnail: { width: 150, height: 150, resize: \"cover\" as const, quality: 70 },\n card: { width: 400, height: 300, resize: \"cover\" as const, quality: 80 },\n hero: { width: 1200, height: 630, resize: \"cover\" as const, quality: 85 },\n og: { width: 1200, height: 630, resize: \"cover\" as const, quality: 90 },\n avatar: { width: 80, height: 80, resize: \"cover\" as const, quality: 75 },\n full: { width: 1920, resize: \"contain\" as const, quality: 85 },\n} as const\n"],"mappings":"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","names":["options","query","data","error","DEFAULT_APP_URL"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@distinctagency/cms-client",
3
- "version": "1.12.0",
3
+ "version": "1.14.0",
4
4
  "description": "Client library for Distinct CMS — query content, products, and manage orders",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",