@guard-rail/core 0.8.0 → 0.9.1

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 (51) hide show
  1. package/README.md +76 -104
  2. package/dist/adapters/supabase/auth.d.ts +9 -15
  3. package/dist/adapters/supabase/auth.js +33 -42
  4. package/dist/adapters/supabase/auth.js.map +1 -1
  5. package/dist/adapters/supabase/confirm.js +2 -2
  6. package/dist/adapters/supabase/confirm.js.map +1 -1
  7. package/dist/adapters/supabase/members.js +13 -2
  8. package/dist/adapters/supabase/members.js.map +1 -1
  9. package/dist/core/adapter-types.d.ts +60 -28
  10. package/dist/core/config.d.ts +2 -1
  11. package/dist/core/config.js +4 -0
  12. package/dist/core/config.js.map +1 -1
  13. package/dist/core/defs.d.ts +11 -1
  14. package/dist/core/defs.js +11 -1
  15. package/dist/core/defs.js.map +1 -1
  16. package/dist/core/types.d.ts +6 -0
  17. package/dist/email/console.d.ts +24 -0
  18. package/dist/email/console.js +30 -0
  19. package/dist/email/console.js.map +1 -0
  20. package/dist/email/resend.d.ts +39 -10
  21. package/dist/email/resend.js +92 -49
  22. package/dist/email/resend.js.map +1 -1
  23. package/dist/index.client.d.ts +6 -4
  24. package/dist/index.client.js +14 -0
  25. package/dist/index.client.js.map +1 -1
  26. package/dist/index.d.ts +4 -3
  27. package/dist/index.js +8 -1
  28. package/dist/index.js.map +1 -1
  29. package/dist/react/Admin.js +105 -107
  30. package/dist/react/Admin.js.map +1 -1
  31. package/dist/react/AuthDrawer.d.ts +23 -0
  32. package/dist/react/AuthDrawer.js +165 -0
  33. package/dist/react/AuthDrawer.js.map +1 -0
  34. package/dist/react/AuthRedirectSync.js +1 -1
  35. package/dist/react/AuthRedirectSync.js.map +1 -1
  36. package/dist/react/Icons.d.ts +2 -1
  37. package/dist/react/Icons.js +2 -0
  38. package/dist/react/Icons.js.map +1 -1
  39. package/dist/react/Team.d.ts +7 -1
  40. package/dist/react/Team.js +23 -3
  41. package/dist/react/Team.js.map +1 -1
  42. package/dist/server/access.d.ts +3 -1
  43. package/dist/server/access.js +5 -3
  44. package/dist/server/access.js.map +1 -1
  45. package/dist/server/actions.d.ts +25 -4
  46. package/dist/server/actions.js +115 -39
  47. package/dist/server/actions.js.map +1 -1
  48. package/dist/server/wrapper.js +2 -1
  49. package/dist/server/wrapper.js.map +1 -1
  50. package/package.json +9 -11
  51. package/scripts/postinstall.mjs +2 -2
