@guard-rail/core 0.9.1 → 0.11.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 (74) hide show
  1. package/README.md +253 -36
  2. package/dist/adapters/supabase/auth.js +1 -2
  3. package/dist/adapters/supabase/auth.js.map +1 -1
  4. package/dist/adapters/supabase/client.js +6 -1
  5. package/dist/adapters/supabase/client.js.map +1 -1
  6. package/dist/adapters/supabase/confirm.d.ts +11 -11
  7. package/dist/adapters/supabase/confirm.js +14 -18
  8. package/dist/adapters/supabase/confirm.js.map +1 -1
  9. package/dist/adapters/supabase/index.d.ts +3 -1
  10. package/dist/adapters/supabase/index.js +5 -0
  11. package/dist/adapters/supabase/index.js.map +1 -1
  12. package/dist/adapters/supabase/rateLimit.d.ts +24 -0
  13. package/dist/adapters/supabase/rateLimit.js +37 -0
  14. package/dist/adapters/supabase/rateLimit.js.map +1 -0
  15. package/dist/cli/doctor.d.ts +1 -0
  16. package/dist/cli/doctor.js +1046 -0
  17. package/dist/cli/doctor.js.map +1 -0
  18. package/dist/cli/init-db.js +23 -5
  19. package/dist/cli/init-db.js.map +1 -1
  20. package/dist/core/adapter-types.d.ts +71 -1
  21. package/dist/core/config.d.ts +12 -1
  22. package/dist/core/config.js +29 -1
  23. package/dist/core/config.js.map +1 -1
  24. package/dist/core/csp.d.ts +32 -0
  25. package/dist/core/csp.js +26 -0
  26. package/dist/core/csp.js.map +1 -0
  27. package/dist/core/validate.d.ts +47 -0
  28. package/dist/core/validate.js +118 -0
  29. package/dist/core/validate.js.map +1 -0
  30. package/dist/index.client.d.ts +2 -1
  31. package/dist/index.client.js +3 -0
  32. package/dist/index.client.js.map +1 -1
  33. package/dist/index.d.ts +2 -1
  34. package/dist/index.js +3 -0
  35. package/dist/index.js.map +1 -1
  36. package/dist/react/Admin.d.ts +5 -3
  37. package/dist/react/Admin.js +29 -41
  38. package/dist/react/Admin.js.map +1 -1
  39. package/dist/react/AuthDrawer.js +22 -34
  40. package/dist/react/AuthDrawer.js.map +1 -1
  41. package/dist/react/AuthRedirectSync.js +28 -22
  42. package/dist/react/AuthRedirectSync.js.map +1 -1
  43. package/dist/react/GRail/client.js +27 -47
  44. package/dist/react/GRail/client.js.map +1 -1
  45. package/dist/react/GRail/render.js +3 -2
  46. package/dist/react/GRail/render.js.map +1 -1
  47. package/dist/react/MediaPicker.js +12 -16
  48. package/dist/react/MediaPicker.js.map +1 -1
  49. package/dist/react/Repeater/client.js +12 -13
  50. package/dist/react/Repeater/client.js.map +1 -1
  51. package/dist/react/Team.d.ts +1 -7
  52. package/dist/react/Team.js +21 -46
  53. package/dist/react/Team.js.map +1 -1
  54. package/dist/react/guardrail.css +563 -0
  55. package/dist/server/actions.js +52 -31
  56. package/dist/server/actions.js.map +1 -1
  57. package/dist/server/loader.js +4 -4
  58. package/dist/server/loader.js.map +1 -1
  59. package/dist/server/rateLimit.d.ts +21 -0
  60. package/dist/server/rateLimit.js +94 -0
  61. package/dist/server/rateLimit.js.map +1 -0
  62. package/dist/server/repeaterActions.js +9 -1
  63. package/dist/server/repeaterActions.js.map +1 -1
  64. package/dist/server/wrapper.js +18 -1
  65. package/dist/server/wrapper.js.map +1 -1
  66. package/package.json +11 -9
  67. package/supabase/admin/check-grants.sql +41 -0
  68. package/supabase/migrations/20261028000000_require_confirmed_email.sql +53 -0
  69. package/supabase/migrations/20261028010000_rate_limits.sql +67 -0
  70. package/supabase/migrations/20261028020000_member_and_bucket_guards.sql +96 -0
  71. package/dist/react/styles.d.ts +0 -504
  72. package/dist/react/styles.js +0 -500
  73. package/dist/react/styles.js.map +0 -1
  74. package/scripts/postinstall.mjs +0 -48
