@effect-auth/core 0.1.0-alpha.4 → 0.1.0-alpha.6

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 (47) hide show
  1. package/dist/AuditLog.d.ts +49 -1
  2. package/dist/AuditLog.d.ts.map +1 -1
  3. package/dist/AuditLog.js +28 -0
  4. package/dist/AuditLog.js.map +1 -1
  5. package/dist/Challenge.d.ts +1 -1
  6. package/dist/Challenge.d.ts.map +1 -1
  7. package/dist/Challenge.js.map +1 -1
  8. package/dist/Client.d.ts +36 -1
  9. package/dist/Client.d.ts.map +1 -1
  10. package/dist/Client.js +59 -1
  11. package/dist/Client.js.map +1 -1
  12. package/dist/D1Kysely.d.ts +1 -1
  13. package/dist/D1Kysely.d.ts.map +1 -1
  14. package/dist/HttpApi/Api.d.ts +352 -3
  15. package/dist/HttpApi/Api.d.ts.map +1 -1
  16. package/dist/HttpApi/Api.js +317 -1
  17. package/dist/HttpApi/Api.js.map +1 -1
  18. package/dist/HttpApi/Endpoints.d.ts +132 -0
  19. package/dist/HttpApi/Endpoints.d.ts.map +1 -1
  20. package/dist/HttpApi/Endpoints.js +34 -1
  21. package/dist/HttpApi/Endpoints.js.map +1 -1
  22. package/dist/HttpApi/Schemas.d.ts +214 -0
  23. package/dist/HttpApi/Schemas.d.ts.map +1 -1
  24. package/dist/HttpApi/Schemas.js +125 -1
  25. package/dist/HttpApi/Schemas.js.map +1 -1
  26. package/dist/KyselyStorage.d.ts +18 -1
  27. package/dist/KyselyStorage.d.ts.map +1 -1
  28. package/dist/KyselyStorage.js +105 -2
  29. package/dist/KyselyStorage.js.map +1 -1
  30. package/dist/OAuth.d.ts +742 -0
  31. package/dist/OAuth.d.ts.map +1 -0
  32. package/dist/OAuth.js +1387 -0
  33. package/dist/OAuth.js.map +1 -0
  34. package/dist/StorageMigrations.d.ts +2 -1
  35. package/dist/StorageMigrations.d.ts.map +1 -1
  36. package/dist/StorageMigrations.js +20 -0
  37. package/dist/StorageMigrations.js.map +1 -1
  38. package/dist/Webhook.d.ts +72 -0
  39. package/dist/Webhook.d.ts.map +1 -0
  40. package/dist/Webhook.js +82 -0
  41. package/dist/Webhook.js.map +1 -0
  42. package/dist/index.d.ts +2 -0
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +2 -0
  45. package/dist/index.js.map +1 -1
  46. package/migrations/0011_auth_oauth_account.sql +16 -0
  47. package/package.json +9 -1
