@guard-rail/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +339 -0
  2. package/dist/adapters/supabase/auth.d.ts +9 -0
  3. package/dist/adapters/supabase/auth.js +57 -0
  4. package/dist/adapters/supabase/auth.js.map +1 -0
  5. package/dist/adapters/supabase/client.d.ts +28 -0
  6. package/dist/adapters/supabase/client.js +44 -0
  7. package/dist/adapters/supabase/client.js.map +1 -0
  8. package/dist/adapters/supabase/confirm.d.ts +19 -0
  9. package/dist/adapters/supabase/confirm.js +27 -0
  10. package/dist/adapters/supabase/confirm.js.map +1 -0
  11. package/dist/adapters/supabase/db.d.ts +9 -0
  12. package/dist/adapters/supabase/db.js +49 -0
  13. package/dist/adapters/supabase/db.js.map +1 -0
  14. package/dist/adapters/supabase/index.d.ts +25 -0
  15. package/dist/adapters/supabase/index.js +29 -0
  16. package/dist/adapters/supabase/index.js.map +1 -0
  17. package/dist/adapters/supabase/members.d.ts +9 -0
  18. package/dist/adapters/supabase/members.js +73 -0
  19. package/dist/adapters/supabase/members.js.map +1 -0
  20. package/dist/adapters/supabase/repeaters.d.ts +9 -0
  21. package/dist/adapters/supabase/repeaters.js +73 -0
  22. package/dist/adapters/supabase/repeaters.js.map +1 -0
  23. package/dist/adapters/supabase/storage.d.ts +18 -0
  24. package/dist/adapters/supabase/storage.js +93 -0
  25. package/dist/adapters/supabase/storage.js.map +1 -0
  26. package/dist/core/adapter-types.d.ts +212 -0
  27. package/dist/core/adapter-types.js +1 -0
  28. package/dist/core/adapter-types.js.map +1 -0
  29. package/dist/core/bailout.d.ts +9 -0
  30. package/dist/core/bailout.js +14 -0
  31. package/dist/core/bailout.js.map +1 -0
  32. package/dist/core/config.d.ts +27 -0
  33. package/dist/core/config.js +46 -0
  34. package/dist/core/config.js.map +1 -0
  35. package/dist/core/defs.d.ts +22 -0
  36. package/dist/core/defs.js +16 -0
  37. package/dist/core/defs.js.map +1 -0
  38. package/dist/core/types.d.ts +136 -0
  39. package/dist/core/types.js +1 -0
  40. package/dist/core/types.js.map +1 -0
  41. package/dist/index.d.ts +13 -0
  42. package/dist/index.js +43 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/react/Admin.d.ts +8 -0
  45. package/dist/react/Admin.js +116 -0
  46. package/dist/react/Admin.js.map +1 -0
  47. package/dist/react/GRail/client.d.ts +11 -0
  48. package/dist/react/GRail/client.js +242 -0
  49. package/dist/react/GRail/client.js.map +1 -0
  50. package/dist/react/GRail/index.d.ts +12 -0
  51. package/dist/react/GRail/index.js +19 -0
  52. package/dist/react/GRail/index.js.map +1 -0
  53. package/dist/react/GRail/render.d.ts +23 -0
  54. package/dist/react/GRail/render.js +67 -0
  55. package/dist/react/GRail/render.js.map +1 -0
  56. package/dist/react/Icons.d.ts +13 -0
  57. package/dist/react/Icons.js +68 -0
  58. package/dist/react/Icons.js.map +1 -0
  59. package/dist/react/MediaPicker.d.ts +14 -0
  60. package/dist/react/MediaPicker.js +172 -0
  61. package/dist/react/MediaPicker.js.map +1 -0
  62. package/dist/react/Provider.d.ts +11 -0
  63. package/dist/react/Provider.js +26 -0
  64. package/dist/react/Provider.js.map +1 -0
  65. package/dist/react/Repeater/client.d.ts +16 -0
  66. package/dist/react/Repeater/client.js +148 -0
  67. package/dist/react/Repeater/client.js.map +1 -0
  68. package/dist/react/Repeater/index.d.ts +35 -0
  69. package/dist/react/Repeater/index.js +22 -0
  70. package/dist/react/Repeater/index.js.map +1 -0
  71. package/dist/react/Syncer.d.ts +17 -0
  72. package/dist/react/Syncer.js +23 -0
  73. package/dist/react/Syncer.js.map +1 -0
  74. package/dist/react/Team.d.ts +7 -0
  75. package/dist/react/Team.js +190 -0
  76. package/dist/react/Team.js.map +1 -0
  77. package/dist/react/resizeImage.d.ts +25 -0
  78. package/dist/react/resizeImage.js +52 -0
  79. package/dist/react/resizeImage.js.map +1 -0
  80. package/dist/react/styles.d.ts +421 -0
  81. package/dist/react/styles.js +402 -0
  82. package/dist/react/styles.js.map +1 -0
  83. package/dist/react/useAnchorRect.d.ts +17 -0
  84. package/dist/react/useAnchorRect.js +32 -0
  85. package/dist/react/useAnchorRect.js.map +1 -0
  86. package/dist/server/actions.d.ts +41 -0
  87. package/dist/server/actions.js +214 -0
  88. package/dist/server/actions.js.map +1 -0
  89. package/dist/server/data.d.ts +10 -0
  90. package/dist/server/data.js +24 -0
  91. package/dist/server/data.js.map +1 -0
  92. package/dist/server/fieldData.d.ts +26 -0
  93. package/dist/server/fieldData.js +9 -0
  94. package/dist/server/fieldData.js.map +1 -0
  95. package/dist/server/loader.d.ts +6 -0
  96. package/dist/server/loader.js +53 -0
  97. package/dist/server/loader.js.map +1 -0
  98. package/dist/server/repeaterActions.d.ts +20 -0
  99. package/dist/server/repeaterActions.js +72 -0
  100. package/dist/server/repeaterActions.js.map +1 -0
  101. package/dist/server/repeaterData.d.ts +18 -0
  102. package/dist/server/repeaterData.js +27 -0
  103. package/dist/server/repeaterData.js.map +1 -0
  104. package/dist/server/session.d.ts +14 -0
  105. package/dist/server/session.js +37 -0
  106. package/dist/server/session.js.map +1 -0
  107. package/dist/server/wrapper.d.ts +12 -0
  108. package/dist/server/wrapper.js +42 -0
  109. package/dist/server/wrapper.js.map +1 -0
  110. package/package.json +58 -0
  111. package/supabase/migrations/20260911000000_guardrail_init.sql +288 -0
