@guard-rail/core 0.8.0 → 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.
- package/README.md +62 -102
- package/dist/adapters/supabase/auth.d.ts +9 -15
- package/dist/adapters/supabase/auth.js +33 -42
- package/dist/adapters/supabase/auth.js.map +1 -1
- package/dist/adapters/supabase/confirm.js +2 -2
- package/dist/adapters/supabase/confirm.js.map +1 -1
- package/dist/adapters/supabase/members.js +13 -2
- package/dist/adapters/supabase/members.js.map +1 -1
- package/dist/core/adapter-types.d.ts +60 -28
- package/dist/core/config.d.ts +2 -1
- package/dist/core/config.js +4 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/defs.d.ts +11 -1
- package/dist/core/defs.js +11 -1
- package/dist/core/defs.js.map +1 -1
- package/dist/core/types.d.ts +6 -0
- package/dist/email/resend.d.ts +39 -10
- package/dist/email/resend.js +92 -49
- package/dist/email/resend.js.map +1 -1
- package/dist/index.client.d.ts +5 -4
- package/dist/index.client.js +9 -0
- package/dist/index.client.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/react/Admin.js +105 -107
- package/dist/react/Admin.js.map +1 -1
- package/dist/react/AuthDrawer.d.ts +23 -0
- package/dist/react/AuthDrawer.js +165 -0
- package/dist/react/AuthDrawer.js.map +1 -0
- package/dist/react/AuthRedirectSync.js +1 -1
- package/dist/react/AuthRedirectSync.js.map +1 -1
- package/dist/react/Icons.d.ts +2 -1
- package/dist/react/Icons.js +2 -0
- package/dist/react/Icons.js.map +1 -1
- package/dist/react/Team.d.ts +7 -1
- package/dist/react/Team.js +23 -3
- package/dist/react/Team.js.map +1 -1
- package/dist/server/access.d.ts +3 -1
- package/dist/server/access.js +5 -3
- package/dist/server/access.js.map +1 -1
- package/dist/server/actions.d.ts +25 -4
- package/dist/server/actions.js +115 -39
- package/dist/server/actions.js.map +1 -1
- package/dist/server/wrapper.js +2 -1
- package/dist/server/wrapper.js.map +1 -1
- package/package.json +8 -10
|
@@ -76,30 +76,21 @@ interface GRAuthAdapter {
|
|
|
76
76
|
*/
|
|
77
77
|
signInUrl?: string;
|
|
78
78
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
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.
|
|
85
|
-
*/
|
|
86
|
-
inviteUser?: (email: string, options?: {
|
|
87
|
-
redirectTo?: string;
|
|
88
|
-
}) => Promise<{
|
|
89
|
-
success: boolean;
|
|
90
|
-
error?: string;
|
|
91
|
-
}>;
|
|
92
|
-
/**
|
|
93
|
-
* Create the account and return a one-time sign-in link, without sending
|
|
94
|
-
* anything — for an app that delivers its own invitation email (see
|
|
95
|
-
* onInvite). Optional, and only worth implementing when the provider can
|
|
96
|
-
* mint a link server-side; Guardrail uses it only when the app also
|
|
97
|
-
* provides onInvite, and otherwise falls back to inviteUser.
|
|
98
86
|
*
|
|
99
87
|
* `origin` is the site's own origin, so the link points back at the site
|
|
100
|
-
* (where AuthRedirectSync completes it) rather than at the provider
|
|
101
|
-
*
|
|
102
|
-
*
|
|
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.
|
|
103
94
|
*/
|
|
104
95
|
createInviteLink?: (email: string, options: {
|
|
105
96
|
origin: string;
|
|
@@ -111,6 +102,20 @@ interface GRAuthAdapter {
|
|
|
111
102
|
success: false;
|
|
112
103
|
error: string;
|
|
113
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;
|
|
118
|
+
}>;
|
|
114
119
|
/**
|
|
115
120
|
* Return the current verified session, or null.
|
|
116
121
|
*
|
|
@@ -193,6 +198,16 @@ interface GRMembershipAdapter {
|
|
|
193
198
|
success: boolean;
|
|
194
199
|
error?: string;
|
|
195
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>;
|
|
196
211
|
/**
|
|
197
212
|
* Bind a pending invitation to the signed-in user.
|
|
198
213
|
*
|
|
@@ -279,6 +294,14 @@ interface GRInviteEvent {
|
|
|
279
294
|
/** Whether the invite created an account (true) or the address already had one. Set alongside `link`. */
|
|
280
295
|
isNewUser?: boolean;
|
|
281
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;
|
|
304
|
+
}
|
|
282
305
|
interface GRConfig {
|
|
283
306
|
db: GRDBAdapter;
|
|
284
307
|
auth: GRAuthAdapter;
|
|
@@ -289,17 +312,26 @@ interface GRConfig {
|
|
|
289
312
|
repeaters?: GRRepeaterAdapter;
|
|
290
313
|
siteId: string | (() => string | Promise<string>);
|
|
291
314
|
/**
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
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.
|
|
296
320
|
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
* the admin who sent the invite.
|
|
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.
|
|
301
324
|
*/
|
|
302
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>;
|
|
303
335
|
/**
|
|
304
336
|
* Extra content rendered in the toolbar pill, next to Team and Log out.
|
|
305
337
|
* Plain JSX, not a component reference or a structured item schema — the
|
|
@@ -321,4 +353,4 @@ interface GRConfig {
|
|
|
321
353
|
prefetch?: false | GRPrefetch;
|
|
322
354
|
}
|
|
323
355
|
|
|
324
|
-
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 };
|
package/dist/core/config.d.ts
CHANGED
|
@@ -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 };
|
package/dist/core/config.js
CHANGED
|
@@ -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,
|
package/dist/core/config.js.map
CHANGED
|
@@ -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":[]}
|
package/dist/core/defs.d.ts
CHANGED
|
@@ -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,
|
package/dist/core/defs.js.map
CHANGED
|
@@ -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;
|
|
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":[]}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -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;
|
package/dist/email/resend.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GRInviteEvent } from '../core/adapter-types.js';
|
|
1
|
+
import { GRInviteEvent, GRPasswordResetEvent } from '../core/adapter-types.js';
|
|
2
2
|
import 'react';
|
|
3
3
|
import '../core/types.js';
|
|
4
4
|
|
|
@@ -9,26 +9,39 @@ interface ResendInviteContent {
|
|
|
9
9
|
/** Plain-text alternative. Worth providing — some clients and spam filters prefer it. */
|
|
10
10
|
text?: string;
|
|
11
11
|
}
|
|
12
|
-
/** What a custom template is given: the invite, plus the link and name to put in it. */
|
|
12
|
+
/** What a custom invite template is given: the invite, plus the link and name to put in it. */
|
|
13
13
|
type ResendInviteTemplateEvent = GRInviteEvent & {
|
|
14
14
|
link: string;
|
|
15
15
|
siteName: string;
|
|
16
16
|
};
|
|
17
|
+
/** What a custom password-reset template is given. */
|
|
18
|
+
type ResendResetTemplateEvent = GRPasswordResetEvent & {
|
|
19
|
+
siteName: string;
|
|
20
|
+
};
|
|
17
21
|
interface ResendInviteOptions {
|
|
18
22
|
/**
|
|
19
23
|
* "Name <hello@yourdomain.com>". The domain has to be verified in Resend, or
|
|
20
24
|
* Resend rejects the send.
|
|
21
25
|
*/
|
|
22
26
|
from: string;
|
|
23
|
-
/** Resend API key. Defaults to `process.env.RESEND_API_KEY`, read when an
|
|
27
|
+
/** Resend API key. Defaults to `process.env.RESEND_API_KEY`, read when an email is sent. */
|
|
24
28
|
apiKey?: string;
|
|
25
|
-
/** Named in the default
|
|
29
|
+
/** Named in the default emails. Defaults to the site id. */
|
|
26
30
|
siteName?: string;
|
|
27
|
-
/**
|
|
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
|
+
*/
|
|
28
37
|
render?: (event: ResendInviteTemplateEvent) => ResendInviteContent | Promise<ResendInviteContent>;
|
|
29
38
|
/** Override the endpoint, e.g. for a proxy or a test server. */
|
|
30
39
|
apiUrl?: string;
|
|
31
40
|
}
|
|
41
|
+
interface ResendEmailOptions extends ResendInviteOptions {
|
|
42
|
+
/** Replaces the default password-reset email. */
|
|
43
|
+
renderReset?: (event: ResendResetTemplateEvent) => ResendInviteContent | Promise<ResendInviteContent>;
|
|
44
|
+
}
|
|
32
45
|
/**
|
|
33
46
|
* An `onInvite` handler that emails the invitee through Resend.
|
|
34
47
|
*
|
|
@@ -37,15 +50,31 @@ interface ResendInviteOptions {
|
|
|
37
50
|
* onInvite: resendInvite({ from: "Acme <hello@acme.com>", siteName: "Acme" }),
|
|
38
51
|
* });
|
|
39
52
|
*
|
|
40
|
-
* Needs an auth adapter that can create
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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.
|
|
44
60
|
*
|
|
45
61
|
* Calls Resend's HTTP API directly, so there is no SDK to install. To send
|
|
46
62
|
* through anything else, write your own onInvite — the event carries the same
|
|
47
63
|
* `link`.
|
|
48
64
|
*/
|
|
49
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
|
+
};
|
|
50
79
|
|
|
51
|
-
export { type ResendInviteContent, type ResendInviteOptions, type ResendInviteTemplateEvent, resendInvite };
|
|
80
|
+
export { type ResendEmailOptions, type ResendInviteContent, type ResendInviteOptions, type ResendInviteTemplateEvent, type ResendResetTemplateEvent, resendEmails, resendInvite };
|
package/dist/email/resend.js
CHANGED
|
@@ -1,75 +1,118 @@
|
|
|
1
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
|
+
}
|
|
2
40
|
function resendInvite(options) {
|
|
3
|
-
return
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
siteName: options.siteName ?? event.siteId
|
|
15
|
-
};
|
|
16
|
-
const content = await (options.render ?? defaultInviteEmail)(templateEvent);
|
|
17
|
-
const res = await fetch(options.apiUrl ?? RESEND_URL, {
|
|
18
|
-
method: "POST",
|
|
19
|
-
headers: {
|
|
20
|
-
Authorization: `Bearer ${apiKey}`,
|
|
21
|
-
"Content-Type": "application/json"
|
|
22
|
-
},
|
|
23
|
-
body: JSON.stringify({
|
|
24
|
-
from: options.from,
|
|
25
|
-
to: [event.email],
|
|
26
|
-
subject: content.subject,
|
|
27
|
-
html: content.html,
|
|
28
|
-
...content.text ? { text: content.text } : {}
|
|
29
|
-
})
|
|
30
|
-
});
|
|
31
|
-
if (!res.ok) {
|
|
32
|
-
const detail = await res.json().then(
|
|
33
|
-
(body) => body?.message,
|
|
34
|
-
() => void 0
|
|
35
|
-
);
|
|
36
|
-
throw new Error(`Resend rejected the email (${res.status})${detail ? `: ${detail}` : "."}`);
|
|
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));
|
|
37
52
|
}
|
|
38
53
|
};
|
|
39
54
|
}
|
|
40
55
|
function escapeHtml(value) {
|
|
41
56
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
42
57
|
}
|
|
43
|
-
function
|
|
44
|
-
const site = event.siteName;
|
|
45
|
-
const who = event.invitedBy.includes("@") ? event.invitedBy : "An admin";
|
|
46
|
-
const intro = `${who} invited you to edit ${site} as an ${event.role}.`;
|
|
47
|
-
const action = event.isNewUser === false ? "Sign in" : "Set up your account";
|
|
48
|
-
const subject = event.isNewUser === false ? `Sign in to edit ${site}` : `You're invited to edit ${site}`;
|
|
58
|
+
function layout(parts) {
|
|
49
59
|
const html = `<!doctype html>
|
|
50
60
|
<html>
|
|
51
61
|
<body style="margin:0;padding:24px;background:#f6f6f6;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:#111;">
|
|
52
62
|
<div style="max-width:480px;margin:0 auto;background:#fff;border-radius:8px;padding:32px;">
|
|
53
|
-
<h1 style="margin:0 0 12px;font-size:20px;">${escapeHtml(subject)}</h1>
|
|
54
|
-
<p style="margin:0 0 24px;line-height:1.5;">${escapeHtml(intro)}</p>
|
|
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>
|
|
55
65
|
<p style="margin:0 0 24px;">
|
|
56
|
-
<a href="${escapeHtml(
|
|
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>
|
|
57
67
|
</p>
|
|
58
|
-
<p style="margin:0;font-size:13px;line-height:1.5;color:#666;"
|
|
68
|
+
<p style="margin:0;font-size:13px;line-height:1.5;color:#666;">${escapeHtml(parts.footnote)}</p>
|
|
59
69
|
</div>
|
|
60
70
|
</body>
|
|
61
71
|
</html>`;
|
|
62
|
-
const text = `${subject}
|
|
72
|
+
const text = `${parts.subject}
|
|
63
73
|
|
|
64
|
-
${intro}
|
|
74
|
+
${parts.intro}
|
|
65
75
|
|
|
66
|
-
${action}: ${
|
|
76
|
+
${parts.action}: ${parts.link}
|
|
67
77
|
|
|
68
|
-
|
|
78
|
+
${parts.footnote}
|
|
69
79
|
`;
|
|
70
|
-
return { subject, html, text };
|
|
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
|
+
});
|
|
71
113
|
}
|
|
72
114
|
export {
|
|
115
|
+
resendEmails,
|
|
73
116
|
resendInvite
|
|
74
117
|
};
|
|
75
118
|
//# sourceMappingURL=resend.js.map
|
package/dist/email/resend.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/email/resend.ts"],"sourcesContent":["import type { GRInviteEvent } 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 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\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 invite is sent. */\n apiKey?: string;\n /** Named in the default email. Defaults to the site id. */\n siteName?: string;\n /** Replaces the default email entirely — your own subject, markup, brand. */\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\nconst RESEND_URL = \"https://api.resend.com/emails\";\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 sign-in links (the Supabase adapter\n * can once it's given a serviceRoleKey); without a link there is nothing to\n * send, and it throws a message saying so. Throwing is how failures reach the\n * admin who clicked Invite — Resend rejecting the sender domain, say.\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 async (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 apiKey = options.apiKey ?? process.env.RESEND_API_KEY;\n if (!apiKey) throw new Error(\"RESEND_API_KEY is not set.\");\n\n const templateEvent: ResendInviteTemplateEvent = {\n ...event,\n link: event.link,\n siteName: options.siteName ?? event.siteId,\n };\n const content = await (options.render ?? defaultInviteEmail)(templateEvent);\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: [event.email],\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}\n\nfunction escapeHtml(value: string): string {\n return value\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n\n/**\n * The email used when no `render` is given: plain, one button, readable\n * everywhere. 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 defaultInviteEmail(event: ResendInviteTemplateEvent): ResendInviteContent {\n const site = event.siteName;\n const who = event.invitedBy.includes(\"@\") ? event.invitedBy : \"An admin\";\n const intro = `${who} invited you to edit ${site} as an ${event.role}.`;\n const action = event.isNewUser === false ? \"Sign in\" : \"Set up your account\";\n const subject = event.isNewUser === false ? `Sign in to edit ${site}` : `You're invited to edit ${site}`;\n\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(subject)}</h1>\n <p style=\"margin:0 0 24px;line-height:1.5;\">${escapeHtml(intro)}</p>\n <p style=\"margin:0 0 24px;\">\n <a href=\"${escapeHtml(event.link)}\" style=\"display:inline-block;background:#111;color:#fff;text-decoration:none;padding:12px 20px;border-radius:6px;font-weight:600;\">${escapeHtml(action)}</a>\n </p>\n <p style=\"margin:0;font-size:13px;line-height:1.5;color:#666;\">This link works once and expires, so open it on the device you'll use. If you weren't expecting this, you can ignore the email.</p>\n </div>\n </body>\n</html>`;\n\n const text = `${subject}\\n\\n${intro}\\n\\n${action}: ${event.link}\\n\\nThis link works once and expires. If you weren't expecting this, you can ignore the email.\\n`;\n return { subject, html, text };\n}\n"],"mappings":"AAgCA,MAAM,aAAa;AAmBZ,SAAS,aAAa,SAA8B;AACzD,SAAO,OAAO,UAAwC;AACpD,QAAI,CAAC,MAAM,MAAM;AACf,YAAM,IAAI;AAAA,QACR;AAAA,MAEF;AAAA,IACF;AACA,UAAM,SAAS,QAAQ,UAAU,QAAQ,IAAI;AAC7C,QAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,4BAA4B;AAEzD,UAAM,gBAA2C;AAAA,MAC/C,GAAG;AAAA,MACH,MAAM,MAAM;AAAA,MACZ,UAAU,QAAQ,YAAY,MAAM;AAAA,IACtC;AACA,UAAM,UAAU,OAAO,QAAQ,UAAU,oBAAoB,aAAa;AAE1E,UAAM,MAAM,MAAM,MAAM,QAAQ,UAAU,YAAY;AAAA,MACpD,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,eAAe,UAAU,MAAM;AAAA,QAC/B,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM,KAAK,UAAU;AAAA,QACnB,MAAM,QAAQ;AAAA,QACd,IAAI,CAAC,MAAM,KAAK;AAAA,QAChB,SAAS,QAAQ;AAAA,QACjB,MAAM,QAAQ;AAAA,QACd,GAAI,QAAQ,OAAO,EAAE,MAAM,QAAQ,KAAK,IAAI,CAAC;AAAA,MAC/C,CAAC;AAAA,IACH,CAAC;AAED,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,SAAS,MAAM,IAAI,KAAK,EAAE;AAAA,QAC9B,CAAC,SAA+B,MAAM;AAAA,QACtC,MAAM;AAAA,MACR;AACA,YAAM,IAAI,MAAM,8BAA8B,IAAI,MAAM,IAAI,SAAS,KAAK,MAAM,KAAK,GAAG,EAAE;AAAA,IAC5F;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,mBAAmB,OAAuD;AACjF,QAAM,OAAO,MAAM;AACnB,QAAM,MAAM,MAAM,UAAU,SAAS,GAAG,IAAI,MAAM,YAAY;AAC9D,QAAM,QAAQ,GAAG,GAAG,wBAAwB,IAAI,UAAU,MAAM,IAAI;AACpE,QAAM,SAAS,MAAM,cAAc,QAAQ,YAAY;AACvD,QAAM,UAAU,MAAM,cAAc,QAAQ,mBAAmB,IAAI,KAAK,0BAA0B,IAAI;AAEtG,QAAM,OAAO;AAAA;AAAA;AAAA;AAAA,oDAIqC,WAAW,OAAO,CAAC;AAAA,oDACnB,WAAW,KAAK,CAAC;AAAA;AAAA,mBAElD,WAAW,MAAM,IAAI,CAAC,uIAAuI,WAAW,MAAM,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAOhM,QAAM,OAAO,GAAG,OAAO;AAAA;AAAA,EAAO,KAAK;AAAA;AAAA,EAAO,MAAM,KAAK,MAAM,IAAI;AAAA;AAAA;AAAA;AAC/D,SAAO,EAAE,SAAS,MAAM,KAAK;AAC/B;","names":[]}
|
|
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, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\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":[]}
|
package/dist/index.client.d.ts
CHANGED
|
@@ -1,11 +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 { ResendInviteContent, ResendInviteOptions, ResendInviteTemplateEvent } from './email/resend.js';
|
|
6
|
+
export { ResendEmailOptions, ResendInviteContent, ResendInviteOptions, ResendInviteTemplateEvent, ResendResetTemplateEvent } from './email/resend.js';
|
|
7
7
|
export { ActionState, GRAsset, GRMember, GRRepeaterItem, GRRole, GRState, GRViewer, HTMLKeys, OverrideRow, OverrideType, OverrideValue, PolymorphicProps, SetPasswordState } from './core/types.js';
|
|
8
|
-
export { AuthRedirectParams, AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, 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';
|
|
9
9
|
import 'react/jsx-runtime';
|
|
10
10
|
import 'react';
|
|
11
11
|
|
|
@@ -19,5 +19,6 @@ declare const isAdmin: (..._args: unknown[]) => never;
|
|
|
19
19
|
declare const isEditor: (..._args: unknown[]) => never;
|
|
20
20
|
declare const canEdit: (..._args: unknown[]) => never;
|
|
21
21
|
declare const resendInvite: (..._args: unknown[]) => never;
|
|
22
|
+
declare const resendEmails: (..._args: unknown[]) => never;
|
|
22
23
|
|
|
23
|
-
export { GRepeater, canEdit, configureGuardrail, getFieldValue, getRepeaterItemBySlug, isAdmin, isEditor, listRepeaterItems, resendInvite, withGuardrail };
|
|
24
|
+
export { GRepeater, canEdit, configureGuardrail, getFieldValue, getRepeaterItemBySlug, isAdmin, isEditor, listRepeaterItems, resendEmails, resendInvite, withGuardrail };
|
package/dist/index.client.js
CHANGED
|
@@ -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 {
|
|
@@ -63,6 +65,10 @@ const resendInvite = serverOnly(
|
|
|
63
65
|
"resendInvite()",
|
|
64
66
|
"Pass it as onInvite in withGuardrail(), which runs in your root layout."
|
|
65
67
|
);
|
|
68
|
+
const resendEmails = serverOnly(
|
|
69
|
+
"resendEmails()",
|
|
70
|
+
"Spread it into withGuardrail(), which runs in your root layout."
|
|
71
|
+
);
|
|
66
72
|
export {
|
|
67
73
|
GRail,
|
|
68
74
|
GRepeater,
|
|
@@ -70,6 +76,7 @@ export {
|
|
|
70
76
|
canEdit,
|
|
71
77
|
configureGuardrail,
|
|
72
78
|
createAssetUpload,
|
|
79
|
+
forgotPasswordAction,
|
|
73
80
|
getFieldValue,
|
|
74
81
|
getRepeaterItemBySlug,
|
|
75
82
|
inviteMember,
|
|
@@ -81,7 +88,9 @@ export {
|
|
|
81
88
|
removeMember,
|
|
82
89
|
removeRepeaterItem,
|
|
83
90
|
reorderRepeaterItems,
|
|
91
|
+
resendEmails,
|
|
84
92
|
resendInvite,
|
|
93
|
+
sendPasswordResetAction,
|
|
85
94
|
setFieldValue,
|
|
86
95
|
setPasswordAction,
|
|
87
96
|
skipSetPasswordAction,
|