@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,214 @@
1
+ "use server";
2
+ import { revalidatePath } from "next/cache";
3
+ import { headers } from "next/headers";
4
+ import {
5
+ getAuth,
6
+ getDB,
7
+ getInviteHandler,
8
+ getMembers,
9
+ getStorage,
10
+ resolveSiteId
11
+ } from "../core/config";
12
+ import { getCachedViewer } from "./session";
13
+ import { rethrowIfBailout } from "../core/bailout";
14
+ const DENIED = { success: false, error: "Unauthorized." };
15
+ async function requireRole(allowed) {
16
+ const viewer = await getCachedViewer();
17
+ if (!viewer || !viewer.role || !allowed.includes(viewer.role)) return null;
18
+ return viewer;
19
+ }
20
+ async function saveOverride(row) {
21
+ const viewer = await requireRole(["admin", "editor"]);
22
+ if (!viewer) return DENIED;
23
+ const rowToSend = {
24
+ pathKey: row.pathKey,
25
+ siteId: await resolveSiteId(),
26
+ type: row.value.type,
27
+ value: row.value
28
+ };
29
+ try {
30
+ const db = getDB();
31
+ const res = await db.setOverride(rowToSend);
32
+ if (!res.success) return res;
33
+ revalidatePath("/", "layout");
34
+ return { success: true };
35
+ } catch (e) {
36
+ rethrowIfBailout(e);
37
+ console.error("Guardrail: save failed", e);
38
+ return { success: false, error: "Database write failed." };
39
+ }
40
+ }
41
+ const IMAGE_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif", "image/avif"];
42
+ const MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
43
+ async function createAssetUpload(file) {
44
+ const viewer = await requireRole(["admin", "editor"]);
45
+ if (!viewer) return { success: false, error: "Unauthorized." };
46
+ const storage = getStorage();
47
+ if (!storage) {
48
+ return { success: false, error: "No storage adapter is configured." };
49
+ }
50
+ if (!IMAGE_TYPES.includes(file.contentType)) {
51
+ return { success: false, error: "Upload a PNG, JPEG, WebP, GIF, or AVIF image." };
52
+ }
53
+ if (!(file.size > 0) || file.size > MAX_UPLOAD_BYTES) {
54
+ return { success: false, error: "Images must be under 10 MB." };
55
+ }
56
+ try {
57
+ const target = await storage.createUpload(await resolveSiteId(), file);
58
+ return { success: true, target };
59
+ } catch (e) {
60
+ rethrowIfBailout(e);
61
+ console.error("Guardrail: createUpload failed", e);
62
+ const detail = process.env.NODE_ENV !== "production" && e instanceof Error ? ` ${e.message}` : "";
63
+ return { success: false, error: `Could not start the upload.${detail}` };
64
+ }
65
+ }
66
+ async function listAssets() {
67
+ const viewer = await requireRole(["admin", "editor"]);
68
+ const storage = getStorage();
69
+ if (!viewer || !storage) return [];
70
+ return storage.listAssets(await resolveSiteId());
71
+ }
72
+ async function listTeam() {
73
+ const viewer = await requireRole(["admin", "editor"]);
74
+ if (!viewer) return [];
75
+ const members = getMembers();
76
+ return members.listMembers(await resolveSiteId());
77
+ }
78
+ async function inviteMember(email, role) {
79
+ const viewer = await requireRole(["admin"]);
80
+ if (!viewer) return DENIED;
81
+ const normalized = email.toLowerCase().trim();
82
+ if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(normalized)) {
83
+ return { success: false, error: "Enter a valid email address." };
84
+ }
85
+ const siteId = await resolveSiteId();
86
+ const members = getMembers();
87
+ const existing = await members.getMember(siteId, { email: normalized });
88
+ if (existing?.status === "active") {
89
+ return { success: false, error: "That address is already on the team." };
90
+ }
91
+ if (!existing) {
92
+ const res = await members.upsertMember(siteId, {
93
+ email: normalized,
94
+ role,
95
+ userId: null,
96
+ status: "pending"
97
+ });
98
+ if (!res.success) return res;
99
+ }
100
+ const auth = getAuth();
101
+ if (auth.inviteUser) {
102
+ const sent = await auth.inviteUser(normalized, {
103
+ redirectTo: `${await requestOrigin()}/?edit=true`
104
+ });
105
+ revalidatePath("/", "layout");
106
+ return sent.success ? { success: true } : {
107
+ success: false,
108
+ error: `Invite saved, but the email failed: ${sent.error}`
109
+ };
110
+ }
111
+ try {
112
+ await getInviteHandler()?.({
113
+ email: normalized,
114
+ role: existing?.role ?? role,
115
+ siteId,
116
+ invitedBy: viewer.email ?? viewer.userId
117
+ });
118
+ } catch (e) {
119
+ console.error("Guardrail: onInvite handler threw", e);
120
+ }
121
+ revalidatePath("/", "layout");
122
+ return { success: true };
123
+ }
124
+ async function updateMemberRole(email, role) {
125
+ const viewer = await requireRole(["admin"]);
126
+ if (!viewer) return DENIED;
127
+ const siteId = await resolveSiteId();
128
+ const members = getMembers();
129
+ const target = await members.getMember(siteId, {
130
+ email: email.toLowerCase().trim()
131
+ });
132
+ if (!target) return { success: false, error: "No such member." };
133
+ if (target.role === "admin" && role !== "admin") {
134
+ const guard = await wouldStrandSite(siteId, target.email);
135
+ if (guard) return guard;
136
+ }
137
+ const res = await members.upsertMember(siteId, {
138
+ email: target.email,
139
+ role,
140
+ userId: target.userId,
141
+ status: target.status
142
+ });
143
+ if (res.success) revalidatePath("/", "layout");
144
+ return res;
145
+ }
146
+ async function removeMember(email) {
147
+ const viewer = await requireRole(["admin"]);
148
+ if (!viewer) return DENIED;
149
+ const siteId = await resolveSiteId();
150
+ const members = getMembers();
151
+ const normalized = email.toLowerCase().trim();
152
+ const guard = await wouldStrandSite(siteId, normalized);
153
+ if (guard) return guard;
154
+ const res = await members.removeMember(siteId, normalized);
155
+ if (res.success) revalidatePath("/", "layout");
156
+ return res;
157
+ }
158
+ async function requestOrigin() {
159
+ const h = await headers();
160
+ const origin = h.get("origin");
161
+ if (origin) return origin;
162
+ const host = h.get("x-forwarded-host") ?? h.get("host") ?? "localhost";
163
+ const proto = h.get("x-forwarded-proto") ?? (host.startsWith("localhost") ? "http" : "https");
164
+ return `${proto}://${host}`;
165
+ }
166
+ async function wouldStrandSite(siteId, email) {
167
+ const members = getMembers();
168
+ const all = await members.listMembers(siteId);
169
+ const admins = all.filter((m) => m.role === "admin");
170
+ if (admins.length <= 1 && admins.some((m) => m.email === email)) {
171
+ return {
172
+ success: false,
173
+ error: "A site must keep at least one admin."
174
+ };
175
+ }
176
+ return null;
177
+ }
178
+ async function loginAction(_prev, formData) {
179
+ const email = String(formData.get("email") ?? "");
180
+ const password = String(formData.get("password") ?? "");
181
+ const auth = getAuth();
182
+ if (!auth.authenticate) {
183
+ return {
184
+ success: false,
185
+ error: "This site's auth provider does not accept passwords.",
186
+ email
187
+ };
188
+ }
189
+ const result = await auth.authenticate(email, password);
190
+ if (!result.success) {
191
+ return { success: false, error: result.error, email };
192
+ }
193
+ revalidatePath("/", "layout");
194
+ return { success: true, email };
195
+ }
196
+ async function logoutAction() {
197
+ const auth = getAuth();
198
+ await auth.logout();
199
+ revalidatePath("/", "layout");
200
+ return { success: true };
201
+ }
202
+ export {
203
+ createAssetUpload,
204
+ inviteMember,
205
+ listAssets,
206
+ listTeam,
207
+ loginAction,
208
+ logoutAction,
209
+ removeMember,
210
+ requireRole,
211
+ saveOverride,
212
+ updateMemberRole
213
+ };
214
+ //# sourceMappingURL=actions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/actions.ts"],"sourcesContent":["\"use server\";\n\nimport { revalidatePath } from \"next/cache\";\nimport { headers } from \"next/headers\";\nimport {\n getAuth,\n getDB,\n getInviteHandler,\n getMembers,\n getStorage,\n resolveSiteId,\n} from \"../core/config\";\nimport { getCachedViewer } from \"./session\";\nimport { rethrowIfBailout } from \"../core/bailout\";\nimport type {\n ActionState,\n GRAsset,\n GRMember,\n GRRole,\n GRViewer,\n OverrideRow,\n} from \"../core/types\";\nimport type { GRStorageRow, GRUploadTarget } from \"../core/adapter-types\";\n\ntype Result = { success: boolean; error?: string };\n\nconst DENIED: Result = { success: false, error: \"Unauthorized.\" };\n\n/**\n * Every mutation re-resolves the viewer server-side. The client's copy of the\n * role decides what to *show*; it never decides what is *allowed*. A hidden\n * button is not authorization — this is the same lesson as the session cookie.\n */\nexport async function requireRole(\n allowed: readonly GRRole[],\n): Promise<GRViewer | null> {\n const viewer = await getCachedViewer();\n if (!viewer || !viewer.role || !allowed.includes(viewer.role)) return null;\n return viewer;\n}\n\nexport async function saveOverride(row: OverrideRow): Promise<Result> {\n const viewer = await requireRole([\"admin\", \"editor\"]);\n if (!viewer) return DENIED;\n\n const rowToSend: GRStorageRow = {\n pathKey: row.pathKey,\n siteId: await resolveSiteId(),\n type: row.value.type,\n value: row.value,\n };\n\n try {\n const db = getDB();\n const res = await db.setOverride(rowToSend);\n if (!res.success) return res;\n\n // TODO: too broad — purges every cached route on any single-field save.\n revalidatePath(\"/\", \"layout\");\n return { success: true };\n } catch (e) {\n rethrowIfBailout(e);\n console.error(\"Guardrail: save failed\", e);\n return { success: false, error: \"Database write failed.\" };\n }\n}\n\n/* --------------------------------------------------------------- media --- */\n\n// SVG is excluded on purpose: it can carry script, and next/image refuses it\n// by default anyway.\nconst IMAGE_TYPES = [\"image/png\", \"image/jpeg\", \"image/webp\", \"image/gif\", \"image/avif\"];\nconst MAX_UPLOAD_BYTES = 10 * 1024 * 1024;\n\n/**\n * Authorizes an upload and returns where the browser should send the bytes.\n * Only metadata comes through here — the file itself goes straight to storage.\n */\nexport async function createAssetUpload(file: {\n name: string;\n contentType: string;\n size: number;\n}): Promise<\n { success: true; target: GRUploadTarget } | { success: false; error: string }\n> {\n const viewer = await requireRole([\"admin\", \"editor\"]);\n if (!viewer) return { success: false, error: \"Unauthorized.\" };\n\n const storage = getStorage();\n if (!storage) {\n return { success: false, error: \"No storage adapter is configured.\" };\n }\n if (!IMAGE_TYPES.includes(file.contentType)) {\n return { success: false, error: \"Upload a PNG, JPEG, WebP, GIF, or AVIF image.\" };\n }\n if (!(file.size > 0) || file.size > MAX_UPLOAD_BYTES) {\n return { success: false, error: \"Images must be under 10 MB.\" };\n }\n\n try {\n const target = await storage.createUpload(await resolveSiteId(), file);\n return { success: true, target };\n } catch (e) {\n rethrowIfBailout(e);\n console.error(\"Guardrail: createUpload failed\", e);\n // Failures here are almost always setup (missing SQL, bucket, or policy),\n // so show the cause while developing. Production keeps it generic rather\n // than exposing backend detail to editors.\n const detail =\n process.env.NODE_ENV !== \"production\" && e instanceof Error\n ? ` ${e.message}`\n : \"\";\n return { success: false, error: `Could not start the upload.${detail}` };\n }\n}\n\nexport async function listAssets(): Promise<GRAsset[]> {\n const viewer = await requireRole([\"admin\", \"editor\"]);\n const storage = getStorage();\n if (!viewer || !storage) return [];\n return storage.listAssets(await resolveSiteId());\n}\n\n/* ---------------------------------------------------------------- team --- */\n\nexport async function listTeam(): Promise<GRMember[]> {\n // Any member may see who else is on the site; only admins may change it.\n const viewer = await requireRole([\"admin\", \"editor\"]);\n if (!viewer) return [];\n\n const members = getMembers();\n return members.listMembers(await resolveSiteId());\n}\n\nexport async function inviteMember(\n email: string,\n role: GRRole,\n): Promise<Result> {\n const viewer = await requireRole([\"admin\"]);\n if (!viewer) return DENIED;\n\n const normalized = email.toLowerCase().trim();\n if (!/^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$/.test(normalized)) {\n return { success: false, error: \"Enter a valid email address.\" };\n }\n\n const siteId = await resolveSiteId();\n const members = getMembers();\n\n const existing = await members.getMember(siteId, { email: normalized });\n if (existing?.status === \"active\") {\n return { success: false, error: \"That address is already on the team.\" };\n }\n\n // A pending invite is re-sent rather than rejected: provider email is\n // rate-limited and unreliable enough that resending has to be possible.\n if (!existing) {\n const res = await members.upsertMember(siteId, {\n email: normalized,\n role,\n userId: null,\n status: \"pending\",\n });\n if (!res.success) return res;\n }\n\n const auth = getAuth();\n if (auth.inviteUser) {\n // The membership is already written; a failed send must not undo it.\n const sent = await auth.inviteUser(normalized, {\n redirectTo: `${await requestOrigin()}/?edit=true`,\n });\n revalidatePath(\"/\", \"layout\");\n return sent.success\n ? { success: true }\n : {\n success: false,\n error: `Invite saved, but the email failed: ${sent.error}`,\n };\n }\n\n // No provider-side delivery, so hand off to the app.\n try {\n await getInviteHandler()?.({\n email: normalized,\n role: existing?.role ?? role,\n siteId,\n invitedBy: viewer.email ?? viewer.userId,\n });\n } catch (e) {\n console.error(\"Guardrail: onInvite handler threw\", e);\n }\n\n revalidatePath(\"/\", \"layout\");\n return { success: true };\n}\n\nexport async function updateMemberRole(\n email: string,\n role: GRRole,\n): Promise<Result> {\n const viewer = await requireRole([\"admin\"]);\n if (!viewer) return DENIED;\n\n const siteId = await resolveSiteId();\n const members = getMembers();\n\n const target = await members.getMember(siteId, {\n email: email.toLowerCase().trim(),\n });\n if (!target) return { success: false, error: \"No such member.\" };\n\n if (target.role === \"admin\" && role !== \"admin\") {\n const guard = await wouldStrandSite(siteId, target.email);\n if (guard) return guard;\n }\n\n const res = await members.upsertMember(siteId, {\n email: target.email,\n role,\n userId: target.userId,\n status: target.status,\n });\n if (res.success) revalidatePath(\"/\", \"layout\");\n return res;\n}\n\nexport async function removeMember(email: string): Promise<Result> {\n const viewer = await requireRole([\"admin\"]);\n if (!viewer) return DENIED;\n\n const siteId = await resolveSiteId();\n const members = getMembers();\n const normalized = email.toLowerCase().trim();\n\n const guard = await wouldStrandSite(siteId, normalized);\n if (guard) return guard;\n\n const res = await members.removeMember(siteId, normalized);\n if (res.success) revalidatePath(\"/\", \"layout\");\n return res;\n}\n\nasync function requestOrigin(): Promise<string> {\n const h = await headers();\n const origin = h.get(\"origin\");\n if (origin) return origin;\n const host = h.get(\"x-forwarded-host\") ?? h.get(\"host\") ?? \"localhost\";\n const proto = h.get(\"x-forwarded-proto\") ?? (host.startsWith(\"localhost\") ? \"http\" : \"https\");\n return `${proto}://${host}`;\n}\n\n/**\n * Refuses changes that would leave a site with no admin, which would lock\n * everyone out of team management with no in-app way to recover.\n */\nasync function wouldStrandSite(\n siteId: string,\n email: string,\n): Promise<Result | null> {\n const members = getMembers();\n const all = await members.listMembers(siteId);\n const admins = all.filter((m) => m.role === \"admin\");\n\n if (admins.length <= 1 && admins.some((m) => m.email === email)) {\n return {\n success: false,\n error: \"A site must keep at least one admin.\",\n };\n }\n return null;\n}\n\n/* ---------------------------------------------------------------- auth --- */\n\nexport async function loginAction(\n _prev: ActionState,\n formData: FormData,\n): Promise<ActionState> {\n const email = String(formData.get(\"email\") ?? \"\");\n const password = String(formData.get(\"password\") ?? \"\");\n\n const auth = getAuth();\n if (!auth.authenticate) {\n return {\n success: false,\n error: \"This site's auth provider does not accept passwords.\",\n email,\n };\n }\n\n const result = await auth.authenticate(email, password);\n if (!result.success) {\n return { success: false, error: result.error, email };\n }\n\n revalidatePath(\"/\", \"layout\");\n return { success: true, email };\n}\n\nexport async function logoutAction(): Promise<{ success: boolean }> {\n const auth = getAuth();\n await auth.logout();\n revalidatePath(\"/\", \"layout\");\n return { success: true };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,10 @@
1
+ import { OverrideRow } from '../core/types.js';
2
+ import 'react';
3
+
4
+ /**
5
+ * Server-side fetch for a single field. Runs during SSR/ISR so anonymous
6
+ * visitors get the real content in the initial HTML.
7
+ */
8
+ declare function fetchOrScaffold(pathKey: string, props: Record<string, unknown>): Promise<OverrideRow>;
9
+
10
+ export { fetchOrScaffold };
@@ -0,0 +1,24 @@
1
+ import { loadOverride } from "./loader";
2
+ async function fetchOrScaffold(pathKey, props) {
3
+ const row = await loadOverride(pathKey);
4
+ return row ?? generateScaffold(pathKey, props);
5
+ }
6
+ function generateScaffold(pathKey, props) {
7
+ const { children, type, src, alt, href } = props;
8
+ const defaultText = typeof children === "string" || typeof children === "number" ? String(children) : "";
9
+ let value;
10
+ if (type === "image" || src) {
11
+ value = { type: "image", src: src ?? "", alt: alt ?? "" };
12
+ } else if (type === "link" || href) {
13
+ value = { type: "link", href: href ?? "", text: defaultText };
14
+ } else if (type === "button") {
15
+ value = { type: "button", text: defaultText };
16
+ } else {
17
+ value = { type: "text", text: defaultText };
18
+ }
19
+ return { id: `scaffold-${pathKey}`, pathKey, value };
20
+ }
21
+ export {
22
+ fetchOrScaffold
23
+ };
24
+ //# sourceMappingURL=data.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/data.ts"],"sourcesContent":["import type { OverrideRow, OverrideValue } from \"../core/types\";\nimport { loadOverride } from \"./loader\";\n\n/**\n * Server-side fetch for a single field. Runs during SSR/ISR so anonymous\n * visitors get the real content in the initial HTML.\n */\nexport async function fetchOrScaffold(\n pathKey: string,\n props: Record<string, unknown>,\n): Promise<OverrideRow> {\n // Adapters normalize storage quirks (stringified JSON, column naming), so\n // anything returned here is already a domain-shaped row.\n const row = await loadOverride(pathKey);\n return row ?? generateScaffold(pathKey, props);\n}\n\n/**\n * Builds a default row from the JSX the developer already wrote, so an\n * un-edited field renders its hardcoded content. Deliberately does not write\n * to the database — nothing is persisted until an editor saves.\n */\nfunction generateScaffold(\n pathKey: string,\n props: Record<string, unknown>,\n): OverrideRow {\n const { children, type, src, alt, href } = props as {\n children?: unknown;\n type?: string;\n src?: string;\n alt?: string;\n href?: string;\n };\n\n const defaultText =\n typeof children === \"string\" || typeof children === \"number\"\n ? String(children)\n : \"\";\n\n let value: OverrideValue;\n\n if (type === \"image\" || src) {\n value = { type: \"image\", src: src ?? \"\", alt: alt ?? \"\" };\n } else if (type === \"link\" || href) {\n value = { type: \"link\", href: href ?? \"\", text: defaultText };\n } else if (type === \"button\") {\n value = { type: \"button\", text: defaultText };\n } else {\n value = { type: \"text\", text: defaultText };\n }\n\n return { id: `scaffold-${pathKey}`, pathKey, value };\n}\n"],"mappings":"AACA,SAAS,oBAAoB;AAM7B,eAAsB,gBACpB,SACA,OACsB;AAGtB,QAAM,MAAM,MAAM,aAAa,OAAO;AACtC,SAAO,OAAO,iBAAiB,SAAS,KAAK;AAC/C;AAOA,SAAS,iBACP,SACA,OACa;AACb,QAAM,EAAE,UAAU,MAAM,KAAK,KAAK,KAAK,IAAI;AAQ3C,QAAM,cACJ,OAAO,aAAa,YAAY,OAAO,aAAa,WAChD,OAAO,QAAQ,IACf;AAEN,MAAI;AAEJ,MAAI,SAAS,WAAW,KAAK;AAC3B,YAAQ,EAAE,MAAM,SAAS,KAAK,OAAO,IAAI,KAAK,OAAO,GAAG;AAAA,EAC1D,WAAW,SAAS,UAAU,MAAM;AAClC,YAAQ,EAAE,MAAM,QAAQ,MAAM,QAAQ,IAAI,MAAM,YAAY;AAAA,EAC9D,WAAW,SAAS,UAAU;AAC5B,YAAQ,EAAE,MAAM,UAAU,MAAM,YAAY;AAAA,EAC9C,OAAO;AACL,YAAQ,EAAE,MAAM,QAAQ,MAAM,YAAY;AAAA,EAC5C;AAEA,SAAO,EAAE,IAAI,YAAY,OAAO,IAAI,SAAS,MAAM;AACrD;","names":[]}
@@ -0,0 +1,26 @@
1
+ import { OverrideValue } from '../core/types.js';
2
+ import 'react';
3
+
4
+ /**
5
+ * Reads a field's current value with no rendering attached.
6
+ *
7
+ * <GRail> covers "render this as an editable element." This covers "give me
8
+ * the data so I can build something GRail doesn't render itself" — an embed
9
+ * iframe's src, an og:image meta tag, structured data, a third-party widget's
10
+ * config.
11
+ *
12
+ * Read-only: this does not make a field editable on its own. Give the same
13
+ * id a normal <GRail> somewhere for an editor to change it; this just reads
14
+ * whatever that field currently holds.
15
+ *
16
+ * Shares <GRail>'s request-scoped batching loader, so calling this alongside
17
+ * other fields — including several of these via Promise.all — still resolves
18
+ * in one query, not one per call.
19
+ *
20
+ * Returns null when nothing has been saved yet. There is no `children`
21
+ * fallback to fall back to here, since nothing is rendered — supply your own
22
+ * default in the calling code.
23
+ */
24
+ declare function getFieldValue(id: string): Promise<OverrideValue | null>;
25
+
26
+ export { getFieldValue };
@@ -0,0 +1,9 @@
1
+ import { loadOverride } from "./loader";
2
+ async function getFieldValue(id) {
3
+ const row = await loadOverride(id);
4
+ return row?.value ?? null;
5
+ }
6
+ export {
7
+ getFieldValue
8
+ };
9
+ //# sourceMappingURL=fieldData.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/fieldData.ts"],"sourcesContent":["import { loadOverride } from \"./loader\";\nimport type { OverrideValue } from \"../core/types\";\n\n/**\n * Reads a field's current value with no rendering attached.\n *\n * <GRail> covers \"render this as an editable element.\" This covers \"give me\n * the data so I can build something GRail doesn't render itself\" — an embed\n * iframe's src, an og:image meta tag, structured data, a third-party widget's\n * config.\n *\n * Read-only: this does not make a field editable on its own. Give the same\n * id a normal <GRail> somewhere for an editor to change it; this just reads\n * whatever that field currently holds.\n *\n * Shares <GRail>'s request-scoped batching loader, so calling this alongside\n * other fields — including several of these via Promise.all — still resolves\n * in one query, not one per call.\n *\n * Returns null when nothing has been saved yet. There is no `children`\n * fallback to fall back to here, since nothing is rendered — supply your own\n * default in the calling code.\n */\nexport async function getFieldValue(id: string): Promise<OverrideValue | null> {\n const row = await loadOverride(id);\n return row?.value ?? null;\n}\n"],"mappings":"AAAA,SAAS,oBAAoB;AAuB7B,eAAsB,cAAc,IAA2C;AAC7E,QAAM,MAAM,MAAM,aAAa,EAAE;AACjC,SAAO,KAAK,SAAS;AACvB;","names":[]}
@@ -0,0 +1,6 @@
1
+ import { OverrideRow } from '../core/types.js';
2
+ import 'react';
3
+
4
+ declare function loadOverride(pathKey: string): Promise<OverrideRow | null>;
5
+
6
+ export { loadOverride };
@@ -0,0 +1,53 @@
1
+ import { cache } from "react";
2
+ import { getDB, resolveSiteId } from "../core/config";
3
+ import { rethrowIfBailout } from "../core/bailout";
4
+ const getLoader = cache(() => {
5
+ let pending = [];
6
+ let scheduled = false;
7
+ const flush = async () => {
8
+ const batch = pending;
9
+ pending = [];
10
+ scheduled = false;
11
+ if (batch.length === 0) return;
12
+ const keys = [...new Set(batch.map((p) => p.key))];
13
+ try {
14
+ const db = getDB();
15
+ const siteId = await resolveSiteId();
16
+ const started = Date.now();
17
+ const rows = await db.getOverrides(keys, siteId);
18
+ if (process.env.GUARDRAIL_DEBUG) {
19
+ console.log(
20
+ `[guardrail] batch: ${keys.length} key(s) -> ${rows.length} row(s) in 1 query (${Date.now() - started}ms) site=${siteId} keys=${keys.join(", ")}`
21
+ );
22
+ }
23
+ const byKey = new Map(rows.map((r) => [r.pathKey, r]));
24
+ for (const p of batch) p.resolve(byKey.get(p.key) ?? null);
25
+ } catch (e) {
26
+ for (const p of batch) p.reject(e);
27
+ }
28
+ };
29
+ return {
30
+ load(key) {
31
+ return new Promise((resolve, reject) => {
32
+ pending.push({ key, resolve, reject });
33
+ if (!scheduled) {
34
+ scheduled = true;
35
+ queueMicrotask(flush);
36
+ }
37
+ });
38
+ }
39
+ };
40
+ });
41
+ async function loadOverride(pathKey) {
42
+ try {
43
+ return await getLoader().load(pathKey);
44
+ } catch (e) {
45
+ rethrowIfBailout(e);
46
+ console.error("Guardrail: failed to load override", e);
47
+ return null;
48
+ }
49
+ }
50
+ export {
51
+ loadOverride
52
+ };
53
+ //# sourceMappingURL=loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/loader.ts"],"sourcesContent":["import { cache } from \"react\";\nimport { getDB, resolveSiteId } from \"../core/config\";\nimport { rethrowIfBailout } from \"../core/bailout\";\nimport type { OverrideRow } from \"../core/types\";\n\ninterface Pending {\n key: string;\n resolve: (row: OverrideRow | null) => void;\n reject: (error: unknown) => void;\n}\n\n/**\n * Request-scoped batching loader.\n *\n * Every <GRail> resolves its own field, so a naive implementation issues one\n * query per field. This collects the keys requested within a microtask and\n * flushes them as a single query. React renders siblings concurrently, so a\n * page's fields land in the same batch; a field nested behind another await\n * simply falls into a later batch rather than degrading to one query each.\n *\n * cache() scopes the queue to a single request so keys never leak between\n * concurrent renders.\n */\nconst getLoader = cache(() => {\n let pending: Pending[] = [];\n let scheduled = false;\n\n const flush = async () => {\n const batch = pending;\n pending = [];\n scheduled = false;\n if (batch.length === 0) return;\n\n // One lookup per distinct key, even if several fields share it.\n const keys = [...new Set(batch.map((p) => p.key))];\n\n try {\n const db = getDB();\n const siteId = await resolveSiteId();\n const started = Date.now();\n const rows = await db.getOverrides(keys, siteId);\n\n if (process.env.GUARDRAIL_DEBUG) {\n console.log(\n `[guardrail] batch: ${keys.length} key(s) -> ${rows.length} row(s) ` +\n `in 1 query (${Date.now() - started}ms) ` +\n `site=${siteId} keys=${keys.join(\", \")}`,\n );\n }\n const byKey = new Map(rows.map((r) => [r.pathKey, r]));\n for (const p of batch) p.resolve(byKey.get(p.key) ?? null);\n } catch (e) {\n for (const p of batch) p.reject(e);\n }\n };\n\n return {\n load(key: string): Promise<OverrideRow | null> {\n return new Promise<OverrideRow | null>((resolve, reject) => {\n pending.push({ key, resolve, reject });\n if (!scheduled) {\n scheduled = true;\n queueMicrotask(flush);\n }\n });\n },\n };\n});\n\nexport async function loadOverride(\n pathKey: string,\n): Promise<OverrideRow | null> {\n try {\n return await getLoader().load(pathKey);\n } catch (e) {\n rethrowIfBailout(e);\n console.error(\"Guardrail: failed to load override\", e);\n return null;\n }\n}\n"],"mappings":"AAAA,SAAS,aAAa;AACtB,SAAS,OAAO,qBAAqB;AACrC,SAAS,wBAAwB;AAqBjC,MAAM,YAAY,MAAM,MAAM;AAC5B,MAAI,UAAqB,CAAC;AAC1B,MAAI,YAAY;AAEhB,QAAM,QAAQ,YAAY;AACxB,UAAM,QAAQ;AACd,cAAU,CAAC;AACX,gBAAY;AACZ,QAAI,MAAM,WAAW,EAAG;AAGxB,UAAM,OAAO,CAAC,GAAG,IAAI,IAAI,MAAM,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAEjD,QAAI;AACF,YAAM,KAAK,MAAM;AACjB,YAAM,SAAS,MAAM,cAAc;AACnC,YAAM,UAAU,KAAK,IAAI;AACzB,YAAM,OAAO,MAAM,GAAG,aAAa,MAAM,MAAM;AAE/C,UAAI,QAAQ,IAAI,iBAAiB;AAC/B,gBAAQ;AAAA,UACN,sBAAsB,KAAK,MAAM,cAAc,KAAK,MAAM,uBACzC,KAAK,IAAI,IAAI,OAAO,YAC3B,MAAM,SAAS,KAAK,KAAK,IAAI,CAAC;AAAA,QAC1C;AAAA,MACF;AACA,YAAM,QAAQ,IAAI,IAAI,KAAK,IAAI,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC;AACrD,iBAAW,KAAK,MAAO,GAAE,QAAQ,MAAM,IAAI,EAAE,GAAG,KAAK,IAAI;AAAA,IAC3D,SAAS,GAAG;AACV,iBAAW,KAAK,MAAO,GAAE,OAAO,CAAC;AAAA,IACnC;AAAA,EACF;AAEA,SAAO;AAAA,IACL,KAAK,KAA0C;AAC7C,aAAO,IAAI,QAA4B,CAAC,SAAS,WAAW;AAC1D,gBAAQ,KAAK,EAAE,KAAK,SAAS,OAAO,CAAC;AACrC,YAAI,CAAC,WAAW;AACd,sBAAY;AACZ,yBAAe,KAAK;AAAA,QACtB;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AACF,CAAC;AAED,eAAsB,aACpB,SAC6B;AAC7B,MAAI;AACF,WAAO,MAAM,UAAU,EAAE,KAAK,OAAO;AAAA,EACvC,SAAS,GAAG;AACV,qBAAiB,CAAC;AAClB,YAAQ,MAAM,sCAAsC,CAAC;AACrD,WAAO;AAAA,EACT;AACF;","names":[]}
@@ -0,0 +1,20 @@
1
+ import { GRRepeaterItem } from '../core/types.js';
2
+ import 'react';
3
+
4
+ type Result = {
5
+ success: boolean;
6
+ error?: string;
7
+ };
8
+ declare function addRepeaterItem(repeaterId: string, options?: {
9
+ slug?: string;
10
+ }): Promise<{
11
+ success: true;
12
+ item: GRRepeaterItem;
13
+ } | {
14
+ success: false;
15
+ error: string;
16
+ }>;
17
+ declare function removeRepeaterItem(repeaterId: string, itemId: string): Promise<Result>;
18
+ declare function reorderRepeaterItems(repeaterId: string, orderedItemIds: string[]): Promise<Result>;
19
+
20
+ export { addRepeaterItem, removeRepeaterItem, reorderRepeaterItems };
@@ -0,0 +1,72 @@
1
+ "use server";
2
+ import { revalidatePath } from "next/cache";
3
+ import { getDB, getRepeaters, resolveSiteId } from "../core/config";
4
+ import { requireRole } from "./actions";
5
+ import { rethrowIfBailout } from "../core/bailout";
6
+ const DENIED = { success: false, error: "Unauthorized." };
7
+ async function addRepeaterItem(repeaterId, options) {
8
+ const viewer = await requireRole(["admin", "editor"]);
9
+ if (!viewer) return { success: false, error: "Unauthorized." };
10
+ if (options?.slug !== void 0) {
11
+ const slug = options.slug.trim();
12
+ if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(slug)) {
13
+ return {
14
+ success: false,
15
+ error: "Use lowercase letters, numbers, and hyphens only."
16
+ };
17
+ }
18
+ options = { slug };
19
+ }
20
+ try {
21
+ const repeaters = getRepeaters();
22
+ const res = await repeaters.addItem(repeaterId, await resolveSiteId(), options);
23
+ if (res.success) revalidatePath("/", "layout");
24
+ return res;
25
+ } catch (e) {
26
+ rethrowIfBailout(e);
27
+ console.error("Guardrail: addRepeaterItem failed", e);
28
+ return { success: false, error: "Could not add the item." };
29
+ }
30
+ }
31
+ async function removeRepeaterItem(repeaterId, itemId) {
32
+ const viewer = await requireRole(["admin", "editor"]);
33
+ if (!viewer) return DENIED;
34
+ try {
35
+ const siteId = await resolveSiteId();
36
+ const repeaters = getRepeaters();
37
+ const res = await repeaters.removeItem(repeaterId, siteId, itemId);
38
+ if (!res.success) return res;
39
+ const db = getDB();
40
+ await db.deleteOverrides?.(`${repeaterId}.${itemId}.`, siteId);
41
+ revalidatePath("/", "layout");
42
+ return { success: true };
43
+ } catch (e) {
44
+ rethrowIfBailout(e);
45
+ console.error("Guardrail: removeRepeaterItem failed", e);
46
+ return { success: false, error: "Could not remove the item." };
47
+ }
48
+ }
49
+ async function reorderRepeaterItems(repeaterId, orderedItemIds) {
50
+ const viewer = await requireRole(["admin", "editor"]);
51
+ if (!viewer) return DENIED;
52
+ try {
53
+ const repeaters = getRepeaters();
54
+ const res = await repeaters.reorderItems(
55
+ repeaterId,
56
+ await resolveSiteId(),
57
+ orderedItemIds
58
+ );
59
+ if (res.success) revalidatePath("/", "layout");
60
+ return res;
61
+ } catch (e) {
62
+ rethrowIfBailout(e);
63
+ console.error("Guardrail: reorderRepeaterItems failed", e);
64
+ return { success: false, error: "Could not reorder the items." };
65
+ }
66
+ }
67
+ export {
68
+ addRepeaterItem,
69
+ removeRepeaterItem,
70
+ reorderRepeaterItems
71
+ };
72
+ //# sourceMappingURL=repeaterActions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/repeaterActions.ts"],"sourcesContent":["\"use server\";\n\nimport { revalidatePath } from \"next/cache\";\nimport { getDB, getRepeaters, resolveSiteId } from \"../core/config\";\nimport { requireRole } from \"./actions\";\nimport { rethrowIfBailout } from \"../core/bailout\";\nimport type { GRRepeaterItem } from \"../core/types\";\n\ntype Result = { success: boolean; error?: string };\n\nconst DENIED: Result = { success: false, error: \"Unauthorized.\" };\n\nexport async function addRepeaterItem(\n repeaterId: string,\n options?: { slug?: string },\n): Promise<\n { success: true; item: GRRepeaterItem } | { success: false; error: string }\n> {\n const viewer = await requireRole([\"admin\", \"editor\"]);\n if (!viewer) return { success: false, error: \"Unauthorized.\" };\n\n if (options?.slug !== undefined) {\n const slug = options.slug.trim();\n if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(slug)) {\n return {\n success: false,\n error: \"Use lowercase letters, numbers, and hyphens only.\",\n };\n }\n options = { slug };\n }\n\n try {\n const repeaters = getRepeaters();\n const res = await repeaters.addItem(repeaterId, await resolveSiteId(), options);\n if (res.success) revalidatePath(\"/\", \"layout\");\n return res;\n } catch (e) {\n rethrowIfBailout(e);\n console.error(\"Guardrail: addRepeaterItem failed\", e);\n return { success: false, error: \"Could not add the item.\" };\n }\n}\n\nexport async function removeRepeaterItem(\n repeaterId: string,\n itemId: string,\n): Promise<Result> {\n const viewer = await requireRole([\"admin\", \"editor\"]);\n if (!viewer) return DENIED;\n\n try {\n const siteId = await resolveSiteId();\n const repeaters = getRepeaters();\n const res = await repeaters.removeItem(repeaterId, siteId, itemId);\n if (!res.success) return res;\n\n // Best-effort: an adapter that can't do this cheaply just leaves the old\n // field values behind, unreachable but harmless.\n const db = getDB();\n await db.deleteOverrides?.(`${repeaterId}.${itemId}.`, siteId);\n\n revalidatePath(\"/\", \"layout\");\n return { success: true };\n } catch (e) {\n rethrowIfBailout(e);\n console.error(\"Guardrail: removeRepeaterItem failed\", e);\n return { success: false, error: \"Could not remove the item.\" };\n }\n}\n\nexport async function reorderRepeaterItems(\n repeaterId: string,\n orderedItemIds: string[],\n): Promise<Result> {\n const viewer = await requireRole([\"admin\", \"editor\"]);\n if (!viewer) return DENIED;\n\n try {\n const repeaters = getRepeaters();\n const res = await repeaters.reorderItems(\n repeaterId,\n await resolveSiteId(),\n orderedItemIds,\n );\n if (res.success) revalidatePath(\"/\", \"layout\");\n return res;\n } catch (e) {\n rethrowIfBailout(e);\n console.error(\"Guardrail: reorderRepeaterItems failed\", e);\n return { success: false, error: \"Could not reorder the items.\" };\n }\n}\n"],"mappings":";AAEA,SAAS,sBAAsB;AAC/B,SAAS,OAAO,cAAc,qBAAqB;AACnD,SAAS,mBAAmB;AAC5B,SAAS,wBAAwB;AAKjC,MAAM,SAAiB,EAAE,SAAS,OAAO,OAAO,gBAAgB;AAEhE,eAAsB,gBACpB,YACA,SAGA;AACA,QAAM,SAAS,MAAM,YAAY,CAAC,SAAS,QAAQ,CAAC;AACpD,MAAI,CAAC,OAAQ,QAAO,EAAE,SAAS,OAAO,OAAO,gBAAgB;AAE7D,MAAI,SAAS,SAAS,QAAW;AAC/B,UAAM,OAAO,QAAQ,KAAK,KAAK;AAC/B,QAAI,CAAC,2BAA2B,KAAK,IAAI,GAAG;AAC1C,aAAO;AAAA,QACL,SAAS;AAAA,QACT,OAAO;AAAA,MACT;AAAA,IACF;AACA,cAAU,EAAE,KAAK;AAAA,EACnB;AAEA,MAAI;AACF,UAAM,YAAY,aAAa;AAC/B,UAAM,MAAM,MAAM,UAAU,QAAQ,YAAY,MAAM,cAAc,GAAG,OAAO;AAC9E,QAAI,IAAI,QAAS,gBAAe,KAAK,QAAQ;AAC7C,WAAO;AAAA,EACT,SAAS,GAAG;AACV,qBAAiB,CAAC;AAClB,YAAQ,MAAM,qCAAqC,CAAC;AACpD,WAAO,EAAE,SAAS,OAAO,OAAO,0BAA0B;AAAA,EAC5D;AACF;AAEA,eAAsB,mBACpB,YACA,QACiB;AACjB,QAAM,SAAS,MAAM,YAAY,CAAC,SAAS,QAAQ,CAAC;AACpD,MAAI,CAAC,OAAQ,QAAO;AAEpB,MAAI;AACF,UAAM,SAAS,MAAM,cAAc;AACnC,UAAM,YAAY,aAAa;AAC/B,UAAM,MAAM,MAAM,UAAU,WAAW,YAAY,QAAQ,MAAM;AACjE,QAAI,CAAC,IAAI,QAAS,QAAO;AAIzB,UAAM,KAAK,MAAM;AACjB,UAAM,GAAG,kBAAkB,GAAG,UAAU,IAAI,MAAM,KAAK,MAAM;AAE7D,mBAAe,KAAK,QAAQ;AAC5B,WAAO,EAAE,SAAS,KAAK;AAAA,EACzB,SAAS,GAAG;AACV,qBAAiB,CAAC;AAClB,YAAQ,MAAM,wCAAwC,CAAC;AACvD,WAAO,EAAE,SAAS,OAAO,OAAO,6BAA6B;AAAA,EAC/D;AACF;AAEA,eAAsB,qBACpB,YACA,gBACiB;AACjB,QAAM,SAAS,MAAM,YAAY,CAAC,SAAS,QAAQ,CAAC;AACpD,MAAI,CAAC,OAAQ,QAAO;AAEpB,MAAI;AACF,UAAM,YAAY,aAAa;AAC/B,UAAM,MAAM,MAAM,UAAU;AAAA,MAC1B;AAAA,MACA,MAAM,cAAc;AAAA,MACpB;AAAA,IACF;AACA,QAAI,IAAI,QAAS,gBAAe,KAAK,QAAQ;AAC7C,WAAO;AAAA,EACT,SAAS,GAAG;AACV,qBAAiB,CAAC;AAClB,YAAQ,MAAM,0CAA0C,CAAC;AACzD,WAAO,EAAE,SAAS,OAAO,OAAO,+BAA+B;AAAA,EACjE;AACF;","names":[]}
@@ -0,0 +1,18 @@
1
+ import { GRRepeaterItem } from '../core/types.js';
2
+ import 'react';
3
+
4
+ /**
5
+ * Public read: anyone rendering the page needs to enumerate items, same trust
6
+ * level as fetchOrScaffold. Not a "use server" export — it is meant to be
7
+ * called from Server Components (a page's own dynamic route, generateStaticParams),
8
+ * not invoked from client code as an action.
9
+ */
10
+ declare function listRepeaterItems(repeaterId: string): Promise<GRRepeaterItem[]>;
11
+ /**
12
+ * Resolves a routed item by its URL segment. Built for a developer's own
13
+ * `[slug]/page.tsx` — call it, and 404 (via Next's notFound()) when it comes
14
+ * back null, the same way you would for any other missing record.
15
+ */
16
+ declare function getRepeaterItemBySlug(repeaterId: string, slug: string): Promise<GRRepeaterItem | null>;
17
+
18
+ export { getRepeaterItemBySlug, listRepeaterItems };
@@ -0,0 +1,27 @@
1
+ import { getRepeaters, resolveSiteId } from "../core/config";
2
+ import { rethrowIfBailout } from "../core/bailout";
3
+ async function listRepeaterItems(repeaterId) {
4
+ try {
5
+ const repeaters = getRepeaters();
6
+ return await repeaters.listItems(repeaterId, await resolveSiteId());
7
+ } catch (e) {
8
+ rethrowIfBailout(e);
9
+ console.error("Guardrail: failed to list repeater items", e);
10
+ return [];
11
+ }
12
+ }
13
+ async function getRepeaterItemBySlug(repeaterId, slug) {
14
+ try {
15
+ const repeaters = getRepeaters();
16
+ return await repeaters.getItemBySlug(repeaterId, await resolveSiteId(), slug);
17
+ } catch (e) {
18
+ rethrowIfBailout(e);
19
+ console.error("Guardrail: failed to resolve repeater item by slug", e);
20
+ return null;
21
+ }
22
+ }
23
+ export {
24
+ getRepeaterItemBySlug,
25
+ listRepeaterItems
26
+ };
27
+ //# sourceMappingURL=repeaterData.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/repeaterData.ts"],"sourcesContent":["import { getRepeaters, resolveSiteId } from \"../core/config\";\nimport { rethrowIfBailout } from \"../core/bailout\";\nimport type { GRRepeaterItem } from \"../core/types\";\n\n/**\n * Public read: anyone rendering the page needs to enumerate items, same trust\n * level as fetchOrScaffold. Not a \"use server\" export — it is meant to be\n * called from Server Components (a page's own dynamic route, generateStaticParams),\n * not invoked from client code as an action.\n */\nexport async function listRepeaterItems(\n repeaterId: string,\n): Promise<GRRepeaterItem[]> {\n try {\n const repeaters = getRepeaters();\n return await repeaters.listItems(repeaterId, await resolveSiteId());\n } catch (e) {\n rethrowIfBailout(e);\n console.error(\"Guardrail: failed to list repeater items\", e);\n return [];\n }\n}\n\n/**\n * Resolves a routed item by its URL segment. Built for a developer's own\n * `[slug]/page.tsx` — call it, and 404 (via Next's notFound()) when it comes\n * back null, the same way you would for any other missing record.\n */\nexport async function getRepeaterItemBySlug(\n repeaterId: string,\n slug: string,\n): Promise<GRRepeaterItem | null> {\n try {\n const repeaters = getRepeaters();\n return await repeaters.getItemBySlug(repeaterId, await resolveSiteId(), slug);\n } catch (e) {\n rethrowIfBailout(e);\n console.error(\"Guardrail: failed to resolve repeater item by slug\", e);\n return null;\n }\n}\n"],"mappings":"AAAA,SAAS,cAAc,qBAAqB;AAC5C,SAAS,wBAAwB;AASjC,eAAsB,kBACpB,YAC2B;AAC3B,MAAI;AACF,UAAM,YAAY,aAAa;AAC/B,WAAO,MAAM,UAAU,UAAU,YAAY,MAAM,cAAc,CAAC;AAAA,EACpE,SAAS,GAAG;AACV,qBAAiB,CAAC;AAClB,YAAQ,MAAM,4CAA4C,CAAC;AAC3D,WAAO,CAAC;AAAA,EACV;AACF;AAOA,eAAsB,sBACpB,YACA,MACgC;AAChC,MAAI;AACF,UAAM,YAAY,aAAa;AAC/B,WAAO,MAAM,UAAU,cAAc,YAAY,MAAM,cAAc,GAAG,IAAI;AAAA,EAC9E,SAAS,GAAG;AACV,qBAAiB,CAAC;AAClB,YAAQ,MAAM,sDAAsD,CAAC;AACrE,WAAO;AAAA,EACT;AACF;","names":[]}
@@ -0,0 +1,14 @@
1
+ import { GRViewer } from '../core/types.js';
2
+ import 'react';
3
+
4
+ /**
5
+ * Request-scoped identity + role resolution.
6
+ *
7
+ * Every <GRail> needs to know whether an editor is present; React's cache()
8
+ * collapses that into one verification per render pass rather than one per
9
+ * field. Identity comes from the auth adapter, the role from Guardrail's
10
+ * membership store — the two are resolved separately and never conflated.
11
+ */
12
+ declare const getCachedViewer: () => Promise<GRViewer | null>;
13
+
14
+ export { getCachedViewer };
@@ -0,0 +1,37 @@
1
+ import { cache } from "react";
2
+ import { getAuth, getMembers, resolveSiteId } from "../core/config";
3
+ import { rethrowIfBailout } from "../core/bailout";
4
+ const getCachedViewer = cache(async () => {
5
+ try {
6
+ const auth = getAuth();
7
+ const session = await auth.getSession();
8
+ if (!session) return null;
9
+ const siteId = await resolveSiteId();
10
+ const members = getMembers();
11
+ const email = session.email?.toLowerCase().trim();
12
+ const byUser = await members.getMember(siteId, { userId: session.userId });
13
+ if (byUser) {
14
+ return { userId: session.userId, email: session.email, role: byUser.role };
15
+ }
16
+ if (email) {
17
+ const invited = await members.getMember(siteId, { email });
18
+ if (invited && invited.status === "pending") {
19
+ await members.claimInvite(siteId);
20
+ return {
21
+ userId: session.userId,
22
+ email: session.email,
23
+ role: invited.role
24
+ };
25
+ }
26
+ }
27
+ return { userId: session.userId, email: session.email, role: null };
28
+ } catch (e) {
29
+ rethrowIfBailout(e);
30
+ console.error("Guardrail: viewer resolution failed", e);
31
+ return null;
32
+ }
33
+ });
34
+ export {
35
+ getCachedViewer
36
+ };
37
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/session.ts"],"sourcesContent":["import { cache } from \"react\";\nimport { getAuth, getMembers, resolveSiteId } from \"../core/config\";\nimport { rethrowIfBailout } from \"../core/bailout\";\nimport type { GRViewer } from \"../core/types\";\n\n/**\n * Request-scoped identity + role resolution.\n *\n * Every <GRail> needs to know whether an editor is present; React's cache()\n * collapses that into one verification per render pass rather than one per\n * field. Identity comes from the auth adapter, the role from Guardrail's\n * membership store — the two are resolved separately and never conflated.\n */\nexport const getCachedViewer = cache(async (): Promise<GRViewer | null> => {\n try {\n const auth = getAuth();\n const session = await auth.getSession();\n if (!session) return null;\n\n const siteId = await resolveSiteId();\n const members = getMembers();\n const email = session.email?.toLowerCase().trim();\n\n // 1. Already a member of this site.\n const byUser = await members.getMember(siteId, { userId: session.userId });\n if (byUser) {\n return { userId: session.userId, email: session.email, role: byUser.role };\n }\n\n // 2. Claim a pending invitation addressed to this email. The adapter binds\n // it server-side; the role it was invited with is not ours to choose.\n if (email) {\n const invited = await members.getMember(siteId, { email });\n if (invited && invited.status === \"pending\") {\n await members.claimInvite(siteId);\n return {\n userId: session.userId,\n email: session.email,\n role: invited.role,\n };\n }\n }\n\n // Signed in, but holds no role here. There is deliberately no self-service\n // path to a role: the first admin is seeded in SQL (see supabase/migrations).\n return { userId: session.userId, email: session.email, role: null };\n } catch (e) {\n rethrowIfBailout(e);\n console.error(\"Guardrail: viewer resolution failed\", e);\n return null;\n }\n});\n"],"mappings":"AAAA,SAAS,aAAa;AACtB,SAAS,SAAS,YAAY,qBAAqB;AACnD,SAAS,wBAAwB;AAW1B,MAAM,kBAAkB,MAAM,YAAsC;AACzE,MAAI;AACF,UAAM,OAAO,QAAQ;AACrB,UAAM,UAAU,MAAM,KAAK,WAAW;AACtC,QAAI,CAAC,QAAS,QAAO;AAErB,UAAM,SAAS,MAAM,cAAc;AACnC,UAAM,UAAU,WAAW;AAC3B,UAAM,QAAQ,QAAQ,OAAO,YAAY,EAAE,KAAK;AAGhD,UAAM,SAAS,MAAM,QAAQ,UAAU,QAAQ,EAAE,QAAQ,QAAQ,OAAO,CAAC;AACzE,QAAI,QAAQ;AACV,aAAO,EAAE,QAAQ,QAAQ,QAAQ,OAAO,QAAQ,OAAO,MAAM,OAAO,KAAK;AAAA,IAC3E;AAIA,QAAI,OAAO;AACT,YAAM,UAAU,MAAM,QAAQ,UAAU,QAAQ,EAAE,MAAM,CAAC;AACzD,UAAI,WAAW,QAAQ,WAAW,WAAW;AAC3C,cAAM,QAAQ,YAAY,MAAM;AAChC,eAAO;AAAA,UACL,QAAQ,QAAQ;AAAA,UAChB,OAAO,QAAQ;AAAA,UACf,MAAM,QAAQ;AAAA,QAChB;AAAA,MACF;AAAA,IACF;AAIA,WAAO,EAAE,QAAQ,QAAQ,QAAQ,OAAO,QAAQ,OAAO,MAAM,KAAK;AAAA,EACpE,SAAS,GAAG;AACV,qBAAiB,CAAC;AAClB,YAAQ,MAAM,uCAAuC,CAAC;AACtD,WAAO;AAAA,EACT;AACF,CAAC;","names":[]}
@@ -0,0 +1,12 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import React from 'react';
3
+ import { GRConfig } from '../core/adapter-types.js';
4
+ import '../core/types.js';
5
+
6
+ /**
7
+ * Wraps a root layout so every route carries Guardrail's edit state and admin
8
+ * chrome. Stays a Server Component: the provider below is the client boundary.
9
+ */
10
+ declare function withGuardrail<P extends object>(LayoutComponent: React.ComponentType<P>, config: GRConfig): (props: P) => Promise<react_jsx_runtime.JSX.Element>;
11
+
12
+ export { withGuardrail };