@@ -0,0 +1,29 @@
1
+ import { supabaseDB } from "./db";
2
+ import { supabaseAuth } from "./auth";
3
+ import { supabaseMembers } from "./members";
4
+ import { supabaseConfirmRoute } from "./confirm";
5
+ import { supabaseStorage, DEFAULT_BUCKET } from "./storage";
6
+ import { supabaseRepeaters } from "./repeaters";
7
+ import { supabaseDB as supabaseDB2 } from "./db";
8
+ import { supabaseAuth as supabaseAuth2 } from "./auth";
9
+ import { supabaseMembers as supabaseMembers2 } from "./members";
10
+ import { supabaseRepeaters as supabaseRepeaters2 } from "./repeaters";
11
+ function supabaseAdapters(creds) {
12
+ return {
13
+ db: supabaseDB2(creds),
14
+ auth: supabaseAuth2(creds),
15
+ members: supabaseMembers2(creds),
16
+ repeaters: supabaseRepeaters2(creds)
17
+ };
18
+ }
19
+ export {
20
+ DEFAULT_BUCKET,
21
+ supabaseAdapters,
22
+ supabaseAuth,
23
+ supabaseConfirmRoute,
24
+ supabaseDB,
25
+ supabaseMembers,
26
+ supabaseRepeaters,
27
+ supabaseStorage
28
+ };
29
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/adapters/supabase/index.ts"],"sourcesContent":["export { supabaseDB } from \"./db\";\nexport { supabaseAuth } from \"./auth\";\nexport { supabaseMembers } from \"./members\";\nexport { supabaseConfirmRoute } from \"./confirm\";\nexport { supabaseStorage, DEFAULT_BUCKET } from \"./storage\";\nexport { supabaseRepeaters } from \"./repeaters\";\nexport type { SupabaseStorageOptions } from \"./storage\";\nexport type { SupabaseCredentials } from \"./client\";\n\nimport { supabaseDB } from \"./db\";\nimport { supabaseAuth } from \"./auth\";\nimport { supabaseMembers } from \"./members\";\nimport { supabaseRepeaters } from \"./repeaters\";\nimport type { SupabaseCredentials } from \"./client\";\n\n/**\n * All three slots backed by one Supabase project. Use the individual factories\n * instead when mixing backends — Supabase for content, another provider for\n * identity, say.\n */\nexport function supabaseAdapters(creds: SupabaseCredentials) {\n return {\n db: supabaseDB(creds),\n auth: supabaseAuth(creds),\n members: supabaseMembers(creds),\n repeaters: supabaseRepeaters(creds),\n };\n}\n"],"mappings":"AAAA,SAAS,kBAAkB;AAC3B,SAAS,oBAAoB;AAC7B,SAAS,uBAAuB;AAChC,SAAS,4BAA4B;AACrC,SAAS,iBAAiB,sBAAsB;AAChD,SAAS,yBAAyB;AAIlC,SAAS,cAAAA,mBAAkB;AAC3B,SAAS,gBAAAC,qBAAoB;AAC7B,SAAS,mBAAAC,wBAAuB;AAChC,SAAS,qBAAAC,0BAAyB;AAQ3B,SAAS,iBAAiB,OAA4B;AAC3D,SAAO;AAAA,IACL,IAAIH,YAAW,KAAK;AAAA,IACpB,MAAMC,cAAa,KAAK;AAAA,IACxB,SAASC,iBAAgB,KAAK;AAAA,IAC9B,WAAWC,mBAAkB,KAAK;AAAA,EACpC;AACF;","names":["supabaseDB","supabaseAuth","supabaseMembers","supabaseRepeaters"]}
@@ -0,0 +1,9 @@
1
+ import { SupabaseCredentials } from './client.js';
2
+ import { GRMembershipAdapter } from '../../core/adapter-types.js';
3
+ import '@supabase/supabase-js';
4
+ import 'react';
5
+ import '../../core/types.js';
6
+
7
+ declare function supabaseMembers(creds: SupabaseCredentials): GRMembershipAdapter;
8
+
9
+ export { supabaseMembers };
@@ -0,0 +1,73 @@
1
+ import { assertCredentials, sessionClient } from "./client";
2
+ import { GR_MEMBERS_TABLE, GR_SCHEMA } from "../../core/defs";
3
+ function toMember(row) {
4
+ return {
5
+ userId: row.user_id,
6
+ email: row.email,
7
+ role: row.role,
8
+ status: row.status,
9
+ ...row.invited_at ? { invitedAt: row.invited_at } : {}
10
+ };
11
+ }
12
+ const COLUMNS = "user_id, email, role, status, invited_at";
13
+ function supabaseMembers(creds) {
14
+ assertCredentials(creds);
15
+ return {
16
+ async getMember(siteId, lookup) {
17
+ const client = await sessionClient(creds);
18
+ let query = client.schema(GR_SCHEMA).from(GR_MEMBERS_TABLE).select(COLUMNS).eq("site_id", siteId);
19
+ if (lookup.userId) query = query.eq("user_id", lookup.userId);
20
+ else if (lookup.email)
21
+ query = query.eq("email", lookup.email.toLowerCase().trim());
22
+ else return null;
23
+ const { data, error } = await query.maybeSingle();
24
+ if (error) {
25
+ console.error("Guardrail (Supabase): member lookup failed", error.message);
26
+ return null;
27
+ }
28
+ return data ? toMember(data) : null;
29
+ },
30
+ async listMembers(siteId) {
31
+ const client = await sessionClient(creds);
32
+ const { data, error } = await client.schema(GR_SCHEMA).from(GR_MEMBERS_TABLE).select(COLUMNS).eq("site_id", siteId).order("email");
33
+ if (error) {
34
+ console.error("Guardrail (Supabase): list members failed", error.message);
35
+ return [];
36
+ }
37
+ return (data ?? []).map(toMember);
38
+ },
39
+ async upsertMember(siteId, member) {
40
+ const client = await sessionClient(creds);
41
+ const { error } = await client.schema(GR_SCHEMA).from(GR_MEMBERS_TABLE).upsert(
42
+ {
43
+ site_id: siteId,
44
+ email: member.email.toLowerCase().trim(),
45
+ role: member.role,
46
+ user_id: member.userId ?? null,
47
+ status: member.status,
48
+ invited_at: (/* @__PURE__ */ new Date()).toISOString()
49
+ },
50
+ { onConflict: "site_id,email" }
51
+ );
52
+ return error ? { success: false, error: error.message } : { success: true };
53
+ },
54
+ async removeMember(siteId, email) {
55
+ const client = await sessionClient(creds);
56
+ const { error } = await client.schema(GR_SCHEMA).from(GR_MEMBERS_TABLE).delete().eq("site_id", siteId).eq("email", email.toLowerCase().trim());
57
+ return error ? { success: false, error: error.message } : { success: true };
58
+ },
59
+ async claimInvite(siteId) {
60
+ const client = await sessionClient(creds);
61
+ const { error } = await client.schema(GR_SCHEMA).rpc("gr_claim_invite", {
62
+ target_site: siteId
63
+ });
64
+ if (error) {
65
+ console.error("Guardrail (Supabase): claim failed", error.message);
66
+ }
67
+ }
68
+ };
69
+ }
70
+ export {
71
+ supabaseMembers
72
+ };
73
+ //# sourceMappingURL=members.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/adapters/supabase/members.ts"],"sourcesContent":["import { assertCredentials, sessionClient } from \"./client\";\nimport type { SupabaseCredentials } from \"./client\";\nimport { GR_MEMBERS_TABLE, GR_SCHEMA } from \"../../core/defs\";\nimport type { GRMembershipAdapter } from \"../../core/adapter-types\";\nimport type { GRMember, GRRole } from \"../../core/types\";\n\ninterface MemberRow {\n user_id: string | null;\n email: string;\n role: GRRole;\n status: \"active\" | \"pending\";\n invited_at: string | null;\n}\n\nfunction toMember(row: MemberRow): GRMember {\n return {\n userId: row.user_id,\n email: row.email,\n role: row.role,\n status: row.status,\n ...(row.invited_at ? { invitedAt: row.invited_at } : {}),\n };\n}\n\nconst COLUMNS = \"user_id, email, role, status, invited_at\";\n\nexport function supabaseMembers(\n creds: SupabaseCredentials,\n): GRMembershipAdapter {\n assertCredentials(creds);\n\n return {\n async getMember(siteId, lookup) {\n // Session-bound by necessity: RLS evaluates auth.uid(), so the anon\n // client would see nothing and every role would resolve to null.\n const client = await sessionClient(creds);\n let query = client\n .schema(GR_SCHEMA)\n .from(GR_MEMBERS_TABLE)\n .select(COLUMNS)\n .eq(\"site_id\", siteId);\n\n if (lookup.userId) query = query.eq(\"user_id\", lookup.userId);\n else if (lookup.email)\n query = query.eq(\"email\", lookup.email.toLowerCase().trim());\n else return null;\n\n const { data, error } = await query.maybeSingle();\n if (error) {\n console.error(\"Guardrail (Supabase): member lookup failed\", error.message);\n return null;\n }\n return data ? toMember(data as MemberRow) : null;\n },\n\n async listMembers(siteId) {\n const client = await sessionClient(creds);\n const { data, error } = await client\n .schema(GR_SCHEMA)\n .from(GR_MEMBERS_TABLE)\n .select(COLUMNS)\n .eq(\"site_id\", siteId)\n .order(\"email\");\n\n if (error) {\n console.error(\"Guardrail (Supabase): list members failed\", error.message);\n return [];\n }\n return ((data ?? []) as MemberRow[]).map(toMember);\n },\n\n async upsertMember(siteId, member) {\n const client = await sessionClient(creds);\n const { error } = await client\n .schema(GR_SCHEMA)\n .from(GR_MEMBERS_TABLE)\n .upsert(\n {\n site_id: siteId,\n email: member.email.toLowerCase().trim(),\n role: member.role,\n user_id: member.userId ?? null,\n status: member.status,\n invited_at: new Date().toISOString(),\n },\n { onConflict: \"site_id,email\" },\n );\n return error\n ? { success: false, error: error.message }\n : { success: true };\n },\n\n async removeMember(siteId, email) {\n const client = await sessionClient(creds);\n const { error } = await client\n .schema(GR_SCHEMA)\n .from(GR_MEMBERS_TABLE)\n .delete()\n .eq(\"site_id\", siteId)\n .eq(\"email\", email.toLowerCase().trim());\n return error\n ? { success: false, error: error.message }\n : { success: true };\n },\n\n async claimInvite(siteId) {\n const client = await sessionClient(creds);\n // SECURITY DEFINER function: sets user_id/status on a pending row\n // matching the caller's own email, and cannot alter the invited role.\n const { error } = await client.schema(GR_SCHEMA).rpc(\"gr_claim_invite\", {\n target_site: siteId,\n });\n if (error) {\n console.error(\"Guardrail (Supabase): claim failed\", error.message);\n }\n },\n };\n}\n"],"mappings":"AAAA,SAAS,mBAAmB,qBAAqB;AAEjD,SAAS,kBAAkB,iBAAiB;AAY5C,SAAS,SAAS,KAA0B;AAC1C,SAAO;AAAA,IACL,QAAQ,IAAI;AAAA,IACZ,OAAO,IAAI;AAAA,IACX,MAAM,IAAI;AAAA,IACV,QAAQ,IAAI;AAAA,IACZ,GAAI,IAAI,aAAa,EAAE,WAAW,IAAI,WAAW,IAAI,CAAC;AAAA,EACxD;AACF;AAEA,MAAM,UAAU;AAET,SAAS,gBACd,OACqB;AACrB,oBAAkB,KAAK;AAEvB,SAAO;AAAA,IACL,MAAM,UAAU,QAAQ,QAAQ;AAG9B,YAAM,SAAS,MAAM,cAAc,KAAK;AACxC,UAAI,QAAQ,OACT,OAAO,SAAS,EAChB,KAAK,gBAAgB,EACrB,OAAO,OAAO,EACd,GAAG,WAAW,MAAM;AAEvB,UAAI,OAAO,OAAQ,SAAQ,MAAM,GAAG,WAAW,OAAO,MAAM;AAAA,eACnD,OAAO;AACd,gBAAQ,MAAM,GAAG,SAAS,OAAO,MAAM,YAAY,EAAE,KAAK,CAAC;AAAA,UACxD,QAAO;AAEZ,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,MAAM,YAAY;AAChD,UAAI,OAAO;AACT,gBAAQ,MAAM,8CAA8C,MAAM,OAAO;AACzE,eAAO;AAAA,MACT;AACA,aAAO,OAAO,SAAS,IAAiB,IAAI;AAAA,IAC9C;AAAA,IAEA,MAAM,YAAY,QAAQ;AACxB,YAAM,SAAS,MAAM,cAAc,KAAK;AACxC,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAC3B,OAAO,SAAS,EAChB,KAAK,gBAAgB,EACrB,OAAO,OAAO,EACd,GAAG,WAAW,MAAM,EACpB,MAAM,OAAO;AAEhB,UAAI,OAAO;AACT,gBAAQ,MAAM,6CAA6C,MAAM,OAAO;AACxE,eAAO,CAAC;AAAA,MACV;AACA,cAAS,QAAQ,CAAC,GAAmB,IAAI,QAAQ;AAAA,IACnD;AAAA,IAEA,MAAM,aAAa,QAAQ,QAAQ;AACjC,YAAM,SAAS,MAAM,cAAc,KAAK;AACxC,YAAM,EAAE,MAAM,IAAI,MAAM,OACrB,OAAO,SAAS,EAChB,KAAK,gBAAgB,EACrB;AAAA,QACC;AAAA,UACE,SAAS;AAAA,UACT,OAAO,OAAO,MAAM,YAAY,EAAE,KAAK;AAAA,UACvC,MAAM,OAAO;AAAA,UACb,SAAS,OAAO,UAAU;AAAA,UAC1B,QAAQ,OAAO;AAAA,UACf,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,QACrC;AAAA,QACA,EAAE,YAAY,gBAAgB;AAAA,MAChC;AACF,aAAO,QACH,EAAE,SAAS,OAAO,OAAO,MAAM,QAAQ,IACvC,EAAE,SAAS,KAAK;AAAA,IACtB;AAAA,IAEA,MAAM,aAAa,QAAQ,OAAO;AAChC,YAAM,SAAS,MAAM,cAAc,KAAK;AACxC,YAAM,EAAE,MAAM,IAAI,MAAM,OACrB,OAAO,SAAS,EAChB,KAAK,gBAAgB,EACrB,OAAO,EACP,GAAG,WAAW,MAAM,EACpB,GAAG,SAAS,MAAM,YAAY,EAAE,KAAK,CAAC;AACzC,aAAO,QACH,EAAE,SAAS,OAAO,OAAO,MAAM,QAAQ,IACvC,EAAE,SAAS,KAAK;AAAA,IACtB;AAAA,IAEA,MAAM,YAAY,QAAQ;AACxB,YAAM,SAAS,MAAM,cAAc,KAAK;AAGxC,YAAM,EAAE,MAAM,IAAI,MAAM,OAAO,OAAO,SAAS,EAAE,IAAI,mBAAmB;AAAA,QACtE,aAAa;AAAA,MACf,CAAC;AACD,UAAI,OAAO;AACT,gBAAQ,MAAM,sCAAsC,MAAM,OAAO;AAAA,MACnE;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,9 @@
1
+ import { SupabaseCredentials } from './client.js';
2
+ import { GRRepeaterAdapter } from '../../core/adapter-types.js';
3
+ import '@supabase/supabase-js';
4
+ import 'react';
5
+ import '../../core/types.js';
6
+
7
+ declare function supabaseRepeaters(creds: SupabaseCredentials): GRRepeaterAdapter;
8
+
9
+ export { supabaseRepeaters };
@@ -0,0 +1,73 @@
1
+ import { anonClient, assertCredentials, sessionClient } from "./client";
2
+ import { GR_REPEATER_TABLE, GR_SCHEMA } from "../../core/defs";
3
+ function toItem(row) {
4
+ return {
5
+ id: row.item_id,
6
+ order: row.sort_order,
7
+ ...row.slug ? { slug: row.slug } : {}
8
+ };
9
+ }
10
+ const SLUG = /^[a-z0-9]+(-[a-z0-9]+)*$/;
11
+ function supabaseRepeaters(creds) {
12
+ assertCredentials(creds);
13
+ return {
14
+ async listItems(repeaterId, siteId) {
15
+ const { data, error } = await anonClient(creds).schema(GR_SCHEMA).from(GR_REPEATER_TABLE).select("item_id, slug, sort_order").eq("site_id", siteId).eq("repeater_id", repeaterId).order("sort_order", { ascending: true });
16
+ if (error) {
17
+ console.error("Guardrail (Supabase): list repeater items failed", error.message);
18
+ return [];
19
+ }
20
+ return (data ?? []).map(toItem);
21
+ },
22
+ async getItemBySlug(repeaterId, siteId, slug) {
23
+ const { data, error } = await anonClient(creds).schema(GR_SCHEMA).from(GR_REPEATER_TABLE).select("item_id, slug, sort_order").eq("site_id", siteId).eq("repeater_id", repeaterId).eq("slug", slug).maybeSingle();
24
+ if (error) {
25
+ console.error("Guardrail (Supabase): repeater item lookup failed", error.message);
26
+ return null;
27
+ }
28
+ return data ? toItem(data) : null;
29
+ },
30
+ async addItem(repeaterId, siteId, options) {
31
+ if (options?.slug && !SLUG.test(options.slug)) {
32
+ return { success: false, error: "Invalid slug." };
33
+ }
34
+ const client = await sessionClient(creds);
35
+ const table = client.schema(GR_SCHEMA).from(GR_REPEATER_TABLE);
36
+ const { data: maxRow } = await table.select("sort_order").eq("site_id", siteId).eq("repeater_id", repeaterId).order("sort_order", { ascending: false }).limit(1).maybeSingle();
37
+ const nextOrder = (maxRow?.sort_order ?? -1) + 1;
38
+ const itemId = crypto.randomUUID();
39
+ const { error } = await client.schema(GR_SCHEMA).from(GR_REPEATER_TABLE).insert({
40
+ site_id: siteId,
41
+ repeater_id: repeaterId,
42
+ item_id: itemId,
43
+ slug: options?.slug ?? null,
44
+ sort_order: nextOrder
45
+ });
46
+ if (error) {
47
+ const message = error.code === "23505" ? "That slug is already used by another item." : error.message;
48
+ return { success: false, error: message };
49
+ }
50
+ return {
51
+ success: true,
52
+ item: { id: itemId, order: nextOrder, ...options?.slug ? { slug: options.slug } : {} }
53
+ };
54
+ },
55
+ async removeItem(repeaterId, siteId, itemId) {
56
+ const client = await sessionClient(creds);
57
+ const { error } = await client.schema(GR_SCHEMA).from(GR_REPEATER_TABLE).delete().eq("site_id", siteId).eq("repeater_id", repeaterId).eq("item_id", itemId);
58
+ return error ? { success: false, error: error.message } : { success: true };
59
+ },
60
+ async reorderItems(repeaterId, siteId, orderedItemIds) {
61
+ const client = await sessionClient(creds);
62
+ for (const [index, itemId] of orderedItemIds.entries()) {
63
+ const { error } = await client.schema(GR_SCHEMA).from(GR_REPEATER_TABLE).update({ sort_order: index }).eq("site_id", siteId).eq("repeater_id", repeaterId).eq("item_id", itemId);
64
+ if (error) return { success: false, error: error.message };
65
+ }
66
+ return { success: true };
67
+ }
68
+ };
69
+ }
70
+ export {
71
+ supabaseRepeaters
72
+ };
73
+ //# sourceMappingURL=repeaters.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/adapters/supabase/repeaters.ts"],"sourcesContent":["import { anonClient, assertCredentials, sessionClient } from \"./client\";\nimport type { SupabaseCredentials } from \"./client\";\nimport { GR_REPEATER_TABLE, GR_SCHEMA } from \"../../core/defs\";\nimport type { GRRepeaterAdapter } from \"../../core/adapter-types\";\nimport type { GRRepeaterItem } from \"../../core/types\";\n\ninterface ItemRow {\n item_id: string;\n slug: string | null;\n sort_order: number;\n}\n\nfunction toItem(row: ItemRow): GRRepeaterItem {\n return {\n id: row.item_id,\n order: row.sort_order,\n ...(row.slug ? { slug: row.slug } : {}),\n };\n}\n\nconst SLUG = /^[a-z0-9]+(-[a-z0-9]+)*$/;\n\nexport function supabaseRepeaters(creds: SupabaseCredentials): GRRepeaterAdapter {\n assertCredentials(creds);\n\n return {\n async listItems(repeaterId, siteId) {\n // Anon client: enumerating items is a public read, same as content.\n const { data, error } = await anonClient(creds)\n .schema(GR_SCHEMA)\n .from(GR_REPEATER_TABLE)\n .select(\"item_id, slug, sort_order\")\n .eq(\"site_id\", siteId)\n .eq(\"repeater_id\", repeaterId)\n .order(\"sort_order\", { ascending: true });\n\n if (error) {\n console.error(\"Guardrail (Supabase): list repeater items failed\", error.message);\n return [];\n }\n return ((data ?? []) as ItemRow[]).map(toItem);\n },\n\n async getItemBySlug(repeaterId, siteId, slug) {\n const { data, error } = await anonClient(creds)\n .schema(GR_SCHEMA)\n .from(GR_REPEATER_TABLE)\n .select(\"item_id, slug, sort_order\")\n .eq(\"site_id\", siteId)\n .eq(\"repeater_id\", repeaterId)\n .eq(\"slug\", slug)\n .maybeSingle();\n\n if (error) {\n console.error(\"Guardrail (Supabase): repeater item lookup failed\", error.message);\n return null;\n }\n return data ? toItem(data as ItemRow) : null;\n },\n\n async addItem(repeaterId, siteId, options) {\n if (options?.slug && !SLUG.test(options.slug)) {\n return { success: false, error: \"Invalid slug.\" };\n }\n\n const client = await sessionClient(creds);\n const table = client.schema(GR_SCHEMA).from(GR_REPEATER_TABLE);\n\n // Two round trips (read max, then insert) rather than one atomic step.\n // A race between two admins adding at the same instant could produce a\n // duplicate sort_order — cosmetic (a reorder fixes it), not a\n // correctness issue, and not worth a database function for how rarely\n // two people edit the same repeater at once.\n const { data: maxRow } = await table\n .select(\"sort_order\")\n .eq(\"site_id\", siteId)\n .eq(\"repeater_id\", repeaterId)\n .order(\"sort_order\", { ascending: false })\n .limit(1)\n .maybeSingle();\n\n const nextOrder = (maxRow?.sort_order ?? -1) + 1;\n const itemId = crypto.randomUUID();\n\n const { error } = await client\n .schema(GR_SCHEMA)\n .from(GR_REPEATER_TABLE)\n .insert({\n site_id: siteId,\n repeater_id: repeaterId,\n item_id: itemId,\n slug: options?.slug ?? null,\n sort_order: nextOrder,\n });\n\n if (error) {\n // 23505: unique_violation — the partial index on (site_id, repeater_id, slug).\n const message =\n error.code === \"23505\"\n ? \"That slug is already used by another item.\"\n : error.message;\n return { success: false, error: message };\n }\n\n return {\n success: true,\n item: { id: itemId, order: nextOrder, ...(options?.slug ? { slug: options.slug } : {}) },\n };\n },\n\n async removeItem(repeaterId, siteId, itemId) {\n const client = await sessionClient(creds);\n const { error } = await client\n .schema(GR_SCHEMA)\n .from(GR_REPEATER_TABLE)\n .delete()\n .eq(\"site_id\", siteId)\n .eq(\"repeater_id\", repeaterId)\n .eq(\"item_id\", itemId);\n return error ? { success: false, error: error.message } : { success: true };\n },\n\n async reorderItems(repeaterId, siteId, orderedItemIds) {\n const client = await sessionClient(creds);\n\n // One update per item. Repeaters are meant to stay small (cards on a\n // page, not a product catalog), so N small writes is the simple choice\n // over a single upsert-by-array RPC.\n for (const [index, itemId] of orderedItemIds.entries()) {\n const { error } = await client\n .schema(GR_SCHEMA)\n .from(GR_REPEATER_TABLE)\n .update({ sort_order: index })\n .eq(\"site_id\", siteId)\n .eq(\"repeater_id\", repeaterId)\n .eq(\"item_id\", itemId);\n if (error) return { success: false, error: error.message };\n }\n return { success: true };\n },\n };\n}\n"],"mappings":"AAAA,SAAS,YAAY,mBAAmB,qBAAqB;AAE7D,SAAS,mBAAmB,iBAAiB;AAU7C,SAAS,OAAO,KAA8B;AAC5C,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,OAAO,IAAI;AAAA,IACX,GAAI,IAAI,OAAO,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;AAAA,EACvC;AACF;AAEA,MAAM,OAAO;AAEN,SAAS,kBAAkB,OAA+C;AAC/E,oBAAkB,KAAK;AAEvB,SAAO;AAAA,IACL,MAAM,UAAU,YAAY,QAAQ;AAElC,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,WAAW,KAAK,EAC3C,OAAO,SAAS,EAChB,KAAK,iBAAiB,EACtB,OAAO,2BAA2B,EAClC,GAAG,WAAW,MAAM,EACpB,GAAG,eAAe,UAAU,EAC5B,MAAM,cAAc,EAAE,WAAW,KAAK,CAAC;AAE1C,UAAI,OAAO;AACT,gBAAQ,MAAM,oDAAoD,MAAM,OAAO;AAC/E,eAAO,CAAC;AAAA,MACV;AACA,cAAS,QAAQ,CAAC,GAAiB,IAAI,MAAM;AAAA,IAC/C;AAAA,IAEA,MAAM,cAAc,YAAY,QAAQ,MAAM;AAC5C,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,WAAW,KAAK,EAC3C,OAAO,SAAS,EAChB,KAAK,iBAAiB,EACtB,OAAO,2BAA2B,EAClC,GAAG,WAAW,MAAM,EACpB,GAAG,eAAe,UAAU,EAC5B,GAAG,QAAQ,IAAI,EACf,YAAY;AAEf,UAAI,OAAO;AACT,gBAAQ,MAAM,qDAAqD,MAAM,OAAO;AAChF,eAAO;AAAA,MACT;AACA,aAAO,OAAO,OAAO,IAAe,IAAI;AAAA,IAC1C;AAAA,IAEA,MAAM,QAAQ,YAAY,QAAQ,SAAS;AACzC,UAAI,SAAS,QAAQ,CAAC,KAAK,KAAK,QAAQ,IAAI,GAAG;AAC7C,eAAO,EAAE,SAAS,OAAO,OAAO,gBAAgB;AAAA,MAClD;AAEA,YAAM,SAAS,MAAM,cAAc,KAAK;AACxC,YAAM,QAAQ,OAAO,OAAO,SAAS,EAAE,KAAK,iBAAiB;AAO7D,YAAM,EAAE,MAAM,OAAO,IAAI,MAAM,MAC5B,OAAO,YAAY,EACnB,GAAG,WAAW,MAAM,EACpB,GAAG,eAAe,UAAU,EAC5B,MAAM,cAAc,EAAE,WAAW,MAAM,CAAC,EACxC,MAAM,CAAC,EACP,YAAY;AAEf,YAAM,aAAa,QAAQ,cAAc,MAAM;AAC/C,YAAM,SAAS,OAAO,WAAW;AAEjC,YAAM,EAAE,MAAM,IAAI,MAAM,OACrB,OAAO,SAAS,EAChB,KAAK,iBAAiB,EACtB,OAAO;AAAA,QACN,SAAS;AAAA,QACT,aAAa;AAAA,QACb,SAAS;AAAA,QACT,MAAM,SAAS,QAAQ;AAAA,QACvB,YAAY;AAAA,MACd,CAAC;AAEH,UAAI,OAAO;AAET,cAAM,UACJ,MAAM,SAAS,UACX,+CACA,MAAM;AACZ,eAAO,EAAE,SAAS,OAAO,OAAO,QAAQ;AAAA,MAC1C;AAEA,aAAO;AAAA,QACL,SAAS;AAAA,QACT,MAAM,EAAE,IAAI,QAAQ,OAAO,WAAW,GAAI,SAAS,OAAO,EAAE,MAAM,QAAQ,KAAK,IAAI,CAAC,EAAG;AAAA,MACzF;AAAA,IACF;AAAA,IAEA,MAAM,WAAW,YAAY,QAAQ,QAAQ;AAC3C,YAAM,SAAS,MAAM,cAAc,KAAK;AACxC,YAAM,EAAE,MAAM,IAAI,MAAM,OACrB,OAAO,SAAS,EAChB,KAAK,iBAAiB,EACtB,OAAO,EACP,GAAG,WAAW,MAAM,EACpB,GAAG,eAAe,UAAU,EAC5B,GAAG,WAAW,MAAM;AACvB,aAAO,QAAQ,EAAE,SAAS,OAAO,OAAO,MAAM,QAAQ,IAAI,EAAE,SAAS,KAAK;AAAA,IAC5E;AAAA,IAEA,MAAM,aAAa,YAAY,QAAQ,gBAAgB;AACrD,YAAM,SAAS,MAAM,cAAc,KAAK;AAKxC,iBAAW,CAAC,OAAO,MAAM,KAAK,eAAe,QAAQ,GAAG;AACtD,cAAM,EAAE,MAAM,IAAI,MAAM,OACrB,OAAO,SAAS,EAChB,KAAK,iBAAiB,EACtB,OAAO,EAAE,YAAY,MAAM,CAAC,EAC5B,GAAG,WAAW,MAAM,EACpB,GAAG,eAAe,UAAU,EAC5B,GAAG,WAAW,MAAM;AACvB,YAAI,MAAO,QAAO,EAAE,SAAS,OAAO,OAAO,MAAM,QAAQ;AAAA,MAC3D;AACA,aAAO,EAAE,SAAS,KAAK;AAAA,IACzB;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,18 @@
1
+ import { SupabaseCredentials } from './client.js';
2
+ import { GRStorageAdapter } from '../../core/adapter-types.js';
3
+ import '@supabase/supabase-js';
4
+ import 'react';
5
+ import '../../core/types.js';
6
+
7
+ declare const DEFAULT_BUCKET = "gr-media";
8
+ interface SupabaseStorageOptions extends SupabaseCredentials {
9
+ /**
10
+ * Bucket to store media in. Defaults to `gr-media`. Any name starting with
11
+ * `gr-` works: the init migration scopes its storage policies to that prefix, and a
12
+ * missing bucket is created on the first upload.
13
+ */
14
+ bucket?: string;
15
+ }
16
+ declare function supabaseStorage(options: SupabaseStorageOptions): GRStorageAdapter;
17
+
18
+ export { DEFAULT_BUCKET, type SupabaseStorageOptions, supabaseStorage };
@@ -0,0 +1,93 @@
1
+ import { assertCredentials, sessionClient } from "./client";
2
+ import { GR_SCHEMA } from "../../core/defs";
3
+ const DEFAULT_BUCKET = "gr-media";
4
+ function safeName(name) {
5
+ const dot = name.lastIndexOf(".");
6
+ const base = (dot > 0 ? name.slice(0, dot) : name).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "file";
7
+ const ext = dot > 0 ? name.slice(dot + 1).toLowerCase().replace(/[^a-z0-9]/g, "") : "";
8
+ return ext ? `${base}.${ext}` : base;
9
+ }
10
+ const BUCKET_NAME = /^gr-[a-z0-9][a-z0-9-]{0,50}$/;
11
+ const UUID_PREFIX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-/;
12
+ function supabaseStorage(options) {
13
+ assertCredentials(options);
14
+ const creds = { url: options.url, anonKey: options.anonKey };
15
+ const bucket = options.bucket || DEFAULT_BUCKET;
16
+ if (!BUCKET_NAME.test(bucket)) {
17
+ throw new Error(
18
+ `Guardrail (Supabase): bucket "${bucket}" must start with "gr-" and use lowercase letters, numbers, and hyphens \u2014 the init migration scopes storage policies to that prefix.`
19
+ );
20
+ }
21
+ let ensured = null;
22
+ async function ensureBucket(client) {
23
+ ensured ??= (async () => {
24
+ const { error } = await client.schema(GR_SCHEMA).rpc("gr_ensure_bucket", {
25
+ target_bucket: bucket
26
+ });
27
+ if (error) throw new Error(`could not ensure bucket "${bucket}" \u2014 ${error.message}`);
28
+ })();
29
+ try {
30
+ await ensured;
31
+ } catch (e) {
32
+ ensured = null;
33
+ throw e;
34
+ }
35
+ }
36
+ return {
37
+ async createUpload(siteId, file) {
38
+ const path = `${siteId}/${crypto.randomUUID()}-${safeName(file.name)}`;
39
+ const client = await sessionClient(creds);
40
+ await ensureBucket(client);
41
+ const storage = client.storage.from(bucket);
42
+ const { data, error } = await storage.createSignedUploadUrl(path);
43
+ if (error || !data) {
44
+ throw new Error(
45
+ `Guardrail (Supabase): could not create upload in "${bucket}" \u2014 ${error?.message ?? "no data"}`
46
+ );
47
+ }
48
+ return {
49
+ url: data.signedUrl,
50
+ method: "PUT",
51
+ headers: {
52
+ "content-type": file.contentType,
53
+ // Names are unique per upload, so the object never changes in place.
54
+ "cache-control": "max-age=31536000"
55
+ },
56
+ asset: {
57
+ id: path,
58
+ url: storage.getPublicUrl(path).data.publicUrl,
59
+ name: file.name,
60
+ size: file.size,
61
+ contentType: file.contentType
62
+ }
63
+ };
64
+ },
65
+ async listAssets(siteId, { limit = 100 } = {}) {
66
+ const storage = (await sessionClient(creds)).storage.from(bucket);
67
+ const { data, error } = await storage.list(siteId, {
68
+ limit,
69
+ sortBy: { column: "created_at", order: "desc" }
70
+ });
71
+ if (error) {
72
+ console.error("Guardrail (Supabase): list assets failed", error.message);
73
+ return [];
74
+ }
75
+ return (data ?? []).filter((f) => f.id && !f.name.startsWith(".")).map((f) => {
76
+ const path = `${siteId}/${f.name}`;
77
+ return {
78
+ id: path,
79
+ url: storage.getPublicUrl(path).data.publicUrl,
80
+ name: f.name.replace(UUID_PREFIX, ""),
81
+ size: f.metadata?.size,
82
+ contentType: f.metadata?.mimetype,
83
+ createdAt: f.created_at
84
+ };
85
+ });
86
+ }
87
+ };
88
+ }
89
+ export {
90
+ DEFAULT_BUCKET,
91
+ supabaseStorage
92
+ };
93
+ //# sourceMappingURL=storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/adapters/supabase/storage.ts"],"sourcesContent":["import type { SupabaseClient } from \"@supabase/supabase-js\";\nimport { assertCredentials, sessionClient } from \"./client\";\nimport type { SupabaseCredentials } from \"./client\";\nimport type { GRStorageAdapter } from \"../../core/adapter-types\";\nimport type { GRAsset } from \"../../core/types\";\nimport { GR_SCHEMA } from \"../../core/defs\";\n\nexport const DEFAULT_BUCKET = \"gr-media\";\n\nexport interface SupabaseStorageOptions extends SupabaseCredentials {\n /**\n * Bucket to store media in. Defaults to `gr-media`. Any name starting with\n * `gr-` works: the init migration scopes its storage policies to that prefix, and a\n * missing bucket is created on the first upload.\n */\n bucket?: string;\n}\n\n/** URL- and filesystem-safe object names that still read as the original file. */\nfunction safeName(name: string): string {\n const dot = name.lastIndexOf(\".\");\n const base =\n (dot > 0 ? name.slice(0, dot) : name)\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 60) || \"file\";\n const ext =\n dot > 0 ? name.slice(dot + 1).toLowerCase().replace(/[^a-z0-9]/g, \"\") : \"\";\n return ext ? `${base}.${ext}` : base;\n}\n\n/** Mirrors the check in gr_ensure_bucket(). */\nconst BUCKET_NAME = /^gr-[a-z0-9][a-z0-9-]{0,50}$/;\n\nconst UUID_PREFIX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-/;\n\nexport function supabaseStorage(options: SupabaseStorageOptions): GRStorageAdapter {\n assertCredentials(options);\n const creds = { url: options.url, anonKey: options.anonKey };\n const bucket = options.bucket || DEFAULT_BUCKET;\n\n // Fail at startup with a clear message rather than at upload time with an\n // opaque RLS denial.\n if (!BUCKET_NAME.test(bucket)) {\n throw new Error(\n `Guardrail (Supabase): bucket \"${bucket}\" must start with \"gr-\" and use ` +\n \"lowercase letters, numbers, and hyphens — the init migration scopes storage \" +\n \"policies to that prefix.\",\n );\n }\n\n /**\n * Created at most once per server process, and only on upload — browsing a\n * library that does not exist yet just shows it empty. A failure is not\n * cached, so the next upload retries.\n */\n let ensured: Promise<void> | null = null;\n async function ensureBucket(client: SupabaseClient): Promise<void> {\n ensured ??= (async () => {\n const { error } = await client.schema(GR_SCHEMA).rpc(\"gr_ensure_bucket\", {\n target_bucket: bucket,\n });\n if (error) throw new Error(`could not ensure bucket \"${bucket}\" — ${error.message}`);\n })();\n try {\n await ensured;\n } catch (e) {\n ensured = null;\n throw e;\n }\n }\n\n return {\n async createUpload(siteId, file) {\n // Namespaced by site: the storage RLS policies key off this first folder.\n const path = `${siteId}/${crypto.randomUUID()}-${safeName(file.name)}`;\n // Session client, so the signed URL is only issued if RLS lets this\n // editor insert into the site's folder.\n const client = await sessionClient(creds);\n await ensureBucket(client);\n const storage = client.storage.from(bucket);\n\n const { data, error } = await storage.createSignedUploadUrl(path);\n if (error || !data) {\n throw new Error(\n `Guardrail (Supabase): could not create upload in \"${bucket}\" — ${error?.message ?? \"no data\"}`,\n );\n }\n\n return {\n url: data.signedUrl,\n method: \"PUT\",\n headers: {\n \"content-type\": file.contentType,\n // Names are unique per upload, so the object never changes in place.\n \"cache-control\": \"max-age=31536000\",\n },\n asset: {\n id: path,\n url: storage.getPublicUrl(path).data.publicUrl,\n name: file.name,\n size: file.size,\n contentType: file.contentType,\n },\n };\n },\n\n async listAssets(siteId, { limit = 100 } = {}) {\n const storage = (await sessionClient(creds)).storage.from(bucket);\n const { data, error } = await storage.list(siteId, {\n limit,\n sortBy: { column: \"created_at\", order: \"desc\" },\n });\n if (error) {\n console.error(\"Guardrail (Supabase): list assets failed\", error.message);\n return [];\n }\n\n return (data ?? [])\n // Folders have no id, and Supabase drops a dotfile placeholder into empty ones.\n .filter((f) => f.id && !f.name.startsWith(\".\"))\n .map((f): GRAsset => {\n const path = `${siteId}/${f.name}`;\n return {\n id: path,\n url: storage.getPublicUrl(path).data.publicUrl,\n name: f.name.replace(UUID_PREFIX, \"\"),\n size: f.metadata?.size,\n contentType: f.metadata?.mimetype,\n createdAt: f.created_at,\n };\n });\n },\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,212 @@
1
+ import { ReactNode } from 'react';
2
+ import { OverrideRow, GRMember, GRRole, GRAsset, GRRepeaterItem } from './types.js';
3
+
4
+ interface GRStorageRow {
5
+ pathKey: string;
6
+ siteId: string;
7
+ type: string;
8
+ value: unknown;
9
+ }
10
+ interface GRDBAdapter {
11
+ /**
12
+ * Resolve many keys in one round trip.
13
+ *
14
+ * Batched rather than single-key because a page has one <GRail> per field;
15
+ * a per-key interface makes a 30-field page issue 30 queries. Keys are
16
+ * pre-deduplicated. Missing keys are simply absent from the result — order
17
+ * is not significant.
18
+ */
19
+ getOverrides: (pathKeys: string[], siteId: string) => Promise<OverrideRow[]>;
20
+ setOverride: (row: GRStorageRow) => Promise<{
21
+ success: boolean;
22
+ error?: string;
23
+ }>;
24
+ /**
25
+ * Bulk-remove every override whose pathKey starts with this prefix.
26
+ *
27
+ * Optional, and called only when a repeater item is deleted — its fields
28
+ * (`${repeaterId}.${itemId}.*`) would otherwise become unreachable orphans.
29
+ * An adapter that cannot do this cheaply may omit it; the item's manifest
30
+ * row is still removed, just not its old field values.
31
+ */
32
+ deleteOverrides?: (pathKeyPrefix: string, siteId: string) => Promise<void>;
33
+ }
34
+ /** Identity of a signed-in editor, as verified by the auth adapter. */
35
+ interface GRSession {
36
+ userId: string;
37
+ email?: string;
38
+ }
39
+ interface GRAuthAdapter {
40
+ /**
41
+ * Verify an email/password pair and establish a provider session.
42
+ *
43
+ * Optional: password auth is one mechanism among several. Providers built
44
+ * around magic links, SSO, or a hosted sign-in page omit this and set
45
+ * `signInUrl` instead, and Guardrail then hands sign-in off rather than
46
+ * rendering a form it cannot fulfil.
47
+ */
48
+ authenticate?: (email: string, password: string) => Promise<AuthResult>;
49
+ /**
50
+ * Where to send someone who needs to sign in, when this adapter does not
51
+ * accept passwords directly. Guardrail links to it instead of showing a form.
52
+ */
53
+ signInUrl?: string;
54
+ /**
55
+ * Ask the provider to create an account for an invited address and contact
56
+ * them. Optional — Guardrail falls back to the onInvite callback when the
57
+ * provider cannot do it.
58
+ *
59
+ * Account creation stays the identity provider's job. Guardrail records who
60
+ * is allowed to edit; it never owns credentials.
61
+ */
62
+ inviteUser?: (email: string, options?: {
63
+ redirectTo?: string;
64
+ }) => Promise<{
65
+ success: boolean;
66
+ error?: string;
67
+ }>;
68
+ /**
69
+ * Return the current verified session, or null.
70
+ *
71
+ * This is the sole authorization check for writes, so it MUST validate the
72
+ * session against the provider rather than trusting a cookie's presence.
73
+ */
74
+ getSession: () => Promise<GRSession | null>;
75
+ logout: () => Promise<void>;
76
+ }
77
+ type AuthResult = {
78
+ success: true;
79
+ session: GRSession;
80
+ } | {
81
+ success: false;
82
+ error: string;
83
+ };
84
+ /**
85
+ * Membership is Guardrail's own data, deliberately not the auth provider's.
86
+ * Keeping it separate means the identity vendor and the storage vendor never
87
+ * have to be the same one — the auth adapter only has to name the user.
88
+ */
89
+ interface GRMembershipAdapter {
90
+ getMember: (siteId: string, lookup: {
91
+ userId?: string;
92
+ email?: string;
93
+ }) => Promise<GRMember | null>;
94
+ listMembers: (siteId: string) => Promise<GRMember[]>;
95
+ upsertMember: (siteId: string, member: {
96
+ email: string;
97
+ role: GRRole;
98
+ userId?: string | null;
99
+ status: "active" | "pending";
100
+ }) => Promise<{
101
+ success: boolean;
102
+ error?: string;
103
+ }>;
104
+ removeMember: (siteId: string, email: string) => Promise<{
105
+ success: boolean;
106
+ error?: string;
107
+ }>;
108
+ /**
109
+ * Bind a pending invitation to the signed-in user.
110
+ *
111
+ * Separate from upsertMember because claiming must not be able to change the
112
+ * role it was invited with. Implementations must enforce that server-side —
113
+ * an RLS policy permissive enough to let someone write their own row is
114
+ * permissive enough to let them write themselves in as an admin.
115
+ */
116
+ claimInvite: (siteId: string) => Promise<void>;
117
+ }
118
+ /** Where and how the browser sends a file's bytes. */
119
+ interface GRUploadTarget {
120
+ url: string;
121
+ method: "PUT" | "POST";
122
+ headers?: Record<string, string>;
123
+ /** The asset as it will exist once the upload completes. */
124
+ asset: GRAsset;
125
+ }
126
+ /**
127
+ * File storage, deliberately separate from the database adapter so content and
128
+ * media can live with different providers — Supabase for content, UploadThing
129
+ * or S3 for images.
130
+ *
131
+ * Bytes never pass through the Next.js server: createUpload returns a signed
132
+ * target and the browser uploads straight to it. Server actions cap request
133
+ * bodies at around 1MB, which a single photo exceeds.
134
+ */
135
+ interface GRStorageAdapter {
136
+ createUpload: (siteId: string, file: {
137
+ name: string;
138
+ contentType: string;
139
+ size: number;
140
+ }) => Promise<GRUploadTarget>;
141
+ listAssets: (siteId: string, options?: {
142
+ limit?: number;
143
+ }) => Promise<GRAsset[]>;
144
+ }
145
+ /**
146
+ * Ordered instances of one developer-authored template component — the
147
+ * repeater's manifest. Deliberately not a content model: it tracks identity
148
+ * and order only. The shape of an item lives entirely in the developer's JSX,
149
+ * exactly like any other Guardrail field, so there is no schema to define, no
150
+ * field-type registry, and no per-repeater configuration beyond this.
151
+ *
152
+ * A separate slot from GRDBAdapter, mirroring storage, so the manifest can
153
+ * live with a different backend than content if that is ever useful — though
154
+ * unlike storage, an item's own fields always resolve through the configured
155
+ * `db` adapter, using the same pathKey convention as everything else.
156
+ */
157
+ interface GRRepeaterAdapter {
158
+ listItems: (repeaterId: string, siteId: string) => Promise<GRRepeaterItem[]>;
159
+ getItemBySlug: (repeaterId: string, siteId: string, slug: string) => Promise<GRRepeaterItem | null>;
160
+ addItem: (repeaterId: string, siteId: string, options?: {
161
+ slug?: string;
162
+ }) => Promise<{
163
+ success: true;
164
+ item: GRRepeaterItem;
165
+ } | {
166
+ success: false;
167
+ error: string;
168
+ }>;
169
+ removeItem: (repeaterId: string, siteId: string, itemId: string) => Promise<{
170
+ success: boolean;
171
+ error?: string;
172
+ }>;
173
+ /** Full replacement of order, since a reorder always touches every item. */
174
+ reorderItems: (repeaterId: string, siteId: string, orderedItemIds: string[]) => Promise<{
175
+ success: boolean;
176
+ error?: string;
177
+ }>;
178
+ }
179
+ interface GRInviteEvent {
180
+ email: string;
181
+ role: GRRole;
182
+ siteId: string;
183
+ invitedBy: string;
184
+ }
185
+ interface GRConfig {
186
+ db: GRDBAdapter;
187
+ auth: GRAuthAdapter;
188
+ members: GRMembershipAdapter;
189
+ /** Optional — only needed when images are editable. */
190
+ storage?: GRStorageAdapter;
191
+ /** Optional — only needed when a page uses <GRepeater>. */
192
+ repeaters?: GRRepeaterAdapter;
193
+ siteId: string | (() => string | Promise<string>);
194
+ /**
195
+ * Called when an admin invites someone. Guardrail records the pending
196
+ * membership; delivering the invitation is the app's job, using whatever
197
+ * mail provider it already has. In-process by design — a library running
198
+ * inside the developer's server has no need for webhooks.
199
+ */
200
+ onInvite?: (event: GRInviteEvent) => void | Promise<void>;
201
+ /**
202
+ * Extra content rendered in the toolbar pill, next to Team and Log out.
203
+ * Plain JSX, not a component reference or a structured item schema — the
204
+ * same "expose the primitive, developer owns the styling" rule as
205
+ * everything else in this library. Render it in a Server Component (a
206
+ * layout, typically) so it crosses into the toolbar's Client Component as
207
+ * an already-resolved element, not a function.
208
+ */
209
+ toolbarExtra?: ReactNode;
210
+ }
211
+
212
+ export type { AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget };
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=adapter-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Next.js and React signal "this render must go dynamic" by throwing. Those
3
+ * throws are control flow, not failures — swallowing one makes a route silently
4
+ * render with missing data instead of bailing out to dynamic rendering.
5
+ * Every catch around framework-touching code has to let them through.
6
+ */
7
+ declare function rethrowIfBailout(error: unknown): void;
8
+
9
+ export { rethrowIfBailout };
@@ -0,0 +1,14 @@
1
+ function rethrowIfBailout(error) {
2
+ if (typeof error !== "object" || error === null) return;
3
+ const digest = error.digest;
4
+ if (typeof digest === "string" && (digest === "DYNAMIC_SERVER_USAGE" || digest === "NEXT_NOT_FOUND" || digest === "NEXT_REDIRECT" || digest.startsWith("BAILOUT_TO_CLIENT_SIDE_RENDERING"))) {
5
+ throw error;
6
+ }
7
+ if (error.$$typeof === /* @__PURE__ */ Symbol.for("react.postpone")) {
8
+ throw error;
9
+ }
10
+ }
11
+ export {
12
+ rethrowIfBailout
13
+ };
14
+ //# sourceMappingURL=bailout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/core/bailout.ts"],"sourcesContent":["/**\n * Next.js and React signal \"this render must go dynamic\" by throwing. Those\n * throws are control flow, not failures — swallowing one makes a route silently\n * render with missing data instead of bailing out to dynamic rendering.\n * Every catch around framework-touching code has to let them through.\n */\nexport function rethrowIfBailout(error: unknown): void {\n if (typeof error !== \"object\" || error === null) return;\n\n const digest = (error as { digest?: unknown }).digest;\n if (\n typeof digest === \"string\" &&\n (digest === \"DYNAMIC_SERVER_USAGE\" ||\n digest === \"NEXT_NOT_FOUND\" ||\n digest === \"NEXT_REDIRECT\" ||\n digest.startsWith(\"BAILOUT_TO_CLIENT_SIDE_RENDERING\"))\n ) {\n throw error;\n }\n\n // React's PPR postpone signal.\n if ((error as { $$typeof?: symbol }).$$typeof === Symbol.for(\"react.postpone\")) {\n throw error;\n }\n}\n"],"mappings":"AAMO,SAAS,iBAAiB,OAAsB;AACrD,MAAI,OAAO,UAAU,YAAY,UAAU,KAAM;AAEjD,QAAM,SAAU,MAA+B;AAC/C,MACE,OAAO,WAAW,aACjB,WAAW,0BACV,WAAW,oBACX,WAAW,mBACX,OAAO,WAAW,kCAAkC,IACtD;AACA,UAAM;AAAA,EACR;AAGA,MAAK,MAAgC,aAAa,uBAAO,IAAI,gBAAgB,GAAG;AAC9E,UAAM;AAAA,EACR;AACF;","names":[]}
@@ -0,0 +1,27 @@
1
+ import { GRConfig, GRAuthAdapter, GRDBAdapter, GRMembershipAdapter, GRRepeaterAdapter, GRStorageAdapter } from './adapter-types.js';
2
+ import 'react';
3
+ import './types.js';
4
+
5
+ /**
6
+ * Registers the adapters Guardrail should use.
7
+ *
8
+ * Idempotent, and re-applied on every render by withGuardrail: Next.js does not
9
+ * guarantee one shared module instance across the SSR graph, the server-action
10
+ * graph, and the workers used for static generation, so config registered once
11
+ * at startup is simply absent in some of them.
12
+ *
13
+ * Core has no knowledge of any particular backend — there is no default and no
14
+ * fallback. An app names its adapters explicitly or Guardrail refuses to run.
15
+ */
16
+ declare function configureGuardrail(config: GRConfig): void;
17
+ declare function getDB(): GRDBAdapter;
18
+ declare function getAuth(): GRAuthAdapter;
19
+ declare function getMembers(): GRMembershipAdapter;
20
+ /** Optional — sites without editable images need no storage. */
21
+ declare function getStorage(): GRStorageAdapter | undefined;
22
+ /** Throws with a clear message if no page on this site uses <GRepeater>. */
23
+ declare function getRepeaters(): GRRepeaterAdapter;
24
+ declare function getInviteHandler(): GRConfig["onInvite"];
25
+ declare function resolveSiteId(): Promise<string>;
26
+
27
+ export { configureGuardrail, getAuth, getDB, getInviteHandler, getMembers, getRepeaters, getStorage, resolveSiteId };