@@ -0,0 +1,118 @@
1
+ const OVERRIDE_TYPES = ["text", "link", "image", "button"];
2
+ const LIMITS = {
3
+ pathKey: 200,
4
+ text: 2e4,
5
+ url: 2048,
6
+ alt: 1e3,
7
+ dimension: 2e4
8
+ };
9
+ const CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
10
+ const SCHEME = /^([a-z][a-z0-9+.\-]*):/i;
11
+ const LINK_SCHEMES = ["http", "https", "mailto", "tel"];
12
+ const IMAGE_SCHEMES = ["http", "https"];
13
+ function allowedUrl(raw, schemes) {
14
+ if (typeof raw !== "string") return null;
15
+ const value = raw.trim();
16
+ if (!value || value.length > LIMITS.url || CONTROL_CHARS.test(value)) return null;
17
+ const scheme = value.replace(/\s/g, "").match(SCHEME)?.[1]?.toLowerCase();
18
+ if (scheme && !schemes.includes(scheme)) return null;
19
+ return value;
20
+ }
21
+ function safeHref(raw) {
22
+ return allowedUrl(raw, LINK_SCHEMES);
23
+ }
24
+ function safeImageSrc(raw) {
25
+ return allowedUrl(raw, IMAGE_SCHEMES);
26
+ }
27
+ function validPathKey(raw) {
28
+ if (typeof raw !== "string") return null;
29
+ const key = raw.trim();
30
+ return key && key.length <= LIMITS.pathKey && !CONTROL_CHARS.test(key) ? key : null;
31
+ }
32
+ function validateOverrideValue(input) {
33
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
34
+ return { ok: false, error: "Invalid value." };
35
+ }
36
+ const v = input;
37
+ if (typeof v.type !== "string" || !OVERRIDE_TYPES.includes(v.type)) {
38
+ return { ok: false, error: `Unknown field type "${String(v.type)}".` };
39
+ }
40
+ const out = { type: v.type };
41
+ if (v.text !== void 0) {
42
+ if (typeof v.text !== "string" || v.text.length > LIMITS.text) {
43
+ return { ok: false, error: `Text must be a string under ${LIMITS.text} characters.` };
44
+ }
45
+ out.text = v.text;
46
+ }
47
+ if (v.alt !== void 0) {
48
+ if (typeof v.alt !== "string" || v.alt.length > LIMITS.alt) {
49
+ return { ok: false, error: `Alt text must be under ${LIMITS.alt} characters.` };
50
+ }
51
+ out.alt = v.alt;
52
+ }
53
+ if (v.href !== void 0 && v.href !== "") {
54
+ const href = safeHref(v.href);
55
+ if (href === null) {
56
+ return {
57
+ ok: false,
58
+ error: "That link isn't allowed. Use a path like /about, or an http(s), mailto or tel address."
59
+ };
60
+ }
61
+ out.href = href;
62
+ } else if (v.href === "") {
63
+ out.href = "";
64
+ }
65
+ if (v.src !== void 0 && v.src !== "") {
66
+ const src = safeImageSrc(v.src);
67
+ if (src === null) {
68
+ return { ok: false, error: "That image address isn't allowed. Use a path or an http(s) URL." };
69
+ }
70
+ out.src = src;
71
+ } else if (v.src === "") {
72
+ out.src = "";
73
+ }
74
+ for (const key of ["width", "height"]) {
75
+ const n = v[key];
76
+ if (n === void 0) continue;
77
+ if (typeof n !== "number" || !Number.isFinite(n) || n <= 0 || n > LIMITS.dimension) {
78
+ return { ok: false, error: `Image ${key} is out of range.` };
79
+ }
80
+ out[key] = Math.round(n);
81
+ }
82
+ return { ok: true, value: out };
83
+ }
84
+ const EMAIL_PATTERN = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
85
+ function cleanEmail(raw) {
86
+ if (typeof raw !== "string") return null;
87
+ const email = raw.trim().toLowerCase();
88
+ return email.length <= 254 && !CONTROL_CHARS.test(email) && EMAIL_PATTERN.test(email) ? email : null;
89
+ }
90
+ function isRole(raw) {
91
+ return raw === "admin" || raw === "editor";
92
+ }
93
+ function validId(raw) {
94
+ return typeof raw === "string" && raw.length > 0 && raw.length <= LIMITS.pathKey && !CONTROL_CHARS.test(raw) ? raw : null;
95
+ }
96
+ function validIdList(raw, max = 1e3) {
97
+ if (!Array.isArray(raw) || raw.length > max) return null;
98
+ const out = [];
99
+ for (const item of raw) {
100
+ const id = validId(item);
101
+ if (id === null) return null;
102
+ out.push(id);
103
+ }
104
+ return out;
105
+ }
106
+ export {
107
+ LIMITS,
108
+ OVERRIDE_TYPES,
109
+ cleanEmail,
110
+ isRole,
111
+ safeHref,
112
+ safeImageSrc,
113
+ validId,
114
+ validIdList,
115
+ validPathKey,
116
+ validateOverrideValue
117
+ };
118
+ //# sourceMappingURL=validate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/core/validate.ts"],"sourcesContent":["import type { GRRole, OverrideType, OverrideValue } from \"./types\";\n\n/**\n * Checks on field values before they are stored and again before they are\n * rendered. Pure and dependency-free, so the same rules run on the server (what\n * may be written) and in the renderer (what rows already in the table may do).\n *\n * Why it matters: an editor types these, a visitor's browser then acts on them.\n * A link whose href is `javascript:...` is a script that runs for whoever\n * clicks it, so a low-privilege editor account must not be able to store one.\n */\n\nexport const OVERRIDE_TYPES: readonly OverrideType[] = [\"text\", \"link\", \"image\", \"button\"];\n\nexport const LIMITS = {\n pathKey: 200,\n text: 20_000,\n url: 2_048,\n alt: 1_000,\n dimension: 20_000,\n} as const;\n\nconst CONTROL_CHARS = /[\\u0000-\\u001f\\u007f]/;\nconst SCHEME = /^([a-z][a-z0-9+.\\-]*):/i;\n\nconst LINK_SCHEMES = [\"http\", \"https\", \"mailto\", \"tel\"];\nconst IMAGE_SCHEMES = [\"http\", \"https\"];\n\nfunction allowedUrl(raw: unknown, schemes: readonly string[]): string | null {\n if (typeof raw !== \"string\") return null;\n const value = raw.trim();\n if (!value || value.length > LIMITS.url || CONTROL_CHARS.test(value)) return null;\n // A browser ignores whitespace inside a scheme (\"java\\tscript:\"), so look for\n // one with it removed. Control characters are already refused above.\n const scheme = value.replace(/\\s/g, \"\").match(SCHEME)?.[1]?.toLowerCase();\n if (scheme && !schemes.includes(scheme)) return null;\n return value;\n}\n\n/** The link, if it is a relative path, an anchor, or http(s)/mailto/tel; otherwise null. */\nexport function safeHref(raw: unknown): string | null {\n return allowedUrl(raw, LINK_SCHEMES);\n}\n\n/** The image address, if it is relative or http(s); otherwise null. */\nexport function safeImageSrc(raw: unknown): string | null {\n return allowedUrl(raw, IMAGE_SCHEMES);\n}\n\nexport function validPathKey(raw: unknown): string | null {\n if (typeof raw !== \"string\") return null;\n const key = raw.trim();\n return key && key.length <= LIMITS.pathKey && !CONTROL_CHARS.test(key) ? key : null;\n}\n\ntype Checked = { ok: true; value: OverrideValue } | { ok: false; error: string };\n\n/**\n * Validates a value from the browser and returns a clean copy: only known\n * fields, only the right shapes, bounded sizes. Anything else is an error, not\n * silently dropped, so a bad write fails visibly.\n */\nexport function validateOverrideValue(input: unknown): Checked {\n if (!input || typeof input !== \"object\" || Array.isArray(input)) {\n return { ok: false, error: \"Invalid value.\" };\n }\n const v = input as Record<string, unknown>;\n if (typeof v.type !== \"string\" || !OVERRIDE_TYPES.includes(v.type as OverrideType)) {\n return { ok: false, error: `Unknown field type \"${String(v.type)}\".` };\n }\n const out: OverrideValue = { type: v.type as OverrideType };\n\n if (v.text !== undefined) {\n if (typeof v.text !== \"string\" || v.text.length > LIMITS.text) {\n return { ok: false, error: `Text must be a string under ${LIMITS.text} characters.` };\n }\n out.text = v.text;\n }\n if (v.alt !== undefined) {\n if (typeof v.alt !== \"string\" || v.alt.length > LIMITS.alt) {\n return { ok: false, error: `Alt text must be under ${LIMITS.alt} characters.` };\n }\n out.alt = v.alt;\n }\n if (v.href !== undefined && v.href !== \"\") {\n const href = safeHref(v.href);\n if (href === null) {\n return {\n ok: false,\n error: \"That link isn't allowed. Use a path like /about, or an http(s), mailto or tel address.\",\n };\n }\n out.href = href;\n } else if (v.href === \"\") {\n out.href = \"\";\n }\n if (v.src !== undefined && v.src !== \"\") {\n const src = safeImageSrc(v.src);\n if (src === null) {\n return { ok: false, error: \"That image address isn't allowed. Use a path or an http(s) URL.\" };\n }\n out.src = src;\n } else if (v.src === \"\") {\n out.src = \"\";\n }\n for (const key of [\"width\", \"height\"] as const) {\n const n = v[key];\n if (n === undefined) continue;\n if (typeof n !== \"number\" || !Number.isFinite(n) || n <= 0 || n > LIMITS.dimension) {\n return { ok: false, error: `Image ${key} is out of range.` };\n }\n out[key] = Math.round(n);\n }\n return { ok: true, value: out };\n}\n\nconst EMAIL_PATTERN = /^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$/;\n\n/** A normalized (trimmed, lower-cased) address, or null if it isn't a plausible one. RFC 5321 caps an address at 254 characters. */\nexport function cleanEmail(raw: unknown): string | null {\n if (typeof raw !== \"string\") return null;\n const email = raw.trim().toLowerCase();\n return email.length <= 254 && !CONTROL_CHARS.test(email) && EMAIL_PATTERN.test(email) ? email : null;\n}\n\nexport function isRole(raw: unknown): raw is GRRole {\n return raw === \"admin\" || raw === \"editor\";\n}\n\n/** An identifier from the browser (a repeater or item id): a bounded string without control characters. */\nexport function validId(raw: unknown): string | null {\n return typeof raw === \"string\" && raw.length > 0 && raw.length <= LIMITS.pathKey && !CONTROL_CHARS.test(raw)\n ? raw\n : null;\n}\n\n/** A list of ids: an array of valid ids, no longer than `max`. */\nexport function validIdList(raw: unknown, max = 1_000): string[] | null {\n if (!Array.isArray(raw) || raw.length > max) return null;\n const out: string[] = [];\n for (const item of raw) {\n const id = validId(item);\n if (id === null) return null;\n out.push(id);\n }\n return out;\n}\n"],"mappings":"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","names":[]}
@@ -1,11 +1,12 @@
1
1
  export { GRail } from './react/GRail/universal.js';
2
2
  export { useGRail } from './react/Provider.js';
3
+ export { GRCspDirectives, GRCspOptions, cspString, guardrailCspDirectives } from './core/csp.js';
3
4
  export { createAssetUpload, forgotPasswordAction, inviteMember, listAssets, listTeam, removeMember, sendPasswordResetAction, setFieldValue, setPasswordAction, skipSetPasswordAction, updateMemberRole } from './server/actions.js';
4
5
  export { addRepeaterItem, removeRepeaterItem, reorderRepeaterItems } from './server/repeaterActions.js';
5
6
  export { GRepeaterProps, RepeaterItemProps } from './react/Repeater/index.js';
6
7
  export { ResendEmailOptions, ResendInviteContent, ResendInviteOptions, ResendInviteTemplateEvent, ResendResetTemplateEvent } from './email/resend.js';
7
8
  export { ActionState, GRAsset, GRMember, GRRepeaterItem, GRRole, GRState, GRViewer, HTMLKeys, OverrideRow, OverrideType, OverrideValue, PolymorphicProps, SetPasswordState } from './core/types.js';
