@shware/security 7.1.1 → 8.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/README.md +113 -32
  2. package/dist/auth/attributes.cjs +4 -3
  3. package/dist/auth/attributes.cjs.map +1 -1
  4. package/dist/auth/attributes.d.cts +3 -3
  5. package/dist/auth/attributes.d.cts.map +1 -1
  6. package/dist/auth/attributes.d.mts +3 -3
  7. package/dist/auth/attributes.d.mts.map +1 -1
  8. package/dist/auth/attributes.mjs +4 -3
  9. package/dist/auth/attributes.mjs.map +1 -1
  10. package/dist/auth/desktop-codes.cjs +57 -0
  11. package/dist/auth/desktop-codes.cjs.map +1 -0
  12. package/dist/auth/desktop-codes.d.cts +24 -0
  13. package/dist/auth/desktop-codes.d.cts.map +1 -0
  14. package/dist/auth/desktop-codes.d.mts +24 -0
  15. package/dist/auth/desktop-codes.d.mts.map +1 -0
  16. package/dist/auth/desktop-codes.mjs +55 -0
  17. package/dist/auth/desktop-codes.mjs.map +1 -0
  18. package/dist/auth/email-codes.cjs +77 -0
  19. package/dist/auth/email-codes.cjs.map +1 -0
  20. package/dist/auth/email-codes.d.cts +39 -0
  21. package/dist/auth/email-codes.d.cts.map +1 -0
  22. package/dist/auth/email-codes.d.mts +39 -0
  23. package/dist/auth/email-codes.d.mts.map +1 -0
  24. package/dist/auth/email-codes.mjs +75 -0
  25. package/dist/auth/email-codes.mjs.map +1 -0
  26. package/dist/auth/error.cjs +47 -0
  27. package/dist/auth/error.cjs.map +1 -0
  28. package/dist/auth/error.d.cts +88 -0
  29. package/dist/auth/error.d.cts.map +1 -0
  30. package/dist/auth/error.d.mts +88 -0
  31. package/dist/auth/error.d.mts.map +1 -0
  32. package/dist/auth/error.mjs +45 -0
  33. package/dist/auth/error.mjs.map +1 -0
  34. package/dist/auth/index.cjs +249 -384
  35. package/dist/auth/index.cjs.map +1 -1
  36. package/dist/auth/index.d.cts +66 -55
  37. package/dist/auth/index.d.cts.map +1 -1
  38. package/dist/auth/index.d.mts +66 -55
  39. package/dist/auth/index.d.mts.map +1 -1
  40. package/dist/auth/index.mjs +252 -387
  41. package/dist/auth/index.mjs.map +1 -1
  42. package/dist/auth/respond.cjs +99 -0
  43. package/dist/auth/respond.cjs.map +1 -0
  44. package/dist/auth/respond.d.cts +39 -0
  45. package/dist/auth/respond.d.cts.map +1 -0
  46. package/dist/auth/respond.d.mts +39 -0
  47. package/dist/auth/respond.d.mts.map +1 -0
  48. package/dist/auth/respond.mjs +92 -0
  49. package/dist/auth/respond.mjs.map +1 -0
  50. package/dist/auth/types.d.cts +27 -11
  51. package/dist/auth/types.d.cts.map +1 -1
  52. package/dist/auth/types.d.mts +27 -11
  53. package/dist/auth/types.d.mts.map +1 -1
  54. package/dist/index.cjs +5 -0
  55. package/dist/index.d.cts +4 -1
  56. package/dist/index.d.mts +4 -1
  57. package/dist/index.mjs +3 -1
  58. package/dist/oauth2/error.cjs +0 -7
  59. package/dist/oauth2/error.cjs.map +1 -1
  60. package/dist/oauth2/error.d.cts +0 -3
  61. package/dist/oauth2/error.d.cts.map +1 -1
  62. package/dist/oauth2/error.d.mts +0 -3
  63. package/dist/oauth2/error.d.mts.map +1 -1
  64. package/dist/oauth2/error.mjs +1 -7
  65. package/dist/oauth2/error.mjs.map +1 -1
  66. package/dist/oauth2/provider/common.cjs +17 -3
  67. package/dist/oauth2/provider/common.cjs.map +1 -1
  68. package/dist/oauth2/provider/common.d.cts.map +1 -1
  69. package/dist/oauth2/provider/common.d.mts.map +1 -1
  70. package/dist/oauth2/provider/common.mjs +17 -3
  71. package/dist/oauth2/provider/common.mjs.map +1 -1
  72. package/dist/oauth2/provider/facebook.cjs +2 -4
  73. package/dist/oauth2/provider/facebook.cjs.map +1 -1
  74. package/dist/oauth2/provider/facebook.d.cts +0 -1
  75. package/dist/oauth2/provider/facebook.d.cts.map +1 -1
  76. package/dist/oauth2/provider/facebook.d.mts +0 -1
  77. package/dist/oauth2/provider/facebook.d.mts.map +1 -1
  78. package/dist/oauth2/provider/facebook.mjs +2 -4
  79. package/dist/oauth2/provider/facebook.mjs.map +1 -1
  80. package/dist/oauth2/provider/github.cjs +1 -1
  81. package/dist/oauth2/provider/github.cjs.map +1 -1
  82. package/dist/oauth2/provider/github.d.cts +1 -1
  83. package/dist/oauth2/provider/github.d.cts.map +1 -1
  84. package/dist/oauth2/provider/github.d.mts +1 -1
  85. package/dist/oauth2/provider/github.d.mts.map +1 -1
  86. package/dist/oauth2/provider/github.mjs +1 -1
  87. package/dist/oauth2/provider/github.mjs.map +1 -1
  88. package/dist/oauth2/provider/x.cjs +6 -4
  89. package/dist/oauth2/provider/x.cjs.map +1 -1
  90. package/dist/oauth2/provider/x.d.cts +3 -2
  91. package/dist/oauth2/provider/x.d.cts.map +1 -1
  92. package/dist/oauth2/provider/x.d.mts +3 -2
  93. package/dist/oauth2/provider/x.d.mts.map +1 -1
  94. package/dist/oauth2/provider/x.mjs +6 -4
  95. package/dist/oauth2/provider/x.mjs.map +1 -1
  96. package/dist/oauth2/types.cjs.map +1 -1
  97. package/dist/oauth2/types.d.cts +5 -0
  98. package/dist/oauth2/types.d.cts.map +1 -1
  99. package/dist/oauth2/types.d.mts +5 -0
  100. package/dist/oauth2/types.d.mts.map +1 -1
  101. package/dist/oauth2/types.mjs.map +1 -1
  102. package/dist/session/common.cjs +10 -13
  103. package/dist/session/common.cjs.map +1 -1
  104. package/dist/session/common.d.cts +7 -3
  105. package/dist/session/common.d.cts.map +1 -1
  106. package/dist/session/common.d.mts +7 -3
  107. package/dist/session/common.d.mts.map +1 -1
  108. package/dist/session/common.mjs +10 -12
  109. package/dist/session/common.mjs.map +1 -1
  110. package/dist/session/db.cjs +2 -2
  111. package/dist/session/db.cjs.map +1 -1
  112. package/dist/session/db.mjs +3 -3
  113. package/dist/session/db.mjs.map +1 -1
  114. package/dist/session/redis.cjs +6 -8
  115. package/dist/session/redis.cjs.map +1 -1
  116. package/dist/session/redis.d.cts.map +1 -1
  117. package/dist/session/redis.d.mts.map +1 -1
  118. package/dist/session/redis.mjs +7 -9
  119. package/dist/session/redis.mjs.map +1 -1
  120. package/dist/session/types.d.cts +0 -12
  121. package/dist/session/types.d.cts.map +1 -1
  122. package/dist/session/types.d.mts +0 -12
  123. package/dist/session/types.d.mts.map +1 -1
  124. package/dist/utils/valid.cjs +34 -52
  125. package/dist/utils/valid.cjs.map +1 -1
  126. package/dist/utils/valid.d.cts +8 -23
  127. package/dist/utils/valid.d.cts.map +1 -1
  128. package/dist/utils/valid.d.mts +8 -23
  129. package/dist/utils/valid.d.mts.map +1 -1
  130. package/dist/utils/valid.mjs +32 -50
  131. package/dist/utils/valid.mjs.map +1 -1
  132. package/package.json +3 -3
