@draftlab/auth 0.15.1 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/dist/esm/allow.js +26 -0
  2. package/dist/esm/client.js +254 -0
  3. package/dist/esm/core.js +597 -0
  4. package/dist/esm/css.d.js +0 -0
  5. package/dist/esm/error.js +88 -0
  6. package/dist/esm/index.js +5 -0
  7. package/dist/esm/keys.js +126 -0
  8. package/dist/esm/mutex.js +53 -0
  9. package/dist/esm/pkce.js +87 -0
  10. package/dist/esm/provider/apple.js +15 -0
  11. package/dist/esm/provider/code.js +62 -0
  12. package/dist/esm/provider/discord.js +15 -0
  13. package/dist/esm/provider/facebook.js +15 -0
  14. package/dist/esm/provider/github.js +15 -0
  15. package/dist/esm/provider/gitlab.js +15 -0
  16. package/dist/esm/provider/google.js +16 -0
  17. package/dist/esm/provider/linkedin.js +15 -0
  18. package/dist/esm/provider/magiclink.js +83 -0
  19. package/dist/esm/provider/microsoft.js +15 -0
  20. package/dist/esm/provider/oauth2.js +130 -0
  21. package/dist/esm/provider/password.js +331 -0
  22. package/dist/esm/provider/provider.js +18 -0
  23. package/dist/esm/provider/reddit.js +15 -0
  24. package/dist/esm/provider/slack.js +15 -0
  25. package/dist/esm/provider/spotify.js +15 -0
  26. package/dist/esm/provider/twitch.js +15 -0
  27. package/dist/esm/provider/vercel.js +17 -0
  28. package/dist/esm/random.js +40 -0
  29. package/dist/esm/revocation.js +27 -0
  30. package/dist/esm/storage/memory.js +110 -0
  31. package/dist/esm/storage/storage.js +56 -0
  32. package/dist/esm/storage/turso.js +93 -0
  33. package/dist/esm/storage/unstorage.js +78 -0
  34. package/dist/esm/subject.js +7 -0
  35. package/dist/esm/themes/theme.js +115 -0
  36. package/dist/esm/toolkit/client.js +119 -0
  37. package/dist/esm/toolkit/index.js +25 -0
  38. package/dist/esm/toolkit/providers/facebook.js +11 -0
  39. package/dist/esm/toolkit/providers/github.js +11 -0
  40. package/dist/esm/toolkit/providers/google.js +11 -0
  41. package/dist/esm/toolkit/providers/strategy.js +0 -0
  42. package/dist/esm/toolkit/storage.js +81 -0
  43. package/dist/esm/toolkit/utils.js +18 -0
  44. package/dist/esm/types.js +0 -0
  45. package/dist/esm/ui/base.js +478 -0
  46. package/dist/esm/ui/code.js +186 -0
  47. package/dist/esm/ui/form.js +46 -0
  48. package/dist/esm/ui/icon.js +242 -0
  49. package/dist/esm/ui/magiclink.js +158 -0
  50. package/dist/esm/ui/password.js +435 -0
  51. package/dist/esm/ui/select.js +102 -0
  52. package/dist/esm/util.js +59 -0
  53. package/dist/{allow.d.mts → types/allow.d.ts} +9 -11
  54. package/dist/types/allow.d.ts.map +1 -0
  55. package/dist/types/client.d.ts +462 -0
  56. package/dist/types/client.d.ts.map +1 -0
  57. package/dist/types/core.d.ts +113 -0
  58. package/dist/types/core.d.ts.map +1 -0
  59. package/dist/{error.d.mts → types/error.d.ts} +95 -97
  60. package/dist/types/error.d.ts.map +1 -0
  61. package/dist/types/index.d.ts +2 -0
  62. package/dist/types/index.d.ts.map +1 -0
  63. package/dist/{keys.d.mts → types/keys.d.ts} +20 -23
  64. package/dist/types/keys.d.ts.map +1 -0
  65. package/dist/types/mutex.d.ts +42 -0
  66. package/dist/types/mutex.d.ts.map +1 -0
  67. package/dist/{pkce.d.mts → types/pkce.d.ts} +10 -11
  68. package/dist/types/pkce.d.ts.map +1 -0
  69. package/dist/types/provider/apple.d.ts +197 -0
  70. package/dist/types/provider/apple.d.ts.map +1 -0
  71. package/dist/types/provider/code.d.ts +288 -0
  72. package/dist/types/provider/code.d.ts.map +1 -0
  73. package/dist/types/provider/discord.d.ts +206 -0
  74. package/dist/types/provider/discord.d.ts.map +1 -0
  75. package/dist/types/provider/facebook.d.ts +200 -0
  76. package/dist/types/provider/facebook.d.ts.map +1 -0
  77. package/dist/types/provider/github.d.ts +220 -0
  78. package/dist/types/provider/github.d.ts.map +1 -0
  79. package/dist/types/provider/gitlab.d.ts +180 -0
  80. package/dist/types/provider/gitlab.d.ts.map +1 -0
  81. package/dist/types/provider/google.d.ts +158 -0
  82. package/dist/types/provider/google.d.ts.map +1 -0
  83. package/dist/types/provider/linkedin.d.ts +190 -0
  84. package/dist/types/provider/linkedin.d.ts.map +1 -0
  85. package/dist/types/provider/magiclink.d.ts +141 -0
  86. package/dist/types/provider/magiclink.d.ts.map +1 -0
  87. package/dist/types/provider/microsoft.d.ts +247 -0
  88. package/dist/types/provider/microsoft.d.ts.map +1 -0
  89. package/dist/types/provider/oauth2.d.ts +229 -0
  90. package/dist/types/provider/oauth2.d.ts.map +1 -0
  91. package/dist/types/provider/password.d.ts +408 -0
  92. package/dist/types/provider/password.d.ts.map +1 -0
  93. package/dist/types/provider/provider.d.ts +226 -0
  94. package/dist/types/provider/provider.d.ts.map +1 -0
  95. package/dist/types/provider/reddit.d.ts +159 -0
  96. package/dist/types/provider/reddit.d.ts.map +1 -0
  97. package/dist/types/provider/slack.d.ts +171 -0
  98. package/dist/types/provider/slack.d.ts.map +1 -0
  99. package/dist/types/provider/spotify.d.ts +168 -0
  100. package/dist/types/provider/spotify.d.ts.map +1 -0
  101. package/dist/types/provider/twitch.d.ts +163 -0
  102. package/dist/types/provider/twitch.d.ts.map +1 -0
  103. package/dist/types/provider/vercel.d.ts +294 -0
  104. package/dist/types/provider/vercel.d.ts.map +1 -0
  105. package/dist/{random.d.mts → types/random.d.ts} +4 -6
  106. package/dist/types/random.d.ts.map +1 -0
  107. package/dist/types/revocation.d.ts +76 -0
  108. package/dist/types/revocation.d.ts.map +1 -0
  109. package/dist/{storage/memory.d.mts → types/storage/memory.d.ts} +17 -20
  110. package/dist/types/storage/memory.d.ts.map +1 -0
  111. package/dist/types/storage/storage.d.ts +177 -0
  112. package/dist/types/storage/storage.d.ts.map +1 -0
  113. package/dist/{storage/turso.d.mts → types/storage/turso.d.ts} +4 -7
  114. package/dist/types/storage/turso.d.ts.map +1 -0
  115. package/dist/{storage/unstorage.d.mts → types/storage/unstorage.d.ts} +12 -10
  116. package/dist/types/storage/unstorage.d.ts.map +1 -0
  117. package/dist/types/subject.d.ts +115 -0
  118. package/dist/types/subject.d.ts.map +1 -0
  119. package/dist/types/themes/theme.d.ts +207 -0
  120. package/dist/types/themes/theme.d.ts.map +1 -0
  121. package/dist/types/toolkit/client.d.ts +235 -0
  122. package/dist/types/toolkit/client.d.ts.map +1 -0
  123. package/dist/types/toolkit/index.d.ts +45 -0
  124. package/dist/types/toolkit/index.d.ts.map +1 -0
  125. package/dist/types/toolkit/providers/facebook.d.ts +8 -0
  126. package/dist/types/toolkit/providers/facebook.d.ts.map +1 -0
  127. package/dist/types/toolkit/providers/github.d.ts +8 -0
  128. package/dist/types/toolkit/providers/github.d.ts.map +1 -0
  129. package/dist/types/toolkit/providers/google.d.ts +8 -0
  130. package/dist/types/toolkit/providers/google.d.ts.map +1 -0
  131. package/dist/types/toolkit/providers/strategy.d.ts +38 -0
  132. package/dist/types/toolkit/providers/strategy.d.ts.map +1 -0
  133. package/dist/{toolkit/storage.d.mts → types/toolkit/storage.d.ts} +37 -39
  134. package/dist/types/toolkit/storage.d.ts.map +1 -0
  135. package/dist/{toolkit/utils.d.mts → types/toolkit/utils.d.ts} +2 -4
  136. package/dist/types/toolkit/utils.d.ts.map +1 -0
  137. package/dist/types/types.d.ts +92 -0
  138. package/dist/types/types.d.ts.map +1 -0
  139. package/dist/types/ui/base.d.ts +18 -0
  140. package/dist/types/ui/base.d.ts.map +1 -0
  141. package/dist/types/ui/code.d.ts +43 -0
  142. package/dist/types/ui/code.d.ts.map +1 -0
  143. package/dist/types/ui/form.d.ts +24 -0
  144. package/dist/types/ui/form.d.ts.map +1 -0
  145. package/dist/types/ui/icon.d.ts +60 -0
  146. package/dist/types/ui/icon.d.ts.map +1 -0
  147. package/dist/types/ui/magiclink.d.ts +41 -0
  148. package/dist/types/ui/magiclink.d.ts.map +1 -0
  149. package/dist/types/ui/password.d.ts +43 -0
  150. package/dist/types/ui/password.d.ts.map +1 -0
  151. package/dist/types/ui/select.d.ts +33 -0
  152. package/dist/types/ui/select.d.ts.map +1 -0
  153. package/dist/{util.d.mts → types/util.d.ts} +11 -12
  154. package/dist/types/util.d.ts.map +1 -0
  155. package/package.json +11 -16
  156. package/dist/adapters/node.d.mts +0 -17
  157. package/dist/adapters/node.mjs +0 -69
  158. package/dist/allow.mjs +0 -63
  159. package/dist/client.d.mts +0 -462
  160. package/dist/client.mjs +0 -284
  161. package/dist/core.d.mts +0 -109
  162. package/dist/core.mjs +0 -595
  163. package/dist/error.mjs +0 -237
  164. package/dist/index.d.mts +0 -2
  165. package/dist/index.mjs +0 -3
  166. package/dist/keys.mjs +0 -146
  167. package/dist/mutex.d.mts +0 -44
  168. package/dist/mutex.mjs +0 -110
  169. package/dist/pkce.mjs +0 -157
  170. package/dist/provider/apple.d.mts +0 -110
  171. package/dist/provider/apple.mjs +0 -164
  172. package/dist/provider/code.d.mts +0 -218
  173. package/dist/provider/code.mjs +0 -246
  174. package/dist/provider/discord.d.mts +0 -145
  175. package/dist/provider/discord.mjs +0 -156
  176. package/dist/provider/facebook.d.mts +0 -141
  177. package/dist/provider/facebook.mjs +0 -150
  178. package/dist/provider/github.d.mts +0 -139
  179. package/dist/provider/github.mjs +0 -169
  180. package/dist/provider/gitlab.d.mts +0 -105
  181. package/dist/provider/gitlab.mjs +0 -147
  182. package/dist/provider/google.d.mts +0 -111
  183. package/dist/provider/google.mjs +0 -109
  184. package/dist/provider/linkedin.d.mts +0 -131
  185. package/dist/provider/linkedin.mjs +0 -142
  186. package/dist/provider/magiclink.d.mts +0 -79
  187. package/dist/provider/magiclink.mjs +0 -143
  188. package/dist/provider/microsoft.d.mts +0 -177
  189. package/dist/provider/microsoft.mjs +0 -177
  190. package/dist/provider/oauth2.d.mts +0 -175
  191. package/dist/provider/oauth2.mjs +0 -222
  192. package/dist/provider/passkey.d.mts +0 -103
  193. package/dist/provider/passkey.mjs +0 -320
  194. package/dist/provider/password.d.mts +0 -384
  195. package/dist/provider/password.mjs +0 -363
  196. package/dist/provider/provider.d.mts +0 -225
  197. package/dist/provider/provider.mjs +0 -44
  198. package/dist/provider/reddit.d.mts +0 -106
  199. package/dist/provider/reddit.mjs +0 -127
  200. package/dist/provider/slack.d.mts +0 -113
  201. package/dist/provider/slack.mjs +0 -138
  202. package/dist/provider/spotify.d.mts +0 -112
  203. package/dist/provider/spotify.mjs +0 -135
  204. package/dist/provider/totp.d.mts +0 -111
  205. package/dist/provider/totp.mjs +0 -191
  206. package/dist/provider/twitch.d.mts +0 -107
  207. package/dist/provider/twitch.mjs +0 -131
  208. package/dist/provider/vercel.d.mts +0 -176
  209. package/dist/provider/vercel.mjs +0 -230
  210. package/dist/random.mjs +0 -86
  211. package/dist/revocation.d.mts +0 -54
  212. package/dist/revocation.mjs +0 -63
  213. package/dist/router/context.d.mts +0 -21
  214. package/dist/router/context.mjs +0 -193
  215. package/dist/router/cookies.d.mts +0 -8
  216. package/dist/router/cookies.mjs +0 -13
  217. package/dist/router/index.d.mts +0 -21
  218. package/dist/router/index.mjs +0 -107
  219. package/dist/router/matcher.d.mts +0 -15
  220. package/dist/router/matcher.mjs +0 -76
  221. package/dist/router/middleware/cors.d.mts +0 -15
  222. package/dist/router/middleware/cors.mjs +0 -114
  223. package/dist/router/safe-request.d.mts +0 -52
  224. package/dist/router/safe-request.mjs +0 -160
  225. package/dist/router/types.d.mts +0 -67
  226. package/dist/router/types.mjs +0 -1
  227. package/dist/router/variables.d.mts +0 -12
  228. package/dist/router/variables.mjs +0 -20
  229. package/dist/storage/memory.mjs +0 -125
  230. package/dist/storage/storage.d.mts +0 -179
  231. package/dist/storage/storage.mjs +0 -104
  232. package/dist/storage/turso.mjs +0 -117
  233. package/dist/storage/unstorage.mjs +0 -103
  234. package/dist/subject.d.mts +0 -61
  235. package/dist/subject.mjs +0 -36
  236. package/dist/themes/theme.d.mts +0 -209
  237. package/dist/themes/theme.mjs +0 -120
  238. package/dist/toolkit/client.d.mts +0 -168
  239. package/dist/toolkit/client.mjs +0 -209
  240. package/dist/toolkit/index.d.mts +0 -9
  241. package/dist/toolkit/index.mjs +0 -9
  242. package/dist/toolkit/providers/facebook.d.mts +0 -11
  243. package/dist/toolkit/providers/facebook.mjs +0 -16
  244. package/dist/toolkit/providers/github.d.mts +0 -11
  245. package/dist/toolkit/providers/github.mjs +0 -16
  246. package/dist/toolkit/providers/google.d.mts +0 -11
  247. package/dist/toolkit/providers/google.mjs +0 -20
  248. package/dist/toolkit/providers/strategy.d.mts +0 -40
  249. package/dist/toolkit/providers/strategy.mjs +0 -1
  250. package/dist/toolkit/storage.mjs +0 -157
  251. package/dist/toolkit/utils.mjs +0 -30
  252. package/dist/types.d.mts +0 -94
  253. package/dist/types.mjs +0 -1
  254. package/dist/ui/base.d.mts +0 -29
  255. package/dist/ui/base.mjs +0 -407
  256. package/dist/ui/code.d.mts +0 -42
  257. package/dist/ui/code.mjs +0 -173
  258. package/dist/ui/form.d.mts +0 -31
  259. package/dist/ui/form.mjs +0 -49
  260. package/dist/ui/icon.d.mts +0 -57
  261. package/dist/ui/icon.mjs +0 -247
  262. package/dist/ui/magiclink.d.mts +0 -40
  263. package/dist/ui/magiclink.mjs +0 -152
  264. package/dist/ui/passkey.d.mts +0 -26
  265. package/dist/ui/passkey.mjs +0 -323
  266. package/dist/ui/password.d.mts +0 -41
  267. package/dist/ui/password.mjs +0 -402
  268. package/dist/ui/select.d.mts +0 -33
  269. package/dist/ui/select.mjs +0 -98
  270. package/dist/ui/totp.d.mts +0 -33
  271. package/dist/ui/totp.mjs +0 -270
  272. package/dist/util.mjs +0 -128
