@reuters-graphics/gfx-better-auth 0.1.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 +81 -0
- package/dist/client/index.d.ts +142 -0
- package/dist/client/index.js +32 -0
- package/dist/domains.d.ts +24 -0
- package/dist/domains.js +30 -0
- package/dist/emails/Template.d.ts +36 -0
- package/dist/emails/Template.js +82 -0
- package/dist/emails/_components/Brand.d.ts +13 -0
- package/dist/emails/_components/Brand.js +28 -0
- package/dist/emails/_components/CentreCard.d.ts +10 -0
- package/dist/emails/_components/CentreCard.js +22 -0
- package/dist/emails/_components/CentreWell.d.ts +7 -0
- package/dist/emails/_components/CentreWell.js +9 -0
- package/dist/emails/_components/Footer.d.ts +7 -0
- package/dist/emails/_components/Footer.js +26 -0
- package/dist/emails/_components/Head.d.ts +10 -0
- package/dist/emails/_components/Head.js +19 -0
- package/dist/emails/_components/OpenButton.d.ts +8 -0
- package/dist/emails/_components/OpenButton.js +21 -0
- package/dist/emails/_components/index.d.ts +14 -0
- package/dist/emails/_components/index.js +14 -0
- package/dist/emails/_components/tokens.d.ts +28 -0
- package/dist/emails/_components/tokens.js +28 -0
- package/dist/emails/index.d.ts +96 -0
- package/dist/emails/index.js +160 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/schema/index.d.ts +718 -0
- package/dist/schema/index.js +108 -0
- package/dist/server/auth.d.ts +67 -0
- package/dist/server/auth.js +152 -0
- package/dist/server/config.d.ts +81 -0
- package/dist/server/config.js +105 -0
- package/dist/server/dev.d.ts +15 -0
- package/dist/server/dev.js +48 -0
- package/dist/server/escape.d.ts +9 -0
- package/dist/server/escape.js +28 -0
- package/dist/server/guard.d.ts +13 -0
- package/dist/server/guard.js +17 -0
- package/dist/server/handle.d.ts +62 -0
- package/dist/server/handle.js +252 -0
- package/dist/server/index.d.ts +12 -0
- package/dist/server/index.js +11 -0
- package/dist/server/pages.d.ts +53 -0
- package/dist/server/pages.js +133 -0
- package/dist/server/testing.d.ts +31 -0
- package/dist/server/testing.js +53 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.js +1 -0
- package/package.json +101 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brand components for the sign-in email, ported from
|
|
3
|
+
* `rngs.io/src/lib/emails/_components/`.
|
|
4
|
+
*
|
|
5
|
+
* Not in the package's `exports` map, so not public API — a consumer cannot
|
|
6
|
+
* import these, and they can change without a major.
|
|
7
|
+
*/
|
|
8
|
+
export { Brand } from './Brand.js';
|
|
9
|
+
export { CentreCard } from './CentreCard.js';
|
|
10
|
+
export { CentreWell } from './CentreWell.js';
|
|
11
|
+
export { Footer } from './Footer.js';
|
|
12
|
+
export { Head } from './Head.js';
|
|
13
|
+
export { OpenButton } from './OpenButton.js';
|
|
14
|
+
export { colors, fontFamily, supportEmail } from './tokens.js';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The design tokens behind the sign-in email, ported from
|
|
3
|
+
* `rngs.io/src/lib/emails/_components/`.
|
|
4
|
+
*
|
|
5
|
+
* Kept in one place because email clients require inline styles — there is no
|
|
6
|
+
* stylesheet to hold them, so every component reaches for these directly.
|
|
7
|
+
*/
|
|
8
|
+
/** Inter Tight, with a full fallback stack for clients that ignore webfonts. */
|
|
9
|
+
export declare const fontFamily = "\"Inter Tight\",-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,Oxygen-Sans,Ubuntu,Cantarell,\"Helvetica Neue\",sans-serif";
|
|
10
|
+
export declare const colors: {
|
|
11
|
+
/** The dark card `rngs.io` uses, and the page behind it. */
|
|
12
|
+
readonly card: "#323944";
|
|
13
|
+
readonly page: "#282e37";
|
|
14
|
+
readonly text: "#ffffff";
|
|
15
|
+
/** Footnotes and the eyebrow above the app name. */
|
|
16
|
+
readonly muted: "rgba(255,255,255,0.72)";
|
|
17
|
+
readonly buttonBg: "#4f5b6d";
|
|
18
|
+
readonly buttonBorder: "#64748b";
|
|
19
|
+
readonly rule: "rgba(255,255,255,0.18)";
|
|
20
|
+
/**
|
|
21
|
+
* Reuters orange. It stands in for the wordmark `rngs.io` loads as a PNG:
|
|
22
|
+
* this package ships no image assets, so the brand is carried by a rule and
|
|
23
|
+
* type rather than by a file we would have to host.
|
|
24
|
+
*/
|
|
25
|
+
readonly reuters: "#ff8000";
|
|
26
|
+
};
|
|
27
|
+
/** The support address `rngs.io` points people at. */
|
|
28
|
+
export declare const supportEmail = "all.graphics@thomsonreuters.com";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The design tokens behind the sign-in email, ported from
|
|
3
|
+
* `rngs.io/src/lib/emails/_components/`.
|
|
4
|
+
*
|
|
5
|
+
* Kept in one place because email clients require inline styles — there is no
|
|
6
|
+
* stylesheet to hold them, so every component reaches for these directly.
|
|
7
|
+
*/
|
|
8
|
+
/** Inter Tight, with a full fallback stack for clients that ignore webfonts. */
|
|
9
|
+
export const fontFamily = '"Inter Tight",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif';
|
|
10
|
+
export const colors = {
|
|
11
|
+
/** The dark card `rngs.io` uses, and the page behind it. */
|
|
12
|
+
card: '#323944',
|
|
13
|
+
page: '#282e37',
|
|
14
|
+
text: '#ffffff',
|
|
15
|
+
/** Footnotes and the eyebrow above the app name. */
|
|
16
|
+
muted: 'rgba(255,255,255,0.72)',
|
|
17
|
+
buttonBg: '#4f5b6d',
|
|
18
|
+
buttonBorder: '#64748b',
|
|
19
|
+
rule: 'rgba(255,255,255,0.18)',
|
|
20
|
+
/**
|
|
21
|
+
* Reuters orange. It stands in for the wordmark `rngs.io` loads as a PNG:
|
|
22
|
+
* this package ships no image assets, so the brand is carried by a rule and
|
|
23
|
+
* type rather than by a file we would have to host.
|
|
24
|
+
*/
|
|
25
|
+
reuters: '#ff8000',
|
|
26
|
+
};
|
|
27
|
+
/** The support address `rngs.io` points people at. */
|
|
28
|
+
export const supportEmail = 'all.graphics@thomsonreuters.com';
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { SignInEmailContext } from './Template.js';
|
|
2
|
+
export type { SignInEmailContext };
|
|
3
|
+
/**
|
|
4
|
+
* The Postmark block of the validated config. Restated structurally rather
|
|
5
|
+
* than imported, so `src/emails/` does not depend on `src/server/`.
|
|
6
|
+
*/
|
|
7
|
+
export interface SignInEmailPostmarkConfig {
|
|
8
|
+
/**
|
|
9
|
+
* Postmark server token. Optional, because the dev flow skips the inbox
|
|
10
|
+
* (ADR-0003, ADR-0007). Sending without one throws.
|
|
11
|
+
*/
|
|
12
|
+
token?: string | undefined;
|
|
13
|
+
/** The `From` address. Must be a verified Postmark sender signature. */
|
|
14
|
+
from: string;
|
|
15
|
+
/** Postmark message stream, e.g. `"login-requests"`. */
|
|
16
|
+
messageStream: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Everything one sign-in email needs. Structurally `SignInEmail` from
|
|
20
|
+
* `src/server/config.ts`.
|
|
21
|
+
*/
|
|
22
|
+
export interface SignInEmailMessage extends SignInEmailContext {
|
|
23
|
+
to: string;
|
|
24
|
+
postmark: SignInEmailPostmarkConfig;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The one method of Postmark's `ServerClient` this package uses.
|
|
28
|
+
*
|
|
29
|
+
* Declared structurally so a fake can be injected without importing
|
|
30
|
+
* `postmark` — which is how tests and the fixture app stay off the wire
|
|
31
|
+
* (conventions § Testing: *no test may send real email*).
|
|
32
|
+
*/
|
|
33
|
+
export interface PostmarkLike {
|
|
34
|
+
sendEmail(message: {
|
|
35
|
+
From: string;
|
|
36
|
+
To: string;
|
|
37
|
+
Subject: string;
|
|
38
|
+
HtmlBody: string;
|
|
39
|
+
TextBody: string;
|
|
40
|
+
MessageStream: string;
|
|
41
|
+
}): Promise<unknown>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The address hard-bounced and Postmark has **deactivated** it.
|
|
45
|
+
*
|
|
46
|
+
* This is the signal a consumer can use to set `user.disabledAt` — see
|
|
47
|
+
* `USAGE.md`. It is thrown,
|
|
48
|
+
* never swallowed: a caller that ignores it silently tells someone a link is
|
|
49
|
+
* on its way when it is not.
|
|
50
|
+
*
|
|
51
|
+
* Distinguishable without importing `postmark` — check
|
|
52
|
+
* `error instanceof InactiveRecipientError`, or
|
|
53
|
+
* `error.name === 'InactiveRecipientError'`, which is assigned from a string
|
|
54
|
+
* literal rather than `constructor.name` so it survives minification.
|
|
55
|
+
*/
|
|
56
|
+
export declare class InactiveRecipientError extends Error {
|
|
57
|
+
/** The address we tried to send to. */
|
|
58
|
+
readonly email: string;
|
|
59
|
+
/** Addresses Postmark named in the message; usually just {@link email}. */
|
|
60
|
+
readonly recipients: string[];
|
|
61
|
+
constructor(email: string, options?: {
|
|
62
|
+
recipients?: string[] | undefined;
|
|
63
|
+
cause?: unknown;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/** The rendered email, before it is handed to a transport. */
|
|
67
|
+
export interface RenderedSignInEmail {
|
|
68
|
+
subject: string;
|
|
69
|
+
html: string;
|
|
70
|
+
text: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Render the sign-in email without sending it. Useful for a snapshot, and for
|
|
74
|
+
* a consumer wiring its own transport.
|
|
75
|
+
*
|
|
76
|
+
* ⚠️ **`render()` is async here.** react-email 6 / React 19 returns a
|
|
77
|
+
* `Promise<string>`; the 2.x examples all over the web are synchronous, and
|
|
78
|
+
* forgetting the `await` puts `[object Promise]` in the body of an email.
|
|
79
|
+
*/
|
|
80
|
+
export declare const renderSignInEmail: (context: SignInEmailContext) => Promise<RenderedSignInEmail>;
|
|
81
|
+
/**
|
|
82
|
+
* Render the sign-in email and post it to Postmark.
|
|
83
|
+
*
|
|
84
|
+
* @param message The address, the **confirm** URL, the app name, the link
|
|
85
|
+
* lifetime, and the Postmark config to send with.
|
|
86
|
+
* @param options.client A stand-in for Postmark's `ServerClient`. Supply it
|
|
87
|
+
* from a test or the fixture app; leave it out and a real client is built
|
|
88
|
+
* from `message.postmark.token` on first use and cached.
|
|
89
|
+
*
|
|
90
|
+
* @throws {InactiveRecipientError} when Postmark has deactivated the address —
|
|
91
|
+
* the `disabledAt` signal (ADR-0004). Deliberately not swallowed.
|
|
92
|
+
* @throws {Error} when no `token` is configured and no `client` was supplied.
|
|
93
|
+
*/
|
|
94
|
+
export declare const sendSignInEmail: (message: SignInEmailMessage, options?: {
|
|
95
|
+
client?: PostmarkLike;
|
|
96
|
+
}) => Promise<void>;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sign-in email: a Reuters-branded React template rendered with `react-email`
|
|
3
|
+
* and sent via Postmark.
|
|
4
|
+
*
|
|
5
|
+
* 🚨 **Server-only.** It reaches for the Postmark token. Nothing under
|
|
6
|
+
* `src/client/` or the root entry may import it.
|
|
7
|
+
*
|
|
8
|
+
* React is a dependency rather than a peer on purpose: it is an
|
|
9
|
+
* implementation detail of rendering email, and no consumer should install it
|
|
10
|
+
* to use this package. The React email ecosystem is simply better than
|
|
11
|
+
* anything in Svelte, which is the only reason it is here.
|
|
12
|
+
*
|
|
13
|
+
* ## The shape, and why
|
|
14
|
+
*
|
|
15
|
+
* {@link sendSignInEmail} takes **one message object carrying its own
|
|
16
|
+
* Postmark config** — `{ to, url, appName, expiresIn, postmark }` — rather
|
|
17
|
+
* than a config-bound factory or a `(to, context)` pair. That is deliberate:
|
|
18
|
+
* it is structurally the `SignInEmail` interface in `src/server/config.ts`,
|
|
19
|
+
* which is also the `sendEmail` seam a consumer can override. One shape means
|
|
20
|
+
* `createGfxAuth` passes the message straight through, and a consumer's own
|
|
21
|
+
* transport is a drop-in for ours:
|
|
22
|
+
*
|
|
23
|
+
* ```ts
|
|
24
|
+
* sendEmail?: (message: SignInEmail) => Promise<void>;
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* Because the config travels with the message, there is nothing to construct
|
|
28
|
+
* at boot and nothing to blow up in the dev flow, where there is no token at
|
|
29
|
+
* all (ADR-0003, ADR-0007). The Postmark client is built on first send and
|
|
30
|
+
* cached per token.
|
|
31
|
+
*
|
|
32
|
+
* ## Authoring note: no JSX
|
|
33
|
+
*
|
|
34
|
+
* These files use `createElement` in `.ts`, not JSX in `.tsx`, because
|
|
35
|
+
* `svelte-package` copies `.tsx` into `dist/` untranspiled. See the docblock
|
|
36
|
+
* at the top of `./Template.ts` — that constraint is load-bearing.
|
|
37
|
+
*/
|
|
38
|
+
import { ServerClient } from 'postmark';
|
|
39
|
+
import { createElement } from 'react';
|
|
40
|
+
import { render } from 'react-email';
|
|
41
|
+
import { subject as buildSubject, Template, text as buildText, } from './Template.js';
|
|
42
|
+
/**
|
|
43
|
+
* The address hard-bounced and Postmark has **deactivated** it.
|
|
44
|
+
*
|
|
45
|
+
* This is the signal a consumer can use to set `user.disabledAt` — see
|
|
46
|
+
* `USAGE.md`. It is thrown,
|
|
47
|
+
* never swallowed: a caller that ignores it silently tells someone a link is
|
|
48
|
+
* on its way when it is not.
|
|
49
|
+
*
|
|
50
|
+
* Distinguishable without importing `postmark` — check
|
|
51
|
+
* `error instanceof InactiveRecipientError`, or
|
|
52
|
+
* `error.name === 'InactiveRecipientError'`, which is assigned from a string
|
|
53
|
+
* literal rather than `constructor.name` so it survives minification.
|
|
54
|
+
*/
|
|
55
|
+
export class InactiveRecipientError extends Error {
|
|
56
|
+
/** The address we tried to send to. */
|
|
57
|
+
email;
|
|
58
|
+
/** Addresses Postmark named in the message; usually just {@link email}. */
|
|
59
|
+
recipients;
|
|
60
|
+
constructor(email, options = {}) {
|
|
61
|
+
super(`Postmark has deactivated ${email}: the address hard bounced, was reported as spam, or was suppressed manually. For a Thomson Reuters inbox this usually means the person has left.`, { cause: options.cause });
|
|
62
|
+
// A string literal, not `this.constructor.name` — see the class docblock.
|
|
63
|
+
this.name = 'InactiveRecipientError';
|
|
64
|
+
this.email = email;
|
|
65
|
+
this.recipients = options.recipients ?? [email];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 🚨 **Detect Postmark's inactive-recipient error by `name` and code, never
|
|
70
|
+
* `instanceof`.**
|
|
71
|
+
*
|
|
72
|
+
* Postmark's error classes do not survive some bundling — `rngs.io` learned
|
|
73
|
+
* this the hard way — so `instanceof Errors.InactiveRecipientsError` quietly
|
|
74
|
+
* stops matching and a genuine deactivation looks like a generic
|
|
75
|
+
* failure. `406` is Postmark's own
|
|
76
|
+
* `ApiInputError.ERROR_CODES.inactiveRecipient`, kept as a second signal
|
|
77
|
+
* because upstream sets `name` from `this.constructor.name` and a minifier
|
|
78
|
+
* can rename the class out from under it.
|
|
79
|
+
*/
|
|
80
|
+
const isInactiveRecipient = (error) => {
|
|
81
|
+
if (typeof error !== 'object' || error === null)
|
|
82
|
+
return false;
|
|
83
|
+
const e = error;
|
|
84
|
+
return e.name === 'InactiveRecipientsError' || e.code === 406;
|
|
85
|
+
};
|
|
86
|
+
/** Postmark parses the bounced addresses out of its own message for us. */
|
|
87
|
+
const inactiveRecipients = (error) => {
|
|
88
|
+
const recipients = error?.recipients;
|
|
89
|
+
return (Array.isArray(recipients) &&
|
|
90
|
+
recipients.every((r) => typeof r === 'string')) ?
|
|
91
|
+
recipients
|
|
92
|
+
: undefined;
|
|
93
|
+
};
|
|
94
|
+
/** One client per token, built on demand. `ServerClient` holds a connection. */
|
|
95
|
+
const clients = new Map();
|
|
96
|
+
const getClient = (postmark, injected) => {
|
|
97
|
+
if (injected)
|
|
98
|
+
return injected;
|
|
99
|
+
const { token } = postmark;
|
|
100
|
+
if (!token) {
|
|
101
|
+
throw new Error('Cannot send the sign-in email: no Postmark token is configured. Set POSTMARK_API_KEY, or let the dev flow skip the inbox (ADR-0007).');
|
|
102
|
+
}
|
|
103
|
+
let client = clients.get(token);
|
|
104
|
+
if (!client) {
|
|
105
|
+
client = new ServerClient(token);
|
|
106
|
+
clients.set(token, client);
|
|
107
|
+
}
|
|
108
|
+
return client;
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Render the sign-in email without sending it. Useful for a snapshot, and for
|
|
112
|
+
* a consumer wiring its own transport.
|
|
113
|
+
*
|
|
114
|
+
* ⚠️ **`render()` is async here.** react-email 6 / React 19 returns a
|
|
115
|
+
* `Promise<string>`; the 2.x examples all over the web are synchronous, and
|
|
116
|
+
* forgetting the `await` puts `[object Promise]` in the body of an email.
|
|
117
|
+
*/
|
|
118
|
+
export const renderSignInEmail = async (context) => ({
|
|
119
|
+
subject: buildSubject(context.appName),
|
|
120
|
+
html: await render(createElement(Template, context)),
|
|
121
|
+
text: buildText(context),
|
|
122
|
+
});
|
|
123
|
+
/**
|
|
124
|
+
* Render the sign-in email and post it to Postmark.
|
|
125
|
+
*
|
|
126
|
+
* @param message The address, the **confirm** URL, the app name, the link
|
|
127
|
+
* lifetime, and the Postmark config to send with.
|
|
128
|
+
* @param options.client A stand-in for Postmark's `ServerClient`. Supply it
|
|
129
|
+
* from a test or the fixture app; leave it out and a real client is built
|
|
130
|
+
* from `message.postmark.token` on first use and cached.
|
|
131
|
+
*
|
|
132
|
+
* @throws {InactiveRecipientError} when Postmark has deactivated the address —
|
|
133
|
+
* the `disabledAt` signal (ADR-0004). Deliberately not swallowed.
|
|
134
|
+
* @throws {Error} when no `token` is configured and no `client` was supplied.
|
|
135
|
+
*/
|
|
136
|
+
export const sendSignInEmail = async (message, options = {}) => {
|
|
137
|
+
const { to, postmark, ...context } = message;
|
|
138
|
+
const { subject, html, text } = await renderSignInEmail(context);
|
|
139
|
+
// Resolved outside the try so a missing token is never read as a bounce.
|
|
140
|
+
const client = getClient(postmark, options.client);
|
|
141
|
+
try {
|
|
142
|
+
await client.sendEmail({
|
|
143
|
+
From: postmark.from,
|
|
144
|
+
To: to,
|
|
145
|
+
Subject: subject,
|
|
146
|
+
HtmlBody: html,
|
|
147
|
+
TextBody: text,
|
|
148
|
+
MessageStream: postmark.messageStream,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
if (isInactiveRecipient(error)) {
|
|
153
|
+
throw new InactiveRecipientError(to, {
|
|
154
|
+
recipients: inactiveRecipients(error),
|
|
155
|
+
cause: error,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Root entry — types and things safe anywhere.
|
|
3
|
+
*
|
|
4
|
+
* Server code is behind `/server`, browser code behind `/client`, tables
|
|
5
|
+
* behind `/schema`. Keeping them apart is what stops a component importing
|
|
6
|
+
* something that touches Postmark or the database.
|
|
7
|
+
*/
|
|
8
|
+
export * from './types.js';
|
|
9
|
+
export * from './domains.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Root entry — types and things safe anywhere.
|
|
3
|
+
*
|
|
4
|
+
* Server code is behind `/server`, browser code behind `/client`, tables
|
|
5
|
+
* behind `/schema`. Keeping them apart is what stops a component importing
|
|
6
|
+
* something that touches Postmark or the database.
|
|
7
|
+
*/
|
|
8
|
+
export * from './types.js';
|
|
9
|
+
export * from './domains.js';
|