@guard-rail/core 0.9.0 → 0.10.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 +182 -38
- package/dist/adapters/supabase/auth.js +1 -2
- package/dist/adapters/supabase/auth.js.map +1 -1
- package/dist/adapters/supabase/client.js +6 -1
- package/dist/adapters/supabase/client.js.map +1 -1
- package/dist/adapters/supabase/confirm.d.ts +11 -11
- package/dist/adapters/supabase/confirm.js +14 -18
- package/dist/adapters/supabase/confirm.js.map +1 -1
- package/dist/adapters/supabase/index.d.ts +3 -1
- package/dist/adapters/supabase/index.js +5 -0
- package/dist/adapters/supabase/index.js.map +1 -1
- package/dist/adapters/supabase/rateLimit.d.ts +24 -0
- package/dist/adapters/supabase/rateLimit.js +37 -0
- package/dist/adapters/supabase/rateLimit.js.map +1 -0
- package/dist/cli/doctor.d.ts +1 -0
- package/dist/cli/doctor.js +911 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/init-db.js +23 -5
- package/dist/cli/init-db.js.map +1 -1
- package/dist/core/adapter-types.d.ts +71 -1
- package/dist/core/config.d.ts +12 -1
- package/dist/core/config.js +29 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/validate.d.ts +47 -0
- package/dist/core/validate.js +118 -0
- package/dist/core/validate.js.map +1 -0
- package/dist/email/console.d.ts +24 -0
- package/dist/email/console.js +30 -0
- package/dist/email/console.js.map +1 -0
- package/dist/index.client.d.ts +3 -2
- package/dist/index.client.js +5 -0
- package/dist/index.client.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/react/AuthRedirectSync.js +28 -22
- package/dist/react/AuthRedirectSync.js.map +1 -1
- package/dist/react/GRail/render.js +3 -2
- package/dist/react/GRail/render.js.map +1 -1
- package/dist/server/actions.js +52 -31
- package/dist/server/actions.js.map +1 -1
- package/dist/server/loader.js +4 -4
- package/dist/server/loader.js.map +1 -1
- package/dist/server/rateLimit.d.ts +21 -0
- package/dist/server/rateLimit.js +94 -0
- package/dist/server/rateLimit.js.map +1 -0
- package/dist/server/repeaterActions.js +9 -1
- package/dist/server/repeaterActions.js.map +1 -1
- package/dist/server/wrapper.js +18 -1
- package/dist/server/wrapper.js.map +1 -1
- package/package.json +15 -14
- package/supabase/admin/check-grants.sql +41 -0
- package/supabase/migrations/20261028000000_require_confirmed_email.sql +53 -0
- package/supabase/migrations/20261028010000_rate_limits.sql +67 -0
- package/supabase/migrations/20261028020000_member_and_bucket_guards.sql +96 -0
- 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":[]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { GRInviteEvent, GRPasswordResetEvent } from '../core/adapter-types.js';
|
|
2
|
+
import 'react';
|
|
3
|
+
import '../core/types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A stand-in for resendEmails() while you're developing: nothing is sent, and
|
|
7
|
+
* each invite or password-reset link is printed to the server's terminal
|
|
8
|
+
* instead, so the whole flow can be tried locally with no mail provider.
|
|
9
|
+
*
|
|
10
|
+
* ...(process.env.GUARDRAIL_EMAIL_FROM
|
|
11
|
+
* ? resendEmails({ from: process.env.GUARDRAIL_EMAIL_FROM, siteName: "My Site" })
|
|
12
|
+
* : consoleEmails()),
|
|
13
|
+
*
|
|
14
|
+
* Refuses in production, with an error that reaches the admin who sent the
|
|
15
|
+
* invite. A link is as good as a password for choosing one, so it doesn't
|
|
16
|
+
* belong in a production log — a deployed site without email configured should
|
|
17
|
+
* say so rather than quietly writing them there.
|
|
18
|
+
*/
|
|
19
|
+
declare function consoleEmails(): {
|
|
20
|
+
onInvite: (event: GRInviteEvent) => void;
|
|
21
|
+
onPasswordReset: (event: GRPasswordResetEvent) => void;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export { consoleEmails };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
function consoleEmails() {
|
|
2
|
+
const refuseInProduction = () => {
|
|
3
|
+
if (process.env.NODE_ENV === "production") {
|
|
4
|
+
throw new Error(
|
|
5
|
+
`no email provider is configured, so this can't be sent \u2014 set GUARDRAIL_EMAIL_FROM and RESEND_API_KEY (see the README's "Passwords, invites and resets").`
|
|
6
|
+
);
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
return {
|
|
10
|
+
onInvite: (event) => {
|
|
11
|
+
refuseInProduction();
|
|
12
|
+
const what = event.isNewUser === false ? `${event.email} already has an account \u2014 they'd be told they've been added` : `Invite for ${event.email} (${event.role})`;
|
|
13
|
+
console.log(`
|
|
14
|
+
[guardrail] ${what}. Nothing was emailed (development only):
|
|
15
|
+
${event.link ?? "(no link: this auth adapter can't create one)"}
|
|
16
|
+
`);
|
|
17
|
+
},
|
|
18
|
+
onPasswordReset: (event) => {
|
|
19
|
+
refuseInProduction();
|
|
20
|
+
console.log(`
|
|
21
|
+
[guardrail] Password reset for ${event.email}. Nothing was emailed (development only):
|
|
22
|
+
${event.link}
|
|
23
|
+
`);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export {
|
|
28
|
+
consoleEmails
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=console.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/email/console.ts"],"sourcesContent":["import type { GRInviteEvent, GRPasswordResetEvent } from \"../core/adapter-types\";\n\n/**\n * A stand-in for resendEmails() while you're developing: nothing is sent, and\n * each invite or password-reset link is printed to the server's terminal\n * instead, so the whole flow can be tried locally with no mail provider.\n *\n * ...(process.env.GUARDRAIL_EMAIL_FROM\n * ? resendEmails({ from: process.env.GUARDRAIL_EMAIL_FROM, siteName: \"My Site\" })\n * : consoleEmails()),\n *\n * Refuses in production, with an error that reaches the admin who sent the\n * invite. A link is as good as a password for choosing one, so it doesn't\n * belong in a production log — a deployed site without email configured should\n * say so rather than quietly writing them there.\n */\nexport function consoleEmails() {\n const refuseInProduction = () => {\n if (process.env.NODE_ENV === \"production\") {\n throw new Error(\n \"no email provider is configured, so this can't be sent — set GUARDRAIL_EMAIL_FROM and RESEND_API_KEY (see the README's \\\"Passwords, invites and resets\\\").\",\n );\n }\n };\n\n return {\n onInvite: (event: GRInviteEvent): void => {\n refuseInProduction();\n const what =\n event.isNewUser === false\n ? `${event.email} already has an account — they'd be told they've been added`\n : `Invite for ${event.email} (${event.role})`;\n console.log(`\\n[guardrail] ${what}. Nothing was emailed (development only):\\n ${event.link ?? \"(no link: this auth adapter can't create one)\"}\\n`);\n },\n onPasswordReset: (event: GRPasswordResetEvent): void => {\n refuseInProduction();\n console.log(`\\n[guardrail] Password reset for ${event.email}. Nothing was emailed (development only):\\n ${event.link}\\n`);\n },\n };\n}\n"],"mappings":"AAgBO,SAAS,gBAAgB;AAC9B,QAAM,qBAAqB,MAAM;AAC/B,QAAI,QAAQ,IAAI,aAAa,cAAc;AACzC,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL,UAAU,CAAC,UAA+B;AACxC,yBAAmB;AACnB,YAAM,OACJ,MAAM,cAAc,QAChB,GAAG,MAAM,KAAK,qEACd,cAAc,MAAM,KAAK,KAAK,MAAM,IAAI;AAC9C,cAAQ,IAAI;AAAA,cAAiB,IAAI;AAAA,IAAgD,MAAM,QAAQ,+CAA+C;AAAA,CAAI;AAAA,IACpJ;AAAA,IACA,iBAAiB,CAAC,UAAsC;AACtD,yBAAmB;AACnB,cAAQ,IAAI;AAAA,iCAAoC,MAAM,KAAK;AAAA,IAAgD,MAAM,IAAI;AAAA,CAAI;AAAA,IAC3H;AAAA,EACF;AACF;","names":[]}
|
package/dist/index.client.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export { addRepeaterItem, removeRepeaterItem, reorderRepeaterItems } from './ser
|
|
|
5
5
|
export { GRepeaterProps, RepeaterItemProps } from './react/Repeater/index.js';
|
|
6
6
|
export { ResendEmailOptions, ResendInviteContent, ResendInviteOptions, ResendInviteTemplateEvent, ResendResetTemplateEvent } from './email/resend.js';
|
|
7
7
|
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';
|
|
8
|
+
export { AuthRedirectParams, AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRPasswordResetEvent, GRPrefetch, GRRateLimitHit, GRRateLimitName, GRRateLimiter, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget } from './core/adapter-types.js';
|
|
9
9
|
import 'react/jsx-runtime';
|
|
10
10
|
import 'react';
|
|
11
11
|
|
|
@@ -20,5 +20,6 @@ declare const isEditor: (..._args: unknown[]) => never;
|
|
|
20
20
|
declare const canEdit: (..._args: unknown[]) => never;
|
|
21
21
|
declare const resendInvite: (..._args: unknown[]) => never;
|
|
22
22
|
declare const resendEmails: (..._args: unknown[]) => never;
|
|
23
|
+
declare const consoleEmails: (..._args: unknown[]) => never;
|
|
23
24
|
|
|
24
|
-
export { GRepeater, canEdit, configureGuardrail, getFieldValue, getRepeaterItemBySlug, isAdmin, isEditor, listRepeaterItems, resendEmails, resendInvite, withGuardrail };
|
|
25
|
+
export { GRepeater, canEdit, configureGuardrail, consoleEmails, getFieldValue, getRepeaterItemBySlug, isAdmin, isEditor, listRepeaterItems, resendEmails, resendInvite, withGuardrail };
|
package/dist/index.client.js
CHANGED
|
@@ -69,12 +69,17 @@ const resendEmails = serverOnly(
|
|
|
69
69
|
"resendEmails()",
|
|
70
70
|
"Spread it into withGuardrail(), which runs in your root layout."
|
|
71
71
|
);
|
|
72
|
+
const consoleEmails = serverOnly(
|
|
73
|
+
"consoleEmails()",
|
|
74
|
+
"Spread it into withGuardrail(), which runs in your root layout."
|
|
75
|
+
);
|
|
72
76
|
export {
|
|
73
77
|
GRail,
|
|
74
78
|
GRepeater,
|
|
75
79
|
addRepeaterItem,
|
|
76
80
|
canEdit,
|
|
77
81
|
configureGuardrail,
|
|
82
|
+
consoleEmails,
|
|
78
83
|
createAssetUpload,
|
|
79
84
|
forgotPasswordAction,
|
|
80
85
|
getFieldValue,
|
package/dist/index.client.js.map
CHANGED
|
@@ -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);\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;","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\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;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
|
@@ -7,9 +7,10 @@ export { getRepeaterItemBySlug, listRepeaterItems } from './server/repeaterData.
|
|
|
7
7
|
export { getFieldValue } from './server/fieldData.js';
|
|
8
8
|
export { canEdit, isAdmin, isEditor } from './server/access.js';
|
|
9
9
|
export { ResendEmailOptions, ResendInviteContent, ResendInviteOptions, ResendInviteTemplateEvent, ResendResetTemplateEvent, resendEmails, resendInvite } from './email/resend.js';
|
|
10
|
+
export { consoleEmails } from './email/console.js';
|
|
10
11
|
export { addRepeaterItem, removeRepeaterItem, reorderRepeaterItems } from './server/repeaterActions.js';
|
|
11
12
|
export { useGRail } from './react/Provider.js';
|
|
12
13
|
export { ActionState, GRAsset, GRMember, GRRepeaterItem, GRRole, GRState, GRViewer, HTMLKeys, OverrideRow, OverrideType, OverrideValue, PolymorphicProps, SetPasswordState } from './core/types.js';
|
|
13
|
-
export { AuthRedirectParams, AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRPasswordResetEvent, GRPrefetch, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget } from './core/adapter-types.js';
|
|
14
|
+
export { AuthRedirectParams, AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRPasswordResetEvent, GRPrefetch, GRRateLimitHit, GRRateLimitName, GRRateLimiter, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget } from './core/adapter-types.js';
|
|
14
15
|
import 'react/jsx-runtime';
|
|
15
16
|
import 'react';
|
package/dist/index.js
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import { getFieldValue } from "./server/fieldData";
|
|
23
23
|
import { isAdmin, isEditor, canEdit } from "./server/access";
|
|
24
24
|
import { resendInvite, resendEmails } from "./email/resend";
|
|
25
|
+
import { consoleEmails } from "./email/console";
|
|
25
26
|
import {
|
|
26
27
|
addRepeaterItem,
|
|
27
28
|
removeRepeaterItem,
|
|
@@ -34,6 +35,7 @@ export {
|
|
|
34
35
|
addRepeaterItem,
|
|
35
36
|
canEdit,
|
|
36
37
|
configureGuardrail,
|
|
38
|
+
consoleEmails,
|
|
37
39
|
createAssetUpload,
|
|
38
40
|
forgotPasswordAction,
|
|
39
41
|
getFieldValue,
|
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 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;
|
|
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 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;","names":[]}
|
|
@@ -6,29 +6,35 @@ function AuthRedirectSync() {
|
|
|
6
6
|
const searchParams = useSearchParams();
|
|
7
7
|
const handled = useRef(false);
|
|
8
8
|
useEffect(() => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
9
|
+
const check = () => {
|
|
10
|
+
if (handled.current) return;
|
|
11
|
+
const rawHash = window.location.hash.startsWith("#") ? window.location.hash.slice(1) : window.location.hash;
|
|
12
|
+
const hasHashTokens = /(?:^|&)access_token=/.test(rawHash);
|
|
13
|
+
const hashParams = new URLSearchParams(rawHash);
|
|
14
|
+
const code = searchParams.get("code") ?? void 0;
|
|
15
|
+
const tokenHash = hashParams.get("token_hash") ?? searchParams.get("token_hash") ?? void 0;
|
|
16
|
+
const type = hashParams.get("type") ?? searchParams.get("type") ?? void 0;
|
|
17
|
+
if (!hasHashTokens && !code && !tokenHash) return;
|
|
18
|
+
handled.current = true;
|
|
19
|
+
const params = {
|
|
20
|
+
...hasHashTokens ? { hash: rawHash } : {},
|
|
21
|
+
...code ? { code } : {},
|
|
22
|
+
...tokenHash ? { tokenHash } : {},
|
|
23
|
+
...type ? { type } : {}
|
|
24
|
+
};
|
|
25
|
+
completeAuthRedirectAction(params).then((res) => {
|
|
26
|
+
const url = new URL(window.location.href);
|
|
27
|
+
url.hash = "";
|
|
28
|
+
url.searchParams.delete("code");
|
|
29
|
+
url.searchParams.delete("token_hash");
|
|
30
|
+
url.searchParams.delete("type");
|
|
31
|
+
window.history.replaceState(null, "", url.toString());
|
|
32
|
+
window.location.reload();
|
|
33
|
+
});
|
|
22
34
|
};
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
url.searchParams.delete("code");
|
|
27
|
-
url.searchParams.delete("token_hash");
|
|
28
|
-
url.searchParams.delete("type");
|
|
29
|
-
window.history.replaceState(null, "", url.toString());
|
|
30
|
-
window.location.reload();
|
|
31
|
-
});
|
|
35
|
+
check();
|
|
36
|
+
window.addEventListener("hashchange", check);
|
|
37
|
+
return () => window.removeEventListener("hashchange", check);
|
|
32
38
|
}, [searchParams]);
|
|
33
39
|
return null;
|
|
34
40
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/react/AuthRedirectSync.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport { useSearchParams } from \"next/navigation\";\nimport { completeAuthRedirectAction } from \"../server/actions\";\n\n/**\n * Notices a session that arrived via a redirect link — an invite, magic\n * link, or password-recovery email — and hands whatever it found to\n * completeAuthRedirectAction, which defers to the auth adapter's own\n * completeAuthRedirect for the actual exchange. Deliberately generic: this\n * file has no idea what a Supabase token looks like, only that a hash\n * carrying `access_token=` or a `code`/`token_hash` query param might be one.\n *\n * Has to run client-side and unconditionally (not gated behind edit mode,\n * unlike EditSyncer) — an invite link redirects to the site's plain URL, not\n * necessarily one with ?edit=true, and hash-based tokens never reach the\n * server on their own; only the browser ever sees window.location.hash.\n *\n * Mirrors the community-documented pattern for this exact problem, just\n * generalized across whichever redirect style a provider's own email\n * templates happen to produce, rather than requiring one specific route.\n */\nexport function AuthRedirectSync() {\n const searchParams = useSearchParams();\n const handled = useRef(false);\n\n useEffect(() => {\n if (handled.current) return;\n\n
|
|
1
|
+
{"version":3,"sources":["../../src/react/AuthRedirectSync.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport { useSearchParams } from \"next/navigation\";\nimport { completeAuthRedirectAction } from \"../server/actions\";\n\n/**\n * Notices a session that arrived via a redirect link — an invite, magic\n * link, or password-recovery email — and hands whatever it found to\n * completeAuthRedirectAction, which defers to the auth adapter's own\n * completeAuthRedirect for the actual exchange. Deliberately generic: this\n * file has no idea what a Supabase token looks like, only that a hash\n * carrying `access_token=` or a `code`/`token_hash` query param might be one.\n *\n * Has to run client-side and unconditionally (not gated behind edit mode,\n * unlike EditSyncer) — an invite link redirects to the site's plain URL, not\n * necessarily one with ?edit=true, and hash-based tokens never reach the\n * server on their own; only the browser ever sees window.location.hash.\n *\n * Mirrors the community-documented pattern for this exact problem, just\n * generalized across whichever redirect style a provider's own email\n * templates happen to produce, rather than requiring one specific route.\n */\nexport function AuthRedirectSync() {\n const searchParams = useSearchParams();\n const handled = useRef(false);\n\n useEffect(() => {\n const check = () => {\n if (handled.current) return;\n\n const rawHash = window.location.hash.startsWith(\"#\")\n ? window.location.hash.slice(1)\n : window.location.hash;\n // The one thing worth checking before assuming this hash is ours: every\n // normal page load has an empty hash, and this runs on all of them.\n const hasHashTokens = /(?:^|&)access_token=/.test(rawHash);\n\n // Links Guardrail mints carry token_hash/type in the fragment; the query\n // string is still read for links emailed before that change.\n const hashParams = new URLSearchParams(rawHash);\n const code = searchParams.get(\"code\") ?? undefined;\n const tokenHash =\n hashParams.get(\"token_hash\") ?? searchParams.get(\"token_hash\") ?? undefined;\n const type = hashParams.get(\"type\") ?? searchParams.get(\"type\") ?? undefined;\n\n if (!hasHashTokens && !code && !tokenHash) return;\n handled.current = true;\n\n const params = {\n ...(hasHashTokens ? { hash: rawHash } : {}),\n ...(code ? { code } : {}),\n ...(tokenHash ? { tokenHash } : {}),\n ...(type ? { type } : {}),\n };\n\n completeAuthRedirectAction(params).then((res) => {\n // Stripped either way — a token that failed to exchange (already used,\n // expired) has no business lingering in the address bar or history\n // either.\n const url = new URL(window.location.href);\n url.hash = \"\";\n url.searchParams.delete(\"code\");\n url.searchParams.delete(\"token_hash\");\n url.searchParams.delete(\"type\");\n window.history.replaceState(null, \"\", url.toString());\n\n // A full reload, not router.refresh(): the session was just set via\n // cookies a Server Action wrote, and the current page's React tree\n // (including this component) was built from the pre-session render —\n // simplest to just start over. On failure the action set a cookie\n // explaining it, and a reload is what shows that, rather than leaving the\n // person on a page where nothing visibly happened.\n window.location.reload();\n });\n };\n\n check();\n // A link pasted into a tab that already has the site open changes only the\n // fragment, which is not a page load.\n window.addEventListener(\"hashchange\", check);\n return () => window.removeEventListener(\"hashchange\", check);\n }, [searchParams]);\n\n return null;\n}\n"],"mappings":";AAEA,SAAS,WAAW,cAAc;AAClC,SAAS,uBAAuB;AAChC,SAAS,kCAAkC;AAmBpC,SAAS,mBAAmB;AACjC,QAAM,eAAe,gBAAgB;AACrC,QAAM,UAAU,OAAO,KAAK;AAE5B,YAAU,MAAM;AACd,UAAM,QAAQ,MAAM;AAClB,UAAI,QAAQ,QAAS;AAErB,YAAM,UAAU,OAAO,SAAS,KAAK,WAAW,GAAG,IAC/C,OAAO,SAAS,KAAK,MAAM,CAAC,IAC5B,OAAO,SAAS;AAGpB,YAAM,gBAAgB,uBAAuB,KAAK,OAAO;AAIzD,YAAM,aAAa,IAAI,gBAAgB,OAAO;AAC9C,YAAM,OAAO,aAAa,IAAI,MAAM,KAAK;AACzC,YAAM,YACJ,WAAW,IAAI,YAAY,KAAK,aAAa,IAAI,YAAY,KAAK;AACpE,YAAM,OAAO,WAAW,IAAI,MAAM,KAAK,aAAa,IAAI,MAAM,KAAK;AAEnE,UAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC,UAAW;AAC3C,cAAQ,UAAU;AAElB,YAAM,SAAS;AAAA,QACb,GAAI,gBAAgB,EAAE,MAAM,QAAQ,IAAI,CAAC;AAAA,QACzC,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,QACvB,GAAI,YAAY,EAAE,UAAU,IAAI,CAAC;AAAA,QACjC,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,MACzB;AAEA,iCAA2B,MAAM,EAAE,KAAK,CAAC,QAAQ;AAI/C,cAAM,MAAM,IAAI,IAAI,OAAO,SAAS,IAAI;AACxC,YAAI,OAAO;AACX,YAAI,aAAa,OAAO,MAAM;AAC9B,YAAI,aAAa,OAAO,YAAY;AACpC,YAAI,aAAa,OAAO,MAAM;AAC9B,eAAO,QAAQ,aAAa,MAAM,IAAI,IAAI,SAAS,CAAC;AAQpD,eAAO,SAAS,OAAO;AAAA,MACzB,CAAC;AAAA,IACH;AAEA,UAAM;AAGN,WAAO,iBAAiB,cAAc,KAAK;AAC3C,WAAO,MAAM,OAAO,oBAAoB,cAAc,KAAK;AAAA,EAC7D,GAAG,CAAC,YAAY,CAAC;AAEjB,SAAO;AACT;","names":[]}
|
|
@@ -2,6 +2,7 @@ import { jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import React from "react";
|
|
3
3
|
import NextImage from "next/image";
|
|
4
4
|
import NextLink from "next/link";
|
|
5
|
+
import { safeHref, safeImageSrc } from "../../core/validate";
|
|
5
6
|
function intrinsicSize(override, width, height) {
|
|
6
7
|
const { width: nw, height: nh } = override;
|
|
7
8
|
if (nw && nh) {
|
|
@@ -22,7 +23,7 @@ function GRailRender({ as, override, children, ref, ...rest }) {
|
|
|
22
23
|
type: _overrideType,
|
|
23
24
|
...imgRest
|
|
24
25
|
} = rest;
|
|
25
|
-
const src = override.src || propSrc || "";
|
|
26
|
+
const src = safeImageSrc(override.src) || propSrc || "";
|
|
26
27
|
const alt = override.alt ?? propAlt ?? "";
|
|
27
28
|
if (!src) return null;
|
|
28
29
|
const imgRef = ref;
|
|
@@ -53,7 +54,7 @@ function GRailRender({ as, override, children, ref, ...rest }) {
|
|
|
53
54
|
{
|
|
54
55
|
ref,
|
|
55
56
|
...linkRest,
|
|
56
|
-
href: override.href || propHref || "#",
|
|
57
|
+
href: safeHref(override.href) || propHref || "#",
|
|
57
58
|
children: override.text || children
|
|
58
59
|
}
|
|
59
60
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/react/GRail/render.tsx"],"sourcesContent":["import React from \"react\";\nimport NextImage from \"next/image\";\nimport NextLink from \"next/link\";\nimport type { HTMLKeys, OverrideValue } from \"../../core/types\";\n\ninterface RenderProps {\n as?: HTMLKeys;\n override: OverrideValue;\n children?: React.ReactNode;\n /** Forwarded to the rendered element so the editor can measure it. */\n ref?: React.Ref<HTMLElement>;\n [key: string]: unknown;\n}\n\n/**\n * The developer's width/height set the layout; the image's own proportions set\n * the shape. Keeping the developer's width and deriving height from the stored\n * natural size means a differently shaped replacement neither breaks the\n * layout nor gets stretched.\n */\nfunction intrinsicSize(override: OverrideValue, width?: number, height?: number) {\n const { width: nw, height: nh } = override;\n if (nw && nh) {\n if (width) return { width, height: Math.round((width * nh) / nw) };\n if (height) return { width: Math.round((height * nw) / nh), height };\n return { width: nw, height: nh };\n }\n return { width: width || 500, height: height || 500 };\n}\n\n/**\n * Renders a resolved value as plain markup. Shared by the anonymous SSR path\n * and the editor, so what an editor sees matches what ships.\n *\n * Resolved props are applied *after* the spread. The developer's original\n * src/href are still in `rest`, and spreading them last silently masked every\n * saved image and link override.\n */\nexport function GRailRender({ as, override, children, ref, ...rest }: RenderProps) {\n if (override.type === \"image\") {\n const {\n width,\n height,\n fill,\n src: propSrc,\n alt: propAlt,\n type: _overrideType,\n ...imgRest\n } = rest as Record<string, any>;\n const src = override.src || propSrc || \"\";\n const alt = override.alt ?? propAlt ?? \"\";\n if (!src) return null;\n\n const imgRef = ref as React.Ref<HTMLImageElement>;\n if (fill) {\n return <NextImage ref={imgRef} {...imgRest} src={src} alt={alt} fill />;\n }\n\n const size = intrinsicSize(\n override,\n Number(width) || undefined,\n Number(height) || undefined,\n );\n return (\n <NextImage\n ref={imgRef}\n {...imgRest}\n src={src}\n alt={alt}\n width={size.width}\n height={size.height}\n />\n );\n }\n\n if (override.type === \"link\") {\n const { href: propHref, type: _overrideType, ...linkRest } =\n rest as Record<string, any>;\n return (\n <NextLink\n ref={ref as React.Ref<HTMLAnchorElement>}\n {...linkRest}\n href={override.href || propHref || \"#\"}\n >\n {override.text || children}\n </NextLink>\n );\n }\n\n const Component = (as || \"span\") as React.ElementType;\n return (\n <Component ref={ref} {...rest}>\n {override.text || children}\n </Component>\n );\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../../src/react/GRail/render.tsx"],"sourcesContent":["import React from \"react\";\nimport NextImage from \"next/image\";\nimport NextLink from \"next/link\";\nimport type { HTMLKeys, OverrideValue } from \"../../core/types\";\nimport { safeHref, safeImageSrc } from \"../../core/validate\";\n\ninterface RenderProps {\n as?: HTMLKeys;\n override: OverrideValue;\n children?: React.ReactNode;\n /** Forwarded to the rendered element so the editor can measure it. */\n ref?: React.Ref<HTMLElement>;\n [key: string]: unknown;\n}\n\n/**\n * The developer's width/height set the layout; the image's own proportions set\n * the shape. Keeping the developer's width and deriving height from the stored\n * natural size means a differently shaped replacement neither breaks the\n * layout nor gets stretched.\n */\nfunction intrinsicSize(override: OverrideValue, width?: number, height?: number) {\n const { width: nw, height: nh } = override;\n if (nw && nh) {\n if (width) return { width, height: Math.round((width * nh) / nw) };\n if (height) return { width: Math.round((height * nw) / nh), height };\n return { width: nw, height: nh };\n }\n return { width: width || 500, height: height || 500 };\n}\n\n/**\n * Renders a resolved value as plain markup. Shared by the anonymous SSR path\n * and the editor, so what an editor sees matches what ships.\n *\n * Resolved props are applied *after* the spread. The developer's original\n * src/href are still in `rest`, and spreading them last silently masked every\n * saved image and link override.\n */\nexport function GRailRender({ as, override, children, ref, ...rest }: RenderProps) {\n if (override.type === \"image\") {\n const {\n width,\n height,\n fill,\n src: propSrc,\n alt: propAlt,\n type: _overrideType,\n ...imgRest\n } = rest as Record<string, any>;\n // Rows written before values were validated may hold anything; only\n // http(s) and relative addresses are rendered.\n const src = safeImageSrc(override.src) || propSrc || \"\";\n const alt = override.alt ?? propAlt ?? \"\";\n if (!src) return null;\n\n const imgRef = ref as React.Ref<HTMLImageElement>;\n if (fill) {\n return <NextImage ref={imgRef} {...imgRest} src={src} alt={alt} fill />;\n }\n\n const size = intrinsicSize(\n override,\n Number(width) || undefined,\n Number(height) || undefined,\n );\n return (\n <NextImage\n ref={imgRef}\n {...imgRest}\n src={src}\n alt={alt}\n width={size.width}\n height={size.height}\n />\n );\n }\n\n if (override.type === \"link\") {\n const { href: propHref, type: _overrideType, ...linkRest } =\n rest as Record<string, any>;\n return (\n <NextLink\n ref={ref as React.Ref<HTMLAnchorElement>}\n {...linkRest}\n href={safeHref(override.href) || propHref || \"#\"}\n >\n {override.text || children}\n </NextLink>\n );\n }\n\n const Component = (as || \"span\") as React.ElementType;\n return (\n <Component ref={ref} {...rest}>\n {override.text || children}\n </Component>\n );\n}\n"],"mappings":"AA0Da;AA1Db,OAAO,WAAW;AAClB,OAAO,eAAe;AACtB,OAAO,cAAc;AAErB,SAAS,UAAU,oBAAoB;AAiBvC,SAAS,cAAc,UAAyB,OAAgB,QAAiB;AAC/E,QAAM,EAAE,OAAO,IAAI,QAAQ,GAAG,IAAI;AAClC,MAAI,MAAM,IAAI;AACZ,QAAI,MAAO,QAAO,EAAE,OAAO,QAAQ,KAAK,MAAO,QAAQ,KAAM,EAAE,EAAE;AACjE,QAAI,OAAQ,QAAO,EAAE,OAAO,KAAK,MAAO,SAAS,KAAM,EAAE,GAAG,OAAO;AACnE,WAAO,EAAE,OAAO,IAAI,QAAQ,GAAG;AAAA,EACjC;AACA,SAAO,EAAE,OAAO,SAAS,KAAK,QAAQ,UAAU,IAAI;AACtD;AAUO,SAAS,YAAY,EAAE,IAAI,UAAU,UAAU,KAAK,GAAG,KAAK,GAAgB;AACjF,MAAI,SAAS,SAAS,SAAS;AAC7B,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,MACA,KAAK;AAAA,MACL,KAAK;AAAA,MACL,MAAM;AAAA,MACN,GAAG;AAAA,IACL,IAAI;AAGJ,UAAM,MAAM,aAAa,SAAS,GAAG,KAAK,WAAW;AACrD,UAAM,MAAM,SAAS,OAAO,WAAW;AACvC,QAAI,CAAC,IAAK,QAAO;AAEjB,UAAM,SAAS;AACf,QAAI,MAAM;AACR,aAAO,oBAAC,aAAU,KAAK,QAAS,GAAG,SAAS,KAAU,KAAU,MAAI,MAAC;AAAA,IACvE;AAEA,UAAM,OAAO;AAAA,MACX;AAAA,MACA,OAAO,KAAK,KAAK;AAAA,MACjB,OAAO,MAAM,KAAK;AAAA,IACpB;AACA,WACE;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACJ,GAAG;AAAA,QACJ;AAAA,QACA;AAAA,QACA,OAAO,KAAK;AAAA,QACZ,QAAQ,KAAK;AAAA;AAAA,IACf;AAAA,EAEJ;AAEA,MAAI,SAAS,SAAS,QAAQ;AAC5B,UAAM,EAAE,MAAM,UAAU,MAAM,eAAe,GAAG,SAAS,IACvD;AACF,WACE;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACC,GAAG;AAAA,QACJ,MAAM,SAAS,SAAS,IAAI,KAAK,YAAY;AAAA,QAE5C,mBAAS,QAAQ;AAAA;AAAA,IACpB;AAAA,EAEJ;AAEA,QAAM,YAAa,MAAM;AACzB,SACE,oBAAC,aAAU,KAAW,GAAG,MACtB,mBAAS,QAAQ,UACpB;AAEJ;","names":[]}
|