8
- export { AuthRedirectParams, AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRPasswordResetEvent, GRPrefetch, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget } from './core/adapter-types.js';
9
+ export { AuthRedirectParams, AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRPasswordResetEvent, GRPrefetch, GRRateLimitHit, GRRateLimitName, GRRateLimiter, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget } from './core/adapter-types.js';
9
10
  import 'react/jsx-runtime';
10
11
  import 'react';
11
12
 
@@ -1,5 +1,6 @@
1
1
  import { GRail } from "./react/GRail/universal";
2
2
  import { useGRail } from "./react/Provider";
3
+ import { guardrailCspDirectives, cspString } from "./core/csp";
3
4
  import {
4
5
  listTeam,
5
6
  inviteMember,
@@ -81,9 +82,11 @@ export {
81
82
  configureGuardrail,
82
83
  consoleEmails,
83
84
  createAssetUpload,
85
+ cspString,
84
86
  forgotPasswordAction,
85
87
  getFieldValue,
86
88
  getRepeaterItemBySlug,
89
+ guardrailCspDirectives,
87
90
  inviteMember,
88
91
  isAdmin,
89
92
  isEditor,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.client.ts"],"sourcesContent":["// Client-graph entry. `import ... from \"@guard-rail/core\"` resolves here from a\n// Client Component (see \"exports\" in package.json) and to ./index from a\n// Server Component, so an app uses one import path everywhere.\n//\n// Nothing server-only may be imported here — this is bundled for the browser.\n// Server Actions are fine: a \"use server\" module is replaced by references in\n// the client bundle, which is what lets an app build its own team/repeater UI\n// in a Client Component.\n\nexport { GRail } from \"./react/GRail/universal\";\nexport { useGRail } from \"./react/Provider\";\n\nexport {\n listTeam,\n inviteMember,\n updateMemberRole,\n removeMember,\n createAssetUpload,\n listAssets,\n setPasswordAction,\n skipSetPasswordAction,\n forgotPasswordAction,\n sendPasswordResetAction,\n setFieldValue,\n} from \"./server/actions\";\nexport {\n addRepeaterItem,\n removeRepeaterItem,\n reorderRepeaterItems,\n} from \"./server/repeaterActions\";\n\n/**\n * Stand-ins for the server-only exports, so importing one from a Client\n * Component fails with an explanation instead of an undefined-is-not-a-function\n * deep in a render. Also what a tool that doesn't set the \"react-server\"\n * condition (a test runner, a plain Node script) gets from the package root —\n * `@guard-rail/core/server` is the explicit path to the real ones there.\n */\nfunction serverOnly(name: string, hint: string) {\n return (..._args: unknown[]): never => {\n throw new Error(\n `Guardrail: ${name} only works in a Server Component. ${hint} ` +\n `(If this isn't a Client Component, your tooling may not set the ` +\n `\"react-server\" condition — import from \"@guard-rail/core/server\" instead.)`,\n );\n };\n}\n\nexport const withGuardrail = serverOnly(\n \"withGuardrail()\",\n \"Call it in your root layout, which is a Server Component.\",\n);\nexport const configureGuardrail = serverOnly(\n \"configureGuardrail()\",\n \"Configuration is applied by withGuardrail() in your root layout.\",\n);\nexport const GRepeater = serverOnly(\n \"<GRepeater>\",\n \"Render it in a Server Component and pass it into this one as children or a prop.\",\n);\nexport const listRepeaterItems = serverOnly(\n \"listRepeaterItems()\",\n \"Read repeater items in a Server Component and pass them down.\",\n);\nexport const getRepeaterItemBySlug = serverOnly(\n \"getRepeaterItemBySlug()\",\n \"Resolve the item in a Server Component (your [slug]/page.tsx) and pass it down.\",\n);\nexport const getFieldValue = serverOnly(\n \"getFieldValue()\",\n \"Read the value in a Server Component and pass it down as a prop.\",\n);\nexport const isAdmin = serverOnly(\n \"isAdmin()\",\n \"Call it inside your own Server Action to authorize it; in a Client Component, read useGRail().role to decide what to show.\",\n);\nexport const isEditor = serverOnly(\n \"isEditor()\",\n \"Call it inside your own Server Action to authorize it; in a Client Component, read useGRail().role to decide what to show.\",\n);\nexport const canEdit = serverOnly(\n \"canEdit()\",\n \"Call it in a Server Component; in a Client Component, read `canEdit` from useGRail().\",\n);\nexport const resendInvite = serverOnly(\n \"resendInvite()\",\n \"Pass it as onInvite in withGuardrail(), which runs in your root layout.\",\n);\nexport const resendEmails = serverOnly(\n \"resendEmails()\",\n \"Spread it into withGuardrail(), which runs in your root layout.\",\n);\nexport const consoleEmails = serverOnly(\n \"consoleEmails()\",\n \"Spread it into withGuardrail(), which runs in your root layout.\",\n);\n\nexport type { GRepeaterProps, RepeaterItemProps } from \"./react/Repeater\";\nexport type {\n ResendInviteOptions,\n ResendEmailOptions,\n ResendInviteContent,\n ResendInviteTemplateEvent,\n ResendResetTemplateEvent,\n} from \"./email/resend\";\nexport type {\n GRState,\n GRRole,\n GRMember,\n GRViewer,\n GRAsset,\n GRRepeaterItem,\n ActionState,\n SetPasswordState,\n OverrideRow,\n OverrideValue,\n OverrideType,\n PolymorphicProps,\n HTMLKeys,\n} from \"./core/types\";\nexport type {\n GRConfig,\n GRPrefetch,\n GRMembershipAdapter,\n GRInviteEvent,\n GRStorageAdapter,\n GRUploadTarget,\n GRRepeaterAdapter,\n GRDBAdapter,\n GRAuthAdapter,\n GRSession,\n GRStorageRow,\n AuthResult,\n AuthRedirectParams,\n GRPasswordResetEvent,\n} from \"./core/adapter-types\";\n"],"mappings":"AASA,SAAS,aAAa;AACtB,SAAS,gBAAgB;AAEzB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AASP,SAAS,WAAW,MAAc,MAAc;AAC9C,SAAO,IAAI,UAA4B;AACrC,UAAM,IAAI;AAAA,MACR,cAAc,IAAI,sCAAsC,IAAI;AAAA,IAG9D;AAAA,EACF;AACF;AAEO,MAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AACF;AACO,MAAM,qBAAqB;AAAA,EAChC;AAAA,EACA;AACF;AACO,MAAM,YAAY;AAAA,EACvB;AAAA,EACA;AACF;AACO,MAAM,oBAAoB;AAAA,EAC/B;AAAA,EACA;AACF;AACO,MAAM,wBAAwB;AAAA,EACnC;AAAA,EACA;AACF;AACO,MAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AACF;AACO,MAAM,UAAU;AAAA,EACrB;AAAA,EACA;AACF;AACO,MAAM,WAAW;AAAA,EACtB;AAAA,EACA;AACF;AACO,MAAM,UAAU;AAAA,EACrB;AAAA,EACA;AACF;AACO,MAAM,eAAe;AAAA,EAC1B;AAAA,EACA;AACF;AACO,MAAM,eAAe;AAAA,EAC1B;AAAA,EACA;AACF;AACO,MAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/index.client.ts"],"sourcesContent":["// Client-graph entry. `import ... from \"@guard-rail/core\"` resolves here from a\n// Client Component (see \"exports\" in package.json) and to ./index from a\n// Server Component, so an app uses one import path everywhere.\n//\n// Nothing server-only may be imported here — this is bundled for the browser.\n// Server Actions are fine: a \"use server\" module is replaced by references in\n// the client bundle, which is what lets an app build its own team/repeater UI\n// in a Client Component.\n\nexport { GRail } from \"./react/GRail/universal\";\nexport { useGRail } from \"./react/Provider\";\n// What a strict Content-Security-Policy has to allow for the editing chrome.\nexport { guardrailCspDirectives, cspString } from \"./core/csp\";\nexport type { GRCspDirectives, GRCspOptions } from \"./core/csp\";\n\nexport {\n listTeam,\n inviteMember,\n updateMemberRole,\n removeMember,\n createAssetUpload,\n listAssets,\n setPasswordAction,\n skipSetPasswordAction,\n forgotPasswordAction,\n sendPasswordResetAction,\n setFieldValue,\n} from \"./server/actions\";\nexport {\n addRepeaterItem,\n removeRepeaterItem,\n reorderRepeaterItems,\n} from \"./server/repeaterActions\";\n\n/**\n * Stand-ins for the server-only exports, so importing one from a Client\n * Component fails with an explanation instead of an undefined-is-not-a-function\n * deep in a render. Also what a tool that doesn't set the \"react-server\"\n * condition (a test runner, a plain Node script) gets from the package root —\n * `@guard-rail/core/server` is the explicit path to the real ones there.\n */\nfunction serverOnly(name: string, hint: string) {\n return (..._args: unknown[]): never => {\n throw new Error(\n `Guardrail: ${name} only works in a Server Component. ${hint} ` +\n `(If this isn't a Client Component, your tooling may not set the ` +\n `\"react-server\" condition — import from \"@guard-rail/core/server\" instead.)`,\n );\n };\n}\n\nexport const withGuardrail = serverOnly(\n \"withGuardrail()\",\n \"Call it in your root layout, which is a Server Component.\",\n);\nexport const configureGuardrail = serverOnly(\n \"configureGuardrail()\",\n \"Configuration is applied by withGuardrail() in your root layout.\",\n);\nexport const GRepeater = serverOnly(\n \"<GRepeater>\",\n \"Render it in a Server Component and pass it into this one as children or a prop.\",\n);\nexport const listRepeaterItems = serverOnly(\n \"listRepeaterItems()\",\n \"Read repeater items in a Server Component and pass them down.\",\n);\nexport const getRepeaterItemBySlug = serverOnly(\n \"getRepeaterItemBySlug()\",\n \"Resolve the item in a Server Component (your [slug]/page.tsx) and pass it down.\",\n);\nexport const getFieldValue = serverOnly(\n \"getFieldValue()\",\n \"Read the value in a Server Component and pass it down as a prop.\",\n);\nexport const isAdmin = serverOnly(\n \"isAdmin()\",\n \"Call it inside your own Server Action to authorize it; in a Client Component, read useGRail().role to decide what to show.\",\n);\nexport const isEditor = serverOnly(\n \"isEditor()\",\n \"Call it inside your own Server Action to authorize it; in a Client Component, read useGRail().role to decide what to show.\",\n);\nexport const canEdit = serverOnly(\n \"canEdit()\",\n \"Call it in a Server Component; in a Client Component, read `canEdit` from useGRail().\",\n);\nexport const resendInvite = serverOnly(\n \"resendInvite()\",\n \"Pass it as onInvite in withGuardrail(), which runs in your root layout.\",\n);\nexport const resendEmails = serverOnly(\n \"resendEmails()\",\n \"Spread it into withGuardrail(), which runs in your root layout.\",\n);\nexport const consoleEmails = serverOnly(\n \"consoleEmails()\",\n \"Spread it into withGuardrail(), which runs in your root layout.\",\n);\n\nexport type { GRepeaterProps, RepeaterItemProps } from \"./react/Repeater\";\nexport type {\n ResendInviteOptions,\n ResendEmailOptions,\n ResendInviteContent,\n ResendInviteTemplateEvent,\n ResendResetTemplateEvent,\n} from \"./email/resend\";\nexport type {\n GRState,\n GRRole,\n GRMember,\n GRViewer,\n GRAsset,\n GRRepeaterItem,\n ActionState,\n SetPasswordState,\n OverrideRow,\n OverrideValue,\n OverrideType,\n PolymorphicProps,\n HTMLKeys,\n} from \"./core/types\";\nexport type {\n GRConfig,\n GRPrefetch,\n GRMembershipAdapter,\n GRInviteEvent,\n GRStorageAdapter,\n GRUploadTarget,\n GRRepeaterAdapter,\n GRDBAdapter,\n GRAuthAdapter,\n GRSession,\n GRStorageRow,\n AuthResult,\n AuthRedirectParams,\n GRPasswordResetEvent,\n GRRateLimiter,\n GRRateLimitHit,\n GRRateLimitName,\n} from \"./core/adapter-types\";\n"],"mappings":"AASA,SAAS,aAAa;AACtB,SAAS,gBAAgB;AAEzB,SAAS,wBAAwB,iBAAiB;AAGlD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AASP,SAAS,WAAW,MAAc,MAAc;AAC9C,SAAO,IAAI,UAA4B;AACrC,UAAM,IAAI;AAAA,MACR,cAAc,IAAI,sCAAsC,IAAI;AAAA,IAG9D;AAAA,EACF;AACF;AAEO,MAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AACF;AACO,MAAM,qBAAqB;AAAA,EAChC;AAAA,EACA;AACF;AACO,MAAM,YAAY;AAAA,EACvB;AAAA,EACA;AACF;AACO,MAAM,oBAAoB;AAAA,EAC/B;AAAA,EACA;AACF;AACO,MAAM,wBAAwB;AAAA,EACnC;AAAA,EACA;AACF;AACO,MAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AACF;AACO,MAAM,UAAU;AAAA,EACrB;AAAA,EACA;AACF;AACO,MAAM,WAAW;AAAA,EACtB;AAAA,EACA;AACF;AACO,MAAM,UAAU;AAAA,EACrB;AAAA,EACA;AACF;AACO,MAAM,eAAe;AAAA,EAC1B;AAAA,EACA;AACF;AACO,MAAM,eAAe;AAAA,EAC1B;AAAA,EACA;AACF;AACO,MAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AACF;","names":[]}
package/dist/index.d.ts CHANGED
@@ -10,7 +10,8 @@ export { ResendEmailOptions, ResendInviteContent, ResendInviteOptions, ResendInv
10
10
  export { consoleEmails } from './email/console.js';
11
11
  export { addRepeaterItem, removeRepeaterItem, reorderRepeaterItems } from './server/repeaterActions.js';
12
12
  export { useGRail } from './react/Provider.js';
13
+ export { GRCspDirectives, GRCspOptions, cspString, guardrailCspDirectives } from './core/csp.js';
13
14
  export { ActionState, GRAsset, GRMember, GRRepeaterItem, GRRole, GRState, GRViewer, HTMLKeys, OverrideRow, OverrideType, OverrideValue, PolymorphicProps, SetPasswordState } from './core/types.js';
14
- export { AuthRedirectParams, AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRPasswordResetEvent, GRPrefetch, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget } from './core/adapter-types.js';
15
+ export { AuthRedirectParams, AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRPasswordResetEvent, GRPrefetch, GRRateLimitHit, GRRateLimitName, GRRateLimiter, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget } from './core/adapter-types.js';
15
16
  import 'react/jsx-runtime';
16
17
  import 'react';
package/dist/index.js CHANGED
@@ -29,6 +29,7 @@ import {
29
29
  reorderRepeaterItems
30
30
  } from "./server/repeaterActions";
31
31
  import { useGRail } from "./react/Provider";
32
+ import { guardrailCspDirectives, cspString } from "./core/csp";
32
33
  export {
33
34
  GRail,
34
35
  GRepeater,
@@ -37,9 +38,11 @@ export {
37
38
  configureGuardrail,
38
39
  consoleEmails,
39
40
  createAssetUpload,
41
+ cspString,
40
42
  forgotPasswordAction,
41
43
  getFieldValue,
42
44
  getRepeaterItemBySlug,
45
+ guardrailCspDirectives,
43
46
  inviteMember,
44
47
  isAdmin,
45
48
  isEditor,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Server-safe entry. Client components are reached through the files that\n// carry their own \"use client\" directive, so importing this from a Server\n// Component does not pull the editor into the client bundle.\nexport { withGuardrail } from \"./server/wrapper\";\n\n// Team management and the set-password step, exported so an app can build\n// its own UI instead of using the bundled drawer / sign-in bar.\nexport {\n listTeam,\n inviteMember,\n updateMemberRole,\n removeMember,\n createAssetUpload,\n listAssets,\n setPasswordAction,\n skipSetPasswordAction,\n forgotPasswordAction,\n sendPasswordResetAction,\n setFieldValue,\n} from \"./server/actions\";\nexport { GRail } from \"./react/GRail\";\nexport { GRepeater } from \"./react/Repeater\";\nexport type { GRepeaterProps, RepeaterItemProps } from \"./react/Repeater\";\nexport { configureGuardrail } from \"./core/config\";\nexport {\n listRepeaterItems,\n getRepeaterItemBySlug,\n} from \"./server/repeaterData\";\nexport { getFieldValue } from \"./server/fieldData\";\n// Role / edit-mode checks for a custom admin UI and the Server Actions behind\n// it. Plain server functions, not actions — see access.ts for why.\nexport { isAdmin, isEditor, canEdit } from \"./server/access\";\n// Invite delivery through Resend, as an onInvite handler — see email/resend.ts.\nexport { resendInvite, resendEmails } from \"./email/resend\";\nexport { consoleEmails } from \"./email/console\";\nexport type {\n ResendInviteOptions,\n ResendEmailOptions,\n ResendInviteContent,\n ResendInviteTemplateEvent,\n ResendResetTemplateEvent,\n} from \"./email/resend\";\n// Mutations, exported so an app can build its own add/remove/reorder UI\n// instead of the bundled chrome — same rationale as the team actions above.\nexport {\n addRepeaterItem,\n removeRepeaterItem,\n reorderRepeaterItems,\n} from \"./server/repeaterActions\";\nexport { useGRail } from \"./react/Provider\";\n\nexport type {\n GRState,\n GRRole,\n GRMember,\n GRViewer,\n GRAsset,\n GRRepeaterItem,\n ActionState,\n SetPasswordState,\n OverrideRow,\n OverrideValue,\n OverrideType,\n PolymorphicProps,\n HTMLKeys,\n} from \"./core/types\";\nexport type {\n GRConfig,\n GRPrefetch,\n GRMembershipAdapter,\n GRInviteEvent,\n GRStorageAdapter,\n GRUploadTarget,\n GRRepeaterAdapter,\n GRDBAdapter,\n GRAuthAdapter,\n GRSession,\n GRStorageRow,\n AuthResult,\n AuthRedirectParams,\n GRPasswordResetEvent,\n} from \"./core/adapter-types\";\n"],"mappings":"AAGA,SAAS,qBAAqB;AAI9B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,aAAa;AACtB,SAAS,iBAAiB;AAE1B,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,qBAAqB;AAG9B,SAAS,SAAS,UAAU,eAAe;AAE3C,SAAS,cAAc,oBAAoB;AAC3C,SAAS,qBAAqB;AAU9B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,gBAAgB;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Server-safe entry. Client components are reached through the files that\n// carry their own \"use client\" directive, so importing this from a Server\n// Component does not pull the editor into the client bundle.\nexport { withGuardrail } from \"./server/wrapper\";\n\n// Team management and the set-password step, exported so an app can build\n// its own UI instead of using the bundled drawer / sign-in bar.\nexport {\n listTeam,\n inviteMember,\n updateMemberRole,\n removeMember,\n createAssetUpload,\n listAssets,\n setPasswordAction,\n skipSetPasswordAction,\n forgotPasswordAction,\n sendPasswordResetAction,\n setFieldValue,\n} from \"./server/actions\";\nexport { GRail } from \"./react/GRail\";\nexport { GRepeater } from \"./react/Repeater\";\nexport type { GRepeaterProps, RepeaterItemProps } from \"./react/Repeater\";\nexport { configureGuardrail } from \"./core/config\";\nexport {\n listRepeaterItems,\n getRepeaterItemBySlug,\n} from \"./server/repeaterData\";\nexport { getFieldValue } from \"./server/fieldData\";\n// Role / edit-mode checks for a custom admin UI and the Server Actions behind\n// it. Plain server functions, not actions — see access.ts for why.\nexport { isAdmin, isEditor, canEdit } from \"./server/access\";\n// Invite delivery through Resend, as an onInvite handler — see email/resend.ts.\nexport { resendInvite, resendEmails } from \"./email/resend\";\nexport { consoleEmails } from \"./email/console\";\nexport type {\n ResendInviteOptions,\n ResendEmailOptions,\n ResendInviteContent,\n ResendInviteTemplateEvent,\n ResendResetTemplateEvent,\n} from \"./email/resend\";\n// Mutations, exported so an app can build its own add/remove/reorder UI\n// instead of the bundled chrome — same rationale as the team actions above.\nexport {\n addRepeaterItem,\n removeRepeaterItem,\n reorderRepeaterItems,\n} from \"./server/repeaterActions\";\nexport { useGRail } from \"./react/Provider\";\n// What a strict Content-Security-Policy has to allow for the editing chrome.\nexport { guardrailCspDirectives, cspString } from \"./core/csp\";\nexport type { GRCspDirectives, GRCspOptions } from \"./core/csp\";\n\nexport type {\n GRState,\n GRRole,\n GRMember,\n GRViewer,\n GRAsset,\n GRRepeaterItem,\n ActionState,\n SetPasswordState,\n OverrideRow,\n OverrideValue,\n OverrideType,\n PolymorphicProps,\n HTMLKeys,\n} from \"./core/types\";\nexport type {\n GRConfig,\n GRPrefetch,\n GRMembershipAdapter,\n GRInviteEvent,\n GRStorageAdapter,\n GRUploadTarget,\n GRRepeaterAdapter,\n GRDBAdapter,\n GRAuthAdapter,\n GRSession,\n GRStorageRow,\n AuthResult,\n AuthRedirectParams,\n GRPasswordResetEvent,\n GRRateLimiter,\n GRRateLimitHit,\n GRRateLimitName,\n} from \"./core/adapter-types\";\n"],"mappings":"AAGA,SAAS,qBAAqB;AAI9B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,aAAa;AACtB,SAAS,iBAAiB;AAE1B,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,qBAAqB;AAG9B,SAAS,SAAS,UAAU,eAAe;AAE3C,SAAS,cAAc,oBAAoB;AAC3C,SAAS,qBAAqB;AAU9B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,gBAAgB;AAEzB,SAAS,wBAAwB,iBAAiB;","names":[]}
@@ -2,9 +2,11 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import React__default from 'react';
3
3
 
4
4
  /**
5
- * Owns the theme stylesheet so every piece of chrome — the sign-in bar, the
6
- * toolbar, and the field popups portaled to <body> — picks up the same
7
- * light/dark variables from one place.
5
+ * Imports the chrome's stylesheet (guardrail.css), which also defines the
6
+ * light/dark theme variables, so every piece of chrome — the sign-in bar, the
7
+ * toolbar, and the field popups portaled to <body> — picks them up from one
8
+ * place. A stylesheet rather than inline styles so the chrome works under a
9
+ * strict Content-Security-Policy.
8
10
  */
9
11
  declare function GRailAdmin(props: {
10
12
  toolbarExtra?: React__default.ReactNode;
@@ -7,16 +7,13 @@ import { useGRail } from "./Provider";
7
7
  import { TeamDrawer } from "./Team";
8
8
  import { AuthDrawer } from "./AuthDrawer";
9
9
  import { CloseIcon, LogOutIcon, PeopleIcon } from "./Icons";
10
- import { S, THEME_CSS, v } from "./styles";
10
+ import "./guardrail.css";
11
11
  import { GR_COOKIES, GR_EDIT_MODE_OFF_TTL_SECONDS } from "../core/defs";
12
12
  const initialState = { email: "", password: "" };
13
13
  function GRailAdmin(props) {
14
14
  const { isEditMode } = useGRail();
15
15
  if (!isEditMode) return null;
16
- return /* @__PURE__ */ jsxs(Fragment, { children: [
17
- /* @__PURE__ */ jsx("style", { href: "guardrail-theme", precedence: "default", children: THEME_CSS }),
18
- /* @__PURE__ */ jsx(AdminBody, { ...props })
19
- ] });
16
+ return /* @__PURE__ */ jsx(AdminBody, { ...props });
20
17
  }
21
18
  function AdminBody({ toolbarExtra }) {
22
19
  const { isAuth, isEditMode, needsPassword, authError, role, auth } = useGRail();
@@ -60,17 +57,17 @@ function AdminBody({ toolbarExtra }) {
60
57
  if (!isAuth) {
61
58
  if (!auth.password) {
62
59
  return /* @__PURE__ */ jsxs(Fragment, { children: [
63
- /* @__PURE__ */ jsx("div", { "data-guardrail": true, style: S.banner, children: /* @__PURE__ */ jsxs("div", { style: S.bannerForm, children: [
64
- /* @__PURE__ */ jsxs("div", { style: S.brandStack, children: [
65
- /* @__PURE__ */ jsx("span", { style: S.brand, children: "Guardrail" }),
66
- !auth.signInUrl && /* @__PURE__ */ jsx("span", { style: { fontSize: 12, color: v("muted") }, children: "No sign-in method is configured for this site." })
60
+ /* @__PURE__ */ jsx("div", { "data-guardrail": true, className: "gr-banner", children: /* @__PURE__ */ jsxs("div", { className: "gr-banner-form", children: [
61
+ /* @__PURE__ */ jsxs("div", { className: "gr-brand-stack", children: [
62
+ /* @__PURE__ */ jsx("span", { className: "gr-brand", children: "Guardrail" }),
63
+ !auth.signInUrl && /* @__PURE__ */ jsx("span", { className: "gr-fs-12 gr-text-muted", children: "No sign-in method is configured for this site." })
67
64
  ] }),
68
- auth.signInUrl && /* @__PURE__ */ jsx("a", { href: auth.signInUrl, style: { ...S.primaryBtn, textDecoration: "none" }, children: "Sign in" }),
65
+ auth.signInUrl && /* @__PURE__ */ jsx("a", { href: auth.signInUrl, className: "gr-primary-btn gr-no-underline", children: "Sign in" }),
69
66
  /* @__PURE__ */ jsx(
70
67
  "button",
71
68
  {
72
69
  type: "button",
73
- style: { ...S.iconBtn, color: v("muted") },
70
+ className: "gr-icon-btn gr-text-muted",
74
71
  onClick: exitEditMode,
75
72
  title: "Not editing? Dismiss",
76
73
  "aria-label": "Dismiss",
@@ -82,10 +79,10 @@ function AdminBody({ toolbarExtra }) {
82
79
  ] });
83
80
  }
84
81
  return /* @__PURE__ */ jsxs(Fragment, { children: [
85
- /* @__PURE__ */ jsx("div", { "data-guardrail": true, style: S.banner, children: /* @__PURE__ */ jsxs("form", { action: formAction, style: S.bannerForm, children: [
86
- /* @__PURE__ */ jsxs("div", { style: S.brandStack, children: [
87
- /* @__PURE__ */ jsx("span", { style: S.brand, children: "Guardrail" }),
88
- formState.error && /* @__PURE__ */ jsx("span", { style: S.error, children: formState.error })
82
+ /* @__PURE__ */ jsx("div", { "data-guardrail": true, className: "gr-banner", children: /* @__PURE__ */ jsxs("form", { action: formAction, className: "gr-banner-form", children: [
83
+ /* @__PURE__ */ jsxs("div", { className: "gr-brand-stack", children: [
84
+ /* @__PURE__ */ jsx("span", { className: "gr-brand", children: "Guardrail" }),
85
+ formState.error && /* @__PURE__ */ jsx("span", { className: "gr-error", children: formState.error })
89
86
  ] }),
90
87
  /* @__PURE__ */ jsx(
91
88
  "input",
@@ -96,7 +93,7 @@ function AdminBody({ toolbarExtra }) {
96
93
  placeholder: "Email",
97
94
  defaultValue: formState.email,
98
95
  required: true,
99
- style: S.input
96
+ className: "gr-input"
100
97
  }
101
98
  ),
102
99
  /* @__PURE__ */ jsx(
@@ -107,25 +104,16 @@ function AdminBody({ toolbarExtra }) {
107
104
  autoComplete: "current-password",
108
105
  placeholder: "Password",
109
106
  required: true,
110
- style: S.input
107
+ className: "gr-input"
111
108
  }
112
109
  ),
113
- /* @__PURE__ */ jsx("button", { type: "submit", disabled: isPending, style: S.primaryBtn, children: isPending ? "Authenticating\u2026" : "Sign In" }),
110
+ /* @__PURE__ */ jsx("button", { type: "submit", disabled: isPending, className: "gr-primary-btn", children: isPending ? "Authenticating\u2026" : "Sign In" }),
114
111
  /* @__PURE__ */ jsx(
115
112
  "button",
116
113
  {
117
114
  type: "button",
118
115
  onClick: () => setForgotOpen(true),
119
- style: {
120
- background: "none",
121
- border: "none",
122
- cursor: "pointer",
123
- color: v("muted"),
124
- fontSize: 12,
125
- textDecoration: "underline",
126
- padding: "0 4px",
127
- whiteSpace: "nowrap"
128
- },
116
+ className: "gr-text-btn",
129
117
  children: "Forgot password?"
130
118
  }
131
119
  ),
@@ -133,7 +121,7 @@ function AdminBody({ toolbarExtra }) {
133
121
  "button",
134
122
  {
135
123
  type: "button",
136
- style: { ...S.iconBtn, color: v("muted") },
124
+ className: "gr-icon-btn gr-text-muted",
137
125
  onClick: exitEditMode,
138
126
  title: "Not editing? Dismiss",
139
127
  "aria-label": "Dismiss",
@@ -147,13 +135,13 @@ function AdminBody({ toolbarExtra }) {
147
135
  if (needsPassword) return /* @__PURE__ */ jsx(AuthDrawer, { mode: "set-password" });
148
136
  if (!role) {
149
137
  return /* @__PURE__ */ jsxs(Fragment, { children: [
150
- /* @__PURE__ */ jsxs("div", { "data-guardrail": true, style: S.toolbar, children: [
151
- /* @__PURE__ */ jsx("span", { style: S.toolbarLabel, children: "No access to this site" }),
152
- /* @__PURE__ */ jsx("span", { style: S.divider }),
138
+ /* @__PURE__ */ jsxs("div", { "data-guardrail": true, className: "gr-toolbar", children: [
139
+ /* @__PURE__ */ jsx("span", { className: "gr-toolbar-label", children: "No access to this site" }),
140
+ /* @__PURE__ */ jsx("span", { className: "gr-divider" }),
153
141
  /* @__PURE__ */ jsx(
154
142
  "button",
155
143
  {
156
- style: { ...S.iconBtn, color: v("muted") },
144
+ className: "gr-icon-btn gr-text-muted",
157
145
  onClick: signOut,
158
146
  title: "Sign out",
159
147
  "aria-label": "Sign out",
@@ -167,14 +155,14 @@ function AdminBody({ toolbarExtra }) {
167
155
  return /* @__PURE__ */ jsxs(Fragment, { children: [
168
156
  linkErrorDrawer,
169
157
  teamOpen && /* @__PURE__ */ jsx(TeamDrawer, { onClose: () => setTeamOpen(false) }),
170
- /* @__PURE__ */ jsxs("div", { "data-guardrail": true, style: S.toolbar, children: [
171
- /* @__PURE__ */ jsx("span", { style: S.dot }),
172
- /* @__PURE__ */ jsx("span", { style: S.toolbarLabel, children: "Edit Mode" }),
173
- /* @__PURE__ */ jsx("span", { style: S.divider }),
158
+ /* @__PURE__ */ jsxs("div", { "data-guardrail": true, className: "gr-toolbar", children: [
159
+ /* @__PURE__ */ jsx("span", { className: "gr-dot" }),
160
+ /* @__PURE__ */ jsx("span", { className: "gr-toolbar-label", children: "Edit Mode" }),
161
+ /* @__PURE__ */ jsx("span", { className: "gr-divider" }),
174
162
  /* @__PURE__ */ jsx(
175
163
  "button",
176
164
  {
177
- style: { ...S.iconBtn, color: v("muted"), width: 24, height: 24 },
165
+ className: "gr-icon-btn gr-text-muted gr-size-24",
178
166
  onClick: () => setTeamOpen(true),
179
167
  title: "Team",
180
168
  "aria-label": "Team",
@@ -182,14 +170,14 @@ function AdminBody({ toolbarExtra }) {
182
170
  }
183
171
  ),
184
172
  toolbarExtra && /* @__PURE__ */ jsxs(Fragment, { children: [
185
- /* @__PURE__ */ jsx("span", { style: S.divider }),
173
+ /* @__PURE__ */ jsx("span", { className: "gr-divider" }),
186
174
  toolbarExtra
187
175
  ] }),
188
- /* @__PURE__ */ jsx("span", { style: S.divider }),
176
+ /* @__PURE__ */ jsx("span", { className: "gr-divider" }),
189
177
  /* @__PURE__ */ jsx(
190
178
  "button",
191
179
  {
192
- style: { ...S.iconBtn, color: v("muted") },
180
+ className: "gr-icon-btn gr-text-muted",
193
181
  onClick: exitEditMode,
194
182
  title: "Exit edit mode",
195
183
  "aria-label": "Exit edit mode",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/react/Admin.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useActionState, useEffect, useState } from \"react\";\nimport { usePathname, useRouter } from \"next/navigation\";\nimport { loginAction, logoutAction } from \"../server/actions\";\nimport { useGRail } from \"./Provider\";\nimport { TeamDrawer } from \"./Team\";\nimport { AuthDrawer } from \"./AuthDrawer\";\nimport { CloseIcon, LogOutIcon, PeopleIcon } from \"./Icons\";\nimport { S, THEME_CSS, v } from \"./styles\";\nimport { GR_COOKIES, GR_EDIT_MODE_OFF_TTL_SECONDS } from \"../core/defs\";\nimport type { ActionState } from \"../core/types\";\n\nconst initialState: ActionState = { email: \"\", password: \"\" };\n\n/**\n * Owns the theme stylesheet so every piece of chrome — the sign-in bar, the\n * toolbar, and the field popups portaled to <body> — picks up the same\n * light/dark variables from one place.\n */\nexport function GRailAdmin(props: { toolbarExtra?: React.ReactNode }) {\n const { isEditMode } = useGRail();\n if (!isEditMode) return null;\n return (\n <>\n {/*\n precedence + href make React hoist this into <head> and dedupe it.\n Required, not cosmetic: this whole component renders as a sibling of\n <html> (withGuardrail can't reach inside the app's own layout), and\n React 19 rejects a bare <style> anywhere outside the main document.\n */}\n <style href=\"guardrail-theme\" precedence=\"default\">\n {THEME_CSS}\n </style>\n <AdminBody {...props} />\n </>\n );\n}\n\nfunction AdminBody({ toolbarExtra }: { toolbarExtra?: React.ReactNode }) {\n const { isAuth, isEditMode, needsPassword, authError, role, auth } = useGRail();\n const [teamOpen, setTeamOpen] = useState(false);\n const [forgotOpen, setForgotOpen] = useState(false);\n const [errorDismissed, setErrorDismissed] = useState(false);\n const [formState, formAction, isPending] = useActionState(\n loginAction,\n initialState,\n );\n const router = useRouter();\n const pathname = usePathname();\n\n useEffect(() => {\n if (formState.success) router.refresh();\n }, [formState.success, router]);\n\n // The message for an emailed link that didn't work. The cookie behind it is\n // read on the server, so dismissing clears it there too (and refreshes), but\n // hides it right away without waiting for that.\n const dismissAuthError = () => {\n document.cookie = `${GR_COOKIES.AUTH_ERROR}=; path=/; max-age=0; SameSite=Lax`;\n setErrorDismissed(true);\n router.refresh();\n };\n const linkErrorDrawer =\n authError && !errorDismissed ? (\n <AuthDrawer\n mode=\"forgot\"\n title=\"That link didn't work\"\n message={\n authError === \"expired\"\n ? \"It has expired or was already used. Links work once. Enter your email to get a new one.\"\n : \"We couldn't complete it. Enter your email to get a new link.\"\n }\n onClose={dismissAuthError}\n />\n ) : null;\n\n // Ends the editing session, not the sign-in: the overlay goes away and the\n // person's session stays, so ?edit=true brings it straight back with no\n // credentials. The sign-in form only returns once the session itself is\n // gone (expired, or ended elsewhere). Explicit \"false\" rather than deleting\n // the cookie: with a live session, deleting it would leave edit mode on —\n // see resolveEditMode. router.replace drops ?edit=true from the URL (a bare\n // refresh would leave it there), and refresh re-renders without chrome.\n //\n // Also what the sign-in bar's dismiss button does, for someone who was never\n // authenticated and doesn't want to be here at all.\n const exitEditMode = () => {\n document.cookie = `${GR_COOKIES.EDIT_MODE}=false; path=/; max-age=${GR_EDIT_MODE_OFF_TTL_SECONDS}; SameSite=Lax`;\n router.replace(pathname);\n router.refresh();\n };\n\n // A real sign-out, kept only for the \"no access\" bar below: someone on the\n // wrong account has no other way to switch to the right one. logoutAction()\n // clears the edit-mode cookie itself — a client-side navigation right after\n // it used to race Next's own post-action route refresh and lose.\n const signOut = async () => {\n await logoutAction();\n router.replace(pathname);\n router.refresh();\n };\n\n // No intent signalled — render nothing at all.\n if (!isEditMode) return null;\n\n if (!isAuth) {\n // The adapter cannot take a password here, so hand sign-in off rather than\n // showing a form that could never succeed.\n if (!auth.password) {\n return (\n <>\n <div data-guardrail style={S.banner}>\n <div style={S.bannerForm}>\n <div style={S.brandStack}>\n <span style={S.brand}>Guardrail</span>\n {!auth.signInUrl && (\n <span style={{ fontSize: 12, color: v(\"muted\") }}>\n No sign-in method is configured for this site.\n </span>\n )}\n </div>\n {auth.signInUrl && (\n <a href={auth.signInUrl} style={{ ...S.primaryBtn, textDecoration: \"none\" }}>\n Sign in\n </a>\n )}\n <button\n type=\"button\"\n style={{ ...S.iconBtn, color: v(\"muted\") }}\n onClick={exitEditMode}\n title=\"Not editing? Dismiss\"\n aria-label=\"Dismiss\"\n >\n <CloseIcon />\n </button>\n </div>\n </div>\n {linkErrorDrawer}\n </>\n );\n }\n\n return (\n <>\n <div data-guardrail style={S.banner}>\n <form action={formAction} style={S.bannerForm}>\n <div style={S.brandStack}>\n <span style={S.brand}>Guardrail</span>\n {formState.error && <span style={S.error}>{formState.error}</span>}\n </div>\n <input\n name=\"email\"\n type=\"email\"\n autoComplete=\"email\"\n placeholder=\"Email\"\n defaultValue={formState.email}\n required\n style={S.input}\n />\n <input\n name=\"password\"\n type=\"password\"\n autoComplete=\"current-password\"\n placeholder=\"Password\"\n required\n style={S.input}\n />\n <button type=\"submit\" disabled={isPending} style={S.primaryBtn}>\n {isPending ? \"Authenticating…\" : \"Sign In\"}\n </button>\n <button\n type=\"button\"\n onClick={() => setForgotOpen(true)}\n style={{\n background: \"none\",\n border: \"none\",\n cursor: \"pointer\",\n color: v(\"muted\"),\n fontSize: 12,\n textDecoration: \"underline\",\n padding: \"0 4px\",\n whiteSpace: \"nowrap\",\n }}\n >\n Forgot password?\n </button>\n <button\n type=\"button\"\n style={{ ...S.iconBtn, color: v(\"muted\") }}\n onClick={exitEditMode}\n title=\"Not editing? Dismiss\"\n aria-label=\"Dismiss\"\n >\n <CloseIcon />\n </button>\n </form>\n </div>\n {linkErrorDrawer ??\n (forgotOpen && <AuthDrawer mode=\"forgot\" onClose={() => setForgotOpen(false)} />)}\n </>\n );\n }\n\n // An invite or password-reset link just completed: they're signed in, but must\n // choose a password before anything else. Ahead of the role check below —\n // getCachedViewer() already claims a pending invite as a side effect of\n // resolving them, so the role is normally set by now; this is purely about the\n // password. Not skippable: a password is the only way back in once this\n // session ends, so the overlay and toolbar wait until one exists.\n if (needsPassword) return <AuthDrawer mode=\"set-password\" />;\n\n // Signed in, but holds no role on this site.\n if (!role) {\n return (\n <>\n <div data-guardrail style={S.toolbar}>\n <span style={S.toolbarLabel}>No access to this site</span>\n <span style={S.divider} />\n <button\n style={{ ...S.iconBtn, color: v(\"muted\") }}\n onClick={signOut}\n title=\"Sign out\"\n aria-label=\"Sign out\"\n >\n <LogOutIcon />\n </button>\n </div>\n {linkErrorDrawer}\n </>\n );\n }\n\n return (\n <>\n {linkErrorDrawer}\n {teamOpen && <TeamDrawer onClose={() => setTeamOpen(false)} />}\n <div data-guardrail style={S.toolbar}>\n <span style={S.dot} />\n <span style={S.toolbarLabel}>Edit Mode</span>\n <span style={S.divider} />\n <button\n style={{ ...S.iconBtn, color: v(\"muted\"), width: 24, height: 24 }}\n onClick={() => setTeamOpen(true)}\n title=\"Team\"\n aria-label=\"Team\"\n >\n <PeopleIcon />\n </button>\n\n {toolbarExtra && (\n <>\n <span style={S.divider} />\n {toolbarExtra}\n </>\n )}\n\n <span style={S.divider} />\n <button\n style={{ ...S.iconBtn, color: v(\"muted\") }}\n onClick={exitEditMode}\n title=\"Exit edit mode\"\n aria-label=\"Exit edit mode\"\n >\n <LogOutIcon />\n </button>\n </div>\n </>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/react/Admin.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useActionState, useEffect, useState } from \"react\";\nimport { usePathname, useRouter } from \"next/navigation\";\nimport { loginAction, logoutAction } from \"../server/actions\";\nimport { useGRail } from \"./Provider\";\nimport { TeamDrawer } from \"./Team\";\nimport { AuthDrawer } from \"./AuthDrawer\";\nimport { CloseIcon, LogOutIcon, PeopleIcon } from \"./Icons\";\nimport \"./guardrail.css\";\nimport { GR_COOKIES, GR_EDIT_MODE_OFF_TTL_SECONDS } from \"../core/defs\";\nimport type { ActionState } from \"../core/types\";\n\nconst initialState: ActionState = { email: \"\", password: \"\" };\n\n/**\n * Imports the chrome's stylesheet (guardrail.css), which also defines the\n * light/dark theme variables, so every piece of chrome — the sign-in bar, the\n * toolbar, and the field popups portaled to <body> — picks them up from one\n * place. A stylesheet rather than inline styles so the chrome works under a\n * strict Content-Security-Policy.\n */\nexport function GRailAdmin(props: { toolbarExtra?: React.ReactNode }) {\n const { isEditMode } = useGRail();\n if (!isEditMode) return null;\n return <AdminBody {...props} />;\n}\n\nfunction AdminBody({ toolbarExtra }: { toolbarExtra?: React.ReactNode }) {\n const { isAuth, isEditMode, needsPassword, authError, role, auth } = useGRail();\n const [teamOpen, setTeamOpen] = useState(false);\n const [forgotOpen, setForgotOpen] = useState(false);\n const [errorDismissed, setErrorDismissed] = useState(false);\n const [formState, formAction, isPending] = useActionState(\n loginAction,\n initialState,\n );\n const router = useRouter();\n const pathname = usePathname();\n\n useEffect(() => {\n if (formState.success) router.refresh();\n }, [formState.success, router]);\n\n // The message for an emailed link that didn't work. The cookie behind it is\n // read on the server, so dismissing clears it there too (and refreshes), but\n // hides it right away without waiting for that.\n const dismissAuthError = () => {\n document.cookie = `${GR_COOKIES.AUTH_ERROR}=; path=/; max-age=0; SameSite=Lax`;\n setErrorDismissed(true);\n router.refresh();\n };\n const linkErrorDrawer =\n authError && !errorDismissed ? (\n <AuthDrawer\n mode=\"forgot\"\n title=\"That link didn't work\"\n message={\n authError === \"expired\"\n ? \"It has expired or was already used. Links work once. Enter your email to get a new one.\"\n : \"We couldn't complete it. Enter your email to get a new link.\"\n }\n onClose={dismissAuthError}\n />\n ) : null;\n\n // Ends the editing session, not the sign-in: the overlay goes away and the\n // person's session stays, so ?edit=true brings it straight back with no\n // credentials. The sign-in form only returns once the session itself is\n // gone (expired, or ended elsewhere). Explicit \"false\" rather than deleting\n // the cookie: with a live session, deleting it would leave edit mode on —\n // see resolveEditMode. router.replace drops ?edit=true from the URL (a bare\n // refresh would leave it there), and refresh re-renders without chrome.\n //\n // Also what the sign-in bar's dismiss button does, for someone who was never\n // authenticated and doesn't want to be here at all.\n const exitEditMode = () => {\n document.cookie = `${GR_COOKIES.EDIT_MODE}=false; path=/; max-age=${GR_EDIT_MODE_OFF_TTL_SECONDS}; SameSite=Lax`;\n router.replace(pathname);\n router.refresh();\n };\n\n // A real sign-out, kept only for the \"no access\" bar below: someone on the\n // wrong account has no other way to switch to the right one. logoutAction()\n // clears the edit-mode cookie itself — a client-side navigation right after\n // it used to race Next's own post-action route refresh and lose.\n const signOut = async () => {\n await logoutAction();\n router.replace(pathname);\n router.refresh();\n };\n\n // No intent signalled — render nothing at all.\n if (!isEditMode) return null;\n\n if (!isAuth) {\n // The adapter cannot take a password here, so hand sign-in off rather than\n // showing a form that could never succeed.\n if (!auth.password) {\n return (\n <>\n <div data-guardrail className=\"gr-banner\">\n <div className=\"gr-banner-form\">\n <div className=\"gr-brand-stack\">\n <span className=\"gr-brand\">Guardrail</span>\n {!auth.signInUrl && (\n <span className=\"gr-fs-12 gr-text-muted\">\n No sign-in method is configured for this site.\n </span>\n )}\n </div>\n {auth.signInUrl && (\n <a href={auth.signInUrl} className=\"gr-primary-btn gr-no-underline\">\n Sign in\n </a>\n )}\n <button\n type=\"button\"\n className=\"gr-icon-btn gr-text-muted\"\n onClick={exitEditMode}\n title=\"Not editing? Dismiss\"\n aria-label=\"Dismiss\"\n >\n <CloseIcon />\n </button>\n </div>\n </div>\n {linkErrorDrawer}\n </>\n );\n }\n\n return (\n <>\n <div data-guardrail className=\"gr-banner\">\n <form action={formAction} className=\"gr-banner-form\">\n <div className=\"gr-brand-stack\">\n <span className=\"gr-brand\">Guardrail</span>\n {formState.error && <span className=\"gr-error\">{formState.error}</span>}\n </div>\n <input\n name=\"email\"\n type=\"email\"\n autoComplete=\"email\"\n placeholder=\"Email\"\n defaultValue={formState.email}\n required\n className=\"gr-input\"\n />\n <input\n name=\"password\"\n type=\"password\"\n autoComplete=\"current-password\"\n placeholder=\"Password\"\n required\n className=\"gr-input\"\n />\n <button type=\"submit\" disabled={isPending} className=\"gr-primary-btn\">\n {isPending ? \"Authenticating…\" : \"Sign In\"}\n </button>\n <button\n type=\"button\"\n onClick={() => setForgotOpen(true)}\n className=\"gr-text-btn\"\n >\n Forgot password?\n </button>\n <button\n type=\"button\"\n className=\"gr-icon-btn gr-text-muted\"\n onClick={exitEditMode}\n title=\"Not editing? Dismiss\"\n aria-label=\"Dismiss\"\n >\n <CloseIcon />\n </button>\n </form>\n </div>\n {linkErrorDrawer ??\n (forgotOpen && <AuthDrawer mode=\"forgot\" onClose={() => setForgotOpen(false)} />)}\n </>\n );\n }\n\n // An invite or password-reset link just completed: they're signed in, but must\n // choose a password before anything else. Ahead of the role check below —\n // getCachedViewer() already claims a pending invite as a side effect of\n // resolving them, so the role is normally set by now; this is purely about the\n // password. Not skippable: a password is the only way back in once this\n // session ends, so the overlay and toolbar wait until one exists.\n if (needsPassword) return <AuthDrawer mode=\"set-password\" />;\n\n // Signed in, but holds no role on this site.\n if (!role) {\n return (\n <>\n <div data-guardrail className=\"gr-toolbar\">\n <span className=\"gr-toolbar-label\">No access to this site</span>\n <span className=\"gr-divider\" />\n <button\n className=\"gr-icon-btn gr-text-muted\"\n onClick={signOut}\n title=\"Sign out\"\n aria-label=\"Sign out\"\n >\n <LogOutIcon />\n </button>\n </div>\n {linkErrorDrawer}\n </>\n );\n }\n\n return (\n <>\n {linkErrorDrawer}\n {teamOpen && <TeamDrawer onClose={() => setTeamOpen(false)} />}\n <div data-guardrail className=\"gr-toolbar\">\n <span className=\"gr-dot\" />\n <span className=\"gr-toolbar-label\">Edit Mode</span>\n <span className=\"gr-divider\" />\n <button\n className=\"gr-icon-btn gr-text-muted gr-size-24\"\n onClick={() => setTeamOpen(true)}\n title=\"Team\"\n aria-label=\"Team\"\n >\n <PeopleIcon />\n </button>\n\n {toolbarExtra && (\n <>\n <span className=\"gr-divider\" />\n {toolbarExtra}\n </>\n )}\n\n <span className=\"gr-divider\" />\n <button\n className=\"gr-icon-btn gr-text-muted\"\n onClick={exitEditMode}\n title=\"Exit edit mode\"\n aria-label=\"Exit edit mode\"\n >\n <LogOutIcon />\n </button>\n </div>\n </>\n );\n}\n"],"mappings":"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","names":[]}