@guard-rail/core 0.7.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +86 -41
  2. package/dist/adapters/supabase/auth.d.ts +13 -1
  3. package/dist/adapters/supabase/auth.js +42 -25
  4. package/dist/adapters/supabase/auth.js.map +1 -1
  5. package/dist/adapters/supabase/client.d.ts +16 -1
  6. package/dist/adapters/supabase/client.js +9 -0
  7. package/dist/adapters/supabase/client.js.map +1 -1
  8. package/dist/adapters/supabase/confirm.d.ts +5 -0
  9. package/dist/adapters/supabase/confirm.js +12 -1
  10. package/dist/adapters/supabase/confirm.js.map +1 -1
  11. package/dist/adapters/supabase/members.js +13 -2
  12. package/dist/adapters/supabase/members.js.map +1 -1
  13. package/dist/core/adapter-types.d.ts +80 -12
  14. package/dist/core/config.d.ts +2 -1
  15. package/dist/core/config.js +4 -0
  16. package/dist/core/config.js.map +1 -1
  17. package/dist/core/defs.d.ts +11 -1
  18. package/dist/core/defs.js +11 -1
  19. package/dist/core/defs.js.map +1 -1
  20. package/dist/core/types.d.ts +6 -0
  21. package/dist/email/resend.d.ts +80 -0
  22. package/dist/email/resend.js +118 -0
  23. package/dist/email/resend.js.map +1 -0
  24. package/dist/index.client.d.ts +6 -3
  25. package/dist/index.client.js +14 -0
  26. package/dist/index.client.js.map +1 -1
  27. package/dist/index.d.ts +3 -2
  28. package/dist/index.js +7 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/react/Admin.js +105 -107
  31. package/dist/react/Admin.js.map +1 -1
  32. package/dist/react/AuthDrawer.d.ts +23 -0
  33. package/dist/react/AuthDrawer.js +165 -0
  34. package/dist/react/AuthDrawer.js.map +1 -0
  35. package/dist/react/AuthRedirectSync.js +1 -1
  36. package/dist/react/AuthRedirectSync.js.map +1 -1
  37. package/dist/react/Icons.d.ts +2 -1
  38. package/dist/react/Icons.js +2 -0
  39. package/dist/react/Icons.js.map +1 -1
  40. package/dist/react/Team.d.ts +7 -1
  41. package/dist/react/Team.js +23 -3
  42. package/dist/react/Team.js.map +1 -1
  43. package/dist/server/access.d.ts +3 -1
  44. package/dist/server/access.js +5 -3
  45. package/dist/server/access.js.map +1 -1
  46. package/dist/server/actions.d.ts +25 -4
  47. package/dist/server/actions.js +118 -11
  48. package/dist/server/actions.js.map +1 -1
  49. package/dist/server/wrapper.js +2 -1
  50. package/dist/server/wrapper.js.map +1 -1
  51. package/package.json +8 -10