@@ -1,21 +1,26 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_auth_error = require("./error.cjs");
3
+ const require_oauth2_error = require("../oauth2/error.cjs");
4
+ const require_auth_respond = require("./respond.cjs");
2
5
  const require_auth_attributes = require("./attributes.cjs");
3
6
  const require_email_schema = require("../email/schema.cjs");
4
- const require_oauth2_error = require("../oauth2/error.cjs");
5
7
  const require_oauth2_client = require("../oauth2/client.cjs");
6
8
  const require_session_common = require("../session/common.cjs");
7
9
  const require_utils_http = require("../utils/http.cjs");
8
10
  const require_utils_timing = require("../utils/timing.cjs");
9
11
  const require_utils_valid = require("../utils/valid.cjs");
12
+ const require_auth_desktop_codes = require("./desktop-codes.cjs");
13
+ const require_auth_email_codes = require("./email-codes.cjs");
10
14
  let node_crypto = require("node:crypto");
11
15
  let _shware_utils = require("@shware/utils");
16
+ let zod_mini = require("zod/mini");
12
17
  // src/auth/index.ts
13
18
  const PKCE_FORMAT = /^[A-Za-z0-9_-]{43,128}$/;
14
19
  const CODE_VERIFIER_FORMAT = /^[A-Za-z0-9._~-]{43,128}$/;
15
- const INVALID_CODE_VERIFIER = {
16
- error: "invalid_request",
17
- error_description: "code_verifier must be 43-128 characters from [A-Za-z0-9-._~]"
18
- };
20
+ /** The header a session id travels in besides the cookie, with `session.header` on. */
21
+ const SESSION_HEADER = "X-Auth-Token";
22
+ /** Seconds an OAuth2 state or nonce may be used within. */
23
+ const OAUTH2_PARAMETER_TTL = 600;
19
24
  const MAX_ERROR_DESCRIPTION_LENGTH = 256;
20
25
  /** An error code safe to put on errorUri: a registered one as is, anything else as `fallback`. */