@@ -0,0 +1,165 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import React, { useActionState, useEffect, useState, useTransition } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { forgotPasswordAction, logoutAction, setPasswordAction } from "../server/actions";
6
+ import { CloseIcon } from "./Icons";
7
+ import { S, v } from "./styles";
8
+ import { ANIMATION_CSS } from "./Team";
9
+ const initialPasswordState = {};
10
+ function AuthDrawer({
11
+ mode,
12
+ title,
13
+ message,
14
+ onClose
15
+ }) {
16
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
17
+ /* @__PURE__ */ jsx("style", { href: "guardrail-team-animation", precedence: "default", children: ANIMATION_CSS }),
18
+ /* @__PURE__ */ jsx(
19
+ "div",
20
+ {
21
+ "data-guardrail": true,
22
+ "data-guardrail-scrim": true,
23
+ style: S.scrim,
24
+ onClick: mode === "forgot" ? onClose : void 0
25
+ }
26
+ ),
27
+ /* @__PURE__ */ jsxs(
28
+ "aside",
29
+ {
30
+ "data-guardrail": true,
31
+ "data-guardrail-team": true,
32
+ style: S.drawer,
33
+ role: "dialog",
34
+ "aria-label": mode === "set-password" ? "Set your password" : "Forgot password",
35
+ children: [
36
+ /* @__PURE__ */ jsxs("header", { style: S.drawerHead, children: [
37
+ /* @__PURE__ */ jsx("h2", { style: S.drawerTitle, children: title ?? (mode === "set-password" ? "Set your password" : "Forgot your password?") }),
38
+ mode === "forgot" && onClose && /* @__PURE__ */ jsx("button", { style: { ...S.iconBtn, color: v("muted") }, onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsx(CloseIcon, {}) })
39
+ ] }),
40
+ /* @__PURE__ */ jsxs("div", { style: S.drawerBody, children: [
41
+ message && /* @__PURE__ */ jsx("p", { style: { fontSize: 13, lineHeight: 1.5, margin: "0 0 16px", color: v("fg") }, children: message }),
42
+ mode === "set-password" ? /* @__PURE__ */ jsx(SetPasswordForm, {}) : /* @__PURE__ */ jsx(ForgotPasswordForm, {})
43
+ ] })
44
+ ]
45
+ }
46
+ )
47
+ ] });
48
+ }
49
+ function SetPasswordForm() {
50
+ const router = useRouter();
51
+ const [state, formAction, isPending] = useActionState(setPasswordAction, initialPasswordState);
52
+ const [signingOut, startSignOut] = useTransition();
53
+ useEffect(() => {
54
+ if (state.success) router.refresh();
55
+ }, [state.success, router]);
56
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
57
+ /* @__PURE__ */ jsx("p", { style: { fontSize: 13, lineHeight: 1.5, margin: "0 0 16px", color: v("muted") }, children: "Choose a password. It's how you'll sign in from now on." }),
58
+ /* @__PURE__ */ jsxs("form", { action: formAction, children: [
59
+ /* @__PURE__ */ jsx("label", { style: S.label, htmlFor: "gr-new-password", children: "New password" }),
60
+ /* @__PURE__ */ jsx(
61
+ "input",
62
+ {
63
+ id: "gr-new-password",
64
+ style: S.field,
65
+ name: "password",
66
+ type: "password",
67
+ autoComplete: "new-password",
68
+ minLength: 8,
69
+ required: true,
70
+ autoFocus: true
71
+ }
72
+ ),
73
+ /* @__PURE__ */ jsx("label", { style: S.label, htmlFor: "gr-confirm-password", children: "Confirm password" }),
74
+ /* @__PURE__ */ jsx(
75
+ "input",
76
+ {
77
+ id: "gr-confirm-password",
78
+ style: S.field,
79
+ name: "confirm",
80
+ type: "password",
81
+ autoComplete: "new-password",
82
+ minLength: 8,
83
+ required: true
84
+ }
85
+ ),
86
+ /* @__PURE__ */ jsx("p", { style: { fontSize: 11, color: v("subtle"), margin: "0 0 12px" }, children: "At least 8 characters." }),
87
+ state.error && /* @__PURE__ */ jsx("div", { style: { ...S.error, marginBottom: 12 }, children: state.error }),
88
+ /* @__PURE__ */ jsx("button", { style: { ...S.primaryBtn, width: "100%" }, type: "submit", disabled: isPending || signingOut, children: isPending ? "Saving\u2026" : "Set password" })
89
+ ] }),
90
+ /* @__PURE__ */ jsx(
91
+ "button",
92
+ {
93
+ type: "button",
94
+ disabled: isPending || signingOut,
95
+ style: {
96
+ background: "none",
97
+ border: "none",
98
+ cursor: "pointer",
99
+ color: v("muted"),
100
+ fontSize: 12,
101
+ textDecoration: "underline",
102
+ padding: 0,
103
+ marginTop: 16
104
+ },
105
+ onClick: () => startSignOut(async () => {
106
+ await logoutAction();
107
+ router.refresh();
108
+ }),
109
+ children: "Cancel and sign out"
110
+ }
111
+ )
112
+ ] });
113
+ }
114
+ function ForgotPasswordForm() {
115
+ const [email, setEmail] = useState("");
116
+ const [status, setStatus] = useState("idle");
117
+ const [error, setError] = useState(null);
118
+ if (status === "sent") {
119
+ return /* @__PURE__ */ jsxs("p", { style: { fontSize: 13, lineHeight: 1.5, margin: 0, color: v("fg") }, children: [
120
+ "If ",
121
+ /* @__PURE__ */ jsx("strong", { children: email.trim() }),
122
+ " has access to this site, a link to choose a new password is on its way. It works once, so open it on this device."
123
+ ] });
124
+ }
125
+ return /* @__PURE__ */ jsxs(
126
+ "form",
127
+ {
128
+ onSubmit: async (e) => {
129
+ e.preventDefault();
130
+ setStatus("sending");
131
+ setError(null);
132
+ const res = await forgotPasswordAction(email);
133
+ if (res.success) setStatus("sent");
134
+ else {
135
+ setError(res.error ?? "Something went wrong.");
136
+ setStatus("idle");
137
+ }
138
+ },
139
+ children: [
140
+ /* @__PURE__ */ jsx("p", { style: { fontSize: 13, lineHeight: 1.5, margin: "0 0 16px", color: v("muted") }, children: "Enter your email and we'll send a link to choose a new password." }),
141
+ /* @__PURE__ */ jsx("label", { style: S.label, htmlFor: "gr-forgot-email", children: "Email" }),
142
+ /* @__PURE__ */ jsx(
143
+ "input",
144
+ {
145
+ id: "gr-forgot-email",
146
+ style: S.field,
147
+ type: "email",
148
+ autoComplete: "email",
149
+ placeholder: "name@company.com",
150
+ value: email,
151
+ onChange: (e) => setEmail(e.target.value),
152
+ required: true,
153
+ autoFocus: true
154
+ }
155
+ ),
156
+ error && /* @__PURE__ */ jsx("div", { style: { ...S.error, marginBottom: 12 }, children: error }),
157
+ /* @__PURE__ */ jsx("button", { style: { ...S.primaryBtn, width: "100%" }, type: "submit", disabled: status === "sending", children: status === "sending" ? "Sending\u2026" : "Send reset link" })
158
+ ]
159
+ }
160
+ );
161
+ }
162
+ export {
163
+ AuthDrawer
164
+ };
165
+ //# sourceMappingURL=AuthDrawer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/react/AuthDrawer.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useActionState, useEffect, useState, useTransition } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport { forgotPasswordAction, logoutAction, setPasswordAction } from \"../server/actions\";\nimport { CloseIcon } from \"./Icons\";\nimport { S, v } from \"./styles\";\nimport { ANIMATION_CSS } from \"./Team\";\nimport type { SetPasswordState } from \"../core/types\";\n\nconst initialPasswordState: SetPasswordState = {};\n\n/**\n * The two password steps, in the same drawer the Team panel uses.\n *\n * Guardrail asks people for one credential, a password. Emailed links exist only\n * to let someone choose or replace it, so this is where they land:\n *\n * - \"set-password\" — after an invite or reset link. Cannot be dismissed: until a\n * password exists there is nothing to sign in with next time. The one way out\n * is to sign out.\n * - \"forgot\" — asks for an address and emails a reset link. Opened from the\n * sign-in bar, and from the \"that link didn't work\" message with `message`.\n */\nexport function AuthDrawer({\n mode,\n title,\n message,\n onClose,\n}: {\n mode: \"set-password\" | \"forgot\";\n title?: string;\n /** Plain-language context shown above the form, e.g. why a link failed. */\n message?: string;\n onClose?: () => void;\n}) {\n return (\n <>\n {/* Same sheet the Team drawer ships; React dedupes it by href. */}\n <style href=\"guardrail-team-animation\" precedence=\"default\">\n {ANIMATION_CSS}\n </style>\n <div\n data-guardrail\n data-guardrail-scrim\n style={S.scrim}\n // A set-password step is not something to click away from.\n onClick={mode === \"forgot\" ? onClose : undefined}\n />\n <aside\n data-guardrail\n data-guardrail-team\n style={S.drawer}\n role=\"dialog\"\n aria-label={mode === \"set-password\" ? \"Set your password\" : \"Forgot password\"}\n >\n <header style={S.drawerHead}>\n <h2 style={S.drawerTitle}>\n {title ?? (mode === \"set-password\" ? \"Set your password\" : \"Forgot your password?\")}\n </h2>\n {mode === \"forgot\" && onClose && (\n <button style={{ ...S.iconBtn, color: v(\"muted\") }} onClick={onClose} aria-label=\"Close\">\n <CloseIcon />\n </button>\n )}\n </header>\n <div style={S.drawerBody}>\n {message && (\n <p style={{ fontSize: 13, lineHeight: 1.5, margin: \"0 0 16px\", color: v(\"fg\") }}>{message}</p>\n )}\n {mode === \"set-password\" ? <SetPasswordForm /> : <ForgotPasswordForm />}\n </div>\n </aside>\n </>\n );\n}\n\nfunction SetPasswordForm() {\n const router = useRouter();\n const [state, formAction, isPending] = useActionState(setPasswordAction, initialPasswordState);\n const [signingOut, startSignOut] = useTransition();\n\n useEffect(() => {\n if (state.success) router.refresh();\n }, [state.success, router]);\n\n return (\n <>\n <p style={{ fontSize: 13, lineHeight: 1.5, margin: \"0 0 16px\", color: v(\"muted\") }}>\n Choose a password. It&apos;s how you&apos;ll sign in from now on.\n </p>\n <form action={formAction}>\n <label style={S.label} htmlFor=\"gr-new-password\">\n New password\n </label>\n <input\n id=\"gr-new-password\"\n style={S.field}\n name=\"password\"\n type=\"password\"\n autoComplete=\"new-password\"\n minLength={8}\n required\n autoFocus\n />\n <label style={S.label} htmlFor=\"gr-confirm-password\">\n Confirm password\n </label>\n <input\n id=\"gr-confirm-password\"\n style={S.field}\n name=\"confirm\"\n type=\"password\"\n autoComplete=\"new-password\"\n minLength={8}\n required\n />\n <p style={{ fontSize: 11, color: v(\"subtle\"), margin: \"0 0 12px\" }}>At least 8 characters.</p>\n {state.error && <div style={{ ...S.error, marginBottom: 12 }}>{state.error}</div>}\n <button style={{ ...S.primaryBtn, width: \"100%\" }} type=\"submit\" disabled={isPending || signingOut}>\n {isPending ? \"Saving…\" : \"Set password\"}\n </button>\n </form>\n <button\n type=\"button\"\n disabled={isPending || signingOut}\n style={{\n background: \"none\",\n border: \"none\",\n cursor: \"pointer\",\n color: v(\"muted\"),\n fontSize: 12,\n textDecoration: \"underline\",\n padding: 0,\n marginTop: 16,\n }}\n onClick={() =>\n startSignOut(async () => {\n await logoutAction();\n router.refresh();\n })\n }\n >\n Cancel and sign out\n </button>\n </>\n );\n}\n\nfunction ForgotPasswordForm() {\n const [email, setEmail] = useState(\"\");\n const [status, setStatus] = useState<\"idle\" | \"sending\" | \"sent\">(\"idle\");\n const [error, setError] = useState<string | null>(null);\n\n if (status === \"sent\") {\n return (\n <p style={{ fontSize: 13, lineHeight: 1.5, margin: 0, color: v(\"fg\") }}>\n If <strong>{email.trim()}</strong> has access to this site, a link to choose a new password is on its\n way. It works once, so open it on this device.\n </p>\n );\n }\n\n return (\n <form\n onSubmit={async (e) => {\n e.preventDefault();\n setStatus(\"sending\");\n setError(null);\n const res = await forgotPasswordAction(email);\n if (res.success) setStatus(\"sent\");\n else {\n setError(res.error ?? \"Something went wrong.\");\n setStatus(\"idle\");\n }\n }}\n >\n <p style={{ fontSize: 13, lineHeight: 1.5, margin: \"0 0 16px\", color: v(\"muted\") }}>\n Enter your email and we&apos;ll send a link to choose a new password.\n </p>\n <label style={S.label} htmlFor=\"gr-forgot-email\">\n Email\n </label>\n <input\n id=\"gr-forgot-email\"\n style={S.field}\n type=\"email\"\n autoComplete=\"email\"\n placeholder=\"name@company.com\"\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n required\n autoFocus\n />\n {error && <div style={{ ...S.error, marginBottom: 12 }}>{error}</div>}\n <button style={{ ...S.primaryBtn, width: \"100%\" }} type=\"submit\" disabled={status === \"sending\"}>\n {status === \"sending\" ? \"Sending…\" : \"Send reset link\"}\n </button>\n </form>\n );\n}\n"],"mappings":"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","names":[]}
@@ -27,7 +27,7 @@ function AuthRedirectSync() {
27
27
  url.searchParams.delete("token_hash");
28
28
  url.searchParams.delete("type");
29
29
  window.history.replaceState(null, "", url.toString());
30
- if (res.success) window.location.reload();
30
+ window.location.reload();
31
31
  });
32
32
  }, [searchParams]);
33
33
  return null;
