@guard-rail/core 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -28
- package/dist/adapters/supabase/auth.js +50 -0
- package/dist/adapters/supabase/auth.js.map +1 -1
- package/dist/cli/init-db.d.ts +1 -0
- package/dist/cli/init-db.js +121 -0
- package/dist/cli/init-db.js.map +1 -0
- package/dist/core/adapter-types.d.ts +43 -1
- package/dist/core/defs.d.ts +43 -2
- package/dist/core/defs.js +29 -2
- package/dist/core/defs.js.map +1 -1
- package/dist/core/types.d.ts +8 -1
- package/dist/index.client.d.ts +3 -3
- package/dist/index.client.js +5 -1
- package/dist/index.client.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/react/Admin.js +83 -7
- package/dist/react/Admin.js.map +1 -1
- package/dist/react/AuthRedirectSync.d.ts +20 -0
- package/dist/react/AuthRedirectSync.js +38 -0
- package/dist/react/AuthRedirectSync.js.map +1 -0
- package/dist/react/GRail/client.js +5 -1
- package/dist/react/GRail/client.js.map +1 -1
- package/dist/react/Provider.js +5 -1
- package/dist/react/Provider.js.map +1 -1
- package/dist/react/Syncer.d.ts +10 -8
- package/dist/react/Syncer.js +8 -6
- package/dist/react/Syncer.js.map +1 -1
- package/dist/react/useAnchorRect.d.ts +17 -1
- package/dist/react/useAnchorRect.js +37 -9
- package/dist/react/useAnchorRect.js.map +1 -1
- package/dist/server/actions.d.ts +31 -3
- package/dist/server/actions.js +45 -1
- package/dist/server/actions.js.map +1 -1
- package/dist/server/wrapper.js +7 -2
- package/dist/server/wrapper.js.map +1 -1
- package/package.json +3 -2
- package/scripts/postinstall.mjs +4 -4
|
@@ -1,30 +1,58 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { useCallback, useEffect, useState } from "react";
|
|
3
|
-
function useAnchorRect(ref, active) {
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
function useAnchorRect(ref, active, options = {}) {
|
|
4
|
+
const { gateByVisibility = false } = options;
|
|
4
5
|
const [rect, setRect] = useState(null);
|
|
6
|
+
const rafRef = useRef(null);
|
|
5
7
|
const measure = useCallback(() => {
|
|
6
8
|
const el = ref.current;
|
|
7
9
|
if (!el) return;
|
|
8
10
|
const r = el.getBoundingClientRect();
|
|
9
11
|
setRect({ top: r.top, left: r.left, bottom: r.bottom, width: r.width });
|
|
10
12
|
}, [ref]);
|
|
13
|
+
const scheduleMeasure = useCallback(() => {
|
|
14
|
+
if (rafRef.current !== null) return;
|
|
15
|
+
rafRef.current = requestAnimationFrame(() => {
|
|
16
|
+
rafRef.current = null;
|
|
17
|
+
measure();
|
|
18
|
+
});
|
|
19
|
+
}, [measure]);
|
|
20
|
+
const [visible, setVisible] = useState(!gateByVisibility);
|
|
11
21
|
useEffect(() => {
|
|
12
|
-
if (!active)
|
|
22
|
+
if (!active || !gateByVisibility) return;
|
|
23
|
+
const el = ref.current;
|
|
24
|
+
if (!el || typeof IntersectionObserver === "undefined") {
|
|
25
|
+
setVisible(true);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const io = new IntersectionObserver(
|
|
29
|
+
(entries) => setVisible(entries[0]?.isIntersecting ?? false),
|
|
30
|
+
{ rootMargin: "200px" }
|
|
31
|
+
);
|
|
32
|
+
io.observe(el);
|
|
33
|
+
return () => io.disconnect();
|
|
34
|
+
}, [active, gateByVisibility, ref]);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!active || gateByVisibility && !visible) {
|
|
13
37
|
setRect(null);
|
|
14
38
|
return;
|
|
15
39
|
}
|
|
16
40
|
measure();
|
|
17
|
-
window.addEventListener("scroll",
|
|
18
|
-
window.addEventListener("resize",
|
|
19
|
-
const observer = typeof ResizeObserver !== "undefined" ? new ResizeObserver(
|
|
41
|
+
window.addEventListener("scroll", scheduleMeasure, true);
|
|
42
|
+
window.addEventListener("resize", scheduleMeasure);
|
|
43
|
+
const observer = typeof ResizeObserver !== "undefined" ? new ResizeObserver(scheduleMeasure) : null;
|
|
20
44
|
if (observer && ref.current) observer.observe(ref.current);
|
|
21
45
|
if (observer) observer.observe(document.body);
|
|
22
46
|
return () => {
|
|
23
|
-
window.removeEventListener("scroll",
|
|
24
|
-
window.removeEventListener("resize",
|
|
47
|
+
window.removeEventListener("scroll", scheduleMeasure, true);
|
|
48
|
+
window.removeEventListener("resize", scheduleMeasure);
|
|
25
49
|
observer?.disconnect();
|
|
50
|
+
if (rafRef.current !== null) {
|
|
51
|
+
cancelAnimationFrame(rafRef.current);
|
|
52
|
+
rafRef.current = null;
|
|
53
|
+
}
|
|
26
54
|
};
|
|
27
|
-
}, [active, measure, ref]);
|
|
55
|
+
}, [active, gateByVisibility, visible, measure, scheduleMeasure, ref]);
|
|
28
56
|
return rect;
|
|
29
57
|
}
|
|
30
58
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/react/useAnchorRect.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useState, type RefObject } from \"react\";\n\nexport interface AnchorRect {\n top: number;\n left: number;\n bottom: number;\n width: number;\n}\n\n/**\n * Tracks an element's viewport rect so editing chrome can sit against it.\n *\n * Listens on the capture phase so scrolling inside any ancestor container\n * repositions the chrome, not just window scroll.\n */\nexport function useAnchorRect(\n ref: RefObject<HTMLElement | null>,\n active: boolean,\n): AnchorRect | null {\n const [rect, setRect] = useState<AnchorRect | null>(null);\n\n const measure = useCallback(() => {\n const el = ref.current;\n if (!el) return;\n const r = el.getBoundingClientRect();\n setRect({ top: r.top, left: r.left, bottom: r.bottom, width: r.width });\n }, [ref]);\n\n useEffect(() => {\n if (!active) {\n setRect(null);\n return;\n }\n\n measure();\n window.addEventListener(\"scroll\",
|
|
1
|
+
{"version":3,"sources":["../../src/react/useAnchorRect.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useRef, useState, type RefObject } from \"react\";\n\nexport interface AnchorRect {\n top: number;\n left: number;\n bottom: number;\n width: number;\n}\n\n/**\n * Tracks an element's viewport rect so editing chrome can sit against it.\n *\n * Listens on the capture phase so scrolling inside any ancestor container\n * repositions the chrome, not just window scroll.\n *\n * Two things keep this cheap when many elements track at once (every image\n * field does, for its idle marker, for as long as edit mode is on — not just\n * the one actively being edited):\n *\n * - Measurement is coalesced to one requestAnimationFrame per scroll/resize\n * burst, not one per raw event. A scroll can fire far more of those than\n * the page ever paints, so N tracked elements without this meant N\n * getBoundingClientRect() calls and N re-renders per event, not per frame.\n * - `gateByVisibility` skips attaching any listener at all — scroll, resize,\n * or the ResizeObserver — for an element nowhere near the viewport, via\n * IntersectionObserver. A long page with many editable images then only\n * pays for the handful actually in view at a given scroll position, not\n * all of them.\n */\nexport function useAnchorRect(\n ref: RefObject<HTMLElement | null>,\n active: boolean,\n options: { gateByVisibility?: boolean } = {},\n): AnchorRect | null {\n const { gateByVisibility = false } = options;\n const [rect, setRect] = useState<AnchorRect | null>(null);\n const rafRef = useRef<number | null>(null);\n\n const measure = useCallback(() => {\n const el = ref.current;\n if (!el) return;\n const r = el.getBoundingClientRect();\n setRect({ top: r.top, left: r.left, bottom: r.bottom, width: r.width });\n }, [ref]);\n\n const scheduleMeasure = useCallback(() => {\n if (rafRef.current !== null) return;\n rafRef.current = requestAnimationFrame(() => {\n rafRef.current = null;\n measure();\n });\n }, [measure]);\n\n const [visible, setVisible] = useState(!gateByVisibility);\n\n useEffect(() => {\n if (!active || !gateByVisibility) return;\n const el = ref.current;\n if (!el || typeof IntersectionObserver === \"undefined\") {\n setVisible(true);\n return;\n }\n // A margin outside the viewport, not just inside it — so tracking (and\n // the marker) is already live the instant an image scrolls into view,\n // not a frame behind.\n const io = new IntersectionObserver(\n (entries) => setVisible(entries[0]?.isIntersecting ?? false),\n { rootMargin: \"200px\" },\n );\n io.observe(el);\n return () => io.disconnect();\n }, [active, gateByVisibility, ref]);\n\n useEffect(() => {\n if (!active || (gateByVisibility && !visible)) {\n setRect(null);\n return;\n }\n\n measure();\n window.addEventListener(\"scroll\", scheduleMeasure, true);\n window.addEventListener(\"resize\", scheduleMeasure);\n\n // Catches layout shifts from the edit itself (text reflowing the element).\n const observer =\n typeof ResizeObserver !== \"undefined\" ? new ResizeObserver(scheduleMeasure) : null;\n if (observer && ref.current) observer.observe(ref.current);\n // Content changing size above or beside the element moves it without\n // resizing it, which observing the element alone never sees. The body's\n // own height changes in most of those cases.\n if (observer) observer.observe(document.body);\n\n return () => {\n window.removeEventListener(\"scroll\", scheduleMeasure, true);\n window.removeEventListener(\"resize\", scheduleMeasure);\n observer?.disconnect();\n if (rafRef.current !== null) {\n cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n }\n };\n }, [active, gateByVisibility, visible, measure, scheduleMeasure, ref]);\n\n return rect;\n}\n"],"mappings":";AAEA,SAAS,aAAa,WAAW,QAAQ,gBAAgC;AA6BlE,SAAS,cACd,KACA,QACA,UAA0C,CAAC,GACxB;AACnB,QAAM,EAAE,mBAAmB,MAAM,IAAI;AACrC,QAAM,CAAC,MAAM,OAAO,IAAI,SAA4B,IAAI;AACxD,QAAM,SAAS,OAAsB,IAAI;AAEzC,QAAM,UAAU,YAAY,MAAM;AAChC,UAAM,KAAK,IAAI;AACf,QAAI,CAAC,GAAI;AACT,UAAM,IAAI,GAAG,sBAAsB;AACnC,YAAQ,EAAE,KAAK,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,EAAE,QAAQ,OAAO,EAAE,MAAM,CAAC;AAAA,EACxE,GAAG,CAAC,GAAG,CAAC;AAER,QAAM,kBAAkB,YAAY,MAAM;AACxC,QAAI,OAAO,YAAY,KAAM;AAC7B,WAAO,UAAU,sBAAsB,MAAM;AAC3C,aAAO,UAAU;AACjB,cAAQ;AAAA,IACV,CAAC;AAAA,EACH,GAAG,CAAC,OAAO,CAAC;AAEZ,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,CAAC,gBAAgB;AAExD,YAAU,MAAM;AACd,QAAI,CAAC,UAAU,CAAC,iBAAkB;AAClC,UAAM,KAAK,IAAI;AACf,QAAI,CAAC,MAAM,OAAO,yBAAyB,aAAa;AACtD,iBAAW,IAAI;AACf;AAAA,IACF;AAIA,UAAM,KAAK,IAAI;AAAA,MACb,CAAC,YAAY,WAAW,QAAQ,CAAC,GAAG,kBAAkB,KAAK;AAAA,MAC3D,EAAE,YAAY,QAAQ;AAAA,IACxB;AACA,OAAG,QAAQ,EAAE;AACb,WAAO,MAAM,GAAG,WAAW;AAAA,EAC7B,GAAG,CAAC,QAAQ,kBAAkB,GAAG,CAAC;AAElC,YAAU,MAAM;AACd,QAAI,CAAC,UAAW,oBAAoB,CAAC,SAAU;AAC7C,cAAQ,IAAI;AACZ;AAAA,IACF;AAEA,YAAQ;AACR,WAAO,iBAAiB,UAAU,iBAAiB,IAAI;AACvD,WAAO,iBAAiB,UAAU,eAAe;AAGjD,UAAM,WACJ,OAAO,mBAAmB,cAAc,IAAI,eAAe,eAAe,IAAI;AAChF,QAAI,YAAY,IAAI,QAAS,UAAS,QAAQ,IAAI,OAAO;AAIzD,QAAI,SAAU,UAAS,QAAQ,SAAS,IAAI;AAE5C,WAAO,MAAM;AACX,aAAO,oBAAoB,UAAU,iBAAiB,IAAI;AAC1D,aAAO,oBAAoB,UAAU,eAAe;AACpD,gBAAU,WAAW;AACrB,UAAI,OAAO,YAAY,MAAM;AAC3B,6BAAqB,OAAO,OAAO;AACnC,eAAO,UAAU;AAAA,MACnB;AAAA,IACF;AAAA,EACF,GAAG,CAAC,QAAQ,kBAAkB,SAAS,SAAS,iBAAiB,GAAG,CAAC;AAErE,SAAO;AACT;","names":[]}
|
package/dist/server/actions.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { GRRole, GRAsset, GRMember, ActionState, GRViewer, OverrideRow } from '../core/types.js';
|
|
2
|
-
import { GRUploadTarget } from '../core/adapter-types.js';
|
|
1
|
+
import { GRRole, GRAsset, GRMember, SetPasswordState, ActionState, GRViewer, OverrideRow } from '../core/types.js';
|
|
2
|
+
import { GRUploadTarget, AuthRedirectParams } from '../core/adapter-types.js';
|
|
3
3
|
import 'react';
|
|
4
4
|
|
|
5
5
|
type Result = {
|
|
@@ -37,5 +37,33 @@ declare function loginAction(_prev: ActionState, formData: FormData): Promise<Ac
|
|
|
37
37
|
declare function logoutAction(): Promise<{
|
|
38
38
|
success: boolean;
|
|
39
39
|
}>;
|
|
40
|
+
/**
|
|
41
|
+
* Completes a session that arrived via an invite, magic-link, or
|
|
42
|
+
* password-recovery redirect. Called by AuthRedirectSync (react/), the
|
|
43
|
+
* generic client component that notices this kind of link and hands off
|
|
44
|
+
* whatever it found in the URL — the actual token exchange is entirely the
|
|
45
|
+
* auth adapter's job (see GRAuthAdapter.completeAuthRedirect); core only
|
|
46
|
+
* orchestrates the result.
|
|
47
|
+
*
|
|
48
|
+
* A no-op {success:false}, not an error, when the adapter doesn't implement
|
|
49
|
+
* this — most page loads carry no redirect data at all, and an adapter with
|
|
50
|
+
* no such concept should not make every one of them fail.
|
|
51
|
+
*/
|
|
52
|
+
declare function completeAuthRedirectAction(params: AuthRedirectParams): Promise<{
|
|
53
|
+
success: boolean;
|
|
54
|
+
needsPassword?: boolean;
|
|
55
|
+
error?: string;
|
|
56
|
+
}>;
|
|
57
|
+
/**
|
|
58
|
+
* Sets the current session's password — the prompt completeAuthRedirectAction
|
|
59
|
+
* triggers via needsPassword. Requires only a valid session, not a role: an
|
|
60
|
+
* invited person's role is claimed as a side effect of getCachedViewer()
|
|
61
|
+
* resolving them, which already happens independently of this.
|
|
62
|
+
*/
|
|
63
|
+
declare function setPasswordAction(_prev: SetPasswordState, formData: FormData): Promise<SetPasswordState>;
|
|
64
|
+
/** Dismisses the set-password prompt without setting one — see Admin.tsx. */
|
|
65
|
+
declare function skipSetPasswordAction(): Promise<{
|
|
66
|
+
success: boolean;
|
|
67
|
+
}>;
|
|
40
68
|
|
|
41
|
-
export { createAssetUpload, inviteMember, listAssets, listTeam, loginAction, logoutAction, removeMember, requireRole, saveOverride, updateMemberRole };
|
|
69
|
+
export { completeAuthRedirectAction, createAssetUpload, inviteMember, listAssets, listTeam, loginAction, logoutAction, removeMember, requireRole, saveOverride, setPasswordAction, skipSetPasswordAction, updateMemberRole };
|
package/dist/server/actions.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use server";
|
|
2
2
|
import { revalidatePath } from "next/cache";
|
|
3
|
-
import { headers } from "next/headers";
|
|
3
|
+
import { cookies, headers } from "next/headers";
|
|
4
4
|
import {
|
|
5
5
|
getAuth,
|
|
6
6
|
getDB,
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "../core/config";
|
|
12
12
|
import { getCachedViewer } from "./session";
|
|
13
13
|
import { rethrowIfBailout } from "../core/bailout";
|
|
14
|
+
import { GR_COOKIES, GR_NEEDS_PASSWORD_TTL_SECONDS } from "../core/defs";
|
|
14
15
|
const DENIED = { success: false, error: "Unauthorized." };
|
|
15
16
|
async function requireRole(allowed) {
|
|
16
17
|
const viewer = await getCachedViewer();
|
|
@@ -196,10 +197,51 @@ async function loginAction(_prev, formData) {
|
|
|
196
197
|
async function logoutAction() {
|
|
197
198
|
const auth = getAuth();
|
|
198
199
|
await auth.logout();
|
|
200
|
+
const cookieStore = await cookies();
|
|
201
|
+
cookieStore.delete(GR_COOKIES.EDIT_MODE);
|
|
202
|
+
revalidatePath("/", "layout");
|
|
203
|
+
return { success: true };
|
|
204
|
+
}
|
|
205
|
+
async function completeAuthRedirectAction(params) {
|
|
206
|
+
const auth = getAuth();
|
|
207
|
+
if (!auth.completeAuthRedirect) return { success: false };
|
|
208
|
+
const res = await auth.completeAuthRedirect(params);
|
|
209
|
+
if (res.success) {
|
|
210
|
+
if (res.needsPassword) {
|
|
211
|
+
const cookieStore = await cookies();
|
|
212
|
+
cookieStore.set(GR_COOKIES.NEEDS_PASSWORD, "true", {
|
|
213
|
+
path: "/",
|
|
214
|
+
maxAge: GR_NEEDS_PASSWORD_TTL_SECONDS
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
revalidatePath("/", "layout");
|
|
218
|
+
}
|
|
219
|
+
return res;
|
|
220
|
+
}
|
|
221
|
+
async function setPasswordAction(_prev, formData) {
|
|
222
|
+
const viewer = await getCachedViewer();
|
|
223
|
+
if (!viewer) return { success: false, error: "Not signed in." };
|
|
224
|
+
const auth = getAuth();
|
|
225
|
+
if (!auth.setPassword) {
|
|
226
|
+
return { success: false, error: "This site's auth provider does not support setting a password." };
|
|
227
|
+
}
|
|
228
|
+
const password = String(formData.get("password") ?? "");
|
|
229
|
+
const res = await auth.setPassword(password);
|
|
230
|
+
if (res.success) {
|
|
231
|
+
const cookieStore = await cookies();
|
|
232
|
+
cookieStore.delete(GR_COOKIES.NEEDS_PASSWORD);
|
|
233
|
+
revalidatePath("/", "layout");
|
|
234
|
+
}
|
|
235
|
+
return res;
|
|
236
|
+
}
|
|
237
|
+
async function skipSetPasswordAction() {
|
|
238
|
+
const cookieStore = await cookies();
|
|
239
|
+
cookieStore.delete(GR_COOKIES.NEEDS_PASSWORD);
|
|
199
240
|
revalidatePath("/", "layout");
|
|
200
241
|
return { success: true };
|
|
201
242
|
}
|
|
202
243
|
export {
|
|
244
|
+
completeAuthRedirectAction,
|
|
203
245
|
createAssetUpload,
|
|
204
246
|
inviteMember,
|
|
205
247
|
listAssets,
|
|
@@ -209,6 +251,8 @@ export {
|
|
|
209
251
|
removeMember,
|
|
210
252
|
requireRole,
|
|
211
253
|
saveOverride,
|
|
254
|
+
setPasswordAction,
|
|
255
|
+
skipSetPasswordAction,
|
|
212
256
|
updateMemberRole
|
|
213
257
|
};
|
|
214
258
|
//# sourceMappingURL=actions.js.map
|
|
@@ -1 +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":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/server/actions.ts"],"sourcesContent":["\"use server\";\n\nimport { revalidatePath } from \"next/cache\";\nimport { cookies, 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 { GR_COOKIES, GR_NEEDS_PASSWORD_TTL_SECONDS } from \"../core/defs\";\nimport type {\n ActionState,\n GRAsset,\n GRMember,\n GRRole,\n GRViewer,\n OverrideRow,\n SetPasswordState,\n} from \"../core/types\";\nimport type {\n AuthRedirectParams,\n GRStorageRow,\n GRUploadTarget,\n} 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 // Cleared here, not by the client afterward: a Server Action's own\n // revalidatePath() below triggers Next's router to refresh the current\n // route automatically, which can race a client-side navigation attempted\n // right after this resolves — confirmed live, where that automatic\n // refresh (for the pre-logout URL, ?edit=true) won the race and silently\n // undid a client-side `window.location.href` to the plain path. Deleting\n // the cookie here instead makes it part of the one server round-trip that\n // revalidatePath already accounts for, with nothing left to race.\n const cookieStore = await cookies();\n cookieStore.delete(GR_COOKIES.EDIT_MODE);\n revalidatePath(\"/\", \"layout\");\n return { success: true };\n}\n\n/* ---------------------------------------------------------- redirects --- */\n\n/**\n * Completes a session that arrived via an invite, magic-link, or\n * password-recovery redirect. Called by AuthRedirectSync (react/), the\n * generic client component that notices this kind of link and hands off\n * whatever it found in the URL — the actual token exchange is entirely the\n * auth adapter's job (see GRAuthAdapter.completeAuthRedirect); core only\n * orchestrates the result.\n *\n * A no-op {success:false}, not an error, when the adapter doesn't implement\n * this — most page loads carry no redirect data at all, and an adapter with\n * no such concept should not make every one of them fail.\n */\nexport async function completeAuthRedirectAction(\n params: AuthRedirectParams,\n): Promise<{ success: boolean; needsPassword?: boolean; error?: string }> {\n const auth = getAuth();\n if (!auth.completeAuthRedirect) return { success: false };\n\n const res = await auth.completeAuthRedirect(params);\n if (res.success) {\n if (res.needsPassword) {\n const cookieStore = await cookies();\n cookieStore.set(GR_COOKIES.NEEDS_PASSWORD, \"true\", {\n path: \"/\",\n maxAge: GR_NEEDS_PASSWORD_TTL_SECONDS,\n });\n }\n revalidatePath(\"/\", \"layout\");\n }\n return res;\n}\n\n/**\n * Sets the current session's password — the prompt completeAuthRedirectAction\n * triggers via needsPassword. Requires only a valid session, not a role: an\n * invited person's role is claimed as a side effect of getCachedViewer()\n * resolving them, which already happens independently of this.\n */\nexport async function setPasswordAction(\n _prev: SetPasswordState,\n formData: FormData,\n): Promise<SetPasswordState> {\n const viewer = await getCachedViewer();\n if (!viewer) return { success: false, error: \"Not signed in.\" };\n\n const auth = getAuth();\n if (!auth.setPassword) {\n return { success: false, error: \"This site's auth provider does not support setting a password.\" };\n }\n\n const password = String(formData.get(\"password\") ?? \"\");\n const res = await auth.setPassword(password);\n if (res.success) {\n const cookieStore = await cookies();\n cookieStore.delete(GR_COOKIES.NEEDS_PASSWORD);\n revalidatePath(\"/\", \"layout\");\n }\n return res;\n}\n\n/** Dismisses the set-password prompt without setting one — see Admin.tsx. */\nexport async function skipSetPasswordAction(): Promise<{ success: boolean }> {\n const cookieStore = await cookies();\n cookieStore.delete(GR_COOKIES.NEEDS_PASSWORD);\n revalidatePath(\"/\", \"layout\");\n return { success: true };\n}\n"],"mappings":"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","names":[]}
|
package/dist/server/wrapper.js
CHANGED
|
@@ -22,14 +22,19 @@ function withGuardrail(LayoutComponent, config) {
|
|
|
22
22
|
loadPreloadedContent()
|
|
23
23
|
]);
|
|
24
24
|
const auth = getAuth();
|
|
25
|
+
const editCookie = cookieStore.get(GR_COOKIES.EDIT_MODE)?.value;
|
|
26
|
+
const isAuth = viewer !== null;
|
|
27
|
+
const needsPassword = cookieStore.get(GR_COOKIES.NEEDS_PASSWORD)?.value === "true";
|
|
28
|
+
const isEditMode = needsPassword || (editCookie === "false" ? false : editCookie === "true" || isAuth);
|
|
25
29
|
const state = {
|
|
26
30
|
storage: getStorage() !== void 0,
|
|
27
31
|
auth: {
|
|
28
32
|
password: typeof auth.authenticate === "function",
|
|
29
33
|
...auth.signInUrl ? { signInUrl: auth.signInUrl } : {}
|
|
30
34
|
},
|
|
31
|
-
isAuth
|
|
32
|
-
isEditMode
|
|
35
|
+
isAuth,
|
|
36
|
+
isEditMode,
|
|
37
|
+
needsPassword,
|
|
33
38
|
siteId: await resolveSiteId(),
|
|
34
39
|
role: viewer?.role ?? null,
|
|
35
40
|
...viewer?.email ? { email: viewer.email } : {}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/server/wrapper.tsx"],"sourcesContent":["import React, { Suspense } from \"react\";\nimport { cookies } from \"next/headers\";\nimport {\n configureGuardrail,\n getAuth,\n getStorage,\n resolveSiteId,\n} from \"../core/config\";\nimport { getCachedViewer } from \"./session\";\nimport { loadPreloadedContent } from \"./loader\";\nimport type { GRConfig } from \"../core/adapter-types\";\nimport { GR_COOKIES } from \"../core/defs\";\nimport { GRailProvider } from \"../react/Provider\";\nimport { GRailAdmin } from \"../react/Admin\";\nimport type { GRState } from \"../core/types\";\n\n/**\n * Wraps a root layout so every route carries Guardrail's edit state and admin\n * chrome. Stays a Server Component: the provider below is the client boundary.\n */\nexport function withGuardrail<P extends object>(\n LayoutComponent: React.ComponentType<P>,\n config: GRConfig,\n) {\n // Applied at module evaluation so the config is present as soon as anything\n // imports the layout, and again per render because Next.js does not\n // guarantee one shared module instance across its server contexts.\n configureGuardrail(config);\n\n return async function GuardrailLayout(props: P) {\n configureGuardrail(config);\n\n const cookieStore = await cookies();\n\n // Verified against the auth provider, not inferred from a cookie. Loaded\n // alongside the content preload: independent round trips, and the preload\n // is shared with the page's own fields, so it costs them nothing extra.\n const [viewer, content] = await Promise.all([\n getCachedViewer(),\n loadPreloadedContent(),\n ]);\n\n const auth = getAuth();\n\n const state: GRState = {\n storage: getStorage() !== undefined,\n auth: {\n password: typeof auth.authenticate === \"function\",\n ...(auth.signInUrl ? { signInUrl: auth.signInUrl } : {}),\n },\n isAuth
|
|
1
|
+
{"version":3,"sources":["../../src/server/wrapper.tsx"],"sourcesContent":["import React, { Suspense } from \"react\";\nimport { cookies } from \"next/headers\";\nimport {\n configureGuardrail,\n getAuth,\n getStorage,\n resolveSiteId,\n} from \"../core/config\";\nimport { getCachedViewer } from \"./session\";\nimport { loadPreloadedContent } from \"./loader\";\nimport type { GRConfig } from \"../core/adapter-types\";\nimport { GR_COOKIES } from \"../core/defs\";\nimport { GRailProvider } from \"../react/Provider\";\nimport { GRailAdmin } from \"../react/Admin\";\nimport type { GRState } from \"../core/types\";\n\n/**\n * Wraps a root layout so every route carries Guardrail's edit state and admin\n * chrome. Stays a Server Component: the provider below is the client boundary.\n */\nexport function withGuardrail<P extends object>(\n LayoutComponent: React.ComponentType<P>,\n config: GRConfig,\n) {\n // Applied at module evaluation so the config is present as soon as anything\n // imports the layout, and again per render because Next.js does not\n // guarantee one shared module instance across its server contexts.\n configureGuardrail(config);\n\n return async function GuardrailLayout(props: P) {\n configureGuardrail(config);\n\n const cookieStore = await cookies();\n\n // Verified against the auth provider, not inferred from a cookie. Loaded\n // alongside the content preload: independent round trips, and the preload\n // is shared with the page's own fields, so it costs them nothing extra.\n const [viewer, content] = await Promise.all([\n getCachedViewer(),\n loadPreloadedContent(),\n ]);\n\n const auth = getAuth();\n\n // \"false\" is an explicit, deliberate suppression (?edit=false while still\n // signed in, to preview without chrome) and wins outright. Otherwise, a\n // real session is enough on its own — an authenticated editor's chrome\n // persists for as long as that session does, with no separate cookie\n // timer to fall out of sync with it. \"true\" only matters for someone not\n // yet authenticated: a short-lived grace window (see GR_EDIT_MODE_TTL_SECONDS)\n // to actually reach the sign-in form, not a standing invitation — the\n // previous design kept this cookie alive indefinitely once set, which\n // meant a stray copy of it kept showing the sign-in bar to whoever's\n // browser it was sitting in, for as long as that browser stayed open.\n const editCookie = cookieStore.get(GR_COOKIES.EDIT_MODE)?.value;\n const isAuth = viewer !== null;\n // Set by completeAuthRedirectAction the moment an invite/recovery link\n // finishes — forces chrome on regardless of the edit-mode cookie, since\n // the whole point is to show the set-password prompt right away, not\n // require also having ?edit=true or an unsuppressed cookie.\n const needsPassword = cookieStore.get(GR_COOKIES.NEEDS_PASSWORD)?.value === \"true\";\n const isEditMode =\n needsPassword || (editCookie === \"false\" ? false : editCookie === \"true\" || isAuth);\n\n const state: GRState = {\n storage: getStorage() !== undefined,\n auth: {\n password: typeof auth.authenticate === \"function\",\n ...(auth.signInUrl ? { signInUrl: auth.signInUrl } : {}),\n },\n isAuth,\n isEditMode,\n needsPassword,\n siteId: await resolveSiteId(),\n role: viewer?.role ?? null,\n ...(viewer?.email ? { email: viewer.email } : {}),\n };\n\n return (\n <GRailProvider state={state} content={content}>\n <LayoutComponent {...props} />\n <Suspense fallback={null}>\n <GRailAdmin toolbarExtra={config.toolbarExtra} />\n </Suspense>\n </GRailProvider>\n );\n };\n}\n"],"mappings":"AA+EM,SACE,KADF;AA/EN,OAAO,SAAS,gBAAgB;AAChC,SAAS,eAAe;AACxB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,uBAAuB;AAChC,SAAS,4BAA4B;AAErC,SAAS,kBAAkB;AAC3B,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AAOpB,SAAS,cACd,iBACA,QACA;AAIA,qBAAmB,MAAM;AAEzB,SAAO,eAAe,gBAAgB,OAAU;AAC9C,uBAAmB,MAAM;AAEzB,UAAM,cAAc,MAAM,QAAQ;AAKlC,UAAM,CAAC,QAAQ,OAAO,IAAI,MAAM,QAAQ,IAAI;AAAA,MAC1C,gBAAgB;AAAA,MAChB,qBAAqB;AAAA,IACvB,CAAC;AAED,UAAM,OAAO,QAAQ;AAYrB,UAAM,aAAa,YAAY,IAAI,WAAW,SAAS,GAAG;AAC1D,UAAM,SAAS,WAAW;AAK1B,UAAM,gBAAgB,YAAY,IAAI,WAAW,cAAc,GAAG,UAAU;AAC5E,UAAM,aACJ,kBAAkB,eAAe,UAAU,QAAQ,eAAe,UAAU;AAE9E,UAAM,QAAiB;AAAA,MACrB,SAAS,WAAW,MAAM;AAAA,MAC1B,MAAM;AAAA,QACJ,UAAU,OAAO,KAAK,iBAAiB;AAAA,QACvC,GAAI,KAAK,YAAY,EAAE,WAAW,KAAK,UAAU,IAAI,CAAC;AAAA,MACxD;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,QAAQ,MAAM,cAAc;AAAA,MAC5B,MAAM,QAAQ,QAAQ;AAAA,MACtB,GAAI,QAAQ,QAAQ,EAAE,OAAO,OAAO,MAAM,IAAI,CAAC;AAAA,IACjD;AAEA,WACE,qBAAC,iBAAc,OAAc,SAC3B;AAAA,0BAAC,mBAAiB,GAAG,OAAO;AAAA,MAC5B,oBAAC,YAAS,UAAU,MAClB,8BAAC,cAAW,cAAc,OAAO,cAAc,GACjD;AAAA,OACF;AAAA,EAEJ;AACF;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guard-rail/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Lightweight inline content editing for developer-owned Next.js sites.",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"type": "module",
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
"scripts"
|
|
33
33
|
],
|
|
34
34
|
"bin": {
|
|
35
|
-
"guardrail-seed-user": "dist/cli/seed-user.js"
|
|
35
|
+
"guardrail-seed-user": "dist/cli/seed-user.js",
|
|
36
|
+
"guardrail-init-db": "dist/cli/init-db.js"
|
|
36
37
|
},
|
|
37
38
|
"publishConfig": {
|
|
38
39
|
"access": "public"
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -30,10 +30,10 @@ Guardrail installed. Next steps:
|
|
|
30
30
|
1. Set up the database (skip both if this Supabase project already has
|
|
31
31
|
Guardrail applied for another site — see "Many sites, one Supabase
|
|
32
32
|
project" in the README):
|
|
33
|
-
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
- npx guardrail-init-db --push
|
|
34
|
+
- If that project is hosted (not local Docker), also add "guardrail"
|
|
35
|
+
under Settings > API > Exposed schemas in the Supabase dashboard —
|
|
36
|
+
config.toml only covers local dev.
|
|
37
37
|
|
|
38
38
|
2. Wrap your root layout with withGuardrail and mark fields with <GRail>.
|
|
39
39
|
Full guide: https://www.npmjs.com/package/@guard-rail/core
|