@@ -76,18 +76,45 @@ interface GRAuthAdapter {
76
76
  */
77
77
  signInUrl?: string;
78
78
  /**
79
- * Ask the provider to create an account for an invited address and contact
80
- * them. Optional — Guardrail falls back to the onInvite callback when the
81
- * provider cannot do it.
79
+ * Create the account and return a one-time link that lets the invitee choose
80
+ * a password, without sending anything — Guardrail hands it to the app's
81
+ * onInvite handler to email. Optional: an adapter that can't mint links
82
+ * leaves delivery entirely to onInvite, which then gets no `link`.
82
83
  *
83
84
  * Account creation stays the identity provider's job. Guardrail records who
84
85
  * is allowed to edit; it never owns credentials.
86
+ *
87
+ * `origin` is the site's own origin, so the link points back at the site
88
+ * (where AuthRedirectSync completes it) rather than at the provider — which
89
+ * is why no redirect allowlist is involved. `isNewUser` says whether this
90
+ * created an account or the address already had one. A new account's link
91
+ * lets them choose a password. An existing account's "link" is just the
92
+ * site's address, with no token: they already have a password, and an emailed
93
+ * link must not be a way to sign in.
85
94
  */
86
- inviteUser?: (email: string, options?: {
87
- redirectTo?: string;
95
+ createInviteLink?: (email: string, options: {
96
+ origin: string;
88
97
  }) => Promise<{
89
- success: boolean;
90
- error?: string;
98
+ success: true;
99
+ link: string;
100
+ isNewUser: boolean;
101
+ } | {
102
+ success: false;
103
+ error: string;
104
+ }>;
105
+ /**
106
+ * Mint a one-time "set a new password" link without sending anything;
107
+ * Guardrail hands it to onPasswordReset to email. Same shape and reasoning as
108
+ * createInviteLink. Without it, password resets can't be sent.
109
+ */
110
+ createPasswordResetLink?: (email: string, options: {
111
+ origin: string;
112
+ }) => Promise<{
113
+ success: true;
114
+ link: string;
115
+ } | {
116
+ success: false;
117
+ error: string;
91
118
  }>;
92
119
  /**
93
120
  * Return the current verified session, or null.
@@ -171,6 +198,16 @@ interface GRMembershipAdapter {
171
198
  success: boolean;
172
199
  error?: string;
173
200
  }>;
201
+ /**
202
+ * Whether this address is a member of the site — checked with no session,
203
+ * for the "forgot password" request that arrives before anyone is signed in.
204
+ * Optional because it needs privileged access to the membership store, which
205
+ * a normal anonymous session doesn't have; without it, Guardrail can't tell
206
+ * a member from anyone else and a reset goes to any address that asks, so an
207
+ * adapter that offers createPasswordResetLink should offer this too. Used
208
+ * only to decide whether to send, never to answer the requester.
209
+ */
210
+ emailHasAccess?: (siteId: string, email: string) => Promise<boolean>;
174
211
  /**
175
212
  * Bind a pending invitation to the signed-in user.
176
213
  *
@@ -247,6 +284,23 @@ interface GRInviteEvent {
247
284
  role: GRRole;
248
285
  siteId: string;
249
286
  invitedBy: string;
287
+ /**
288
+ * A one-time sign-in link for the invitee, present when the auth adapter can
289
+ * create one (see createInviteLink). Put it in the email. Absent for an
290
+ * adapter that can't, in which case delivering access is entirely the app's
291
+ * job.
292
+ */
293
+ link?: string;
294
+ /** Whether the invite created an account (true) or the address already had one. Set alongside `link`. */
295
+ isNewUser?: boolean;
296
+ }
297
+ interface GRPasswordResetEvent {
298
+ email: string;
299
+ siteId: string;
300
+ /** A one-time link that lets the person choose a new password. */
301
+ link: string;
302
+ /** "self" for someone who asked from the sign-in bar; otherwise the admin who sent it. */
303
+ requestedBy: string;
250
304
  }
251
305
  interface GRConfig {
252
306
  db: GRDBAdapter;
@@ -258,12 +312,26 @@ interface GRConfig {
258
312
  repeaters?: GRRepeaterAdapter;
259
313
  siteId: string | (() => string | Promise<string>);
260
314
  /**
261
- * Called when an admin invites someone. Guardrail records the pending
262
- * membership; delivering the invitation is the app's job, using whatever
263
- * mail provider it already has. In-process by design — a library running
264
- * inside the developer's server has no need for webhooks.
315
+ * How an invitation gets delivered. Guardrail records the pending membership
316
+ * and — when the auth adapter can (createInviteLink) — creates the account and
317
+ * a one-time link; this handler emails it, with whatever mail provider the app
318
+ * already has. `resendEmails()` is a ready-made one. In-process by design — a
319
+ * library running inside the developer's server has no need for webhooks.
320
+ *
321
+ * Required for invites to reach anyone: without it the admin is told the
322
+ * invite was saved but nothing sent it. Throwing reports the failure to the
323
+ * admin who sent the invite.
265
324
  */
266
325
  onInvite?: (event: GRInviteEvent) => void | Promise<void>;
326
+ /**
327
+ * Delivers a password-reset email — for "Forgot password?" on the sign-in
328
+ * bar, and for the reset an admin sends from the Team drawer. Needs an auth
329
+ * adapter that can mint the link (createPasswordResetLink); the event carries
330
+ * it. `resendEmails()` provides this too. Without it, resets can't be sent.
331
+ * Throwing is reported to the admin who sent it; a self-service request always
332
+ * looks successful to the requester, so a missing account can't be probed.
333
+ */
334
+ onPasswordReset?: (event: GRPasswordResetEvent) => void | Promise<void>;
267
335
  /**
268
336
  * Extra content rendered in the toolbar pill, next to Team and Log out.
269
337
  * Plain JSX, not a component reference or a structured item schema — the
@@ -285,4 +353,4 @@ interface GRConfig {
285
353
  prefetch?: false | GRPrefetch;
286
354
  }
287
355
 
288
- export type { AuthRedirectParams, AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRPrefetch, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget };
356
+ export type { AuthRedirectParams, AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRPasswordResetEvent, GRPrefetch, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget };
@@ -28,6 +28,7 @@ declare function getPrefetch(): {
28
28
  limit: number;
29
29
  } | null;
30
30
  declare function getInviteHandler(): GRConfig["onInvite"];
31
+ declare function getPasswordResetHandler(): GRConfig["onPasswordReset"];
31
32
  declare function resolveSiteId(): Promise<string>;
32
33
 
33
- export { DEFAULT_PREFETCH_LIMIT, configureGuardrail, getAuth, getDB, getInviteHandler, getMembers, getPrefetch, getRepeaters, getStorage, resolveSiteId };
34
+ export { DEFAULT_PREFETCH_LIMIT, configureGuardrail, getAuth, getDB, getInviteHandler, getMembers, getPasswordResetHandler, getPrefetch, getRepeaters, getStorage, resolveSiteId };
@@ -38,6 +38,9 @@ function getPrefetch() {
38
38
  function getInviteHandler() {
39
39
  return _config?.onInvite;
40
40
  }
41
+ function getPasswordResetHandler() {
42
+ return _config?.onPasswordReset;
43
+ }
41
44
  async function resolveSiteId() {
42
45
  const sid = _config?.siteId ?? "default";
43
46
  return typeof sid === "function" ? await sid() : sid || "default";
@@ -49,6 +52,7 @@ export {
49
52
  getDB,
50
53
  getInviteHandler,
51
54
  getMembers,
55
+ getPasswordResetHandler,
52
56
  getPrefetch,
53
57
  getRepeaters,
54
58
  getStorage,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/config.ts"],"sourcesContent":["import type {\n GRConfig,\n GRAuthAdapter,\n GRDBAdapter,\n GRMembershipAdapter,\n GRRepeaterAdapter,\n GRStorageAdapter,\n} from \"./adapter-types\";\n\nlet _config: GRConfig | null = null;\n\n/**\n * Registers the adapters Guardrail should use.\n *\n * Idempotent, and re-applied on every render by withGuardrail: Next.js does not\n * guarantee one shared module instance across the SSR graph, the server-action\n * graph, and the workers used for static generation, so config registered once\n * at startup is simply absent in some of them.\n *\n * Core has no knowledge of any particular backend — there is no default and no\n * fallback. An app names its adapters explicitly or Guardrail refuses to run.\n */\nexport function configureGuardrail(config: GRConfig): void {\n _config = config;\n}\n\nfunction required<K extends keyof GRConfig>(key: K): GRConfig[K] {\n const value = _config?.[key];\n if (!value) {\n throw new Error(\n `Guardrail: no ${String(key)} adapter configured. Pass one to ` +\n `withGuardrail(Layout, { db, auth, members }).`,\n );\n }\n return value;\n}\n\nexport function getDB(): GRDBAdapter {\n return required(\"db\") as GRDBAdapter;\n}\n\nexport function getAuth(): GRAuthAdapter {\n return required(\"auth\") as GRAuthAdapter;\n}\n\nexport function getMembers(): GRMembershipAdapter {\n return required(\"members\") as GRMembershipAdapter;\n}\n\n/** Optional — sites without editable images need no storage. */\nexport function getStorage(): GRStorageAdapter | undefined {\n return _config?.storage;\n}\n\n/** Throws with a clear message if no page on this site uses <GRepeater>. */\nexport function getRepeaters(): GRRepeaterAdapter {\n return required(\"repeaters\") as GRRepeaterAdapter;\n}\n\nexport const DEFAULT_PREFETCH_LIMIT = 500;\n\n/** Normalized preload settings, or null when preloading is off. */\nexport function getPrefetch(): { prefix?: string[]; limit: number } | null {\n const p = _config?.prefetch;\n if (p === false) return null;\n return {\n ...(p?.prefix?.length ? { prefix: p.prefix } : {}),\n limit: p?.limit ?? DEFAULT_PREFETCH_LIMIT,\n };\n}\n\nexport function getInviteHandler(): GRConfig[\"onInvite\"] {\n return _config?.onInvite;\n}\n\nexport async function resolveSiteId(): Promise<string> {\n const sid = _config?.siteId ?? \"default\";\n return typeof sid === \"function\" ? await sid() : sid || \"default\";\n}\n"],"mappings":"AASA,IAAI,UAA2B;AAaxB,SAAS,mBAAmB,QAAwB;AACzD,YAAU;AACZ;AAEA,SAAS,SAAmC,KAAqB;AAC/D,QAAM,QAAQ,UAAU,GAAG;AAC3B,MAAI,CAAC,OAAO;AACV,UAAM,IAAI;AAAA,MACR,iBAAiB,OAAO,GAAG,CAAC;AAAA,IAE9B;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,QAAqB;AACnC,SAAO,SAAS,IAAI;AACtB;AAEO,SAAS,UAAyB;AACvC,SAAO,SAAS,MAAM;AACxB;AAEO,SAAS,aAAkC;AAChD,SAAO,SAAS,SAAS;AAC3B;AAGO,SAAS,aAA2C;AACzD,SAAO,SAAS;AAClB;AAGO,SAAS,eAAkC;AAChD,SAAO,SAAS,WAAW;AAC7B;AAEO,MAAM,yBAAyB;AAG/B,SAAS,cAA2D;AACzE,QAAM,IAAI,SAAS;AACnB,MAAI,MAAM,MAAO,QAAO;AACxB,SAAO;AAAA,IACL,GAAI,GAAG,QAAQ,SAAS,EAAE,QAAQ,EAAE,OAAO,IAAI,CAAC;AAAA,IAChD,OAAO,GAAG,SAAS;AAAA,EACrB;AACF;AAEO,SAAS,mBAAyC;AACvD,SAAO,SAAS;AAClB;AAEA,eAAsB,gBAAiC;AACrD,QAAM,MAAM,SAAS,UAAU;AAC/B,SAAO,OAAO,QAAQ,aAAa,MAAM,IAAI,IAAI,OAAO;AAC1D;","names":[]}
1
+ {"version":3,"sources":["../../src/core/config.ts"],"sourcesContent":["import type {\n GRConfig,\n GRAuthAdapter,\n GRDBAdapter,\n GRMembershipAdapter,\n GRRepeaterAdapter,\n GRStorageAdapter,\n} from \"./adapter-types\";\n\nlet _config: GRConfig | null = null;\n\n/**\n * Registers the adapters Guardrail should use.\n *\n * Idempotent, and re-applied on every render by withGuardrail: Next.js does not\n * guarantee one shared module instance across the SSR graph, the server-action\n * graph, and the workers used for static generation, so config registered once\n * at startup is simply absent in some of them.\n *\n * Core has no knowledge of any particular backend — there is no default and no\n * fallback. An app names its adapters explicitly or Guardrail refuses to run.\n */\nexport function configureGuardrail(config: GRConfig): void {\n _config = config;\n}\n\nfunction required<K extends keyof GRConfig>(key: K): GRConfig[K] {\n const value = _config?.[key];\n if (!value) {\n throw new Error(\n `Guardrail: no ${String(key)} adapter configured. Pass one to ` +\n `withGuardrail(Layout, { db, auth, members }).`,\n );\n }\n return value;\n}\n\nexport function getDB(): GRDBAdapter {\n return required(\"db\") as GRDBAdapter;\n}\n\nexport function getAuth(): GRAuthAdapter {\n return required(\"auth\") as GRAuthAdapter;\n}\n\nexport function getMembers(): GRMembershipAdapter {\n return required(\"members\") as GRMembershipAdapter;\n}\n\n/** Optional — sites without editable images need no storage. */\nexport function getStorage(): GRStorageAdapter | undefined {\n return _config?.storage;\n}\n\n/** Throws with a clear message if no page on this site uses <GRepeater>. */\nexport function getRepeaters(): GRRepeaterAdapter {\n return required(\"repeaters\") as GRRepeaterAdapter;\n}\n\nexport const DEFAULT_PREFETCH_LIMIT = 500;\n\n/** Normalized preload settings, or null when preloading is off. */\nexport function getPrefetch(): { prefix?: string[]; limit: number } | null {\n const p = _config?.prefetch;\n if (p === false) return null;\n return {\n ...(p?.prefix?.length ? { prefix: p.prefix } : {}),\n limit: p?.limit ?? DEFAULT_PREFETCH_LIMIT,\n };\n}\n\nexport function getInviteHandler(): GRConfig[\"onInvite\"] {\n return _config?.onInvite;\n}\n\nexport function getPasswordResetHandler(): GRConfig[\"onPasswordReset\"] {\n return _config?.onPasswordReset;\n}\n\nexport async function resolveSiteId(): Promise<string> {\n const sid = _config?.siteId ?? \"default\";\n return typeof sid === \"function\" ? await sid() : sid || \"default\";\n}\n"],"mappings":"AASA,IAAI,UAA2B;AAaxB,SAAS,mBAAmB,QAAwB;AACzD,YAAU;AACZ;AAEA,SAAS,SAAmC,KAAqB;AAC/D,QAAM,QAAQ,UAAU,GAAG;AAC3B,MAAI,CAAC,OAAO;AACV,UAAM,IAAI;AAAA,MACR,iBAAiB,OAAO,GAAG,CAAC;AAAA,IAE9B;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,QAAqB;AACnC,SAAO,SAAS,IAAI;AACtB;AAEO,SAAS,UAAyB;AACvC,SAAO,SAAS,MAAM;AACxB;AAEO,SAAS,aAAkC;AAChD,SAAO,SAAS,SAAS;AAC3B;AAGO,SAAS,aAA2C;AACzD,SAAO,SAAS;AAClB;AAGO,SAAS,eAAkC;AAChD,SAAO,SAAS,WAAW;AAC7B;AAEO,MAAM,yBAAyB;AAG/B,SAAS,cAA2D;AACzE,QAAM,IAAI,SAAS;AACnB,MAAI,MAAM,MAAO,QAAO;AACxB,SAAO;AAAA,IACL,GAAI,GAAG,QAAQ,SAAS,EAAE,QAAQ,EAAE,OAAO,IAAI,CAAC;AAAA,IAChD,OAAO,GAAG,SAAS;AAAA,EACrB;AACF;AAEO,SAAS,mBAAyC;AACvD,SAAO,SAAS;AAClB;AAEO,SAAS,0BAAuD;AACrE,SAAO,SAAS;AAClB;AAEA,eAAsB,gBAAiC;AACrD,QAAM,MAAM,SAAS,UAAU;AAC/B,SAAO,OAAO,QAAQ,aAAa,MAAM,IAAI,IAAI,OAAO;AAC1D;","names":[]}
@@ -24,7 +24,17 @@ declare const GR_COOKIES: {
24
24
  * they explicitly skip.
25
25
  */
26
26
  readonly NEEDS_PASSWORD: "gr_needs_password";
27
+ /**
28
+ * Set server-side when an emailed link fails to complete — expired, already
29
+ * used, or malformed — so the next render can say so instead of leaving the
30
+ * person on a plain page wondering why nothing happened. Holds a short code
31
+ * ("expired" | "failed"), never the provider's own message. Short-lived and
32
+ * cleared when the message is dismissed.
33
+ */
34
+ readonly AUTH_ERROR: "gr_auth_error";
27
35
  };
36
+ /** How long a "that link didn't work" message waits to be seen. */
37
+ declare const GR_AUTH_ERROR_TTL_SECONDS: number;
28
38
  /**
29
39
  * How long the edit-mode cookie's "true"/"false" values last before
30
40
  * reverting to the session-derived default. Short on purpose: it only ever
@@ -72,4 +82,4 @@ declare const GR_TABLE = "gr_content";
72
82
  declare const GR_MEMBERS_TABLE = "gr_members";
73
83
  declare const GR_REPEATER_TABLE = "gr_repeater_items";
74
84
 
75
- export { GR_COOKIES, GR_EDIT_MODE_OFF_TTL_SECONDS, GR_EDIT_MODE_TTL_SECONDS, GR_MEMBERS_TABLE, GR_NEEDS_PASSWORD_TTL_SECONDS, GR_REPEATER_TABLE, GR_SCHEMA, GR_TABLE };
85
+ export { GR_AUTH_ERROR_TTL_SECONDS, GR_COOKIES, GR_EDIT_MODE_OFF_TTL_SECONDS, GR_EDIT_MODE_TTL_SECONDS, GR_MEMBERS_TABLE, GR_NEEDS_PASSWORD_TTL_SECONDS, GR_REPEATER_TABLE, GR_SCHEMA, GR_TABLE };
package/dist/core/defs.js CHANGED
@@ -23,8 +23,17 @@ const GR_COOKIES = {
23
23
  * instead of showing the normal toolbar. Cleared once they do, or once
24
24
  * they explicitly skip.
25
25
  */
26
- NEEDS_PASSWORD: "gr_needs_password"
26
+ NEEDS_PASSWORD: "gr_needs_password",
27
+ /**
28
+ * Set server-side when an emailed link fails to complete — expired, already
29
+ * used, or malformed — so the next render can say so instead of leaving the
30
+ * person on a plain page wondering why nothing happened. Holds a short code
31
+ * ("expired" | "failed"), never the provider's own message. Short-lived and
32
+ * cleared when the message is dismissed.
33
+ */
34
+ AUTH_ERROR: "gr_auth_error"
27
35
  };
36
+ const GR_AUTH_ERROR_TTL_SECONDS = 2 * 60;
28
37
  const GR_EDIT_MODE_TTL_SECONDS = 10 * 60;
29
38
  const GR_EDIT_MODE_OFF_TTL_SECONDS = 400 * 24 * 60 * 60;
30
39
  const GR_NEEDS_PASSWORD_TTL_SECONDS = 30 * 60;
@@ -33,6 +42,7 @@ const GR_TABLE = "gr_content";
33
42
  const GR_MEMBERS_TABLE = "gr_members";
34
43
  const GR_REPEATER_TABLE = "gr_repeater_items";
35
44
  export {
45
+ GR_AUTH_ERROR_TTL_SECONDS,
36
46
  GR_COOKIES,
37
47
  GR_EDIT_MODE_OFF_TTL_SECONDS,
38
48
  GR_EDIT_MODE_TTL_SECONDS,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/defs.ts"],"sourcesContent":["export const GR_COOKIES = {\n /**\n * Signals intent to edit. Carries no authority on its own — every write\n * still goes through a real, server-verified role check, independent of\n * this — but it does decide whether the admin chrome (the sign-in bar, or\n * the toolbar) renders at all, so it is not harmless either: a stale copy\n * of it is a real, if low-stakes, exposure (\"there's a CMS here\") to\n * whoever's browser it's sitting in next, not just its original visitor.\n *\n * Values: \"true\" (show chrome — the short-lived grace window below, for\n * someone not yet authenticated to reach the sign-in form), \"false\"\n * (explicitly suppressed, e.g. ?edit=false previewing without chrome while\n * still signed in), or absent. Absent defers entirely to whether a real\n * session exists (see withGuardrail) — an authenticated editor's chrome\n * persists for as long as their actual session does, with no separate\n * timer of its own to fall out of sync with it.\n */\n EDIT_MODE: \"gr_edit_mode\",\n /**\n * Set server-side the moment a redirect-carried session (an invite or\n * password-recovery link) completes, so the next render — which has no\n * other way to know that just happened — prompts to set a password\n * instead of showing the normal toolbar. Cleared once they do, or once\n * they explicitly skip.\n */\n NEEDS_PASSWORD: \"gr_needs_password\",\n} as const;\n\n/**\n * How long the edit-mode cookie's \"true\"/\"false\" values last before\n * reverting to the session-derived default. Short on purpose: it only ever\n * needs to outlive the handful of clicks between landing on ?edit=true and\n * actually typing a password (or, for ?edit=false, a quick chrome-free\n * preview) — not a whole working session, which persists through real auth\n * instead. Bounds how long a stray copy of this cookie can show anything to\n * whoever's browser it's left in, self-healing without needing an explicit\n * logout.\n */\nexport const GR_EDIT_MODE_TTL_SECONDS = 10 * 60;\n\n/**\n * How long an explicit \"off\" (the toolbar's exit button, ?edit=false, or the\n * sign-in bar's dismiss) is remembered. Long, unlike \"true\" above, because the\n * two fail differently: a stale \"true\" can put a sign-in bar in front of\n * someone who never asked for it, so it's kept short — a stale \"false\" only\n * ever hides chrome, so there's nothing to bound. It has to last as long as\n * the session it sits on top of: an editor who exited stays exited (instead of\n * the overlay reappearing by itself once a short timer lapsed) until they ask\n * for edit mode again with ?edit=true. Matches the Supabase session cookie's\n * 400 days.\n */\nexport const GR_EDIT_MODE_OFF_TTL_SECONDS = 400 * 24 * 60 * 60;\n\n/**\n * How long the set-a-password prompt stays up after an invite/recovery link\n * completes. Longer than the edit-mode window above — picking a password\n * takes more than one click — but still bounded, rather than nagging\n * indefinitely if it's skipped and the cookie is somehow never cleared.\n */\nexport const GR_NEEDS_PASSWORD_TTL_SECONDS = 30 * 60;\n\n/**\n * Dedicated Postgres schema, not `public`. A name prefix alone only guards\n * against accidental collisions; it does nothing against a `supabase db\n * reset`, a broad `DROP ... CASCADE`, or a migration tool that diffs and\n * \"reconciles\" everything in `public` as if it owns that namespace. A\n * separate schema keeps Guardrail's tables outside that blast radius —\n * the same reason auth.* and storage.* are their own schemas, not public.*.\n *\n * Requires \"guardrail\" to be added under Settings > API > Exposed schemas in\n * the Supabase dashboard — PostgREST only serves schemas on that list, and no\n * migration can add itself to it.\n */\nexport const GR_SCHEMA = \"guardrail\";\n\nexport const GR_TABLE = \"gr_content\";\nexport const GR_MEMBERS_TABLE = \"gr_members\";\nexport const GR_REPEATER_TABLE = \"gr_repeater_items\";\n"],"mappings":"AAAO,MAAM,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBxB,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQX,gBAAgB;AAClB;AAYO,MAAM,2BAA2B,KAAK;AAatC,MAAM,+BAA+B,MAAM,KAAK,KAAK;AAQrD,MAAM,gCAAgC,KAAK;AAc3C,MAAM,YAAY;AAElB,MAAM,WAAW;AACjB,MAAM,mBAAmB;AACzB,MAAM,oBAAoB;","names":[]}
1
+ {"version":3,"sources":["../../src/core/defs.ts"],"sourcesContent":["export const GR_COOKIES = {\n /**\n * Signals intent to edit. Carries no authority on its own — every write\n * still goes through a real, server-verified role check, independent of\n * this — but it does decide whether the admin chrome (the sign-in bar, or\n * the toolbar) renders at all, so it is not harmless either: a stale copy\n * of it is a real, if low-stakes, exposure (\"there's a CMS here\") to\n * whoever's browser it's sitting in next, not just its original visitor.\n *\n * Values: \"true\" (show chrome — the short-lived grace window below, for\n * someone not yet authenticated to reach the sign-in form), \"false\"\n * (explicitly suppressed, e.g. ?edit=false previewing without chrome while\n * still signed in), or absent. Absent defers entirely to whether a real\n * session exists (see withGuardrail) — an authenticated editor's chrome\n * persists for as long as their actual session does, with no separate\n * timer of its own to fall out of sync with it.\n */\n EDIT_MODE: \"gr_edit_mode\",\n /**\n * Set server-side the moment a redirect-carried session (an invite or\n * password-recovery link) completes, so the next render — which has no\n * other way to know that just happened — prompts to set a password\n * instead of showing the normal toolbar. Cleared once they do, or once\n * they explicitly skip.\n */\n NEEDS_PASSWORD: \"gr_needs_password\",\n /**\n * Set server-side when an emailed link fails to complete — expired, already\n * used, or malformed — so the next render can say so instead of leaving the\n * person on a plain page wondering why nothing happened. Holds a short code\n * (\"expired\" | \"failed\"), never the provider's own message. Short-lived and\n * cleared when the message is dismissed.\n */\n AUTH_ERROR: \"gr_auth_error\",\n} as const;\n\n/** How long a \"that link didn't work\" message waits to be seen. */\nexport const GR_AUTH_ERROR_TTL_SECONDS = 2 * 60;\n\n/**\n * How long the edit-mode cookie's \"true\"/\"false\" values last before\n * reverting to the session-derived default. Short on purpose: it only ever\n * needs to outlive the handful of clicks between landing on ?edit=true and\n * actually typing a password (or, for ?edit=false, a quick chrome-free\n * preview) — not a whole working session, which persists through real auth\n * instead. Bounds how long a stray copy of this cookie can show anything to\n * whoever's browser it's left in, self-healing without needing an explicit\n * logout.\n */\nexport const GR_EDIT_MODE_TTL_SECONDS = 10 * 60;\n\n/**\n * How long an explicit \"off\" (the toolbar's exit button, ?edit=false, or the\n * sign-in bar's dismiss) is remembered. Long, unlike \"true\" above, because the\n * two fail differently: a stale \"true\" can put a sign-in bar in front of\n * someone who never asked for it, so it's kept short — a stale \"false\" only\n * ever hides chrome, so there's nothing to bound. It has to last as long as\n * the session it sits on top of: an editor who exited stays exited (instead of\n * the overlay reappearing by itself once a short timer lapsed) until they ask\n * for edit mode again with ?edit=true. Matches the Supabase session cookie's\n * 400 days.\n */\nexport const GR_EDIT_MODE_OFF_TTL_SECONDS = 400 * 24 * 60 * 60;\n\n/**\n * How long the set-a-password prompt stays up after an invite/recovery link\n * completes. Longer than the edit-mode window above — picking a password\n * takes more than one click — but still bounded, rather than nagging\n * indefinitely if it's skipped and the cookie is somehow never cleared.\n */\nexport const GR_NEEDS_PASSWORD_TTL_SECONDS = 30 * 60;\n\n/**\n * Dedicated Postgres schema, not `public`. A name prefix alone only guards\n * against accidental collisions; it does nothing against a `supabase db\n * reset`, a broad `DROP ... CASCADE`, or a migration tool that diffs and\n * \"reconciles\" everything in `public` as if it owns that namespace. A\n * separate schema keeps Guardrail's tables outside that blast radius —\n * the same reason auth.* and storage.* are their own schemas, not public.*.\n *\n * Requires \"guardrail\" to be added under Settings > API > Exposed schemas in\n * the Supabase dashboard — PostgREST only serves schemas on that list, and no\n * migration can add itself to it.\n */\nexport const GR_SCHEMA = \"guardrail\";\n\nexport const GR_TABLE = \"gr_content\";\nexport const GR_MEMBERS_TABLE = \"gr_members\";\nexport const GR_REPEATER_TABLE = \"gr_repeater_items\";\n"],"mappings":"AAAO,MAAM,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBxB,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQX,gBAAgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQhB,YAAY;AACd;AAGO,MAAM,4BAA4B,IAAI;AAYtC,MAAM,2BAA2B,KAAK;AAatC,MAAM,+BAA+B,MAAM,KAAK,KAAK;AAQrD,MAAM,gCAAgC,KAAK;AAc3C,MAAM,YAAY;AAElB,MAAM,WAAW;AACjB,MAAM,mBAAmB;AACzB,MAAM,oBAAoB;","names":[]}
@@ -116,6 +116,12 @@ interface GRState {
116
116
  /** A redirect-carried session (an invite or recovery link) just completed
117
117
  * and has no password yet — see completeAuthRedirect. */
118
118
  needsPassword: boolean;
119
+ /**
120
+ * An emailed link just failed to complete — see GR_COOKIES.AUTH_ERROR. Set
121
+ * only for the render right after it happens, so the chrome can explain it
122
+ * and offer a fresh link.
123
+ */
124
+ authError?: "expired" | "failed";
119
125
  siteId: string;
120
126
  /** Null when the signed-in user holds no role on this site. */
121
127
  role: GRRole | null;
@@ -0,0 +1,80 @@
1
+ import { GRInviteEvent, GRPasswordResetEvent } from '../core/adapter-types.js';
2
+ import 'react';
3
+ import '../core/types.js';
4
+
5
+ /** What gets sent: the pieces of one email. */
6
+ interface ResendInviteContent {
7
+ subject: string;
8
+ html: string;
9
+ /** Plain-text alternative. Worth providing — some clients and spam filters prefer it. */
10
+ text?: string;
11
+ }
12
+ /** What a custom invite template is given: the invite, plus the link and name to put in it. */
13
+ type ResendInviteTemplateEvent = GRInviteEvent & {
14
+ link: string;
15
+ siteName: string;
16
+ };
17
+ /** What a custom password-reset template is given. */
18
+ type ResendResetTemplateEvent = GRPasswordResetEvent & {
19
+ siteName: string;
20
+ };
21
+ interface ResendInviteOptions {
22
+ /**
23
+ * "Name <hello@yourdomain.com>". The domain has to be verified in Resend, or
24
+ * Resend rejects the send.
25
+ */
26
+ from: string;
27
+ /** Resend API key. Defaults to `process.env.RESEND_API_KEY`, read when an email is sent. */
28
+ apiKey?: string;
29
+ /** Named in the default emails. Defaults to the site id. */
30
+ siteName?: string;
31
+ /**
32
+ * Replaces the default invite email entirely — your own subject, markup, brand.
33
+ * Called for both a brand-new account (`isNewUser` true: the link lets them
34
+ * choose a password) and an existing one (`isNewUser` false: the link just
35
+ * opens the site, where they sign in with the password they already have).
36
+ */
37
+ render?: (event: ResendInviteTemplateEvent) => ResendInviteContent | Promise<ResendInviteContent>;
38
+ /** Override the endpoint, e.g. for a proxy or a test server. */
39
+ apiUrl?: string;
40
+ }
41
+ interface ResendEmailOptions extends ResendInviteOptions {
42
+ /** Replaces the default password-reset email. */
43
+ renderReset?: (event: ResendResetTemplateEvent) => ResendInviteContent | Promise<ResendInviteContent>;
44
+ }
45
+ /**
46
+ * An `onInvite` handler that emails the invitee through Resend.
47
+ *
48
+ * withGuardrail(Layout, {
49
+ * ...supabaseAdapters({ url, anonKey, serviceRoleKey }),
50
+ * onInvite: resendInvite({ from: "Acme <hello@acme.com>", siteName: "Acme" }),
51
+ * });
52
+ *
53
+ * Needs an auth adapter that can create links (the Supabase adapter can once
54
+ * it's given a serviceRoleKey); without a link there is nothing to send, and it
55
+ * throws a message saying so. Throwing is how failures reach the admin who
56
+ * clicked Invite — Resend rejecting the sender domain, say.
57
+ *
58
+ * To send password-reset emails the same way, use resendEmails() instead, which
59
+ * supplies both handlers.
60
+ *
61
+ * Calls Resend's HTTP API directly, so there is no SDK to install. To send
62
+ * through anything else, write your own onInvite — the event carries the same
63
+ * `link`.
64
+ */
65
+ declare function resendInvite(options: ResendInviteOptions): (event: GRInviteEvent) => Promise<void>;
66
+ /**
67
+ * Both email handlers, backed by Resend: spread it into withGuardrail() and
68
+ * invites *and* password resets go out through your own domain.
69
+ *
70
+ * withGuardrail(Layout, {
71
+ * ...supabaseAdapters({ url, anonKey, serviceRoleKey }),
72
+ * ...resendEmails({ from: "Acme <hello@acme.com>", siteName: "Acme" }),
73
+ * });
74
+ */
75
+ declare function resendEmails(options: ResendEmailOptions): {
76
+ onInvite: (event: GRInviteEvent) => Promise<void>;
77
+ onPasswordReset: (event: GRPasswordResetEvent) => Promise<void>;
78
+ };
79
+
80
+ export { type ResendEmailOptions, type ResendInviteContent, type ResendInviteOptions, type ResendInviteTemplateEvent, type ResendResetTemplateEvent, resendEmails, resendInvite };
@@ -0,0 +1,118 @@
1
+ const RESEND_URL = "https://api.resend.com/emails";
2
+ async function send(options, to, content) {
3
+ const apiKey = options.apiKey ?? process.env.RESEND_API_KEY;
4
+ if (!apiKey) throw new Error("RESEND_API_KEY is not set.");
5
+ const res = await fetch(options.apiUrl ?? RESEND_URL, {
6
+ method: "POST",
7
+ headers: {
8
+ Authorization: `Bearer ${apiKey}`,
9
+ "Content-Type": "application/json"
10
+ },
11
+ body: JSON.stringify({
12
+ from: options.from,
13
+ to: [to],
14
+ subject: content.subject,
15
+ html: content.html,
16
+ ...content.text ? { text: content.text } : {}
17
+ })
18
+ });
19
+ if (!res.ok) {
20
+ const detail = await res.json().then(
21
+ (body) => body?.message,
22
+ () => void 0
23
+ );
24
+ throw new Error(`Resend rejected the email (${res.status})${detail ? `: ${detail}` : "."}`);
25
+ }
26
+ }
27
+ async function sendInvite(options, event) {
28
+ if (!event.link) {
29
+ throw new Error(
30
+ "resendInvite needs a sign-in link, but the auth adapter didn't provide one \u2014 pass serviceRoleKey to supabaseAdapters() so it can create them."
31
+ );
32
+ }
33
+ const templateEvent = {
34
+ ...event,
35
+ link: event.link,
36
+ siteName: options.siteName ?? event.siteId
37
+ };
38
+ await send(options, event.email, await (options.render ?? defaultInviteEmail)(templateEvent));
39
+ }
40
+ function resendInvite(options) {
41
+ return (event) => sendInvite(options, event);
42
+ }
43
+ function resendEmails(options) {
44
+ return {
45
+ onInvite: (event) => sendInvite(options, event),
46
+ onPasswordReset: async (event) => {
47
+ const templateEvent = {
48
+ ...event,
49
+ siteName: options.siteName ?? event.siteId
50
+ };
51
+ await send(options, event.email, await (options.renderReset ?? defaultResetEmail)(templateEvent));
52
+ }
53
+ };
54
+ }
55
+ function escapeHtml(value) {
56
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
57
+ }
58
+ function layout(parts) {
59
+ const html = `<!doctype html>
60
+ <html>
61
+ <body style="margin:0;padding:24px;background:#f6f6f6;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:#111;">
62
+ <div style="max-width:480px;margin:0 auto;background:#fff;border-radius:8px;padding:32px;">
63
+ <h1 style="margin:0 0 12px;font-size:20px;">${escapeHtml(parts.subject)}</h1>
64
+ <p style="margin:0 0 24px;line-height:1.5;">${escapeHtml(parts.intro)}</p>
65
+ <p style="margin:0 0 24px;">
66
+ <a href="${escapeHtml(parts.link)}" style="display:inline-block;background:#111;color:#fff;text-decoration:none;padding:12px 20px;border-radius:6px;font-weight:600;">${escapeHtml(parts.action)}</a>
67
+ </p>
68
+ <p style="margin:0;font-size:13px;line-height:1.5;color:#666;">${escapeHtml(parts.footnote)}</p>
69
+ </div>
70
+ </body>
71
+ </html>`;
72
+ const text = `${parts.subject}
73
+
74
+ ${parts.intro}
75
+
76
+ ${parts.action}: ${parts.link}
77
+
78
+ ${parts.footnote}
79
+ `;
80
+ return { subject: parts.subject, html, text };
81
+ }
82
+ function inviteEmail(event) {
83
+ const site = event.siteName;
84
+ const who = event.invitedBy.includes("@") ? event.invitedBy : "An admin";
85
+ if (event.isNewUser === false) {
86
+ return layout({
87
+ subject: `You've been added to ${site}`,
88
+ intro: `${who} added you to ${site} as an ${event.role}. You already have an account, so sign in with the password you already use.`,
89
+ action: `Open ${site}`,
90
+ link: event.link,
91
+ footnote: `Forgot your password? Choose "Forgot password?" on the sign-in bar and we'll email you a link to choose a new one.`
92
+ });
93
+ }
94
+ return layout({
95
+ subject: `You're invited to edit ${site}`,
96
+ intro: `${who} invited you to edit ${site} as an ${event.role}. Choose a password to finish setting up your account.`,
97
+ action: "Set your password",
98
+ link: event.link,
99
+ footnote: "This link works once and expires, so open it on the device you'll use. After you set a password, that's how you'll sign in."
100
+ });
101
+ }
102
+ const defaultInviteEmail = inviteEmail;
103
+ function defaultResetEmail(event) {
104
+ const site = event.siteName;
105
+ const intro = event.requestedBy === "self" ? `You asked to reset your password for ${site}. Use the button below to choose a new one.` : `An admin of ${site} sent you a link to choose a new password.`;
106
+ return layout({
107
+ subject: `Reset your password for ${site}`,
108
+ intro,
109
+ action: "Choose a new password",
110
+ link: event.link,
111
+ footnote: "This link works once and expires. If you weren't expecting this, you can ignore the email and your password won't change."
112
+ });
113
+ }
114
+ export {
115
+ resendEmails,
116
+ resendInvite
117
+ };
118
+ //# sourceMappingURL=resend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/email/resend.ts"],"sourcesContent":["import type { GRInviteEvent, GRPasswordResetEvent } from \"../core/adapter-types\";\n\n/** What gets sent: the pieces of one email. */\nexport interface ResendInviteContent {\n subject: string;\n html: string;\n /** Plain-text alternative. Worth providing — some clients and spam filters prefer it. */\n text?: string;\n}\n\n/** What a custom invite template is given: the invite, plus the link and name to put in it. */\nexport type ResendInviteTemplateEvent = GRInviteEvent & {\n link: string;\n siteName: string;\n};\n\n/** What a custom password-reset template is given. */\nexport type ResendResetTemplateEvent = GRPasswordResetEvent & {\n siteName: string;\n};\n\nexport interface ResendInviteOptions {\n /**\n * \"Name <hello@yourdomain.com>\". The domain has to be verified in Resend, or\n * Resend rejects the send.\n */\n from: string;\n /** Resend API key. Defaults to `process.env.RESEND_API_KEY`, read when an email is sent. */\n apiKey?: string;\n /** Named in the default emails. Defaults to the site id. */\n siteName?: string;\n /**\n * Replaces the default invite email entirely — your own subject, markup, brand.\n * Called for both a brand-new account (`isNewUser` true: the link lets them\n * choose a password) and an existing one (`isNewUser` false: the link just\n * opens the site, where they sign in with the password they already have).\n */\n render?: (event: ResendInviteTemplateEvent) => ResendInviteContent | Promise<ResendInviteContent>;\n /** Override the endpoint, e.g. for a proxy or a test server. */\n apiUrl?: string;\n}\n\nexport interface ResendEmailOptions extends ResendInviteOptions {\n /** Replaces the default password-reset email. */\n renderReset?: (event: ResendResetTemplateEvent) => ResendInviteContent | Promise<ResendInviteContent>;\n}\n\nconst RESEND_URL = \"https://api.resend.com/emails\";\n\nasync function send(\n options: ResendInviteOptions,\n to: string,\n content: ResendInviteContent,\n): Promise<void> {\n const apiKey = options.apiKey ?? process.env.RESEND_API_KEY;\n if (!apiKey) throw new Error(\"RESEND_API_KEY is not set.\");\n\n const res = await fetch(options.apiUrl ?? RESEND_URL, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n from: options.from,\n to: [to],\n subject: content.subject,\n html: content.html,\n ...(content.text ? { text: content.text } : {}),\n }),\n });\n\n if (!res.ok) {\n const detail = await res.json().then(\n (body: { message?: string }) => body?.message,\n () => undefined,\n );\n throw new Error(`Resend rejected the email (${res.status})${detail ? `: ${detail}` : \".\"}`);\n }\n}\n\nasync function sendInvite(options: ResendInviteOptions, event: GRInviteEvent): Promise<void> {\n if (!event.link) {\n throw new Error(\n \"resendInvite needs a sign-in link, but the auth adapter didn't provide one — \" +\n \"pass serviceRoleKey to supabaseAdapters() so it can create them.\",\n );\n }\n const templateEvent: ResendInviteTemplateEvent = {\n ...event,\n link: event.link,\n siteName: options.siteName ?? event.siteId,\n };\n await send(options, event.email, await (options.render ?? defaultInviteEmail)(templateEvent));\n}\n\n/**\n * An `onInvite` handler that emails the invitee through Resend.\n *\n * withGuardrail(Layout, {\n * ...supabaseAdapters({ url, anonKey, serviceRoleKey }),\n * onInvite: resendInvite({ from: \"Acme <hello@acme.com>\", siteName: \"Acme\" }),\n * });\n *\n * Needs an auth adapter that can create links (the Supabase adapter can once\n * it's given a serviceRoleKey); without a link there is nothing to send, and it\n * throws a message saying so. Throwing is how failures reach the admin who\n * clicked Invite — Resend rejecting the sender domain, say.\n *\n * To send password-reset emails the same way, use resendEmails() instead, which\n * supplies both handlers.\n *\n * Calls Resend's HTTP API directly, so there is no SDK to install. To send\n * through anything else, write your own onInvite — the event carries the same\n * `link`.\n */\nexport function resendInvite(options: ResendInviteOptions) {\n return (event: GRInviteEvent): Promise<void> => sendInvite(options, event);\n}\n\n/**\n * Both email handlers, backed by Resend: spread it into withGuardrail() and\n * invites *and* password resets go out through your own domain.\n *\n * withGuardrail(Layout, {\n * ...supabaseAdapters({ url, anonKey, serviceRoleKey }),\n * ...resendEmails({ from: \"Acme <hello@acme.com>\", siteName: \"Acme\" }),\n * });\n */\nexport function resendEmails(options: ResendEmailOptions) {\n return {\n onInvite: (event: GRInviteEvent): Promise<void> => sendInvite(options, event),\n onPasswordReset: async (event: GRPasswordResetEvent): Promise<void> => {\n const templateEvent: ResendResetTemplateEvent = {\n ...event,\n siteName: options.siteName ?? event.siteId,\n };\n await send(options, event.email, await (options.renderReset ?? defaultResetEmail)(templateEvent));\n },\n };\n}\n\nfunction escapeHtml(value: string): string {\n return value\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#39;\");\n}\n\n/**\n * One plain, readable-everywhere email: a heading, a sentence, one button, a\n * footnote. Interpolated values are escaped — the site name and inviter come\n * from config and from a person, and neither belongs in markup unescaped.\n */\nfunction layout(parts: {\n subject: string;\n intro: string;\n action: string;\n link: string;\n footnote: string;\n}): ResendInviteContent {\n const html = `<!doctype html>\n<html>\n <body style=\"margin:0;padding:24px;background:#f6f6f6;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:#111;\">\n <div style=\"max-width:480px;margin:0 auto;background:#fff;border-radius:8px;padding:32px;\">\n <h1 style=\"margin:0 0 12px;font-size:20px;\">${escapeHtml(parts.subject)}</h1>\n <p style=\"margin:0 0 24px;line-height:1.5;\">${escapeHtml(parts.intro)}</p>\n <p style=\"margin:0 0 24px;\">\n <a href=\"${escapeHtml(parts.link)}\" style=\"display:inline-block;background:#111;color:#fff;text-decoration:none;padding:12px 20px;border-radius:6px;font-weight:600;\">${escapeHtml(parts.action)}</a>\n </p>\n <p style=\"margin:0;font-size:13px;line-height:1.5;color:#666;\">${escapeHtml(parts.footnote)}</p>\n </div>\n </body>\n</html>`;\n const text = `${parts.subject}\\n\\n${parts.intro}\\n\\n${parts.action}: ${parts.link}\\n\\n${parts.footnote}\\n`;\n return { subject: parts.subject, html, text };\n}\n\nfunction inviteEmail(event: ResendInviteTemplateEvent): ResendInviteContent {\n const site = event.siteName;\n const who = event.invitedBy.includes(\"@\") ? event.invitedBy : \"An admin\";\n\n if (event.isNewUser === false) {\n return layout({\n subject: `You've been added to ${site}`,\n intro: `${who} added you to ${site} as an ${event.role}. You already have an account, so sign in with the password you already use.`,\n action: `Open ${site}`,\n link: event.link,\n footnote: `Forgot your password? Choose \"Forgot password?\" on the sign-in bar and we'll email you a link to choose a new one.`,\n });\n }\n return layout({\n subject: `You're invited to edit ${site}`,\n intro: `${who} invited you to edit ${site} as an ${event.role}. Choose a password to finish setting up your account.`,\n action: \"Set your password\",\n link: event.link,\n footnote: \"This link works once and expires, so open it on the device you'll use. After you set a password, that's how you'll sign in.\",\n });\n}\n\nconst defaultInviteEmail = inviteEmail;\n\nfunction defaultResetEmail(event: ResendResetTemplateEvent): ResendInviteContent {\n const site = event.siteName;\n const intro =\n event.requestedBy === \"self\"\n ? `You asked to reset your password for ${site}. Use the button below to choose a new one.`\n : `An admin of ${site} sent you a link to choose a new password.`;\n return layout({\n subject: `Reset your password for ${site}`,\n intro,\n action: \"Choose a new password\",\n link: event.link,\n footnote: \"This link works once and expires. If you weren't expecting this, you can ignore the email and your password won't change.\",\n });\n}\n"],"mappings":"AA+CA,MAAM,aAAa;AAEnB,eAAe,KACb,SACA,IACA,SACe;AACf,QAAM,SAAS,QAAQ,UAAU,QAAQ,IAAI;AAC7C,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,4BAA4B;AAEzD,QAAM,MAAM,MAAM,MAAM,QAAQ,UAAU,YAAY;AAAA,IACpD,QAAQ;AAAA,IACR,SAAS;AAAA,MACP,eAAe,UAAU,MAAM;AAAA,MAC/B,gBAAgB;AAAA,IAClB;AAAA,IACA,MAAM,KAAK,UAAU;AAAA,MACnB,MAAM,QAAQ;AAAA,MACd,IAAI,CAAC,EAAE;AAAA,MACP,SAAS,QAAQ;AAAA,MACjB,MAAM,QAAQ;AAAA,MACd,GAAI,QAAQ,OAAO,EAAE,MAAM,QAAQ,KAAK,IAAI,CAAC;AAAA,IAC/C,CAAC;AAAA,EACH,CAAC;AAED,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,SAAS,MAAM,IAAI,KAAK,EAAE;AAAA,MAC9B,CAAC,SAA+B,MAAM;AAAA,MACtC,MAAM;AAAA,IACR;AACA,UAAM,IAAI,MAAM,8BAA8B,IAAI,MAAM,IAAI,SAAS,KAAK,MAAM,KAAK,GAAG,EAAE;AAAA,EAC5F;AACF;AAEA,eAAe,WAAW,SAA8B,OAAqC;AAC3F,MAAI,CAAC,MAAM,MAAM;AACf,UAAM,IAAI;AAAA,MACR;AAAA,IAEF;AAAA,EACF;AACA,QAAM,gBAA2C;AAAA,IAC/C,GAAG;AAAA,IACH,MAAM,MAAM;AAAA,IACZ,UAAU,QAAQ,YAAY,MAAM;AAAA,EACtC;AACA,QAAM,KAAK,SAAS,MAAM,OAAO,OAAO,QAAQ,UAAU,oBAAoB,aAAa,CAAC;AAC9F;AAsBO,SAAS,aAAa,SAA8B;AACzD,SAAO,CAAC,UAAwC,WAAW,SAAS,KAAK;AAC3E;AAWO,SAAS,aAAa,SAA6B;AACxD,SAAO;AAAA,IACL,UAAU,CAAC,UAAwC,WAAW,SAAS,KAAK;AAAA,IAC5E,iBAAiB,OAAO,UAA+C;AACrE,YAAM,gBAA0C;AAAA,QAC9C,GAAG;AAAA,QACH,UAAU,QAAQ,YAAY,MAAM;AAAA,MACtC;AACA,YAAM,KAAK,SAAS,MAAM,OAAO,OAAO,QAAQ,eAAe,mBAAmB,aAAa,CAAC;AAAA,IAClG;AAAA,EACF;AACF;AAEA,SAAS,WAAW,OAAuB;AACzC,SAAO,MACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ,EACtB,QAAQ,MAAM,OAAO;AAC1B;AAOA,SAAS,OAAO,OAMQ;AACtB,QAAM,OAAO;AAAA;AAAA;AAAA;AAAA,oDAIqC,WAAW,MAAM,OAAO,CAAC;AAAA,oDACzB,WAAW,MAAM,KAAK,CAAC;AAAA;AAAA,mBAExD,WAAW,MAAM,IAAI,CAAC,uIAAuI,WAAW,MAAM,MAAM,CAAC;AAAA;AAAA,uEAEjI,WAAW,MAAM,QAAQ,CAAC;AAAA;AAAA;AAAA;AAI/F,QAAM,OAAO,GAAG,MAAM,OAAO;AAAA;AAAA,EAAO,MAAM,KAAK;AAAA;AAAA,EAAO,MAAM,MAAM,KAAK,MAAM,IAAI;AAAA;AAAA,EAAO,MAAM,QAAQ;AAAA;AACtG,SAAO,EAAE,SAAS,MAAM,SAAS,MAAM,KAAK;AAC9C;AAEA,SAAS,YAAY,OAAuD;AAC1E,QAAM,OAAO,MAAM;AACnB,QAAM,MAAM,MAAM,UAAU,SAAS,GAAG,IAAI,MAAM,YAAY;AAE9D,MAAI,MAAM,cAAc,OAAO;AAC7B,WAAO,OAAO;AAAA,MACZ,SAAS,wBAAwB,IAAI;AAAA,MACrC,OAAO,GAAG,GAAG,iBAAiB,IAAI,UAAU,MAAM,IAAI;AAAA,MACtD,QAAQ,QAAQ,IAAI;AAAA,MACpB,MAAM,MAAM;AAAA,MACZ,UAAU;AAAA,IACZ,CAAC;AAAA,EACH;AACA,SAAO,OAAO;AAAA,IACZ,SAAS,0BAA0B,IAAI;AAAA,IACvC,OAAO,GAAG,GAAG,wBAAwB,IAAI,UAAU,MAAM,IAAI;AAAA,IAC7D,QAAQ;AAAA,IACR,MAAM,MAAM;AAAA,IACZ,UAAU;AAAA,EACZ,CAAC;AACH;AAEA,MAAM,qBAAqB;AAE3B,SAAS,kBAAkB,OAAsD;AAC/E,QAAM,OAAO,MAAM;AACnB,QAAM,QACJ,MAAM,gBAAgB,SAClB,wCAAwC,IAAI,gDAC5C,eAAe,IAAI;AACzB,SAAO,OAAO;AAAA,IACZ,SAAS,2BAA2B,IAAI;AAAA,IACxC;AAAA,IACA,QAAQ;AAAA,IACR,MAAM,MAAM;AAAA,IACZ,UAAU;AAAA,EACZ,CAAC;AACH;","names":[]}
@@ -1,10 +1,11 @@
1
1
  export { GRail } from './react/GRail/universal.js';
2
2
  export { useGRail } from './react/Provider.js';
3
- export { createAssetUpload, inviteMember, listAssets, listTeam, removeMember, setFieldValue, setPasswordAction, skipSetPasswordAction, updateMemberRole } from './server/actions.js';
3
+ export { createAssetUpload, forgotPasswordAction, inviteMember, listAssets, listTeam, removeMember, sendPasswordResetAction, setFieldValue, setPasswordAction, skipSetPasswordAction, updateMemberRole } from './server/actions.js';
4
4
  export { addRepeaterItem, removeRepeaterItem, reorderRepeaterItems } from './server/repeaterActions.js';
5
5
  export { GRepeaterProps, RepeaterItemProps } from './react/Repeater/index.js';
6
+ export { ResendEmailOptions, ResendInviteContent, ResendInviteOptions, ResendInviteTemplateEvent, ResendResetTemplateEvent } from './email/resend.js';
6
7
  export { ActionState, GRAsset, GRMember, GRRepeaterItem, GRRole, GRState, GRViewer, HTMLKeys, OverrideRow, OverrideType, OverrideValue, PolymorphicProps, SetPasswordState } from './core/types.js';
7
- export { AuthRedirectParams, AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRPrefetch, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget } from './core/adapter-types.js';
8
+ export { AuthRedirectParams, AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRPasswordResetEvent, GRPrefetch, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget } from './core/adapter-types.js';
8
9
  import 'react/jsx-runtime';
9
10
  import 'react';
10
11
 
@@ -17,5 +18,7 @@ declare const getFieldValue: (..._args: unknown[]) => never;
17
18
  declare const isAdmin: (..._args: unknown[]) => never;
18
19
  declare const isEditor: (..._args: unknown[]) => never;
19
20
  declare const canEdit: (..._args: unknown[]) => never;
21
+ declare const resendInvite: (..._args: unknown[]) => never;
22
+ declare const resendEmails: (..._args: unknown[]) => never;
20
23
 
21
- export { GRepeater, canEdit, configureGuardrail, getFieldValue, getRepeaterItemBySlug, isAdmin, isEditor, listRepeaterItems, withGuardrail };
24
+ export { GRepeater, canEdit, configureGuardrail, getFieldValue, getRepeaterItemBySlug, isAdmin, isEditor, listRepeaterItems, resendEmails, resendInvite, withGuardrail };
@@ -9,6 +9,8 @@ import {
9
9
  listAssets,
10
10
  setPasswordAction,
11
11
  skipSetPasswordAction,
12
+ forgotPasswordAction,
13
+ sendPasswordResetAction,
12
14
  setFieldValue
13
15
  } from "./server/actions";
14
16
  import {
@@ -59,6 +61,14 @@ const canEdit = serverOnly(
59
61
  "canEdit()",
60
62
  "Call it in a Server Component; in a Client Component, read `canEdit` from useGRail()."
61
63
  );
64
+ const resendInvite = serverOnly(
65
+ "resendInvite()",
66
+ "Pass it as onInvite in withGuardrail(), which runs in your root layout."
67
+ );
68
+ const resendEmails = serverOnly(
69
+ "resendEmails()",
70
+ "Spread it into withGuardrail(), which runs in your root layout."
71
+ );
62
72
  export {
63
73
  GRail,
64
74
  GRepeater,
@@ -66,6 +76,7 @@ export {
66
76
  canEdit,
67
77
  configureGuardrail,
68
78
  createAssetUpload,
79
+ forgotPasswordAction,
69
80
  getFieldValue,
70
81
  getRepeaterItemBySlug,
71
82
  inviteMember,
@@ -77,6 +88,9 @@ export {
77
88
  removeMember,
78
89
  removeRepeaterItem,
79
90
  reorderRepeaterItems,
91
+ resendEmails,
92
+ resendInvite,
93
+ sendPasswordResetAction,
80
94
  setFieldValue,
81
95
  setPasswordAction,
82
96
  skipSetPasswordAction,
@@ -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 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);\n\nexport type { GRepeaterProps, RepeaterItemProps } from \"./react/Repeater\";\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} 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,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;","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);\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":[]}
package/dist/index.d.ts CHANGED
@@ -1,14 +1,15 @@
1
1
  export { withGuardrail } from './server/wrapper.js';
2
- export { createAssetUpload, inviteMember, listAssets, listTeam, removeMember, setFieldValue, setPasswordAction, skipSetPasswordAction, updateMemberRole } from './server/actions.js';
2
+ export { createAssetUpload, forgotPasswordAction, inviteMember, listAssets, listTeam, removeMember, sendPasswordResetAction, setFieldValue, setPasswordAction, skipSetPasswordAction, updateMemberRole } from './server/actions.js';
3
3
  export { GRail } from './react/GRail/index.js';
4
4
  export { GRepeater, GRepeaterProps, RepeaterItemProps } from './react/Repeater/index.js';
5
5
  export { configureGuardrail } from './core/config.js';
6
6
  export { getRepeaterItemBySlug, listRepeaterItems } from './server/repeaterData.js';
7
7
  export { getFieldValue } from './server/fieldData.js';
8
8
  export { canEdit, isAdmin, isEditor } from './server/access.js';
9
+ export { ResendEmailOptions, ResendInviteContent, ResendInviteOptions, ResendInviteTemplateEvent, ResendResetTemplateEvent, resendEmails, resendInvite } from './email/resend.js';
9
10
  export { addRepeaterItem, removeRepeaterItem, reorderRepeaterItems } from './server/repeaterActions.js';
10
11
  export { useGRail } from './react/Provider.js';
11
12
  export { ActionState, GRAsset, GRMember, GRRepeaterItem, GRRole, GRState, GRViewer, HTMLKeys, OverrideRow, OverrideType, OverrideValue, PolymorphicProps, SetPasswordState } from './core/types.js';
12
- export { AuthRedirectParams, AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRPrefetch, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget } from './core/adapter-types.js';
13
+ export { AuthRedirectParams, AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRPasswordResetEvent, GRPrefetch, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget } from './core/adapter-types.js';
13
14
  import 'react/jsx-runtime';
14
15
  import 'react';
package/dist/index.js CHANGED
@@ -8,6 +8,8 @@ import {
8
8
  listAssets,
9
9
  setPasswordAction,
10
10
  skipSetPasswordAction,
11
+ forgotPasswordAction,
12
+ sendPasswordResetAction,
11
13
  setFieldValue
12
14
  } from "./server/actions";
13
15
  import { GRail } from "./react/GRail";
@@ -19,6 +21,7 @@ import {
19
21
  } from "./server/repeaterData";
20
22
  import { getFieldValue } from "./server/fieldData";
21
23
  import { isAdmin, isEditor, canEdit } from "./server/access";
24
+ import { resendInvite, resendEmails } from "./email/resend";
22
25
  import {
23
26
  addRepeaterItem,
24
27
  removeRepeaterItem,
@@ -32,6 +35,7 @@ export {
32
35
  canEdit,
33
36
  configureGuardrail,
34
37
  createAssetUpload,
38
+ forgotPasswordAction,
35
39
  getFieldValue,
36
40
  getRepeaterItemBySlug,
37
41
  inviteMember,
@@ -43,6 +47,9 @@ export {
43
47
  removeMember,
44
48
  removeRepeaterItem,
45
49
  reorderRepeaterItems,
50
+ resendEmails,
51
+ resendInvite,
52
+ sendPasswordResetAction,
46
53
  setFieldValue,
47
54
  setPasswordAction,
48
55
  skipSetPasswordAction,