21
26
  function safeErrorType(error, fallback) {
@@ -42,14 +47,20 @@ function authorizationAttributes(attributes) {
42
47
  return defined;
43
48
  }
44
49
  /** Sign in with Apple answers with `response_mode=form_post`; a charset parameter may follow. */
45
- const INVALID_ATTRIBUTES = {
46
- error: "invalid_request",
47
- error_description: `attributes must map strings to strings, at most ${require_auth_attributes.MAX_ATTRIBUTES_BYTES} bytes`
48
- };
49
50
  function isFormPost(request) {
50
51
  const contentType = request.headers.get("content-type")?.toLowerCase();
51
52
  return request.method === "POST" && !!contentType?.startsWith("application/x-www-form-urlencoded");
52
53
  }
54
+ /** Google's own form post carries no attributes; an app posting JSON may add them. */
55
+ const oneTapBodySchema = (0, zod_mini.extend)(require_oauth2_client.googleOneTapSchema, { attributes: require_auth_attributes.attributesSchema });
56
+ const desktopAuthorizeSchema = (0, zod_mini.object)({
57
+ code_challenge: (0, zod_mini.string)().check((0, zod_mini.regex)(PKCE_FORMAT)),
58
+ code_challenge_method: (0, zod_mini.literal)("S256")
59
+ });
60
+ const desktopExchangeSchema = (0, zod_mini.object)({
61
+ code: (0, zod_mini.string)(),
62
+ code_verifier: (0, zod_mini.string)().check((0, zod_mini.regex)(PKCE_FORMAT))
63
+ });
53
64
  const PATH = {
54
65
  CSRF: "/csrf",
55
66
  LOGOUT: "/logout",
@@ -61,15 +72,18 @@ const PATH = {
61
72
  LOGIN_OAUTH2_NATIVE: "/login/oauth2/native/:provider",
62
73
  LOGIN_OAUTH2_ONETAP: "/login/oauth2/onetap/google",
63
74
  LOGIN_EMAIL: "/login/email",
64
- LOGIN_PHONE: "/login/phone",
65
75
  SEND_EMAIL_VERIFICATION_CODE: "/auth/sendEmailVerificationCode",
66
- SEND_PHONE_VERIFICATION_CODE: "/auth/sendPhoneVerificationCode",
67
76
  DESKTOP_AUTHORIZE: "/auth/desktop/authorize",
68
77
  DESKTOP_EXCHANGE: "/auth/desktop/exchange",
69
78
  CLEANUP_EXPIRED_SESSIONS: "/sessions/expired/cleanup"
70
79
  };
71
- /** Seconds an email verification code, and its attempt counter, stay valid. */
72
- const EMAIL_VERIFICATION_CODE_TTL = 600;
80
+ /**
81
+ * The login routes and session helpers. Every route is one straight line: read and validate the
82
+ * request, check the credential, call the host's `onAuthorized`, sign in. Failures are thrown and
83
+ * answered in one place per kind of route: a JSON route runs under `guard`, which maps a
84
+ * `RequestError`, an `AuthenticationError` or an `OAuth2Error` to its google.rpc body (see
85
+ * `respond.ts`); the redirect callback maps the same errors to `errorUri` parameters (`redirect`).
86
+ */
73
87
  var Auth = class {
74
88
  timing;
75
89
  cookieName;
@@ -77,21 +91,27 @@ var Auth = class {
77
91
  kv;
78
92
  repository;
79
93
  turnstileSecretKey;
80
- maxVerificationAttempts;
81
- verificationResendCooldown;
94
+ emailCodes;
95
+ desktopCodes;
82
96
  oauth2Client;
83
97
  onetapRequireNonce;
84
98
  touchInterval;
99
+ sessionHeader;
85
100
  ATTR_OAUTH2_AUTHORIZATION_REQUEST = "oauth2AuthorizationRequest";
86
101
  constructor({ sessionRepository, kvRepository, turnstileSecretKey, verificationCode, oauth2, cookie, session, timing }) {
87
102
  this.timing = timing ?? false;
88
103
  this.kv = kvRepository;
89
104
  this.repository = sessionRepository;
90
105
  this.turnstileSecretKey = turnstileSecretKey;
91
- this.maxVerificationAttempts = verificationCode?.maxAttempts ?? 5;
92
- this.verificationResendCooldown = verificationCode?.resendCooldown ?? 60;
93
- (0, _shware_utils.invariant)(this.maxVerificationAttempts >= 1, "verificationCode.maxAttempts must be at least 1");
94
- (0, _shware_utils.invariant)(this.verificationResendCooldown >= 1, "verificationCode.resendCooldown must be at least 1 second");
106
+ const maxAttempts = verificationCode?.maxAttempts ?? 5;
107
+ const resendCooldown = verificationCode?.resendCooldown ?? 60;
108
+ (0, _shware_utils.invariant)(maxAttempts >= 1, "verificationCode.maxAttempts must be at least 1");
109
+ (0, _shware_utils.invariant)(resendCooldown >= 1, "verificationCode.resendCooldown must be at least 1 second");
110
+ this.emailCodes = new require_auth_email_codes.EmailVerificationCodes(kvRepository, {
111
+ maxAttempts,
112
+ resendCooldown
113
+ });
114
+ this.desktopCodes = new require_auth_desktop_codes.DesktopCodes(kvRepository);
95
115
  const { name, ...cookieOptions } = cookie ?? {};
96
116
  this.cookieName = name ?? "SESSION";
97
117
  this.cookieOptions = {
@@ -104,14 +124,76 @@ var Auth = class {
104
124
  this.oauth2Client = oauth2?.client ? new require_oauth2_client.OAuth2Client(oauth2.client) : null;
105
125
  this.onetapRequireNonce = oauth2?.onetap?.requireNonce ?? true;
106
126
  this.touchInterval = (session?.touchInterval ?? 3600) * 1e3;
127
+ this.sessionHeader = session?.header ?? false;
107
128
  (0, _shware_utils.invariant)(this.touchInterval >= 0, "session.touchInterval must not be negative");
108
129
  }
109
130
  getSessionId(request) {
110
- let sessionId;
111
- const header = request.headers.get("Authorization");
112
- if (header?.startsWith("Bearer ")) sessionId = header.replace("Bearer ", "");
113
- if (!sessionId) sessionId = require_utils_http.getCookie(request, this.cookieName);
114
- return sessionId;
131
+ const fromCookie = require_utils_http.getCookie(request, this.cookieName);
132
+ if (fromCookie) return fromCookie;
133
+ return this.sessionHeader && request.headers.get(SESSION_HEADER) || void 0;
134
+ }
135
+ /** The session the request carries, if it exists. */
136
+ async loadSession(request) {
137
+ const sessionId = this.getSessionId(request);
138
+ return sessionId ? this.repository.findById(sessionId) : null;
139
+ }
140
+ /** The signed-in principal of a session, if any. */
141
+ principalOf(session) {
142
+ const name = session.getAttribute(require_session_common.PRINCIPAL_NAME_INDEX_NAME);
143
+ return name ? { name: String(name) } : null;
144
+ }
145
+ /**
146
+ * A new session signed in as `principal`. With `carryOver`, what the host kept in the pre-login
147
+ * session moves to the new one; the authorization request (state, PKCE verifier, flow
148
+ * attributes) is single-use and was consumed, and a principal left there from an earlier
149
+ * sign-in is replaced. The new id is never the one the browser brought, which may have been
150
+ * planted by an attacker who started the flow (session fixation).
151
+ */
152
+ async signIn(principal, carryOver) {
153
+ const session = this.repository.createSession();
154
+ for (const attribute of carryOver?.getAttributeNames() ?? []) {
155
+ if (attribute === this.ATTR_OAUTH2_AUTHORIZATION_REQUEST) continue;
156
+ if (attribute === "principalName") continue;
157
+ const value = carryOver?.getAttribute(attribute);
158
+ if (value !== null && value !== void 0) session.setAttribute(attribute, value);
159
+ }
160
+ session.setAttribute(require_session_common.PRINCIPAL_NAME_INDEX_NAME, principal.name);
161
+ await this.repository.save(session);
162
+ return session;
163
+ }
164
+ /**
165
+ * `response` carrying `session`: its cookie, renewed for the whole inactive interval, and with
166
+ * `session.header` on, the `X-Auth-Token` header a client without a cookie jar reads.
167
+ */
168
+ withSession(response, session, cookie) {
169
+ const maxAge = session.getMaxInactiveInterval();
170
+ require_utils_http.setCookie(response, this.cookieName, session.getId(), {
171
+ ...this.cookieOptions,
172
+ ...cookie,
173
+ maxAge
174
+ });
175
+ if (this.sessionHeader) response.headers.set(SESSION_HEADER, session.getId());
176
+ return response;
177
+ }
178
+ /** Runs a JSON route: a failure it knows is its response, anything else is the host's. */
179
+ async guard(run) {
180
+ try {
181
+ return await run();
182
+ } catch (error) {
183
+ const response = require_auth_respond.errorResponse(error);
184
+ if (response) return response;
185
+ throw error;
186
+ }
187
+ }
188
+ /** The error redirect of the callback flow; `refusal`'s reason and metadata ride along. */
189
+ redirect(error, description, refusal) {
190
+ (0, _shware_utils.invariant)(this.oauth2Client, "oauth2Client is not initialized");
191
+ const uri = new URL(this.oauth2Client.errorUri);
192
+ for (const [key, value] of Object.entries(refusal?.metadata ?? {})) if (!uri.searchParams.has(key)) uri.searchParams.set(key, value);
193
+ uri.searchParams.set("error", error);
194
+ if (description) uri.searchParams.set("error_description", description.slice(0, MAX_ERROR_DESCRIPTION_LENGTH));
195
+ if (refusal) uri.searchParams.set("reason", refusal.reason);
196
+ return Response.redirect(uri.href, 302);
115
197
  }
116
198
  csrf = async (_request) => {
117
199
  const token = (0, node_crypto.randomUUID)();
@@ -137,24 +219,16 @@ var Auth = class {
137
219
  return response;
138
220
  };
139
221
  logged = async (request, onLogged) => {
140
- const sessionId = this.getSessionId(request);
141
- if (!sessionId) return Response.json({ data: false });
142
- const session = await this.repository.findById(sessionId);
222
+ const session = await this.loadSession(request);
143
223
  if (!session) return Response.json({ data: false });
144
- const name = session.getAttribute(require_session_common.PRINCIPAL_NAME_INDEX_NAME);
145
224
  session.setLastAccessedTime(Date.now());
146
225
  await this.repository.save(session);
147
- if (name !== null) await onLogged?.({ name: String(name) });
148
- const response = Response.json({ data: name !== null });
149
- const maxAge = session.getMaxInactiveInterval();
150
- require_utils_http.setCookie(response, this.cookieName, session.getId(), {
151
- ...this.cookieOptions,
152
- maxAge
153
- });
154
- return response;
226
+ const principal = this.principalOf(session);
227
+ if (principal) await onLogged?.(principal);
228
+ return this.withSession(Response.json({ data: principal !== null }), session);
155
229
  };
156
230
  createPkceParameters() {
157
- const code_verifier = (0, node_crypto.randomUUID)();
231
+ const code_verifier = (0, node_crypto.randomBytes)(32).toString("base64url");
158
232
  return {
159
233
  code_verifier,
160
234
  code_challenge: (0, node_crypto.createHash)("sha256").update(code_verifier).digest().toString("base64url"),
@@ -167,34 +241,29 @@ var Auth = class {
167
241
  getOauth2NonceKey(nonce) {
168
242
  return `oauth2:nonce:${nonce}`;
169
243
  }
170
- getEmailVerificationCodeKey(email) {
171
- return `email:verification:${email}`;
172
- }
173
- getEmailVerificationAttemptsKey(email) {
174
- return `email:verification-attempts:${email}`;
175
- }
176
- getEmailVerificationCooldownKey(email) {
177
- return `email:verification-cooldown:${email}`;
178
- }
179
- getPhoneVerificationCodeKey(phone) {
180
- return `phone:verification:${phone}`;
181
- }
182
- getDesktopCodeKey(code) {
183
- return `desktop:code:${code}`;
244
+ /**
245
+ * Spends a nonce `oauth2Nonce` issued for `provider`: `NONCE_MISMATCH` for one that was not,
246
+ * or was used already. An id token is replayable until it expires; the nonce it carries makes
247
+ * it sign in once.
248
+ */
249
+ async consumeNonce(nonce, provider) {
250
+ const key = this.getOauth2NonceKey(nonce);
251
+ if (await this.kv.getItem(key) !== provider) throw new require_auth_error.AuthenticationError("NONCE_MISMATCH");
252
+ await this.kv.removeItem(key);
184
253
  }
185
254
  oauth2State = async (request) => {
186
255
  const { provider } = require_utils_http.param(request, PATH.OAUTH2_STATE);
187
256
  const state = (0, node_crypto.randomUUID)();
188
- await this.kv.setItem(this.getOauth2StateKey(state), provider, 600);
257
+ await this.kv.setItem(this.getOauth2StateKey(state), provider, OAUTH2_PARAMETER_TTL);
189
258
  return Response.json({ state });
190
259
  };
191
260
  oauth2Nonce = async (request) => {
192
261
  const { provider } = require_utils_http.param(request, PATH.OAUTH2_NONCE);
193
262
  const nonce = (0, node_crypto.randomUUID)();
194
- await this.kv.setItem(this.getOauth2NonceKey(nonce), provider, 600);
263
+ await this.kv.setItem(this.getOauth2NonceKey(nonce), provider, OAUTH2_PARAMETER_TTL);
195
264
  return Response.json({ nonce });
196
265
  };
197
- oauth2Authorization = async (request, options) => {
266
+ oauth2Authorization = (request, options) => this.guard(async () => {
198
267
  (0, _shware_utils.invariant)(this.oauth2Client, "oauth2Client is not initialized");
199
268
  const attributes = authorizationAttributes(options?.attributes);
200
269
  const { mark, setTiming } = require_utils_timing.timing({ enabled: this.timing });
@@ -206,8 +275,11 @@ var Auth = class {
206
275
  state,
207
276
  pkce
208
277
  });
209
- const sessionId = this.getSessionId(request);
210
- const session = sessionId ? await this.repository.findById(sessionId) ?? this.repository.createSession() : this.repository.createSession();
278
+ let session = await this.loadSession(request);
279
+ if (!session) {
280
+ session = this.repository.createSession();
281
+ session.setMaxInactiveInterval(OAUTH2_PARAMETER_TTL);
282
+ }
211
283
  mark("fetch_session");
212
284
  const authorizationRequest = {
213
285
  state,
@@ -216,56 +288,38 @@ var Auth = class {
216
288
  additionalParameters: pkce,
217
289
  ...attributes ? { attributes } : {}
218
290
  };
219
- const value = JSON.stringify(authorizationRequest);
220
- session.setAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST, value);
291
+ session.setAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST, JSON.stringify(authorizationRequest));
221
292
  await this.repository.save(session);
222
293
  mark("save_session");
223
294
  const response = new Response(null, {
224
295
  status: 302,
225
296
  headers: { location: uri.href }
226
297
  });
227
- const maxAge = session.getMaxInactiveInterval();
228
- require_utils_http.setCookie(response, this.cookieName, session.getId(), {
229
- ...this.cookieOptions,
230
- maxAge
231
- });
232
298
  setTiming(response);
233
- return response;
234
- };
235
- redirect = (error, description) => {
236
- (0, _shware_utils.invariant)(this.oauth2Client, "oauth2Client is not initialized");
237
- const uri = new URL(this.oauth2Client.errorUri);
238
- uri.searchParams.set("error", error);
239
- if (description) uri.searchParams.set("error_description", description.slice(0, MAX_ERROR_DESCRIPTION_LENGTH));
240
- return Response.redirect(uri.href, 302);
241
- };
299
+ return this.withSession(response, session);
300
+ });
242
301
  loginOAuth2Code = async (request, handler) => {
243
302
  (0, _shware_utils.invariant)(this.oauth2Client, "oauth2Client is not initialized");
244
303
  const { mark, setTiming } = require_utils_timing.timing({ enabled: this.timing });
245
- const sessionId = this.getSessionId(request);
246
- const session = sessionId ? await this.repository.findById(sessionId) : null;
304
+ const session = await this.loadSession(request);
247
305
  if (!session) return this.redirect("invalid_request", "session not found");
248
306
  mark("load_session");
249
307
  const json = session.getAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST);
250
308
  if (!json) return this.redirect("invalid_request", "authorization request not found");
251
309
  const cached = JSON.parse(json);
252
310
  const { provider } = require_utils_http.param(request, PATH.LOGIN_OAUTH2_CODE);
253
- let data;
254
- if (isFormPost(request)) {
255
- const params = new URLSearchParams(await request.text());
256
- data = Object.fromEntries(params.entries());
257
- } else data = require_utils_http.query(request);
311
+ const data = isFormPost(request) ? Object.fromEntries(new URLSearchParams(await request.text())) : require_utils_http.query(request);
258
312
  const parsed = require_oauth2_client.oauth2RedirectQuerySchema.safeParse(data);
259
313
  if (!parsed.success) return this.redirect("invalid_request", "invalid redirect query");
260
314
  if (parsed.data.error) {
261
315
  if (parsed.data.state && parsed.data.state !== cached.state) return this.redirect("invalid_request", "redirect state mismatch");
262
316
  return this.redirect(safeErrorType(parsed.data.error, "server_error"), parsed.data.error_description);
263
317
  }
264
- if (!parsed.data.code || !parsed.data.state) return this.redirect("invalid_request", "invalid redirect query");
265
- if (parsed.data.state !== cached.state) return this.redirect("invalid_request", "redirect state mismatch");
318
+ const { code, state, user } = parsed.data;
319
+ if (!code || !state) return this.redirect("invalid_request", "invalid redirect query");
320
+ if (state !== cached.state) return this.redirect("invalid_request", "redirect state mismatch");
266
321
  if (provider !== cached.provider) return this.redirect("invalid_request", "redirect provider mismatch");
267
322
  mark("validate_session");
268
- const { code, user } = parsed.data;
269
323
  let signedIn;
270
324
  try {
271
325
  const { clientId, token } = await this.oauth2Client.exchangeAuthorizationCode({
@@ -288,8 +342,7 @@ var Auth = class {
288
342
  attributes: cached.attributes ?? {}
289
343
  });
290
344
  mark("save_principal");
291
- signedIn = this.signedInSession(session, principal.name);
292
- await this.repository.save(signedIn);
345
+ signedIn = await this.signIn(principal, session);
293
346
  await this.repository.deleteById(session.getId());
294
347
  mark("update_session");
295
348
  } catch (error) {
@@ -298,6 +351,10 @@ var Auth = class {
298
351
  const type = CALLBACK_ERROR_TYPES.has(error.error) ? error.error : "server_error";
299
352
  return this.redirect(type);
300
353
  }
354
+ if (error instanceof require_auth_error.AuthenticationError) {
355
+ console.warn("OAuth2 sign-in refused", error.reason, error.metadata);
356
+ return this.redirect("access_denied", void 0, error);
357
+ }
301
358
  console.error("OAuth2 callback failed", error);
302
359
  return this.redirect("server_error");
303
360
  }
@@ -308,328 +365,157 @@ var Auth = class {
308
365
  status: 302,
309
366
  headers: { location: uri.href }
310
367
  });
311
- const maxAge = signedIn.getMaxInactiveInterval();
312
- require_utils_http.setCookie(response, this.cookieName, signedIn.getId(), {
313
- ...this.cookieOptions,
314
- maxAge
315
- });
316
368
  setTiming(response);
317
- return response;
369
+ return this.withSession(response, signedIn);
318
370
  };
319
- /**
320
- * A fresh session for `name`, carrying over what the host kept in the pre-login one. The
321
- * authorization request (state, PKCE verifier, flow attributes) is single-use and was consumed
322
- * by this callback, and a principal left there from an earlier sign-in is replaced.
323
- */
324
- signedInSession(previous, name) {
325
- const session = this.repository.createSession();
326
- for (const attribute of previous.getAttributeNames()) {
327
- if (attribute === this.ATTR_OAUTH2_AUTHORIZATION_REQUEST) continue;
328
- if (attribute === "org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME") continue;
329
- const value = previous.getAttribute(attribute);
330
- if (value !== null) session.setAttribute(attribute, value);
331
- }
332
- session.setAttribute(require_session_common.PRINCIPAL_NAME_INDEX_NAME, name);
333
- return session;
371
+ /** The shared tail of the JSON login routes: the host's handler, then a signed-in session. */
372
+ async authorize(request, handler, authorization, setTiming) {
373
+ const principal = await handler(request, authorization);
374
+ const session = await this.signIn(principal);
375
+ const response = Response.json(principal);
376
+ setTiming(response);
377
+ return this.withSession(response, session);
334
378
  }
335
- loginOAuth2Native = async (request, handler) => {
379
+ loginOAuth2Native = (request, handler) => this.guard(async () => {
336
380
  (0, _shware_utils.invariant)(this.oauth2Client, "oauth2Client is not initialized");
337
381
  const { mark, setTiming } = require_utils_timing.timing({ enabled: this.timing });
338
382
  const { provider } = require_utils_http.param(request, PATH.LOGIN_OAUTH2_NATIVE);
339
- const body = await request.json();
383
+ const body = await require_utils_valid.readJson(request);
340
384
  const attributes = require_auth_attributes.bodyAttributes(body);
341
- if (!attributes) return Response.json(INVALID_ATTRIBUTES, { status: 400 });
342
385
  const { attributes: _, ...credentials } = body;
343
386
  const { code_verifier } = credentials;
344
- if (code_verifier !== void 0 && (typeof code_verifier !== "string" || !CODE_VERIFIER_FORMAT.test(code_verifier))) return Response.json(INVALID_CODE_VERIFIER, { status: 400 });
387
+ if (code_verifier !== void 0 && (typeof code_verifier !== "string" || !CODE_VERIFIER_FORMAT.test(code_verifier))) throw require_auth_respond.invalidArgument("INVALID_CODE_VERIFIER", "code_verifier must be 43-128 characters from [A-Za-z0-9-._~]");
345
388
  const { clientId, userInfo, token } = await this.oauth2Client.loginOAuth2Native({
346
389
  provider,
347
390
  credentials
348
391
  });
349
392
  mark("login_oauth2_native");
350
393
  const { nonce } = userInfo.data;
351
- if (nonce) {
352
- if (await this.kv.getItem(this.getOauth2NonceKey(nonce)) !== provider) return Response.json({
353
- error: "invalid_request",
354
- error_description: "nonce mismatch"
355
- }, { status: 400 });
356
- await this.kv.removeItem(this.getOauth2NonceKey(nonce));
394
+ if (typeof nonce === "string" && nonce) {
395
+ await this.consumeNonce(nonce, provider);
357
396
  mark("check_nonce");
358
397
  }
359
- const principal = await handler(request, {
398
+ return this.authorize(request, handler, {
360
399
  provider,
361
400
  clientId,
362
401
  userInfo,
363
402
  token,
364
403
  attributes
365
- });
366
- mark("save_principal");
367
- const session = this.repository.createSession();
368
- session.setAttribute(require_session_common.PRINCIPAL_NAME_INDEX_NAME, principal.name);
369
- await this.repository.save(session);
370
- const response = new Response(JSON.stringify(principal), {
371
- status: 200,
372
- headers: { "Content-Type": "application/json" }
373
- });
374
- const maxAge = session.getMaxInactiveInterval();
375
- require_utils_http.setCookie(response, this.cookieName, session.getId(), {
376
- ...this.cookieOptions,
377
- maxAge
378
- });
379
- setTiming(response);
380
- return response;
381
- };
382
- loginOAuth2Onetap = async (request, handler, provider = "google") => {
404
+ }, setTiming);
405
+ });
406
+ loginOAuth2Onetap = (request, handler, provider = "google") => this.guard(async () => {
383
407
  (0, _shware_utils.invariant)(this.oauth2Client, "oauth2Client is not initialized");
384
408
  const { mark, setTiming } = require_utils_timing.timing({ enabled: this.timing });
385
- let body;
386
- let attributes = {};
387
- if (request.headers.get("Content-Type")?.includes("application/x-www-form-urlencoded")) {
388
- const text = await request.text();
389
- body = Object.fromEntries(new URLSearchParams(text));
390
- } else if (request.headers.get("Content-Type")?.includes("application/json")) {
391
- const json = await request.json();
392
- attributes = require_auth_attributes.bodyAttributes(json);
393
- body = json;
394
- } else return Response.json({
395
- error: "invalid_request",
396
- error_description: "invalid content type"
397
- }, { status: 400 });
398
- if (!attributes) return Response.json(INVALID_ATTRIBUTES, { status: 400 });
399
- const parsed = require_oauth2_client.googleOneTapSchema.parse(body);
400
- let principal;
409
+ const body = (request.headers.get("content-type") ?? "").includes("application/json") ? await require_utils_valid.readJson(request) : await require_utils_valid.readForm(request);
410
+ const { attributes = {}, ...parsed } = require_utils_valid.parse(oneTapBodySchema, body);
411
+ let clientId;
412
+ let token;
413
+ let userInfo;
401
414
  if (parsed.credential) {
402
- const { clientId, token, userInfo } = await this.oauth2Client.getTokenInfo({
415
+ ({clientId, token, userInfo} = await this.oauth2Client.getTokenInfo({
403
416
  provider,
404
417
  credential: parsed.credential
405
- });
418
+ }));
406
419
  mark("verify_credential");
407
420
  const { nonce } = userInfo.data;
408
421
  if (typeof nonce === "string" && nonce) {
409
- if (await this.kv.getItem(this.getOauth2NonceKey(nonce)) !== provider) return Response.json({
410
- error: "invalid_request",
411
- error_description: "nonce mismatch"
412
- }, { status: 401 });
413
- await this.kv.removeItem(this.getOauth2NonceKey(nonce));
422
+ await this.consumeNonce(nonce, provider);
414
423
  mark("check_nonce");
415
- } else if (this.onetapRequireNonce) return Response.json({
416
- error: "invalid_request",
417
- error_description: "nonce missing"
418
- }, { status: 401 });
419
- principal = await handler(request, {
420
- provider,
421
- clientId,
422
- userInfo,
423
- token,
424
- attributes
425
- });
424
+ } else if (this.onetapRequireNonce) throw new require_auth_error.AuthenticationError("NONCE_MISSING");
426
425
  } else if (parsed.code) {
427
- const { clientId, token } = await this.oauth2Client.exchangeAuthorizationCode({
426
+ ({clientId, token} = await this.oauth2Client.exchangeAuthorizationCode({
428
427
  provider,
429
428
  code: parsed.code
430
- });
431
- principal = await handler(request, {
429
+ }));
430
+ userInfo = await this.oauth2Client.getUserInfo({
432
431
  provider,
433
432
  clientId,
434
- userInfo: await this.oauth2Client.getUserInfo({
435
- provider,
436
- clientId,
437
- token
438
- }),
439
- token,
440
- attributes
433
+ token
441
434
  });
442
- } else throw new Error("invalid request");
443
- mark("save_principal");
444
- const session = this.repository.createSession();
445
- session.setAttribute(require_session_common.PRINCIPAL_NAME_INDEX_NAME, principal.name);
446
- await this.repository.save(session);
447
- const response = new Response(JSON.stringify(principal), {
448
- status: 200,
449
- headers: { "Content-Type": "application/json" }
450
- });
451
- const maxAge = session.getMaxInactiveInterval();
452
- require_utils_http.setCookie(response, this.cookieName, session.getId(), {
453
- ...this.cookieOptions,
454
- maxAge
455
- });
456
- setTiming(response);
457
- return response;
458
- };
459
- sendEmailVerificationCode = async (request, sender) => {
435
+ } else throw require_auth_respond.invalidArgument("INVALID_ARGUMENT", "credential or code is required");
436
+ return this.authorize(request, handler, {
437
+ provider,
438
+ clientId,
439
+ userInfo,
440
+ token,
441
+ attributes
442
+ }, setTiming);
443
+ });
444
+ sendEmailVerificationCode = (request, sender) => this.guard(async () => {
460
445
  (0, _shware_utils.invariant)(this.turnstileSecretKey, "turnstileSecretKey is not configured");
461
- const { data, error } = require_utils_valid.valid(require_email_schema.sendEmailVerificationCodeSchema, await request.json());
462
- if (error) return Response.json(error, { status: error.error.code });
463
- const { email, turnstileToken } = data;
464
- const result = await require_utils_valid.verifyTurnstileToken(this.turnstileSecretKey, turnstileToken);
465
- if (result.error) return Response.json(result.error, { status: result.error.error.code });
466
- const cooldownKey = this.getEmailVerificationCooldownKey(email);
467
- if (await this.kv.increment(cooldownKey, this.verificationResendCooldown) > 1) {
468
- const error = require_utils_valid.resourceExhausted("VERIFICATION_CODE_RESEND_TOO_SOON", "Verification code was sent recently");
469
- return Response.json(error, { status: error.error.code });
470
- }
471
- try {
472
- const verificationCode = (0, node_crypto.randomInt)(1e6).toString().padStart(6, "0");
473
- await sender({
474
- email,
475
- verificationCode
476
- });
477
- await this.kv.removeItem(this.getEmailVerificationAttemptsKey(email));
478
- await this.kv.setItem(this.getEmailVerificationCodeKey(email), verificationCode, EMAIL_VERIFICATION_CODE_TTL);
479
- return Response.json({});
480
- } catch (e) {
481
- console.error("Failed to send email verification code", e);
482
- await this.kv.removeItem(cooldownKey);
483
- return Response.json(require_utils_valid.invalidArgument("EMAIL_DELIVERY_FAILED"), { status: 400 });
484
- }
485
- };
486
- loginEmail = async (request, handler) => {
487
- const { data, error } = require_utils_valid.valid(require_email_schema.loginEmailSchema, await request.json());
488
- if (error) return Response.json(error, { status: error.error.code });
489
- const { email, verificationCode, attributes } = data;
490
- const key = this.getEmailVerificationCodeKey(email);
491
- const attemptsKey = this.getEmailVerificationAttemptsKey(email);
492
- const stored = await this.kv.getItem(key);
493
- if (typeof stored !== "string") return Response.json(require_utils_valid.invalidArgument("INVALID_VERIFICATION_CODE"), { status: 400 });
494
- if (await this.kv.increment(attemptsKey, EMAIL_VERIFICATION_CODE_TTL) > this.maxVerificationAttempts) {
495
- await this.kv.removeItem(key);
496
- const error = require_utils_valid.invalidArgument("VERIFICATION_CODE_ATTEMPTS_EXCEEDED");
497
- return Response.json(error, { status: error.error.code });
498
- }
499
- if (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 });
500
- await this.kv.removeItem(key);
501
- await this.kv.removeItem(attemptsKey);
502
- const principal = await handler(request, {
446
+ const { email, turnstileToken } = require_utils_valid.parse(require_email_schema.sendEmailVerificationCodeSchema, await require_utils_valid.readJson(request));
447
+ await require_utils_valid.verifyTurnstileToken(this.turnstileSecretKey, turnstileToken);
448
+ await this.emailCodes.send(email, sender);
449
+ return Response.json({});
450
+ });
451
+ verifyEmailCode = (email, verificationCode) => this.emailCodes.verify(email, verificationCode);
452
+ loginEmail = (request, handler) => this.guard(async () => {
453
+ const { setTiming } = require_utils_timing.timing({ enabled: this.timing });
454
+ const { email, verificationCode, attributes = {} } = require_utils_valid.parse(require_email_schema.loginEmailSchema, await require_utils_valid.readJson(request));
455
+ await this.emailCodes.verify(email, verificationCode);
456
+ const claims = {
457
+ sub: email,
458
+ email,
459
+ email_verified: true
460
+ };
461
+ return this.authorize(request, handler, {
503
462
  provider: "EMAIL",
504
463
  userInfo: {
505
- claims: {
506
- sub: email,
507
- email,
508
- email_verified: true
509
- },
464
+ claims,
510
465
  data: {}
511
466
  },
512
- attributes: attributes ?? {}
513
- });
514
- const session = this.repository.createSession();
515
- session.setAttribute(require_session_common.PRINCIPAL_NAME_INDEX_NAME, principal.name);
516
- await this.repository.save(session);
517
- const response = new Response(JSON.stringify(principal), {
518
- status: 200,
519
- headers: { "Content-Type": "application/json" }
520
- });
521
- const maxAge = session.getMaxInactiveInterval();
522
- require_utils_http.setCookie(response, this.cookieName, session.getId(), {
523
- ...this.cookieOptions,
524
- maxAge
525
- });
526
- return response;
527
- };
467
+ attributes
468
+ }, setTiming);
469
+ });
528
470
  /**
529
- * Step 1 of the desktop / native app sign-in handshake. The caller already
530
- * completed normal sign-in on the web (so a cookie session exists); this
531
- * issues a 5-minute single-use code that the web page hands off to the
532
- * desktop client via a loopback redirect.
471
+ * Step 1 of the desktop / native app sign-in handshake. The caller already completed normal
472
+ * sign-in on the web (so a cookie session exists); this issues a 5-minute single-use code that
473
+ * the web page hands off to the desktop client via a loopback redirect.
533
474
  *
534
- * PKCE (RFC 7636, S256 only): the desktop generates a random `code_verifier`
535
- * before opening the browser, derives `code_challenge = base64url(sha256(verifier))`,
536
- * and threads the challenge through the URL → web page → here. We pin
537
- * the challenge to the code in KV; `desktopExchange` enforces that the
538
- * caller knows the matching verifier. This blocks an attacker who only
539
- * intercepts the auth code (browser history, malicious extension, packet
540
- * capture on loopback) from exchanging it.
475
+ * PKCE (RFC 7636, S256 only): the desktop generates a random `code_verifier` before opening the
476
+ * browser, derives `code_challenge = base64url(sha256(verifier))`, and threads the challenge
477
+ * through the URL → web page → here. The challenge is pinned to the code; `desktopExchange`
478
+ * enforces that the caller knows the matching verifier. This blocks an attacker who only
479
+ * intercepts the auth code (browser history, malicious extension, packet capture on loopback)
480
+ * from exchanging it.
541
481
  *
542
- * Requires an authenticated cookie session — anonymous callers get 401.
543
- * Throttling / abuse protection on this endpoint is the API layer's job
544
- * (rate-limit per principal); the SDK doesn't gate it because the
545
- * upstream sign-in flow already passed CAPTCHA / OAuth / verification.
482
+ * Requires an authenticated cookie session: anonymous callers get 401. Throttling / abuse
483
+ * protection on this endpoint is the API layer's job (rate-limit per principal); the SDK does
484
+ * not gate it because the upstream sign-in flow already passed CAPTCHA / OAuth / verification.
546
485
  */
547
- desktopAuthorize = async (request) => {
486
+ desktopAuthorize = (request) => this.guard(async () => {
548
487
  const principal = await this.getPrincipal(request);
549
- if (!principal) return Response.json({ error: {
550
- code: 401,
551
- status: "UNAUTHENTICATED",
552
- message: "sign in first",
553
- details: []
554
- } }, { status: 401 });
555
- let body;
556
- try {
557
- body = await request.json();
558
- } catch {
559
- return Response.json(require_utils_valid.invalidArgument("INVALID_ARGUMENT"), { status: 400 });
560
- }
561
- const codeChallenge = typeof body.code_challenge === "string" ? body.code_challenge : null;
562
- if (!codeChallenge || body.code_challenge_method !== "S256" || !PKCE_FORMAT.test(codeChallenge)) return Response.json(require_utils_valid.invalidArgument("INVALID_ARGUMENT"), { status: 400 });
563
- const code = (0, node_crypto.randomUUID)();
564
- const record = {
565
- name: principal.name,
566
- cc: codeChallenge
567
- };
568
- await this.kv.setItem(this.getDesktopCodeKey(code), JSON.stringify(record), 300);
488
+ if (!principal) throw require_auth_respond.unauthenticated("Sign in first");
489
+ const { code_challenge } = require_utils_valid.parse(desktopAuthorizeSchema, await require_utils_valid.readJson(request));
490
+ const code = await this.desktopCodes.issue(principal, code_challenge);
569
491
  return Response.json({ code });
570
- };
492
+ });
571
493
  /**
572
- * Step 2 of the desktop / native app sign-in handshake. The desktop
573
- * client hands back the auth code it received via the loopback callback,
574
- * and we mint a fresh session for it — independent of the web cookie
575
- * session that originally `authorize`'d the code, so logging out on
576
- * either side doesn't cascade to the other.
494
+ * Step 2 of the desktop / native app sign-in handshake. The desktop client hands back the auth
495
+ * code it received via the loopback callback, and we mint a fresh session for it, independent
496
+ * of the web cookie session that originally authorized the code, so logging out on either side
497
+ * does not cascade to the other.
577
498
  *
578
- * Cookie attributes: defaults to `SameSite=None; Secure` so the cookie
579
- * survives cross-site fetches from the desktop renderer's `file://` /
580
- * `app://` origin. The configured base `cookieOptions` provide
581
- * everything else (`domain`, `path`, `httpOnly`, `maxAge`); the override
582
- * is targeted at the SameSite/Secure pair only. In a non-HTTPS dev
583
- * backend this combination is rejected by Chromium — see the desktop
584
- * sign-in PRD for workarounds.
499
+ * Cookie attributes: `SameSite=None; Secure` so the cookie survives cross-site fetches from the
500
+ * desktop renderer's `file://` / `app://` origin. The configured `cookieOptions` provide
501
+ * everything else (`domain`, `path`, `httpOnly`); the override is targeted at the
502
+ * SameSite/Secure pair only. In a non-HTTPS dev backend this combination is rejected by
503
+ * Chromium, see the desktop sign-in PRD for workarounds.
585
504
  *
586
- * No CSRF check needed — the request carries no cookie, the auth code
587
- * itself is the credential, and KV's single-use removal blocks replay.
505
+ * No CSRF check needed: the request carries no cookie, the auth code itself is the credential,
506
+ * and the code's single-use removal blocks replay.
588
507
  */
589
- desktopExchange = async (request) => {
590
- let body;
591
- try {
592
- body = await request.json();
593
- } catch {
594
- return Response.json(require_utils_valid.invalidArgument("INVALID_ARGUMENT"), { status: 400 });
595
- }
596
- const code = typeof body.code === "string" ? body.code : null;
597
- const verifier = typeof body.code_verifier === "string" ? body.code_verifier : null;
598
- if (!code || !verifier || !PKCE_FORMAT.test(verifier)) return Response.json(require_utils_valid.invalidArgument("INVALID_ARGUMENT"), { status: 400 });
599
- const key = this.getDesktopCodeKey(code);
600
- const raw = await this.kv.getItem(key);
601
- if (typeof raw !== "string") return Response.json(require_utils_valid.invalidArgument("INVALID_DESKTOP_CODE"), { status: 400 });
602
- let record;
603
- try {
604
- record = JSON.parse(raw);
605
- } catch {
606
- await this.kv.removeItem(key);
607
- return Response.json(require_utils_valid.invalidArgument("INVALID_DESKTOP_CODE"), { status: 400 });
608
- }
609
- const computed = (0, node_crypto.createHash)("sha256").update(verifier).digest();
610
- const stored = Buffer.from(record.cc, "base64url");
611
- if (stored.length !== computed.length || !(0, node_crypto.timingSafeEqual)(computed, stored)) return Response.json(require_utils_valid.invalidArgument("INVALID_DESKTOP_CODE"), { status: 400 });
612
- await this.kv.removeItem(key);
613
- const session = this.repository.createSession();
614
- session.setAttribute(require_session_common.PRINCIPAL_NAME_INDEX_NAME, record.name);
508
+ desktopExchange = (request) => this.guard(async () => {
509
+ const { code, code_verifier } = require_utils_valid.parse(desktopExchangeSchema, await require_utils_valid.readJson(request));
510
+ const principal = await this.desktopCodes.redeem(code, code_verifier);
511
+ const session = await this.signIn(principal);
615
512
  session.setAttribute("client_type", "desktop");
616
513
  await this.repository.save(session);
617
- const response = Response.json({ ok: true });
618
- const maxAge = session.getMaxInactiveInterval();
619
- require_utils_http.setCookie(response, this.cookieName, session.getId(), {
620
- ...this.cookieOptions,
514
+ return this.withSession(Response.json({ ok: true }), session, {
621
515
  sameSite: "none",
622
- secure: true,
623
- maxAge
516
+ secure: true
624
517
  });
625
- return response;
626
- };
627
- sendPhoneVerificationCode = async (_request, _sender) => {
628
- return Response.json({ error: "not_implemented" }, { status: 501 });
629
- };
630
- loginPhone = async (_request, _handler) => {
631
- return Response.json({ error: "not_implemented" }, { status: 501 });
632
- };
518
+ });
633
519
  kick = async (principal) => {
634
520
  const sessions = await this.repository.findByPrincipalName(principal.name);
635
521
  for (const sessionId of sessions.keys()) await this.repository.deleteById(sessionId);
@@ -647,47 +533,26 @@ var Auth = class {
647
533
  session.setLastAccessedTime(now);
648
534
  await this.repository.save(session);
649
535
  }
650
- isAuthenticated = async (request) => {
651
- const sessionId = this.getSessionId(request);
652
- if (!sessionId) return false;
653
- const session = await this.repository.findById(sessionId);
654
- if (!session) return false;
655
- if (!session.getAttribute("org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME")) return false;
536
+ getPrincipal = async (request) => {
537
+ const session = await this.loadSession(request);
538
+ const principal = session && this.principalOf(session);
539
+ if (!principal) return null;
656
540
  await this.touch(session);
657
- return true;
541
+ return principal;
542
+ };
543
+ isAuthenticated = async (request) => {
544
+ return await this.getPrincipal(request) !== null;
658
545
  };
659
546
  getSession = async (request, create) => {
660
- const sessionId = this.getSessionId(request);
661
- if (!sessionId) {
662
- if (create) {
663
- const session = this.repository.createSession();
664
- await this.repository.save(session);
665
- return session;
666
- } else return null;
667
- }
668
- const session = await this.repository.findById(sessionId);
669
- if (!session) {
670
- if (create) {
671
- const session = this.repository.createSession();
672
- await this.repository.save(session);
673
- return session;
674
- } else return null;
675
- }
676
- return session;
547
+ const session = await this.loadSession(request);
548
+ if (session || !create) return session;
549
+ const created = this.repository.createSession();
550
+ await this.repository.save(created);
551
+ return created;
677
552
  };
678
553
  deleteSession = async (sessionId) => {
679
554
  await this.repository.deleteById(sessionId);
680
555
  };
681
- getPrincipal = async (request) => {
682
- const sessionId = this.getSessionId(request);
683
- if (!sessionId) return null;
684
- const session = await this.repository.findById(sessionId);
685
- if (!session) return null;
686
- const name = session.getAttribute(require_session_common.PRINCIPAL_NAME_INDEX_NAME);
687
- if (!name) return null;
688
- await this.touch(session);
689
- return { name: String(name) };
690
- };
691
556
  listSessions = async (principal) => {
692
557
  const sessions = await this.repository.findByPrincipalName(principal.name);
693
558
  return Array.from(sessions.values());