@nextblock-cms/db 0.22.0 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/README.md +47 -47
  2. package/lib/supabase/server.d.ts +151 -2
  3. package/lib/supabase/types.d.ts +160 -2
  4. package/package.json +1 -1
  5. package/supabase/functions/nextblock-migration-ingest/index.ts +64 -1
  6. package/supabase/migrations/02000_catchup_gen1.sql +5880 -5880
  7. package/supabase/migrations/02001_baseline_schema.sql +2734 -2734
  8. package/supabase/migrations/02002_baseline_constraints_and_indexes.sql +1262 -1262
  9. package/supabase/migrations/02003_baseline_security_and_grants.sql +1577 -1577
  10. package/supabase/migrations/02004_baseline_seed.sql +565 -565
  11. package/supabase/migrations/02005_home_live_demo_promo_cadence_15min.sql +53 -53
  12. package/supabase/migrations/02006_home_marketing_contrast.sql +56 -56
  13. package/supabase/migrations/02007_home_pillar_caption_contrast.sql +44 -44
  14. package/supabase/migrations/02008_home_fr_parity.sql +89 -89
  15. package/supabase/migrations/02009_fr_cortex_posts.sql +67 -67
  16. package/supabase/migrations/02010_shop_hero_redesign.sql +72 -72
  17. package/supabase/migrations/02011_drop_backdrop_blur_on_pages.sql +26 -26
  18. package/supabase/migrations/02012_drop_variant_backdrop_blur.sql +17 -17
  19. package/supabase/migrations/02013_advisor_rls_and_trigger_grants.sql +102 -102
  20. package/supabase/migrations/02014_package_activations_staff_read.sql +18 -18
  21. package/supabase/migrations/02018_install_paths_and_seed_copy_accuracy.sql +1820 -1820
  22. package/supabase/migrations/02019_open_handed_licensing_and_mcp_clients.sql +448 -448
  23. package/supabase/migrations/02020_commerce_product_accuracy.sql +93 -93
  24. package/supabase/migrations/02021_profiles_staff_read_and_role_guard.sql +362 -362
  25. package/supabase/migrations/02022_security_hardening_writer_boundary_and_aal.sql +504 -0
  26. package/supabase/migrations/02023_aal2_for_orders_profiles_site_settings.sql +171 -0
@@ -3,7 +3,7 @@ export type Json = string | number | boolean | null | {
3
3
  } | Json[];