@@ -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 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 const code = searchParams.get(\"code\") ?? undefined;\n const tokenHash = searchParams.get(\"token_hash\") ?? undefined;\n const 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.\n if (res.success) window.location.reload();\n });\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,QAAI,QAAQ,QAAS;AAErB,UAAM,UAAU,OAAO,SAAS,KAAK,WAAW,GAAG,IAC/C,OAAO,SAAS,KAAK,MAAM,CAAC,IAC5B,OAAO,SAAS;AAGpB,UAAM,gBAAgB,uBAAuB,KAAK,OAAO;AAEzD,UAAM,OAAO,aAAa,IAAI,MAAM,KAAK;AACzC,UAAM,YAAY,aAAa,IAAI,YAAY,KAAK;AACpD,UAAM,OAAO,aAAa,IAAI,MAAM,KAAK;AAEzC,QAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC,UAAW;AAC3C,YAAQ,UAAU;AAElB,UAAM,SAAS;AAAA,MACb,GAAI,gBAAgB,EAAE,MAAM,QAAQ,IAAI,CAAC;AAAA,MACzC,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,MACvB,GAAI,YAAY,EAAE,UAAU,IAAI,CAAC;AAAA,MACjC,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,IACzB;AAEA,+BAA2B,MAAM,EAAE,KAAK,CAAC,QAAQ;AAI/C,YAAM,MAAM,IAAI,IAAI,OAAO,SAAS,IAAI;AACxC,UAAI,OAAO;AACX,UAAI,aAAa,OAAO,MAAM;AAC9B,UAAI,aAAa,OAAO,YAAY;AACpC,UAAI,aAAa,OAAO,MAAM;AAC9B,aAAO,QAAQ,aAAa,MAAM,IAAI,IAAI,SAAS,CAAC;AAMpD,UAAI,IAAI,QAAS,QAAO,SAAS,OAAO;AAAA,IAC1C,CAAC;AAAA,EACH,GAAG,CAAC,YAAY,CAAC;AAEjB,SAAO;AACT;","names":[]}
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 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 const code = searchParams.get(\"code\") ?? undefined;\n const tokenHash = searchParams.get(\"token_hash\") ?? undefined;\n const 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 }, [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,QAAI,QAAQ,QAAS;AAErB,UAAM,UAAU,OAAO,SAAS,KAAK,WAAW,GAAG,IAC/C,OAAO,SAAS,KAAK,MAAM,CAAC,IAC5B,OAAO,SAAS;AAGpB,UAAM,gBAAgB,uBAAuB,KAAK,OAAO;AAEzD,UAAM,OAAO,aAAa,IAAI,MAAM,KAAK;AACzC,UAAM,YAAY,aAAa,IAAI,YAAY,KAAK;AACpD,UAAM,OAAO,aAAa,IAAI,MAAM,KAAK;AAEzC,QAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC,UAAW;AAC3C,YAAQ,UAAU;AAElB,UAAM,SAAS;AAAA,MACb,GAAI,gBAAgB,EAAE,MAAM,QAAQ,IAAI,CAAC;AAAA,MACzC,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,MACvB,GAAI,YAAY,EAAE,UAAU,IAAI,CAAC;AAAA,MACjC,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,IACzB;AAEA,+BAA2B,MAAM,EAAE,KAAK,CAAC,QAAQ;AAI/C,YAAM,MAAM,IAAI,IAAI,OAAO,SAAS,IAAI;AACxC,UAAI,OAAO;AACX,UAAI,aAAa,OAAO,MAAM;AAC9B,UAAI,aAAa,OAAO,YAAY;AACpC,UAAI,aAAa,OAAO,MAAM;AAC9B,aAAO,QAAQ,aAAa,MAAM,IAAI,IAAI,SAAS,CAAC;AAQpD,aAAO,SAAS,OAAO;AAAA,IACzB,CAAC;AAAA,EACH,GAAG,CAAC,YAAY,CAAC;AAEjB,SAAO;AACT;","names":[]}
@@ -11,5 +11,6 @@ declare const ArrowDownIcon: () => react_jsx_runtime.JSX.Element;
11
11
  declare const LogOutIcon: () => react_jsx_runtime.JSX.Element;
12
12
  declare const SpinnerIcon: () => react_jsx_runtime.JSX.Element;
13
13
  declare const OpenIcon: () => react_jsx_runtime.JSX.Element;
14
+ declare const KeyIcon: () => react_jsx_runtime.JSX.Element;
14
15
 
15
- export { ArrowDownIcon, ArrowUpIcon, CheckIcon, CloseIcon, LogOutIcon, OpenIcon, PencilIcon, PeopleIcon, PlusIcon, SpinnerIcon, TrashIcon };
16
+ export { ArrowDownIcon, ArrowUpIcon, CheckIcon, CloseIcon, KeyIcon, LogOutIcon, OpenIcon, PencilIcon, PeopleIcon, PlusIcon, SpinnerIcon, TrashIcon };
@@ -63,11 +63,13 @@ const OpenIcon = () => /* @__PURE__ */ jsxs("svg", { ...base, strokeWidth: 2, "a
63
63
  /* @__PURE__ */ jsx("polyline", { points: "15 3 21 3 21 9" }),
64
64
  /* @__PURE__ */ jsx("line", { x1: "10", y1: "14", x2: "21", y2: "3" })
65
65
  ] });
66
+ const KeyIcon = () => /* @__PURE__ */ jsx("svg", { ...base, strokeWidth: 2, "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" }) });
66
67
  export {
67
68
  ArrowDownIcon,
68
69
  ArrowUpIcon,
69
70
  CheckIcon,
70
71
  CloseIcon,
72
+ KeyIcon,
71
73
  LogOutIcon,
72
74
  OpenIcon,
73
75
  PencilIcon,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/react/Icons.tsx"],"sourcesContent":["import React from \"react\";\n\n/**\n * Inline SVG rather than an icon package: Guardrail's chrome should not add a\n * runtime dependency to every consuming app.\n */\nconst base = {\n width: 14,\n height: 14,\n viewBox: \"0 0 24 24\",\n fill: \"none\",\n stroke: \"currentColor\",\n strokeWidth: 3,\n strokeLinecap: \"round\" as const,\n strokeLinejoin: \"round\" as const,\n};\n\nexport const CheckIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <polyline points=\"20 6 9 17 4 12\" />\n </svg>\n);\n\nexport const CloseIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n </svg>\n);\n\nexport const PencilIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <path d=\"M12 20h9\" />\n <path d=\"M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z\" />\n </svg>\n);\n\nexport const PeopleIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <path d=\"M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2\" />\n <circle cx=\"9\" cy=\"7\" r=\"4\" />\n <path d=\"M23 21v-2a4 4 0 0 0-3-3.87\" />\n <path d=\"M16 3.13a4 4 0 0 1 0 7.75\" />\n </svg>\n);\n\nexport const PlusIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\" />\n <line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\" />\n </svg>\n);\n\nexport const TrashIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <polyline points=\"3 6 5 6 21 6\" />\n <path d=\"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6\" />\n <path d=\"M10 11v6M14 11v6M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2\" />\n </svg>\n);\n\nexport const ArrowUpIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"5\" />\n <polyline points=\"6 11 12 5 18 11\" />\n </svg>\n);\n\nexport const ArrowDownIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\" />\n <polyline points=\"18 13 12 19 6 13\" />\n </svg>\n);\n\nexport const LogOutIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\" />\n <polyline points=\"16 17 21 12 16 7\" />\n <line x1=\"21\" y1=\"12\" x2=\"9\" y2=\"12\" />\n </svg>\n);\n\nexport const SpinnerIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <path d=\"M21 12a9 9 0 1 1-6.2-8.6\">\n <animateTransform\n attributeName=\"transform\"\n type=\"rotate\"\n from=\"0 12 12\"\n to=\"360 12 12\"\n dur=\"0.7s\"\n repeatCount=\"indefinite\"\n />\n </path>\n </svg>\n);\n\nexport const OpenIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\" />\n <polyline points=\"15 3 21 3 21 9\" />\n <line x1=\"10\" y1=\"14\" x2=\"21\" y2=\"3\" />\n </svg>\n);\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/react/Icons.tsx"],"sourcesContent":["import React from \"react\";\n\n/**\n * Inline SVG rather than an icon package: Guardrail's chrome should not add a\n * runtime dependency to every consuming app.\n */\nconst base = {\n width: 14,\n height: 14,\n viewBox: \"0 0 24 24\",\n fill: \"none\",\n stroke: \"currentColor\",\n strokeWidth: 3,\n strokeLinecap: \"round\" as const,\n strokeLinejoin: \"round\" as const,\n};\n\nexport const CheckIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <polyline points=\"20 6 9 17 4 12\" />\n </svg>\n);\n\nexport const CloseIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n </svg>\n);\n\nexport const PencilIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <path d=\"M12 20h9\" />\n <path d=\"M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z\" />\n </svg>\n);\n\nexport const PeopleIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <path d=\"M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2\" />\n <circle cx=\"9\" cy=\"7\" r=\"4\" />\n <path d=\"M23 21v-2a4 4 0 0 0-3-3.87\" />\n <path d=\"M16 3.13a4 4 0 0 1 0 7.75\" />\n </svg>\n);\n\nexport const PlusIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\" />\n <line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\" />\n </svg>\n);\n\nexport const TrashIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <polyline points=\"3 6 5 6 21 6\" />\n <path d=\"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6\" />\n <path d=\"M10 11v6M14 11v6M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2\" />\n </svg>\n);\n\nexport const ArrowUpIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"5\" />\n <polyline points=\"6 11 12 5 18 11\" />\n </svg>\n);\n\nexport const ArrowDownIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\" />\n <polyline points=\"18 13 12 19 6 13\" />\n </svg>\n);\n\nexport const LogOutIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\" />\n <polyline points=\"16 17 21 12 16 7\" />\n <line x1=\"21\" y1=\"12\" x2=\"9\" y2=\"12\" />\n </svg>\n);\n\nexport const SpinnerIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <path d=\"M21 12a9 9 0 1 1-6.2-8.6\">\n <animateTransform\n attributeName=\"transform\"\n type=\"rotate\"\n from=\"0 12 12\"\n to=\"360 12 12\"\n dur=\"0.7s\"\n repeatCount=\"indefinite\"\n />\n </path>\n </svg>\n);\n\nexport const OpenIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\" />\n <polyline points=\"15 3 21 3 21 9\" />\n <line x1=\"10\" y1=\"14\" x2=\"21\" y2=\"3\" />\n </svg>\n);\n\nexport const KeyIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <path d=\"M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4\" />\n </svg>\n);\n"],"mappings":"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","names":[]}