@@ -0,0 +1,331 @@
1
+ // src/provider/password.ts
2
+ import { UnknownStateError } from "../error.js";
3
+ import { generateUnbiasedDigits, timingSafeCompare } from "../random.js";
4
+ import { Storage } from "../storage/storage.js";
5
+ import { getRelativeUrl } from "../util.js";
6
+ import { TextEncoder } from "node:util";
7
+ import * as jose from "jose";
8
+ import { randomBytes, scrypt, timingSafeEqual } from "node:crypto";
9
+ var PasswordProvider = (config) => {
10
+ const hasher = config.hasher ?? ScryptHasher();
11
+ const generateCode = () => {
12
+ return generateUnbiasedDigits(config.length ?? 6);
13
+ };
14
+ return {
15
+ type: "password",
16
+ init(routes, ctx) {
17
+ routes.get("/authorize", async (c) => ctx.forward(c, await config.login(c.req.raw)));
18
+ routes.post("/authorize", async (c) => {
19
+ const formData = await c.req.formData();
20
+ const error = async (err) => {
21
+ return ctx.forward(c, await config.login(c.req.raw, formData, err));
22
+ };
23
+ const email = formData.get("email")?.toString()?.toLowerCase();
24
+ if (!email) {
25
+ return error({ type: "invalid_email" });
26
+ }
27
+ const storedHash = await Storage.get(ctx.storage, [
28
+ "email",
29
+ email,
30
+ "password"
31
+ ]);
32
+ const password = formData.get("password")?.toString();
33
+ if (!(password && storedHash && await hasher.verify(password, storedHash))) {
34
+ return error({ type: "invalid_password" });
35
+ }
36
+ return ctx.success(c, { email }, {
37
+ invalidate: async (subject) => {
38
+ await Storage.set(ctx.storage, ["email", email, "subject"], subject);
39
+ }
40
+ });
41
+ });
42
+ routes.get("/register", async (c) => {
43
+ const state = { type: "start" };
44
+ await ctx.set(c, "provider", 60 * 60 * 24, state);
45
+ return ctx.forward(c, await config.register(c.req.raw, state));
46
+ });
47
+ routes.post("/register", async (c) => {
48
+ const formData = await c.req.formData();
49
+ const email = formData.get("email")?.toString()?.toLowerCase();
50
+ const action = formData.get("action")?.toString();
51
+ let provider = await ctx.get(c, "provider");
52
+ if (!provider) {
53
+ const state = { type: "start" };
54
+ await ctx.set(c, "provider", 60 * 60 * 24, state);
55
+ if (action === "register") {
56
+ provider = state;
57
+ } else {
58
+ return ctx.forward(c, await config.register(c.req.raw, state));
59
+ }
60
+ }
61
+ const transition = async (next, err) => {
62
+ await ctx.set(c, "provider", 60 * 60 * 24, next);
63
+ return ctx.forward(c, await config.register(c.req.raw, next, formData, err));
64
+ };
65
+ if (action === "register" && provider.type === "start") {
66
+ const password = formData.get("password")?.toString();
67
+ const repeat = formData.get("repeat")?.toString();
68
+ if (!email) {
69
+ return transition(provider, { type: "invalid_email" });
70
+ }
71
+ if (!password) {
72
+ return transition(provider, { type: "invalid_password" });
73
+ }
74
+ if (password !== repeat) {
75
+ return transition(provider, { type: "password_mismatch" });
76
+ }
77
+ if (config.validatePassword) {
78
+ let validationError;
79
+ try {
80
+ if (typeof config.validatePassword === "function") {
81
+ validationError = await config.validatePassword(password);
82
+ } else {
83
+ const result = await config.validatePassword["~standard"].validate(password);
84
+ if (result.issues?.length) {
85
+ throw new Error(result.issues.map((issue) => issue.message).join(", "));
86
+ }
87
+ }
88
+ } catch (error) {
89
+ validationError = error instanceof Error ? error.message : undefined;
90
+ }
91
+ if (validationError) {
92
+ return transition(provider, {
93
+ type: "validation_error",
94
+ message: validationError
95
+ });
96
+ }
97
+ }
98
+ const existingUser = await Storage.get(ctx.storage, ["email", email, "password"]);
99
+ if (existingUser) {
100
+ return transition(provider, { type: "email_taken" });
101
+ }
102
+ const code = generateCode();
103
+ await config.sendCode(email, code, "register");
104
+ return transition({
105
+ type: "code",
106
+ code,
107
+ password: await hasher.hash(password),
108
+ email
109
+ });
110
+ }
111
+ if (action === "register" && provider.type === "code") {
112
+ const code = generateCode();
113
+ await config.sendCode(provider.email, code, "register:resend");
114
+ return transition({
115
+ type: "code",
116
+ code,
117
+ password: provider.password,
118
+ email: provider.email
119
+ });
120
+ }
121
+ if (action === "verify" && provider.type === "code") {
122
+ const code = formData.get("code")?.toString();
123
+ if (!(code && timingSafeCompare(code, provider.code))) {
124
+ return transition(provider, { type: "invalid_code" });
125
+ }
126
+ const existingUser = await Storage.get(ctx.storage, [
127
+ "email",
128
+ provider.email,
129
+ "password"
130
+ ]);
131
+ if (existingUser) {
132
+ return transition({ type: "start" }, { type: "email_taken" });
133
+ }
134
+ await Storage.set(ctx.storage, ["email", provider.email, "password"], provider.password);
135
+ return ctx.success(c, { email: provider.email });
136
+ }
137
+ return transition({ type: "start" });
138
+ });
139
+ routes.get("/change", async (c) => {
140
+ const redirect = c.req.query("redirect_uri") || getRelativeUrl(c, "./authorize");
141
+ const state = {
142
+ type: "start",
143
+ redirect
144
+ };
145
+ await ctx.set(c, "provider", 60 * 60 * 24, state);
146
+ return ctx.forward(c, await config.change(c.req.raw, state));
147
+ });
148
+ routes.post("/change", async (c) => {
149
+ const formData = await c.req.formData();
150
+ const action = formData.get("action")?.toString();
151
+ const provider = await ctx.get(c, "provider");
152
+ if (!provider) {
153
+ throw new UnknownStateError;
154
+ }
155
+ const transition = async (next, err) => {
156
+ await ctx.set(c, "provider", 60 * 60 * 24, next);
157
+ return ctx.forward(c, await config.change(c.req.raw, next, formData, err));
158
+ };
159
+ if (action === "code") {
160
+ const email = formData.get("email")?.toString()?.toLowerCase();
161
+ if (!email) {
162
+ return transition({ type: "start", redirect: provider.redirect }, { type: "invalid_email" });
163
+ }
164
+ const existingPassword = await Storage.get(ctx.storage, ["email", email, "password"]);
165
+ if (!existingPassword) {
166
+ return transition({ type: "start", redirect: provider.redirect }, { type: "invalid_email" });
167
+ }
168
+ const code = generateCode();
169
+ const context = provider.type === "code" && provider.email === email ? "reset:resend" : "reset";
170
+ await config.sendCode(email, code, context);
171
+ return transition({
172
+ type: "code",
173
+ code,
174
+ email,
175
+ redirect: provider.redirect
176
+ });
177
+ }
178
+ if (action === "verify" && provider.type === "code") {
179
+ const code = formData.get("code")?.toString();
180
+ if (!(code && timingSafeCompare(code, provider.code))) {
181
+ return transition(provider, { type: "invalid_code" });
182
+ }
183
+ return transition({
184
+ type: "update",
185
+ email: provider.email,
186
+ redirect: provider.redirect
187
+ });
188
+ }
189
+ if (action === "update" && provider.type === "update") {
190
+ const existingPassword = await Storage.get(ctx.storage, [
191
+ "email",
192
+ provider.email,
193
+ "password"
194
+ ]);
195
+ if (!existingPassword) {
196
+ return c.redirect(provider.redirect, 302);
197
+ }
198
+ const password = formData.get("password")?.toString();
199
+ const repeat = formData.get("repeat")?.toString();
200
+ if (!password) {
201
+ return transition(provider, { type: "invalid_password" });
202
+ }
203
+ if (!repeat) {
204
+ return transition(provider, { type: "invalid_password" });
205
+ }
206
+ if (password !== repeat) {
207
+ return transition(provider, { type: "password_mismatch" });
208
+ }
209
+ if (config.validatePassword) {
210
+ let validationError;
211
+ try {
212
+ if (typeof config.validatePassword === "function") {
213
+ validationError = await config.validatePassword(password);
214
+ } else {
215
+ const result = await config.validatePassword["~standard"].validate(password);
216
+ if (result.issues?.length) {
217
+ throw new Error(result.issues.map((issue) => issue.message).join(", "));
218
+ }
219
+ }
220
+ } catch (error) {
221
+ validationError = error instanceof Error ? error.message : undefined;
222
+ }
223
+ if (validationError) {
224
+ return transition(provider, {
225
+ type: "validation_error",
226
+ message: validationError
227
+ });
228
+ }
229
+ }
230
+ await Storage.set(ctx.storage, ["email", provider.email, "password"], await hasher.hash(password));
231
+ const subject = await Storage.get(ctx.storage, [
232
+ "email",
233
+ provider.email,
234
+ "subject"
235
+ ]);
236
+ if (subject) {
237
+ await ctx.invalidate(subject);
238
+ }
239
+ return c.redirect(provider.redirect, 302);
240
+ }
241
+ return transition({ type: "start", redirect: provider.redirect });
242
+ });
243
+ }
244
+ };
245
+ };
246
+ var PBKDF2Hasher = (opts) => {
247
+ const iterations = opts?.iterations ?? 600000;
248
+ return {
249
+ async hash(password) {
250
+ const encoder = new TextEncoder;
251
+ const passwordBytes = encoder.encode(password);
252
+ const salt = crypto.getRandomValues(new Uint8Array(16));
253
+ const keyMaterial = await crypto.subtle.importKey("raw", passwordBytes, "PBKDF2", false, ["deriveBits"]);
254
+ const hashBuffer = await crypto.subtle.deriveBits({
255
+ name: "PBKDF2",
256
+ hash: "SHA-256",
257
+ salt,
258
+ iterations
259
+ }, keyMaterial, 256);
260
+ const hashBase64 = jose.base64url.encode(new Uint8Array(hashBuffer));
261
+ const saltBase64 = jose.base64url.encode(salt);
262
+ return {
263
+ hash: hashBase64,
264
+ salt: saltBase64,
265
+ iterations
266
+ };
267
+ },
268
+ async verify(password, compare) {
269
+ const encoder = new TextEncoder;
270
+ const passwordBytes = encoder.encode(password);
271
+ const salt = jose.base64url.decode(compare.salt);
272
+ const keyMaterial = await crypto.subtle.importKey("raw", passwordBytes, "PBKDF2", false, ["deriveBits"]);
273
+ const hashBuffer = await crypto.subtle.deriveBits({
274
+ name: "PBKDF2",
275
+ hash: "SHA-256",
276
+ salt,
277
+ iterations: compare.iterations
278
+ }, keyMaterial, 256);
279
+ const hashBase64 = jose.base64url.encode(new Uint8Array(hashBuffer));
280
+ return timingSafeCompare(hashBase64, compare.hash);
281
+ }
282
+ };
283
+ };
284
+ var ScryptHasher = (opts) => {
285
+ const N = opts?.N ?? 16384;
286
+ const r = opts?.r ?? 8;
287
+ const p = opts?.p ?? 1;
288
+ return {
289
+ async hash(password) {
290
+ const salt = randomBytes(16);
291
+ const keyLength = 32;
292
+ const derivedKey = await new Promise((resolve, reject) => {
293
+ scrypt(password, salt, keyLength, { N, r, p }, (err, derivedKey2) => {
294
+ if (err) {
295
+ reject(err);
296
+ } else {
297
+ resolve(derivedKey2);
298
+ }
299
+ });
300
+ });
301
+ const hashBase64 = derivedKey.toString("base64");
302
+ const saltBase64 = salt.toString("base64");
303
+ return {
304
+ hash: hashBase64,
305
+ salt: saltBase64,
306
+ N,
307
+ r,
308
+ p
309
+ };
310
+ },
311
+ async verify(password, compare) {
312
+ const salt = Buffer.from(compare.salt, "base64");
313
+ const keyLength = 32;
314
+ const derivedKey = await new Promise((resolve, reject) => {
315
+ scrypt(password, salt, keyLength, { N: compare.N, r: compare.r, p: compare.p }, (err, derivedKey2) => {
316
+ if (err) {
317
+ reject(err);
318
+ } else {
319
+ resolve(derivedKey2);
320
+ }
321
+ });
322
+ });
323
+ return timingSafeEqual(derivedKey, Buffer.from(compare.hash, "base64"));
324
+ }
325
+ };
326
+ };
327
+ export {
328
+ ScryptHasher,
329
+ PasswordProvider,
330
+ PBKDF2Hasher
331
+ };
@@ -0,0 +1,18 @@
1
+ // src/provider/provider.ts
2
+ class ProviderError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "ProviderError";
6
+ }
7
+ }
8
+
9
+ class ProviderUnknownError extends ProviderError {
10
+ constructor(message) {
11
+ super(message || "An unknown provider error occurred");
12
+ this.name = "ProviderUnknownError";
13
+ }
14
+ }
15
+ export {
16
+ ProviderUnknownError,
17
+ ProviderError
18
+ };
@@ -0,0 +1,15 @@
1
+ // src/provider/reddit.ts
2
+ import { Oauth2Provider } from "./oauth2.js";
3
+ var RedditProvider = (config) => {
4
+ return Oauth2Provider({
5
+ ...config,
6
+ type: "reddit",
7
+ endpoint: {
8
+ authorization: "https://www.reddit.com/api/v1/authorize",
9
+ token: "https://www.reddit.com/api/v1/access_token"
10
+ }
11
+ });
12
+ };
13
+ export {
14
+ RedditProvider
15
+ };
@@ -0,0 +1,15 @@
1
+ // src/provider/slack.ts
2
+ import { Oauth2Provider } from "./oauth2.js";
3
+ var SlackProvider = (config) => {
4
+ return Oauth2Provider({
5
+ ...config,
6
+ type: "slack",
7
+ endpoint: {
8
+ authorization: "https://slack.com/oauth/v2/authorize",
9
+ token: "https://slack.com/api/oauth.v2.access"
10
+ }
11
+ });
12
+ };
13
+ export {
14
+ SlackProvider
15
+ };
@@ -0,0 +1,15 @@
1
+ // src/provider/spotify.ts
2
+ import { Oauth2Provider } from "./oauth2.js";
3
+ var SpotifyProvider = (config) => {
4
+ return Oauth2Provider({
5
+ ...config,
6
+ type: "spotify",
7
+ endpoint: {
8
+ authorization: "https://accounts.spotify.com/authorize",
9
+ token: "https://accounts.spotify.com/api/token"
10
+ }
11
+ });
12
+ };
13
+ export {
14
+ SpotifyProvider
15
+ };
@@ -0,0 +1,15 @@
1
+ // src/provider/twitch.ts
2
+ import { Oauth2Provider } from "./oauth2.js";
3
+ var TwitchProvider = (config) => {
4
+ return Oauth2Provider({
5
+ ...config,
6
+ type: "twitch",
7
+ endpoint: {
8
+ authorization: "https://id.twitch.tv/oauth2/authorize",
9
+ token: "https://id.twitch.tv/oauth2/token"
10
+ }
11
+ });
12
+ };
13
+ export {
14
+ TwitchProvider
15
+ };
@@ -0,0 +1,17 @@
1
+ // src/provider/vercel.ts
2
+ import { Oauth2Provider } from "./oauth2.js";
3
+ var VercelProvider = (config) => {
4
+ return Oauth2Provider({
5
+ ...config,
6
+ type: "vercel",
7
+ endpoint: {
8
+ authorization: "https://vercel.com/oauth/authorize",
9
+ token: "https://api.vercel.com/login/oauth/token",
10
+ jwks: "https://vercel.com/.well-known/jwks.json"
11
+ },
12
+ pkce: true
13
+ });
14
+ };
15
+ export {
16
+ VercelProvider
17
+ };
@@ -0,0 +1,40 @@
1
+ // src/random.ts
2
+ import { timingSafeEqual } from "node:crypto";
3
+ var generateSecureToken = (length = 32) => {
4
+ if (length <= 0 || !Number.isInteger(length)) {
5
+ throw new RangeError("Token length must be a positive integer");
6
+ }
7
+ const randomBytes = new Uint8Array(length);
8
+ crypto.getRandomValues(randomBytes);
9
+ const base64 = btoa(String.fromCharCode.apply(null, Array.from(randomBytes)));
10
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
11
+ };
12
+ var generateUnbiasedDigits = (length) => {
13
+ if (length <= 0 || !Number.isInteger(length)) {
14
+ throw new RangeError("Length must be a positive integer");
15
+ }
16
+ const result = [];
17
+ while (result.length < length) {
18
+ const buffer = crypto.getRandomValues(new Uint8Array(length * 2));
19
+ for (const byte of buffer) {
20
+ if (byte < 250 && result.length < length) {
21
+ result.push(byte % 10);
22
+ }
23
+ }
24
+ }
25
+ return result.join("");
26
+ };
27
+ var timingSafeCompare = (a, b) => {
28
+ if (typeof a !== "string" || typeof b !== "string") {
29
+ return false;
30
+ }
31
+ if (a.length !== b.length) {
32
+ return false;
33
+ }
34
+ return timingSafeEqual(Buffer.from(a), Buffer.from(b));
35
+ };
36
+ export {
37
+ timingSafeCompare,
38
+ generateUnbiasedDigits,
39
+ generateSecureToken
40
+ };
@@ -0,0 +1,27 @@
1
+ // src/revocation.ts
2
+ import { createHash } from "node:crypto";
3
+ import { Storage } from "./storage/storage.js";
4
+ var hashToken = (token) => {
5
+ return createHash("sha256").update(token).digest("hex");
6
+ };
7
+ var Revocation = {
8
+ revoke: async (storage, token, expiresAt) => {
9
+ const hash = hashToken(token);
10
+ const key = ["revocation:token", hash];
11
+ const record = {
12
+ revokedAt: Date.now(),
13
+ expiresAt
14
+ };
15
+ const ttlSeconds = Math.ceil((expiresAt - Date.now()) / 1000);
16
+ await Storage.set(storage, key, record, Math.max(1, ttlSeconds));
17
+ },
18
+ isRevoked: async (storage, token) => {
19
+ const hash = hashToken(token);
20
+ const key = ["revocation:token", hash];
21
+ const record = await Storage.get(storage, key);
22
+ return !!record;
23
+ }
24
+ };
25
+ export {
26
+ Revocation
27
+ };
@@ -0,0 +1,110 @@
1
+ // src/storage/memory.ts
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { writeFile } from "node:fs/promises";
4
+ import { joinKey, splitKey } from "./storage.js";
5
+ var MemoryStorage = (options) => {
6
+ const store = [];
7
+ const isValidStoreData = (data) => {
8
+ return Array.isArray(data) && data.every((item) => Array.isArray(item) && item.length === 2 && typeof item[0] === "string" && typeof item[1] === "object" && item[1] !== null && ("value" in item[1]));
9
+ };
10
+ if (options?.persist && existsSync(options.persist)) {
11
+ try {
12
+ const fileContent = readFileSync(options.persist, "utf8");
13
+ const parsed = JSON.parse(fileContent);
14
+ if (isValidStoreData(parsed)) {
15
+ store.push(...parsed);
16
+ }
17
+ } catch {}
18
+ }
19
+ const save = async () => {
20
+ if (!options?.persist) {
21
+ return;
22
+ }
23
+ try {
24
+ const serialized = JSON.stringify(store, null, 2);
25
+ await writeFile(options.persist, serialized, "utf8");
26
+ } catch {}
27
+ };
28
+ const search = (key) => {
29
+ let left = 0;
30
+ let right = store.length - 1;
31
+ while (left <= right) {
32
+ const mid = Math.floor((left + right) / 2);
33
+ const midEntry = store[mid];
34
+ if (!midEntry) {
35
+ return { found: false, index: left };
36
+ }
37
+ const comparison = key.localeCompare(midEntry[0]);
38
+ if (comparison === 0) {
39
+ return { found: true, index: mid };
40
+ }
41
+ if (comparison < 0) {
42
+ right = mid - 1;
43
+ } else {
44
+ left = mid + 1;
45
+ }
46
+ }
47
+ return { found: false, index: left };
48
+ };
49
+ return {
50
+ async get(key) {
51
+ const searchKey = joinKey(key);
52
+ const match = search(searchKey);
53
+ if (!match.found) {
54
+ return;
55
+ }
56
+ const storeEntry = store[match.index];
57
+ if (!storeEntry) {
58
+ return;
59
+ }
60
+ const entry = storeEntry[1];
61
+ if (entry.expiry && Date.now() >= entry.expiry) {
62
+ store.splice(match.index, 1);
63
+ await save();
64
+ return;
65
+ }
66
+ return entry.value;
67
+ },
68
+ async set(key, value, expiry) {
69
+ const searchKey = joinKey(key);
70
+ const match = search(searchKey);
71
+ const entry = [
72
+ searchKey,
73
+ {
74
+ value,
75
+ expiry: expiry?.getTime()
76
+ }
77
+ ];
78
+ if (match.found) {
79
+ store[match.index] = entry;
80
+ } else {
81
+ store.splice(match.index, 0, entry);
82
+ }
83
+ await save();
84
+ },
85
+ async remove(key) {
86
+ const searchKey = joinKey(key);
87
+ const match = search(searchKey);
88
+ if (match.found) {
89
+ store.splice(match.index, 1);
90
+ await save();
91
+ }
92
+ },
93
+ async* scan(prefix) {
94
+ const now = Date.now();
95
+ const prefixStr = joinKey(prefix);
96
+ for (const [key, entry] of store) {
97
+ if (!key.startsWith(prefixStr)) {
98
+ continue;
99
+ }
100
+ if (entry.expiry && now >= entry.expiry) {
101
+ continue;
102
+ }
103
+ yield [splitKey(key), entry.value];
104
+ }
105
+ }
106
+ };
107
+ };
108
+ export {
109
+ MemoryStorage
110
+ };
@@ -0,0 +1,56 @@
1
+ // src/storage/storage.ts
2
+ var SEPARATOR = String.fromCharCode(31);
3
+ var ESCAPE = "\\";
4
+ var joinKey = (key) => {
5
+ return key.join(SEPARATOR);
6
+ };
7
+ var splitKey = (key) => {
8
+ return key.split(SEPARATOR);
9
+ };
10
+ var encodeSegment = (segment) => {
11
+ if (!segment || !segment.trim()) {
12
+ throw new Error(`Storage key segment cannot be empty or whitespace-only: "${segment}"`);
13
+ }
14
+ return segment.replaceAll(ESCAPE, ESCAPE + ESCAPE).replaceAll(SEPARATOR, ESCAPE + SEPARATOR);
15
+ };
16
+ var decodeSegment = (segment) => {
17
+ return segment.replaceAll(ESCAPE + SEPARATOR, SEPARATOR).replaceAll(ESCAPE + ESCAPE, ESCAPE);
18
+ };
19
+ var Storage = {
20
+ encode: (key) => {
21
+ return key.map(encodeSegment);
22
+ },
23
+ decode: (key) => {
24
+ return key.map(decodeSegment);
25
+ },
26
+ get: (adapter, key) => {
27
+ return adapter.get(Storage.encode(key));
28
+ },
29
+ set: (adapter, key, value, ttlSeconds) => {
30
+ if (ttlSeconds !== undefined && ttlSeconds !== null) {
31
+ if (!Number.isInteger(ttlSeconds)) {
32
+ throw new RangeError(`Storage TTL must be an integer in seconds, received ${typeof ttlSeconds}`);
33
+ }
34
+ if (ttlSeconds <= 0) {
35
+ throw new RangeError(`Storage TTL must be positive, received ${ttlSeconds}`);
36
+ }
37
+ const maxTtlSeconds = 60 * 60 * 24 * 365 * 10;
38
+ if (ttlSeconds > maxTtlSeconds) {
39
+ throw new RangeError(`Storage TTL exceeds maximum (${maxTtlSeconds}s = 10 years), received ${ttlSeconds}s`);
40
+ }
41
+ }
42
+ const expiry = ttlSeconds ? new Date(Date.now() + ttlSeconds * 1000) : undefined;
43
+ return adapter.set(Storage.encode(key), value, expiry);
44
+ },
45
+ remove: (adapter, key) => {
46
+ return adapter.remove(Storage.encode(key));
47
+ },
48
+ scan: (adapter, prefix) => {
49
+ return adapter.scan(Storage.encode(prefix));
50
+ }
51
+ };
52
+ export {
53
+ splitKey,
54
+ joinKey,
55
+ Storage
56
+ };