@shware/security 2.5.3 → 2.6.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/dist/auth/index.cjs +520 -561
- package/dist/auth/index.cjs.map +1 -1
- package/dist/auth/index.d.cts +104 -106
- package/dist/auth/index.d.cts.map +1 -0
- package/dist/auth/index.d.mts +109 -0
- package/dist/auth/index.d.mts.map +1 -0
- package/dist/auth/index.mjs +513 -536
- package/dist/auth/index.mjs.map +1 -1
- package/dist/auth/types.cjs +0 -19
- package/dist/auth/types.d.cts +44 -44
- package/dist/auth/types.d.cts.map +1 -0
- package/dist/auth/types.d.mts +50 -0
- package/dist/auth/types.d.mts.map +1 -0
- package/dist/auth/types.mjs +1 -1
- package/dist/core/index.cjs +34 -59
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +23 -21
- package/dist/core/index.d.cts.map +1 -0
- package/dist/core/index.d.mts +34 -0
- package/dist/core/index.d.mts.map +1 -0
- package/dist/core/index.mjs +32 -33
- package/dist/core/index.mjs.map +1 -1
- package/dist/email/schema.cjs +13 -38
- package/dist/email/schema.cjs.map +1 -1
- package/dist/email/schema.d.cts +11 -12
- package/dist/email/schema.d.cts.map +1 -0
- package/dist/email/schema.d.mts +12 -0
- package/dist/email/schema.d.mts.map +1 -0
- package/dist/email/schema.mjs +10 -11
- package/dist/email/schema.mjs.map +1 -1
- package/dist/index.cjs +17 -54
- package/dist/index.d.cts +10 -15
- package/dist/index.d.mts +10 -0
- package/dist/index.mjs +4 -17
- package/dist/oauth2/client.cjs +109 -145
- package/dist/oauth2/client.cjs.map +1 -1
- package/dist/oauth2/client.d.cts +55 -56
- package/dist/oauth2/client.d.cts.map +1 -0
- package/dist/oauth2/client.d.mts +58 -0
- package/dist/oauth2/client.d.mts.map +1 -0
- package/dist/oauth2/client.mjs +104 -116
- package/dist/oauth2/client.mjs.map +1 -1
- package/dist/oauth2/error.cjs +19 -44
- package/dist/oauth2/error.cjs.map +1 -1
- package/dist/oauth2/error.d.cts +8 -6
- package/dist/oauth2/error.d.cts.map +1 -0
- package/dist/oauth2/{error.d.ts → error.d.mts} +8 -6
- package/dist/oauth2/error.d.mts.map +1 -0
- package/dist/oauth2/error.mjs +18 -19
- package/dist/oauth2/error.mjs.map +1 -1
- package/dist/oauth2/provider/apple.cjs +75 -88
- package/dist/oauth2/provider/apple.cjs.map +1 -1
- package/dist/oauth2/provider/apple.d.cts +31 -30
- package/dist/oauth2/provider/apple.d.cts.map +1 -0
- package/dist/oauth2/provider/apple.d.mts +42 -0
- package/dist/oauth2/provider/apple.d.mts.map +1 -0
- package/dist/oauth2/provider/apple.mjs +71 -60
- package/dist/oauth2/provider/apple.mjs.map +1 -1
- package/dist/oauth2/provider/common.cjs +86 -109
- package/dist/oauth2/provider/common.cjs.map +1 -1
- package/dist/oauth2/provider/common.d.cts +19 -18
- package/dist/oauth2/provider/common.d.cts.map +1 -0
- package/dist/oauth2/provider/common.d.mts +30 -0
- package/dist/oauth2/provider/common.d.mts.map +1 -0
- package/dist/oauth2/provider/common.mjs +80 -79
- package/dist/oauth2/provider/common.mjs.map +1 -1
- package/dist/oauth2/provider/facebook.cjs +93 -109
- package/dist/oauth2/provider/facebook.cjs.map +1 -1
- package/dist/oauth2/provider/facebook.d.cts +29 -28
- package/dist/oauth2/provider/facebook.d.cts.map +1 -0
- package/dist/oauth2/provider/facebook.d.mts +45 -0
- package/dist/oauth2/provider/facebook.d.mts.map +1 -0
- package/dist/oauth2/provider/facebook.mjs +89 -81
- package/dist/oauth2/provider/facebook.mjs.map +1 -1
- package/dist/oauth2/provider/github.cjs +64 -91
- package/dist/oauth2/provider/github.cjs.map +1 -1
- package/dist/oauth2/provider/github.d.cts +52 -51
- package/dist/oauth2/provider/github.d.cts.map +1 -0
- package/dist/oauth2/provider/github.d.mts +60 -0
- package/dist/oauth2/provider/github.d.mts.map +1 -0
- package/dist/oauth2/provider/github.mjs +60 -63
- package/dist/oauth2/provider/github.mjs.map +1 -1
- package/dist/oauth2/provider/google.cjs +105 -122
- package/dist/oauth2/provider/google.cjs.map +1 -1
- package/dist/oauth2/provider/google.d.cts +34 -33
- package/dist/oauth2/provider/google.d.cts.map +1 -0
- package/dist/oauth2/provider/google.d.mts +44 -0
- package/dist/oauth2/provider/google.d.mts.map +1 -0
- package/dist/oauth2/provider/google.mjs +101 -94
- package/dist/oauth2/provider/google.mjs.map +1 -1
- package/dist/oauth2/provider/index.cjs +13 -46
- package/dist/oauth2/provider/index.d.cts +7 -7
- package/dist/oauth2/provider/index.d.mts +7 -0
- package/dist/oauth2/provider/index.mjs +1 -10
- package/dist/oauth2/provider/wechat.cjs +77 -99
- package/dist/oauth2/provider/wechat.cjs.map +1 -1
- package/dist/oauth2/provider/wechat.d.cts +28 -27
- package/dist/oauth2/provider/wechat.d.cts.map +1 -0
- package/dist/oauth2/provider/wechat.d.mts +39 -0
- package/dist/oauth2/provider/wechat.d.mts.map +1 -0
- package/dist/oauth2/provider/wechat.mjs +74 -72
- package/dist/oauth2/provider/wechat.mjs.map +1 -1
- package/dist/oauth2/provider/x.cjs +102 -125
- package/dist/oauth2/provider/x.cjs.map +1 -1
- package/dist/oauth2/provider/x.d.cts +51 -50
- package/dist/oauth2/provider/x.d.cts.map +1 -0
- package/dist/oauth2/provider/x.d.mts +69 -0
- package/dist/oauth2/provider/x.d.mts.map +1 -0
- package/dist/oauth2/provider/x.mjs +99 -103
- package/dist/oauth2/provider/x.mjs.map +1 -1
- package/dist/oauth2/types.cjs +13 -37
- package/dist/oauth2/types.cjs.map +1 -1
- package/dist/oauth2/types.d.cts +180 -178
- package/dist/oauth2/types.d.cts.map +1 -0
- package/dist/oauth2/types.d.mts +213 -0
- package/dist/oauth2/types.d.mts.map +1 -0
- package/dist/oauth2/types.mjs +13 -13
- package/dist/oauth2/types.mjs.map +1 -1
- package/dist/session/common.cjs +17 -43
- package/dist/session/common.cjs.map +1 -1
- package/dist/session/common.d.cts +4 -4
- package/dist/session/common.d.cts.map +1 -0
- package/dist/session/{common.d.ts → common.d.mts} +4 -4
- package/dist/session/common.d.mts.map +1 -0
- package/dist/session/common.mjs +14 -16
- package/dist/session/common.mjs.map +1 -1
- package/dist/session/db.cjs +88 -123
- package/dist/session/db.cjs.map +1 -1
- package/dist/session/db.d.cts +36 -36
- package/dist/session/db.d.cts.map +1 -0
- package/dist/session/db.d.mts +44 -0
- package/dist/session/db.d.mts.map +1 -0
- package/dist/session/db.mjs +84 -95
- package/dist/session/db.mjs.map +1 -1
- package/dist/session/map-session.cjs +71 -103
- package/dist/session/map-session.cjs.map +1 -1
- package/dist/session/map-session.d.cts +24 -24
- package/dist/session/map-session.d.cts.map +1 -0
- package/dist/session/map-session.d.mts +27 -0
- package/dist/session/map-session.d.mts.map +1 -0
- package/dist/session/map-session.mjs +70 -78
- package/dist/session/map-session.mjs.map +1 -1
- package/dist/session/redis.cjs +270 -319
- package/dist/session/redis.cjs.map +1 -1
- package/dist/session/redis.d.cts +51 -51
- package/dist/session/redis.d.cts.map +1 -0
- package/dist/session/redis.d.mts +58 -0
- package/dist/session/redis.d.mts.map +1 -0
- package/dist/session/redis.mjs +266 -291
- package/dist/session/redis.mjs.map +1 -1
- package/dist/session/types.cjs +0 -19
- package/dist/session/types.d.cts +34 -33
- package/dist/session/types.d.cts.map +1 -0
- package/dist/session/types.d.mts +45 -0
- package/dist/session/types.d.mts.map +1 -0
- package/dist/session/types.mjs +1 -1
- package/dist/utils/http.cjs +31 -55
- package/dist/utils/http.cjs.map +1 -1
- package/dist/utils/http.d.cts +7 -10
- package/dist/utils/http.d.cts.map +1 -0
- package/dist/utils/{http.d.ts → http.d.mts} +7 -10
- package/dist/utils/http.d.mts.map +1 -0
- package/dist/utils/http.mjs +24 -24
- package/dist/utils/http.mjs.map +1 -1
- package/dist/utils/oauth2.cjs +6 -30
- package/dist/utils/oauth2.cjs.map +1 -1
- package/dist/utils/oauth2.d.cts +3 -1
- package/dist/utils/oauth2.d.cts.map +1 -0
- package/dist/utils/oauth2.d.mts +5 -0
- package/dist/utils/oauth2.d.mts.map +1 -0
- package/dist/utils/oauth2.mjs +5 -5
- package/dist/utils/oauth2.mjs.map +1 -1
- package/dist/utils/timing.cjs +49 -70
- package/dist/utils/timing.cjs.map +1 -1
- package/dist/utils/timing.d.cts +10 -8
- package/dist/utils/timing.d.cts.map +1 -0
- package/dist/utils/timing.d.mts +15 -0
- package/dist/utils/timing.d.mts.map +1 -0
- package/dist/utils/timing.mjs +48 -45
- package/dist/utils/timing.mjs.map +1 -1
- package/dist/utils/valid.cjs +50 -71
- package/dist/utils/valid.cjs.map +1 -1
- package/dist/utils/valid.d.cts +17 -16
- package/dist/utils/valid.d.cts.map +1 -0
- package/dist/utils/valid.d.mts +28 -0
- package/dist/utils/valid.d.mts.map +1 -0
- package/dist/utils/valid.mjs +47 -44
- package/dist/utils/valid.mjs.map +1 -1
- package/package.json +22 -12
- package/dist/auth/index.d.ts +0 -111
- package/dist/auth/types.cjs.map +0 -1
- package/dist/auth/types.d.ts +0 -50
- package/dist/auth/types.mjs.map +0 -1
- package/dist/core/index.d.ts +0 -32
- package/dist/email/schema.d.ts +0 -13
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.ts +0 -15
- package/dist/index.mjs.map +0 -1
- package/dist/oauth2/client.d.ts +0 -59
- package/dist/oauth2/provider/apple.d.ts +0 -41
- package/dist/oauth2/provider/common.d.ts +0 -29
- package/dist/oauth2/provider/facebook.d.ts +0 -44
- package/dist/oauth2/provider/github.d.ts +0 -59
- package/dist/oauth2/provider/google.d.ts +0 -43
- package/dist/oauth2/provider/index.cjs.map +0 -1
- package/dist/oauth2/provider/index.d.ts +0 -7
- package/dist/oauth2/provider/index.mjs.map +0 -1
- package/dist/oauth2/provider/wechat.d.ts +0 -38
- package/dist/oauth2/provider/x.d.ts +0 -68
- package/dist/oauth2/types.d.ts +0 -211
- package/dist/session/__tests__/redis.test.cjs +0 -247
- package/dist/session/__tests__/redis.test.cjs.map +0 -1
- package/dist/session/__tests__/redis.test.d.cts +0 -2
- package/dist/session/__tests__/redis.test.d.ts +0 -2
- package/dist/session/__tests__/redis.test.mjs +0 -252
- package/dist/session/__tests__/redis.test.mjs.map +0 -1
- package/dist/session/db.d.ts +0 -44
- package/dist/session/map-session.d.ts +0 -27
- package/dist/session/redis.d.ts +0 -58
- package/dist/session/types.cjs.map +0 -1
- package/dist/session/types.d.ts +0 -44
- package/dist/session/types.mjs.map +0 -1
- package/dist/utils/oauth2.d.ts +0 -3
- package/dist/utils/timing.d.ts +0 -13
- package/dist/utils/valid.d.ts +0 -27
package/dist/auth/index.mjs
CHANGED
|
@@ -1,545 +1,522 @@
|
|
|
1
|
-
|
|
2
|
-
import { createHash, randomInt, randomUUID, timingSafeEqual } from "crypto";
|
|
3
|
-
import { invariant } from "@shware/utils";
|
|
4
|
-
import { Provider } from "../core/index.mjs";
|
|
1
|
+
import "../core/index.mjs";
|
|
5
2
|
import { loginEmailSchema, sendEmailVerificationCodeSchema } from "../email/schema.mjs";
|
|
6
3
|
import { OAuth2Client, googleOneTapSchema, oauth2RedirectQuerySchema } from "../oauth2/client.mjs";
|
|
7
|
-
import { google } from "../oauth2/provider/
|
|
4
|
+
import { google } from "../oauth2/provider/google.mjs";
|
|
5
|
+
import "../oauth2/provider/index.mjs";
|
|
8
6
|
import { PRINCIPAL_NAME_INDEX_NAME } from "../session/common.mjs";
|
|
9
|
-
import {
|
|
10
|
-
deleteCookie,
|
|
11
|
-
getCookie,
|
|
12
|
-
param,
|
|
13
|
-
query,
|
|
14
|
-
setCookie
|
|
15
|
-
} from "../utils/http.mjs";
|
|
7
|
+
import { deleteCookie, getCookie, param, query, setCookie } from "../utils/http.mjs";
|
|
16
8
|
import { timing } from "../utils/timing.mjs";
|
|
17
9
|
import { invalidArgument, valid, verifyTurnstileToken } from "../utils/valid.mjs";
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
// 5-minute single-use code transits via the loopback.
|
|
40
|
-
DESKTOP_AUTHORIZE: "/auth/desktop/authorize",
|
|
41
|
-
DESKTOP_EXCHANGE: "/auth/desktop/exchange",
|
|
42
|
-
CLEANUP_EXPIRED_SESSIONS: "/sessions/expired/cleanup"
|
|
10
|
+
import { createHash, randomInt, randomUUID, timingSafeEqual } from "node:crypto";
|
|
11
|
+
import { invariant } from "@shware/utils";
|
|
12
|
+
//#region src/auth/index.ts
|
|
13
|
+
const PKCE_FORMAT = /^[A-Za-z0-9_-]{43,128}$/;
|
|
14
|
+
const PATH = {
|
|
15
|
+
CSRF: "/csrf",
|
|
16
|
+
LOGOUT: "/logout",
|
|
17
|
+
LOGGED: "/logged",
|
|
18
|
+
OAUTH2_STATE: "/oauth2/state/:registrationId",
|
|
19
|
+
OAUTH2_NONCE: "/oauth2/nonce/:registrationId",
|
|
20
|
+
OAUTH2_AUTHORIZATION: "/oauth2/authorization/:registrationId",
|
|
21
|
+
LOGIN_OAUTH2_CODE: "/login/oauth2/code/:registrationId",
|
|
22
|
+
LOGIN_OAUTH2_NATIVE: "/login/oauth2/native/:registrationId",
|
|
23
|
+
LOGIN_OAUTH2_ONETAP: "/login/oauth2/onetap/google",
|
|
24
|
+
LOGIN_EMAIL: "/login/email",
|
|
25
|
+
LOGIN_PHONE: "/login/phone",
|
|
26
|
+
SEND_EMAIL_VERIFICATION_CODE: "/auth/sendEmailVerificationCode",
|
|
27
|
+
SEND_PHONE_VERIFICATION_CODE: "/auth/sendPhoneVerificationCode",
|
|
28
|
+
DESKTOP_AUTHORIZE: "/auth/desktop/authorize",
|
|
29
|
+
DESKTOP_EXCHANGE: "/auth/desktop/exchange",
|
|
30
|
+
CLEANUP_EXPIRED_SESSIONS: "/sessions/expired/cleanup"
|
|
43
31
|
};
|
|
44
32
|
var Auth = class {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
return { name: String(name) };
|
|
531
|
-
};
|
|
532
|
-
listSessions = async (principal) => {
|
|
533
|
-
const sessions = await this.repository.findByPrincipalName(principal.name);
|
|
534
|
-
return Array.from(sessions.values());
|
|
535
|
-
};
|
|
536
|
-
cleanupExpiredSessions = async (cleanupCount) => {
|
|
537
|
-
await this.repository.cleanupExpiredSessions(cleanupCount);
|
|
538
|
-
return Response.json({});
|
|
539
|
-
};
|
|
540
|
-
};
|
|
541
|
-
export {
|
|
542
|
-
Auth,
|
|
543
|
-
PATH
|
|
33
|
+
timing;
|
|
34
|
+
cookieName;
|
|
35
|
+
cookieOptions;
|
|
36
|
+
kv;
|
|
37
|
+
repository;
|
|
38
|
+
turnstileSecretKey;
|
|
39
|
+
oauth2Client;
|
|
40
|
+
ATTR_OAUTH2_AUTHORIZATION_REQUEST = "oauth2AuthorizationRequest";
|
|
41
|
+
constructor({ sessionRepository, kvRepository, turnstileSecretKey, oauth2, cookie, timing }) {
|
|
42
|
+
this.timing = timing ?? false;
|
|
43
|
+
this.kv = kvRepository;
|
|
44
|
+
this.repository = sessionRepository;
|
|
45
|
+
this.turnstileSecretKey = turnstileSecretKey;
|
|
46
|
+
const { name, ...cookieOptions } = cookie ?? {};
|
|
47
|
+
this.cookieName = name ?? "SESSION";
|
|
48
|
+
this.cookieOptions = {
|
|
49
|
+
path: "/",
|
|
50
|
+
sameSite: "none",
|
|
51
|
+
secure: true,
|
|
52
|
+
httpOnly: true,
|
|
53
|
+
...cookieOptions
|
|
54
|
+
};
|
|
55
|
+
this.oauth2Client = oauth2?.client ? new OAuth2Client(oauth2.client) : null;
|
|
56
|
+
}
|
|
57
|
+
getSessionId(request) {
|
|
58
|
+
let sessionId;
|
|
59
|
+
const header = request.headers.get("Authorization");
|
|
60
|
+
if (header?.startsWith("Bearer ")) sessionId = header.replace("Bearer ", "");
|
|
61
|
+
if (!sessionId) sessionId = getCookie(request, this.cookieName);
|
|
62
|
+
return sessionId;
|
|
63
|
+
}
|
|
64
|
+
csrf = async (_request) => {
|
|
65
|
+
const token = randomUUID();
|
|
66
|
+
const response = Response.json({
|
|
67
|
+
token,
|
|
68
|
+
parameterName: "_csrf",
|
|
69
|
+
headerName: "X-XSRF-TOKEN"
|
|
70
|
+
});
|
|
71
|
+
setCookie(response, "XSRF-TOKEN", token, {
|
|
72
|
+
path: "/",
|
|
73
|
+
secure: true,
|
|
74
|
+
httpOnly: false,
|
|
75
|
+
sameSite: "none",
|
|
76
|
+
domain: this.cookieOptions.domain
|
|
77
|
+
});
|
|
78
|
+
return response;
|
|
79
|
+
};
|
|
80
|
+
logout = async (request) => {
|
|
81
|
+
const sessionId = this.getSessionId(request);
|
|
82
|
+
if (sessionId) await this.repository.deleteById(sessionId);
|
|
83
|
+
const response = Response.json({});
|
|
84
|
+
deleteCookie(response, this.cookieName);
|
|
85
|
+
return response;
|
|
86
|
+
};
|
|
87
|
+
logged = async (request, onLogged) => {
|
|
88
|
+
const sessionId = this.getSessionId(request);
|
|
89
|
+
if (!sessionId) return Response.json({ data: false });
|
|
90
|
+
const session = await this.repository.findById(sessionId);
|
|
91
|
+
if (!session) return Response.json({ data: false });
|
|
92
|
+
const name = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);
|
|
93
|
+
session.setLastAccessedTime(Date.now());
|
|
94
|
+
await this.repository.save(session);
|
|
95
|
+
if (name !== null) await onLogged?.({ name: String(name) });
|
|
96
|
+
const response = Response.json({ data: name !== null });
|
|
97
|
+
const maxAge = session.getMaxInactiveInterval();
|
|
98
|
+
setCookie(response, this.cookieName, session.getId(), {
|
|
99
|
+
...this.cookieOptions,
|
|
100
|
+
maxAge
|
|
101
|
+
});
|
|
102
|
+
return response;
|
|
103
|
+
};
|
|
104
|
+
createPkceParameters() {
|
|
105
|
+
const code_verifier = randomUUID();
|
|
106
|
+
return {
|
|
107
|
+
code_verifier,
|
|
108
|
+
code_challenge: createHash("sha256").update(code_verifier).digest().toString("base64url"),
|
|
109
|
+
code_challenge_method: "S256"
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
getOauth2StateKey(state) {
|
|
113
|
+
return `oauth2:state:${state}`;
|
|
114
|
+
}
|
|
115
|
+
getOauth2NonceKey(nonce) {
|
|
116
|
+
return `oauth2:nonce:${nonce}`;
|
|
117
|
+
}
|
|
118
|
+
getEmailVerificationCodeKey(email) {
|
|
119
|
+
return `email:verification:${email}`;
|
|
120
|
+
}
|
|
121
|
+
getPhoneVerificationCodeKey(phone) {
|
|
122
|
+
return `phone:verification:${phone}`;
|
|
123
|
+
}
|
|
124
|
+
getDesktopCodeKey(code) {
|
|
125
|
+
return `desktop:code:${code}`;
|
|
126
|
+
}
|
|
127
|
+
oauth2State = async (request) => {
|
|
128
|
+
const { registrationId } = param(request, PATH.OAUTH2_STATE);
|
|
129
|
+
const state = randomUUID();
|
|
130
|
+
await this.kv.setItem(this.getOauth2StateKey(state), registrationId, 600);
|
|
131
|
+
return Response.json({ state });
|
|
132
|
+
};
|
|
133
|
+
oauth2Nonce = async (request) => {
|
|
134
|
+
const { registrationId } = param(request, PATH.OAUTH2_NONCE);
|
|
135
|
+
const nonce = randomUUID();
|
|
136
|
+
await this.kv.setItem(this.getOauth2NonceKey(nonce), registrationId, 600);
|
|
137
|
+
return Response.json({ nonce });
|
|
138
|
+
};
|
|
139
|
+
oauth2Authorization = async (request) => {
|
|
140
|
+
invariant(this.oauth2Client, "oauth2Client is not initialized");
|
|
141
|
+
const { mark, setTiming } = timing({ enabled: this.timing });
|
|
142
|
+
const { registrationId } = param(request, PATH.OAUTH2_AUTHORIZATION);
|
|
143
|
+
const state = randomUUID();
|
|
144
|
+
const pkce = this.createPkceParameters();
|
|
145
|
+
const uri = await this.oauth2Client.createAuthorizationUri({
|
|
146
|
+
registrationId,
|
|
147
|
+
state,
|
|
148
|
+
pkce
|
|
149
|
+
});
|
|
150
|
+
const sessionId = this.getSessionId(request);
|
|
151
|
+
const session = sessionId ? await this.repository.findById(sessionId) ?? this.repository.createSession() : this.repository.createSession();
|
|
152
|
+
mark("fetch_session");
|
|
153
|
+
const authorizationRequest = {
|
|
154
|
+
state,
|
|
155
|
+
registrationId,
|
|
156
|
+
authorizationRequestUri: uri.href,
|
|
157
|
+
additionalParameters: pkce
|
|
158
|
+
};
|
|
159
|
+
const value = JSON.stringify(authorizationRequest);
|
|
160
|
+
session.setAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST, value);
|
|
161
|
+
await this.repository.save(session);
|
|
162
|
+
mark("save_session");
|
|
163
|
+
const response = new Response(null, {
|
|
164
|
+
status: 302,
|
|
165
|
+
headers: { location: uri.href }
|
|
166
|
+
});
|
|
167
|
+
const maxAge = session.getMaxInactiveInterval();
|
|
168
|
+
setCookie(response, this.cookieName, session.getId(), {
|
|
169
|
+
...this.cookieOptions,
|
|
170
|
+
maxAge
|
|
171
|
+
});
|
|
172
|
+
setTiming(response);
|
|
173
|
+
return response;
|
|
174
|
+
};
|
|
175
|
+
redirect = (error, description) => {
|
|
176
|
+
invariant(this.oauth2Client, "oauth2Client is not initialized");
|
|
177
|
+
const uri = new URL(this.oauth2Client.errorUri);
|
|
178
|
+
uri.searchParams.set("error", error);
|
|
179
|
+
if (description) uri.searchParams.set("error_description", description);
|
|
180
|
+
return Response.redirect(uri.href, 302);
|
|
181
|
+
};
|
|
182
|
+
loginOAuth2Code = async (request, handler) => {
|
|
183
|
+
invariant(this.oauth2Client, "oauth2Client is not initialized");
|
|
184
|
+
const { mark, setTiming } = timing({ enabled: this.timing });
|
|
185
|
+
const sessionId = this.getSessionId(request);
|
|
186
|
+
const session = sessionId ? await this.repository.findById(sessionId) : null;
|
|
187
|
+
if (!session) return this.redirect("invalid_request", "session not found");
|
|
188
|
+
mark("load_session");
|
|
189
|
+
const json = session.getAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST);
|
|
190
|
+
if (!json) return this.redirect("invalid_request", "authorization request not found");
|
|
191
|
+
const cached = JSON.parse(json);
|
|
192
|
+
const { registrationId } = param(request, PATH.LOGIN_OAUTH2_CODE);
|
|
193
|
+
let data;
|
|
194
|
+
if (request.method === "POST" && request.headers.get("content-type")?.toLowerCase() === "application/x-www-form-urlencoded") {
|
|
195
|
+
const params = new URLSearchParams(await request.text());
|
|
196
|
+
data = Object.fromEntries(params.entries());
|
|
197
|
+
} else data = query(request);
|
|
198
|
+
const parsed = oauth2RedirectQuerySchema.safeParse(data);
|
|
199
|
+
if (!parsed.success) return this.redirect("invalid_request", "invalid redirect query");
|
|
200
|
+
if (!parsed.data.code || !parsed.data.state) return this.redirect("invalid_request", "invalid redirect query");
|
|
201
|
+
if (parsed.data.state !== cached.state) return this.redirect("invalid_request", "redirect state mismatch");
|
|
202
|
+
if (registrationId !== cached.registrationId) return this.redirect("invalid_request", "redirect registration mismatch");
|
|
203
|
+
mark("validate_session");
|
|
204
|
+
const { code } = parsed.data;
|
|
205
|
+
const token = await this.oauth2Client.exchangeAuthorizationCode({
|
|
206
|
+
registrationId,
|
|
207
|
+
code,
|
|
208
|
+
pkce: cached.additionalParameters
|
|
209
|
+
});
|
|
210
|
+
const userInfo = await this.oauth2Client.getUserInfo({
|
|
211
|
+
registrationId,
|
|
212
|
+
token
|
|
213
|
+
});
|
|
214
|
+
mark("exchange_code");
|
|
215
|
+
const principal = await handler(request, registrationId, userInfo, token);
|
|
216
|
+
mark("save_principal");
|
|
217
|
+
session.setLastAccessedTime(Date.now());
|
|
218
|
+
session.removeAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST);
|
|
219
|
+
session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);
|
|
220
|
+
await this.repository.save(session);
|
|
221
|
+
mark("update_session");
|
|
222
|
+
const uri = new URL(this.oauth2Client.successUri);
|
|
223
|
+
uri.searchParams.set("status", "success");
|
|
224
|
+
uri.searchParams.set("registrationId", registrationId);
|
|
225
|
+
const response = new Response(null, {
|
|
226
|
+
status: 302,
|
|
227
|
+
headers: { location: uri.href }
|
|
228
|
+
});
|
|
229
|
+
const maxAge = session.getMaxInactiveInterval();
|
|
230
|
+
setCookie(response, this.cookieName, session.getId(), {
|
|
231
|
+
...this.cookieOptions,
|
|
232
|
+
maxAge
|
|
233
|
+
});
|
|
234
|
+
setTiming(response);
|
|
235
|
+
return response;
|
|
236
|
+
};
|
|
237
|
+
loginOAuth2Native = async (request, handler) => {
|
|
238
|
+
invariant(this.oauth2Client, "oauth2Client is not initialized");
|
|
239
|
+
const { mark, setTiming } = timing({ enabled: this.timing });
|
|
240
|
+
const { registrationId } = param(request, PATH.LOGIN_OAUTH2_NATIVE);
|
|
241
|
+
const credentials = await request.json();
|
|
242
|
+
const { userInfo, token } = await this.oauth2Client.loginOAuth2Native({
|
|
243
|
+
registrationId,
|
|
244
|
+
credentials
|
|
245
|
+
});
|
|
246
|
+
mark("login_oauth2_native");
|
|
247
|
+
const { nonce } = userInfo.data;
|
|
248
|
+
if (nonce) {
|
|
249
|
+
if (await this.kv.getItem(this.getOauth2NonceKey(nonce)) !== registrationId) return Response.json({
|
|
250
|
+
error: "invalid_request",
|
|
251
|
+
error_description: "nonce mismatch"
|
|
252
|
+
}, { status: 400 });
|
|
253
|
+
await this.kv.removeItem(this.getOauth2NonceKey(nonce));
|
|
254
|
+
mark("check_nonce");
|
|
255
|
+
}
|
|
256
|
+
const principal = await handler(request, registrationId, userInfo, token);
|
|
257
|
+
mark("save_principal");
|
|
258
|
+
const session = this.repository.createSession();
|
|
259
|
+
session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);
|
|
260
|
+
await this.repository.save(session);
|
|
261
|
+
const response = new Response(JSON.stringify(principal), {
|
|
262
|
+
status: 200,
|
|
263
|
+
headers: { "Content-Type": "application/json" }
|
|
264
|
+
});
|
|
265
|
+
const maxAge = session.getMaxInactiveInterval();
|
|
266
|
+
setCookie(response, this.cookieName, session.getId(), {
|
|
267
|
+
...this.cookieOptions,
|
|
268
|
+
maxAge
|
|
269
|
+
});
|
|
270
|
+
setTiming(response);
|
|
271
|
+
return response;
|
|
272
|
+
};
|
|
273
|
+
loginOAuth2Onetap = async (request, handler, registrationId = "google") => {
|
|
274
|
+
invariant(this.oauth2Client, "oauth2Client is not initialized");
|
|
275
|
+
const { mark, setTiming } = timing({ enabled: this.timing });
|
|
276
|
+
let body;
|
|
277
|
+
if (request.headers.get("Content-Type")?.includes("application/x-www-form-urlencoded")) {
|
|
278
|
+
const text = await request.text();
|
|
279
|
+
body = Object.fromEntries(new URLSearchParams(text));
|
|
280
|
+
} else if (request.headers.get("Content-Type")?.includes("application/json")) body = await request.json();
|
|
281
|
+
else return Response.json({
|
|
282
|
+
error: "invalid_request",
|
|
283
|
+
error_description: "invalid content type"
|
|
284
|
+
}, { status: 400 });
|
|
285
|
+
const parsed = googleOneTapSchema.parse(body);
|
|
286
|
+
let principal;
|
|
287
|
+
if (parsed.credential) {
|
|
288
|
+
const { token, userInfo } = await google.getTokenInfo(parsed.credential);
|
|
289
|
+
principal = await handler(request, registrationId, userInfo, token);
|
|
290
|
+
} else if (parsed.code) {
|
|
291
|
+
const token = await this.oauth2Client.exchangeAuthorizationCode({
|
|
292
|
+
registrationId,
|
|
293
|
+
code: parsed.code
|
|
294
|
+
});
|
|
295
|
+
principal = await handler(request, registrationId, await this.oauth2Client.getUserInfo({
|
|
296
|
+
registrationId,
|
|
297
|
+
token
|
|
298
|
+
}), token);
|
|
299
|
+
} else throw new Error("invalid request");
|
|
300
|
+
mark("save_principal");
|
|
301
|
+
const session = this.repository.createSession();
|
|
302
|
+
session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);
|
|
303
|
+
await this.repository.save(session);
|
|
304
|
+
const response = new Response(JSON.stringify(principal), {
|
|
305
|
+
status: 200,
|
|
306
|
+
headers: { "Content-Type": "application/json" }
|
|
307
|
+
});
|
|
308
|
+
const maxAge = session.getMaxInactiveInterval();
|
|
309
|
+
setCookie(response, this.cookieName, session.getId(), {
|
|
310
|
+
...this.cookieOptions,
|
|
311
|
+
maxAge
|
|
312
|
+
});
|
|
313
|
+
setTiming(response);
|
|
314
|
+
return response;
|
|
315
|
+
};
|
|
316
|
+
sendEmailVerificationCode = async (request, sender) => {
|
|
317
|
+
invariant(this.turnstileSecretKey, "turnstileSecretKey is not configured");
|
|
318
|
+
const { data, error } = valid(sendEmailVerificationCodeSchema, await request.json());
|
|
319
|
+
if (error) return Response.json(error, { status: error.error.code });
|
|
320
|
+
const { email, turnstileToken } = data;
|
|
321
|
+
const result = await verifyTurnstileToken(this.turnstileSecretKey, turnstileToken);
|
|
322
|
+
if (result.error) return Response.json(result.error, { status: result.error.error.code });
|
|
323
|
+
try {
|
|
324
|
+
const verificationCode = randomInt(1e6).toString().padStart(6, "0");
|
|
325
|
+
await sender({
|
|
326
|
+
email,
|
|
327
|
+
verificationCode
|
|
328
|
+
});
|
|
329
|
+
await this.kv.setItem(this.getEmailVerificationCodeKey(email), verificationCode, 600);
|
|
330
|
+
return Response.json({});
|
|
331
|
+
} catch (e) {
|
|
332
|
+
console.error("Failed to send email verification code", e);
|
|
333
|
+
return Response.json(invalidArgument("EMAIL_DELIVERY_FAILED"), { status: 400 });
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
loginEmail = async (request, handler) => {
|
|
337
|
+
const { data, error } = valid(loginEmailSchema, await request.json());
|
|
338
|
+
if (error) return Response.json(error, { status: error.error.code });
|
|
339
|
+
const { email, verificationCode } = data;
|
|
340
|
+
const key = this.getEmailVerificationCodeKey(email);
|
|
341
|
+
const stored = await this.kv.getItem(key);
|
|
342
|
+
if (typeof stored !== "string" || stored.length !== verificationCode.length || !timingSafeEqual(Buffer.from(stored), Buffer.from(verificationCode))) return Response.json(invalidArgument("INVALID_VERIFICATION_CODE"), { status: 400 });
|
|
343
|
+
await this.kv.removeItem(key);
|
|
344
|
+
const principal = await handler(request, "EMAIL", {
|
|
345
|
+
claims: {
|
|
346
|
+
sub: email,
|
|
347
|
+
email,
|
|
348
|
+
email_verified: true
|
|
349
|
+
},
|
|
350
|
+
data: {}
|
|
351
|
+
});
|
|
352
|
+
const session = this.repository.createSession();
|
|
353
|
+
session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);
|
|
354
|
+
await this.repository.save(session);
|
|
355
|
+
const response = new Response(JSON.stringify(principal), {
|
|
356
|
+
status: 200,
|
|
357
|
+
headers: { "Content-Type": "application/json" }
|
|
358
|
+
});
|
|
359
|
+
const maxAge = session.getMaxInactiveInterval();
|
|
360
|
+
setCookie(response, this.cookieName, session.getId(), {
|
|
361
|
+
...this.cookieOptions,
|
|
362
|
+
maxAge
|
|
363
|
+
});
|
|
364
|
+
return response;
|
|
365
|
+
};
|
|
366
|
+
/**
|
|
367
|
+
* Step 1 of the desktop / native app sign-in handshake. The caller already
|
|
368
|
+
* completed normal sign-in on the web (so a cookie session exists); this
|
|
369
|
+
* issues a 5-minute single-use code that the web page hands off to the
|
|
370
|
+
* desktop client via a loopback redirect.
|
|
371
|
+
*
|
|
372
|
+
* PKCE (RFC 7636, S256 only): the desktop generates a random `code_verifier`
|
|
373
|
+
* before opening the browser, derives `code_challenge = base64url(sha256(verifier))`,
|
|
374
|
+
* and threads the challenge through the URL → web page → here. We pin
|
|
375
|
+
* the challenge to the code in KV; `desktopExchange` enforces that the
|
|
376
|
+
* caller knows the matching verifier. This blocks an attacker who only
|
|
377
|
+
* intercepts the auth code (browser history, malicious extension, packet
|
|
378
|
+
* capture on loopback) from exchanging it.
|
|
379
|
+
*
|
|
380
|
+
* Requires an authenticated cookie session — anonymous callers get 401.
|
|
381
|
+
* Throttling / abuse protection on this endpoint is the API layer's job
|
|
382
|
+
* (rate-limit per principal); the SDK doesn't gate it because the
|
|
383
|
+
* upstream sign-in flow already passed CAPTCHA / OAuth / verification.
|
|
384
|
+
*/
|
|
385
|
+
desktopAuthorize = async (request) => {
|
|
386
|
+
const principal = await this.getPrincipal(request);
|
|
387
|
+
if (!principal) return Response.json({ error: {
|
|
388
|
+
code: 401,
|
|
389
|
+
status: "UNAUTHENTICATED",
|
|
390
|
+
message: "sign in first",
|
|
391
|
+
details: []
|
|
392
|
+
} }, { status: 401 });
|
|
393
|
+
let body;
|
|
394
|
+
try {
|
|
395
|
+
body = await request.json();
|
|
396
|
+
} catch {
|
|
397
|
+
return Response.json(invalidArgument("INVALID_ARGUMENT"), { status: 400 });
|
|
398
|
+
}
|
|
399
|
+
const codeChallenge = typeof body.code_challenge === "string" ? body.code_challenge : null;
|
|
400
|
+
if (!codeChallenge || body.code_challenge_method !== "S256" || !PKCE_FORMAT.test(codeChallenge)) return Response.json(invalidArgument("INVALID_ARGUMENT"), { status: 400 });
|
|
401
|
+
const code = randomUUID();
|
|
402
|
+
const record = {
|
|
403
|
+
name: principal.name,
|
|
404
|
+
cc: codeChallenge
|
|
405
|
+
};
|
|
406
|
+
await this.kv.setItem(this.getDesktopCodeKey(code), JSON.stringify(record), 300);
|
|
407
|
+
return Response.json({ code });
|
|
408
|
+
};
|
|
409
|
+
/**
|
|
410
|
+
* Step 2 of the desktop / native app sign-in handshake. The desktop
|
|
411
|
+
* client hands back the auth code it received via the loopback callback,
|
|
412
|
+
* and we mint a fresh session for it — independent of the web cookie
|
|
413
|
+
* session that originally `authorize`'d the code, so logging out on
|
|
414
|
+
* either side doesn't cascade to the other.
|
|
415
|
+
*
|
|
416
|
+
* Cookie attributes: defaults to `SameSite=None; Secure` so the cookie
|
|
417
|
+
* survives cross-site fetches from the desktop renderer's `file://` /
|
|
418
|
+
* `app://` origin. The configured base `cookieOptions` provide
|
|
419
|
+
* everything else (`domain`, `path`, `httpOnly`, `maxAge`); the override
|
|
420
|
+
* is targeted at the SameSite/Secure pair only. In a non-HTTPS dev
|
|
421
|
+
* backend this combination is rejected by Chromium — see the desktop
|
|
422
|
+
* sign-in PRD for workarounds.
|
|
423
|
+
*
|
|
424
|
+
* No CSRF check needed — the request carries no cookie, the auth code
|
|
425
|
+
* itself is the credential, and KV's single-use removal blocks replay.
|
|
426
|
+
*/
|
|
427
|
+
desktopExchange = async (request) => {
|
|
428
|
+
let body;
|
|
429
|
+
try {
|
|
430
|
+
body = await request.json();
|
|
431
|
+
} catch {
|
|
432
|
+
return Response.json(invalidArgument("INVALID_ARGUMENT"), { status: 400 });
|
|
433
|
+
}
|
|
434
|
+
const code = typeof body.code === "string" ? body.code : null;
|
|
435
|
+
const verifier = typeof body.code_verifier === "string" ? body.code_verifier : null;
|
|
436
|
+
if (!code || !verifier || !PKCE_FORMAT.test(verifier)) return Response.json(invalidArgument("INVALID_ARGUMENT"), { status: 400 });
|
|
437
|
+
const key = this.getDesktopCodeKey(code);
|
|
438
|
+
const raw = await this.kv.getItem(key);
|
|
439
|
+
if (typeof raw !== "string") return Response.json(invalidArgument("INVALID_DESKTOP_CODE"), { status: 400 });
|
|
440
|
+
let record;
|
|
441
|
+
try {
|
|
442
|
+
record = JSON.parse(raw);
|
|
443
|
+
} catch {
|
|
444
|
+
await this.kv.removeItem(key);
|
|
445
|
+
return Response.json(invalidArgument("INVALID_DESKTOP_CODE"), { status: 400 });
|
|
446
|
+
}
|
|
447
|
+
const computed = createHash("sha256").update(verifier).digest();
|
|
448
|
+
const stored = Buffer.from(record.cc, "base64url");
|
|
449
|
+
if (stored.length !== computed.length || !timingSafeEqual(computed, stored)) return Response.json(invalidArgument("INVALID_DESKTOP_CODE"), { status: 400 });
|
|
450
|
+
await this.kv.removeItem(key);
|
|
451
|
+
const session = this.repository.createSession();
|
|
452
|
+
session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, record.name);
|
|
453
|
+
session.setAttribute("client_type", "desktop");
|
|
454
|
+
await this.repository.save(session);
|
|
455
|
+
const response = Response.json({ ok: true });
|
|
456
|
+
const maxAge = session.getMaxInactiveInterval();
|
|
457
|
+
setCookie(response, this.cookieName, session.getId(), {
|
|
458
|
+
...this.cookieOptions,
|
|
459
|
+
sameSite: "none",
|
|
460
|
+
secure: true,
|
|
461
|
+
maxAge
|
|
462
|
+
});
|
|
463
|
+
return response;
|
|
464
|
+
};
|
|
465
|
+
sendPhoneVerificationCode = async (_request, _sender) => {
|
|
466
|
+
return Response.json({ error: "not_implemented" }, { status: 501 });
|
|
467
|
+
};
|
|
468
|
+
loginPhone = async (_request, _handler) => {
|
|
469
|
+
return Response.json({ error: "not_implemented" }, { status: 501 });
|
|
470
|
+
};
|
|
471
|
+
kick = async (principal) => {
|
|
472
|
+
const sessions = await this.repository.findByPrincipalName(principal.name);
|
|
473
|
+
for (const sessionId of sessions.keys()) await this.repository.deleteById(sessionId);
|
|
474
|
+
};
|
|
475
|
+
isAuthenticated = async (request) => {
|
|
476
|
+
const sessionId = this.getSessionId(request);
|
|
477
|
+
if (!sessionId) return false;
|
|
478
|
+
const session = await this.repository.findById(sessionId);
|
|
479
|
+
if (!session) return false;
|
|
480
|
+
if (!session.getAttribute(PRINCIPAL_NAME_INDEX_NAME)) return false;
|
|
481
|
+
return true;
|
|
482
|
+
};
|
|
483
|
+
getSession = async (request, create) => {
|
|
484
|
+
const sessionId = this.getSessionId(request);
|
|
485
|
+
if (!sessionId) if (create) {
|
|
486
|
+
const session = this.repository.createSession();
|
|
487
|
+
await this.repository.save(session);
|
|
488
|
+
return session;
|
|
489
|
+
} else return null;
|
|
490
|
+
const session = await this.repository.findById(sessionId);
|
|
491
|
+
if (!session) if (create) {
|
|
492
|
+
const session = this.repository.createSession();
|
|
493
|
+
await this.repository.save(session);
|
|
494
|
+
return session;
|
|
495
|
+
} else return null;
|
|
496
|
+
return session;
|
|
497
|
+
};
|
|
498
|
+
deleteSession = async (sessionId) => {
|
|
499
|
+
await this.repository.deleteById(sessionId);
|
|
500
|
+
};
|
|
501
|
+
getPrincipal = async (request) => {
|
|
502
|
+
const sessionId = this.getSessionId(request);
|
|
503
|
+
if (!sessionId) return null;
|
|
504
|
+
const session = await this.repository.findById(sessionId);
|
|
505
|
+
if (!session) return null;
|
|
506
|
+
const name = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);
|
|
507
|
+
if (!name) return null;
|
|
508
|
+
return { name: String(name) };
|
|
509
|
+
};
|
|
510
|
+
listSessions = async (principal) => {
|
|
511
|
+
const sessions = await this.repository.findByPrincipalName(principal.name);
|
|
512
|
+
return Array.from(sessions.values());
|
|
513
|
+
};
|
|
514
|
+
cleanupExpiredSessions = async (cleanupCount) => {
|
|
515
|
+
await this.repository.cleanupExpiredSessions(cleanupCount);
|
|
516
|
+
return Response.json({});
|
|
517
|
+
};
|
|
544
518
|
};
|
|
519
|
+
//#endregion
|
|
520
|
+
export { Auth, PATH };
|
|
521
|
+
|
|
545
522
|
//# sourceMappingURL=index.mjs.map
|