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