@@ -1,7 +1,13 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
 
3
+ /**
4
+ * Keyframes cannot be expressed as inline styles, so the drawer ships the few
5
+ * it needs. Scoped to [data-guardrail-team] to stay clear of app styles, and
6
+ * disabled under prefers-reduced-motion.
7
+ */
8
+ declare const ANIMATION_CSS = "\n@keyframes gr-fade-in { from { opacity: 0 } to { opacity: 1 } }\n@keyframes gr-slide-in {\n from { transform: translateX(100%) }\n to { transform: translateX(0) }\n}\n[data-guardrail-scrim] { animation: gr-fade-in 160ms ease-out both }\n[data-guardrail-team] { animation: gr-slide-in 220ms cubic-bezier(0.32, 0.72, 0, 1) both }\n[data-guardrail-team] [data-gr-row] { animation: gr-fade-in 240ms ease-out both }\n@keyframes gr-fade-out { from { opacity: 1 } to { opacity: 0 } }\n@keyframes gr-slide-out {\n from { transform: translateX(0) }\n to { transform: translateX(100%) }\n}\n[data-guardrail-scrim][data-closing] { animation: gr-fade-out 180ms ease-in both }\n[data-guardrail-team][data-closing] { animation: gr-slide-out 180ms cubic-bezier(0.4, 0, 1, 1) both }\n@media (prefers-reduced-motion: reduce) {\n [data-guardrail-scrim], [data-guardrail-team], [data-guardrail-team] [data-gr-row] {\n animation: none\n }\n}\n";
3
9
  declare function TeamDrawer({ onClose }: {
4
10
  onClose: () => void;
5
11
  }): react_jsx_runtime.JSX.Element;
6
12
 
7
- export { TeamDrawer };
13
+ export { ANIMATION_CSS, TeamDrawer };
@@ -5,10 +5,11 @@ import {
5
5
  inviteMember,
6
6
  listTeam,
7
7
  removeMember,
8
+ sendPasswordResetAction,
8
9
  updateMemberRole
9
10
  } from "../server/actions";
10
11
  import { useGRail } from "./Provider";
11
- import { CloseIcon } from "./Icons";
12
+ import { CloseIcon, KeyIcon } from "./Icons";
12
13
  import { S, v } from "./styles";
13
14
  const ROLES = ["admin", "editor"];
