@draftlab/auth 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/node.d.ts +18 -0
- package/dist/adapters/node.js +71 -0
- package/dist/allow-CixonwTW.d.ts +59 -0
- package/dist/allow-DX5cehSc.js +63 -0
- package/dist/allow.d.ts +2 -0
- package/dist/allow.js +4 -0
- package/dist/base-DRutbxgL.js +422 -0
- package/dist/client.d.ts +413 -0
- package/dist/client.js +209 -0
- package/dist/code-l_uvMR1j.d.ts +212 -0
- package/dist/core-8WTqfnb4.d.ts +129 -0
- package/dist/core-CncE5rPg.js +498 -0
- package/dist/core.d.ts +9 -0
- package/dist/core.js +14 -0
- package/dist/error-CWAdNAzm.d.ts +243 -0
- package/dist/error-DgAKK7b2.js +237 -0
- package/dist/error.d.ts +2 -0
- package/dist/error.js +3 -0
- package/dist/form-6XKM_cOk.js +61 -0
- package/dist/icon-Ci5uqGB_.js +192 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +14 -0
- package/dist/keys-EEfxEGfO.js +140 -0
- package/dist/keys.d.ts +67 -0
- package/dist/keys.js +5 -0
- package/dist/oauth2-B7-6Z7Lc.js +155 -0
- package/dist/oauth2-DtKwtl8p.d.ts +176 -0
- package/dist/password-Cm0dRMwa.d.ts +385 -0
- package/dist/pkce-276Za_rZ.js +162 -0
- package/dist/pkce.d.ts +72 -0
- package/dist/pkce.js +3 -0
- package/dist/provider/code.d.ts +4 -0
- package/dist/provider/code.js +145 -0
- package/dist/provider/facebook.d.ts +137 -0
- package/dist/provider/facebook.js +85 -0
- package/dist/provider/github.d.ts +141 -0
- package/dist/provider/github.js +88 -0
- package/dist/provider/google.d.ts +113 -0
- package/dist/provider/google.js +62 -0
- package/dist/provider/oauth2.d.ts +4 -0
- package/dist/provider/oauth2.js +7 -0
- package/dist/provider/password.d.ts +4 -0
- package/dist/provider/password.js +366 -0
- package/dist/provider/provider.d.ts +3 -0
- package/dist/provider/provider.js +44 -0
- package/dist/provider-CwWMG-1l.d.ts +227 -0
- package/dist/random-SXMYlaVr.js +87 -0
- package/dist/random.d.ts +66 -0
- package/dist/random.js +3 -0
- package/dist/select-BjySLL8I.js +280 -0
- package/dist/storage/memory.d.ts +82 -0
- package/dist/storage/memory.js +127 -0
- package/dist/storage/storage.d.ts +2 -0
- package/dist/storage/storage.js +3 -0
- package/dist/storage/turso.d.ts +31 -0
- package/dist/storage/turso.js +117 -0
- package/dist/storage/unstorage.d.ts +38 -0
- package/dist/storage/unstorage.js +97 -0
- package/dist/storage-BEaqEPNQ.js +62 -0
- package/dist/storage-CxKerLlc.d.ts +162 -0
- package/dist/subject-DiQdRWGt.d.ts +62 -0
- package/dist/subject.d.ts +3 -0
- package/dist/subject.js +36 -0
- package/dist/theme-C9by7VXf.d.ts +209 -0
- package/dist/theme-CswaLtbW.js +120 -0
- package/dist/themes/theme.d.ts +2 -0
- package/dist/themes/theme.js +3 -0
- package/dist/types.d.ts +94 -0
- package/dist/types.js +0 -0
- package/dist/ui/base.d.ts +43 -0
- package/dist/ui/base.js +4 -0
- package/dist/ui/code.d.ts +158 -0
- package/dist/ui/code.js +197 -0
- package/dist/ui/form.d.ts +31 -0
- package/dist/ui/form.js +3 -0
- package/dist/ui/icon.d.ts +98 -0
- package/dist/ui/icon.js +3 -0
- package/dist/ui/password.d.ts +54 -0
- package/dist/ui/password.js +300 -0
- package/dist/ui/select.d.ts +233 -0
- package/dist/ui/select.js +6 -0
- package/dist/util-CSdHUFOo.js +108 -0
- package/dist/util-ChlgVqPN.d.ts +72 -0
- package/dist/util.d.ts +2 -0
- package/dist/util.js +3 -0
- package/package.json +63 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import "../theme-CswaLtbW.js";
|
|
2
|
+
import { Layout } from "../base-DRutbxgL.js";
|
|
3
|
+
import { FormAlert } from "../form-6XKM_cOk.js";
|
|
4
|
+
|
|
5
|
+
//#region src/ui/password.ts
|
|
6
|
+
/**
|
|
7
|
+
* Default text copy for all password authentication UI screens.
|
|
8
|
+
* All text can be customized via the copy prop.
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_COPY = {
|
|
11
|
+
error_email_taken: "There is already an account with this email.",
|
|
12
|
+
error_invalid_code: "Code is incorrect.",
|
|
13
|
+
error_invalid_email: "Email is not valid.",
|
|
14
|
+
error_invalid_password: "Password is incorrect.",
|
|
15
|
+
error_password_mismatch: "Passwords do not match.",
|
|
16
|
+
error_validation_error: "Password does not meet requirements.",
|
|
17
|
+
register_title: "Welcome to the app",
|
|
18
|
+
register_description: "Sign in with your email",
|
|
19
|
+
login_title: "Welcome to the app",
|
|
20
|
+
login_description: "Sign in with your email",
|
|
21
|
+
register: "Register",
|
|
22
|
+
register_prompt: "Don't have an account?",
|
|
23
|
+
login_prompt: "Already have an account?",
|
|
24
|
+
login: "Login",
|
|
25
|
+
change_prompt: "Forgot password?",
|
|
26
|
+
code_resend: "Resend code",
|
|
27
|
+
code_return: "Back to",
|
|
28
|
+
input_email: "Email",
|
|
29
|
+
input_password: "Password",
|
|
30
|
+
input_code: "Code",
|
|
31
|
+
input_repeat: "Repeat password",
|
|
32
|
+
button_continue: "Continue",
|
|
33
|
+
logo: "A"
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Creates a complete UI configuration for password-based authentication.
|
|
37
|
+
*/
|
|
38
|
+
const PasswordUI = (options) => {
|
|
39
|
+
const copy = {
|
|
40
|
+
...DEFAULT_COPY,
|
|
41
|
+
...options.copy
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Gets the appropriate error message for display.
|
|
45
|
+
*/
|
|
46
|
+
const getErrorMessage = (error) => {
|
|
47
|
+
if (!error?.type) return;
|
|
48
|
+
if (error.type === "validation_error" && "message" in error && error.message) return error.message;
|
|
49
|
+
return copy[`error_${error.type}`];
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
validatePassword: options.validatePassword,
|
|
53
|
+
sendCode: options.sendCode,
|
|
54
|
+
login: async (_req, form, error) => {
|
|
55
|
+
const formContent = `
|
|
56
|
+
<form data-component="form" method="post">
|
|
57
|
+
${FormAlert({ message: getErrorMessage(error) })}
|
|
58
|
+
|
|
59
|
+
<input
|
|
60
|
+
autocomplete="email"
|
|
61
|
+
data-component="input"
|
|
62
|
+
value="${form?.get("email")?.toString() || ""}"
|
|
63
|
+
name="email"
|
|
64
|
+
placeholder="${copy.input_email}"
|
|
65
|
+
required
|
|
66
|
+
type="email"
|
|
67
|
+
/>
|
|
68
|
+
|
|
69
|
+
<input
|
|
70
|
+
autocomplete="current-password"
|
|
71
|
+
data-component="input"
|
|
72
|
+
name="password"
|
|
73
|
+
placeholder="${copy.input_password}"
|
|
74
|
+
required
|
|
75
|
+
type="password"
|
|
76
|
+
/>
|
|
77
|
+
|
|
78
|
+
<button data-component="button" type="submit">
|
|
79
|
+
${copy.button_continue}
|
|
80
|
+
</button>
|
|
81
|
+
|
|
82
|
+
<div data-component="form-footer">
|
|
83
|
+
<span>
|
|
84
|
+
${copy.register_prompt}
|
|
85
|
+
<a data-component="link" href="./register">
|
|
86
|
+
${copy.register}
|
|
87
|
+
</a>
|
|
88
|
+
</span>
|
|
89
|
+
<a data-component="link" href="./change">
|
|
90
|
+
${copy.change_prompt}
|
|
91
|
+
</a>
|
|
92
|
+
</div>
|
|
93
|
+
</form>
|
|
94
|
+
`;
|
|
95
|
+
const html = Layout({ children: formContent });
|
|
96
|
+
return new Response(html, {
|
|
97
|
+
status: error ? 401 : 200,
|
|
98
|
+
headers: { "Content-Type": "text/html" }
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
register: async (_req, state, form, error) => {
|
|
102
|
+
const emailError = ["invalid_email", "email_taken"].includes(error?.type || "");
|
|
103
|
+
const passwordError = [
|
|
104
|
+
"invalid_password",
|
|
105
|
+
"password_mismatch",
|
|
106
|
+
"validation_error"
|
|
107
|
+
].includes(error?.type || "");
|
|
108
|
+
let formContent = "";
|
|
109
|
+
if (state.type === "start") formContent = `
|
|
110
|
+
<form data-component="form" method="post">
|
|
111
|
+
${FormAlert({ message: getErrorMessage(error) })}
|
|
112
|
+
|
|
113
|
+
<input name="action" type="hidden" value="register" />
|
|
114
|
+
|
|
115
|
+
<input
|
|
116
|
+
autocomplete="email"
|
|
117
|
+
data-component="input"
|
|
118
|
+
value="${emailError ? "" : form?.get("email")?.toString() || ""}"
|
|
119
|
+
name="email"
|
|
120
|
+
placeholder="${copy.input_email}"
|
|
121
|
+
required
|
|
122
|
+
type="email"
|
|
123
|
+
/>
|
|
124
|
+
|
|
125
|
+
<input
|
|
126
|
+
autocomplete="new-password"
|
|
127
|
+
data-component="input"
|
|
128
|
+
value="${passwordError ? "" : form?.get("password")?.toString() || ""}"
|
|
129
|
+
name="password"
|
|
130
|
+
placeholder="${copy.input_password}"
|
|
131
|
+
required
|
|
132
|
+
type="password"
|
|
133
|
+
/>
|
|
134
|
+
|
|
135
|
+
<input
|
|
136
|
+
autocomplete="new-password"
|
|
137
|
+
data-component="input"
|
|
138
|
+
name="repeat"
|
|
139
|
+
placeholder="${copy.input_repeat}"
|
|
140
|
+
required
|
|
141
|
+
type="password"
|
|
142
|
+
/>
|
|
143
|
+
|
|
144
|
+
<button data-component="button" type="submit">
|
|
145
|
+
${copy.button_continue}
|
|
146
|
+
</button>
|
|
147
|
+
|
|
148
|
+
<div data-component="form-footer">
|
|
149
|
+
<span>
|
|
150
|
+
${copy.login_prompt}
|
|
151
|
+
<a data-component="link" href="./authorize">
|
|
152
|
+
${copy.login}
|
|
153
|
+
</a>
|
|
154
|
+
</span>
|
|
155
|
+
</div>
|
|
156
|
+
</form>
|
|
157
|
+
`;
|
|
158
|
+
else if (state.type === "code") formContent = `
|
|
159
|
+
<form data-component="form" method="post">
|
|
160
|
+
${FormAlert({ message: getErrorMessage(error) })}
|
|
161
|
+
|
|
162
|
+
<input name="action" type="hidden" value="verify" />
|
|
163
|
+
|
|
164
|
+
<input
|
|
165
|
+
aria-label="6-digit verification code"
|
|
166
|
+
autocomplete="one-time-code"
|
|
167
|
+
data-component="input"
|
|
168
|
+
inputmode="numeric"
|
|
169
|
+
maxlength="6"
|
|
170
|
+
minlength="6"
|
|
171
|
+
name="code"
|
|
172
|
+
pattern="[0-9]{6}"
|
|
173
|
+
placeholder="${copy.input_code}"
|
|
174
|
+
required
|
|
175
|
+
type="text"
|
|
176
|
+
/>
|
|
177
|
+
|
|
178
|
+
<button data-component="button" type="submit">
|
|
179
|
+
${copy.button_continue}
|
|
180
|
+
</button>
|
|
181
|
+
</form>
|
|
182
|
+
`;
|
|
183
|
+
const html = Layout({ children: formContent });
|
|
184
|
+
return new Response(html, { headers: { "Content-Type": "text/html" } });
|
|
185
|
+
},
|
|
186
|
+
change: async (_req, state, form, error) => {
|
|
187
|
+
const passwordError = [
|
|
188
|
+
"invalid_password",
|
|
189
|
+
"password_mismatch",
|
|
190
|
+
"validation_error"
|
|
191
|
+
].includes(error?.type || "");
|
|
192
|
+
let formContent = "";
|
|
193
|
+
let additionalForms = "";
|
|
194
|
+
if (state.type === "start") formContent = `
|
|
195
|
+
<form data-component="form" method="post">
|
|
196
|
+
${FormAlert({ message: getErrorMessage(error) })}
|
|
197
|
+
|
|
198
|
+
<input name="action" type="hidden" value="code" />
|
|
199
|
+
|
|
200
|
+
<input
|
|
201
|
+
autocomplete="email"
|
|
202
|
+
data-component="input"
|
|
203
|
+
value="${form?.get("email")?.toString() || ""}"
|
|
204
|
+
name="email"
|
|
205
|
+
placeholder="${copy.input_email}"
|
|
206
|
+
required
|
|
207
|
+
type="email"
|
|
208
|
+
/>
|
|
209
|
+
|
|
210
|
+
<button data-component="button" type="submit">
|
|
211
|
+
${copy.button_continue}
|
|
212
|
+
</button>
|
|
213
|
+
</form>
|
|
214
|
+
`;
|
|
215
|
+
else if (state.type === "code") {
|
|
216
|
+
formContent = `
|
|
217
|
+
<form data-component="form" method="post">
|
|
218
|
+
${FormAlert({ message: getErrorMessage(error) })}
|
|
219
|
+
|
|
220
|
+
<input name="action" type="hidden" value="verify" />
|
|
221
|
+
|
|
222
|
+
<input
|
|
223
|
+
aria-label="6-digit verification code"
|
|
224
|
+
autocomplete="one-time-code"
|
|
225
|
+
data-component="input"
|
|
226
|
+
inputmode="numeric"
|
|
227
|
+
maxlength="6"
|
|
228
|
+
minlength="6"
|
|
229
|
+
name="code"
|
|
230
|
+
pattern="[0-9]{6}"
|
|
231
|
+
placeholder="${copy.input_code}"
|
|
232
|
+
required
|
|
233
|
+
type="text"
|
|
234
|
+
/>
|
|
235
|
+
|
|
236
|
+
<button data-component="button" type="submit">
|
|
237
|
+
${copy.button_continue}
|
|
238
|
+
</button>
|
|
239
|
+
</form>
|
|
240
|
+
`;
|
|
241
|
+
additionalForms = `
|
|
242
|
+
<form method="post">
|
|
243
|
+
<input name="action" type="hidden" value="code" />
|
|
244
|
+
<input name="email" type="hidden" value="${state.email}" />
|
|
245
|
+
|
|
246
|
+
<div data-component="form-footer">
|
|
247
|
+
<span>
|
|
248
|
+
${copy.code_return}
|
|
249
|
+
<a data-component="link" href="./authorize">
|
|
250
|
+
${copy.login.toLowerCase()}
|
|
251
|
+
</a>
|
|
252
|
+
</span>
|
|
253
|
+
<button data-component="link" type="submit">
|
|
254
|
+
${copy.code_resend}
|
|
255
|
+
</button>
|
|
256
|
+
</div>
|
|
257
|
+
</form>
|
|
258
|
+
`;
|
|
259
|
+
} else if (state.type === "update") formContent = `
|
|
260
|
+
<form data-component="form" method="post">
|
|
261
|
+
${FormAlert({ message: getErrorMessage(error) })}
|
|
262
|
+
|
|
263
|
+
<input name="action" type="hidden" value="update" />
|
|
264
|
+
|
|
265
|
+
<input
|
|
266
|
+
autocomplete="new-password"
|
|
267
|
+
data-component="input"
|
|
268
|
+
value="${passwordError ? "" : form?.get("password")?.toString() || ""}"
|
|
269
|
+
name="password"
|
|
270
|
+
placeholder="${copy.input_password}"
|
|
271
|
+
required
|
|
272
|
+
type="password"
|
|
273
|
+
/>
|
|
274
|
+
|
|
275
|
+
<input
|
|
276
|
+
autocomplete="new-password"
|
|
277
|
+
data-component="input"
|
|
278
|
+
value="${passwordError ? "" : form?.get("repeat")?.toString() || ""}"
|
|
279
|
+
name="repeat"
|
|
280
|
+
placeholder="${copy.input_repeat}"
|
|
281
|
+
required
|
|
282
|
+
type="password"
|
|
283
|
+
/>
|
|
284
|
+
|
|
285
|
+
<button data-component="button" type="submit">
|
|
286
|
+
${copy.button_continue}
|
|
287
|
+
</button>
|
|
288
|
+
</form>
|
|
289
|
+
`;
|
|
290
|
+
const html = Layout({ children: formContent + additionalForms });
|
|
291
|
+
return new Response(html, {
|
|
292
|
+
status: error ? 400 : 200,
|
|
293
|
+
headers: { "Content-Type": "text/html" }
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
//#endregion
|
|
300
|
+
export { PasswordUI };
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
//#region src/ui/select.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Pre-built UI component for OAuth provider selection.
|
|
4
|
+
* Displays a clean selection interface where users can choose their preferred authentication method.
|
|
5
|
+
*
|
|
6
|
+
* ## Features
|
|
7
|
+
*
|
|
8
|
+
* - **Multiple providers**: Support for popular OAuth providers with built-in icons
|
|
9
|
+
* - **Customizable displays**: Override provider names and copy text
|
|
10
|
+
* - **Conditional visibility**: Hide specific providers using configuration
|
|
11
|
+
* - **Type safety**: Full TypeScript support with autocomplete for known providers
|
|
12
|
+
* - **Accessibility**: Proper ARIA labels and semantic markup
|
|
13
|
+
*
|
|
14
|
+
* ## Usage
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { Select } from "@draftauth/core/ui/select"
|
|
18
|
+
*
|
|
19
|
+
* export default issuer({
|
|
20
|
+
* select: Select({
|
|
21
|
+
* copy: {
|
|
22
|
+
* button_provider: "Continuar com"
|
|
23
|
+
* },
|
|
24
|
+
* displays: {
|
|
25
|
+
* code: "Código PIN",
|
|
26
|
+
* github: "Conta GitHub"
|
|
27
|
+
* },
|
|
28
|
+
* providers: {
|
|
29
|
+
* github: {
|
|
30
|
+
* hide: true
|
|
31
|
+
* },
|
|
32
|
+
* google: {
|
|
33
|
+
* display: "Google"
|
|
34
|
+
* }
|
|
35
|
+
* }
|
|
36
|
+
* })
|
|
37
|
+
* // ...
|
|
38
|
+
* })
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* ## Provider Configuration
|
|
42
|
+
*
|
|
43
|
+
* Each provider can be individually configured:
|
|
44
|
+
*
|
|
45
|
+
* - **hide**: Boolean to control visibility
|
|
46
|
+
* - **display**: Custom display name override
|
|
47
|
+
*
|
|
48
|
+
* Global display overrides are applied through the `displays` property, which affects
|
|
49
|
+
* all providers of that type unless specifically overridden at the provider level.
|
|
50
|
+
*
|
|
51
|
+
* @packageDocumentation
|
|
52
|
+
*/
|
|
53
|
+
/**
|
|
54
|
+
* Default copy text used throughout the select UI.
|
|
55
|
+
* These values are used when no custom copy is provided.
|
|
56
|
+
*/
|
|
57
|
+
declare const DEFAULT_COPY: {
|
|
58
|
+
/**
|
|
59
|
+
* Copy for the provider button prefix text.
|
|
60
|
+
* @example "Continue with GitHub" where "Continue with" is this value
|
|
61
|
+
*/
|
|
62
|
+
button_provider: string;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Default display names for all known provider types.
|
|
66
|
+
* These provide consistent naming across the application and serve as fallbacks
|
|
67
|
+
* when no custom display name is configured.
|
|
68
|
+
*/
|
|
69
|
+
declare const DEFAULT_DISPLAY: {
|
|
70
|
+
steam: string;
|
|
71
|
+
twitch: string;
|
|
72
|
+
google: string;
|
|
73
|
+
github: string;
|
|
74
|
+
apple: string;
|
|
75
|
+
code: string;
|
|
76
|
+
x: string;
|
|
77
|
+
facebook: string;
|
|
78
|
+
microsoft: string;
|
|
79
|
+
slack: string;
|
|
80
|
+
password: string;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Customizable copy text for the Select UI component.
|
|
84
|
+
* Allows overriding default text to support internationalization or custom branding.
|
|
85
|
+
*/
|
|
86
|
+
type SelectCopy = typeof DEFAULT_COPY;
|
|
87
|
+
/**
|
|
88
|
+
* Union type of all known provider types that have default display names and icons.
|
|
89
|
+
* These types get autocompletion support in the displays configuration.
|
|
90
|
+
*/
|
|
91
|
+
type KnownProviderType = keyof typeof DEFAULT_DISPLAY;
|
|
92
|
+
/**
|
|
93
|
+
* Configuration options for individual providers in the select UI.
|
|
94
|
+
* Each provider can be individually controlled and customized.
|
|
95
|
+
*/
|
|
96
|
+
interface ProviderConfig {
|
|
97
|
+
/**
|
|
98
|
+
* Whether to hide the provider from the select UI.
|
|
99
|
+
* When true, the provider will not appear in the selection interface.
|
|
100
|
+
*
|
|
101
|
+
* @default false
|
|
102
|
+
*/
|
|
103
|
+
hide?: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Custom display name for this specific provider instance.
|
|
106
|
+
* Overrides both the default display name and any global display override.
|
|
107
|
+
*
|
|
108
|
+
* @example "My Custom Google Login"
|
|
109
|
+
*/
|
|
110
|
+
display?: string;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Configuration properties for the Select UI component.
|
|
114
|
+
* Provides comprehensive customization of appearance and behavior.
|
|
115
|
+
*/
|
|
116
|
+
interface SelectProps {
|
|
117
|
+
/**
|
|
118
|
+
* Provider-specific configurations mapped by provider key.
|
|
119
|
+
* Each key corresponds to a provider instance, allowing individual customization.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts
|
|
123
|
+
* {
|
|
124
|
+
* github: {
|
|
125
|
+
* hide: true
|
|
126
|
+
* },
|
|
127
|
+
* google: {
|
|
128
|
+
* display: "Google Account"
|
|
129
|
+
* },
|
|
130
|
+
* "custom-provider": {
|
|
131
|
+
* display: "Custom OAuth"
|
|
132
|
+
* }
|
|
133
|
+
* }
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
providers?: Record<string, ProviderConfig>;
|
|
137
|
+
/**
|
|
138
|
+
* Custom copy text overrides for UI elements.
|
|
139
|
+
* Useful for internationalization and custom branding.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```ts
|
|
143
|
+
* {
|
|
144
|
+
* button_provider: "Sign in with"
|
|
145
|
+
* }
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
copy?: Partial<SelectCopy>;
|
|
149
|
+
/**
|
|
150
|
+
* Global display name overrides for provider types.
|
|
151
|
+
*
|
|
152
|
+
* This allows you to override the default display names globally for all providers
|
|
153
|
+
* of a specific type. For known provider types, you get full autocompletion support.
|
|
154
|
+
* Custom provider types are also supported.
|
|
155
|
+
*
|
|
156
|
+
* The priority order for display names is:
|
|
157
|
+
* 1. Provider-specific `display` in `providers` config
|
|
158
|
+
* 2. Type-specific `displays` override (this property)
|
|
159
|
+
* 3. Default display name from `DEFAULT_DISPLAY`
|
|
160
|
+
* 4. Provider type string as fallback
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```ts
|
|
164
|
+
* {
|
|
165
|
+
* displays: {
|
|
166
|
+
* code: "PIN Code", // ✅ Autocomplete available
|
|
167
|
+
* github: "GitHub Account", // ✅ Autocomplete available
|
|
168
|
+
* customType: "Custom" // ✅ Also works for unknown types
|
|
169
|
+
* }
|
|
170
|
+
* }
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
displays?: Partial<Record<KnownProviderType, string>> & Record<string, string>;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Creates a provider selection UI component for OAuth authentication.
|
|
177
|
+
*
|
|
178
|
+
* This component generates a complete authentication provider selection interface that:
|
|
179
|
+
* - Displays available OAuth providers as clickable buttons
|
|
180
|
+
* - Includes appropriate icons for recognized providers
|
|
181
|
+
* - Supports custom theming and internationalization
|
|
182
|
+
* - Provides accessible markup with proper ARIA attributes
|
|
183
|
+
* - Handles provider visibility and custom display names
|
|
184
|
+
*
|
|
185
|
+
* @param props - Configuration options for customizing the select UI
|
|
186
|
+
* @returns An async function that generates the HTML response for the selection interface
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```ts
|
|
190
|
+
* // Basic usage with defaults
|
|
191
|
+
* const selectUI = Select()
|
|
192
|
+
*
|
|
193
|
+
* // Customized with provider configuration
|
|
194
|
+
* const selectUI = Select({
|
|
195
|
+
* copy: {
|
|
196
|
+
* button_provider: "Sign in with"
|
|
197
|
+
* },
|
|
198
|
+
* displays: {
|
|
199
|
+
* github: "GitHub Account",
|
|
200
|
+
* code: "Email Code"
|
|
201
|
+
* },
|
|
202
|
+
* providers: {
|
|
203
|
+
* facebook: { hide: true },
|
|
204
|
+
* google: { display: "Google Workspace" }
|
|
205
|
+
* }
|
|
206
|
+
* })
|
|
207
|
+
*
|
|
208
|
+
* // Use in issuer configuration
|
|
209
|
+
* export default issuer({
|
|
210
|
+
* select: selectUI,
|
|
211
|
+
* // ... other configuration
|
|
212
|
+
* })
|
|
213
|
+
* ```
|
|
214
|
+
*
|
|
215
|
+
* ## Provider Resolution Logic
|
|
216
|
+
*
|
|
217
|
+
* Display names are resolved in the following priority order:
|
|
218
|
+
* 1. Provider-specific `display` property
|
|
219
|
+
* 2. Global `displays` type override
|
|
220
|
+
* 3. Default display name from `DEFAULT_DISPLAY`
|
|
221
|
+
* 4. Provider type string as final fallback
|
|
222
|
+
*
|
|
223
|
+
* ## Accessibility Features
|
|
224
|
+
*
|
|
225
|
+
* The generated UI includes:
|
|
226
|
+
* - Semantic HTML structure
|
|
227
|
+
* - Proper button roles and keyboard navigation
|
|
228
|
+
* - Icon accessibility with `aria-hidden="true"`
|
|
229
|
+
* - Screen reader friendly provider names
|
|
230
|
+
*/
|
|
231
|
+
declare const Select: (props?: SelectProps) => (providers: Record<string, string>, _req: Request) => Promise<Response>;
|
|
232
|
+
//#endregion
|
|
233
|
+
export { KnownProviderType, ProviderConfig, Select, SelectCopy, SelectProps };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
//#region src/util.ts
|
|
2
|
+
/**
|
|
3
|
+
* Constructs a complete URL relative to the current request context.
|
|
4
|
+
* Handles proxy headers (x-forwarded-*) to ensure correct URL generation
|
|
5
|
+
* in containerized and load-balanced environments.
|
|
6
|
+
*
|
|
7
|
+
* @param ctx - Router context containing request information
|
|
8
|
+
* @param path - Relative path to append to the base URL
|
|
9
|
+
* @returns Complete URL string with proper protocol, host, and port
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const callbackUrl = getRelativeUrl(ctx, "/callback")
|
|
14
|
+
* // Returns: "https://myapp.com/auth/callback"
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
const getRelativeUrl = (ctx, path) => {
|
|
18
|
+
const result = new URL(path, ctx.request.url);
|
|
19
|
+
result.host = ctx.header("x-forwarded-host") || result.host;
|
|
20
|
+
result.protocol = ctx.header("x-forwarded-proto") || result.protocol;
|
|
21
|
+
result.port = ctx.header("x-forwarded-port") || result.port;
|
|
22
|
+
return result.toString();
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* List of known two-part top-level domains that require special handling
|
|
26
|
+
* for domain matching. These domains have an additional level that should
|
|
27
|
+
* be considered when determining effective domain boundaries.
|
|
28
|
+
*/
|
|
29
|
+
const twoPartTlds = [
|
|
30
|
+
"co.uk",
|
|
31
|
+
"co.jp",
|
|
32
|
+
"co.kr",
|
|
33
|
+
"co.nz",
|
|
34
|
+
"co.za",
|
|
35
|
+
"co.in",
|
|
36
|
+
"com.au",
|
|
37
|
+
"com.br",
|
|
38
|
+
"com.cn",
|
|
39
|
+
"com.mx",
|
|
40
|
+
"com.tw",
|
|
41
|
+
"net.au",
|
|
42
|
+
"org.uk",
|
|
43
|
+
"ne.jp",
|
|
44
|
+
"ac.uk",
|
|
45
|
+
"gov.uk",
|
|
46
|
+
"edu.au",
|
|
47
|
+
"gov.au"
|
|
48
|
+
];
|
|
49
|
+
/**
|
|
50
|
+
* Determines if two domain names are considered a match for security purposes.
|
|
51
|
+
* Uses effective TLD+1 matching to allow subdomains while preventing
|
|
52
|
+
* unauthorized cross-domain requests.
|
|
53
|
+
*
|
|
54
|
+
* @param a - First domain name to compare
|
|
55
|
+
* @param b - Second domain name to compare
|
|
56
|
+
* @returns True if domains are considered a security match
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* isDomainMatch("app.example.com", "auth.example.com") // true
|
|
61
|
+
* isDomainMatch("example.com", "evil.com") // false
|
|
62
|
+
* isDomainMatch("app.co.uk", "auth.co.uk") // true (handles two-part TLD)
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
const isDomainMatch = (a, b) => {
|
|
66
|
+
if (a === b) return true;
|
|
67
|
+
const partsA = a.split(".");
|
|
68
|
+
const partsB = b.split(".");
|
|
69
|
+
const hasTwoPartTld = twoPartTlds.some((tld) => a.endsWith(`.${tld}`) || b.endsWith(`.${tld}`));
|
|
70
|
+
const numParts = hasTwoPartTld ? -3 : -2;
|
|
71
|
+
const min = Math.min(partsA.length, partsB.length, numParts);
|
|
72
|
+
const tailA = partsA.slice(min).join(".");
|
|
73
|
+
const tailB = partsB.slice(min).join(".");
|
|
74
|
+
return tailA === tailB;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Creates a lazy-evaluated function that caches the result of the first execution.
|
|
78
|
+
* Subsequent calls return the cached value without re-executing the function.
|
|
79
|
+
*
|
|
80
|
+
* @template T - The return type of the lazy function
|
|
81
|
+
* @param fn - Function to execute lazily
|
|
82
|
+
* @returns Function that returns the cached result
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* const expensiveOperation = lazy(() => {
|
|
87
|
+
* // Computing... (only logs once)
|
|
88
|
+
* return heavyComputation()
|
|
89
|
+
* })
|
|
90
|
+
*
|
|
91
|
+
* const result1 = expensiveOperation() // Executes and caches
|
|
92
|
+
* const result2 = expensiveOperation() // Returns cached value
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
const lazy = (fn) => {
|
|
96
|
+
let value;
|
|
97
|
+
let hasValue = false;
|
|
98
|
+
return () => {
|
|
99
|
+
if (!hasValue) {
|
|
100
|
+
value = fn();
|
|
101
|
+
hasValue = true;
|
|
102
|
+
}
|
|
103
|
+
return value;
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
//#endregion
|
|
108
|
+
export { getRelativeUrl, isDomainMatch, lazy };
|