package/dist/OAuth.js ADDED
@@ -0,0 +1,1387 @@
1
+ import { Brand, Context, Data, Duration, Effect, Encoding, Layer, Option, Redacted, } from "effect";
2
+ import { Challenge } from "./Challenge.js";
3
+ import { Crypto } from "./Crypto.js";
4
+ import { ChallengeId, Email, OAuthAccountId, OAuthProviderId, UnixMillis, UserId, } from "./Identifiers.js";
5
+ import { currentUnixMillis } from "./Internal.js";
6
+ import { JwtAlgorithms, JwtError, JwtKeyId, JwtKeys, JwtVerifier, } from "./Jwt.js";
7
+ export const OAuthStateToken = Brand.nominal();
8
+ export const OAuthCodeVerifier = Brand.nominal();
9
+ export const OAuthCodeChallenge = Brand.nominal();
10
+ export const OAuthNonce = Brand.nominal();
11
+ export const OAuthAuthorizationCode = Brand.nominal();
12
+ export const OAuthAccessToken = Brand.nominal();
13
+ export const OAuthRefreshToken = Brand.nominal();
14
+ export const OAuthIdToken = Brand.nominal();
15
+ export const defaultOAuthStateTtl = Duration.minutes(10);
16
+ export const defaultOAuthLinkConfirmationTtl = Duration.minutes(15);
17
+ export const defaultOAuthJwksCacheTtl = Duration.minutes(10);
18
+ export const defaultOAuthStateSecretBytes = 32;
19
+ export const defaultOAuthCodeVerifierBytes = 32;
20
+ export const defaultOAuthLinkConfirmationSecretBytes = 32;
21
+ export const defaultOAuthNonceBytes = 16;
22
+ export const appleSignInProviderId = OAuthProviderId("apple");
23
+ export const appleSignInIssuer = "https://appleid.apple.com";
24
+ export const appleSignInAuthorizationEndpoint = "https://appleid.apple.com/auth/authorize";
25
+ export const appleSignInTokenEndpoint = "https://appleid.apple.com/auth/token";
26
+ export const appleSignInRevocationEndpoint = "https://appleid.apple.com/auth/revoke";
27
+ export const appleSignInJwksUri = "https://appleid.apple.com/auth/keys";
28
+ export const appleSignInPrivateRelayDomain = "privaterelay.appleid.com";
29
+ export const appleSignInDefaultScopes = ["openid", "email", "name"];
30
+ export const appleSignInDefaultAuthorizationParams = {
31
+ response_mode: "form_post",
32
+ };
33
+ export const githubOAuthProviderId = OAuthProviderId("github");
34
+ export const githubOAuthAuthorizationEndpoint = "https://github.com/login/oauth/authorize";
35
+ export const githubOAuthTokenEndpoint = "https://github.com/login/oauth/access_token";
36
+ export const githubOAuthUserEndpoint = "https://api.github.com/user";
37
+ export const githubOAuthEmailsEndpoint = "https://api.github.com/user/emails";
38
+ export const githubOAuthDefaultScopes = ["read:user", "user:email"];
39
+ export const githubOAuthApiVersion = "2022-11-28";
40
+ export const githubOAuthApiVersionHeader = {
41
+ "x-github-api-version": githubOAuthApiVersion,
42
+ };
43
+ export const googleOidcProviderId = OAuthProviderId("google");
44
+ export const googleOidcIssuer = "https://accounts.google.com";
45
+ export const googleOidcAuthorizationEndpoint = "https://accounts.google.com/o/oauth2/v2/auth";
46
+ export const googleOidcTokenEndpoint = "https://oauth2.googleapis.com/token";
47
+ export const googleOidcRevocationEndpoint = "https://oauth2.googleapis.com/revoke";
48
+ export const googleOidcUserInfoEndpoint = "https://openidconnect.googleapis.com/v1/userinfo";
49
+ export const googleOidcJwksUri = "https://www.googleapis.com/oauth2/v3/certs";
50
+ export const googleOidcDefaultScopes = ["openid", "email", "profile"];
51
+ export const microsoftOidcProviderId = OAuthProviderId("microsoft");
52
+ export const microsoftOidcLoginBaseUrl = "https://login.microsoftonline.com";
53
+ export const microsoftOidcUserInfoEndpoint = "https://graph.microsoft.com/oidc/userinfo";
54
+ export const microsoftOidcDefaultScopes = ["openid", "email", "profile"];
55
+ export const genericOidcDefaultScopes = ["openid", "email", "profile"];
56
+ const microsoftOidcTenantPathSegment = (tenant) => encodeURIComponent(tenant.trim());
57
+ export const microsoftOidcAuthorizationEndpoint = (tenant) => `${microsoftOidcLoginBaseUrl}/${microsoftOidcTenantPathSegment(tenant)}/oauth2/v2.0/authorize`;
58
+ export const microsoftOidcTokenEndpoint = (tenant) => `${microsoftOidcLoginBaseUrl}/${microsoftOidcTenantPathSegment(tenant)}/oauth2/v2.0/token`;
59
+ export const microsoftOidcJwksUri = (tenant) => `${microsoftOidcLoginBaseUrl}/${microsoftOidcTenantPathSegment(tenant)}/discovery/v2.0/keys`;
60
+ export const microsoftOidcIssuer = (tenant) => `${microsoftOidcLoginBaseUrl}/${microsoftOidcTenantPathSegment(tenant)}/v2.0`;
61
+ export class OAuthProviderError extends Data.TaggedError("OAuthProviderError") {
62
+ }
63
+ export class OAuthUserInfoError extends Data.TaggedError("OAuthUserInfoError") {
64
+ }
65
+ export class OidcIdTokenVerifierError extends Data.TaggedError("OidcIdTokenVerifierError") {
66
+ }
67
+ export class OidcIdTokenVerifier extends Context.Service()("auth/OidcIdTokenVerifier") {
68
+ }
69
+ (function (OidcIdTokenVerifier) {
70
+ OidcIdTokenVerifier.make = (service) => OidcIdTokenVerifier.of(service);
71
+ })(OidcIdTokenVerifier || (OidcIdTokenVerifier = {}));
72
+ export class OAuthProfileNormalizationError extends Data.TaggedError("OAuthProfileNormalizationError") {
73
+ }
74
+ export class OAuthProfileNormalizer extends Context.Service()("auth/OAuthProfileNormalizer") {
75
+ }
76
+ (function (OAuthProfileNormalizer) {
77
+ OAuthProfileNormalizer.make = (service) => OAuthProfileNormalizer.of(service);
78
+ })(OAuthProfileNormalizer || (OAuthProfileNormalizer = {}));
79
+ export class OAuthTokenExchangeError extends Data.TaggedError("OAuthTokenExchangeError") {
80
+ }
81
+ export class OAuthTokenExchange extends Context.Service()("auth/OAuthTokenExchange") {
82
+ }
83
+ (function (OAuthTokenExchange) {
84
+ OAuthTokenExchange.make = (service) => OAuthTokenExchange.of(service);
85
+ })(OAuthTokenExchange || (OAuthTokenExchange = {}));
86
+ export class OAuthProviders extends Context.Service()("auth/OAuthProviders") {
87
+ }
88
+ (function (OAuthProviders) {
89
+ OAuthProviders.make = (service) => OAuthProviders.of(service);
90
+ })(OAuthProviders || (OAuthProviders = {}));
91
+ export class OAuthStateError extends Data.TaggedError("OAuthStateError") {
92
+ }
93
+ export class OAuthState extends Context.Service()("auth/OAuthState") {
94
+ }
95
+ (function (OAuthState) {
96
+ OAuthState.make = (service) => OAuthState.of(service);
97
+ })(OAuthState || (OAuthState = {}));
98
+ export class OAuthAccountStoreError extends Data.TaggedError("OAuthAccountStoreError") {
99
+ }
100
+ export class OAuthAccountStore extends Context.Service()("auth/OAuthAccountStore") {
101
+ }
102
+ (function (OAuthAccountStore) {
103
+ OAuthAccountStore.make = (service) => OAuthAccountStore.of(service);
104
+ })(OAuthAccountStore || (OAuthAccountStore = {}));
105
+ export class OAuthAccountLinkingError extends Data.TaggedError("OAuthAccountLinkingError") {
106
+ }
107
+ export class OAuthAccountLinking extends Context.Service()("auth/OAuthAccountLinking") {
108
+ }
109
+ (function (OAuthAccountLinking) {
110
+ OAuthAccountLinking.make = (service) => OAuthAccountLinking.of(service);
111
+ })(OAuthAccountLinking || (OAuthAccountLinking = {}));
112
+ export class OAuthAccountUnlinkingError extends Data.TaggedError("OAuthAccountUnlinkingError") {
113
+ }
114
+ export class OAuthAccountUnlinking extends Context.Service()("auth/OAuthAccountUnlinking") {
115
+ }
116
+ (function (OAuthAccountUnlinking) {
117
+ OAuthAccountUnlinking.make = (service) => OAuthAccountUnlinking.of(service);
118
+ })(OAuthAccountUnlinking || (OAuthAccountUnlinking = {}));
119
+ export class OAuthLinkConfirmationError extends Data.TaggedError("OAuthLinkConfirmationError") {
120
+ }
121
+ export class OAuthLinkConfirmation extends Context.Service()("auth/OAuthLinkConfirmation") {
122
+ }
123
+ (function (OAuthLinkConfirmation) {
124
+ OAuthLinkConfirmation.make = (service) => OAuthLinkConfirmation.of(service);
125
+ })(OAuthLinkConfirmation || (OAuthLinkConfirmation = {}));
126
+ const oauthStateError = (operation, message, cause) => new OAuthStateError({ operation, message, cause });
127
+ const oauthTokenExchangeError = (operation, message, cause) => new OAuthTokenExchangeError({ operation, message, cause });
128
+ const oidcIdTokenVerifierError = (operation, message, cause) => new OidcIdTokenVerifierError({ operation, message, cause });
129
+ const oauthAccountLinkingError = (operation, message, cause) => new OAuthAccountLinkingError({ operation, message, cause });
130
+ const oauthAccountUnlinkingError = (operation, message, cause) => new OAuthAccountUnlinkingError({ operation, message, cause });
131
+ const oauthLinkConfirmationError = (operation, message, cause) => new OAuthLinkConfirmationError({ operation, message, cause });
132
+ const oauthProfileNormalizationError = (reason, message, cause) => new OAuthProfileNormalizationError({
133
+ operation: "normalize",
134
+ reason,
135
+ message,
136
+ cause,
137
+ });
138
+ const oauthJwksJwtError = (message, cause) => new JwtError({
139
+ operation: "keys",
140
+ message,
141
+ ...(cause === undefined ? {} : { cause }),
142
+ });
143
+ const oauthUserInfoError = (operation, message, cause) => new OAuthUserInfoError({
144
+ operation,
145
+ message,
146
+ ...(cause === undefined ? {} : { cause }),
147
+ });
148
+ const textEncoder = new TextEncoder();
149
+ const oauthJwksPrivateMembers = new Set([
150
+ "d",
151
+ "p",
152
+ "q",
153
+ "dp",
154
+ "dq",
155
+ "qi",
156
+ "oth",
157
+ "k",
158
+ ]);
159
+ const validateByteLength = (value, operation, label) => Number.isInteger(value) && value >= 16 && value <= 128
160
+ ? Effect.succeed(value)
161
+ : Effect.fail(oauthStateError(operation, `${label} byte length must be an integer between 16 and 128`));
162
+ const validateOAuthLinkConfirmationSecretBytes = (value) => Number.isInteger(value) && value >= 16 && value <= 128
163
+ ? Effect.succeed(value)
164
+ : Effect.fail(oauthLinkConfirmationError("start", "OAuth link confirmation secret byte length must be an integer between 16 and 128"));
165
+ const metadataValue = (value) => value === undefined ? undefined : structuredClone(value);
166
+ export const makeAppleSignInProvider = (input) => ({
167
+ id: input.id ?? appleSignInProviderId,
168
+ clientId: input.clientId,
169
+ authorizationEndpoint: appleSignInAuthorizationEndpoint,
170
+ tokenEndpoint: appleSignInTokenEndpoint,
171
+ revocationEndpoint: appleSignInRevocationEndpoint,
172
+ jwksUri: appleSignInJwksUri,
173
+ issuer: appleSignInIssuer,
174
+ redirectUri: input.redirectUri,
175
+ scopes: [...(input.scopes ?? appleSignInDefaultScopes)],
176
+ authorizationParams: {
177
+ ...appleSignInDefaultAuthorizationParams,
178
+ ...input.authorizationParams,
179
+ },
180
+ ...(input.metadata === undefined ? {} : { metadata: metadataValue(input.metadata) }),
181
+ });
182
+ export const makeGithubOAuthProvider = (input) => ({
183
+ id: input.id ?? githubOAuthProviderId,
184
+ clientId: input.clientId,
185
+ authorizationEndpoint: githubOAuthAuthorizationEndpoint,
186
+ tokenEndpoint: githubOAuthTokenEndpoint,
187
+ userInfoEndpoint: githubOAuthUserEndpoint,
188
+ redirectUri: input.redirectUri,
189
+ scopes: [...(input.scopes ?? githubOAuthDefaultScopes)],
190
+ ...(input.authorizationParams === undefined
191
+ ? {}
192
+ : { authorizationParams: { ...input.authorizationParams } }),
193
+ ...(input.metadata === undefined ? {} : { metadata: metadataValue(input.metadata) }),
194
+ });
195
+ export const makeGoogleOidcProvider = (input) => ({
196
+ id: input.id ?? googleOidcProviderId,
197
+ clientId: input.clientId,
198
+ authorizationEndpoint: googleOidcAuthorizationEndpoint,
199
+ tokenEndpoint: googleOidcTokenEndpoint,
200
+ revocationEndpoint: googleOidcRevocationEndpoint,
201
+ userInfoEndpoint: googleOidcUserInfoEndpoint,
202
+ jwksUri: googleOidcJwksUri,
203
+ issuer: googleOidcIssuer,
204
+ redirectUri: input.redirectUri,
205
+ scopes: [...(input.scopes ?? googleOidcDefaultScopes)],
206
+ ...(input.authorizationParams === undefined
207
+ ? {}
208
+ : { authorizationParams: { ...input.authorizationParams } }),
209
+ ...(input.metadata === undefined ? {} : { metadata: metadataValue(input.metadata) }),
210
+ });
211
+ export const makeMicrosoftOidcProvider = (input) => ({
212
+ id: input.id ?? microsoftOidcProviderId,
213
+ clientId: input.clientId,
214
+ authorizationEndpoint: microsoftOidcAuthorizationEndpoint(input.tenant),
215
+ tokenEndpoint: microsoftOidcTokenEndpoint(input.tenant),
216
+ userInfoEndpoint: microsoftOidcUserInfoEndpoint,
217
+ jwksUri: microsoftOidcJwksUri(input.tenant),
218
+ issuer: microsoftOidcIssuer(input.tenant),
219
+ redirectUri: input.redirectUri,
220
+ scopes: [...(input.scopes ?? microsoftOidcDefaultScopes)],
221
+ ...(input.authorizationParams === undefined
222
+ ? {}
223
+ : { authorizationParams: { ...input.authorizationParams } }),
224
+ ...(input.metadata === undefined ? {} : { metadata: metadataValue(input.metadata) }),
225
+ });
226
+ export const makeGenericOidcProvider = (input) => ({
227
+ id: input.id,
228
+ clientId: input.clientId,
229
+ authorizationEndpoint: input.authorizationEndpoint,
230
+ tokenEndpoint: input.tokenEndpoint,
231
+ issuer: input.issuer,
232
+ redirectUri: input.redirectUri,
233
+ scopes: [...(input.scopes ?? genericOidcDefaultScopes)],
234
+ ...(input.userInfoEndpoint === undefined
235
+ ? {}
236
+ : { userInfoEndpoint: input.userInfoEndpoint }),
237
+ ...(input.jwksUri === undefined ? {} : { jwksUri: input.jwksUri }),
238
+ ...(input.revocationEndpoint === undefined
239
+ ? {}
240
+ : { revocationEndpoint: input.revocationEndpoint }),
241
+ ...(input.authorizationParams === undefined
242
+ ? {}
243
+ : { authorizationParams: { ...input.authorizationParams } }),
244
+ ...(input.metadata === undefined ? {} : { metadata: metadataValue(input.metadata) }),
245
+ });
246
+ const cloneProviderConfig = (provider) => ({
247
+ ...provider,
248
+ ...(provider.scopes === undefined ? {} : { scopes: [...provider.scopes] }),
249
+ ...(provider.authorizationParams === undefined
250
+ ? {}
251
+ : { authorizationParams: { ...provider.authorizationParams } }),
252
+ ...(provider.metadata === undefined ? {} : { metadata: metadataValue(provider.metadata) }),
253
+ });
254
+ const encodeOAuthState = (input) => OAuthStateToken(`${input.challengeId}.${Redacted.value(input.secret)}`);
255
+ export const parseOAuthState = (state) => {
256
+ const parts = String(state).split(".");
257
+ return parts.length === 2 && parts[0] !== "" && parts[1] !== ""
258
+ ? Effect.succeed({
259
+ challengeId: ChallengeId(parts[0]),
260
+ secret: Redacted.make(parts[1]),
261
+ })
262
+ : Effect.fail(oauthStateError("parse", "OAuth state must use '<id>.<secret>' format"));
263
+ };
264
+ const stringArrayFromUnknown = (value) => Array.isArray(value) && value.every((item) => typeof item === "string")
265
+ ? value
266
+ : [];
267
+ const stringFromUnknown = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
268
+ const nonEmptyStringFromUnknown = (value) => typeof value === "string" && value.trim() !== "" ? value : undefined;
269
+ export const githubOAuthVerifiedEmail = (emails) => {
270
+ for (const email of emails) {
271
+ if (email.primary === true && email.verified === true) {
272
+ const value = nonEmptyStringFromUnknown(email.email);
273
+ if (value !== undefined) {
274
+ return value;
275
+ }
276
+ }
277
+ }
278
+ for (const email of emails) {
279
+ if (email.verified === true) {
280
+ const value = nonEmptyStringFromUnknown(email.email);
281
+ if (value !== undefined) {
282
+ return value;
283
+ }
284
+ }
285
+ }
286
+ return undefined;
287
+ };
288
+ export const googleOidcHostedDomain = (value) => nonEmptyStringFromUnknown(value.hd);
289
+ export const microsoftOidcTenantId = (value) => nonEmptyStringFromUnknown(value.tid);
290
+ export const microsoftOidcObjectId = (value) => nonEmptyStringFromUnknown(value.oid);
291
+ export const microsoftOidcAccountId = (value) => {
292
+ const tenantId = microsoftOidcTenantId(value);
293
+ const objectId = microsoftOidcObjectId(value);
294
+ return tenantId === undefined || objectId === undefined
295
+ ? undefined
296
+ : `${tenantId}:${objectId}`;
297
+ };
298
+ const optionalRecord = (value) => (Object.keys(value).length === 0 ? undefined : value);
299
+ const recordFromUnknown = (value) => typeof value === "object" && value !== null && !Array.isArray(value)
300
+ ? value
301
+ : undefined;
302
+ const isJwtAlgorithm = (value) => typeof value === "string" && JwtAlgorithms.includes(value);
303
+ const isPublicJwksAlgorithm = (value) => isJwtAlgorithm(value) && value !== "HS256";
304
+ const booleanFromUnknown = (value) => typeof value === "boolean" ? value : undefined;
305
+ const emailFromUnknown = (value) => {
306
+ const email = nonEmptyStringFromUnknown(value);
307
+ return email === undefined ? undefined : Email(email);
308
+ };
309
+ const appleSignInBooleanClaim = (value) => {
310
+ if (typeof value === "boolean") {
311
+ return value;
312
+ }
313
+ if (value === "true") {
314
+ return true;
315
+ }
316
+ if (value === "false") {
317
+ return false;
318
+ }
319
+ return undefined;
320
+ };
321
+ export const decodeAppleSignInAuthorizationUser = (value) => {
322
+ const decoded = typeof value === "string"
323
+ ? (() => {
324
+ try {
325
+ return JSON.parse(value);
326
+ }
327
+ catch {
328
+ return undefined;
329
+ }
330
+ })()
331
+ : value;
332
+ const record = recordFromUnknown(decoded);
333
+ if (record === undefined) {
334
+ return undefined;
335
+ }
336
+ const nameRecord = recordFromUnknown(record.name);
337
+ const firstName = nonEmptyStringFromUnknown(nameRecord?.firstName);
338
+ const lastName = nonEmptyStringFromUnknown(nameRecord?.lastName);
339
+ const name = firstName === undefined && lastName === undefined
340
+ ? undefined
341
+ : {
342
+ ...(firstName === undefined ? {} : { firstName }),
343
+ ...(lastName === undefined ? {} : { lastName }),
344
+ };
345
+ const email = nonEmptyStringFromUnknown(record.email);
346
+ return email === undefined && name === undefined
347
+ ? undefined
348
+ : {
349
+ ...(email === undefined ? {} : { email }),
350
+ ...(name === undefined ? {} : { name }),
351
+ };
352
+ };
353
+ export const appleSignInAuthorizationUserFullName = (user) => {
354
+ const firstName = nonEmptyStringFromUnknown(user?.name?.firstName);
355
+ const lastName = nonEmptyStringFromUnknown(user?.name?.lastName);
356
+ const parts = [firstName, lastName].filter((part) => part !== undefined);
357
+ return parts.length === 0 ? undefined : parts.join(" ");
358
+ };
359
+ export const appleSignInAuthorizationUserEmail = (user) => emailFromUnknown(user?.email);
360
+ export const appleSignInIsPrivateEmail = (input) => {
361
+ const claimValue = appleSignInBooleanClaim(input.claims?.is_private_email);
362
+ if (claimValue !== undefined) {
363
+ return claimValue;
364
+ }
365
+ const email = nonEmptyStringFromUnknown(input.email);
366
+ return email === undefined
367
+ ? undefined
368
+ : email.toLowerCase().endsWith(`@${appleSignInPrivateRelayDomain}`);
369
+ };
370
+ const firstString = (...values) => {
371
+ for (const value of values) {
372
+ const stringValue = nonEmptyStringFromUnknown(value);
373
+ if (stringValue !== undefined) {
374
+ return stringValue;
375
+ }
376
+ }
377
+ return undefined;
378
+ };
379
+ const optionalStringArrayFromUnknown = (value) => Array.isArray(value) && value.every((item) => typeof item === "string")
380
+ ? value
381
+ : undefined;
382
+ const numberFromUnknown = (value) => {
383
+ if (typeof value === "number" && Number.isFinite(value)) {
384
+ return value;
385
+ }
386
+ if (typeof value === "string" && value.trim() !== "") {
387
+ const parsed = Number(value);
388
+ return Number.isFinite(parsed) ? parsed : undefined;
389
+ }
390
+ return undefined;
391
+ };
392
+ const isTokenResponseRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
393
+ const scopeValues = (value) => {
394
+ if (typeof value !== "string" || value.trim() === "") {
395
+ return undefined;
396
+ }
397
+ return value.trim().split(/\s+/);
398
+ };
399
+ const audienceValues = (value) => {
400
+ if (typeof value === "string") {
401
+ return [value];
402
+ }
403
+ if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
404
+ return value;
405
+ }
406
+ return [];
407
+ };
408
+ const oidcIdTokenClaims = (claims, input) => {
409
+ if (nonEmptyStringFromUnknown(claims.sub) === undefined) {
410
+ return "missing_subject";
411
+ }
412
+ if (typeof claims.exp !== "number" || !Number.isFinite(claims.exp)) {
413
+ return "missing_expiration";
414
+ }
415
+ if (typeof claims.iat !== "number" || !Number.isFinite(claims.iat)) {
416
+ return "missing_issued_at";
417
+ }
418
+ const audiences = audienceValues(claims.aud);
419
+ const authorizedParty = input.authorizedParty ?? input.provider.clientId;
420
+ if (audiences.length > 1 && claims.azp === undefined) {
421
+ return "missing_authorized_party";
422
+ }
423
+ if (claims.azp !== undefined && claims.azp !== authorizedParty) {
424
+ return "authorized_party_mismatch";
425
+ }
426
+ if (input.nonce !== undefined && claims.nonce !== String(input.nonce)) {
427
+ return "nonce_mismatch";
428
+ }
429
+ return claims;
430
+ };
431
+ const tokenResponseExtra = (value) => {
432
+ const extra = Object.fromEntries(Object.entries(value).filter(([key]) => ![
433
+ "access_token",
434
+ "token_type",
435
+ "expires_in",
436
+ "refresh_token",
437
+ "id_token",
438
+ "scope",
439
+ ].includes(key)));
440
+ return Object.keys(extra).length === 0 ? undefined : extra;
441
+ };
442
+ const cloneVerifiedOAuthIdentity = (identity) => ({
443
+ ...identity,
444
+ ...(identity.metadata === undefined
445
+ ? {}
446
+ : { metadata: metadataValue(identity.metadata) }),
447
+ });
448
+ const oauthLinkConfirmationChallengeType = "oauth-link-confirmation";
449
+ const oauthLinkConfirmationMetadata = (input) => ({
450
+ userId: input.userId,
451
+ providerId: input.identity.providerId,
452
+ providerAccountId: input.identity.providerAccountId,
453
+ ...(input.identity.email === undefined ? {} : { email: input.identity.email }),
454
+ ...(input.identity.emailVerified === undefined
455
+ ? {}
456
+ : { emailVerified: input.identity.emailVerified }),
457
+ ...(input.identity.metadata === undefined
458
+ ? {}
459
+ : { identityMetadata: metadataValue(input.identity.metadata) }),
460
+ reason: input.reason,
461
+ ...(input.metadata === undefined ? {} : { metadata: metadataValue(input.metadata) }),
462
+ });
463
+ const pendingOAuthLinkConfirmationFromChallenge = (challenge, operation) => {
464
+ const metadata = challenge.metadata ?? {};
465
+ const userId = stringFromUnknown(metadata.userId);
466
+ const providerId = stringFromUnknown(metadata.providerId);
467
+ const providerAccountId = stringFromUnknown(metadata.providerAccountId);
468
+ if (userId === undefined ||
469
+ userId !== challenge.subject ||
470
+ providerId === undefined ||
471
+ providerAccountId === undefined) {
472
+ return Effect.fail(oauthLinkConfirmationError(operation, "OAuth link confirmation challenge is missing required metadata"));
473
+ }
474
+ const email = emailFromUnknown(metadata.email);
475
+ const emailVerified = booleanFromUnknown(metadata.emailVerified);
476
+ const identityMetadata = recordFromUnknown(metadata.identityMetadata);
477
+ const appMetadata = recordFromUnknown(metadata.metadata);
478
+ return Effect.succeed({
479
+ challengeId: challenge.id,
480
+ userId: UserId(userId),
481
+ identity: {
482
+ providerId: OAuthProviderId(providerId),
483
+ providerAccountId,
484
+ ...(email === undefined ? {} : { email }),
485
+ ...(emailVerified === undefined ? {} : { emailVerified }),
486
+ ...(identityMetadata === undefined
487
+ ? {}
488
+ : { metadata: metadataValue(identityMetadata) }),
489
+ },
490
+ reason: stringFromUnknown(metadata.reason) ??
491
+ "manual",
492
+ expiresAt: challenge.expiresAt,
493
+ ...(appMetadata === undefined ? {} : { metadata: metadataValue(appMetadata) }),
494
+ });
495
+ };
496
+ const confirmedOAuthLinkConfirmationFromPending = (input) => ({
497
+ challengeId: input.pending.challengeId,
498
+ userId: input.pending.userId,
499
+ identity: cloneVerifiedOAuthIdentity(input.pending.identity),
500
+ reason: input.pending.reason,
501
+ ...(input.existingAccount === undefined
502
+ ? {}
503
+ : { existingAccount: input.existingAccount }),
504
+ ...(input.pending.metadata === undefined
505
+ ? {}
506
+ : { metadata: metadataValue(input.pending.metadata) }),
507
+ });
508
+ const userInfoRecord = (value) => {
509
+ if (value === undefined) {
510
+ return Effect.succeed(undefined);
511
+ }
512
+ const record = recordFromUnknown(value);
513
+ return record === undefined
514
+ ? Effect.fail(oauthProfileNormalizationError("invalid_userinfo", "OIDC userinfo response must be an object"))
515
+ : Effect.succeed(record);
516
+ };
517
+ const makeOAuthProfile = (input) => ({
518
+ providerId: input.providerId,
519
+ providerAccountId: input.providerAccountId,
520
+ ...(input.email === undefined ? {} : { email: input.email }),
521
+ ...(input.emailVerified === undefined
522
+ ? {}
523
+ : { emailVerified: input.emailVerified }),
524
+ ...(input.name === undefined ? {} : { name: input.name }),
525
+ ...(input.username === undefined ? {} : { username: input.username }),
526
+ ...(input.avatarUrl === undefined ? {} : { avatarUrl: input.avatarUrl }),
527
+ ...(input.metadata === undefined ? {} : { metadata: metadataValue(input.metadata) }),
528
+ });
529
+ const oauthJwksPublicJwkFromUnknown = (value) => {
530
+ const record = recordFromUnknown(value);
531
+ const kty = record === undefined ? undefined : nonEmptyStringFromUnknown(record.kty);
532
+ if (record === undefined || kty === undefined) {
533
+ return undefined;
534
+ }
535
+ const jwk = {};
536
+ for (const [name, member] of Object.entries(record)) {
537
+ if (!oauthJwksPrivateMembers.has(name) && member !== undefined) {
538
+ jwk[name] = member;
539
+ }
540
+ }
541
+ return {
542
+ ...jwk,
543
+ kty,
544
+ };
545
+ };
546
+ const oauthJwksAllowsSignatureUse = (jwk) => {
547
+ const use = nonEmptyStringFromUnknown(jwk.use);
548
+ const keyOps = optionalStringArrayFromUnknown(jwk.key_ops);
549
+ return ((use === undefined || use === "sig") &&
550
+ (keyOps === undefined || keyOps.includes("verify")));
551
+ };
552
+ const oauthJwksDefaultAlgorithms = (jwk) => {
553
+ switch (jwk.kty) {
554
+ case "RSA": {
555
+ return ["RS256"];
556
+ }
557
+ case "EC": {
558
+ return ["ES256"];
559
+ }
560
+ case "OKP": {
561
+ return ["EdDSA"];
562
+ }
563
+ default: {
564
+ return [];
565
+ }
566
+ }
567
+ };
568
+ const oauthJwksAlgorithms = (jwk, allowedAlgorithms) => {
569
+ const jwkAlgorithm = nonEmptyStringFromUnknown(jwk.alg);
570
+ const algorithms = jwkAlgorithm === undefined
571
+ ? oauthJwksDefaultAlgorithms(jwk)
572
+ : isPublicJwksAlgorithm(jwkAlgorithm)
573
+ ? [jwkAlgorithm]
574
+ : [];
575
+ if (allowedAlgorithms === undefined) {
576
+ return algorithms;
577
+ }
578
+ return algorithms.filter((algorithm) => allowedAlgorithms.includes(algorithm));
579
+ };
580
+ const oauthJwksRecordsFromDocument = (provider, document, allowedAlgorithms) => document.keys.flatMap((jwk) => {
581
+ const kid = nonEmptyStringFromUnknown(jwk.kid);
582
+ if (kid === undefined || !oauthJwksAllowsSignatureUse(jwk)) {
583
+ return [];
584
+ }
585
+ return oauthJwksAlgorithms(jwk, allowedAlgorithms).map((alg) => ({
586
+ id: JwtKeyId(kid),
587
+ alg,
588
+ status: "active",
589
+ publicJwk: {
590
+ ...jwk,
591
+ kid,
592
+ alg,
593
+ use: nonEmptyStringFromUnknown(jwk.use) ?? "sig",
594
+ },
595
+ metadata: {
596
+ providerId: provider.id,
597
+ },
598
+ }));
599
+ });
600
+ const oauthJwksKeyUsableAt = (key, now) => now === undefined || key.expiresAt === undefined || Number(now) < Number(key.expiresAt);
601
+ const oauthJwksVerificationKeyMatches = (key, input) => key.id === input.kid &&
602
+ key.status !== "disabled" &&
603
+ key.publicJwk !== undefined &&
604
+ (input.alg === undefined || key.alg === input.alg) &&
605
+ oauthJwksKeyUsableAt(key, input.now);
606
+ const oauthJwksNow = (now) => now === undefined ? currentUnixMillis : Effect.succeed(now);
607
+ const formUrlEncode = (value) => new URLSearchParams([["", value]]).toString().slice(1);
608
+ const clientSecretBasic = (provider, secret) => `Basic ${Encoding.encodeBase64(textEncoder.encode(`${formUrlEncode(provider.clientId)}:${formUrlEncode(secret)}`))}`;
609
+ const oauthTokenRequestHeaders = () => ({
610
+ "content-type": "application/x-www-form-urlencoded",
611
+ accept: "application/json",
612
+ });
613
+ const oauthRequestHeadersWithoutAuthorization = (headers) => {
614
+ const value = {};
615
+ for (const [name, headerValue] of Object.entries(headers ?? {})) {
616
+ if (name.toLowerCase() !== "authorization") {
617
+ value[name] = headerValue;
618
+ }
619
+ }
620
+ return value;
621
+ };
622
+ const applyOAuthClientAuthentication = (input) => {
623
+ const clientAuthentication = input.clientAuthentication ?? { method: "none" };
624
+ if (clientAuthentication.method === "client_secret_basic") {
625
+ input.body.delete("client_id");
626
+ input.body.delete("client_secret");
627
+ input.headers.authorization = clientSecretBasic(input.provider, Redacted.value(clientAuthentication.clientSecret));
628
+ }
629
+ else {
630
+ input.body.set("client_id", input.provider.clientId);
631
+ }
632
+ if (clientAuthentication.method === "client_secret_post") {
633
+ input.body.set("client_secret", Redacted.value(clientAuthentication.clientSecret));
634
+ }
635
+ };
636
+ export const makeOAuthTokenRequest = (input) => Effect.try({
637
+ try: () => {
638
+ if (input.provider.tokenEndpoint === undefined) {
639
+ throw new Error("OAuth provider tokenEndpoint is required");
640
+ }
641
+ const body = new URLSearchParams(input.params);
642
+ const headers = oauthTokenRequestHeaders();
643
+ body.set("grant_type", "authorization_code");
644
+ body.set("code", String(input.code));
645
+ body.set("redirect_uri", input.redirectUri ?? input.provider.redirectUri);
646
+ body.set("code_verifier", Redacted.value(input.codeVerifier));
647
+ applyOAuthClientAuthentication({
648
+ provider: input.provider,
649
+ body,
650
+ headers,
651
+ clientAuthentication: input.clientAuthentication,
652
+ });
653
+ return {
654
+ url: input.provider.tokenEndpoint,
655
+ method: "POST",
656
+ headers,
657
+ body: body.toString(),
658
+ };
659
+ },
660
+ catch: (cause) => oauthTokenExchangeError("token-request", "Failed to build OAuth token request", cause),
661
+ });
662
+ export const makeOAuthRefreshTokenRequest = (input) => Effect.try({
663
+ try: () => {
664
+ if (input.provider.tokenEndpoint === undefined) {
665
+ throw new Error("OAuth provider tokenEndpoint is required");
666
+ }
667
+ const body = new URLSearchParams(input.params);
668
+ const headers = oauthTokenRequestHeaders();
669
+ body.set("grant_type", "refresh_token");
670
+ body.set("refresh_token", Redacted.value(input.refreshToken));
671
+ if (input.scopes !== undefined) {
672
+ body.set("scope", input.scopes.join(" "));
673
+ }
674
+ applyOAuthClientAuthentication({
675
+ provider: input.provider,
676
+ body,
677
+ headers,
678
+ clientAuthentication: input.clientAuthentication,
679
+ });
680
+ return {
681
+ url: input.provider.tokenEndpoint,
682
+ method: "POST",
683
+ headers,
684
+ body: body.toString(),
685
+ };
686
+ },
687
+ catch: (cause) => oauthTokenExchangeError("refresh-token-request", "Failed to build OAuth refresh token request", cause),
688
+ });
689
+ export const makeOAuthTokenRevocationRequest = (input) => Effect.try({
690
+ try: () => {
691
+ if (input.provider.revocationEndpoint === undefined) {
692
+ throw new Error("OAuth provider revocationEndpoint is required");
693
+ }
694
+ const body = new URLSearchParams(input.params);
695
+ const headers = oauthTokenRequestHeaders();
696
+ body.set("token", Redacted.value(input.token));
697
+ if (input.tokenTypeHint !== undefined) {
698
+ body.set("token_type_hint", input.tokenTypeHint);
699
+ }
700
+ applyOAuthClientAuthentication({
701
+ provider: input.provider,
702
+ body,
703
+ headers,
704
+ clientAuthentication: input.clientAuthentication,
705
+ });
706
+ return {
707
+ url: input.provider.revocationEndpoint,
708
+ method: "POST",
709
+ headers,
710
+ body: body.toString(),
711
+ };
712
+ },
713
+ catch: (cause) => oauthTokenExchangeError("revocation-request", "Failed to build OAuth token revocation request", cause),
714
+ });
715
+ export const makeOAuthUserInfoRequest = (input) => Effect.try({
716
+ try: () => {
717
+ if (input.provider.userInfoEndpoint === undefined) {
718
+ throw new Error("OAuth provider userInfoEndpoint is required");
719
+ }
720
+ const url = new URL(input.provider.userInfoEndpoint);
721
+ for (const [name, value] of Object.entries(input.params ?? {})) {
722
+ url.searchParams.set(name, value);
723
+ }
724
+ return {
725
+ url: url.toString(),
726
+ method: "GET",
727
+ headers: {
728
+ accept: "application/json",
729
+ ...oauthRequestHeadersWithoutAuthorization(input.headers),
730
+ authorization: `Bearer ${Redacted.value(input.accessToken)}`,
731
+ },
732
+ };
733
+ },
734
+ catch: (cause) => oauthUserInfoError("request", "Failed to build OAuth userinfo request", cause),
735
+ });
736
+ export const decodeOAuthUserInfoResponse = (value) => {
737
+ const record = recordFromUnknown(value);
738
+ return record === undefined
739
+ ? Effect.fail(oauthUserInfoError("decode", "OAuth userinfo response must be an object"))
740
+ : Effect.succeed(metadataValue(record) ?? {});
741
+ };
742
+ export const decodeOAuthTokenResponse = (value) => Effect.gen(function* () {
743
+ if (!isTokenResponseRecord(value)) {
744
+ return yield* oauthTokenExchangeError("decode-token-response", "OAuth token response must be an object");
745
+ }
746
+ const accessToken = stringFromUnknown(value.access_token);
747
+ const tokenType = stringFromUnknown(value.token_type);
748
+ if (accessToken === undefined) {
749
+ return yield* oauthTokenExchangeError("decode-token-response", "OAuth token response is missing access_token");
750
+ }
751
+ if (tokenType === undefined) {
752
+ return yield* oauthTokenExchangeError("decode-token-response", "OAuth token response is missing token_type");
753
+ }
754
+ const refreshToken = stringFromUnknown(value.refresh_token);
755
+ const idToken = stringFromUnknown(value.id_token);
756
+ return {
757
+ accessToken: Redacted.make(OAuthAccessToken(accessToken)),
758
+ tokenType,
759
+ expiresIn: numberFromUnknown(value.expires_in),
760
+ ...(refreshToken === undefined
761
+ ? {}
762
+ : { refreshToken: Redacted.make(OAuthRefreshToken(refreshToken)) }),
763
+ ...(idToken === undefined
764
+ ? {}
765
+ : { idToken: Redacted.make(OAuthIdToken(idToken)) }),
766
+ scopes: scopeValues(value.scope),
767
+ extra: tokenResponseExtra(value),
768
+ };
769
+ });
770
+ export const makeOidcIdTokenVerifier = (dependencies) => OidcIdTokenVerifier.make({
771
+ verify: Effect.fn("auth.oidc_id_token.verify")(function* (input) {
772
+ const { provider } = input;
773
+ const { issuer } = provider;
774
+ if (issuer === undefined) {
775
+ return {
776
+ valid: false,
777
+ reason: "missing_provider_issuer",
778
+ };
779
+ }
780
+ const verified = yield* dependencies.jwtVerifier
781
+ .verify({
782
+ token: Redacted.make(Redacted.value(input.idToken)),
783
+ issuer,
784
+ audience: input.audience ?? provider.clientId,
785
+ now: input.now,
786
+ clockTolerance: input.clockTolerance,
787
+ })
788
+ .pipe(Effect.mapError((cause) => oidcIdTokenVerifierError("verify", "Failed to verify OIDC ID token JWT", cause)));
789
+ if (!verified.valid) {
790
+ return verified;
791
+ }
792
+ const claims = oidcIdTokenClaims(verified.claims, input);
793
+ if (typeof claims === "string") {
794
+ return { valid: false, reason: claims };
795
+ }
796
+ return {
797
+ valid: true,
798
+ header: verified.header,
799
+ claims,
800
+ key: verified.key,
801
+ };
802
+ }),
803
+ });
804
+ export const verifyOidcIdToken = (input) => OidcIdTokenVerifier.use((verifier) => verifier.verify(input));
805
+ export const decodeOAuthJwksDocument = (input) => Effect.gen(function* () {
806
+ const record = recordFromUnknown(input);
807
+ if (record === undefined || !Array.isArray(record.keys)) {
808
+ return yield* oauthJwksJwtError("OAuth JWKS document must contain keys");
809
+ }
810
+ const keys = [];
811
+ for (const key of record.keys) {
812
+ const publicJwk = oauthJwksPublicJwkFromUnknown(key);
813
+ if (publicJwk === undefined) {
814
+ return yield* oauthJwksJwtError("OAuth JWKS key must be a public JWK");
815
+ }
816
+ keys.push(publicJwk);
817
+ }
818
+ return { keys };
819
+ });
820
+ export const makeOAuthJwksJwtKeys = (dependencies) => {
821
+ const cacheTtlMillis = Math.max(0, Duration.toMillis(dependencies.cacheTtl ?? defaultOAuthJwksCacheTtl));
822
+ const allowedAlgorithms = dependencies.allowedAlgorithms?.filter(isPublicJwksAlgorithm);
823
+ let cache;
824
+ const loadRecords = (now) => Effect.gen(function* () {
825
+ if (cache !== undefined && Number(now) < Number(cache.expiresAt)) {
826
+ return cache.records;
827
+ }
828
+ const url = nonEmptyStringFromUnknown(dependencies.provider.jwksUri);
829
+ if (url === undefined) {
830
+ return yield* oauthJwksJwtError("OAuth provider JWKS URI is required");
831
+ }
832
+ const rawDocument = yield* dependencies.fetch({
833
+ provider: dependencies.provider,
834
+ url,
835
+ });
836
+ const document = yield* decodeOAuthJwksDocument(rawDocument);
837
+ const records = oauthJwksRecordsFromDocument(dependencies.provider, document, allowedAlgorithms);
838
+ cache = {
839
+ records,
840
+ expiresAt: UnixMillis(Number(now) + cacheTtlMillis),
841
+ };
842
+ return records;
843
+ });
844
+ return JwtKeys.make({
845
+ selectSigningKey: () => Effect.succeed(Option.none()),
846
+ findVerificationKey: Effect.fn("auth.oauth_jwks.find_verification_key")(function* (input) {
847
+ const now = yield* oauthJwksNow(input.now);
848
+ const records = yield* loadRecords(now);
849
+ const key = records.find((record) => oauthJwksVerificationKeyMatches(record, { ...input, now }));
850
+ return key === undefined ? Option.none() : Option.some(key);
851
+ }),
852
+ listPublicJwks: Effect.fn("auth.oauth_jwks.list_public_jwks")(function* (input) {
853
+ const now = yield* oauthJwksNow(input?.now);
854
+ const records = yield* loadRecords(now);
855
+ return records.flatMap((record) => record.publicJwk === undefined || !oauthJwksKeyUsableAt(record, now)
856
+ ? []
857
+ : [record.publicJwk]);
858
+ }),
859
+ });
860
+ };
861
+ export const normalizeOidcProfile = (input) => Effect.gen(function* () {
862
+ const { claims, provider } = input;
863
+ if (claims === undefined) {
864
+ return yield* oauthProfileNormalizationError("missing_claims", "OIDC ID token claims are required to normalize an OAuth profile");
865
+ }
866
+ const subject = nonEmptyStringFromUnknown(claims.sub);
867
+ if (subject === undefined) {
868
+ return yield* oauthProfileNormalizationError("missing_subject", "OIDC profile is missing subject");
869
+ }
870
+ const userInfo = yield* userInfoRecord(input.userInfo);
871
+ const userInfoSubject = userInfo === undefined ? undefined : nonEmptyStringFromUnknown(userInfo.sub);
872
+ if (userInfoSubject !== undefined && userInfoSubject !== subject) {
873
+ return yield* oauthProfileNormalizationError("subject_mismatch", "OIDC userinfo subject does not match ID token subject");
874
+ }
875
+ const email = emailFromUnknown(userInfo?.email) ?? emailFromUnknown(claims.email);
876
+ const emailVerified = booleanFromUnknown(userInfo?.email_verified) ??
877
+ booleanFromUnknown(claims.email_verified);
878
+ const metadata = optionalRecord({
879
+ ...(input.metadata === undefined ? {} : input.metadata),
880
+ });
881
+ return makeOAuthProfile({
882
+ providerId: provider.id,
883
+ providerAccountId: subject,
884
+ email,
885
+ emailVerified,
886
+ name: firstString(userInfo?.name, claims.name),
887
+ username: firstString(userInfo?.preferred_username, claims.preferred_username),
888
+ avatarUrl: firstString(userInfo?.picture, claims.picture),
889
+ metadata,
890
+ });
891
+ });
892
+ export const makeOidcOAuthProfileNormalizer = () => OAuthProfileNormalizer.make({
893
+ normalize: Effect.fn("auth.oauth_profile.normalize_oidc")(normalizeOidcProfile),
894
+ });
895
+ export const normalizeOAuthProfile = (input) => OAuthProfileNormalizer.use((normalizer) => normalizer.normalize(input));
896
+ export const verifiedOAuthIdentityFromProfile = (profile) => {
897
+ const metadata = optionalRecord({
898
+ ...(profile.name === undefined ? {} : { name: profile.name }),
899
+ ...(profile.username === undefined ? {} : { username: profile.username }),
900
+ ...(profile.avatarUrl === undefined ? {} : { avatarUrl: profile.avatarUrl }),
901
+ ...(profile.metadata === undefined ? {} : profile.metadata),
902
+ });
903
+ return {
904
+ providerId: profile.providerId,
905
+ providerAccountId: profile.providerAccountId,
906
+ ...(profile.email === undefined ? {} : { email: profile.email }),
907
+ ...(profile.emailVerified === undefined
908
+ ? {}
909
+ : { emailVerified: profile.emailVerified }),
910
+ ...(metadata === undefined ? {} : { metadata: metadataValue(metadata) }),
911
+ };
912
+ };
913
+ export const makeOAuthTokenExchange = (dependencies) => OAuthTokenExchange.make({
914
+ exchangeAuthorizationCode: Effect.fn("auth.oauth_token.exchange_code")(function* (input) {
915
+ const request = yield* makeOAuthTokenRequest(input);
916
+ const response = yield* dependencies.request(request);
917
+ return yield* decodeOAuthTokenResponse(response).pipe(Effect.mapError((cause) => oauthTokenExchangeError("exchange-code", "Failed to decode OAuth token exchange response", cause)));
918
+ }),
919
+ });
920
+ export const completeOAuthCallback = (input) => Effect.gen(function* () {
921
+ const oauthState = yield* OAuthState;
922
+ const tokenExchange = yield* OAuthTokenExchange;
923
+ const verifiedState = yield* oauthState.verify({ state: input.state });
924
+ if (verifiedState.providerId !== input.provider.id) {
925
+ return yield* oauthStateError("verify", "OAuth callback provider does not match verified state");
926
+ }
927
+ const tokens = yield* tokenExchange.exchangeAuthorizationCode({
928
+ provider: input.provider,
929
+ code: input.code,
930
+ codeVerifier: input.codeVerifier,
931
+ redirectUri: input.redirectUri ?? verifiedState.redirectUri ?? input.provider.redirectUri,
932
+ clientAuthentication: input.clientAuthentication,
933
+ params: input.tokenParams,
934
+ });
935
+ return { state: verifiedState, tokens };
936
+ });
937
+ export const makeOAuthAccountLinkingPolicy = (dependencies) => OAuthAccountLinking.make({
938
+ resolve: Effect.fn("auth.oauth_account_linking.resolve")(function* (input) {
939
+ const identity = cloneVerifiedOAuthIdentity(input.identity);
940
+ const linked = yield* dependencies.accounts
941
+ .findByProviderAccount({
942
+ providerId: identity.providerId,
943
+ providerAccountId: identity.providerAccountId,
944
+ })
945
+ .pipe(Effect.mapError((cause) => oauthAccountLinkingError("resolve", "Failed to look up OAuth provider account", cause)));
946
+ if (Option.isSome(linked)) {
947
+ const account = linked.value;
948
+ if (input.currentUserId !== undefined &&
949
+ account.userId !== input.currentUserId) {
950
+ return {
951
+ type: "deny",
952
+ reason: "provider-account-linked-to-different-user",
953
+ identity,
954
+ account,
955
+ currentUserId: input.currentUserId,
956
+ };
957
+ }
958
+ return {
959
+ type: "sign-in",
960
+ reason: "linked-account",
961
+ userId: account.userId,
962
+ account,
963
+ };
964
+ }
965
+ if (input.currentUserId !== undefined) {
966
+ return {
967
+ type: "link-existing-user",
968
+ reason: "current-user",
969
+ userId: input.currentUserId,
970
+ identity,
971
+ };
972
+ }
973
+ if (input.matchedUserId !== undefined) {
974
+ if (dependencies.autoLinkVerifiedEmail === true &&
975
+ identity.emailVerified === true) {
976
+ return {
977
+ type: "link-existing-user",
978
+ reason: "verified-email",
979
+ userId: input.matchedUserId,
980
+ identity,
981
+ };
982
+ }
983
+ return {
984
+ type: "require-explicit-linking",
985
+ reason: identity.emailVerified === true ? "matched-email" : "unverified-email",
986
+ identity,
987
+ matchedUserId: input.matchedUserId,
988
+ };
989
+ }
990
+ return {
991
+ type: "require-explicit-linking",
992
+ reason: "no-linked-account",
993
+ identity,
994
+ };
995
+ }),
996
+ });
997
+ export const resolveOAuthAccountLinking = (input) => OAuthAccountLinking.use((linking) => linking.resolve(input));
998
+ export const makeOAuthAccountUnlinkingPolicy = (dependencies) => OAuthAccountUnlinking.make({
999
+ resolve: Effect.fn("auth.oauth_account_unlinking.resolve")(function* (input) {
1000
+ const account = yield* dependencies.accounts
1001
+ .findByProviderAccount({
1002
+ providerId: input.providerId,
1003
+ providerAccountId: input.providerAccountId,
1004
+ })
1005
+ .pipe(Effect.mapError((cause) => oauthAccountUnlinkingError("resolve", "Failed to look up OAuth provider account", cause)));
1006
+ if (Option.isNone(account)) {
1007
+ return {
1008
+ type: "deny-not-found",
1009
+ reason: "provider-account-not-found",
1010
+ userId: input.userId,
1011
+ providerId: input.providerId,
1012
+ providerAccountId: input.providerAccountId,
1013
+ };
1014
+ }
1015
+ const linkedAccount = account.value;
1016
+ if (linkedAccount.userId !== input.userId) {
1017
+ return {
1018
+ type: "deny-not-owner",
1019
+ reason: "provider-account-linked-to-different-user",
1020
+ userId: input.userId,
1021
+ account: linkedAccount,
1022
+ };
1023
+ }
1024
+ const remainingAccounts = yield* dependencies.accounts
1025
+ .listByUser({ userId: input.userId })
1026
+ .pipe(Effect.map((accounts) => accounts.filter((linked) => linked.id !== linkedAccount.id)), Effect.mapError((cause) => oauthAccountUnlinkingError("resolve", "Failed to list OAuth provider accounts for user", cause)));
1027
+ if (dependencies.allowLastAccount !== true && remainingAccounts.length === 0) {
1028
+ return {
1029
+ type: "deny-last-account",
1030
+ reason: "last-linked-account",
1031
+ userId: input.userId,
1032
+ account: linkedAccount,
1033
+ };
1034
+ }
1035
+ if (dependencies.requireStepUp === true && input.stepUpSatisfied !== true) {
1036
+ return {
1037
+ type: "requires-step-up",
1038
+ reason: "step-up-required",
1039
+ userId: input.userId,
1040
+ account: linkedAccount,
1041
+ remainingAccounts,
1042
+ };
1043
+ }
1044
+ return {
1045
+ type: "allow",
1046
+ reason: "owned-account",
1047
+ userId: input.userId,
1048
+ account: linkedAccount,
1049
+ remainingAccounts,
1050
+ };
1051
+ }),
1052
+ });
1053
+ export const resolveOAuthAccountUnlinking = (input) => OAuthAccountUnlinking.use((unlinking) => unlinking.resolve(input));
1054
+ const findOAuthLinkConfirmationExistingAccount = (dependencies, input) => dependencies.accounts
1055
+ .findByProviderAccount({
1056
+ providerId: input.identity.providerId,
1057
+ providerAccountId: input.identity.providerAccountId,
1058
+ })
1059
+ .pipe(Effect.mapError((cause) => oauthLinkConfirmationError(input.operation, "Failed to look up OAuth provider account", cause)), Effect.flatMap((account) => {
1060
+ if (Option.isNone(account)) {
1061
+ return Effect.succeed(account);
1062
+ }
1063
+ if (account.value.userId !== input.userId) {
1064
+ return Effect.fail(oauthLinkConfirmationError(input.operation, "OAuth provider account is linked to a different user"));
1065
+ }
1066
+ return Effect.succeed(account);
1067
+ }));
1068
+ export const makeOAuthLinkConfirmation = (dependencies) => OAuthLinkConfirmation.make({
1069
+ start: Effect.fn("auth.oauth_link_confirmation.start")(function* (input) {
1070
+ const identity = cloneVerifiedOAuthIdentity(input.identity);
1071
+ const reason = input.reason ?? "manual";
1072
+ const existing = yield* findOAuthLinkConfirmationExistingAccount(dependencies, {
1073
+ operation: "start",
1074
+ userId: input.userId,
1075
+ identity,
1076
+ });
1077
+ if (Option.isSome(existing)) {
1078
+ return yield* oauthLinkConfirmationError("start", "OAuth provider account is already linked to this user");
1079
+ }
1080
+ const secret = input.secret ??
1081
+ (yield* dependencies.crypto
1082
+ .randomToken(yield* validateOAuthLinkConfirmationSecretBytes(dependencies.secretBytes ?? defaultOAuthLinkConfirmationSecretBytes))
1083
+ .pipe(Effect.map(Redacted.make), Effect.mapError((cause) => oauthLinkConfirmationError("start", "Failed to generate OAuth link confirmation secret", cause))));
1084
+ const issued = yield* dependencies.challenge
1085
+ .issue({
1086
+ type: oauthLinkConfirmationChallengeType,
1087
+ subject: input.userId,
1088
+ ttl: input.ttl ?? defaultOAuthLinkConfirmationTtl,
1089
+ secret,
1090
+ metadata: oauthLinkConfirmationMetadata({
1091
+ userId: input.userId,
1092
+ identity,
1093
+ reason,
1094
+ metadata: input.metadata,
1095
+ }),
1096
+ })
1097
+ .pipe(Effect.mapError((cause) => oauthLinkConfirmationError("start", "Failed to issue OAuth link confirmation challenge", cause)));
1098
+ return {
1099
+ challengeId: issued.id,
1100
+ userId: input.userId,
1101
+ identity,
1102
+ reason,
1103
+ secret,
1104
+ expiresAt: issued.expiresAt,
1105
+ ...(input.metadata === undefined
1106
+ ? {}
1107
+ : { metadata: metadataValue(input.metadata) }),
1108
+ };
1109
+ }),
1110
+ inspect: Effect.fn("auth.oauth_link_confirmation.inspect")(function* (input) {
1111
+ const inspected = yield* dependencies.challenge
1112
+ .inspect({
1113
+ challengeId: input.challengeId,
1114
+ type: oauthLinkConfirmationChallengeType,
1115
+ secret: input.secret,
1116
+ })
1117
+ .pipe(Effect.mapError((cause) => oauthLinkConfirmationError("inspect", "Invalid OAuth link confirmation challenge", cause)));
1118
+ const pending = yield* pendingOAuthLinkConfirmationFromChallenge(inspected, "inspect");
1119
+ yield* findOAuthLinkConfirmationExistingAccount(dependencies, {
1120
+ operation: "inspect",
1121
+ userId: pending.userId,
1122
+ identity: pending.identity,
1123
+ });
1124
+ return pending;
1125
+ }),
1126
+ confirm: Effect.fn("auth.oauth_link_confirmation.confirm")(function* (input) {
1127
+ const inspected = yield* dependencies.challenge
1128
+ .inspect({
1129
+ challengeId: input.challengeId,
1130
+ type: oauthLinkConfirmationChallengeType,
1131
+ secret: input.secret,
1132
+ })
1133
+ .pipe(Effect.mapError((cause) => oauthLinkConfirmationError("confirm", "Invalid OAuth link confirmation challenge", cause)));
1134
+ const pending = yield* pendingOAuthLinkConfirmationFromChallenge(inspected, "confirm");
1135
+ if (input.currentUserId !== undefined && input.currentUserId !== pending.userId) {
1136
+ return yield* oauthLinkConfirmationError("confirm", "OAuth link confirmation does not match current user");
1137
+ }
1138
+ if (input.expectedProviderId !== undefined &&
1139
+ input.expectedProviderId !== pending.identity.providerId) {
1140
+ return yield* oauthLinkConfirmationError("confirm", "OAuth link confirmation provider mismatch");
1141
+ }
1142
+ if (input.expectedProviderAccountId !== undefined &&
1143
+ input.expectedProviderAccountId !== pending.identity.providerAccountId) {
1144
+ return yield* oauthLinkConfirmationError("confirm", "OAuth link confirmation provider account mismatch");
1145
+ }
1146
+ const existing = yield* findOAuthLinkConfirmationExistingAccount(dependencies, {
1147
+ operation: "confirm",
1148
+ userId: pending.userId,
1149
+ identity: pending.identity,
1150
+ });
1151
+ yield* dependencies.challenge
1152
+ .verify({
1153
+ challengeId: input.challengeId,
1154
+ type: oauthLinkConfirmationChallengeType,
1155
+ secret: input.secret,
1156
+ })
1157
+ .pipe(Effect.mapError((cause) => oauthLinkConfirmationError("confirm", "Failed to consume OAuth link confirmation challenge", cause)));
1158
+ return confirmedOAuthLinkConfirmationFromPending({
1159
+ pending,
1160
+ ...(Option.isSome(existing) ? { existingAccount: existing.value } : {}),
1161
+ });
1162
+ }),
1163
+ });
1164
+ export const startOAuthLinkConfirmation = (input) => OAuthLinkConfirmation.use((confirmation) => confirmation.start(input));
1165
+ export const inspectOAuthLinkConfirmation = (input) => OAuthLinkConfirmation.use((confirmation) => confirmation.inspect(input));
1166
+ export const confirmOAuthLinkConfirmation = (input) => OAuthLinkConfirmation.use((confirmation) => confirmation.confirm(input));
1167
+ const makeOAuthPkceWithCrypto = (dependencies, input) => Effect.gen(function* () {
1168
+ const verifier = input?.codeVerifier === undefined
1169
+ ? yield* dependencies.crypto
1170
+ .randomToken(yield* validateByteLength(input?.verifierBytes ??
1171
+ dependencies.codeVerifierBytes ??
1172
+ defaultOAuthCodeVerifierBytes, "pkce", "OAuth code verifier"))
1173
+ .pipe(Effect.map(OAuthCodeVerifier), Effect.mapError((cause) => oauthStateError("pkce", "Failed to generate OAuth code verifier", cause)))
1174
+ : OAuthCodeVerifier(Redacted.value(input.codeVerifier));
1175
+ const codeChallenge = yield* dependencies.crypto
1176
+ .digestSha256({ data: verifier })
1177
+ .pipe(Effect.map(OAuthCodeChallenge), Effect.mapError((cause) => oauthStateError("pkce", "Failed to calculate OAuth PKCE challenge", cause)));
1178
+ return {
1179
+ codeVerifier: Redacted.make(verifier),
1180
+ codeChallenge,
1181
+ codeChallengeMethod: "S256",
1182
+ };
1183
+ });
1184
+ export const makeOAuthPkce = (input) => Effect.gen(function* () {
1185
+ const crypto = yield* Crypto;
1186
+ return yield* makeOAuthPkceWithCrypto({ crypto }, input);
1187
+ });
1188
+ const appendParams = (url, params) => {
1189
+ if (params === undefined) {
1190
+ return;
1191
+ }
1192
+ for (const [key, value] of Object.entries(params)) {
1193
+ if (value !== undefined) {
1194
+ url.searchParams.set(key, value);
1195
+ }
1196
+ }
1197
+ };
1198
+ const makeAuthorizationUrl = (input) => Effect.try({
1199
+ try: () => {
1200
+ const url = new URL(input.provider.authorizationEndpoint);
1201
+ appendParams(url, input.provider.authorizationParams);
1202
+ appendParams(url, input.params);
1203
+ appendParams(url, {
1204
+ response_type: "code",
1205
+ client_id: input.provider.clientId,
1206
+ redirect_uri: input.redirectUri,
1207
+ scope: input.scopes.length === 0 ? undefined : input.scopes.join(" "),
1208
+ state: input.state,
1209
+ code_challenge: input.pkce.codeChallenge,
1210
+ code_challenge_method: input.pkce.codeChallengeMethod,
1211
+ nonce: input.nonce,
1212
+ });
1213
+ return url.toString();
1214
+ },
1215
+ catch: (cause) => oauthStateError("authorization-url", "Failed to build OAuth authorization URL", cause),
1216
+ });
1217
+ export const makeOAuthState = (dependencies) => OAuthState.make({
1218
+ start: Effect.fn("auth.oauth_state.start")(function* (input) {
1219
+ const stateSecretBytes = yield* validateByteLength(dependencies.stateSecretBytes ?? defaultOAuthStateSecretBytes, "start", "OAuth state secret");
1220
+ const stateSecret = yield* dependencies.crypto.randomToken(stateSecretBytes).pipe(Effect.map(Redacted.make), Effect.mapError((cause) => oauthStateError("start", "Failed to generate OAuth state secret", cause)));
1221
+ const pkce = yield* makeOAuthPkceWithCrypto(dependencies, {
1222
+ codeVerifier: input.codeVerifier,
1223
+ verifierBytes: dependencies.codeVerifierBytes,
1224
+ });
1225
+ const includeNonce = input.includeNonce ?? input.provider.issuer !== undefined;
1226
+ const nonce = input.nonce === undefined && !includeNonce
1227
+ ? undefined
1228
+ : input.nonce === undefined
1229
+ ? yield* dependencies.crypto
1230
+ .randomToken(dependencies.nonceBytes ?? defaultOAuthNonceBytes)
1231
+ .pipe(Effect.map(OAuthNonce), Effect.mapError((cause) => oauthStateError("start", "Failed to generate OAuth nonce", cause)))
1232
+ : OAuthNonce(input.nonce);
1233
+ const redirectUri = input.redirectUri ?? input.provider.redirectUri;
1234
+ const scopes = input.scopes ?? input.provider.scopes ?? [];
1235
+ const issued = yield* dependencies.challenge
1236
+ .issue({
1237
+ type: "oauth-state",
1238
+ subject: input.provider.id,
1239
+ ttl: input.ttl ?? defaultOAuthStateTtl,
1240
+ secret: stateSecret,
1241
+ metadata: {
1242
+ providerId: input.provider.id,
1243
+ redirectUri,
1244
+ scopes,
1245
+ codeChallenge: pkce.codeChallenge,
1246
+ ...(nonce === undefined ? {} : { nonce }),
1247
+ ...(input.metadata === undefined ? {} : { metadata: input.metadata }),
1248
+ },
1249
+ })
1250
+ .pipe(Effect.mapError((cause) => oauthStateError("start", "Failed to issue OAuth state challenge", cause)));
1251
+ const state = encodeOAuthState({ challengeId: issued.id, secret: stateSecret });
1252
+ const authorizationUrl = yield* makeAuthorizationUrl({
1253
+ provider: input.provider,
1254
+ redirectUri,
1255
+ scopes,
1256
+ params: input.authorizationParams,
1257
+ state,
1258
+ pkce,
1259
+ nonce,
1260
+ });
1261
+ return {
1262
+ providerId: input.provider.id,
1263
+ authorizationUrl,
1264
+ state,
1265
+ challengeId: issued.id,
1266
+ expiresAt: issued.expiresAt,
1267
+ codeVerifier: pkce.codeVerifier,
1268
+ codeChallenge: pkce.codeChallenge,
1269
+ codeChallengeMethod: pkce.codeChallengeMethod,
1270
+ ...(nonce === undefined ? {} : { nonce }),
1271
+ redirectUri,
1272
+ scopes,
1273
+ };
1274
+ }),
1275
+ verify: Effect.fn("auth.oauth_state.verify")(function* (input) {
1276
+ const parsed = yield* parseOAuthState(input.state);
1277
+ const verified = yield* dependencies.challenge
1278
+ .verify({
1279
+ challengeId: parsed.challengeId,
1280
+ type: "oauth-state",
1281
+ secret: parsed.secret,
1282
+ })
1283
+ .pipe(Effect.mapError((cause) => oauthStateError("verify", "Failed to verify OAuth state challenge", cause)));
1284
+ const metadata = verified.metadata ?? {};
1285
+ const appMetadata = metadataValue(typeof metadata.metadata === "object" && metadata.metadata !== null
1286
+ ? metadata.metadata
1287
+ : undefined);
1288
+ const codeChallenge = stringFromUnknown(metadata.codeChallenge);
1289
+ const nonce = stringFromUnknown(metadata.nonce);
1290
+ return {
1291
+ challengeId: parsed.challengeId,
1292
+ providerId: OAuthProviderId(verified.subject),
1293
+ redirectUri: stringFromUnknown(metadata.redirectUri),
1294
+ scopes: stringArrayFromUnknown(metadata.scopes),
1295
+ ...(codeChallenge === undefined
1296
+ ? {}
1297
+ : { codeChallenge: OAuthCodeChallenge(codeChallenge) }),
1298
+ ...(nonce === undefined ? {} : { nonce: OAuthNonce(nonce) }),
1299
+ ...(appMetadata === undefined ? {} : { metadata: appMetadata }),
1300
+ };
1301
+ }),
1302
+ });
1303
+ export const OAuthStateLive = Layer.effect(OAuthState)(Effect.gen(function* () {
1304
+ const crypto = yield* Crypto;
1305
+ const challenge = yield* Challenge;
1306
+ return makeOAuthState({ crypto, challenge });
1307
+ }));
1308
+ export const makeOAuthProviders = (providers) => {
1309
+ const providersById = new Map(providers.map((provider) => [provider.id, cloneProviderConfig(provider)]));
1310
+ return OAuthProviders.make({
1311
+ get: (id) => Effect.sync(() => {
1312
+ const provider = providersById.get(id);
1313
+ return provider === undefined
1314
+ ? Option.none()
1315
+ : Option.some(cloneProviderConfig(provider));
1316
+ }),
1317
+ });
1318
+ };
1319
+ export const OAuthProvidersLive = (providers) => Layer.succeed(OAuthProviders)(makeOAuthProviders(providers));
1320
+ export const OidcIdTokenVerifierLive = Layer.effect(OidcIdTokenVerifier)(Effect.gen(function* () {
1321
+ const jwtVerifier = yield* JwtVerifier;
1322
+ return makeOidcIdTokenVerifier({ jwtVerifier });
1323
+ }));
1324
+ export const OAuthOidcProfileNormalizerLive = Layer.succeed(OAuthProfileNormalizer, makeOidcOAuthProfileNormalizer());
1325
+ export const OAuthAccountLinkingLive = Layer.effect(OAuthAccountLinking)(Effect.gen(function* () {
1326
+ const accounts = yield* OAuthAccountStore;
1327
+ return makeOAuthAccountLinkingPolicy({ accounts });
1328
+ }));
1329
+ export const OAuthAccountUnlinkingLive = Layer.effect(OAuthAccountUnlinking)(Effect.gen(function* () {
1330
+ const accounts = yield* OAuthAccountStore;
1331
+ return makeOAuthAccountUnlinkingPolicy({ accounts });
1332
+ }));
1333
+ export const OAuthLinkConfirmationLive = Layer.effect(OAuthLinkConfirmation)(Effect.gen(function* () {
1334
+ const challenge = yield* Challenge;
1335
+ const accounts = yield* OAuthAccountStore;
1336
+ const crypto = yield* Crypto;
1337
+ return makeOAuthLinkConfirmation({ challenge, accounts, crypto });
1338
+ }));
1339
+ const accountKey = (input) => `${input.providerId}:${input.providerAccountId}`;
1340
+ const cloneOAuthAccountRecord = (record) => ({
1341
+ ...record,
1342
+ ...(record.metadata === undefined ? {} : { metadata: metadataValue(record.metadata) }),
1343
+ });
1344
+ const includeAccount = (record, includeUnlinked) => includeUnlinked === true || record.unlinkedAt === undefined;
1345
+ export const makeOAuthAccountStoreMemory = () => {
1346
+ const accountsById = new Map();
1347
+ const idByProviderAccount = new Map();
1348
+ return OAuthAccountStore.make({
1349
+ insert: (row) => Effect.sync(() => {
1350
+ const cloned = cloneOAuthAccountRecord(row);
1351
+ accountsById.set(row.id, cloned);
1352
+ idByProviderAccount.set(accountKey(row), row.id);
1353
+ }),
1354
+ findById: (id) => Effect.sync(() => {
1355
+ const row = accountsById.get(id);
1356
+ return row === undefined ? Option.none() : Option.some(cloneOAuthAccountRecord(row));
1357
+ }),
1358
+ findByProviderAccount: (input) => Effect.sync(() => {
1359
+ const id = idByProviderAccount.get(accountKey(input));
1360
+ const row = id === undefined ? undefined : accountsById.get(id);
1361
+ return row === undefined || !includeAccount(row, input.includeUnlinked)
1362
+ ? Option.none()
1363
+ : Option.some(cloneOAuthAccountRecord(row));
1364
+ }),
1365
+ listByUser: (input) => Effect.sync(() => [...accountsById.values()]
1366
+ .filter((row) => row.userId === input.userId && includeAccount(row, input.includeUnlinked))
1367
+ .map(cloneOAuthAccountRecord)),
1368
+ unlink: (input) => Effect.sync(() => {
1369
+ const id = idByProviderAccount.get(accountKey(input));
1370
+ const row = id === undefined ? undefined : accountsById.get(id);
1371
+ if (row === undefined) {
1372
+ return Option.none();
1373
+ }
1374
+ const updated = {
1375
+ ...row,
1376
+ unlinkedAt: input.unlinkedAt,
1377
+ metadata: input.reason === undefined
1378
+ ? row.metadata
1379
+ : { ...row.metadata, unlinkReason: input.reason },
1380
+ };
1381
+ accountsById.set(row.id, updated);
1382
+ return Option.some(cloneOAuthAccountRecord(updated));
1383
+ }),
1384
+ });
1385
+ };
1386
+ export const OAuthAccountStoreMemoryLive = Layer.sync(OAuthAccountStore)(makeOAuthAccountStoreMemory);
1387
+ //# sourceMappingURL=OAuth.js.map