14
15
  const ANIMATION_CSS = `
@@ -39,6 +40,7 @@ function TeamDrawer({ onClose }) {
39
40
  const [members, setMembers] = useState([]);
40
41
  const [loading, setLoading] = useState(true);
41
42
  const [error, setError] = useState(null);
43
+ const [notice, setNotice] = useState(null);
42
44
  const [inviteEmail, setInviteEmail] = useState("");
43
45
  const [inviteRole, setInviteRole] = useState("editor");
44
46
  const [pending, startTransition] = useTransition();
@@ -51,8 +53,10 @@ function TeamDrawer({ onClose }) {
51
53
  }, []);
52
54
  const run = (fn) => startTransition(async () => {
53
55
  setError(null);
56
+ setNotice(null);
54
57
  const res = await fn();
55
58
  if (!res.success) setError(res.error ?? "Something went wrong.");
59
+ else if (res.notice) setNotice(res.notice);
56
60
  await refresh();
57
61
  });
58
62
  const [closing, setClosing] = useState(false);
@@ -102,6 +106,7 @@ function TeamDrawer({ onClose }) {
102
106
  ] }),
103
107
  /* @__PURE__ */ jsxs("div", { style: S.drawerBody, children: [
104
108
  error && /* @__PURE__ */ jsx("div", { style: { ...S.error, marginBottom: 12 }, children: error }),
109
+ notice && /* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: v("muted"), marginBottom: 12 }, role: "status", children: notice }),
105
110
  loading ? /* @__PURE__ */ jsx("p", { style: { fontSize: 13, color: v("muted") }, children: "Loading\u2026" }) : members.map((m) => /* @__PURE__ */ jsxs("div", { "data-gr-row": true, style: S.memberRow, children: [
106
111
  /* @__PURE__ */ jsxs("span", { style: S.memberEmail, title: m.email, children: [
107
112
  m.email,
@@ -120,6 +125,17 @@ function TeamDrawer({ onClose }) {
120
125
  children: ROLES.map((r) => /* @__PURE__ */ jsx("option", { value: r, children: r }, r))
121
126
  }
122
127
  ) : /* @__PURE__ */ jsx("span", { style: { fontSize: 12, color: v("muted") }, children: m.role }),
128
+ isAdmin && /* @__PURE__ */ jsx(
129
+ "button",
130
+ {
131
+ style: { ...S.iconBtn, color: v("muted"), width: 22, height: 22 },
132
+ disabled: pending,
133
+ onClick: () => run(() => sendPasswordResetAction(m.email)),
134
+ "aria-label": `Send password reset to ${m.email}`,
135
+ title: "Send password reset",
136
+ children: /* @__PURE__ */ jsx(KeyIcon, {})
137
+ }
138
+ ),
123
139
  isAdmin && /* @__PURE__ */ jsx(
124
140
  "button",
125
141
  {
@@ -173,9 +189,12 @@ function TeamDrawer({ onClose }) {
173
189
  }
174
190
  ),
175
191
  /* @__PURE__ */ jsxs("p", { style: { fontSize: 11, color: v("subtle"), marginTop: 10 }, children: [
176
- "Guardrail records the invite. Sending the email is your app's job \u2014 wire the ",
192
+ "New people get an email to set a password. Invites and resets go out through your ",
177
193
  /* @__PURE__ */ jsx("code", { children: "onInvite" }),
178
- " handler."
194
+ " / ",
195
+ /* @__PURE__ */ jsx("code", { children: "onPasswordReset" }),
196
+ " ",
197
+ "handlers, or your auth provider's own emails if you haven't set them."
179
198
  ] })
180
199
  ] })
181
200
  ] })
@@ -185,6 +204,7 @@ function TeamDrawer({ onClose }) {
185
204
  ] });
186
205
  }
187
206
  export {
207
+ ANIMATION_CSS,
188
208
  TeamDrawer
189
209
  };
190
210
  //# sourceMappingURL=Team.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/react/Team.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useCallback, useEffect, useState, useTransition } from \"react\";\nimport {\n inviteMember,\n listTeam,\n removeMember,\n updateMemberRole,\n} from \"../server/actions\";\nimport { useGRail } from \"./Provider\";\nimport { CloseIcon } from \"./Icons\";\nimport { S, v } from \"./styles\";\nimport type { GRMember, GRRole } from \"../core/types\";\n\nconst ROLES: GRRole[] = [\"admin\", \"editor\"];\n\n/**\n * Keyframes cannot be expressed as inline styles, so the drawer ships the few\n * it needs. Scoped to [data-guardrail-team] to stay clear of app styles, and\n * disabled under prefers-reduced-motion.\n */\nconst ANIMATION_CSS = `\n@keyframes gr-fade-in { from { opacity: 0 } to { opacity: 1 } }\n@keyframes gr-slide-in {\n from { transform: translateX(100%) }\n to { transform: translateX(0) }\n}\n[data-guardrail-scrim] { animation: gr-fade-in 160ms ease-out both }\n[data-guardrail-team] { animation: gr-slide-in 220ms cubic-bezier(0.32, 0.72, 0, 1) both }\n[data-guardrail-team] [data-gr-row] { animation: gr-fade-in 240ms ease-out both }\n@keyframes gr-fade-out { from { opacity: 1 } to { opacity: 0 } }\n@keyframes gr-slide-out {\n from { transform: translateX(0) }\n to { transform: translateX(100%) }\n}\n[data-guardrail-scrim][data-closing] { animation: gr-fade-out 180ms ease-in both }\n[data-guardrail-team][data-closing] { animation: gr-slide-out 180ms cubic-bezier(0.4, 0, 1, 1) both }\n@media (prefers-reduced-motion: reduce) {\n [data-guardrail-scrim], [data-guardrail-team], [data-guardrail-team] [data-gr-row] {\n animation: none\n }\n}\n`;\n\nexport function TeamDrawer({ onClose }: { onClose: () => void }) {\n const { role, email: currentEmail } = useGRail();\n const isAdmin = role === \"admin\";\n\n const [members, setMembers] = useState<GRMember[]>([]);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n const [inviteEmail, setInviteEmail] = useState(\"\");\n const [inviteRole, setInviteRole] = useState<GRRole>(\"editor\");\n const [pending, startTransition] = useTransition();\n\n const refresh = () =>\n listTeam().then((rows) => {\n setMembers(rows);\n setLoading(false);\n });\n\n useEffect(() => {\n void refresh();\n }, []);\n\n // Server actions re-check the role; this only decides what to render.\n // Always refresh: a failure can still have changed the roster (an invite\n // that saved but whose email failed, for instance).\n const run = (fn: () => Promise<{ success: boolean; error?: string }>) =>\n startTransition(async () => {\n setError(null);\n const res = await fn();\n if (!res.success) setError(res.error ?? \"Something went wrong.\");\n await refresh();\n });\n\n /**\n * Unmounting on close skips the exit animation entirely. Stay mounted, play\n * it, and let the parent unmount once it finishes.\n */\n const [closing, setClosing] = useState(false);\n const requestClose = useCallback(() => {\n const reduced =\n typeof window !== \"undefined\" &&\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n if (reduced) return onClose(); // No animation, so no animationend to wait for.\n setClosing(true);\n }, [onClose]);\n\n useEffect(() => {\n if (!closing) return;\n // Safety net if animationend never arrives (tab hidden, styles stripped).\n const t = window.setTimeout(onClose, 400);\n return () => window.clearTimeout(t);\n }, [closing, onClose]);\n\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => e.key === \"Escape\" && requestClose();\n window.addEventListener(\"keydown\", onKey);\n return () => window.removeEventListener(\"keydown\", onKey);\n }, [requestClose]);\n\n return (\n <>\n {/* Hoisted to <head> — see the note on the theme <style> in Admin.tsx. */}\n <style href=\"guardrail-team-animation\" precedence=\"default\">\n {ANIMATION_CSS}\n </style>\n <div\n data-guardrail\n data-guardrail-scrim\n {...(closing ? { \"data-closing\": \"\" } : {})}\n style={S.scrim}\n onClick={requestClose}\n />\n <aside\n data-guardrail\n data-guardrail-team\n {...(closing ? { \"data-closing\": \"\" } : {})}\n onAnimationEnd={(e) => {\n // Row fade-ins bubble up here too; only the drawer's own exit counts.\n if (closing && e.target === e.currentTarget) onClose();\n }}\n style={S.drawer}\n role=\"dialog\"\n aria-label=\"Team\"\n >\n <header style={S.drawerHead}>\n <h2 style={S.drawerTitle}>Team</h2>\n <button style={{ ...S.iconBtn, color: v(\"muted\") }} onClick={requestClose} aria-label=\"Close\">\n <CloseIcon />\n </button>\n </header>\n\n <div style={S.drawerBody}>\n {error && <div style={{ ...S.error, marginBottom: 12 }}>{error}</div>}\n\n {loading ? (\n <p style={{ fontSize: 13, color: v(\"muted\") }}>Loading…</p>\n ) : (\n members.map((m) => (\n <div key={m.email} data-gr-row style={S.memberRow}>\n <span style={S.memberEmail} title={m.email}>\n {m.email}\n {m.email === currentEmail && \" (you)\"}\n </span>\n\n {m.status === \"pending\" && <span style={S.badge}>Invited</span>}\n\n {isAdmin ? (\n <select\n style={S.select}\n value={m.role}\n disabled={pending}\n onChange={(e) =>\n run(() =>\n updateMemberRole(m.email, e.target.value as GRRole),\n )\n }\n >\n {ROLES.map((r) => (\n <option key={r} value={r}>\n {r}\n </option>\n ))}\n </select>\n ) : (\n <span style={{ fontSize: 12, color: v(\"muted\") }}>{m.role}</span>\n )}\n\n {isAdmin && (\n <button\n style={{ ...S.iconBtn, color: v(\"muted\"), width: 22, height: 22 }}\n disabled={pending}\n onClick={() => run(() => removeMember(m.email))}\n aria-label={`Remove ${m.email}`}\n title=\"Remove\"\n >\n <CloseIcon />\n </button>\n )}\n </div>\n ))\n )}\n\n {isAdmin && (\n <>\n <p style={S.sectionLabel}>Invite someone</p>\n <form\n onSubmit={(e) => {\n e.preventDefault();\n run(async () => {\n const res = await inviteMember(inviteEmail, inviteRole);\n if (res.success) setInviteEmail(\"\");\n return res;\n });\n }}\n >\n <input\n style={S.field}\n type=\"email\"\n placeholder=\"name@company.com\"\n value={inviteEmail}\n onChange={(e) => setInviteEmail(e.target.value)}\n required\n />\n <div style={{ display: \"flex\", gap: 8 }}>\n <select\n style={{ ...S.select, flex: 1 }}\n value={inviteRole}\n onChange={(e) => setInviteRole(e.target.value as GRRole)}\n >\n {ROLES.map((r) => (\n <option key={r} value={r}>\n {r}\n </option>\n ))}\n </select>\n <button style={S.primaryBtn} type=\"submit\" disabled={pending}>\n {pending ? \"Working…\" : \"Invite\"}\n </button>\n </div>\n </form>\n <p style={{ fontSize: 11, color: v(\"subtle\"), marginTop: 10 }}>\n Guardrail records the invite. Sending the email is your app&apos;s\n job — wire the <code>onInvite</code> handler.\n </p>\n </>\n )}\n </div>\n </aside>\n </>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/react/Team.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useCallback, useEffect, useState, useTransition } from \"react\";\nimport {\n inviteMember,\n listTeam,\n removeMember,\n sendPasswordResetAction,\n updateMemberRole,\n} from \"../server/actions\";\nimport { useGRail } from \"./Provider\";\nimport { CloseIcon, KeyIcon } from \"./Icons\";\nimport { S, v } from \"./styles\";\nimport type { GRMember, GRRole } from \"../core/types\";\n\nconst ROLES: GRRole[] = [\"admin\", \"editor\"];\n\n/**\n * Keyframes cannot be expressed as inline styles, so the drawer ships the few\n * it needs. Scoped to [data-guardrail-team] to stay clear of app styles, and\n * disabled under prefers-reduced-motion.\n */\nexport const ANIMATION_CSS = `\n@keyframes gr-fade-in { from { opacity: 0 } to { opacity: 1 } }\n@keyframes gr-slide-in {\n from { transform: translateX(100%) }\n to { transform: translateX(0) }\n}\n[data-guardrail-scrim] { animation: gr-fade-in 160ms ease-out both }\n[data-guardrail-team] { animation: gr-slide-in 220ms cubic-bezier(0.32, 0.72, 0, 1) both }\n[data-guardrail-team] [data-gr-row] { animation: gr-fade-in 240ms ease-out both }\n@keyframes gr-fade-out { from { opacity: 1 } to { opacity: 0 } }\n@keyframes gr-slide-out {\n from { transform: translateX(0) }\n to { transform: translateX(100%) }\n}\n[data-guardrail-scrim][data-closing] { animation: gr-fade-out 180ms ease-in both }\n[data-guardrail-team][data-closing] { animation: gr-slide-out 180ms cubic-bezier(0.4, 0, 1, 1) both }\n@media (prefers-reduced-motion: reduce) {\n [data-guardrail-scrim], [data-guardrail-team], [data-guardrail-team] [data-gr-row] {\n animation: none\n }\n}\n`;\n\nexport function TeamDrawer({ onClose }: { onClose: () => void }) {\n const { role, email: currentEmail } = useGRail();\n const isAdmin = role === \"admin\";\n\n const [members, setMembers] = useState<GRMember[]>([]);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n const [notice, setNotice] = useState<string | null>(null);\n const [inviteEmail, setInviteEmail] = useState(\"\");\n const [inviteRole, setInviteRole] = useState<GRRole>(\"editor\");\n const [pending, startTransition] = useTransition();\n\n const refresh = () =>\n listTeam().then((rows) => {\n setMembers(rows);\n setLoading(false);\n });\n\n useEffect(() => {\n void refresh();\n }, []);\n\n // Server actions re-check the role; this only decides what to render.\n // Always refresh: a failure can still have changed the roster (an invite\n // that saved but whose email failed, for instance).\n const run = (fn: () => Promise<{ success: boolean; error?: string; notice?: string }>) =>\n startTransition(async () => {\n setError(null);\n setNotice(null);\n const res = await fn();\n if (!res.success) setError(res.error ?? \"Something went wrong.\");\n else if (res.notice) setNotice(res.notice);\n await refresh();\n });\n\n /**\n * Unmounting on close skips the exit animation entirely. Stay mounted, play\n * it, and let the parent unmount once it finishes.\n */\n const [closing, setClosing] = useState(false);\n const requestClose = useCallback(() => {\n const reduced =\n typeof window !== \"undefined\" &&\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n if (reduced) return onClose(); // No animation, so no animationend to wait for.\n setClosing(true);\n }, [onClose]);\n\n useEffect(() => {\n if (!closing) return;\n // Safety net if animationend never arrives (tab hidden, styles stripped).\n const t = window.setTimeout(onClose, 400);\n return () => window.clearTimeout(t);\n }, [closing, onClose]);\n\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => e.key === \"Escape\" && requestClose();\n window.addEventListener(\"keydown\", onKey);\n return () => window.removeEventListener(\"keydown\", onKey);\n }, [requestClose]);\n\n return (\n <>\n {/* Hoisted to <head> — see the note on the theme <style> in Admin.tsx. */}\n <style href=\"guardrail-team-animation\" precedence=\"default\">\n {ANIMATION_CSS}\n </style>\n <div\n data-guardrail\n data-guardrail-scrim\n {...(closing ? { \"data-closing\": \"\" } : {})}\n style={S.scrim}\n onClick={requestClose}\n />\n <aside\n data-guardrail\n data-guardrail-team\n {...(closing ? { \"data-closing\": \"\" } : {})}\n onAnimationEnd={(e) => {\n // Row fade-ins bubble up here too; only the drawer's own exit counts.\n if (closing && e.target === e.currentTarget) onClose();\n }}\n style={S.drawer}\n role=\"dialog\"\n aria-label=\"Team\"\n >\n <header style={S.drawerHead}>\n <h2 style={S.drawerTitle}>Team</h2>\n <button style={{ ...S.iconBtn, color: v(\"muted\") }} onClick={requestClose} aria-label=\"Close\">\n <CloseIcon />\n </button>\n </header>\n\n <div style={S.drawerBody}>\n {error && <div style={{ ...S.error, marginBottom: 12 }}>{error}</div>}\n {notice && (\n <div style={{ fontSize: 12, color: v(\"muted\"), marginBottom: 12 }} role=\"status\">\n {notice}\n </div>\n )}\n\n {loading ? (\n <p style={{ fontSize: 13, color: v(\"muted\") }}>Loading…</p>\n ) : (\n members.map((m) => (\n <div key={m.email} data-gr-row style={S.memberRow}>\n <span style={S.memberEmail} title={m.email}>\n {m.email}\n {m.email === currentEmail && \" (you)\"}\n </span>\n\n {m.status === \"pending\" && <span style={S.badge}>Invited</span>}\n\n {isAdmin ? (\n <select\n style={S.select}\n value={m.role}\n disabled={pending}\n onChange={(e) =>\n run(() =>\n updateMemberRole(m.email, e.target.value as GRRole),\n )\n }\n >\n {ROLES.map((r) => (\n <option key={r} value={r}>\n {r}\n </option>\n ))}\n </select>\n ) : (\n <span style={{ fontSize: 12, color: v(\"muted\") }}>{m.role}</span>\n )}\n\n {isAdmin && (\n <button\n style={{ ...S.iconBtn, color: v(\"muted\"), width: 22, height: 22 }}\n disabled={pending}\n onClick={() => run(() => sendPasswordResetAction(m.email))}\n aria-label={`Send password reset to ${m.email}`}\n title=\"Send password reset\"\n >\n <KeyIcon />\n </button>\n )}\n\n {isAdmin && (\n <button\n style={{ ...S.iconBtn, color: v(\"muted\"), width: 22, height: 22 }}\n disabled={pending}\n onClick={() => run(() => removeMember(m.email))}\n aria-label={`Remove ${m.email}`}\n title=\"Remove\"\n >\n <CloseIcon />\n </button>\n )}\n </div>\n ))\n )}\n\n {isAdmin && (\n <>\n <p style={S.sectionLabel}>Invite someone</p>\n <form\n onSubmit={(e) => {\n e.preventDefault();\n run(async () => {\n const res = await inviteMember(inviteEmail, inviteRole);\n if (res.success) setInviteEmail(\"\");\n return res;\n });\n }}\n >\n <input\n style={S.field}\n type=\"email\"\n placeholder=\"name@company.com\"\n value={inviteEmail}\n onChange={(e) => setInviteEmail(e.target.value)}\n required\n />\n <div style={{ display: \"flex\", gap: 8 }}>\n <select\n style={{ ...S.select, flex: 1 }}\n value={inviteRole}\n onChange={(e) => setInviteRole(e.target.value as GRRole)}\n >\n {ROLES.map((r) => (\n <option key={r} value={r}>\n {r}\n </option>\n ))}\n </select>\n <button style={S.primaryBtn} type=\"submit\" disabled={pending}>\n {pending ? \"Working…\" : \"Invite\"}\n </button>\n </div>\n </form>\n <p style={{ fontSize: 11, color: v(\"subtle\"), marginTop: 10 }}>\n New people get an email to set a password. Invites and resets go\n out through your <code>onInvite</code> / <code>onPasswordReset</code>{\" \"}\n handlers, or your auth provider&apos;s own emails if you haven&apos;t\n set them.\n </p>\n </>\n )}\n </div>\n </aside>\n </>\n );\n}\n"],"mappings":"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","names":[]}
@@ -62,8 +62,9 @@ declare function canEdit(): Promise<boolean>;
62
62
  *
63
63
  * needsPassword is set by completeAuthRedirectAction the moment an
64
64
  * invite/recovery link finishes — it forces chrome on regardless of the
65
- * edit-mode cookie, since the whole point is to show the set-password prompt
65
+ * edit-mode cookie, since the whole point is to show the set-password step
66
66
  * right away, not require also having ?edit=true or an unsuppressed cookie.
67
+ * authError (a link that failed) does the same, to explain what happened.
67
68
  */
68
69
  declare function resolveEditMode(viewer: GRViewer | null, cookieStore: {
69
70
  get(name: string): {
@@ -72,6 +73,7 @@ declare function resolveEditMode(viewer: GRViewer | null, cookieStore: {
72
73
  }): {
73
74
  isEditMode: boolean;
74
75
  needsPassword: boolean;
76
+ authError?: "expired" | "failed";
75
77
  };
76
78
 
77
79
  export { canEdit, isAdmin, isEditor, requireRole, resolveEditMode };
@@ -19,9 +19,11 @@ async function canEdit() {
19
19
  }
20
20
  function resolveEditMode(viewer, cookieStore) {
21
21
  const editCookie = cookieStore.get(GR_COOKIES.EDIT_MODE)?.value;
22
- const needsPassword = cookieStore.get(GR_COOKIES.NEEDS_PASSWORD)?.value === "true";
23
- const isEditMode = needsPassword || (editCookie === "false" ? false : editCookie === "true" || viewer !== null);
24
- return { isEditMode, needsPassword };
22
+ const needsPassword = viewer !== null && cookieStore.get(GR_COOKIES.NEEDS_PASSWORD)?.value === "true";
23
+ const rawError = cookieStore.get(GR_COOKIES.AUTH_ERROR)?.value;
24
+ const authError = rawError === "expired" || rawError === "failed" ? rawError : void 0;
25
+ const isEditMode = needsPassword || authError !== void 0 || (editCookie === "false" ? false : editCookie === "true" || viewer !== null);
26
+ return { isEditMode, needsPassword, ...authError ? { authError } : {} };
25
27
  }
26
28
  export {
27
29
  canEdit,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/server/access.ts"],"sourcesContent":["import { cookies } from \"next/headers\";\nimport { getCachedViewer } from \"./session\";\nimport { GR_COOKIES } from \"../core/defs\";\nimport type { GRRole, GRViewer } from \"../core/types\";\n\n/**\n * Role checks, server-side only.\n *\n * Deliberately NOT in a \"use server\" file: every export of one of those is a\n * callable Server Action endpoint, which is the wrong shape for an\n * authorization helper — it should only ever run inside the server code that\n * calls it.\n *\n * The client's copy of the role (useGRail().role) decides what to *show*; it\n * never decides what is *allowed*. A hidden button is not authorization, so\n * any Server Action behind a custom admin UI must call one of these itself.\n */\n\n/**\n * Returns the signed-in viewer if their role is in `allowed`, otherwise null.\n * The building block for the two helpers below; reach for this directly only\n * when you need the viewer itself (their email, say), not just a yes/no.\n */\nexport async function requireRole(\n allowed: readonly GRRole[],\n): Promise<GRViewer | null> {\n const viewer = await getCachedViewer();\n if (!viewer || !viewer.role || !allowed.includes(viewer.role)) return null;\n return viewer;\n}\n\n/** True for a signed-in admin of this site — team management, and everything an editor can do. */\nexport async function isAdmin(): Promise<boolean> {\n return (await requireRole([\"admin\"])) !== null;\n}\n\n/**\n * True for anyone who may edit this site's content: an editor, or an admin\n * (admins can do everything editors can). The check for a custom drawer's\n * Server Action that changes content. Use isAdmin() instead for anything that\n * touches the team.\n */\nexport async function isEditor(): Promise<boolean> {\n return (await requireRole([\"admin\", \"editor\"])) !== null;\n}\n\n/**\n * True when this viewer may edit AND the site is currently in edit mode — the\n * one check for \"should I show editing UI / do edit-only work right now.\"\n * Admin vs editor doesn't matter; use isAdmin() when it does.\n *\n * For a custom drawer's trigger rendered in a Server Component. In a Client\n * Component, read `canEdit` from useGRail() instead — same answer, same rule.\n * Don't use it as the only authorization on a Server Action: edit mode is a\n * display preference (a cookie the visitor controls), so the action should\n * call isEditor() — this answers \"show it,\" that answers \"allow it.\"\n *\n * Reads cookies, which makes the calling route dynamic — nothing new in\n * practice, since withGuardrail() already does the same in the root layout.\n */\nexport async function canEdit(): Promise<boolean> {\n const viewer = await requireRole([\"admin\", \"editor\"]);\n if (!viewer) return false;\n return resolveEditMode(viewer, await cookies()).isEditMode;\n}\n\n/**\n * The one definition of \"is the site in edit mode,\" shared by withGuardrail()\n * (which hands it to the client as useGRail().isEditMode) and canEdit() so the\n * two can't drift apart.\n *\n * \"false\" is an explicit, deliberate suppression (the toolbar's exit button,\n * or ?edit=false, while still signed in) and wins outright — it lasts until\n * the next ?edit=true, and ends only the editing session, not the sign-in. Otherwise, a real\n * session is enough on its own — an authenticated editor's chrome persists for\n * as long as that session does, with no separate cookie timer to fall out of\n * sync with it. \"true\" only matters for someone not yet authenticated: a\n * short-lived grace window (see GR_EDIT_MODE_TTL_SECONDS) to actually reach\n * the sign-in form, not a standing invitation — the previous design kept this\n * cookie alive indefinitely once set, which meant a stray copy of it kept\n * showing the sign-in bar to whoever's browser it was sitting in, for as long\n * as that browser stayed open.\n *\n * needsPassword is set by completeAuthRedirectAction the moment an\n * invite/recovery link finishes — it forces chrome on regardless of the\n * edit-mode cookie, since the whole point is to show the set-password prompt\n * right away, not require also having ?edit=true or an unsuppressed cookie.\n */\nexport function resolveEditMode(\n viewer: GRViewer | null,\n cookieStore: { get(name: string): { value: string } | undefined },\n): { isEditMode: boolean; needsPassword: boolean } {\n const editCookie = cookieStore.get(GR_COOKIES.EDIT_MODE)?.value;\n const needsPassword = cookieStore.get(GR_COOKIES.NEEDS_PASSWORD)?.value === \"true\";\n const isEditMode =\n needsPassword ||\n (editCookie === \"false\" ? false : editCookie === \"true\" || viewer !== null);\n return { isEditMode, needsPassword };\n}\n"],"mappings":"AAAA,SAAS,eAAe;AACxB,SAAS,uBAAuB;AAChC,SAAS,kBAAkB;AAqB3B,eAAsB,YACpB,SAC0B;AAC1B,QAAM,SAAS,MAAM,gBAAgB;AACrC,MAAI,CAAC,UAAU,CAAC,OAAO,QAAQ,CAAC,QAAQ,SAAS,OAAO,IAAI,EAAG,QAAO;AACtE,SAAO;AACT;AAGA,eAAsB,UAA4B;AAChD,SAAQ,MAAM,YAAY,CAAC,OAAO,CAAC,MAAO;AAC5C;AAQA,eAAsB,WAA6B;AACjD,SAAQ,MAAM,YAAY,CAAC,SAAS,QAAQ,CAAC,MAAO;AACtD;AAgBA,eAAsB,UAA4B;AAChD,QAAM,SAAS,MAAM,YAAY,CAAC,SAAS,QAAQ,CAAC;AACpD,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,gBAAgB,QAAQ,MAAM,QAAQ,CAAC,EAAE;AAClD;AAwBO,SAAS,gBACd,QACA,aACiD;AACjD,QAAM,aAAa,YAAY,IAAI,WAAW,SAAS,GAAG;AAC1D,QAAM,gBAAgB,YAAY,IAAI,WAAW,cAAc,GAAG,UAAU;AAC5E,QAAM,aACJ,kBACC,eAAe,UAAU,QAAQ,eAAe,UAAU,WAAW;AACxE,SAAO,EAAE,YAAY,cAAc;AACrC;","names":[]}
1
+ {"version":3,"sources":["../../src/server/access.ts"],"sourcesContent":["import { cookies } from \"next/headers\";\nimport { getCachedViewer } from \"./session\";\nimport { GR_COOKIES } from \"../core/defs\";\nimport type { GRRole, GRViewer } from \"../core/types\";\n\n/**\n * Role checks, server-side only.\n *\n * Deliberately NOT in a \"use server\" file: every export of one of those is a\n * callable Server Action endpoint, which is the wrong shape for an\n * authorization helper — it should only ever run inside the server code that\n * calls it.\n *\n * The client's copy of the role (useGRail().role) decides what to *show*; it\n * never decides what is *allowed*. A hidden button is not authorization, so\n * any Server Action behind a custom admin UI must call one of these itself.\n */\n\n/**\n * Returns the signed-in viewer if their role is in `allowed`, otherwise null.\n * The building block for the two helpers below; reach for this directly only\n * when you need the viewer itself (their email, say), not just a yes/no.\n */\nexport async function requireRole(\n allowed: readonly GRRole[],\n): Promise<GRViewer | null> {\n const viewer = await getCachedViewer();\n if (!viewer || !viewer.role || !allowed.includes(viewer.role)) return null;\n return viewer;\n}\n\n/** True for a signed-in admin of this site — team management, and everything an editor can do. */\nexport async function isAdmin(): Promise<boolean> {\n return (await requireRole([\"admin\"])) !== null;\n}\n\n/**\n * True for anyone who may edit this site's content: an editor, or an admin\n * (admins can do everything editors can). The check for a custom drawer's\n * Server Action that changes content. Use isAdmin() instead for anything that\n * touches the team.\n */\nexport async function isEditor(): Promise<boolean> {\n return (await requireRole([\"admin\", \"editor\"])) !== null;\n}\n\n/**\n * True when this viewer may edit AND the site is currently in edit mode — the\n * one check for \"should I show editing UI / do edit-only work right now.\"\n * Admin vs editor doesn't matter; use isAdmin() when it does.\n *\n * For a custom drawer's trigger rendered in a Server Component. In a Client\n * Component, read `canEdit` from useGRail() instead — same answer, same rule.\n * Don't use it as the only authorization on a Server Action: edit mode is a\n * display preference (a cookie the visitor controls), so the action should\n * call isEditor() — this answers \"show it,\" that answers \"allow it.\"\n *\n * Reads cookies, which makes the calling route dynamic — nothing new in\n * practice, since withGuardrail() already does the same in the root layout.\n */\nexport async function canEdit(): Promise<boolean> {\n const viewer = await requireRole([\"admin\", \"editor\"]);\n if (!viewer) return false;\n return resolveEditMode(viewer, await cookies()).isEditMode;\n}\n\n/**\n * The one definition of \"is the site in edit mode,\" shared by withGuardrail()\n * (which hands it to the client as useGRail().isEditMode) and canEdit() so the\n * two can't drift apart.\n *\n * \"false\" is an explicit, deliberate suppression (the toolbar's exit button,\n * or ?edit=false, while still signed in) and wins outright — it lasts until\n * the next ?edit=true, and ends only the editing session, not the sign-in. Otherwise, a real\n * session is enough on its own — an authenticated editor's chrome persists for\n * as long as that session does, with no separate cookie timer to fall out of\n * sync with it. \"true\" only matters for someone not yet authenticated: a\n * short-lived grace window (see GR_EDIT_MODE_TTL_SECONDS) to actually reach\n * the sign-in form, not a standing invitation — the previous design kept this\n * cookie alive indefinitely once set, which meant a stray copy of it kept\n * showing the sign-in bar to whoever's browser it was sitting in, for as long\n * as that browser stayed open.\n *\n * needsPassword is set by completeAuthRedirectAction the moment an\n * invite/recovery link finishes — it forces chrome on regardless of the\n * edit-mode cookie, since the whole point is to show the set-password step\n * right away, not require also having ?edit=true or an unsuppressed cookie.\n * authError (a link that failed) does the same, to explain what happened.\n */\nexport function resolveEditMode(\n viewer: GRViewer | null,\n cookieStore: { get(name: string): { value: string } | undefined },\n): { isEditMode: boolean; needsPassword: boolean; authError?: \"expired\" | \"failed\" } {\n const editCookie = cookieStore.get(GR_COOKIES.EDIT_MODE)?.value;\n // Only means anything with a session to set a password on. A leftover flag —\n // from a link whose session has since been signed out or expired — must not\n // be able to force the chrome on for someone nobody is signed in as, which no\n // \"off\" setting could undo.\n const needsPassword =\n viewer !== null && cookieStore.get(GR_COOKIES.NEEDS_PASSWORD)?.value === \"true\";\n const rawError = cookieStore.get(GR_COOKIES.AUTH_ERROR)?.value;\n const authError = rawError === \"expired\" || rawError === \"failed\" ? rawError : undefined;\n // A failed link forces the chrome on for the same reason a pending password\n // does: the whole point is to tell the person right now, even if they arrived\n // with no ?edit=true and no session.\n const isEditMode =\n needsPassword ||\n authError !== undefined ||\n (editCookie === \"false\" ? false : editCookie === \"true\" || viewer !== null);\n return { isEditMode, needsPassword, ...(authError ? { authError } : {}) };\n}\n"],"mappings":"AAAA,SAAS,eAAe;AACxB,SAAS,uBAAuB;AAChC,SAAS,kBAAkB;AAqB3B,eAAsB,YACpB,SAC0B;AAC1B,QAAM,SAAS,MAAM,gBAAgB;AACrC,MAAI,CAAC,UAAU,CAAC,OAAO,QAAQ,CAAC,QAAQ,SAAS,OAAO,IAAI,EAAG,QAAO;AACtE,SAAO;AACT;AAGA,eAAsB,UAA4B;AAChD,SAAQ,MAAM,YAAY,CAAC,OAAO,CAAC,MAAO;AAC5C;AAQA,eAAsB,WAA6B;AACjD,SAAQ,MAAM,YAAY,CAAC,SAAS,QAAQ,CAAC,MAAO;AACtD;AAgBA,eAAsB,UAA4B;AAChD,QAAM,SAAS,MAAM,YAAY,CAAC,SAAS,QAAQ,CAAC;AACpD,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,gBAAgB,QAAQ,MAAM,QAAQ,CAAC,EAAE;AAClD;AAyBO,SAAS,gBACd,QACA,aACmF;AACnF,QAAM,aAAa,YAAY,IAAI,WAAW,SAAS,GAAG;AAK1D,QAAM,gBACJ,WAAW,QAAQ,YAAY,IAAI,WAAW,cAAc,GAAG,UAAU;AAC3E,QAAM,WAAW,YAAY,IAAI,WAAW,UAAU,GAAG;AACzD,QAAM,YAAY,aAAa,aAAa,aAAa,WAAW,WAAW;AAI/E,QAAM,aACJ,iBACA,cAAc,WACb,eAAe,UAAU,QAAQ,eAAe,UAAU,WAAW;AACxE,SAAO,EAAE,YAAY,eAAe,GAAI,YAAY,EAAE,UAAU,IAAI,CAAC,EAAG;AAC1E;","names":[]}
@@ -2,9 +2,11 @@ import { GRRole, GRAsset, GRMember, OverrideValue, SetPasswordState, ActionState
2
2
  import { GRUploadTarget, AuthRedirectParams } from '../core/adapter-types.js';
3
3
  import 'react';
4
4
 
5
+ /** `notice` is for a success the admin should still read, e.g. "no email was sent because...". */
5
6
  type Result = {
6
7
  success: boolean;
7
8
  error?: string;
9
+ notice?: string;
8
10
  };
9
11
  declare function saveOverride(row: OverrideRow): Promise<Result>;
10
12
  /**
@@ -46,13 +48,26 @@ declare function listTeam(): Promise<GRMember[]>;
46
48
  declare function inviteMember(email: string, role: GRRole): Promise<Result>;
47
49
  declare function updateMemberRole(email: string, role: GRRole): Promise<Result>;
48
50
  declare function removeMember(email: string): Promise<Result>;
51
+ /**
52
+ * "Forgot password?" from the sign-in bar. Called with no session, so it says
53
+ * the same thing whether or not the address belongs to anyone: a member gets an
54
+ * email, a stranger gets nothing, and neither can tell which happened from the
55
+ * response or how long it took. Only a malformed address is rejected, since
56
+ * that says nothing about anyone.
57
+ */
58
+ declare function forgotPasswordAction(email: string): Promise<Result>;
59
+ /**
60
+ * An admin sending a password reset to someone on the team, from the Team
61
+ * drawer. Unlike the self-service request this reports what happened — an admin
62
+ * is allowed to know, and needs to when it didn't work.
63
+ */
64
+ declare function sendPasswordResetAction(email: string): Promise<Result>;
49
65
  declare function loginAction(_prev: ActionState, formData: FormData): Promise<ActionState>;
50
66
  declare function logoutAction(): Promise<{
51
67
  success: boolean;
52
68
  }>;
53
69
  /**
54
- * Completes a session that arrived via an invite, magic-link, or
55
- * password-recovery redirect. Called by AuthRedirectSync (react/), the
70
+ * Completes a session that arrived via an invite or password-recovery link. Called by AuthRedirectSync (react/), the
56
71
  * generic client component that notices this kind of link and hands off
57
72
  * whatever it found in the URL — the actual token exchange is entirely the
58
73
  * auth adapter's job (see GRAuthAdapter.completeAuthRedirect); core only
@@ -74,9 +89,15 @@ declare function completeAuthRedirectAction(params: AuthRedirectParams): Promise
74
89
  * resolving them, which already happens independently of this.
75
90
  */
76
91
  declare function setPasswordAction(_prev: SetPasswordState, formData: FormData): Promise<SetPasswordState>;
77
- /** Dismisses the set-password prompt without setting one — see Admin.tsx. */
92
+ /**
93
+ * Dismisses the set-password step without setting a password.
94
+ *
95
+ * @deprecated Guardrail's own UI no longer offers this: passwords are the only
96
+ * way back in, so the step can't be skipped. Kept so an app's own UI built on
97
+ * it keeps working.
98
+ */
78
99
  declare function skipSetPasswordAction(): Promise<{
79
100
  success: boolean;
80
101
  }>;
81
102
 
82
- export { completeAuthRedirectAction, createAssetUpload, inviteMember, listAssets, listTeam, loginAction, logoutAction, removeMember, saveOverride, setFieldValue, setPasswordAction, skipSetPasswordAction, updateMemberRole };
103
+ export { completeAuthRedirectAction, createAssetUpload, forgotPasswordAction, inviteMember, listAssets, listTeam, loginAction, logoutAction, removeMember, saveOverride, sendPasswordResetAction, setFieldValue, setPasswordAction, skipSetPasswordAction, updateMemberRole };