4
4
  export type Database = {
5
5
  __InternalSupabase: {
6
- PostgrestVersion: "14.5";
6
+ PostgrestVersion: "14.1";
7
7
  };
8
8
  public: {
9
9
  Tables: {
@@ -119,6 +119,11 @@ export type Database = {
119
119
  type: Database["public"]["Enums"]["interaction_type"];
120
120
  updated_at: string;
121
121
  user_id: string;
122
+ author: {
123
+ avatar_url: string | null;
124
+ full_name: string | null;
125
+ id: string | null;
126
+ } | null;
122
127
  };
123
128
  Insert: {
124
129
  content: string;
@@ -170,6 +175,13 @@ export type Database = {
170
175
  referencedRelation: "products";
171
176
  referencedColumns: ["id"];
172
177
  },
178
+ {
179
+ foreignKeyName: "cms_interactions_user_id_profiles_fkey";
180
+ columns: ["user_id"];
181
+ isOneToOne: false;
182
+ referencedRelation: "profile_cards";
183
+ referencedColumns: ["id"];
184
+ },
173
185
  {
174
186
  foreignKeyName: "cms_interactions_user_id_profiles_fkey";
175
187
  columns: ["user_id"];
@@ -244,6 +256,13 @@ export type Database = {
244
256
  updated_at?: string;
245
257
  };
246
258
  Relationships: [
259
+ {
260
+ foreignKeyName: "content_drafts_author_id_fkey";
261
+ columns: ["author_id"];
262
+ isOneToOne: false;
263
+ referencedRelation: "profile_cards";
264
+ referencedColumns: ["id"];
265
+ },
247
266
  {
248
267
  foreignKeyName: "content_drafts_author_id_fkey";
249
268
  columns: ["author_id"];
@@ -297,6 +316,13 @@ export type Database = {
297
316
  tool_name?: string;
298
317
  };
299
318
  Relationships: [
319
+ {
320
+ foreignKeyName: "cortex_ai_db_mutation_audit_actor_user_id_fkey";
321
+ columns: ["actor_user_id"];
322
+ isOneToOne: false;
323
+ referencedRelation: "profile_cards";
324
+ referencedColumns: ["id"];
325
+ },
300
326
  {
301
327
  foreignKeyName: "cortex_ai_db_mutation_audit_actor_user_id_fkey";
302
328
  columns: ["actor_user_id"];
@@ -306,6 +332,27 @@ export type Database = {
306
332
  }
307
333
  ];
308
334
  };
335
+ cortex_request_log: {
336
+ Row: {
337
+ created_at: string;
338
+ id: string;
339
+ route: string;
340
+ user_id: string;
341
+ };
342
+ Insert: {
343
+ created_at?: string;
344
+ id?: string;
345
+ route: string;
346
+ user_id: string;
347
+ };
348
+ Update: {
349
+ created_at?: string;
350
+ id?: string;
351
+ route?: string;
352
+ user_id?: string;
353
+ };
354
+ Relationships: [];
355
+ };
309
356
  coupon_freemius_mappings: {
310
357
  Row: {
311
358
  coupon_id: string;
@@ -596,6 +643,7 @@ export type Database = {
596
643
  };
597
644
  email_2fa_challenges: {
598
645
  Row: {
646
+ attempts: number;
599
647
  consumed_at: string | null;
600
648
  created_at: string;
601
649
  expires_at: string;
@@ -604,6 +652,7 @@ export type Database = {
604
652
  user_id: string;
605
653
  };
606
654
  Insert: {
655
+ attempts?: number;
607
656
  consumed_at?: string | null;
608
657
  created_at?: string;
609
658
  expires_at: string;
@@ -612,6 +661,7 @@ export type Database = {
612
661
  user_id: string;
613
662
  };
614
663
  Update: {
664
+ attempts?: number;
615
665
  consumed_at?: string | null;
616
666
  created_at?: string;
617
667
  expires_at?: string;
@@ -908,6 +958,13 @@ export type Database = {
908
958
  width?: number | null;
909
959
  };
910
960
  Relationships: [
961
+ {
962
+ foreignKeyName: "media_uploader_id_fkey";
963
+ columns: ["uploader_id"];
964
+ isOneToOne: false;
965
+ referencedRelation: "profile_cards";
966
+ referencedColumns: ["id"];
967
+ },
911
968
  {
912
969
  foreignKeyName: "media_uploader_id_fkey";
913
970
  columns: ["uploader_id"];
@@ -1274,6 +1331,13 @@ export type Database = {
1274
1331
  version?: number;
1275
1332
  };
1276
1333
  Relationships: [
1334
+ {
1335
+ foreignKeyName: "page_revisions_author_id_fkey";
1336
+ columns: ["author_id"];
1337
+ isOneToOne: false;
1338
+ referencedRelation: "profile_cards";
1339
+ referencedColumns: ["id"];
1340
+ },
1277
1341
  {
1278
1342
  foreignKeyName: "page_revisions_author_id_fkey";
1279
1343
  columns: ["author_id"];
@@ -1343,6 +1407,13 @@ export type Database = {
1343
1407
  version?: number;
1344
1408
  };
1345
1409
  Relationships: [
1410
+ {
1411
+ foreignKeyName: "pages_author_id_fkey";
1412
+ columns: ["author_id"];
1413
+ isOneToOne: false;
1414
+ referencedRelation: "profile_cards";
1415
+ referencedColumns: ["id"];
1416
+ },
1346
1417
  {
1347
1418
  foreignKeyName: "pages_author_id_fkey";
1348
1419
  columns: ["author_id"];
@@ -1395,6 +1466,13 @@ export type Database = {
1395
1466
  version?: number;
1396
1467
  };
1397
1468
  Relationships: [
1469
+ {
1470
+ foreignKeyName: "post_revisions_author_id_fkey";
1471
+ columns: ["author_id"];
1472
+ isOneToOne: false;
1473
+ referencedRelation: "profile_cards";
1474
+ referencedColumns: ["id"];
1475
+ },
1398
1476
  {
1399
1477
  foreignKeyName: "post_revisions_author_id_fkey";
1400
1478
  columns: ["author_id"];
@@ -1473,6 +1551,13 @@ export type Database = {
1473
1551
  version?: number;
1474
1552
  };
1475
1553
  Relationships: [
1554
+ {
1555
+ foreignKeyName: "posts_author_id_fkey";
1556
+ columns: ["author_id"];
1557
+ isOneToOne: false;
1558
+ referencedRelation: "profile_cards";
1559
+ referencedColumns: ["id"];
1560
+ },
1476
1561
  {
1477
1562
  foreignKeyName: "posts_author_id_fkey";
1478
1563
  columns: ["author_id"];
@@ -1650,6 +1735,13 @@ export type Database = {
1650
1735
  updated_at?: string;
1651
1736
  };
1652
1737
  Relationships: [
1738
+ {
1739
+ foreignKeyName: "product_drafts_author_id_fkey";
1740
+ columns: ["author_id"];
1741
+ isOneToOne: false;
1742
+ referencedRelation: "profile_cards";
1743
+ referencedColumns: ["id"];
1744
+ },
1653
1745
  {
1654
1746
  foreignKeyName: "product_drafts_author_id_fkey";
1655
1747
  columns: ["author_id"];
@@ -1841,6 +1933,13 @@ export type Database = {
1841
1933
  version?: number;
1842
1934
  };
1843
1935
  Relationships: [
1936
+ {
1937
+ foreignKeyName: "product_revisions_author_id_fkey";
1938
+ columns: ["author_id"];
1939
+ isOneToOne: false;
1940
+ referencedRelation: "profile_cards";
1941
+ referencedColumns: ["id"];
1942
+ },
1844
1943
  {
1845
1944
  foreignKeyName: "product_revisions_author_id_fkey";
1846
1945
  columns: ["author_id"];
@@ -2509,6 +2608,7 @@ export type Database = {
2509
2608
  };
2510
2609
  ucp_cart_sessions: {
2511
2610
  Row: {
2611
+ access_token_hash: string | null;
2512
2612
  attribution: Json;
2513
2613
  buyer_identity: Json;
2514
2614
  checkout_url: string | null;
@@ -2526,6 +2626,7 @@ export type Database = {
2526
2626
  updated_at: string;
2527
2627
  };
2528
2628
  Insert: {
2629
+ access_token_hash?: string | null;
2529
2630
  attribution?: Json;
2530
2631
  buyer_identity?: Json;
2531
2632
  checkout_url?: string | null;
@@ -2543,6 +2644,7 @@ export type Database = {
2543
2644
  updated_at?: string;
2544
2645
  };
2545
2646
  Update: {
2647
+ access_token_hash?: string | null;
2546
2648
  attribution?: Json;
2547
2649
  buyer_identity?: Json;
2548
2650
  checkout_url?: string | null;
@@ -2611,6 +2713,13 @@ export type Database = {
2611
2713
  user_id?: string;
2612
2714
  };
2613
2715
  Relationships: [
2716
+ {
2717
+ foreignKeyName: "user_addresses_user_id_fkey";
2718
+ columns: ["user_id"];
2719
+ isOneToOne: false;
2720
+ referencedRelation: "profile_cards";
2721
+ referencedColumns: ["id"];
2722
+ },
2614
2723
  {
2615
2724
  foreignKeyName: "user_addresses_user_id_fkey";
2616
2725
  columns: ["user_id"];
@@ -2700,7 +2809,24 @@ export type Database = {
2700
2809
  };
2701
2810
  };
2702
2811
  Views: {
2703
- [_ in never]: never;
2812
+ profile_cards: {
2813
+ Row: {
2814
+ avatar_url: string | null;
2815
+ full_name: string | null;
2816
+ id: string | null;
2817
+ };
2818
+ Insert: {
2819
+ avatar_url?: string | null;
2820
+ full_name?: string | null;
2821
+ id?: string | null;
2822
+ };
2823
+ Update: {
2824
+ avatar_url?: string | null;
2825
+ full_name?: string | null;
2826
+ id?: string | null;
2827
+ };
2828
+ Relationships: [];
2829
+ };
2704
2830
  };
2705
2831
  Functions: {
2706
2832
  apply_order_inventory_deduction: {
@@ -2719,6 +2845,28 @@ export type Database = {
2719
2845
  paid_at: string;
2720
2846
  }[];
2721
2847
  };
2848
+ author: {
2849
+ Args: {
2850
+ "": Database["public"]["Tables"]["cms_interactions"]["Row"];
2851
+ };
2852
+ Returns: {
2853
+ avatar_url: string | null;
2854
+ full_name: string | null;
2855
+ id: string | null;
2856
+ };
2857
+ SetofOptions: {
2858
+ from: "cms_interactions";
2859
+ to: "profile_cards";
2860
+ isOneToOne: true;
2861
+ isSetofReturn: true;
2862
+ };
2863
+ };
2864
+ bump_email_2fa_attempts: {
2865
+ Args: {
2866
+ p_user_id: string;
2867
+ };
2868
+ Returns: number;
2869
+ };
2722
2870
  clear_currency_price_overrides: {
2723
2871
  Args: {
2724
2872
  target_currency: string;
@@ -2777,6 +2925,10 @@ export type Database = {
2777
2925
  Args: never;
2778
2926
  Returns: boolean;
2779
2927
  };
2928
+ is_admin_mfa: {
2929
+ Args: never;
2930
+ Returns: boolean;
2931
+ };
2780
2932
  is_valid_currency_amount_map: {
2781
2933
  Args: {
2782
2934
  amounts: Json;
@@ -2808,6 +2960,12 @@ export type Database = {
2808
2960
  };
2809
2961
  Returns: Json;
2810
2962
  };
2963
+ redeem_coupon_atomically: {
2964
+ Args: {
2965
+ p_coupon_id: string;
2966
+ };
2967
+ Returns: boolean;
2968
+ };
2811
2969
  sync_inventory_cache_for_sku: {
2812
2970
  Args: {
2813
2971
  p_sku: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextblock-cms/db",
3
- "version": "0.22.0",
3
+ "version": "0.22.1",
4
4
  "license": "AGPL-3.0-or-later",
5
5
  "repository": {
6
6
  "type": "git",
@@ -7,6 +7,58 @@ const corsHeaders = {
7
7
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
8
8
  };
9
9
 
10
+ /**
11
+ * Dedicated bearer for this function. It runs with the service-role key (which bypasses RLS) and
12
+ * can wipe content, mint confirmed users and fetch arbitrary URLs, so it must NEVER be reachable
13
+ * with the public anon JWT (the platform `verify_jwt` default accepts the anon key, which ships in
14
+ * every site bundle). The operator sets MIGRATION_INGEST_BEARER to a high-entropy secret that is
15
+ * NOT the anon or service key; the function is fail-closed — with the var unset it rejects every
16
+ * request, because this is a maintainer migration tool, not a public endpoint.
17
+ */
18
+ function isAuthorizedIngestRequest(req: Request): boolean {
19
+ const expected = (Deno.env.get('MIGRATION_INGEST_BEARER') ?? '').trim();
20
+ if (!expected) return false;
21
+ const provided = (req.headers.get('Authorization') ?? '').replace(/^Bearer\s+/i, '').trim();
22
+ if (!provided || provided.length !== expected.length) return false;
23
+ // constant-time compare
24
+ let diff = 0;
25
+ for (let i = 0; i < expected.length; i += 1) diff |= provided.charCodeAt(i) ^ expected.charCodeAt(i);
26
+ return diff === 0;
27
+ }
28
+
29
+ /**
30
+ * SSRF guard for the outbound media fetch. Blocks obvious private, loopback, link-local and
31
+ * cloud-metadata destinations and requires http(s). Defence in depth behind the bearer: an
32
+ * authorized caller still should not be able to turn this into an internal port scanner or a
33
+ * cloud-credential exfiltrator. (DNS rebinding is further limited by redirect:'error' on the
34
+ * fetch; full rebind protection would require pinning the resolved IP, which the edge runtime
35
+ * does not expose.)
36
+ */
37
+ function isBlockedFetchTarget(rawUrl: string): boolean {
38
+ let u: URL;
39
+ try {
40
+ u = new URL(rawUrl);
41
+ } catch {
42
+ return true;
43
+ }
44
+ if (u.protocol !== 'http:' && u.protocol !== 'https:') return true;
45
+ const host = u.hostname.toLowerCase().replace(/^\[|\]$/g, '');
46
+ if (host === 'localhost' || host.endsWith('.localhost') || host.endsWith('.internal')) return true;
47
+ // IPv6 loopback / link-local / unique-local
48
+ if (host === '::1' || host.startsWith('fe80:') || host.startsWith('fc') || host.startsWith('fd')) return true;
49
+ // IPv4 literals in private / loopback / link-local / metadata ranges
50
+ const m = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
51
+ if (m) {
52
+ const [a, b] = [Number(m[1]), Number(m[2])];
53
+ if (a === 10 || a === 127 || a === 0) return true;
54
+ if (a === 169 && b === 254) return true; // link-local incl. 169.254.169.254 metadata
55
+ if (a === 172 && b >= 16 && b <= 31) return true;
56
+ if (a === 192 && b === 168) return true;
57
+ if (a === 100 && b >= 64 && b <= 127) return true; // CGNAT
58
+ }
59
+ return false;
60
+ }
61
+
10
62
  // Upload S3 / R2 Proxy Handler
11
63
  async function processMediaUpload(url: string, r2Config: any, warnings: string[]): Promise<{ publicUrl: string; objectKey: string; contentType: string } | null> {
12
64
  if (!r2Config || !r2Config.account_id) {
@@ -37,11 +89,15 @@ async function processMediaUpload(url: string, r2Config: any, warnings: string[]
37
89
  const extension = contentType.split('/')[1] || 'jpg';
38
90
  filename = `upload_${crypto.randomUUID()}.${extension}`;
39
91
  } else {
92
+ if (isBlockedFetchTarget(url)) {
93
+ warnings.push(`Refused to fetch blocked/internal URL: ${url}`);
94
+ return null;
95
+ }
40
96
  const controller = new AbortController();
41
97
  const timeoutId = setTimeout(() => controller.abort(), 6000); // 6-second timeout
42
98
  let fileResponse;
43
99
  try {
44
- fileResponse = await fetch(url, { signal: controller.signal });
100
+ fileResponse = await fetch(url, { signal: controller.signal, redirect: 'error' });
45
101
  clearTimeout(timeoutId);
46
102
  } catch (err: any) {
47
103
  clearTimeout(timeoutId);
@@ -91,6 +147,13 @@ Deno.serve(async (req: Request) => {
91
147
  return new Response('ok', { headers: corsHeaders });
92
148
  }
93
149
 
150
+ if (!isAuthorizedIngestRequest(req)) {
151
+ return new Response(JSON.stringify({ error: 'Unauthorized' }), {
152
+ headers: { ...corsHeaders, 'Content-Type': 'application/json', 'WWW-Authenticate': 'Bearer' },
153
+ status: 401,
154
+ });
155
+ }
156
+
94
157
  try {
95
158
  const supabaseClient = createClient(
96
159
  Deno.env.get('SUPABASE_URL') ?? '',