@effect-auth/core 0.1.0-alpha.1 → 0.1.0-alpha.13

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 (255) hide show
  1. package/README.md +790 -73
  2. package/dist/ApiKey.d.ts +207 -0
  3. package/dist/ApiKey.d.ts.map +1 -0
  4. package/dist/ApiKey.js +275 -0
  5. package/dist/ApiKey.js.map +1 -0
  6. package/dist/AuditLog.d.ts +335 -1
  7. package/dist/AuditLog.d.ts.map +1 -1
  8. package/dist/AuditLog.js +318 -1
  9. package/dist/AuditLog.js.map +1 -1
  10. package/dist/AuthConfig.d.ts +42 -0
  11. package/dist/AuthConfig.d.ts.map +1 -0
  12. package/dist/AuthConfig.js +43 -0
  13. package/dist/AuthConfig.js.map +1 -0
  14. package/dist/AuthFlow.d.ts +462 -36
  15. package/dist/AuthFlow.d.ts.map +1 -1
  16. package/dist/AuthFlow.js +657 -5
  17. package/dist/AuthFlow.js.map +1 -1
  18. package/dist/AuthKernel.d.ts +3 -0
  19. package/dist/AuthKernel.d.ts.map +1 -0
  20. package/dist/AuthKernel.js +9 -0
  21. package/dist/AuthKernel.js.map +1 -0
  22. package/dist/Challenge.d.ts +24 -4
  23. package/dist/Challenge.d.ts.map +1 -1
  24. package/dist/Challenge.js +167 -1
  25. package/dist/Challenge.js.map +1 -1
  26. package/dist/Client.d.ts +449 -0
  27. package/dist/Client.d.ts.map +1 -0
  28. package/dist/Client.js +985 -0
  29. package/dist/Client.js.map +1 -0
  30. package/dist/Crypto.d.ts +6 -1
  31. package/dist/Crypto.d.ts.map +1 -1
  32. package/dist/Crypto.js +62 -1
  33. package/dist/Crypto.js.map +1 -1
  34. package/dist/DomainVerification.d.ts +200 -0
  35. package/dist/DomainVerification.d.ts.map +1 -0
  36. package/dist/DomainVerification.js +304 -0
  37. package/dist/DomainVerification.js.map +1 -0
  38. package/dist/EffectQbSqliteStorage.d.ts +985 -0
  39. package/dist/EffectQbSqliteStorage.d.ts.map +1 -0
  40. package/dist/EffectQbSqliteStorage.js +2824 -0
  41. package/dist/EffectQbSqliteStorage.js.map +1 -0
  42. package/dist/EmailOtp.d.ts +120 -0
  43. package/dist/EmailOtp.d.ts.map +1 -0
  44. package/dist/EmailOtp.js +286 -0
  45. package/dist/EmailOtp.js.map +1 -0
  46. package/dist/EmailVerification.d.ts +121 -0
  47. package/dist/EmailVerification.d.ts.map +1 -0
  48. package/dist/EmailVerification.js +200 -0
  49. package/dist/EmailVerification.js.map +1 -0
  50. package/dist/HttpApi/Api.d.ts +2410 -0
  51. package/dist/HttpApi/Api.d.ts.map +1 -0
  52. package/dist/HttpApi/Api.js +3026 -0
  53. package/dist/HttpApi/Api.js.map +1 -0
  54. package/dist/HttpApi/Endpoints.d.ts +1073 -0
  55. package/dist/HttpApi/Endpoints.d.ts.map +1 -0
  56. package/dist/HttpApi/Endpoints.js +428 -0
  57. package/dist/HttpApi/Endpoints.js.map +1 -0
  58. package/dist/HttpApi/Errors.d.ts +114 -0
  59. package/dist/HttpApi/Errors.d.ts.map +1 -0
  60. package/dist/HttpApi/Errors.js +230 -0
  61. package/dist/HttpApi/Errors.js.map +1 -0
  62. package/dist/HttpApi/Schemas.d.ts +1254 -0
  63. package/dist/HttpApi/Schemas.d.ts.map +1 -0
  64. package/dist/HttpApi/Schemas.js +862 -0
  65. package/dist/HttpApi/Schemas.js.map +1 -0
  66. package/dist/HttpApi/Security.d.ts +159 -0
  67. package/dist/HttpApi/Security.d.ts.map +1 -0
  68. package/dist/HttpApi/Security.js +530 -0
  69. package/dist/HttpApi/Security.js.map +1 -0
  70. package/dist/HttpApi/Transport.d.ts +65 -0
  71. package/dist/HttpApi/Transport.d.ts.map +1 -0
  72. package/dist/HttpApi/Transport.js +311 -0
  73. package/dist/HttpApi/Transport.js.map +1 -0
  74. package/dist/HttpApi/index.d.ts +8 -0
  75. package/dist/HttpApi/index.d.ts.map +1 -0
  76. package/dist/HttpApi/index.js +7 -0
  77. package/dist/HttpApi/index.js.map +1 -0
  78. package/dist/HttpApi.d.ts +2 -0
  79. package/dist/HttpApi.d.ts.map +1 -0
  80. package/dist/HttpApi.js +2 -0
  81. package/dist/HttpApi.js.map +1 -0
  82. package/dist/Identifiers.d.ts +3 -0
  83. package/dist/Identifiers.d.ts.map +1 -1
  84. package/dist/Identifiers.js +2 -0
  85. package/dist/Identifiers.js.map +1 -1
  86. package/dist/IncidentAction.d.ts +80 -0
  87. package/dist/IncidentAction.d.ts.map +1 -0
  88. package/dist/IncidentAction.js +158 -0
  89. package/dist/IncidentAction.js.map +1 -0
  90. package/dist/Internal.d.ts +7 -0
  91. package/dist/Internal.d.ts.map +1 -0
  92. package/dist/Internal.js +12 -0
  93. package/dist/Internal.js.map +1 -0
  94. package/dist/Jwt.d.ts +281 -0
  95. package/dist/Jwt.d.ts.map +1 -0
  96. package/dist/Jwt.js +721 -0
  97. package/dist/Jwt.js.map +1 -0
  98. package/dist/LoginApproval.d.ts +314 -0
  99. package/dist/LoginApproval.d.ts.map +1 -0
  100. package/dist/LoginApproval.js +551 -0
  101. package/dist/LoginApproval.js.map +1 -0
  102. package/dist/LoginNotification.d.ts +183 -0
  103. package/dist/LoginNotification.d.ts.map +1 -0
  104. package/dist/LoginNotification.js +329 -0
  105. package/dist/LoginNotification.js.map +1 -0
  106. package/dist/LoginRisk.d.ts +344 -0
  107. package/dist/LoginRisk.d.ts.map +1 -0
  108. package/dist/LoginRisk.js +484 -0
  109. package/dist/LoginRisk.js.map +1 -0
  110. package/dist/MachineAuth.d.ts +73 -0
  111. package/dist/MachineAuth.d.ts.map +1 -0
  112. package/dist/MachineAuth.js +147 -0
  113. package/dist/MachineAuth.js.map +1 -0
  114. package/dist/MagicLink.d.ts +85 -0
  115. package/dist/MagicLink.d.ts.map +1 -0
  116. package/dist/MagicLink.js +206 -0
  117. package/dist/MagicLink.js.map +1 -0
  118. package/dist/Mailer.d.ts +147 -0
  119. package/dist/Mailer.d.ts.map +1 -0
  120. package/dist/Mailer.js +208 -0
  121. package/dist/Mailer.js.map +1 -0
  122. package/dist/OAuth.d.ts +1890 -0
  123. package/dist/OAuth.d.ts.map +1 -0
  124. package/dist/OAuth.js +3063 -0
  125. package/dist/OAuth.js.map +1 -0
  126. package/dist/Passkey.d.ts +334 -0
  127. package/dist/Passkey.d.ts.map +1 -0
  128. package/dist/Passkey.js +422 -0
  129. package/dist/Passkey.js.map +1 -0
  130. package/dist/PasskeySimpleWebAuthn.d.ts +11 -0
  131. package/dist/PasskeySimpleWebAuthn.d.ts.map +1 -0
  132. package/dist/PasskeySimpleWebAuthn.js +181 -0
  133. package/dist/PasskeySimpleWebAuthn.js.map +1 -0
  134. package/dist/Password.d.ts +237 -0
  135. package/dist/Password.d.ts.map +1 -0
  136. package/dist/Password.js +641 -0
  137. package/dist/Password.js.map +1 -0
  138. package/dist/Policy.d.ts +7 -4
  139. package/dist/Policy.d.ts.map +1 -1
  140. package/dist/Policy.js +28 -5
  141. package/dist/Policy.js.map +1 -1
  142. package/dist/Privacy.d.ts +15 -0
  143. package/dist/Privacy.d.ts.map +1 -1
  144. package/dist/Privacy.js +58 -1
  145. package/dist/Privacy.js.map +1 -1
  146. package/dist/RateLimiter.d.ts +104 -0
  147. package/dist/RateLimiter.d.ts.map +1 -0
  148. package/dist/RateLimiter.js +118 -0
  149. package/dist/RateLimiter.js.map +1 -0
  150. package/dist/RecoveryCode.d.ts +169 -0
  151. package/dist/RecoveryCode.d.ts.map +1 -0
  152. package/dist/RecoveryCode.js +299 -0
  153. package/dist/RecoveryCode.js.map +1 -0
  154. package/dist/RefreshToken.d.ts +222 -0
  155. package/dist/RefreshToken.d.ts.map +1 -0
  156. package/dist/RefreshToken.js +342 -0
  157. package/dist/RefreshToken.js.map +1 -0
  158. package/dist/Retention.d.ts +98 -0
  159. package/dist/Retention.d.ts.map +1 -0
  160. package/dist/Retention.js +204 -0
  161. package/dist/Retention.js.map +1 -0
  162. package/dist/SecurityTimeline.d.ts +78 -0
  163. package/dist/SecurityTimeline.d.ts.map +1 -0
  164. package/dist/SecurityTimeline.js +461 -0
  165. package/dist/SecurityTimeline.js.map +1 -0
  166. package/dist/Sessions.d.ts +123 -11
  167. package/dist/Sessions.d.ts.map +1 -1
  168. package/dist/Sessions.js +534 -1
  169. package/dist/Sessions.js.map +1 -1
  170. package/dist/StepUp.d.ts +44 -0
  171. package/dist/StepUp.d.ts.map +1 -0
  172. package/dist/StepUp.js +82 -0
  173. package/dist/StepUp.js.map +1 -0
  174. package/dist/Storage.d.ts +73 -5
  175. package/dist/Storage.d.ts.map +1 -1
  176. package/dist/Storage.js +11 -1
  177. package/dist/Storage.js.map +1 -1
  178. package/dist/StorageMigrations.d.ts +23 -1
  179. package/dist/StorageMigrations.d.ts.map +1 -1
  180. package/dist/StorageMigrations.js +462 -2
  181. package/dist/StorageMigrations.js.map +1 -1
  182. package/dist/Testing.d.ts +9 -1
  183. package/dist/Testing.d.ts.map +1 -1
  184. package/dist/Testing.js +30 -2
  185. package/dist/Testing.js.map +1 -1
  186. package/dist/Totp.d.ts +220 -0
  187. package/dist/Totp.d.ts.map +1 -0
  188. package/dist/Totp.js +477 -0
  189. package/dist/Totp.js.map +1 -0
  190. package/dist/TrustedDevice.d.ts +127 -0
  191. package/dist/TrustedDevice.d.ts.map +1 -0
  192. package/dist/TrustedDevice.js +252 -0
  193. package/dist/TrustedDevice.js.map +1 -0
  194. package/dist/WaitUntil.d.ts.map +1 -1
  195. package/dist/WaitUntil.js +1 -1
  196. package/dist/WaitUntil.js.map +1 -1
  197. package/dist/Webhook.d.ts +380 -0
  198. package/dist/Webhook.d.ts.map +1 -0
  199. package/dist/Webhook.js +592 -0
  200. package/dist/Webhook.js.map +1 -0
  201. package/dist/alchemy/cloudflare/Email.d.ts +25 -0
  202. package/dist/alchemy/cloudflare/Email.d.ts.map +1 -0
  203. package/dist/alchemy/cloudflare/Email.js +117 -0
  204. package/dist/alchemy/cloudflare/Email.js.map +1 -0
  205. package/dist/alchemy/cloudflare/RateLimitDurableObject.d.ts +34 -0
  206. package/dist/alchemy/cloudflare/RateLimitDurableObject.d.ts.map +1 -0
  207. package/dist/alchemy/cloudflare/RateLimitDurableObject.js +30 -0
  208. package/dist/alchemy/cloudflare/RateLimitDurableObject.js.map +1 -0
  209. package/dist/cloudflare/Email.d.ts +25 -0
  210. package/dist/cloudflare/Email.d.ts.map +1 -0
  211. package/dist/cloudflare/Email.js +153 -0
  212. package/dist/cloudflare/Email.js.map +1 -0
  213. package/dist/cloudflare/RateLimitDurableObject.d.ts +50 -0
  214. package/dist/cloudflare/RateLimitDurableObject.d.ts.map +1 -0
  215. package/dist/cloudflare/RateLimitDurableObject.js +51 -0
  216. package/dist/cloudflare/RateLimitDurableObject.js.map +1 -0
  217. package/dist/index.d.ts +25 -0
  218. package/dist/index.d.ts.map +1 -1
  219. package/dist/index.js +25 -0
  220. package/dist/index.js.map +1 -1
  221. package/dist/internal/RateLimitDurableObject.d.ts +32 -0
  222. package/dist/internal/RateLimitDurableObject.d.ts.map +1 -0
  223. package/dist/internal/RateLimitDurableObject.js +80 -0
  224. package/dist/internal/RateLimitDurableObject.js.map +1 -0
  225. package/migrations/0002_auth_password.sql +25 -0
  226. package/migrations/0003_auth_login_approval_review.sql +22 -0
  227. package/migrations/0004_auth_trusted_device.sql +12 -0
  228. package/migrations/0005_auth_passkey.sql +17 -0
  229. package/migrations/0006_auth_totp_factor.sql +17 -0
  230. package/migrations/0007_auth_recovery_code.sql +13 -0
  231. package/migrations/0008_auth_api_key.sql +17 -0
  232. package/migrations/0009_auth_refresh_token.sql +21 -0
  233. package/migrations/0010_auth_jwt_revocation.sql +8 -0
  234. package/migrations/0011_auth_oauth_account.sql +16 -0
  235. package/migrations/0012_auth_oauth_provider_token_vault.sql +20 -0
  236. package/migrations/0013_auth_security_timeline.sql +16 -0
  237. package/migrations/0014_auth_webhook_outbox.sql +16 -0
  238. package/migrations/0015_auth_webhook_replay.sql +7 -0
  239. package/migrations/0016_auth_login_risk_history.sql +23 -0
  240. package/migrations/0017_auth_audit_log.sql +17 -0
  241. package/migrations/0018_auth_domain_verification.sql +18 -0
  242. package/migrations/0019_auth_oauth_authorization_code.sql +17 -0
  243. package/migrations/0020_auth_oauth_provider_mode_token.sql +24 -0
  244. package/migrations/0021_auth_oauth_client_secret.sql +14 -0
  245. package/migrations/0022_auth_oauth_client.sql +15 -0
  246. package/migrations/0023_auth_oauth_consent.sql +15 -0
  247. package/package.json +162 -26
  248. package/dist/D1Kysely.d.ts +0 -40
  249. package/dist/D1Kysely.d.ts.map +0 -1
  250. package/dist/D1Kysely.js +0 -75
  251. package/dist/D1Kysely.js.map +0 -1
  252. package/dist/KyselyStorage.d.ts +0 -41
  253. package/dist/KyselyStorage.d.ts.map +0 -1
  254. package/dist/KyselyStorage.js +0 -176
  255. package/dist/KyselyStorage.js.map +0 -1
package/dist/OAuth.js ADDED
@@ -0,0 +1,3063 @@
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, OAuthClientId, 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 defaultOAuthAuthorizationCodeBytes = 32;
21
+ export const defaultOAuthAuthorizationCodeTtl = Duration.minutes(5);
22
+ export const defaultOAuthLinkConfirmationSecretBytes = 32;
23
+ export const defaultOAuthNonceBytes = 16;
24
+ export const defaultOAuthProviderModeAccessTokenBytes = 32;
25
+ export const defaultOAuthProviderModeRefreshTokenBytes = 32;
26
+ export const defaultOAuthProviderModeAccessTokenTtl = Duration.hours(1);
27
+ export const defaultOAuthProviderModeRefreshTokenTtl = Duration.days(30);
28
+ export const appleSignInProviderId = OAuthProviderId("apple");
29
+ export const appleSignInIssuer = "https://appleid.apple.com";
30
+ export const appleSignInAuthorizationEndpoint = "https://appleid.apple.com/auth/authorize";
31
+ export const appleSignInTokenEndpoint = "https://appleid.apple.com/auth/token";
32
+ export const appleSignInRevocationEndpoint = "https://appleid.apple.com/auth/revoke";
33
+ export const appleSignInJwksUri = "https://appleid.apple.com/auth/keys";
34
+ export const appleSignInPrivateRelayDomain = "privaterelay.appleid.com";
35
+ export const appleSignInDefaultScopes = ["openid", "email", "name"];
36
+ export const appleSignInDefaultAuthorizationParams = {
37
+ response_mode: "form_post",
38
+ };
39
+ export const githubOAuthProviderId = OAuthProviderId("github");
40
+ export const githubOAuthAuthorizationEndpoint = "https://github.com/login/oauth/authorize";
41
+ export const githubOAuthTokenEndpoint = "https://github.com/login/oauth/access_token";
42
+ export const githubOAuthUserEndpoint = "https://api.github.com/user";
43
+ export const githubOAuthEmailsEndpoint = "https://api.github.com/user/emails";
44
+ export const githubOAuthDefaultScopes = ["read:user", "user:email"];
45
+ export const githubOAuthApiVersion = "2022-11-28";
46
+ export const githubOAuthApiVersionHeader = {
47
+ "x-github-api-version": githubOAuthApiVersion,
48
+ };
49
+ export const googleOidcProviderId = OAuthProviderId("google");
50
+ export const googleOidcIssuer = "https://accounts.google.com";
51
+ export const googleOidcAuthorizationEndpoint = "https://accounts.google.com/o/oauth2/v2/auth";
52
+ export const googleOidcTokenEndpoint = "https://oauth2.googleapis.com/token";
53
+ export const googleOidcRevocationEndpoint = "https://oauth2.googleapis.com/revoke";
54
+ export const googleOidcUserInfoEndpoint = "https://openidconnect.googleapis.com/v1/userinfo";
55
+ export const googleOidcJwksUri = "https://www.googleapis.com/oauth2/v3/certs";
56
+ export const googleOidcDefaultScopes = ["openid", "email", "profile"];
57
+ export const googleOidcOfflineAccessAuthorizationParams = {
58
+ access_type: "offline",
59
+ prompt: "consent",
60
+ };
61
+ export const microsoftOidcProviderId = OAuthProviderId("microsoft");
62
+ export const microsoftOidcLoginBaseUrl = "https://login.microsoftonline.com";
63
+ export const microsoftOidcUserInfoEndpoint = "https://graph.microsoft.com/oidc/userinfo";
64
+ export const microsoftOidcDefaultScopes = ["openid", "email", "profile"];
65
+ export const microsoftOidcOfflineAccessScopes = [
66
+ "openid",
67
+ "email",
68
+ "profile",
69
+ "offline_access",
70
+ ];
71
+ export const genericOidcDefaultScopes = ["openid", "email", "profile"];
72
+ const microsoftOidcTenantPathSegment = (tenant) => encodeURIComponent(tenant.trim());
73
+ export const microsoftOidcAuthorizationEndpoint = (tenant) => `${microsoftOidcLoginBaseUrl}/${microsoftOidcTenantPathSegment(tenant)}/oauth2/v2.0/authorize`;
74
+ export const microsoftOidcTokenEndpoint = (tenant) => `${microsoftOidcLoginBaseUrl}/${microsoftOidcTenantPathSegment(tenant)}/oauth2/v2.0/token`;
75
+ export const microsoftOidcJwksUri = (tenant) => `${microsoftOidcLoginBaseUrl}/${microsoftOidcTenantPathSegment(tenant)}/discovery/v2.0/keys`;
76
+ export const microsoftOidcIssuer = (tenant) => `${microsoftOidcLoginBaseUrl}/${microsoftOidcTenantPathSegment(tenant)}/v2.0`;
77
+ export class OAuthProviderError extends Data.TaggedError("OAuthProviderError") {
78
+ }
79
+ export const OAuthClientSecretPrefix = Brand.nominal();
80
+ export const OAuthClientSecret = Brand.nominal();
81
+ export const OAuthClientSecretHash = Brand.nominal();
82
+ export const defaultOAuthClientSecretPrefixBytes = 8;
83
+ export const defaultOAuthClientSecretBytes = 32;
84
+ export class OAuthClientRegistryError extends Data.TaggedError("OAuthClientRegistryError") {
85
+ }
86
+ export class OAuthClientRegistry extends Context.Service()("auth/OAuthClientRegistry") {
87
+ }
88
+ (function (OAuthClientRegistry) {
89
+ OAuthClientRegistry.make = (service) => OAuthClientRegistry.of(service);
90
+ })(OAuthClientRegistry || (OAuthClientRegistry = {}));
91
+ export class OAuthClientStoreError extends Data.TaggedError("OAuthClientStoreError") {
92
+ }
93
+ export class OAuthClientStore extends Context.Service()("auth/OAuthClientStore") {
94
+ }
95
+ (function (OAuthClientStore) {
96
+ OAuthClientStore.make = (service) => OAuthClientStore.of(service);
97
+ })(OAuthClientStore || (OAuthClientStore = {}));
98
+ export class OAuthConsentStoreError extends Data.TaggedError("OAuthConsentStoreError") {
99
+ }
100
+ export class OAuthConsentStore extends Context.Service()("auth/OAuthConsentStore") {
101
+ }
102
+ (function (OAuthConsentStore) {
103
+ OAuthConsentStore.make = (service) => OAuthConsentStore.of(service);
104
+ })(OAuthConsentStore || (OAuthConsentStore = {}));
105
+ export class OAuthClientSecretError extends Data.TaggedError("OAuthClientSecretError") {
106
+ }
107
+ export class OAuthClientSecrets extends Context.Service()("auth/OAuthClientSecrets") {
108
+ }
109
+ (function (OAuthClientSecrets) {
110
+ OAuthClientSecrets.make = (service) => OAuthClientSecrets.of(service);
111
+ })(OAuthClientSecrets || (OAuthClientSecrets = {}));
112
+ export class OAuthClientSecretStoreError extends Data.TaggedError("OAuthClientSecretStoreError") {
113
+ }
114
+ export class OAuthClientSecretStore extends Context.Service()("auth/OAuthClientSecretStore") {
115
+ }
116
+ (function (OAuthClientSecretStore) {
117
+ OAuthClientSecretStore.make = (service) => OAuthClientSecretStore.of(service);
118
+ })(OAuthClientSecretStore || (OAuthClientSecretStore = {}));
119
+ export class OAuthClientSecretVerificationError extends Data.TaggedError("OAuthClientSecretVerificationError") {
120
+ }
121
+ export class OAuthClientSecretVerification extends Context.Service()("auth/OAuthClientSecretVerification") {
122
+ }
123
+ (function (OAuthClientSecretVerification) {
124
+ OAuthClientSecretVerification.make = (service) => OAuthClientSecretVerification.of(service);
125
+ })(OAuthClientSecretVerification || (OAuthClientSecretVerification = {}));
126
+ export class OAuthAuthorizationCodeStoreError extends Data.TaggedError("OAuthAuthorizationCodeStoreError") {
127
+ }
128
+ export class OAuthAuthorizationCodeStore extends Context.Service()("auth/OAuthAuthorizationCodeStore") {
129
+ }
130
+ (function (OAuthAuthorizationCodeStore) {
131
+ OAuthAuthorizationCodeStore.make = (service) => OAuthAuthorizationCodeStore.of(service);
132
+ })(OAuthAuthorizationCodeStore || (OAuthAuthorizationCodeStore = {}));
133
+ export class OAuthAuthorizationCodeGrantError extends Data.TaggedError("OAuthAuthorizationCodeGrantError") {
134
+ }
135
+ export class OAuthAuthorizationCodeGrant extends Context.Service()("auth/OAuthAuthorizationCodeGrant") {
136
+ }
137
+ (function (OAuthAuthorizationCodeGrant) {
138
+ OAuthAuthorizationCodeGrant.make = (service) => OAuthAuthorizationCodeGrant.of(service);
139
+ })(OAuthAuthorizationCodeGrant || (OAuthAuthorizationCodeGrant = {}));
140
+ export class OAuthAuthorizationCodeTokenIssuerError extends Data.TaggedError("OAuthAuthorizationCodeTokenIssuerError") {
141
+ }
142
+ export class OAuthAuthorizationCodeTokenIssuer extends Context.Service()("auth/OAuthAuthorizationCodeTokenIssuer") {
143
+ }
144
+ (function (OAuthAuthorizationCodeTokenIssuer) {
145
+ OAuthAuthorizationCodeTokenIssuer.make = (service) => OAuthAuthorizationCodeTokenIssuer.of(service);
146
+ })(OAuthAuthorizationCodeTokenIssuer || (OAuthAuthorizationCodeTokenIssuer = {}));
147
+ export class OAuthAuthorizationCodeTokenGrantError extends Data.TaggedError("OAuthAuthorizationCodeTokenGrantError") {
148
+ }
149
+ export class OAuthAuthorizationCodeTokenGrant extends Context.Service()("auth/OAuthAuthorizationCodeTokenGrant") {
150
+ }
151
+ (function (OAuthAuthorizationCodeTokenGrant) {
152
+ OAuthAuthorizationCodeTokenGrant.make = (service) => OAuthAuthorizationCodeTokenGrant.of(service);
153
+ })(OAuthAuthorizationCodeTokenGrant || (OAuthAuthorizationCodeTokenGrant = {}));
154
+ export class OAuthRefreshTokenIssuerError extends Data.TaggedError("OAuthRefreshTokenIssuerError") {
155
+ }
156
+ export class OAuthRefreshTokenIssuer extends Context.Service()("auth/OAuthRefreshTokenIssuer") {
157
+ }
158
+ (function (OAuthRefreshTokenIssuer) {
159
+ OAuthRefreshTokenIssuer.make = (service) => OAuthRefreshTokenIssuer.of(service);
160
+ })(OAuthRefreshTokenIssuer || (OAuthRefreshTokenIssuer = {}));
161
+ export class OAuthRefreshTokenGrantError extends Data.TaggedError("OAuthRefreshTokenGrantError") {
162
+ }
163
+ export class OAuthRefreshTokenGrant extends Context.Service()("auth/OAuthRefreshTokenGrant") {
164
+ }
165
+ (function (OAuthRefreshTokenGrant) {
166
+ OAuthRefreshTokenGrant.make = (service) => OAuthRefreshTokenGrant.of(service);
167
+ })(OAuthRefreshTokenGrant || (OAuthRefreshTokenGrant = {}));
168
+ export class OAuthClientCredentialsTokenIssuerError extends Data.TaggedError("OAuthClientCredentialsTokenIssuerError") {
169
+ }
170
+ export class OAuthClientCredentialsTokenIssuer extends Context.Service()("auth/OAuthClientCredentialsTokenIssuer") {
171
+ }
172
+ (function (OAuthClientCredentialsTokenIssuer) {
173
+ OAuthClientCredentialsTokenIssuer.make = (service) => OAuthClientCredentialsTokenIssuer.of(service);
174
+ })(OAuthClientCredentialsTokenIssuer || (OAuthClientCredentialsTokenIssuer = {}));
175
+ export class OAuthClientCredentialsGrantError extends Data.TaggedError("OAuthClientCredentialsGrantError") {
176
+ }
177
+ export class OAuthClientCredentialsGrant extends Context.Service()("auth/OAuthClientCredentialsGrant") {
178
+ }
179
+ (function (OAuthClientCredentialsGrant) {
180
+ OAuthClientCredentialsGrant.make = (service) => OAuthClientCredentialsGrant.of(service);
181
+ })(OAuthClientCredentialsGrant || (OAuthClientCredentialsGrant = {}));
182
+ export class OAuthTokenRevokerError extends Data.TaggedError("OAuthTokenRevokerError") {
183
+ }
184
+ export class OAuthTokenRevoker extends Context.Service()("auth/OAuthTokenRevoker") {
185
+ }
186
+ (function (OAuthTokenRevoker) {
187
+ OAuthTokenRevoker.make = (service) => OAuthTokenRevoker.of(service);
188
+ })(OAuthTokenRevoker || (OAuthTokenRevoker = {}));
189
+ export class OAuthTokenRevocationError extends Data.TaggedError("OAuthTokenRevocationError") {
190
+ }
191
+ export class OAuthTokenRevocation extends Context.Service()("auth/OAuthTokenRevocation") {
192
+ }
193
+ (function (OAuthTokenRevocation) {
194
+ OAuthTokenRevocation.make = (service) => OAuthTokenRevocation.of(service);
195
+ })(OAuthTokenRevocation || (OAuthTokenRevocation = {}));
196
+ export class OAuthTokenIntrospectorError extends Data.TaggedError("OAuthTokenIntrospectorError") {
197
+ }
198
+ export class OAuthTokenIntrospector extends Context.Service()("auth/OAuthTokenIntrospector") {
199
+ }
200
+ (function (OAuthTokenIntrospector) {
201
+ OAuthTokenIntrospector.make = (service) => OAuthTokenIntrospector.of(service);
202
+ })(OAuthTokenIntrospector || (OAuthTokenIntrospector = {}));
203
+ export class OAuthTokenIntrospectionError extends Data.TaggedError("OAuthTokenIntrospectionError") {
204
+ }
205
+ export class OAuthTokenIntrospection extends Context.Service()("auth/OAuthTokenIntrospection") {
206
+ }
207
+ (function (OAuthTokenIntrospection) {
208
+ OAuthTokenIntrospection.make = (service) => OAuthTokenIntrospection.of(service);
209
+ })(OAuthTokenIntrospection || (OAuthTokenIntrospection = {}));
210
+ export class OAuthProviderAuthorizationPolicyError extends Data.TaggedError("OAuthProviderAuthorizationPolicyError") {
211
+ }
212
+ export class OAuthProviderAuthorizationPolicy extends Context.Service()("auth/OAuthProviderAuthorizationPolicy") {
213
+ }
214
+ (function (OAuthProviderAuthorizationPolicy) {
215
+ OAuthProviderAuthorizationPolicy.make = (service) => OAuthProviderAuthorizationPolicy.of(service);
216
+ })(OAuthProviderAuthorizationPolicy || (OAuthProviderAuthorizationPolicy = {}));
217
+ export class OAuthProviderAuthorizationError extends Data.TaggedError("OAuthProviderAuthorizationError") {
218
+ }
219
+ export class OAuthProviderAuthorization extends Context.Service()("auth/OAuthProviderAuthorization") {
220
+ }
221
+ (function (OAuthProviderAuthorization) {
222
+ OAuthProviderAuthorization.make = (service) => OAuthProviderAuthorization.of(service);
223
+ })(OAuthProviderAuthorization || (OAuthProviderAuthorization = {}));
224
+ export class OAuthUserInfoError extends Data.TaggedError("OAuthUserInfoError") {
225
+ }
226
+ export class OAuthProviderModeTokenStoreError extends Data.TaggedError("OAuthProviderModeTokenStoreError") {
227
+ }
228
+ export class OAuthProviderModeTokenStore extends Context.Service()("auth/OAuthProviderModeTokenStore") {
229
+ }
230
+ (function (OAuthProviderModeTokenStore) {
231
+ OAuthProviderModeTokenStore.make = (service) => OAuthProviderModeTokenStore.of(service);
232
+ })(OAuthProviderModeTokenStore || (OAuthProviderModeTokenStore = {}));
233
+ export class OAuthProviderTokenVaultError extends Data.TaggedError("OAuthProviderTokenVaultError") {
234
+ }
235
+ export class OAuthProviderTokenVault extends Context.Service()("auth/OAuthProviderTokenVault") {
236
+ }
237
+ (function (OAuthProviderTokenVault) {
238
+ OAuthProviderTokenVault.make = (service) => OAuthProviderTokenVault.of(service);
239
+ })(OAuthProviderTokenVault || (OAuthProviderTokenVault = {}));
240
+ export class OAuthProviderTokenLifecycleError extends Data.TaggedError("OAuthProviderTokenLifecycleError") {
241
+ }
242
+ export class OAuthProviderTokenLifecycle extends Context.Service()("auth/OAuthProviderTokenLifecycle") {
243
+ }
244
+ (function (OAuthProviderTokenLifecycle) {
245
+ OAuthProviderTokenLifecycle.make = (service) => OAuthProviderTokenLifecycle.of(service);
246
+ })(OAuthProviderTokenLifecycle || (OAuthProviderTokenLifecycle = {}));
247
+ export class OidcIdTokenVerifierError extends Data.TaggedError("OidcIdTokenVerifierError") {
248
+ }
249
+ export class OidcIdTokenVerifier extends Context.Service()("auth/OidcIdTokenVerifier") {
250
+ }
251
+ (function (OidcIdTokenVerifier) {
252
+ OidcIdTokenVerifier.make = (service) => OidcIdTokenVerifier.of(service);
253
+ })(OidcIdTokenVerifier || (OidcIdTokenVerifier = {}));
254
+ export class OAuthProfileNormalizationError extends Data.TaggedError("OAuthProfileNormalizationError") {
255
+ }
256
+ export class OAuthProfileNormalizer extends Context.Service()("auth/OAuthProfileNormalizer") {
257
+ }
258
+ (function (OAuthProfileNormalizer) {
259
+ OAuthProfileNormalizer.make = (service) => OAuthProfileNormalizer.of(service);
260
+ })(OAuthProfileNormalizer || (OAuthProfileNormalizer = {}));
261
+ export class OAuthTokenExchangeError extends Data.TaggedError("OAuthTokenExchangeError") {
262
+ }
263
+ export class OAuthTokenExchange extends Context.Service()("auth/OAuthTokenExchange") {
264
+ }
265
+ (function (OAuthTokenExchange) {
266
+ OAuthTokenExchange.make = (service) => OAuthTokenExchange.of(service);
267
+ })(OAuthTokenExchange || (OAuthTokenExchange = {}));
268
+ export class OAuthProviders extends Context.Service()("auth/OAuthProviders") {
269
+ }
270
+ (function (OAuthProviders) {
271
+ OAuthProviders.make = (service) => OAuthProviders.of(service);
272
+ })(OAuthProviders || (OAuthProviders = {}));
273
+ export class OAuthStateError extends Data.TaggedError("OAuthStateError") {
274
+ }
275
+ export class OAuthState extends Context.Service()("auth/OAuthState") {
276
+ }
277
+ (function (OAuthState) {
278
+ OAuthState.make = (service) => OAuthState.of(service);
279
+ })(OAuthState || (OAuthState = {}));
280
+ export class OAuthAccountStoreError extends Data.TaggedError("OAuthAccountStoreError") {
281
+ }
282
+ export class OAuthAccountStore extends Context.Service()("auth/OAuthAccountStore") {
283
+ }
284
+ (function (OAuthAccountStore) {
285
+ OAuthAccountStore.make = (service) => OAuthAccountStore.of(service);
286
+ })(OAuthAccountStore || (OAuthAccountStore = {}));
287
+ export class OAuthAccountLinkingError extends Data.TaggedError("OAuthAccountLinkingError") {
288
+ }
289
+ export class OAuthAccountLinking extends Context.Service()("auth/OAuthAccountLinking") {
290
+ }
291
+ (function (OAuthAccountLinking) {
292
+ OAuthAccountLinking.make = (service) => OAuthAccountLinking.of(service);
293
+ })(OAuthAccountLinking || (OAuthAccountLinking = {}));
294
+ export class OAuthAccountUnlinkingError extends Data.TaggedError("OAuthAccountUnlinkingError") {
295
+ }
296
+ export class OAuthAccountUnlinking extends Context.Service()("auth/OAuthAccountUnlinking") {
297
+ }
298
+ (function (OAuthAccountUnlinking) {
299
+ OAuthAccountUnlinking.make = (service) => OAuthAccountUnlinking.of(service);
300
+ })(OAuthAccountUnlinking || (OAuthAccountUnlinking = {}));
301
+ export class OAuthLinkConfirmationError extends Data.TaggedError("OAuthLinkConfirmationError") {
302
+ }
303
+ export class OAuthLinkConfirmation extends Context.Service()("auth/OAuthLinkConfirmation") {
304
+ }
305
+ (function (OAuthLinkConfirmation) {
306
+ OAuthLinkConfirmation.make = (service) => OAuthLinkConfirmation.of(service);
307
+ })(OAuthLinkConfirmation || (OAuthLinkConfirmation = {}));
308
+ const oauthStateError = (operation, message, cause) => new OAuthStateError({ operation, message, cause });
309
+ const oauthAuthorizationCodeGrantError = (operation, message, cause) => new OAuthAuthorizationCodeGrantError({ operation, message, cause });
310
+ const oauthAuthorizationCodeTokenGrantError = (operation, message, cause) => new OAuthAuthorizationCodeTokenGrantError({ operation, message, cause });
311
+ const oauthAuthorizationCodeTokenIssuerError = (message, cause) => new OAuthAuthorizationCodeTokenIssuerError({
312
+ operation: "issue",
313
+ message,
314
+ ...(cause === undefined ? {} : { cause }),
315
+ });
316
+ const oauthRefreshTokenGrantError = (operation, message, cause) => new OAuthRefreshTokenGrantError({ operation, message, cause });
317
+ const oauthRefreshTokenIssuerError = (message, cause) => new OAuthRefreshTokenIssuerError({
318
+ operation: "issue",
319
+ message,
320
+ ...(cause === undefined ? {} : { cause }),
321
+ });
322
+ const oauthClientCredentialsGrantError = (operation, message, cause) => new OAuthClientCredentialsGrantError({ operation, message, cause });
323
+ const oauthClientCredentialsTokenIssuerError = (message, cause) => new OAuthClientCredentialsTokenIssuerError({
324
+ operation: "issue",
325
+ message,
326
+ ...(cause === undefined ? {} : { cause }),
327
+ });
328
+ const oauthClientRegistryError = (operation, message, cause) => new OAuthClientRegistryError({
329
+ operation,
330
+ message,
331
+ ...(cause === undefined ? {} : { cause }),
332
+ });
333
+ const oauthClientSecretError = (operation, message, cause) => new OAuthClientSecretError({
334
+ operation,
335
+ message,
336
+ ...(cause === undefined ? {} : { cause }),
337
+ });
338
+ const oauthClientSecretVerificationError = (message, cause) => new OAuthClientSecretVerificationError({
339
+ operation: "authenticate-client",
340
+ message,
341
+ ...(cause === undefined ? {} : { cause }),
342
+ });
343
+ const oauthTokenRevocationError = (operation, message, cause) => new OAuthTokenRevocationError({ operation, message, cause });
344
+ const oauthTokenIntrospectionError = (operation, message, cause) => new OAuthTokenIntrospectionError({ operation, message, cause });
345
+ const oauthProviderAuthorizationError = (operation, message, cause) => new OAuthProviderAuthorizationError({ operation, message, cause });
346
+ const oauthTokenExchangeError = (operation, message, cause) => new OAuthTokenExchangeError({ operation, message, cause });
347
+ const oauthProviderTokenLifecycleError = (operation, message, cause) => new OAuthProviderTokenLifecycleError({ operation, message, cause });
348
+ const oidcIdTokenVerifierError = (operation, message, cause) => new OidcIdTokenVerifierError({ operation, message, cause });
349
+ const oauthAccountLinkingError = (operation, message, cause) => new OAuthAccountLinkingError({ operation, message, cause });
350
+ const oauthAccountUnlinkingError = (operation, message, cause) => new OAuthAccountUnlinkingError({ operation, message, cause });
351
+ const oauthLinkConfirmationError = (operation, message, cause) => new OAuthLinkConfirmationError({ operation, message, cause });
352
+ const oauthProfileNormalizationError = (reason, message, cause) => new OAuthProfileNormalizationError({
353
+ operation: "normalize",
354
+ reason,
355
+ message,
356
+ cause,
357
+ });
358
+ const oauthJwksJwtError = (message, cause) => new JwtError({
359
+ operation: "keys",
360
+ message,
361
+ ...(cause === undefined ? {} : { cause }),
362
+ });
363
+ const oauthUserInfoError = (operation, message, cause) => new OAuthUserInfoError({
364
+ operation,
365
+ message,
366
+ ...(cause === undefined ? {} : { cause }),
367
+ });
368
+ const textEncoder = new TextEncoder();
369
+ const oauthJwksPrivateMembers = new Set([
370
+ "d",
371
+ "p",
372
+ "q",
373
+ "dp",
374
+ "dq",
375
+ "qi",
376
+ "oth",
377
+ "k",
378
+ ]);
379
+ const validateByteLength = (value, operation, label) => Number.isInteger(value) && value >= 16 && value <= 128
380
+ ? Effect.succeed(value)
381
+ : Effect.fail(oauthStateError(operation, `${label} byte length must be an integer between 16 and 128`));
382
+ const validateOAuthLinkConfirmationSecretBytes = (value) => Number.isInteger(value) && value >= 16 && value <= 128
383
+ ? Effect.succeed(value)
384
+ : Effect.fail(oauthLinkConfirmationError("start", "OAuth link confirmation secret byte length must be an integer between 16 and 128"));
385
+ const validateOAuthAuthorizationCodeBytes = (value) => Number.isInteger(value) && value >= 16 && value <= 128
386
+ ? Effect.succeed(value)
387
+ : Effect.fail(oauthAuthorizationCodeGrantError("issue", "OAuth authorization code byte length must be an integer between 16 and 128"));
388
+ const metadataValue = (value) => value === undefined ? undefined : structuredClone(value);
389
+ const validateOAuthClientSecretByteLength = (value, operation, label) => Number.isInteger(value) && value >= 4 && value <= 128
390
+ ? Effect.succeed(value)
391
+ : Effect.fail(oauthClientSecretError(operation, `${label} byte length must be an integer between 4 and 128`));
392
+ const parseOAuthClientSecretValue = (secret) => {
393
+ const value = secret.trim();
394
+ const parts = value.split(".");
395
+ if (parts.length !== 2 || parts[0] === "" || parts[1] === "") {
396
+ return Effect.fail(oauthClientSecretError("parse", "OAuth client secret must use '<prefix>.<secret>' format"));
397
+ }
398
+ return Effect.succeed(OAuthClientSecretPrefix(parts[0]));
399
+ };
400
+ const hashOAuthClientSecretValue = (dependencies, secret, operation) => dependencies.crypto.digestSha256({ data: `oauth-client-secret:${secret}` }).pipe(Effect.map((hash) => OAuthClientSecretHash(`sha256:${hash}`)), Effect.mapError((cause) => oauthClientSecretError(operation, "Failed to hash OAuth client secret", cause)));
401
+ export const makeOAuthClientSecrets = (dependencies) => OAuthClientSecrets.make({
402
+ generate: Effect.fn("auth.oauth.client_secret.generate")(function* (input) {
403
+ const prefixBytes = yield* validateOAuthClientSecretByteLength(input?.prefixBytes ?? defaultOAuthClientSecretPrefixBytes, "generate", "OAuth client secret prefix");
404
+ const secretBytes = yield* validateOAuthClientSecretByteLength(input?.secretBytes ?? defaultOAuthClientSecretBytes, "generate", "OAuth client secret");
405
+ const prefix = yield* dependencies.crypto.randomToken(prefixBytes).pipe(Effect.map(OAuthClientSecretPrefix), Effect.mapError((cause) => oauthClientSecretError("generate", "Failed to generate OAuth client secret prefix", cause)));
406
+ const secret = yield* dependencies.crypto.randomToken(secretBytes).pipe(Effect.map((value) => OAuthClientSecret(`${prefix}.${value}`)), Effect.mapError((cause) => oauthClientSecretError("generate", "Failed to generate OAuth client secret", cause)));
407
+ return {
408
+ prefix,
409
+ secret: Redacted.make(secret),
410
+ };
411
+ }),
412
+ parse: Effect.fn("auth.oauth.client_secret.parse")(function* (secret) {
413
+ return yield* parseOAuthClientSecretValue(Redacted.value(secret));
414
+ }),
415
+ hash: Effect.fn("auth.oauth.client_secret.hash")(function* (input) {
416
+ const secret = Redacted.value(input.secret).trim();
417
+ yield* parseOAuthClientSecretValue(secret).pipe(Effect.mapError((cause) => oauthClientSecretError("hash", "Cannot hash malformed OAuth client secret", cause)));
418
+ return yield* hashOAuthClientSecretValue(dependencies, secret, "hash");
419
+ }),
420
+ verify: Effect.fn("auth.oauth.client_secret.verify")(function* (input) {
421
+ const secret = Redacted.value(input.secret).trim();
422
+ const prefix = yield* parseOAuthClientSecretValue(secret).pipe(Effect.catchTag("OAuthClientSecretError", () => Effect.succeed(undefined)));
423
+ if (prefix === undefined) {
424
+ return false;
425
+ }
426
+ const hash = yield* hashOAuthClientSecretValue(dependencies, secret, "verify");
427
+ return yield* dependencies.crypto
428
+ .timingSafeEqual({
429
+ left: textEncoder.encode(hash),
430
+ right: textEncoder.encode(input.hash),
431
+ })
432
+ .pipe(Effect.mapError((cause) => oauthClientSecretError("verify", "Failed to compare OAuth client secret hashes", cause)));
433
+ }),
434
+ });
435
+ export const makeAppleSignInProvider = (input) => ({
436
+ id: input.id ?? appleSignInProviderId,
437
+ clientId: input.clientId,
438
+ authorizationEndpoint: appleSignInAuthorizationEndpoint,
439
+ tokenEndpoint: appleSignInTokenEndpoint,
440
+ revocationEndpoint: appleSignInRevocationEndpoint,
441
+ jwksUri: appleSignInJwksUri,
442
+ issuer: appleSignInIssuer,
443
+ redirectUri: input.redirectUri,
444
+ scopes: [...(input.scopes ?? appleSignInDefaultScopes)],
445
+ authorizationParams: {
446
+ ...appleSignInDefaultAuthorizationParams,
447
+ ...input.authorizationParams,
448
+ },
449
+ ...(input.metadata === undefined ? {} : { metadata: metadataValue(input.metadata) }),
450
+ });
451
+ export const makeGithubOAuthProvider = (input) => ({
452
+ id: input.id ?? githubOAuthProviderId,
453
+ clientId: input.clientId,
454
+ authorizationEndpoint: githubOAuthAuthorizationEndpoint,
455
+ tokenEndpoint: githubOAuthTokenEndpoint,
456
+ userInfoEndpoint: githubOAuthUserEndpoint,
457
+ redirectUri: input.redirectUri,
458
+ scopes: [...(input.scopes ?? githubOAuthDefaultScopes)],
459
+ ...(input.authorizationParams === undefined
460
+ ? {}
461
+ : { authorizationParams: { ...input.authorizationParams } }),
462
+ ...(input.metadata === undefined ? {} : { metadata: metadataValue(input.metadata) }),
463
+ });
464
+ export const makeGoogleOidcProvider = (input) => ({
465
+ id: input.id ?? googleOidcProviderId,
466
+ clientId: input.clientId,
467
+ authorizationEndpoint: googleOidcAuthorizationEndpoint,
468
+ tokenEndpoint: googleOidcTokenEndpoint,
469
+ revocationEndpoint: googleOidcRevocationEndpoint,
470
+ userInfoEndpoint: googleOidcUserInfoEndpoint,
471
+ jwksUri: googleOidcJwksUri,
472
+ issuer: googleOidcIssuer,
473
+ redirectUri: input.redirectUri,
474
+ scopes: [...(input.scopes ?? googleOidcDefaultScopes)],
475
+ ...(input.authorizationParams === undefined
476
+ ? {}
477
+ : { authorizationParams: { ...input.authorizationParams } }),
478
+ ...(input.metadata === undefined ? {} : { metadata: metadataValue(input.metadata) }),
479
+ });
480
+ export const makeMicrosoftOidcProvider = (input) => ({
481
+ id: input.id ?? microsoftOidcProviderId,
482
+ clientId: input.clientId,
483
+ authorizationEndpoint: microsoftOidcAuthorizationEndpoint(input.tenant),
484
+ tokenEndpoint: microsoftOidcTokenEndpoint(input.tenant),
485
+ userInfoEndpoint: microsoftOidcUserInfoEndpoint,
486
+ jwksUri: microsoftOidcJwksUri(input.tenant),
487
+ issuer: microsoftOidcIssuer(input.tenant),
488
+ redirectUri: input.redirectUri,
489
+ scopes: [...(input.scopes ?? microsoftOidcDefaultScopes)],
490
+ ...(input.authorizationParams === undefined
491
+ ? {}
492
+ : { authorizationParams: { ...input.authorizationParams } }),
493
+ ...(input.metadata === undefined ? {} : { metadata: metadataValue(input.metadata) }),
494
+ });
495
+ export const makeGenericOidcProvider = (input) => ({
496
+ id: input.id,
497
+ clientId: input.clientId,
498
+ authorizationEndpoint: input.authorizationEndpoint,
499
+ tokenEndpoint: input.tokenEndpoint,
500
+ issuer: input.issuer,
501
+ redirectUri: input.redirectUri,
502
+ scopes: [...(input.scopes ?? genericOidcDefaultScopes)],
503
+ ...(input.userInfoEndpoint === undefined
504
+ ? {}
505
+ : { userInfoEndpoint: input.userInfoEndpoint }),
506
+ ...(input.jwksUri === undefined ? {} : { jwksUri: input.jwksUri }),
507
+ ...(input.revocationEndpoint === undefined
508
+ ? {}
509
+ : { revocationEndpoint: input.revocationEndpoint }),
510
+ ...(input.authorizationParams === undefined
511
+ ? {}
512
+ : { authorizationParams: { ...input.authorizationParams } }),
513
+ ...(input.metadata === undefined ? {} : { metadata: metadataValue(input.metadata) }),
514
+ });
515
+ const oauthClientSecretPost = (clientSecret) => ({
516
+ method: "client_secret_post",
517
+ clientSecret,
518
+ });
519
+ export const makeGoogleOidcProviderTokenRefreshInput = (input) => ({
520
+ accountId: input.accountId,
521
+ userId: input.userId,
522
+ providerId: input.provider.id,
523
+ providerAccountId: input.providerAccountId,
524
+ provider: input.provider,
525
+ scopes: [...(input.scopes ?? googleOidcDefaultScopes)],
526
+ ...(input.now === undefined ? {} : { now: input.now }),
527
+ ...(input.clientSecret === undefined
528
+ ? {}
529
+ : { clientAuthentication: oauthClientSecretPost(input.clientSecret) }),
530
+ ...(input.params === undefined ? {} : { params: input.params }),
531
+ });
532
+ export const makeGoogleOidcProviderTokenRevokeInput = (input) => ({
533
+ accountId: input.accountId,
534
+ provider: input.provider,
535
+ reason: input.reason,
536
+ ...(input.now === undefined ? {} : { now: input.now }),
537
+ ...(input.clientSecret === undefined
538
+ ? {}
539
+ : { clientAuthentication: oauthClientSecretPost(input.clientSecret) }),
540
+ ...(input.params === undefined ? {} : { params: input.params }),
541
+ });
542
+ export const makeMicrosoftOidcProviderTokenRefreshInput = (input) => ({
543
+ accountId: input.accountId,
544
+ userId: input.userId,
545
+ providerId: input.provider.id,
546
+ providerAccountId: input.providerAccountId,
547
+ provider: input.provider,
548
+ scopes: [...(input.scopes ?? microsoftOidcOfflineAccessScopes)],
549
+ ...(input.now === undefined ? {} : { now: input.now }),
550
+ ...(input.clientSecret === undefined
551
+ ? {}
552
+ : { clientAuthentication: oauthClientSecretPost(input.clientSecret) }),
553
+ ...(input.params === undefined ? {} : { params: input.params }),
554
+ });
555
+ export const makeAppleSignInProviderTokenRefreshInput = (input) => ({
556
+ accountId: input.accountId,
557
+ userId: input.userId,
558
+ providerId: input.provider.id,
559
+ providerAccountId: input.providerAccountId,
560
+ provider: input.provider,
561
+ clientAuthentication: oauthClientSecretPost(input.clientSecret),
562
+ ...(input.scopes === undefined ? {} : { scopes: input.scopes }),
563
+ ...(input.now === undefined ? {} : { now: input.now }),
564
+ ...(input.params === undefined ? {} : { params: input.params }),
565
+ });
566
+ export const makeAppleSignInProviderTokenRevokeInput = (input) => ({
567
+ accountId: input.accountId,
568
+ provider: input.provider,
569
+ reason: input.reason,
570
+ clientAuthentication: oauthClientSecretPost(input.clientSecret),
571
+ ...(input.now === undefined ? {} : { now: input.now }),
572
+ ...(input.params === undefined ? {} : { params: input.params }),
573
+ });
574
+ const cloneProviderConfig = (provider) => ({
575
+ ...provider,
576
+ ...(provider.scopes === undefined ? {} : { scopes: [...provider.scopes] }),
577
+ ...(provider.authorizationParams === undefined
578
+ ? {}
579
+ : { authorizationParams: { ...provider.authorizationParams } }),
580
+ ...(provider.metadata === undefined ? {} : { metadata: metadataValue(provider.metadata) }),
581
+ });
582
+ const cloneOAuthClientRecord = (client) => ({
583
+ ...client,
584
+ redirectUris: [...client.redirectUris],
585
+ allowedGrantTypes: [...client.allowedGrantTypes],
586
+ allowedResponseTypes: [...client.allowedResponseTypes],
587
+ ...(client.allowedScopes === undefined
588
+ ? {}
589
+ : { allowedScopes: [...client.allowedScopes] }),
590
+ ...(client.metadata === undefined ? {} : { metadata: metadataValue(client.metadata) }),
591
+ });
592
+ const validateOAuthClientRedirectUriRecord = (client, input) => {
593
+ if (client === undefined) {
594
+ return { valid: false, reason: "unknown_client" };
595
+ }
596
+ if (client.status !== "active") {
597
+ return { valid: false, reason: "disabled_client" };
598
+ }
599
+ const responseType = input.responseType ?? "code";
600
+ if (!client.allowedResponseTypes.includes(responseType)) {
601
+ return { valid: false, reason: "unsupported_response_type" };
602
+ }
603
+ if (input.grantType !== undefined &&
604
+ !client.allowedGrantTypes.includes(input.grantType)) {
605
+ return { valid: false, reason: "unsupported_grant_type" };
606
+ }
607
+ if (!client.redirectUris.includes(input.redirectUri)) {
608
+ return { valid: false, reason: "redirect_uri_mismatch" };
609
+ }
610
+ return {
611
+ valid: true,
612
+ client: cloneOAuthClientRecord(client),
613
+ };
614
+ };
615
+ const cloneOAuthConsentRecord = (record) => ({
616
+ userId: record.userId,
617
+ clientId: record.clientId,
618
+ scopes: [...record.scopes],
619
+ grantedAt: record.grantedAt,
620
+ ...(record.expiresAt === undefined ? {} : { expiresAt: record.expiresAt }),
621
+ ...(record.revokedAt === undefined ? {} : { revokedAt: record.revokedAt }),
622
+ ...(record.metadata === undefined
623
+ ? {}
624
+ : { metadata: metadataValue(record.metadata) }),
625
+ });
626
+ const oauthConsentCoversScopes = (record, scopes = []) => scopes.every((scope) => record.scopes.includes(scope));
627
+ const isOAuthConsentActive = (record, now, scopes = []) => record.revokedAt === undefined &&
628
+ (record.expiresAt === undefined || Number(record.expiresAt) > Number(now)) &&
629
+ oauthConsentCoversScopes(record, scopes);
630
+ const cloneOAuthClientSecretRecord = (record) => ({
631
+ ...record,
632
+ authenticationMethods: [...record.authenticationMethods],
633
+ ...(record.metadata === undefined ? {} : { metadata: metadataValue(record.metadata) }),
634
+ });
635
+ const cloneAuthorizationCodeRecord = (record) => ({
636
+ ...record,
637
+ scopes: [...record.scopes],
638
+ ...(record.metadata === undefined ? {} : { metadata: metadataValue(record.metadata) }),
639
+ });
640
+ const cloneOAuthTokenResponse = (tokens) => ({
641
+ accessToken: tokens.accessToken,
642
+ tokenType: tokens.tokenType,
643
+ ...(tokens.expiresIn === undefined ? {} : { expiresIn: tokens.expiresIn }),
644
+ ...(tokens.refreshToken === undefined ? {} : { refreshToken: tokens.refreshToken }),
645
+ ...(tokens.idToken === undefined ? {} : { idToken: tokens.idToken }),
646
+ ...(tokens.scopes === undefined ? {} : { scopes: [...tokens.scopes] }),
647
+ ...(tokens.extra === undefined ? {} : { extra: metadataValue(tokens.extra) }),
648
+ });
649
+ const cloneOAuthTokenIntrospectionResponse = (response) => response.active
650
+ ? {
651
+ active: true,
652
+ ...(response.scopes === undefined ? {} : { scopes: [...response.scopes] }),
653
+ ...(response.clientId === undefined ? {} : { clientId: response.clientId }),
654
+ ...(response.username === undefined ? {} : { username: response.username }),
655
+ ...(response.tokenType === undefined ? {} : { tokenType: response.tokenType }),
656
+ ...(response.expiresAt === undefined ? {} : { expiresAt: response.expiresAt }),
657
+ ...(response.issuedAt === undefined ? {} : { issuedAt: response.issuedAt }),
658
+ ...(response.notBefore === undefined ? {} : { notBefore: response.notBefore }),
659
+ ...(response.subject === undefined ? {} : { subject: response.subject }),
660
+ ...(response.audience === undefined
661
+ ? {}
662
+ : {
663
+ audience: Array.isArray(response.audience)
664
+ ? [...response.audience]
665
+ : response.audience,
666
+ }),
667
+ ...(response.issuer === undefined ? {} : { issuer: response.issuer }),
668
+ ...(response.jwtId === undefined ? {} : { jwtId: response.jwtId }),
669
+ ...(response.extra === undefined ? {} : { extra: metadataValue(response.extra) }),
670
+ }
671
+ : { active: false };
672
+ const cloneOAuthProviderModeTokenRecord = (record) => ({
673
+ ...record,
674
+ scopes: [...record.scopes],
675
+ ...(record.audience === undefined
676
+ ? {}
677
+ : {
678
+ audience: Array.isArray(record.audience)
679
+ ? [...record.audience]
680
+ : record.audience,
681
+ }),
682
+ ...(record.metadata === undefined ? {} : { metadata: metadataValue(record.metadata) }),
683
+ });
684
+ const consumedAuthorizationCode = (record, consumedAt) => ({
685
+ clientId: record.clientId,
686
+ subject: record.subject,
687
+ redirectUri: record.redirectUri,
688
+ scopes: [...record.scopes],
689
+ ...(record.codeChallenge === undefined
690
+ ? {}
691
+ : { codeChallenge: record.codeChallenge }),
692
+ ...(record.codeChallengeMethod === undefined
693
+ ? {}
694
+ : { codeChallengeMethod: record.codeChallengeMethod }),
695
+ issuedAt: record.issuedAt,
696
+ expiresAt: record.expiresAt,
697
+ consumedAt,
698
+ ...(record.metadata === undefined ? {} : { metadata: metadataValue(record.metadata) }),
699
+ });
700
+ const issuedAuthorizationCode = (input) => ({
701
+ code: Redacted.make(input.code),
702
+ clientId: input.record.clientId,
703
+ subject: input.record.subject,
704
+ redirectUri: input.record.redirectUri,
705
+ scopes: [...input.record.scopes],
706
+ ...(input.record.codeChallenge === undefined
707
+ ? {}
708
+ : { codeChallenge: input.record.codeChallenge }),
709
+ ...(input.record.codeChallengeMethod === undefined
710
+ ? {}
711
+ : { codeChallengeMethod: input.record.codeChallengeMethod }),
712
+ issuedAt: input.record.issuedAt,
713
+ expiresAt: input.record.expiresAt,
714
+ ...(input.record.metadata === undefined
715
+ ? {}
716
+ : { metadata: metadataValue(input.record.metadata) }),
717
+ });
718
+ const encodeOAuthState = (input) => OAuthStateToken(`${input.challengeId}.${Redacted.value(input.secret)}`);
719
+ export const parseOAuthState = (state) => {
720
+ const parts = String(state).split(".");
721
+ return parts.length === 2 && parts[0] !== "" && parts[1] !== ""
722
+ ? Effect.succeed({
723
+ challengeId: ChallengeId(parts[0]),
724
+ secret: Redacted.make(parts[1]),
725
+ })
726
+ : Effect.fail(oauthStateError("parse", "OAuth state must use '<id>.<secret>' format"));
727
+ };
728
+ const stringArrayFromUnknown = (value) => Array.isArray(value) && value.every((item) => typeof item === "string")
729
+ ? value
730
+ : [];
731
+ const stringFromUnknown = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
732
+ const nonEmptyStringFromUnknown = (value) => typeof value === "string" && value.trim() !== "" ? value : undefined;
733
+ export const githubOAuthVerifiedEmail = (emails) => {
734
+ for (const email of emails) {
735
+ if (email.primary === true && email.verified === true) {
736
+ const value = nonEmptyStringFromUnknown(email.email);
737
+ if (value !== undefined) {
738
+ return value;
739
+ }
740
+ }
741
+ }
742
+ for (const email of emails) {
743
+ if (email.verified === true) {
744
+ const value = nonEmptyStringFromUnknown(email.email);
745
+ if (value !== undefined) {
746
+ return value;
747
+ }
748
+ }
749
+ }
750
+ return undefined;
751
+ };
752
+ export const googleOidcHostedDomain = (value) => nonEmptyStringFromUnknown(value.hd);
753
+ export const microsoftOidcTenantId = (value) => nonEmptyStringFromUnknown(value.tid);
754
+ export const microsoftOidcObjectId = (value) => nonEmptyStringFromUnknown(value.oid);
755
+ export const microsoftOidcAccountId = (value) => {
756
+ const tenantId = microsoftOidcTenantId(value);
757
+ const objectId = microsoftOidcObjectId(value);
758
+ return tenantId === undefined || objectId === undefined
759
+ ? undefined
760
+ : `${tenantId}:${objectId}`;
761
+ };
762
+ const optionalRecord = (value) => (Object.keys(value).length === 0 ? undefined : value);
763
+ const recordFromUnknown = (value) => typeof value === "object" && value !== null && !Array.isArray(value)
764
+ ? value
765
+ : undefined;
766
+ const isJwtAlgorithm = (value) => typeof value === "string" && JwtAlgorithms.includes(value);
767
+ const isPublicJwksAlgorithm = (value) => isJwtAlgorithm(value) && value !== "HS256";
768
+ const booleanFromUnknown = (value) => typeof value === "boolean" ? value : undefined;
769
+ const emailFromUnknown = (value) => {
770
+ const email = nonEmptyStringFromUnknown(value);
771
+ return email === undefined ? undefined : Email(email);
772
+ };
773
+ const appleSignInBooleanClaim = (value) => {
774
+ if (typeof value === "boolean") {
775
+ return value;
776
+ }
777
+ if (value === "true") {
778
+ return true;
779
+ }
780
+ if (value === "false") {
781
+ return false;
782
+ }
783
+ return undefined;
784
+ };
785
+ export const decodeAppleSignInAuthorizationUser = (value) => {
786
+ const decoded = typeof value === "string"
787
+ ? (() => {
788
+ try {
789
+ return JSON.parse(value);
790
+ }
791
+ catch {
792
+ return undefined;
793
+ }
794
+ })()
795
+ : value;
796
+ const record = recordFromUnknown(decoded);
797
+ if (record === undefined) {
798
+ return undefined;
799
+ }
800
+ const nameRecord = recordFromUnknown(record.name);
801
+ const firstName = nonEmptyStringFromUnknown(nameRecord?.firstName);
802
+ const lastName = nonEmptyStringFromUnknown(nameRecord?.lastName);
803
+ const name = firstName === undefined && lastName === undefined
804
+ ? undefined
805
+ : {
806
+ ...(firstName === undefined ? {} : { firstName }),
807
+ ...(lastName === undefined ? {} : { lastName }),
808
+ };
809
+ const email = nonEmptyStringFromUnknown(record.email);
810
+ return email === undefined && name === undefined
811
+ ? undefined
812
+ : {
813
+ ...(email === undefined ? {} : { email }),
814
+ ...(name === undefined ? {} : { name }),
815
+ };
816
+ };
817
+ export const appleSignInAuthorizationUserFullName = (user) => {
818
+ const firstName = nonEmptyStringFromUnknown(user?.name?.firstName);
819
+ const lastName = nonEmptyStringFromUnknown(user?.name?.lastName);
820
+ const parts = [firstName, lastName].filter((part) => part !== undefined);
821
+ return parts.length === 0 ? undefined : parts.join(" ");
822
+ };
823
+ export const appleSignInAuthorizationUserEmail = (user) => emailFromUnknown(user?.email);
824
+ export const appleSignInIsPrivateEmail = (input) => {
825
+ const claimValue = appleSignInBooleanClaim(input.claims?.is_private_email);
826
+ if (claimValue !== undefined) {
827
+ return claimValue;
828
+ }
829
+ const email = nonEmptyStringFromUnknown(input.email);
830
+ return email === undefined
831
+ ? undefined
832
+ : email.toLowerCase().endsWith(`@${appleSignInPrivateRelayDomain}`);
833
+ };
834
+ const firstString = (...values) => {
835
+ for (const value of values) {
836
+ const stringValue = nonEmptyStringFromUnknown(value);
837
+ if (stringValue !== undefined) {
838
+ return stringValue;
839
+ }
840
+ }
841
+ return undefined;
842
+ };
843
+ const optionalStringArrayFromUnknown = (value) => Array.isArray(value) && value.every((item) => typeof item === "string")
844
+ ? value
845
+ : undefined;
846
+ const numberFromUnknown = (value) => {
847
+ if (typeof value === "number" && Number.isFinite(value)) {
848
+ return value;
849
+ }
850
+ if (typeof value === "string" && value.trim() !== "") {
851
+ const parsed = Number(value);
852
+ return Number.isFinite(parsed) ? parsed : undefined;
853
+ }
854
+ return undefined;
855
+ };
856
+ const isTokenResponseRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
857
+ const scopeValues = (value) => {
858
+ if (typeof value !== "string" || value.trim() === "") {
859
+ return undefined;
860
+ }
861
+ return value.trim().split(/\s+/);
862
+ };
863
+ const audienceValues = (value) => {
864
+ if (typeof value === "string") {
865
+ return [value];
866
+ }
867
+ if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
868
+ return value;
869
+ }
870
+ return [];
871
+ };
872
+ const oidcIdTokenClaims = (claims, input) => {
873
+ if (nonEmptyStringFromUnknown(claims.sub) === undefined) {
874
+ return "missing_subject";
875
+ }
876
+ if (typeof claims.exp !== "number" || !Number.isFinite(claims.exp)) {
877
+ return "missing_expiration";
878
+ }
879
+ if (typeof claims.iat !== "number" || !Number.isFinite(claims.iat)) {
880
+ return "missing_issued_at";
881
+ }
882
+ const audiences = audienceValues(claims.aud);
883
+ const authorizedParty = input.authorizedParty ?? input.provider.clientId;
884
+ if (audiences.length > 1 && claims.azp === undefined) {
885
+ return "missing_authorized_party";
886
+ }
887
+ if (claims.azp !== undefined && claims.azp !== authorizedParty) {
888
+ return "authorized_party_mismatch";
889
+ }
890
+ if (input.nonce !== undefined && claims.nonce !== String(input.nonce)) {
891
+ return "nonce_mismatch";
892
+ }
893
+ return claims;
894
+ };
895
+ const tokenResponseExtra = (value) => {
896
+ const extra = Object.fromEntries(Object.entries(value).filter(([key]) => ![
897
+ "access_token",
898
+ "token_type",
899
+ "expires_in",
900
+ "refresh_token",
901
+ "id_token",
902
+ "scope",
903
+ ].includes(key)));
904
+ return Object.keys(extra).length === 0 ? undefined : extra;
905
+ };
906
+ const cloneVerifiedOAuthIdentity = (identity) => ({
907
+ ...identity,
908
+ ...(identity.metadata === undefined
909
+ ? {}
910
+ : { metadata: metadataValue(identity.metadata) }),
911
+ });
912
+ const oauthLinkConfirmationChallengeType = "oauth-link-confirmation";
913
+ const oauthLinkConfirmationMetadata = (input) => ({
914
+ userId: input.userId,
915
+ providerId: input.identity.providerId,
916
+ providerAccountId: input.identity.providerAccountId,
917
+ ...(input.identity.email === undefined ? {} : { email: input.identity.email }),
918
+ ...(input.identity.emailVerified === undefined
919
+ ? {}
920
+ : { emailVerified: input.identity.emailVerified }),
921
+ ...(input.identity.metadata === undefined
922
+ ? {}
923
+ : { identityMetadata: metadataValue(input.identity.metadata) }),
924
+ reason: input.reason,
925
+ ...(input.metadata === undefined ? {} : { metadata: metadataValue(input.metadata) }),
926
+ });
927
+ const pendingOAuthLinkConfirmationFromChallenge = (challenge, operation) => {
928
+ const metadata = challenge.metadata ?? {};
929
+ const userId = stringFromUnknown(metadata.userId);
930
+ const providerId = stringFromUnknown(metadata.providerId);
931
+ const providerAccountId = stringFromUnknown(metadata.providerAccountId);
932
+ if (userId === undefined ||
933
+ userId !== challenge.subject ||
934
+ providerId === undefined ||
935
+ providerAccountId === undefined) {
936
+ return Effect.fail(oauthLinkConfirmationError(operation, "OAuth link confirmation challenge is missing required metadata"));
937
+ }
938
+ const email = emailFromUnknown(metadata.email);
939
+ const emailVerified = booleanFromUnknown(metadata.emailVerified);
940
+ const identityMetadata = recordFromUnknown(metadata.identityMetadata);
941
+ const appMetadata = recordFromUnknown(metadata.metadata);
942
+ return Effect.succeed({
943
+ challengeId: challenge.id,
944
+ userId: UserId(userId),
945
+ identity: {
946
+ providerId: OAuthProviderId(providerId),
947
+ providerAccountId,
948
+ ...(email === undefined ? {} : { email }),
949
+ ...(emailVerified === undefined ? {} : { emailVerified }),
950
+ ...(identityMetadata === undefined
951
+ ? {}
952
+ : { metadata: metadataValue(identityMetadata) }),
953
+ },
954
+ reason: stringFromUnknown(metadata.reason) ??
955
+ "manual",
956
+ expiresAt: challenge.expiresAt,
957
+ ...(appMetadata === undefined ? {} : { metadata: metadataValue(appMetadata) }),
958
+ });
959
+ };
960
+ const confirmedOAuthLinkConfirmationFromPending = (input) => ({
961
+ challengeId: input.pending.challengeId,
962
+ userId: input.pending.userId,
963
+ identity: cloneVerifiedOAuthIdentity(input.pending.identity),
964
+ reason: input.pending.reason,
965
+ ...(input.existingAccount === undefined
966
+ ? {}
967
+ : { existingAccount: input.existingAccount }),
968
+ ...(input.pending.metadata === undefined
969
+ ? {}
970
+ : { metadata: metadataValue(input.pending.metadata) }),
971
+ });
972
+ const userInfoRecord = (value) => {
973
+ if (value === undefined) {
974
+ return Effect.succeed(undefined);
975
+ }
976
+ const record = recordFromUnknown(value);
977
+ return record === undefined
978
+ ? Effect.fail(oauthProfileNormalizationError("invalid_userinfo", "OIDC userinfo response must be an object"))
979
+ : Effect.succeed(record);
980
+ };
981
+ const makeOAuthProfile = (input) => ({
982
+ providerId: input.providerId,
983
+ providerAccountId: input.providerAccountId,
984
+ ...(input.email === undefined ? {} : { email: input.email }),
985
+ ...(input.emailVerified === undefined
986
+ ? {}
987
+ : { emailVerified: input.emailVerified }),
988
+ ...(input.name === undefined ? {} : { name: input.name }),
989
+ ...(input.username === undefined ? {} : { username: input.username }),
990
+ ...(input.avatarUrl === undefined ? {} : { avatarUrl: input.avatarUrl }),
991
+ ...(input.metadata === undefined ? {} : { metadata: metadataValue(input.metadata) }),
992
+ });
993
+ const oauthJwksPublicJwkFromUnknown = (value) => {
994
+ const record = recordFromUnknown(value);
995
+ const kty = record === undefined ? undefined : nonEmptyStringFromUnknown(record.kty);
996
+ if (record === undefined || kty === undefined) {
997
+ return undefined;
998
+ }
999
+ const jwk = {};
1000
+ for (const [name, member] of Object.entries(record)) {
1001
+ if (!oauthJwksPrivateMembers.has(name) && member !== undefined) {
1002
+ jwk[name] = member;
1003
+ }
1004
+ }
1005
+ return {
1006
+ ...jwk,
1007
+ kty,
1008
+ };
1009
+ };
1010
+ const oauthJwksAllowsSignatureUse = (jwk) => {
1011
+ const use = nonEmptyStringFromUnknown(jwk.use);
1012
+ const keyOps = optionalStringArrayFromUnknown(jwk.key_ops);
1013
+ return ((use === undefined || use === "sig") &&
1014
+ (keyOps === undefined || keyOps.includes("verify")));
1015
+ };
1016
+ const oauthJwksDefaultAlgorithms = (jwk) => {
1017
+ switch (jwk.kty) {
1018
+ case "RSA": {
1019
+ return ["RS256"];
1020
+ }
1021
+ case "EC": {
1022
+ return ["ES256"];
1023
+ }
1024
+ case "OKP": {
1025
+ return ["EdDSA"];
1026
+ }
1027
+ default: {
1028
+ return [];
1029
+ }
1030
+ }
1031
+ };
1032
+ const oauthJwksAlgorithms = (jwk, allowedAlgorithms) => {
1033
+ const jwkAlgorithm = nonEmptyStringFromUnknown(jwk.alg);
1034
+ const algorithms = jwkAlgorithm === undefined
1035
+ ? oauthJwksDefaultAlgorithms(jwk)
1036
+ : isPublicJwksAlgorithm(jwkAlgorithm)
1037
+ ? [jwkAlgorithm]
1038
+ : [];
1039
+ if (allowedAlgorithms === undefined) {
1040
+ return algorithms;
1041
+ }
1042
+ return algorithms.filter((algorithm) => allowedAlgorithms.includes(algorithm));
1043
+ };
1044
+ const oauthJwksRecordsFromDocument = (provider, document, allowedAlgorithms) => document.keys.flatMap((jwk) => {
1045
+ const kid = nonEmptyStringFromUnknown(jwk.kid);
1046
+ if (kid === undefined || !oauthJwksAllowsSignatureUse(jwk)) {
1047
+ return [];
1048
+ }
1049
+ return oauthJwksAlgorithms(jwk, allowedAlgorithms).map((alg) => ({
1050
+ id: JwtKeyId(kid),
1051
+ alg,
1052
+ status: "active",
1053
+ publicJwk: {
1054
+ ...jwk,
1055
+ kid,
1056
+ alg,
1057
+ use: nonEmptyStringFromUnknown(jwk.use) ?? "sig",
1058
+ },
1059
+ metadata: {
1060
+ providerId: provider.id,
1061
+ },
1062
+ }));
1063
+ });
1064
+ const oauthJwksKeyUsableAt = (key, now) => now === undefined || key.expiresAt === undefined || Number(now) < Number(key.expiresAt);
1065
+ const oauthJwksVerificationKeyMatches = (key, input) => key.id === input.kid &&
1066
+ key.status !== "disabled" &&
1067
+ key.publicJwk !== undefined &&
1068
+ (input.alg === undefined || key.alg === input.alg) &&
1069
+ oauthJwksKeyUsableAt(key, input.now);
1070
+ const oauthJwksNow = (now) => now === undefined ? currentUnixMillis : Effect.succeed(now);
1071
+ const formUrlEncode = (value) => new URLSearchParams([["", value]]).toString().slice(1);
1072
+ const clientSecretBasic = (provider, secret) => `Basic ${Encoding.encodeBase64(textEncoder.encode(`${formUrlEncode(provider.clientId)}:${formUrlEncode(secret)}`))}`;
1073
+ const oauthTokenRequestHeaders = () => ({
1074
+ "content-type": "application/x-www-form-urlencoded",
1075
+ accept: "application/json",
1076
+ });
1077
+ const oauthRequestHeadersWithoutAuthorization = (headers) => {
1078
+ const value = {};
1079
+ for (const [name, headerValue] of Object.entries(headers ?? {})) {
1080
+ if (name.toLowerCase() !== "authorization") {
1081
+ value[name] = headerValue;
1082
+ }
1083
+ }
1084
+ return value;
1085
+ };
1086
+ const applyOAuthClientAuthentication = (input) => {
1087
+ const clientAuthentication = input.clientAuthentication ?? { method: "none" };
1088
+ if (clientAuthentication.method === "client_secret_basic") {
1089
+ input.body.delete("client_id");
1090
+ input.body.delete("client_secret");
1091
+ input.headers.authorization = clientSecretBasic(input.provider, Redacted.value(clientAuthentication.clientSecret));
1092
+ }
1093
+ else {
1094
+ input.body.set("client_id", input.provider.clientId);
1095
+ }
1096
+ if (clientAuthentication.method === "client_secret_post") {
1097
+ input.body.set("client_secret", Redacted.value(clientAuthentication.clientSecret));
1098
+ }
1099
+ };
1100
+ export const makeOAuthTokenRequest = (input) => Effect.try({
1101
+ try: () => {
1102
+ if (input.provider.tokenEndpoint === undefined) {
1103
+ throw new Error("OAuth provider tokenEndpoint is required");
1104
+ }
1105
+ const body = new URLSearchParams(input.params);
1106
+ const headers = oauthTokenRequestHeaders();
1107
+ body.set("grant_type", "authorization_code");
1108
+ body.set("code", String(input.code));
1109
+ body.set("redirect_uri", input.redirectUri ?? input.provider.redirectUri);
1110
+ body.set("code_verifier", Redacted.value(input.codeVerifier));
1111
+ applyOAuthClientAuthentication({
1112
+ provider: input.provider,
1113
+ body,
1114
+ headers,
1115
+ clientAuthentication: input.clientAuthentication,
1116
+ });
1117
+ return {
1118
+ url: input.provider.tokenEndpoint,
1119
+ method: "POST",
1120
+ headers,
1121
+ body: body.toString(),
1122
+ };
1123
+ },
1124
+ catch: (cause) => oauthTokenExchangeError("token-request", "Failed to build OAuth token request", cause),
1125
+ });
1126
+ export const makeOAuthRefreshTokenRequest = (input) => Effect.try({
1127
+ try: () => {
1128
+ if (input.provider.tokenEndpoint === undefined) {
1129
+ throw new Error("OAuth provider tokenEndpoint is required");
1130
+ }
1131
+ const body = new URLSearchParams(input.params);
1132
+ const headers = oauthTokenRequestHeaders();
1133
+ body.set("grant_type", "refresh_token");
1134
+ body.set("refresh_token", Redacted.value(input.refreshToken));
1135
+ if (input.scopes !== undefined) {
1136
+ body.set("scope", input.scopes.join(" "));
1137
+ }
1138
+ applyOAuthClientAuthentication({
1139
+ provider: input.provider,
1140
+ body,
1141
+ headers,
1142
+ clientAuthentication: input.clientAuthentication,
1143
+ });
1144
+ return {
1145
+ url: input.provider.tokenEndpoint,
1146
+ method: "POST",
1147
+ headers,
1148
+ body: body.toString(),
1149
+ };
1150
+ },
1151
+ catch: (cause) => oauthTokenExchangeError("refresh-token-request", "Failed to build OAuth refresh token request", cause),
1152
+ });
1153
+ export const makeOAuthTokenRevocationRequest = (input) => Effect.try({
1154
+ try: () => {
1155
+ if (input.provider.revocationEndpoint === undefined) {
1156
+ throw new Error("OAuth provider revocationEndpoint is required");
1157
+ }
1158
+ const body = new URLSearchParams(input.params);
1159
+ const headers = oauthTokenRequestHeaders();
1160
+ body.set("token", Redacted.value(input.token));
1161
+ if (input.tokenTypeHint !== undefined) {
1162
+ body.set("token_type_hint", input.tokenTypeHint);
1163
+ }
1164
+ applyOAuthClientAuthentication({
1165
+ provider: input.provider,
1166
+ body,
1167
+ headers,
1168
+ clientAuthentication: input.clientAuthentication,
1169
+ });
1170
+ return {
1171
+ url: input.provider.revocationEndpoint,
1172
+ method: "POST",
1173
+ headers,
1174
+ body: body.toString(),
1175
+ };
1176
+ },
1177
+ catch: (cause) => oauthTokenExchangeError("revocation-request", "Failed to build OAuth token revocation request", cause),
1178
+ });
1179
+ export const makeOAuthUserInfoRequest = (input) => Effect.try({
1180
+ try: () => {
1181
+ if (input.provider.userInfoEndpoint === undefined) {
1182
+ throw new Error("OAuth provider userInfoEndpoint is required");
1183
+ }
1184
+ const url = new URL(input.provider.userInfoEndpoint);
1185
+ for (const [name, value] of Object.entries(input.params ?? {})) {
1186
+ url.searchParams.set(name, value);
1187
+ }
1188
+ return {
1189
+ url: url.toString(),
1190
+ method: "GET",
1191
+ headers: {
1192
+ accept: "application/json",
1193
+ ...oauthRequestHeadersWithoutAuthorization(input.headers),
1194
+ authorization: `Bearer ${Redacted.value(input.accessToken)}`,
1195
+ },
1196
+ };
1197
+ },
1198
+ catch: (cause) => oauthUserInfoError("request", "Failed to build OAuth userinfo request", cause),
1199
+ });
1200
+ export const decodeOAuthUserInfoResponse = (value) => {
1201
+ const record = recordFromUnknown(value);
1202
+ return record === undefined
1203
+ ? Effect.fail(oauthUserInfoError("decode", "OAuth userinfo response must be an object"))
1204
+ : Effect.succeed(metadataValue(record) ?? {});
1205
+ };
1206
+ export const decodeOAuthTokenResponse = (value) => Effect.gen(function* () {
1207
+ if (!isTokenResponseRecord(value)) {
1208
+ return yield* oauthTokenExchangeError("decode-token-response", "OAuth token response must be an object");
1209
+ }
1210
+ const accessToken = stringFromUnknown(value.access_token);
1211
+ const tokenType = stringFromUnknown(value.token_type);
1212
+ if (accessToken === undefined) {
1213
+ return yield* oauthTokenExchangeError("decode-token-response", "OAuth token response is missing access_token");
1214
+ }
1215
+ if (tokenType === undefined) {
1216
+ return yield* oauthTokenExchangeError("decode-token-response", "OAuth token response is missing token_type");
1217
+ }
1218
+ const refreshToken = stringFromUnknown(value.refresh_token);
1219
+ const idToken = stringFromUnknown(value.id_token);
1220
+ return {
1221
+ accessToken: Redacted.make(OAuthAccessToken(accessToken)),
1222
+ tokenType,
1223
+ expiresIn: numberFromUnknown(value.expires_in),
1224
+ ...(refreshToken === undefined
1225
+ ? {}
1226
+ : { refreshToken: Redacted.make(OAuthRefreshToken(refreshToken)) }),
1227
+ ...(idToken === undefined
1228
+ ? {}
1229
+ : { idToken: Redacted.make(OAuthIdToken(idToken)) }),
1230
+ scopes: scopeValues(value.scope),
1231
+ extra: tokenResponseExtra(value),
1232
+ };
1233
+ });
1234
+ export const makeOidcIdTokenVerifier = (dependencies) => OidcIdTokenVerifier.make({
1235
+ verify: Effect.fn("auth.oidc_id_token.verify")(function* (input) {
1236
+ const { provider } = input;
1237
+ const { issuer } = provider;
1238
+ if (issuer === undefined) {
1239
+ return {
1240
+ valid: false,
1241
+ reason: "missing_provider_issuer",
1242
+ };
1243
+ }
1244
+ const verified = yield* dependencies.jwtVerifier
1245
+ .verify({
1246
+ token: Redacted.make(Redacted.value(input.idToken)),
1247
+ issuer,
1248
+ audience: input.audience ?? provider.clientId,
1249
+ now: input.now,
1250
+ clockTolerance: input.clockTolerance,
1251
+ })
1252
+ .pipe(Effect.mapError((cause) => oidcIdTokenVerifierError("verify", "Failed to verify OIDC ID token JWT", cause)));
1253
+ if (!verified.valid) {
1254
+ return verified;
1255
+ }
1256
+ const claims = oidcIdTokenClaims(verified.claims, input);
1257
+ if (typeof claims === "string") {
1258
+ return { valid: false, reason: claims };
1259
+ }
1260
+ return {
1261
+ valid: true,
1262
+ header: verified.header,
1263
+ claims,
1264
+ key: verified.key,
1265
+ };
1266
+ }),
1267
+ });
1268
+ export const verifyOidcIdToken = (input) => OidcIdTokenVerifier.use((verifier) => verifier.verify(input));
1269
+ export const decodeOAuthJwksDocument = (input) => Effect.gen(function* () {
1270
+ const record = recordFromUnknown(input);
1271
+ if (record === undefined || !Array.isArray(record.keys)) {
1272
+ return yield* oauthJwksJwtError("OAuth JWKS document must contain keys");
1273
+ }
1274
+ const keys = [];
1275
+ for (const key of record.keys) {
1276
+ const publicJwk = oauthJwksPublicJwkFromUnknown(key);
1277
+ if (publicJwk === undefined) {
1278
+ return yield* oauthJwksJwtError("OAuth JWKS key must be a public JWK");
1279
+ }
1280
+ keys.push(publicJwk);
1281
+ }
1282
+ return { keys };
1283
+ });
1284
+ export const makeOAuthJwksJwtKeys = (dependencies) => {
1285
+ const cacheTtlMillis = Math.max(0, Duration.toMillis(dependencies.cacheTtl ?? defaultOAuthJwksCacheTtl));
1286
+ const allowedAlgorithms = dependencies.allowedAlgorithms?.filter(isPublicJwksAlgorithm);
1287
+ let cache;
1288
+ const loadRecords = (now) => Effect.gen(function* () {
1289
+ if (cache !== undefined && Number(now) < Number(cache.expiresAt)) {
1290
+ return cache.records;
1291
+ }
1292
+ const url = nonEmptyStringFromUnknown(dependencies.provider.jwksUri);
1293
+ if (url === undefined) {
1294
+ return yield* oauthJwksJwtError("OAuth provider JWKS URI is required");
1295
+ }
1296
+ const rawDocument = yield* dependencies.fetch({
1297
+ provider: dependencies.provider,
1298
+ url,
1299
+ });
1300
+ const document = yield* decodeOAuthJwksDocument(rawDocument);
1301
+ const records = oauthJwksRecordsFromDocument(dependencies.provider, document, allowedAlgorithms);
1302
+ cache = {
1303
+ records,
1304
+ expiresAt: UnixMillis(Number(now) + cacheTtlMillis),
1305
+ };
1306
+ return records;
1307
+ });
1308
+ return JwtKeys.make({
1309
+ selectSigningKey: () => Effect.succeed(Option.none()),
1310
+ findVerificationKey: Effect.fn("auth.oauth_jwks.find_verification_key")(function* (input) {
1311
+ const now = yield* oauthJwksNow(input.now);
1312
+ const records = yield* loadRecords(now);
1313
+ const key = records.find((record) => oauthJwksVerificationKeyMatches(record, { ...input, now }));
1314
+ return key === undefined ? Option.none() : Option.some(key);
1315
+ }),
1316
+ listPublicJwks: Effect.fn("auth.oauth_jwks.list_public_jwks")(function* (input) {
1317
+ const now = yield* oauthJwksNow(input?.now);
1318
+ const records = yield* loadRecords(now);
1319
+ return records.flatMap((record) => record.publicJwk === undefined || !oauthJwksKeyUsableAt(record, now)
1320
+ ? []
1321
+ : [record.publicJwk]);
1322
+ }),
1323
+ });
1324
+ };
1325
+ export const normalizeOidcProfile = (input) => Effect.gen(function* () {
1326
+ const { claims, provider } = input;
1327
+ if (claims === undefined) {
1328
+ return yield* oauthProfileNormalizationError("missing_claims", "OIDC ID token claims are required to normalize an OAuth profile");
1329
+ }
1330
+ const subject = nonEmptyStringFromUnknown(claims.sub);
1331
+ if (subject === undefined) {
1332
+ return yield* oauthProfileNormalizationError("missing_subject", "OIDC profile is missing subject");
1333
+ }
1334
+ const userInfo = yield* userInfoRecord(input.userInfo);
1335
+ const userInfoSubject = userInfo === undefined ? undefined : nonEmptyStringFromUnknown(userInfo.sub);
1336
+ if (userInfoSubject !== undefined && userInfoSubject !== subject) {
1337
+ return yield* oauthProfileNormalizationError("subject_mismatch", "OIDC userinfo subject does not match ID token subject");
1338
+ }
1339
+ const email = emailFromUnknown(userInfo?.email) ?? emailFromUnknown(claims.email);
1340
+ const emailVerified = booleanFromUnknown(userInfo?.email_verified) ??
1341
+ booleanFromUnknown(claims.email_verified);
1342
+ const metadata = optionalRecord({
1343
+ ...(input.metadata === undefined ? {} : input.metadata),
1344
+ });
1345
+ return makeOAuthProfile({
1346
+ providerId: provider.id,
1347
+ providerAccountId: subject,
1348
+ email,
1349
+ emailVerified,
1350
+ name: firstString(userInfo?.name, claims.name),
1351
+ username: firstString(userInfo?.preferred_username, claims.preferred_username),
1352
+ avatarUrl: firstString(userInfo?.picture, claims.picture),
1353
+ metadata,
1354
+ });
1355
+ });
1356
+ export const makeOidcOAuthProfileNormalizer = () => OAuthProfileNormalizer.make({
1357
+ normalize: Effect.fn("auth.oauth_profile.normalize_oidc")(normalizeOidcProfile),
1358
+ });
1359
+ export const normalizeOAuthProfile = (input) => OAuthProfileNormalizer.use((normalizer) => normalizer.normalize(input));
1360
+ export const verifiedOAuthIdentityFromProfile = (profile) => {
1361
+ const metadata = optionalRecord({
1362
+ ...(profile.name === undefined ? {} : { name: profile.name }),
1363
+ ...(profile.username === undefined ? {} : { username: profile.username }),
1364
+ ...(profile.avatarUrl === undefined ? {} : { avatarUrl: profile.avatarUrl }),
1365
+ ...(profile.metadata === undefined ? {} : profile.metadata),
1366
+ });
1367
+ return {
1368
+ providerId: profile.providerId,
1369
+ providerAccountId: profile.providerAccountId,
1370
+ ...(profile.email === undefined ? {} : { email: profile.email }),
1371
+ ...(profile.emailVerified === undefined
1372
+ ? {}
1373
+ : { emailVerified: profile.emailVerified }),
1374
+ ...(metadata === undefined ? {} : { metadata: metadataValue(metadata) }),
1375
+ };
1376
+ };
1377
+ export const makeOAuthTokenExchange = (dependencies) => OAuthTokenExchange.make({
1378
+ exchangeAuthorizationCode: Effect.fn("auth.oauth_token.exchange_code")(function* (input) {
1379
+ const request = yield* makeOAuthTokenRequest(input);
1380
+ const response = yield* dependencies.request(request);
1381
+ return yield* decodeOAuthTokenResponse(response).pipe(Effect.mapError((cause) => oauthTokenExchangeError("exchange-code", "Failed to decode OAuth token exchange response", cause)));
1382
+ }),
1383
+ });
1384
+ const oauthProviderTokenString = (token) => Redacted.make(String(Redacted.value(token)));
1385
+ const oauthProviderTokenExpiresAt = (input) => input.expiresIn === undefined
1386
+ ? undefined
1387
+ : UnixMillis(Number(input.now) + input.expiresIn * 1000);
1388
+ const storeOAuthProviderTokenResponse = (dependencies, input, operation) => Effect.gen(function* () {
1389
+ const now = input.now ?? (yield* currentUnixMillis);
1390
+ const accessTokenCiphertext = yield* dependencies
1391
+ .encryptToken(oauthProviderTokenString(input.tokens.accessToken))
1392
+ .pipe(Effect.mapError((cause) => oauthProviderTokenLifecycleError(operation, "Failed to encrypt OAuth provider access token", cause)));
1393
+ const refreshTokenCiphertext = input.tokens.refreshToken === undefined
1394
+ ? input.fallbackRefreshTokenCiphertext
1395
+ : yield* dependencies
1396
+ .encryptToken(oauthProviderTokenString(input.tokens.refreshToken))
1397
+ .pipe(Effect.mapError((cause) => oauthProviderTokenLifecycleError(operation, "Failed to encrypt OAuth provider refresh token", cause)));
1398
+ const idTokenCiphertext = input.tokens.idToken === undefined
1399
+ ? undefined
1400
+ : yield* dependencies
1401
+ .encryptToken(oauthProviderTokenString(input.tokens.idToken))
1402
+ .pipe(Effect.mapError((cause) => oauthProviderTokenLifecycleError(operation, "Failed to encrypt OAuth provider ID token", cause)));
1403
+ const expiresAt = oauthProviderTokenExpiresAt({
1404
+ now,
1405
+ expiresIn: input.tokens.expiresIn,
1406
+ });
1407
+ yield* dependencies.tokenVault
1408
+ .upsert({
1409
+ accountId: input.accountId,
1410
+ userId: input.userId,
1411
+ providerId: input.providerId,
1412
+ providerAccountId: input.providerAccountId,
1413
+ accessTokenCiphertext,
1414
+ tokenType: input.tokens.tokenType,
1415
+ updatedAt: now,
1416
+ ...(refreshTokenCiphertext === undefined ? {} : { refreshTokenCiphertext }),
1417
+ ...(idTokenCiphertext === undefined ? {} : { idTokenCiphertext }),
1418
+ ...(input.tokens.scopes === undefined ? {} : { scopes: input.tokens.scopes }),
1419
+ ...(expiresAt === undefined ? {} : { expiresAt }),
1420
+ })
1421
+ .pipe(Effect.mapError((cause) => oauthProviderTokenLifecycleError(operation, "Failed to store OAuth provider token ciphertext", cause)));
1422
+ return {
1423
+ accountId: input.accountId,
1424
+ ...(expiresAt === undefined ? {} : { expiresAt }),
1425
+ hasRefreshToken: refreshTokenCiphertext !== undefined,
1426
+ scopes: [...(input.tokens.scopes ?? [])],
1427
+ };
1428
+ });
1429
+ const revokeOAuthProviderTokenCiphertext = (dependencies, input, tokenCiphertext, tokenTypeHint) => Effect.gen(function* () {
1430
+ const token = yield* dependencies.decryptToken(tokenCiphertext).pipe(Effect.mapError((cause) => oauthProviderTokenLifecycleError("revoke", `Failed to decrypt OAuth provider ${tokenTypeHint}`, cause)));
1431
+ const request = yield* makeOAuthTokenRevocationRequest({
1432
+ provider: input.provider,
1433
+ token,
1434
+ tokenTypeHint,
1435
+ ...(input.clientAuthentication === undefined
1436
+ ? {}
1437
+ : { clientAuthentication: input.clientAuthentication }),
1438
+ ...(input.params === undefined ? {} : { params: input.params }),
1439
+ }).pipe(Effect.mapError((cause) => oauthProviderTokenLifecycleError("revoke", "Failed to build OAuth provider token revocation request", cause)));
1440
+ yield* dependencies.request(request).pipe(Effect.mapError((cause) => oauthProviderTokenLifecycleError("revoke", "Failed to execute OAuth provider token revocation request", cause)));
1441
+ });
1442
+ export const makeOAuthProviderTokenLifecycle = (dependencies) => OAuthProviderTokenLifecycle.make({
1443
+ store: Effect.fn("auth.oauth_provider_token_lifecycle.store")(function* (input) {
1444
+ return yield* storeOAuthProviderTokenResponse(dependencies, input, "store");
1445
+ }),
1446
+ refresh: Effect.fn("auth.oauth_provider_token_lifecycle.refresh")(function* (input) {
1447
+ const refreshTokenCiphertext = yield* dependencies.tokenVault
1448
+ .refreshTokenForAccount({ accountId: input.accountId })
1449
+ .pipe(Effect.mapError((cause) => oauthProviderTokenLifecycleError("refresh", "Failed to load OAuth provider refresh token ciphertext", cause)));
1450
+ if (Option.isNone(refreshTokenCiphertext)) {
1451
+ return {
1452
+ status: "missing-refresh-token",
1453
+ accountId: input.accountId,
1454
+ };
1455
+ }
1456
+ const refreshToken = yield* dependencies
1457
+ .decryptToken(refreshTokenCiphertext.value)
1458
+ .pipe(Effect.mapError((cause) => oauthProviderTokenLifecycleError("refresh", "Failed to decrypt OAuth provider refresh token", cause)));
1459
+ const request = yield* makeOAuthRefreshTokenRequest({
1460
+ provider: input.provider,
1461
+ refreshToken,
1462
+ ...(input.scopes === undefined ? {} : { scopes: input.scopes }),
1463
+ ...(input.clientAuthentication === undefined
1464
+ ? {}
1465
+ : { clientAuthentication: input.clientAuthentication }),
1466
+ ...(input.params === undefined ? {} : { params: input.params }),
1467
+ }).pipe(Effect.mapError((cause) => oauthProviderTokenLifecycleError("refresh", "Failed to build OAuth provider token refresh request", cause)));
1468
+ const rawResponse = yield* dependencies.request(request).pipe(Effect.mapError((cause) => oauthProviderTokenLifecycleError("refresh", "Failed to execute OAuth provider token refresh request", cause)));
1469
+ const tokens = yield* decodeOAuthTokenResponse(rawResponse).pipe(Effect.mapError((cause) => oauthProviderTokenLifecycleError("refresh", "Failed to decode OAuth provider token refresh response", cause)));
1470
+ const stored = yield* storeOAuthProviderTokenResponse(dependencies, {
1471
+ accountId: input.accountId,
1472
+ userId: input.userId,
1473
+ providerId: input.providerId,
1474
+ providerAccountId: input.providerAccountId,
1475
+ tokens,
1476
+ fallbackRefreshTokenCiphertext: refreshTokenCiphertext.value,
1477
+ now: input.now,
1478
+ }, "refresh");
1479
+ return {
1480
+ status: "refreshed",
1481
+ ...stored,
1482
+ };
1483
+ }),
1484
+ revoke: Effect.fn("auth.oauth_provider_token_lifecycle.revoke")(function* (input) {
1485
+ const tokens = yield* dependencies.tokenVault
1486
+ .tokensForAccount({ accountId: input.accountId })
1487
+ .pipe(Effect.mapError((cause) => oauthProviderTokenLifecycleError("revoke", "Failed to load OAuth provider token ciphertext", cause)));
1488
+ if (Option.isNone(tokens)) {
1489
+ return {
1490
+ status: "missing-tokens",
1491
+ accountId: input.accountId,
1492
+ };
1493
+ }
1494
+ yield* revokeOAuthProviderTokenCiphertext(dependencies, input, tokens.value.accessTokenCiphertext, "access_token");
1495
+ if (tokens.value.refreshTokenCiphertext !== undefined) {
1496
+ yield* revokeOAuthProviderTokenCiphertext(dependencies, input, tokens.value.refreshTokenCiphertext, "refresh_token");
1497
+ }
1498
+ const revokedAt = input.now ?? (yield* currentUnixMillis);
1499
+ yield* dependencies.tokenVault
1500
+ .markRevoked({
1501
+ accountId: input.accountId,
1502
+ revokedAt,
1503
+ reason: input.reason,
1504
+ })
1505
+ .pipe(Effect.mapError((cause) => oauthProviderTokenLifecycleError("revoke", "Failed to mark OAuth provider tokens revoked", cause)));
1506
+ return {
1507
+ status: "revoked",
1508
+ accountId: input.accountId,
1509
+ revokedAt,
1510
+ };
1511
+ }),
1512
+ });
1513
+ export const OAuthProviderTokenLifecycleLive = (options) => Layer.effect(OAuthProviderTokenLifecycle)(Effect.gen(function* () {
1514
+ const tokenVault = yield* OAuthProviderTokenVault;
1515
+ return makeOAuthProviderTokenLifecycle({
1516
+ tokenVault,
1517
+ ...options,
1518
+ });
1519
+ }));
1520
+ export const storeOAuthProviderTokens = (input) => OAuthProviderTokenLifecycle.use((lifecycle) => lifecycle.store(input));
1521
+ export const refreshOAuthProviderTokens = (input) => OAuthProviderTokenLifecycle.use((lifecycle) => lifecycle.refresh(input));
1522
+ export const revokeOAuthProviderTokens = (input) => OAuthProviderTokenLifecycle.use((lifecycle) => lifecycle.revoke(input));
1523
+ export const completeOAuthCallback = (input) => Effect.gen(function* () {
1524
+ const oauthState = yield* OAuthState;
1525
+ const tokenExchange = yield* OAuthTokenExchange;
1526
+ const verifiedState = yield* oauthState.verify({ state: input.state });
1527
+ if (verifiedState.providerId !== input.provider.id) {
1528
+ return yield* oauthStateError("verify", "OAuth callback provider does not match verified state");
1529
+ }
1530
+ const tokens = yield* tokenExchange.exchangeAuthorizationCode({
1531
+ provider: input.provider,
1532
+ code: input.code,
1533
+ codeVerifier: input.codeVerifier,
1534
+ redirectUri: input.redirectUri ?? verifiedState.redirectUri ?? input.provider.redirectUri,
1535
+ clientAuthentication: input.clientAuthentication,
1536
+ params: input.tokenParams,
1537
+ });
1538
+ return { state: verifiedState, tokens };
1539
+ });
1540
+ export const makeOAuthAccountLinkingPolicy = (dependencies) => OAuthAccountLinking.make({
1541
+ resolve: Effect.fn("auth.oauth_account_linking.resolve")(function* (input) {
1542
+ const identity = cloneVerifiedOAuthIdentity(input.identity);
1543
+ const linked = yield* dependencies.accounts
1544
+ .findByProviderAccount({
1545
+ providerId: identity.providerId,
1546
+ providerAccountId: identity.providerAccountId,
1547
+ })
1548
+ .pipe(Effect.mapError((cause) => oauthAccountLinkingError("resolve", "Failed to look up OAuth provider account", cause)));
1549
+ if (Option.isSome(linked)) {
1550
+ const account = linked.value;
1551
+ if (input.currentUserId !== undefined &&
1552
+ account.userId !== input.currentUserId) {
1553
+ return {
1554
+ type: "deny",
1555
+ reason: "provider-account-linked-to-different-user",
1556
+ identity,
1557
+ account,
1558
+ currentUserId: input.currentUserId,
1559
+ };
1560
+ }
1561
+ return {
1562
+ type: "sign-in",
1563
+ reason: "linked-account",
1564
+ userId: account.userId,
1565
+ account,
1566
+ };
1567
+ }
1568
+ if (input.currentUserId !== undefined) {
1569
+ return {
1570
+ type: "link-existing-user",
1571
+ reason: "current-user",
1572
+ userId: input.currentUserId,
1573
+ identity,
1574
+ };
1575
+ }
1576
+ if (input.matchedUserId !== undefined) {
1577
+ if (dependencies.autoLinkVerifiedEmail === true &&
1578
+ identity.emailVerified === true) {
1579
+ return {
1580
+ type: "link-existing-user",
1581
+ reason: "verified-email",
1582
+ userId: input.matchedUserId,
1583
+ identity,
1584
+ };
1585
+ }
1586
+ return {
1587
+ type: "require-explicit-linking",
1588
+ reason: identity.emailVerified === true ? "matched-email" : "unverified-email",
1589
+ identity,
1590
+ matchedUserId: input.matchedUserId,
1591
+ };
1592
+ }
1593
+ return {
1594
+ type: "require-explicit-linking",
1595
+ reason: "no-linked-account",
1596
+ identity,
1597
+ };
1598
+ }),
1599
+ });
1600
+ export const resolveOAuthAccountLinking = (input) => OAuthAccountLinking.use((linking) => linking.resolve(input));
1601
+ export const makeOAuthAccountUnlinkingPolicy = (dependencies) => OAuthAccountUnlinking.make({
1602
+ resolve: Effect.fn("auth.oauth_account_unlinking.resolve")(function* (input) {
1603
+ const account = yield* dependencies.accounts
1604
+ .findByProviderAccount({
1605
+ providerId: input.providerId,
1606
+ providerAccountId: input.providerAccountId,
1607
+ })
1608
+ .pipe(Effect.mapError((cause) => oauthAccountUnlinkingError("resolve", "Failed to look up OAuth provider account", cause)));
1609
+ if (Option.isNone(account)) {
1610
+ return {
1611
+ type: "deny-not-found",
1612
+ reason: "provider-account-not-found",
1613
+ userId: input.userId,
1614
+ providerId: input.providerId,
1615
+ providerAccountId: input.providerAccountId,
1616
+ };
1617
+ }
1618
+ const linkedAccount = account.value;
1619
+ if (linkedAccount.userId !== input.userId) {
1620
+ return {
1621
+ type: "deny-not-owner",
1622
+ reason: "provider-account-linked-to-different-user",
1623
+ userId: input.userId,
1624
+ account: linkedAccount,
1625
+ };
1626
+ }
1627
+ const remainingAccounts = yield* dependencies.accounts
1628
+ .listByUser({ userId: input.userId })
1629
+ .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)));
1630
+ if (dependencies.allowLastAccount !== true && remainingAccounts.length === 0) {
1631
+ return {
1632
+ type: "deny-last-account",
1633
+ reason: "last-linked-account",
1634
+ userId: input.userId,
1635
+ account: linkedAccount,
1636
+ };
1637
+ }
1638
+ if (dependencies.requireStepUp === true && input.stepUpSatisfied !== true) {
1639
+ return {
1640
+ type: "requires-step-up",
1641
+ reason: "step-up-required",
1642
+ userId: input.userId,
1643
+ account: linkedAccount,
1644
+ remainingAccounts,
1645
+ };
1646
+ }
1647
+ return {
1648
+ type: "allow",
1649
+ reason: "owned-account",
1650
+ userId: input.userId,
1651
+ account: linkedAccount,
1652
+ remainingAccounts,
1653
+ };
1654
+ }),
1655
+ });
1656
+ export const resolveOAuthAccountUnlinking = (input) => OAuthAccountUnlinking.use((unlinking) => unlinking.resolve(input));
1657
+ const findOAuthLinkConfirmationExistingAccount = (dependencies, input) => dependencies.accounts
1658
+ .findByProviderAccount({
1659
+ providerId: input.identity.providerId,
1660
+ providerAccountId: input.identity.providerAccountId,
1661
+ })
1662
+ .pipe(Effect.mapError((cause) => oauthLinkConfirmationError(input.operation, "Failed to look up OAuth provider account", cause)), Effect.flatMap((account) => {
1663
+ if (Option.isNone(account)) {
1664
+ return Effect.succeed(account);
1665
+ }
1666
+ if (account.value.userId !== input.userId) {
1667
+ return Effect.fail(oauthLinkConfirmationError(input.operation, "OAuth provider account is linked to a different user"));
1668
+ }
1669
+ return Effect.succeed(account);
1670
+ }));
1671
+ export const makeOAuthLinkConfirmation = (dependencies) => OAuthLinkConfirmation.make({
1672
+ start: Effect.fn("auth.oauth_link_confirmation.start")(function* (input) {
1673
+ const identity = cloneVerifiedOAuthIdentity(input.identity);
1674
+ const reason = input.reason ?? "manual";
1675
+ const existing = yield* findOAuthLinkConfirmationExistingAccount(dependencies, {
1676
+ operation: "start",
1677
+ userId: input.userId,
1678
+ identity,
1679
+ });
1680
+ if (Option.isSome(existing)) {
1681
+ return yield* oauthLinkConfirmationError("start", "OAuth provider account is already linked to this user");
1682
+ }
1683
+ const secret = input.secret ??
1684
+ (yield* dependencies.crypto
1685
+ .randomToken(yield* validateOAuthLinkConfirmationSecretBytes(dependencies.secretBytes ?? defaultOAuthLinkConfirmationSecretBytes))
1686
+ .pipe(Effect.map(Redacted.make), Effect.mapError((cause) => oauthLinkConfirmationError("start", "Failed to generate OAuth link confirmation secret", cause))));
1687
+ const issued = yield* dependencies.challenge
1688
+ .issue({
1689
+ type: oauthLinkConfirmationChallengeType,
1690
+ subject: input.userId,
1691
+ ttl: input.ttl ?? defaultOAuthLinkConfirmationTtl,
1692
+ secret,
1693
+ metadata: oauthLinkConfirmationMetadata({
1694
+ userId: input.userId,
1695
+ identity,
1696
+ reason,
1697
+ metadata: input.metadata,
1698
+ }),
1699
+ })
1700
+ .pipe(Effect.mapError((cause) => oauthLinkConfirmationError("start", "Failed to issue OAuth link confirmation challenge", cause)));
1701
+ return {
1702
+ challengeId: issued.id,
1703
+ userId: input.userId,
1704
+ identity,
1705
+ reason,
1706
+ secret,
1707
+ expiresAt: issued.expiresAt,
1708
+ ...(input.metadata === undefined
1709
+ ? {}
1710
+ : { metadata: metadataValue(input.metadata) }),
1711
+ };
1712
+ }),
1713
+ inspect: Effect.fn("auth.oauth_link_confirmation.inspect")(function* (input) {
1714
+ const inspected = yield* dependencies.challenge
1715
+ .inspect({
1716
+ challengeId: input.challengeId,
1717
+ type: oauthLinkConfirmationChallengeType,
1718
+ secret: input.secret,
1719
+ })
1720
+ .pipe(Effect.mapError((cause) => oauthLinkConfirmationError("inspect", "Invalid OAuth link confirmation challenge", cause)));
1721
+ const pending = yield* pendingOAuthLinkConfirmationFromChallenge(inspected, "inspect");
1722
+ yield* findOAuthLinkConfirmationExistingAccount(dependencies, {
1723
+ operation: "inspect",
1724
+ userId: pending.userId,
1725
+ identity: pending.identity,
1726
+ });
1727
+ return pending;
1728
+ }),
1729
+ confirm: Effect.fn("auth.oauth_link_confirmation.confirm")(function* (input) {
1730
+ const inspected = yield* dependencies.challenge
1731
+ .inspect({
1732
+ challengeId: input.challengeId,
1733
+ type: oauthLinkConfirmationChallengeType,
1734
+ secret: input.secret,
1735
+ })
1736
+ .pipe(Effect.mapError((cause) => oauthLinkConfirmationError("confirm", "Invalid OAuth link confirmation challenge", cause)));
1737
+ const pending = yield* pendingOAuthLinkConfirmationFromChallenge(inspected, "confirm");
1738
+ if (input.currentUserId !== undefined && input.currentUserId !== pending.userId) {
1739
+ return yield* oauthLinkConfirmationError("confirm", "OAuth link confirmation does not match current user");
1740
+ }
1741
+ if (input.expectedProviderId !== undefined &&
1742
+ input.expectedProviderId !== pending.identity.providerId) {
1743
+ return yield* oauthLinkConfirmationError("confirm", "OAuth link confirmation provider mismatch");
1744
+ }
1745
+ if (input.expectedProviderAccountId !== undefined &&
1746
+ input.expectedProviderAccountId !== pending.identity.providerAccountId) {
1747
+ return yield* oauthLinkConfirmationError("confirm", "OAuth link confirmation provider account mismatch");
1748
+ }
1749
+ const existing = yield* findOAuthLinkConfirmationExistingAccount(dependencies, {
1750
+ operation: "confirm",
1751
+ userId: pending.userId,
1752
+ identity: pending.identity,
1753
+ });
1754
+ yield* dependencies.challenge
1755
+ .verify({
1756
+ challengeId: input.challengeId,
1757
+ type: oauthLinkConfirmationChallengeType,
1758
+ secret: input.secret,
1759
+ })
1760
+ .pipe(Effect.mapError((cause) => oauthLinkConfirmationError("confirm", "Failed to consume OAuth link confirmation challenge", cause)));
1761
+ return confirmedOAuthLinkConfirmationFromPending({
1762
+ pending,
1763
+ ...(Option.isSome(existing) ? { existingAccount: existing.value } : {}),
1764
+ });
1765
+ }),
1766
+ });
1767
+ export const startOAuthLinkConfirmation = (input) => OAuthLinkConfirmation.use((confirmation) => confirmation.start(input));
1768
+ export const inspectOAuthLinkConfirmation = (input) => OAuthLinkConfirmation.use((confirmation) => confirmation.inspect(input));
1769
+ export const confirmOAuthLinkConfirmation = (input) => OAuthLinkConfirmation.use((confirmation) => confirmation.confirm(input));
1770
+ const makeOAuthPkceWithCrypto = (dependencies, input) => Effect.gen(function* () {
1771
+ const verifier = input?.codeVerifier === undefined
1772
+ ? yield* dependencies.crypto
1773
+ .randomToken(yield* validateByteLength(input?.verifierBytes ??
1774
+ dependencies.codeVerifierBytes ??
1775
+ defaultOAuthCodeVerifierBytes, "pkce", "OAuth code verifier"))
1776
+ .pipe(Effect.map(OAuthCodeVerifier), Effect.mapError((cause) => oauthStateError("pkce", "Failed to generate OAuth code verifier", cause)))
1777
+ : OAuthCodeVerifier(Redacted.value(input.codeVerifier));
1778
+ const codeChallenge = yield* dependencies.crypto
1779
+ .digestSha256({ data: verifier })
1780
+ .pipe(Effect.map(OAuthCodeChallenge), Effect.mapError((cause) => oauthStateError("pkce", "Failed to calculate OAuth PKCE challenge", cause)));
1781
+ return {
1782
+ codeVerifier: Redacted.make(verifier),
1783
+ codeChallenge,
1784
+ codeChallengeMethod: "S256",
1785
+ };
1786
+ });
1787
+ const authorizationCodeHash = (crypto, code, operation) => crypto.digestSha256({ data: String(code) }).pipe(Effect.mapError((cause) => oauthAuthorizationCodeGrantError(operation, "Failed to hash OAuth authorization code", cause)));
1788
+ const pkceChallengeForVerifier = (crypto, verifier) => crypto.digestSha256({ data: Redacted.value(verifier) }).pipe(Effect.map(OAuthCodeChallenge), Effect.mapError((cause) => oauthAuthorizationCodeGrantError("consume", "Failed to calculate OAuth PKCE challenge", cause)));
1789
+ const timingSafeEqualString = (crypto, left, right) => crypto
1790
+ .timingSafeEqual({
1791
+ left: textEncoder.encode(left),
1792
+ right: textEncoder.encode(right),
1793
+ })
1794
+ .pipe(Effect.mapError((cause) => oauthAuthorizationCodeGrantError("consume", "Failed to compare OAuth PKCE challenge", cause)));
1795
+ const authorizationCodeValue = (code) => OAuthAuthorizationCode(String(Redacted.value(code)).trim());
1796
+ const authorizationCodeExpiresAt = (input, issuedAt, defaultTtl) => input.expiresAt ??
1797
+ UnixMillis(Number(issuedAt) + Duration.toMillis(input.expiresIn ?? defaultTtl));
1798
+ export const makeOAuthAuthorizationCodeGrant = (dependencies) => {
1799
+ const codeBytes = dependencies.codeBytes ?? defaultOAuthAuthorizationCodeBytes;
1800
+ const codeTtl = dependencies.codeTtl ?? defaultOAuthAuthorizationCodeTtl;
1801
+ return OAuthAuthorizationCodeGrant.make({
1802
+ issue: Effect.fn("auth.oauth.authorization_code.issue")(function* (input) {
1803
+ const issuedAt = input.issuedAt ?? (yield* currentUnixMillis);
1804
+ const expiresAt = authorizationCodeExpiresAt(input, issuedAt, codeTtl);
1805
+ const code = input.code === undefined
1806
+ ? OAuthAuthorizationCode(yield* Effect.gen(function* () {
1807
+ const validatedCodeBytes = yield* validateOAuthAuthorizationCodeBytes(codeBytes);
1808
+ return yield* dependencies.crypto
1809
+ .randomToken(validatedCodeBytes)
1810
+ .pipe(Effect.mapError((cause) => oauthAuthorizationCodeGrantError("issue", "Failed to generate OAuth authorization code", cause)));
1811
+ }))
1812
+ : authorizationCodeValue(input.code);
1813
+ const codeHash = yield* authorizationCodeHash(dependencies.crypto, code, "issue");
1814
+ const codeChallenge = input.codeChallenge === undefined
1815
+ ? undefined
1816
+ : OAuthCodeChallenge(String(input.codeChallenge));
1817
+ const record = {
1818
+ codeHash,
1819
+ clientId: input.clientId,
1820
+ subject: input.subject,
1821
+ redirectUri: input.redirectUri,
1822
+ scopes: [...(input.scopes ?? [])],
1823
+ ...(codeChallenge === undefined
1824
+ ? {}
1825
+ : {
1826
+ codeChallenge,
1827
+ codeChallengeMethod: input.codeChallengeMethod ?? "S256",
1828
+ }),
1829
+ issuedAt,
1830
+ expiresAt,
1831
+ ...(input.metadata === undefined
1832
+ ? {}
1833
+ : { metadata: metadataValue(input.metadata) }),
1834
+ };
1835
+ yield* dependencies.store.upsert(record).pipe(Effect.mapError((cause) => oauthAuthorizationCodeGrantError("issue", "Failed to store OAuth authorization code", cause)));
1836
+ return issuedAuthorizationCode({ code, record });
1837
+ }),
1838
+ consume: Effect.fn("auth.oauth.authorization_code.consume")(function* (input) {
1839
+ const now = input.now ?? (yield* currentUnixMillis);
1840
+ const codeHash = yield* authorizationCodeHash(dependencies.crypto, authorizationCodeValue(input.code), "consume");
1841
+ const recordOption = yield* dependencies.store.findByHash({ codeHash }).pipe(Effect.mapError((cause) => oauthAuthorizationCodeGrantError("consume", "Failed to load OAuth authorization code", cause)));
1842
+ if (Option.isNone(recordOption)) {
1843
+ return { valid: false, reason: "unknown_code" };
1844
+ }
1845
+ const record = recordOption.value;
1846
+ if (record.consumedAt !== undefined) {
1847
+ return { valid: false, reason: "already_consumed" };
1848
+ }
1849
+ if (Number(now) >= Number(record.expiresAt)) {
1850
+ return { valid: false, reason: "expired" };
1851
+ }
1852
+ if (record.clientId !== input.clientId) {
1853
+ return { valid: false, reason: "client_mismatch" };
1854
+ }
1855
+ if (record.redirectUri !== input.redirectUri) {
1856
+ return { valid: false, reason: "redirect_uri_mismatch" };
1857
+ }
1858
+ if (record.codeChallenge !== undefined) {
1859
+ if (input.codeVerifier === undefined) {
1860
+ return { valid: false, reason: "pkce_required" };
1861
+ }
1862
+ const actualChallenge = yield* pkceChallengeForVerifier(dependencies.crypto, input.codeVerifier);
1863
+ const pkceMatches = yield* timingSafeEqualString(dependencies.crypto, record.codeChallenge, actualChallenge);
1864
+ if (!pkceMatches) {
1865
+ return { valid: false, reason: "pkce_mismatch" };
1866
+ }
1867
+ }
1868
+ const consumed = yield* dependencies.store
1869
+ .markConsumed({ codeHash, consumedAt: now })
1870
+ .pipe(Effect.mapError((cause) => oauthAuthorizationCodeGrantError("consume", "Failed to consume OAuth authorization code", cause)));
1871
+ if (consumed.status === "missing") {
1872
+ return { valid: false, reason: "unknown_code" };
1873
+ }
1874
+ if (consumed.status === "already_consumed") {
1875
+ return { valid: false, reason: "already_consumed" };
1876
+ }
1877
+ return {
1878
+ valid: true,
1879
+ code: consumedAuthorizationCode(consumed.record, now),
1880
+ };
1881
+ }),
1882
+ });
1883
+ };
1884
+ const oauthAuthorizationCodeTokenInvalid = (reason) => ({ valid: false, reason });
1885
+ const oauthRefreshTokenInvalid = (reason) => ({ valid: false, reason });
1886
+ const oauthRefreshTokenIssueInvalid = (reason) => ({ valid: false, reason });
1887
+ const oauthClientCredentialsInvalid = (reason) => ({ valid: false, reason });
1888
+ const oauthClientCredentialsIssueInvalid = (reason) => ({ valid: false, reason });
1889
+ const oauthTokenRevocationInvalid = (reason) => ({ valid: false, reason });
1890
+ const oauthTokenIntrospectionInvalid = (reason) => ({ valid: false, reason });
1891
+ const oauthTokenGrantString = (value) => {
1892
+ const trimmed = value?.trim();
1893
+ return trimmed === undefined || trimmed === "" ? undefined : trimmed;
1894
+ };
1895
+ const oauthTokenGrantScopes = (scopes) => {
1896
+ const values = typeof scopes === "string" ? scopes.split(/\s+/) : scopes ?? [];
1897
+ const unique = new Set();
1898
+ for (const value of values) {
1899
+ const scope = value.trim();
1900
+ if (scope !== "") {
1901
+ unique.add(scope);
1902
+ }
1903
+ }
1904
+ return [...unique];
1905
+ };
1906
+ const oauthTokenGrantScopesAllowed = (scopes, allowedScopes) => allowedScopes === undefined || scopes.every((scope) => allowedScopes.includes(scope));
1907
+ const validateOAuthProviderModeOpaqueTokenBytes = (value, label, error) => Number.isInteger(value) && value >= 16 && value <= 128
1908
+ ? Effect.succeed(value)
1909
+ : Effect.fail(error(`${label} byte length must be an integer between 16 and 128`));
1910
+ const generateOAuthProviderModeOpaqueToken = (crypto, byteLength, brand, label, error) => Effect.gen(function* () {
1911
+ const validByteLength = yield* validateOAuthProviderModeOpaqueTokenBytes(byteLength, label, error);
1912
+ const token = yield* crypto.randomToken(validByteLength).pipe(Effect.mapError((cause) => error(`Failed to generate OAuth ${label} token`, cause)));
1913
+ return Redacted.make(brand(token));
1914
+ });
1915
+ const oauthProviderModeOpaqueTokenHash = (crypto, token, error) => crypto.digestSha256({ data: Redacted.value(token) }).pipe(Effect.map((digest) => `sha256:${digest}`), Effect.mapError((cause) => error("Failed to hash OAuth provider-mode token", cause)));
1916
+ const oauthProviderModeOpaqueTokenAudience = (audience) => audience === undefined ? undefined : Array.isArray(audience) ? [...audience] : audience;
1917
+ const oauthProviderModeOpaqueTokenRecord = (dependencies, input) => ({
1918
+ tokenHash: input.tokenHash,
1919
+ tokenType: input.tokenType,
1920
+ clientId: input.clientId,
1921
+ subject: input.subject,
1922
+ scopes: [...input.scopes],
1923
+ issuedAt: input.issuedAt,
1924
+ expiresAt: input.expiresAt,
1925
+ ...(dependencies.issuer === undefined ? {} : { issuer: dependencies.issuer }),
1926
+ ...(dependencies.audience === undefined
1927
+ ? {}
1928
+ : { audience: oauthProviderModeOpaqueTokenAudience(dependencies.audience) }),
1929
+ ...(dependencies.metadata === undefined
1930
+ ? {}
1931
+ : { metadata: metadataValue(dependencies.metadata) }),
1932
+ });
1933
+ const oauthProviderModeOpaqueExpiresAt = (issuedAt, ttl) => UnixMillis(Number(issuedAt) + Duration.toMillis(ttl));
1934
+ const oauthProviderModeOpaqueExpiresInSeconds = (issuedAt, expiresAt) => Math.max(0, Math.floor((Number(expiresAt) - Number(issuedAt)) / 1000));
1935
+ const oauthProviderModeOpaqueTokenResponse = (input) => ({
1936
+ accessToken: input.accessToken,
1937
+ tokenType: "Bearer",
1938
+ expiresIn: oauthProviderModeOpaqueExpiresInSeconds(input.accessTokenIssuedAt, input.accessTokenExpiresAt),
1939
+ ...(input.refreshToken === undefined
1940
+ ? {}
1941
+ : { refreshToken: input.refreshToken }),
1942
+ ...(input.scopes.length === 0 ? {} : { scopes: [...input.scopes] }),
1943
+ });
1944
+ const oauthProviderModeOpaqueStoreToken = (store, record, error) => store.upsert(record).pipe(Effect.mapError((cause) => error("Failed to store OAuth provider-mode token", cause)));
1945
+ const oauthProviderModeScopesSubset = (scopes, grantedScopes) => scopes.every((scope) => grantedScopes.includes(scope));
1946
+ const oauthTokenGrantClientId = (value) => {
1947
+ const clientId = oauthTokenGrantString(value === undefined ? undefined : String(value));
1948
+ return clientId === undefined ? undefined : OAuthClientId(clientId);
1949
+ };
1950
+ const oauthAuthorizationCodeConsumeFailureToTokenFailure = (reason) => {
1951
+ switch (reason) {
1952
+ case "unknown_code":
1953
+ case "already_consumed":
1954
+ case "expired":
1955
+ case "client_mismatch":
1956
+ case "redirect_uri_mismatch":
1957
+ case "pkce_required":
1958
+ case "pkce_mismatch": {
1959
+ return "invalid_grant";
1960
+ }
1961
+ }
1962
+ };
1963
+ export const makeOAuthAuthorizationCodeTokenGrant = (dependencies) => OAuthAuthorizationCodeTokenGrant.make({
1964
+ exchange: Effect.fn("auth.oauth.authorization_code_token.exchange")(function* (input) {
1965
+ if (input.grantType !== "authorization_code") {
1966
+ return oauthAuthorizationCodeTokenInvalid("unsupported_grant_type");
1967
+ }
1968
+ const { code } = input;
1969
+ const clientId = oauthTokenGrantClientId(input.clientId);
1970
+ const redirectUri = oauthTokenGrantString(input.redirectUri);
1971
+ if (code === undefined || clientId === undefined || redirectUri === undefined) {
1972
+ return oauthAuthorizationCodeTokenInvalid("invalid_request");
1973
+ }
1974
+ const clientOption = yield* dependencies.clients.find({ clientId }).pipe(Effect.mapError((cause) => oauthAuthorizationCodeTokenGrantError("exchange", "Failed to load OAuth client", cause)));
1975
+ if (Option.isNone(clientOption) || clientOption.value.status !== "active") {
1976
+ return oauthAuthorizationCodeTokenInvalid("invalid_client");
1977
+ }
1978
+ const client = clientOption.value;
1979
+ if (!client.allowedGrantTypes.includes("authorization_code")) {
1980
+ return oauthAuthorizationCodeTokenInvalid("unauthorized_client");
1981
+ }
1982
+ if (client.type === "confidential" && input.clientAuthenticated !== true) {
1983
+ return oauthAuthorizationCodeTokenInvalid("invalid_client");
1984
+ }
1985
+ const now = input.now ?? (yield* currentUnixMillis);
1986
+ const consumed = yield* dependencies.authorizationCodes
1987
+ .consume({
1988
+ code,
1989
+ clientId,
1990
+ redirectUri,
1991
+ ...(input.codeVerifier === undefined
1992
+ ? {}
1993
+ : { codeVerifier: input.codeVerifier }),
1994
+ now,
1995
+ })
1996
+ .pipe(Effect.mapError((cause) => oauthAuthorizationCodeTokenGrantError("exchange", "Failed to consume OAuth authorization code", cause)));
1997
+ if (!consumed.valid) {
1998
+ return oauthAuthorizationCodeTokenInvalid(oauthAuthorizationCodeConsumeFailureToTokenFailure(consumed.reason));
1999
+ }
2000
+ const tokens = yield* dependencies.issuer
2001
+ .issue({ client, code: consumed.code, now })
2002
+ .pipe(Effect.mapError((cause) => oauthAuthorizationCodeTokenGrantError("exchange", "Failed to issue OAuth tokens", cause)));
2003
+ return {
2004
+ valid: true,
2005
+ client: cloneOAuthClientRecord(client),
2006
+ code: consumed.code,
2007
+ tokens: cloneOAuthTokenResponse(tokens),
2008
+ };
2009
+ }),
2010
+ });
2011
+ export const makeOAuthRefreshTokenGrant = (dependencies) => OAuthRefreshTokenGrant.make({
2012
+ exchange: Effect.fn("auth.oauth.refresh_token.exchange")(function* (input) {
2013
+ if (input.grantType !== "refresh_token") {
2014
+ return oauthRefreshTokenInvalid("unsupported_grant_type");
2015
+ }
2016
+ const { refreshToken } = input;
2017
+ const clientId = oauthTokenGrantClientId(input.clientId);
2018
+ if (refreshToken === undefined || clientId === undefined) {
2019
+ return oauthRefreshTokenInvalid("invalid_request");
2020
+ }
2021
+ const clientOption = yield* dependencies.clients.find({ clientId }).pipe(Effect.mapError((cause) => oauthRefreshTokenGrantError("exchange", "Failed to load OAuth client", cause)));
2022
+ if (Option.isNone(clientOption) || clientOption.value.status !== "active") {
2023
+ return oauthRefreshTokenInvalid("invalid_client");
2024
+ }
2025
+ const client = clientOption.value;
2026
+ if (!client.allowedGrantTypes.includes("refresh_token")) {
2027
+ return oauthRefreshTokenInvalid("unauthorized_client");
2028
+ }
2029
+ if (client.type === "confidential" && input.clientAuthenticated !== true) {
2030
+ return oauthRefreshTokenInvalid("invalid_client");
2031
+ }
2032
+ const scopes = oauthTokenGrantScopes(input.scopes);
2033
+ if (!oauthTokenGrantScopesAllowed(scopes, client.allowedScopes)) {
2034
+ return oauthRefreshTokenInvalid("invalid_scope");
2035
+ }
2036
+ const now = input.now ?? (yield* currentUnixMillis);
2037
+ const issued = yield* dependencies.issuer
2038
+ .issue({
2039
+ client,
2040
+ refreshToken,
2041
+ ...(scopes.length === 0 ? {} : { scopes }),
2042
+ now,
2043
+ })
2044
+ .pipe(Effect.mapError((cause) => oauthRefreshTokenGrantError("exchange", "Failed to issue OAuth tokens", cause)));
2045
+ if (!issued.valid) {
2046
+ return oauthRefreshTokenInvalid(issued.reason);
2047
+ }
2048
+ return {
2049
+ valid: true,
2050
+ client: cloneOAuthClientRecord(client),
2051
+ tokens: cloneOAuthTokenResponse(issued.tokens),
2052
+ };
2053
+ }),
2054
+ });
2055
+ export const makeOAuthClientCredentialsGrant = (dependencies) => OAuthClientCredentialsGrant.make({
2056
+ exchange: Effect.fn("auth.oauth.client_credentials.exchange")(function* (input) {
2057
+ if (input.grantType !== "client_credentials") {
2058
+ return oauthClientCredentialsInvalid("unsupported_grant_type");
2059
+ }
2060
+ const clientId = oauthTokenGrantClientId(input.clientId);
2061
+ if (clientId === undefined) {
2062
+ return oauthClientCredentialsInvalid("invalid_request");
2063
+ }
2064
+ const clientOption = yield* dependencies.clients.find({ clientId }).pipe(Effect.mapError((cause) => oauthClientCredentialsGrantError("exchange", "Failed to load OAuth client", cause)));
2065
+ if (Option.isNone(clientOption) || clientOption.value.status !== "active") {
2066
+ return oauthClientCredentialsInvalid("invalid_client");
2067
+ }
2068
+ const client = clientOption.value;
2069
+ if (!client.allowedGrantTypes.includes("client_credentials")) {
2070
+ return oauthClientCredentialsInvalid("unauthorized_client");
2071
+ }
2072
+ if (client.type !== "confidential") {
2073
+ return oauthClientCredentialsInvalid("unauthorized_client");
2074
+ }
2075
+ if (input.clientAuthenticated !== true) {
2076
+ return oauthClientCredentialsInvalid("invalid_client");
2077
+ }
2078
+ const scopes = oauthTokenGrantScopes(input.scopes);
2079
+ if (!oauthTokenGrantScopesAllowed(scopes, client.allowedScopes)) {
2080
+ return oauthClientCredentialsInvalid("invalid_scope");
2081
+ }
2082
+ const now = input.now ?? (yield* currentUnixMillis);
2083
+ const issued = yield* dependencies.issuer
2084
+ .issue({
2085
+ client,
2086
+ ...(scopes.length === 0 ? {} : { scopes }),
2087
+ now,
2088
+ })
2089
+ .pipe(Effect.mapError((cause) => oauthClientCredentialsGrantError("exchange", "Failed to issue OAuth tokens", cause)));
2090
+ if (!issued.valid) {
2091
+ return oauthClientCredentialsInvalid(issued.reason);
2092
+ }
2093
+ return {
2094
+ valid: true,
2095
+ client: cloneOAuthClientRecord(client),
2096
+ tokens: cloneOAuthTokenResponse(issued.tokens),
2097
+ };
2098
+ }),
2099
+ });
2100
+ export const makeOAuthProviderModeOpaqueTokenIssuer = (dependencies) => {
2101
+ const accessTokenBytes = dependencies.accessTokenBytes ?? defaultOAuthProviderModeAccessTokenBytes;
2102
+ const refreshTokenBytes = dependencies.refreshTokenBytes ?? defaultOAuthProviderModeRefreshTokenBytes;
2103
+ const accessTokenTtl = dependencies.accessTokenTtl ?? defaultOAuthProviderModeAccessTokenTtl;
2104
+ const refreshTokenTtl = dependencies.refreshTokenTtl ?? defaultOAuthProviderModeRefreshTokenTtl;
2105
+ return {
2106
+ authorizationCode: OAuthAuthorizationCodeTokenIssuer.make({
2107
+ issue: Effect.fn("auth.oauth.provider_mode_opaque.authorization_code.issue")(function* (input) {
2108
+ const scopes = [...input.code.scopes];
2109
+ const accessToken = yield* generateOAuthProviderModeOpaqueToken(dependencies.crypto, accessTokenBytes, OAuthAccessToken, "access", oauthAuthorizationCodeTokenIssuerError);
2110
+ const accessTokenHash = yield* oauthProviderModeOpaqueTokenHash(dependencies.crypto, accessToken, oauthAuthorizationCodeTokenIssuerError);
2111
+ const accessTokenExpiresAt = oauthProviderModeOpaqueExpiresAt(input.now, accessTokenTtl);
2112
+ const accessTokenRecord = oauthProviderModeOpaqueTokenRecord(dependencies, {
2113
+ tokenHash: accessTokenHash,
2114
+ tokenType: "access_token",
2115
+ clientId: input.client.id,
2116
+ subject: input.code.subject,
2117
+ scopes,
2118
+ issuedAt: input.now,
2119
+ expiresAt: accessTokenExpiresAt,
2120
+ });
2121
+ yield* oauthProviderModeOpaqueStoreToken(dependencies.tokenStore, accessTokenRecord, oauthAuthorizationCodeTokenIssuerError);
2122
+ if (!input.client.allowedGrantTypes.includes("refresh_token")) {
2123
+ return oauthProviderModeOpaqueTokenResponse({
2124
+ accessToken,
2125
+ accessTokenIssuedAt: input.now,
2126
+ accessTokenExpiresAt,
2127
+ scopes,
2128
+ });
2129
+ }
2130
+ const refreshToken = yield* generateOAuthProviderModeOpaqueToken(dependencies.crypto, refreshTokenBytes, OAuthRefreshToken, "refresh", oauthAuthorizationCodeTokenIssuerError);
2131
+ const refreshTokenHash = yield* oauthProviderModeOpaqueTokenHash(dependencies.crypto, refreshToken, oauthAuthorizationCodeTokenIssuerError);
2132
+ const refreshTokenRecord = oauthProviderModeOpaqueTokenRecord(dependencies, {
2133
+ tokenHash: refreshTokenHash,
2134
+ tokenType: "refresh_token",
2135
+ clientId: input.client.id,
2136
+ subject: input.code.subject,
2137
+ scopes,
2138
+ issuedAt: input.now,
2139
+ expiresAt: oauthProviderModeOpaqueExpiresAt(input.now, refreshTokenTtl),
2140
+ });
2141
+ yield* oauthProviderModeOpaqueStoreToken(dependencies.tokenStore, refreshTokenRecord, oauthAuthorizationCodeTokenIssuerError);
2142
+ return oauthProviderModeOpaqueTokenResponse({
2143
+ accessToken,
2144
+ accessTokenIssuedAt: input.now,
2145
+ accessTokenExpiresAt,
2146
+ refreshToken,
2147
+ scopes,
2148
+ });
2149
+ }),
2150
+ }),
2151
+ refreshToken: OAuthRefreshTokenIssuer.make({
2152
+ issue: Effect.fn("auth.oauth.provider_mode_opaque.refresh_token.issue")(function* (input) {
2153
+ const tokenHash = yield* oauthProviderModeOpaqueTokenHash(dependencies.crypto, input.refreshToken, oauthRefreshTokenIssuerError);
2154
+ const recordOption = yield* dependencies.tokenStore
2155
+ .findByHash({ tokenHash })
2156
+ .pipe(Effect.mapError((cause) => oauthRefreshTokenIssuerError("Failed to load OAuth provider-mode refresh token", cause)));
2157
+ if (Option.isNone(recordOption)) {
2158
+ return oauthRefreshTokenIssueInvalid("invalid_grant");
2159
+ }
2160
+ const record = recordOption.value;
2161
+ if (record.tokenType !== "refresh_token" ||
2162
+ record.clientId !== input.client.id ||
2163
+ record.revokedAt !== undefined ||
2164
+ record.rotatedAt !== undefined ||
2165
+ Number(input.now) >= Number(record.expiresAt)) {
2166
+ return oauthRefreshTokenIssueInvalid("invalid_grant");
2167
+ }
2168
+ const requestedScopes = oauthTokenGrantScopes(input.scopes);
2169
+ if (!oauthTokenGrantScopesAllowed(requestedScopes, input.client.allowedScopes) ||
2170
+ !oauthProviderModeScopesSubset(requestedScopes, record.scopes)) {
2171
+ return oauthRefreshTokenIssueInvalid("invalid_scope");
2172
+ }
2173
+ const scopes = requestedScopes.length === 0 ? [...record.scopes] : requestedScopes;
2174
+ const accessToken = yield* generateOAuthProviderModeOpaqueToken(dependencies.crypto, accessTokenBytes, OAuthAccessToken, "access", oauthRefreshTokenIssuerError);
2175
+ const accessTokenHash = yield* oauthProviderModeOpaqueTokenHash(dependencies.crypto, accessToken, oauthRefreshTokenIssuerError);
2176
+ const refreshToken = yield* generateOAuthProviderModeOpaqueToken(dependencies.crypto, refreshTokenBytes, OAuthRefreshToken, "refresh", oauthRefreshTokenIssuerError);
2177
+ const refreshTokenHash = yield* oauthProviderModeOpaqueTokenHash(dependencies.crypto, refreshToken, oauthRefreshTokenIssuerError);
2178
+ const accessTokenExpiresAt = oauthProviderModeOpaqueExpiresAt(input.now, accessTokenTtl);
2179
+ yield* oauthProviderModeOpaqueStoreToken(dependencies.tokenStore, oauthProviderModeOpaqueTokenRecord(dependencies, {
2180
+ tokenHash: accessTokenHash,
2181
+ tokenType: "access_token",
2182
+ clientId: input.client.id,
2183
+ subject: record.subject,
2184
+ scopes,
2185
+ issuedAt: input.now,
2186
+ expiresAt: accessTokenExpiresAt,
2187
+ }), oauthRefreshTokenIssuerError);
2188
+ yield* oauthProviderModeOpaqueStoreToken(dependencies.tokenStore, oauthProviderModeOpaqueTokenRecord(dependencies, {
2189
+ tokenHash: refreshTokenHash,
2190
+ tokenType: "refresh_token",
2191
+ clientId: input.client.id,
2192
+ subject: record.subject,
2193
+ scopes,
2194
+ issuedAt: input.now,
2195
+ expiresAt: oauthProviderModeOpaqueExpiresAt(input.now, refreshTokenTtl),
2196
+ }), oauthRefreshTokenIssuerError);
2197
+ const rotated = yield* dependencies.tokenStore
2198
+ .rotateRefreshToken({
2199
+ tokenHash,
2200
+ replacedByTokenHash: refreshTokenHash,
2201
+ rotatedAt: input.now,
2202
+ })
2203
+ .pipe(Effect.mapError((cause) => oauthRefreshTokenIssuerError("Failed to rotate OAuth provider-mode refresh token", cause)));
2204
+ if (Option.isNone(rotated)) {
2205
+ return oauthRefreshTokenIssueInvalid("invalid_grant");
2206
+ }
2207
+ return {
2208
+ valid: true,
2209
+ tokens: oauthProviderModeOpaqueTokenResponse({
2210
+ accessToken,
2211
+ accessTokenIssuedAt: input.now,
2212
+ accessTokenExpiresAt,
2213
+ refreshToken,
2214
+ scopes,
2215
+ }),
2216
+ };
2217
+ }),
2218
+ }),
2219
+ clientCredentials: OAuthClientCredentialsTokenIssuer.make({
2220
+ issue: Effect.fn("auth.oauth.provider_mode_opaque.client_credentials.issue")(function* (input) {
2221
+ const scopes = [...(input.scopes ?? [])];
2222
+ if (!oauthTokenGrantScopesAllowed(scopes, input.client.allowedScopes)) {
2223
+ return oauthClientCredentialsIssueInvalid("invalid_scope");
2224
+ }
2225
+ const subject = yield* Effect.try({
2226
+ try: () => dependencies.clientCredentialsSubject === undefined
2227
+ ? String(input.client.id)
2228
+ : dependencies.clientCredentialsSubject(input),
2229
+ catch: (cause) => oauthClientCredentialsTokenIssuerError("Failed to resolve OAuth client credentials subject", cause),
2230
+ }).pipe(Effect.flatMap((value) => {
2231
+ const resolvedSubject = oauthTokenGrantString(value);
2232
+ return resolvedSubject === undefined
2233
+ ? Effect.fail(oauthClientCredentialsTokenIssuerError("OAuth client credentials subject must not be empty"))
2234
+ : Effect.succeed(resolvedSubject);
2235
+ }));
2236
+ const accessToken = yield* generateOAuthProviderModeOpaqueToken(dependencies.crypto, accessTokenBytes, OAuthAccessToken, "access", oauthClientCredentialsTokenIssuerError);
2237
+ const accessTokenHash = yield* oauthProviderModeOpaqueTokenHash(dependencies.crypto, accessToken, oauthClientCredentialsTokenIssuerError);
2238
+ const accessTokenExpiresAt = oauthProviderModeOpaqueExpiresAt(input.now, accessTokenTtl);
2239
+ yield* oauthProviderModeOpaqueStoreToken(dependencies.tokenStore, oauthProviderModeOpaqueTokenRecord(dependencies, {
2240
+ tokenHash: accessTokenHash,
2241
+ tokenType: "access_token",
2242
+ clientId: input.client.id,
2243
+ subject,
2244
+ scopes,
2245
+ issuedAt: input.now,
2246
+ expiresAt: accessTokenExpiresAt,
2247
+ }), oauthClientCredentialsTokenIssuerError);
2248
+ return {
2249
+ valid: true,
2250
+ tokens: oauthProviderModeOpaqueTokenResponse({
2251
+ accessToken,
2252
+ accessTokenIssuedAt: input.now,
2253
+ accessTokenExpiresAt,
2254
+ scopes,
2255
+ }),
2256
+ };
2257
+ }),
2258
+ }),
2259
+ };
2260
+ };
2261
+ export const makeOAuthClientSecretVerification = (dependencies) => OAuthClientSecretVerification.make({
2262
+ authenticateClient: Effect.fn("auth.oauth.client_secret.authenticate_client")(function* (input) {
2263
+ const prefix = yield* dependencies.clientSecrets.parse(input.clientSecret).pipe(Effect.catchTag("OAuthClientSecretError", () => Effect.succeed(undefined)));
2264
+ if (prefix === undefined) {
2265
+ return false;
2266
+ }
2267
+ const secret = yield* dependencies.secretStore
2268
+ .findByClientIdAndPrefix({ clientId: input.clientId, prefix })
2269
+ .pipe(Effect.mapError((cause) => oauthClientSecretVerificationError("Failed to load OAuth client secret", cause)));
2270
+ if (Option.isNone(secret) || secret.value.revokedAt !== undefined) {
2271
+ return false;
2272
+ }
2273
+ if (!secret.value.authenticationMethods.includes(input.method)) {
2274
+ return false;
2275
+ }
2276
+ const now = input.now ?? (yield* currentUnixMillis);
2277
+ if (secret.value.expiresAt !== undefined &&
2278
+ Number(secret.value.expiresAt) <= Number(now)) {
2279
+ return false;
2280
+ }
2281
+ const valid = yield* dependencies.clientSecrets
2282
+ .verify({ secret: input.clientSecret, hash: secret.value.secretHash })
2283
+ .pipe(Effect.mapError((cause) => oauthClientSecretVerificationError("Failed to verify OAuth client secret", cause)));
2284
+ if (!valid) {
2285
+ return false;
2286
+ }
2287
+ const updated = yield* dependencies.secretStore
2288
+ .markUsed({
2289
+ clientId: input.clientId,
2290
+ prefix,
2291
+ lastUsedAt: now,
2292
+ ...(input.metadata === undefined ? {} : { metadata: input.metadata }),
2293
+ })
2294
+ .pipe(Effect.mapError((cause) => oauthClientSecretVerificationError("Failed to mark OAuth client secret as used", cause)));
2295
+ return Option.isSome(updated);
2296
+ }),
2297
+ });
2298
+ export const makeOAuthTokenRevocation = (dependencies) => OAuthTokenRevocation.make({
2299
+ revoke: Effect.fn("auth.oauth.token_revocation.revoke")(function* (input) {
2300
+ const { token } = input;
2301
+ const tokenValue = token === undefined ? undefined : String(Redacted.value(token)).trim();
2302
+ const clientId = oauthTokenGrantClientId(input.clientId);
2303
+ const tokenTypeHint = oauthTokenGrantString(input.tokenTypeHint);
2304
+ if (token === undefined || tokenValue === undefined || clientId === undefined) {
2305
+ return oauthTokenRevocationInvalid("invalid_request");
2306
+ }
2307
+ if (tokenTypeHint !== undefined &&
2308
+ tokenTypeHint !== "access_token" &&
2309
+ tokenTypeHint !== "refresh_token") {
2310
+ return oauthTokenRevocationInvalid("unsupported_token_type");
2311
+ }
2312
+ const clientOption = yield* dependencies.clients.find({ clientId }).pipe(Effect.mapError((cause) => oauthTokenRevocationError("revoke", "Failed to load OAuth client", cause)));
2313
+ if (Option.isNone(clientOption) || clientOption.value.status !== "active") {
2314
+ return oauthTokenRevocationInvalid("invalid_client");
2315
+ }
2316
+ const client = clientOption.value;
2317
+ if (client.type !== "confidential" || input.clientAuthenticated !== true) {
2318
+ return oauthTokenRevocationInvalid("invalid_client");
2319
+ }
2320
+ const now = input.now ?? (yield* currentUnixMillis);
2321
+ yield* dependencies.revoker
2322
+ .revoke({
2323
+ client,
2324
+ token,
2325
+ ...(tokenTypeHint === undefined ? {} : { tokenTypeHint }),
2326
+ now,
2327
+ })
2328
+ .pipe(Effect.mapError((cause) => oauthTokenRevocationError("revoke", "Failed to revoke OAuth token", cause)));
2329
+ return {
2330
+ valid: true,
2331
+ client: cloneOAuthClientRecord(client),
2332
+ };
2333
+ }),
2334
+ });
2335
+ export const makeOAuthTokenIntrospection = (dependencies) => OAuthTokenIntrospection.make({
2336
+ introspect: Effect.fn("auth.oauth.token_introspection.introspect")(function* (input) {
2337
+ const { token } = input;
2338
+ const tokenValue = token === undefined ? undefined : String(Redacted.value(token)).trim();
2339
+ const clientId = oauthTokenGrantClientId(input.clientId);
2340
+ const tokenTypeHint = oauthTokenGrantString(input.tokenTypeHint);
2341
+ if (token === undefined || tokenValue === undefined || clientId === undefined) {
2342
+ return oauthTokenIntrospectionInvalid("invalid_request");
2343
+ }
2344
+ const clientOption = yield* dependencies.clients.find({ clientId }).pipe(Effect.mapError((cause) => oauthTokenIntrospectionError("introspect", "Failed to load OAuth client", cause)));
2345
+ if (Option.isNone(clientOption) || clientOption.value.status !== "active") {
2346
+ return oauthTokenIntrospectionInvalid("invalid_client");
2347
+ }
2348
+ const client = clientOption.value;
2349
+ if (client.type !== "confidential" || input.clientAuthenticated !== true) {
2350
+ return oauthTokenIntrospectionInvalid("invalid_client");
2351
+ }
2352
+ const now = input.now ?? (yield* currentUnixMillis);
2353
+ const introspection = yield* dependencies.introspector
2354
+ .introspect({
2355
+ client,
2356
+ token,
2357
+ ...(tokenTypeHint === undefined ? {} : { tokenTypeHint }),
2358
+ now,
2359
+ })
2360
+ .pipe(Effect.mapError((cause) => oauthTokenIntrospectionError("introspect", "Failed to introspect OAuth token", cause)));
2361
+ return {
2362
+ valid: true,
2363
+ client: cloneOAuthClientRecord(client),
2364
+ introspection: cloneOAuthTokenIntrospectionResponse(introspection),
2365
+ };
2366
+ }),
2367
+ });
2368
+ const oauthProviderAuthorizationInvalid = (reason, input) => ({
2369
+ valid: false,
2370
+ reason,
2371
+ ...(input?.redirectUri === undefined ? {} : { redirectUri: input.redirectUri }),
2372
+ ...(input?.state === undefined ? {} : { state: input.state }),
2373
+ ...(input?.client === undefined
2374
+ ? {}
2375
+ : { client: cloneOAuthClientRecord(input.client) }),
2376
+ ...(input?.errorDescription === undefined
2377
+ ? {}
2378
+ : { errorDescription: input.errorDescription }),
2379
+ });
2380
+ const oauthProviderAuthorizationString = (value) => {
2381
+ const trimmed = value?.trim();
2382
+ return trimmed === undefined || trimmed === "" ? undefined : trimmed;
2383
+ };
2384
+ const oauthProviderAuthorizationClientId = (value) => {
2385
+ const clientId = oauthProviderAuthorizationString(value === undefined ? undefined : String(value));
2386
+ return clientId === undefined ? undefined : OAuthClientId(clientId);
2387
+ };
2388
+ const oauthProviderAuthorizationScopes = (scopes) => {
2389
+ const values = typeof scopes === "string" ? scopes.split(/\s+/) : scopes ?? [];
2390
+ const unique = new Set();
2391
+ for (const value of values) {
2392
+ const scope = value.trim();
2393
+ if (scope !== "") {
2394
+ unique.add(scope);
2395
+ }
2396
+ }
2397
+ return [...unique];
2398
+ };
2399
+ const oauthProviderAuthorizationScopeAllowed = (scopes, allowedScopes) => allowedScopes === undefined || scopes.every((scope) => allowedScopes.includes(scope));
2400
+ const oauthProviderAuthorizationScopeSubset = (scopes, requestedScopes) => scopes.every((scope) => requestedScopes.includes(scope));
2401
+ const oauthProviderAuthorizationClientAllowsCode = (client) => client.allowedResponseTypes.includes("code") &&
2402
+ client.allowedGrantTypes.includes("authorization_code");
2403
+ const oauthProviderAuthorizationGrantedScopesValid = (scopes, requestedScopes, allowedScopes) => oauthProviderAuthorizationScopeSubset(scopes, requestedScopes) &&
2404
+ oauthProviderAuthorizationScopeAllowed(scopes, allowedScopes);
2405
+ export const makeOAuthProviderAuthorization = (dependencies) => OAuthProviderAuthorization.make({
2406
+ authorize: Effect.fn("auth.oauth.provider_authorization.authorize")(function* (input) {
2407
+ const state = oauthProviderAuthorizationString(input.state);
2408
+ const clientId = oauthProviderAuthorizationClientId(input.clientId);
2409
+ const redirectUri = oauthProviderAuthorizationString(input.redirectUri);
2410
+ if (clientId === undefined || redirectUri === undefined) {
2411
+ return oauthProviderAuthorizationInvalid("invalid_request");
2412
+ }
2413
+ const clientOption = yield* dependencies.clients.find({ clientId }).pipe(Effect.mapError((cause) => oauthProviderAuthorizationError("authorize", "Failed to load OAuth client", cause)));
2414
+ if (Option.isNone(clientOption) || clientOption.value.status !== "active") {
2415
+ return oauthProviderAuthorizationInvalid("invalid_request");
2416
+ }
2417
+ const client = clientOption.value;
2418
+ if (!client.redirectUris.includes(redirectUri)) {
2419
+ return oauthProviderAuthorizationInvalid("invalid_request");
2420
+ }
2421
+ const redirectError = (reason, errorDescription) => oauthProviderAuthorizationInvalid(reason, {
2422
+ client,
2423
+ redirectUri,
2424
+ state,
2425
+ ...(errorDescription === undefined ? {} : { errorDescription }),
2426
+ });
2427
+ if (input.responseType !== "code") {
2428
+ return redirectError("unsupported_response_type");
2429
+ }
2430
+ if (!oauthProviderAuthorizationClientAllowsCode(client)) {
2431
+ return redirectError("unauthorized_client");
2432
+ }
2433
+ const codeChallenge = oauthProviderAuthorizationString(input.codeChallenge === undefined ? undefined : String(input.codeChallenge));
2434
+ if (codeChallenge === undefined ||
2435
+ oauthProviderAuthorizationString(input.codeChallengeMethod) !== "S256") {
2436
+ return redirectError("invalid_request");
2437
+ }
2438
+ const requestedScopes = oauthProviderAuthorizationScopes(input.scopes);
2439
+ if (!oauthProviderAuthorizationScopeAllowed(requestedScopes, client.allowedScopes)) {
2440
+ return redirectError("invalid_scope");
2441
+ }
2442
+ const decision = yield* dependencies.policy
2443
+ .decide({
2444
+ client: cloneOAuthClientRecord(client),
2445
+ requestedScopes,
2446
+ ...(state === undefined ? {} : { state }),
2447
+ request: input,
2448
+ })
2449
+ .pipe(Effect.mapError((cause) => oauthProviderAuthorizationError("authorize", "Failed to decide OAuth authorization request", cause)));
2450
+ if (decision.type === "deny") {
2451
+ return redirectError(decision.reason, decision.errorDescription);
2452
+ }
2453
+ const scopes = oauthProviderAuthorizationScopes(decision.scopes ?? requestedScopes);
2454
+ if (!oauthProviderAuthorizationGrantedScopesValid(scopes, requestedScopes, client.allowedScopes)) {
2455
+ return redirectError("invalid_scope");
2456
+ }
2457
+ const code = yield* dependencies.authorizationCodes
2458
+ .issue({
2459
+ clientId,
2460
+ subject: decision.subject,
2461
+ redirectUri,
2462
+ scopes,
2463
+ codeChallenge: OAuthCodeChallenge(codeChallenge),
2464
+ codeChallengeMethod: "S256",
2465
+ ...(input.now === undefined ? {} : { issuedAt: input.now }),
2466
+ ...(decision.metadata === undefined
2467
+ ? {}
2468
+ : { metadata: decision.metadata }),
2469
+ })
2470
+ .pipe(Effect.mapError((cause) => oauthProviderAuthorizationError("authorize", "Failed to issue OAuth authorization code", cause)));
2471
+ return {
2472
+ valid: true,
2473
+ client: cloneOAuthClientRecord(client),
2474
+ redirectUri,
2475
+ ...(state === undefined ? {} : { state }),
2476
+ code,
2477
+ };
2478
+ }),
2479
+ });
2480
+ export const issueOAuthAuthorizationCode = (input) => OAuthAuthorizationCodeGrant.use((grant) => grant.issue(input));
2481
+ export const consumeOAuthAuthorizationCode = (input) => OAuthAuthorizationCodeGrant.use((grant) => grant.consume(input));
2482
+ export const exchangeOAuthAuthorizationCodeToken = (input) => OAuthAuthorizationCodeTokenGrant.use((grant) => grant.exchange(input));
2483
+ export const exchangeOAuthRefreshToken = (input) => OAuthRefreshTokenGrant.use((grant) => grant.exchange(input));
2484
+ export const exchangeOAuthClientCredentialsToken = (input) => OAuthClientCredentialsGrant.use((grant) => grant.exchange(input));
2485
+ export const revokeOAuthToken = (input) => OAuthTokenRevocation.use((revocation) => revocation.revoke(input));
2486
+ export const introspectOAuthToken = (input) => OAuthTokenIntrospection.use((introspection) => introspection.introspect(input));
2487
+ export const authorizeOAuthProviderAuthorization = (input) => OAuthProviderAuthorization.use((authorization) => authorization.authorize(input));
2488
+ export const makeOAuthPkce = (input) => Effect.gen(function* () {
2489
+ const crypto = yield* Crypto;
2490
+ return yield* makeOAuthPkceWithCrypto({ crypto }, input);
2491
+ });
2492
+ const appendParams = (url, params) => {
2493
+ if (params === undefined) {
2494
+ return;
2495
+ }
2496
+ for (const [key, value] of Object.entries(params)) {
2497
+ if (value !== undefined) {
2498
+ url.searchParams.set(key, value);
2499
+ }
2500
+ }
2501
+ };
2502
+ const makeAuthorizationUrl = (input) => Effect.try({
2503
+ try: () => {
2504
+ const url = new URL(input.provider.authorizationEndpoint);
2505
+ appendParams(url, input.provider.authorizationParams);
2506
+ appendParams(url, input.params);
2507
+ appendParams(url, {
2508
+ response_type: "code",
2509
+ client_id: input.provider.clientId,
2510
+ redirect_uri: input.redirectUri,
2511
+ scope: input.scopes.length === 0 ? undefined : input.scopes.join(" "),
2512
+ state: input.state,
2513
+ code_challenge: input.pkce.codeChallenge,
2514
+ code_challenge_method: input.pkce.codeChallengeMethod,
2515
+ nonce: input.nonce,
2516
+ });
2517
+ return url.toString();
2518
+ },
2519
+ catch: (cause) => oauthStateError("authorization-url", "Failed to build OAuth authorization URL", cause),
2520
+ });
2521
+ export const makeOAuthState = (dependencies) => OAuthState.make({
2522
+ start: Effect.fn("auth.oauth_state.start")(function* (input) {
2523
+ const stateSecretBytes = yield* validateByteLength(dependencies.stateSecretBytes ?? defaultOAuthStateSecretBytes, "start", "OAuth state secret");
2524
+ const stateSecret = yield* dependencies.crypto.randomToken(stateSecretBytes).pipe(Effect.map(Redacted.make), Effect.mapError((cause) => oauthStateError("start", "Failed to generate OAuth state secret", cause)));
2525
+ const pkce = yield* makeOAuthPkceWithCrypto(dependencies, {
2526
+ codeVerifier: input.codeVerifier,
2527
+ verifierBytes: dependencies.codeVerifierBytes,
2528
+ });
2529
+ const includeNonce = input.includeNonce ?? input.provider.issuer !== undefined;
2530
+ const nonce = input.nonce === undefined && !includeNonce
2531
+ ? undefined
2532
+ : input.nonce === undefined
2533
+ ? yield* dependencies.crypto
2534
+ .randomToken(dependencies.nonceBytes ?? defaultOAuthNonceBytes)
2535
+ .pipe(Effect.map(OAuthNonce), Effect.mapError((cause) => oauthStateError("start", "Failed to generate OAuth nonce", cause)))
2536
+ : OAuthNonce(input.nonce);
2537
+ const redirectUri = input.redirectUri ?? input.provider.redirectUri;
2538
+ const scopes = input.scopes ?? input.provider.scopes ?? [];
2539
+ const issued = yield* dependencies.challenge
2540
+ .issue({
2541
+ type: "oauth-state",
2542
+ subject: input.provider.id,
2543
+ ttl: input.ttl ?? defaultOAuthStateTtl,
2544
+ secret: stateSecret,
2545
+ metadata: {
2546
+ providerId: input.provider.id,
2547
+ redirectUri,
2548
+ scopes,
2549
+ codeChallenge: pkce.codeChallenge,
2550
+ ...(nonce === undefined ? {} : { nonce }),
2551
+ ...(input.metadata === undefined ? {} : { metadata: input.metadata }),
2552
+ },
2553
+ })
2554
+ .pipe(Effect.mapError((cause) => oauthStateError("start", "Failed to issue OAuth state challenge", cause)));
2555
+ const state = encodeOAuthState({ challengeId: issued.id, secret: stateSecret });
2556
+ const authorizationUrl = yield* makeAuthorizationUrl({
2557
+ provider: input.provider,
2558
+ redirectUri,
2559
+ scopes,
2560
+ params: input.authorizationParams,
2561
+ state,
2562
+ pkce,
2563
+ nonce,
2564
+ });
2565
+ return {
2566
+ providerId: input.provider.id,
2567
+ authorizationUrl,
2568
+ state,
2569
+ challengeId: issued.id,
2570
+ expiresAt: issued.expiresAt,
2571
+ codeVerifier: pkce.codeVerifier,
2572
+ codeChallenge: pkce.codeChallenge,
2573
+ codeChallengeMethod: pkce.codeChallengeMethod,
2574
+ ...(nonce === undefined ? {} : { nonce }),
2575
+ redirectUri,
2576
+ scopes,
2577
+ };
2578
+ }),
2579
+ verify: Effect.fn("auth.oauth_state.verify")(function* (input) {
2580
+ const parsed = yield* parseOAuthState(input.state);
2581
+ const verified = yield* dependencies.challenge
2582
+ .verify({
2583
+ challengeId: parsed.challengeId,
2584
+ type: "oauth-state",
2585
+ secret: parsed.secret,
2586
+ })
2587
+ .pipe(Effect.mapError((cause) => oauthStateError("verify", "Failed to verify OAuth state challenge", cause)));
2588
+ const metadata = verified.metadata ?? {};
2589
+ const appMetadata = metadataValue(typeof metadata.metadata === "object" && metadata.metadata !== null
2590
+ ? metadata.metadata
2591
+ : undefined);
2592
+ const codeChallenge = stringFromUnknown(metadata.codeChallenge);
2593
+ const nonce = stringFromUnknown(metadata.nonce);
2594
+ return {
2595
+ challengeId: parsed.challengeId,
2596
+ providerId: OAuthProviderId(verified.subject),
2597
+ redirectUri: stringFromUnknown(metadata.redirectUri),
2598
+ scopes: stringArrayFromUnknown(metadata.scopes),
2599
+ ...(codeChallenge === undefined
2600
+ ? {}
2601
+ : { codeChallenge: OAuthCodeChallenge(codeChallenge) }),
2602
+ ...(nonce === undefined ? {} : { nonce: OAuthNonce(nonce) }),
2603
+ ...(appMetadata === undefined ? {} : { metadata: appMetadata }),
2604
+ };
2605
+ }),
2606
+ });
2607
+ export const OAuthStateLive = Layer.effect(OAuthState)(Effect.gen(function* () {
2608
+ const crypto = yield* Crypto;
2609
+ const challenge = yield* Challenge;
2610
+ return makeOAuthState({ crypto, challenge });
2611
+ }));
2612
+ export const OAuthAuthorizationCodeGrantLive = Layer.effect(OAuthAuthorizationCodeGrant)(Effect.gen(function* () {
2613
+ const crypto = yield* Crypto;
2614
+ const store = yield* OAuthAuthorizationCodeStore;
2615
+ return makeOAuthAuthorizationCodeGrant({ crypto, store });
2616
+ }));
2617
+ export const OAuthAuthorizationCodeTokenGrantLive = Layer.effect(OAuthAuthorizationCodeTokenGrant)(Effect.gen(function* () {
2618
+ const clients = yield* OAuthClientRegistry;
2619
+ const authorizationCodes = yield* OAuthAuthorizationCodeGrant;
2620
+ const issuer = yield* OAuthAuthorizationCodeTokenIssuer;
2621
+ return makeOAuthAuthorizationCodeTokenGrant({
2622
+ clients,
2623
+ authorizationCodes,
2624
+ issuer,
2625
+ });
2626
+ }));
2627
+ export const OAuthRefreshTokenGrantLive = Layer.effect(OAuthRefreshTokenGrant)(Effect.gen(function* () {
2628
+ const clients = yield* OAuthClientRegistry;
2629
+ const issuer = yield* OAuthRefreshTokenIssuer;
2630
+ return makeOAuthRefreshTokenGrant({ clients, issuer });
2631
+ }));
2632
+ export const OAuthClientCredentialsGrantLive = Layer.effect(OAuthClientCredentialsGrant)(Effect.gen(function* () {
2633
+ const clients = yield* OAuthClientRegistry;
2634
+ const issuer = yield* OAuthClientCredentialsTokenIssuer;
2635
+ return makeOAuthClientCredentialsGrant({ clients, issuer });
2636
+ }));
2637
+ export const OAuthProviderModeOpaqueTokenIssuerLive = (options = {}) => Layer.mergeAll(Layer.effect(OAuthAuthorizationCodeTokenIssuer)(Effect.gen(function* () {
2638
+ const crypto = yield* Crypto;
2639
+ const tokenStore = yield* OAuthProviderModeTokenStore;
2640
+ return makeOAuthProviderModeOpaqueTokenIssuer({
2641
+ ...options,
2642
+ crypto,
2643
+ tokenStore,
2644
+ }).authorizationCode;
2645
+ })), Layer.effect(OAuthRefreshTokenIssuer)(Effect.gen(function* () {
2646
+ const crypto = yield* Crypto;
2647
+ const tokenStore = yield* OAuthProviderModeTokenStore;
2648
+ return makeOAuthProviderModeOpaqueTokenIssuer({
2649
+ ...options,
2650
+ crypto,
2651
+ tokenStore,
2652
+ }).refreshToken;
2653
+ })), Layer.effect(OAuthClientCredentialsTokenIssuer)(Effect.gen(function* () {
2654
+ const crypto = yield* Crypto;
2655
+ const tokenStore = yield* OAuthProviderModeTokenStore;
2656
+ return makeOAuthProviderModeOpaqueTokenIssuer({
2657
+ ...options,
2658
+ crypto,
2659
+ tokenStore,
2660
+ }).clientCredentials;
2661
+ })));
2662
+ export const OAuthTokenRevocationLive = Layer.effect(OAuthTokenRevocation)(Effect.gen(function* () {
2663
+ const clients = yield* OAuthClientRegistry;
2664
+ const revoker = yield* OAuthTokenRevoker;
2665
+ return makeOAuthTokenRevocation({ clients, revoker });
2666
+ }));
2667
+ export const OAuthTokenIntrospectionLive = Layer.effect(OAuthTokenIntrospection)(Effect.gen(function* () {
2668
+ const clients = yield* OAuthClientRegistry;
2669
+ const introspector = yield* OAuthTokenIntrospector;
2670
+ return makeOAuthTokenIntrospection({ clients, introspector });
2671
+ }));
2672
+ export const OAuthProviderAuthorizationLive = Layer.effect(OAuthProviderAuthorization)(Effect.gen(function* () {
2673
+ const clients = yield* OAuthClientRegistry;
2674
+ const authorizationCodes = yield* OAuthAuthorizationCodeGrant;
2675
+ const policy = yield* OAuthProviderAuthorizationPolicy;
2676
+ return makeOAuthProviderAuthorization({ clients, authorizationCodes, policy });
2677
+ }));
2678
+ export const makeOAuthProviders = (providers) => {
2679
+ const providersById = new Map(providers.map((provider) => [provider.id, cloneProviderConfig(provider)]));
2680
+ return OAuthProviders.make({
2681
+ get: (id) => Effect.sync(() => {
2682
+ const provider = providersById.get(id);
2683
+ return provider === undefined
2684
+ ? Option.none()
2685
+ : Option.some(cloneProviderConfig(provider));
2686
+ }),
2687
+ });
2688
+ };
2689
+ export const OAuthProvidersLive = (providers) => Layer.succeed(OAuthProviders)(makeOAuthProviders(providers));
2690
+ export const OAuthClientSecretsLive = Layer.effect(OAuthClientSecrets)(Effect.gen(function* () {
2691
+ const crypto = yield* Crypto;
2692
+ return makeOAuthClientSecrets({ crypto });
2693
+ }));
2694
+ export const OAuthClientSecretVerificationLive = Layer.effect(OAuthClientSecretVerification)(Effect.gen(function* () {
2695
+ const clientSecrets = yield* OAuthClientSecrets;
2696
+ const secretStore = yield* OAuthClientSecretStore;
2697
+ return makeOAuthClientSecretVerification({ clientSecrets, secretStore });
2698
+ }));
2699
+ export const makeOAuthClientRegistryFromStore = (store) => OAuthClientRegistry.make({
2700
+ find: (input) => store.find(input).pipe(Effect.mapError((cause) => oauthClientRegistryError("find", "Failed to load OAuth client", cause))),
2701
+ validateRedirectUri: (input) => store.validateRedirectUri(input).pipe(Effect.mapError((cause) => oauthClientRegistryError("validate-redirect-uri", "Failed to validate OAuth client redirect URI", cause))),
2702
+ });
2703
+ export const OAuthClientRegistryFromStoreLive = Layer.effect(OAuthClientRegistry)(Effect.gen(function* () {
2704
+ const store = yield* OAuthClientStore;
2705
+ return makeOAuthClientRegistryFromStore(store);
2706
+ }));
2707
+ export const makeOAuthClientRegistryMemory = (clients = []) => {
2708
+ const clientsById = new Map(clients.map((client) => [client.id, cloneOAuthClientRecord(client)]));
2709
+ return OAuthClientRegistry.make({
2710
+ find: (input) => Effect.sync(() => {
2711
+ const client = clientsById.get(input.clientId);
2712
+ return client === undefined
2713
+ ? Option.none()
2714
+ : Option.some(cloneOAuthClientRecord(client));
2715
+ }),
2716
+ validateRedirectUri: (input) => Effect.sync(() => {
2717
+ const client = clientsById.get(input.clientId);
2718
+ return validateOAuthClientRedirectUriRecord(client, input);
2719
+ }),
2720
+ });
2721
+ };
2722
+ export const OAuthClientRegistryMemoryLive = (clients = []) => Layer.succeed(OAuthClientRegistry)(makeOAuthClientRegistryMemory(clients));
2723
+ export const makeOAuthClientStoreMemory = (clients = []) => {
2724
+ const clientsById = new Map(clients.map((client) => [client.id, cloneOAuthClientRecord(client)]));
2725
+ return OAuthClientStore.make({
2726
+ upsert: (record) => Effect.sync(() => {
2727
+ clientsById.set(record.id, cloneOAuthClientRecord(record));
2728
+ }),
2729
+ find: (input) => Effect.sync(() => {
2730
+ const client = clientsById.get(input.clientId);
2731
+ return client === undefined
2732
+ ? Option.none()
2733
+ : Option.some(cloneOAuthClientRecord(client));
2734
+ }),
2735
+ list: (input = {}) => Effect.sync(() => [...clientsById.values()]
2736
+ .filter((client) => input.includeDisabled === true || client.status === "active")
2737
+ .map(cloneOAuthClientRecord)),
2738
+ validateRedirectUri: (input) => Effect.sync(() => validateOAuthClientRedirectUriRecord(clientsById.get(input.clientId), input)),
2739
+ });
2740
+ };
2741
+ export const OAuthClientStoreMemoryLive = (clients = []) => Layer.succeed(OAuthClientStore)(makeOAuthClientStoreMemory(clients));
2742
+ const oauthConsentMemoryKey = (input) => `${input.userId}\u0000${input.clientId}`;
2743
+ export const makeOAuthConsentStoreMemory = (records = []) => {
2744
+ const recordsByKey = new Map(records.map((record) => [
2745
+ oauthConsentMemoryKey(record),
2746
+ cloneOAuthConsentRecord(record),
2747
+ ]));
2748
+ return OAuthConsentStore.make({
2749
+ upsert: (record) => Effect.sync(() => {
2750
+ recordsByKey.set(oauthConsentMemoryKey(record), cloneOAuthConsentRecord(record));
2751
+ }),
2752
+ findActive: (input) => Effect.sync(() => {
2753
+ const record = recordsByKey.get(oauthConsentMemoryKey(input));
2754
+ return record === undefined ||
2755
+ !isOAuthConsentActive(record, input.now, input.scopes)
2756
+ ? Option.none()
2757
+ : Option.some(cloneOAuthConsentRecord(record));
2758
+ }),
2759
+ listForUser: (input) => Effect.sync(() => [...recordsByKey.values()]
2760
+ .filter((record) => record.userId === input.userId)
2761
+ .filter((record) => input.includeInactive === true ||
2762
+ isOAuthConsentActive(record, input.now))
2763
+ .map(cloneOAuthConsentRecord)),
2764
+ revoke: (input) => Effect.sync(() => {
2765
+ const key = oauthConsentMemoryKey(input);
2766
+ const record = recordsByKey.get(key);
2767
+ if (record === undefined) {
2768
+ return Option.none();
2769
+ }
2770
+ const updated = cloneOAuthConsentRecord({
2771
+ ...record,
2772
+ revokedAt: record.revokedAt ?? input.revokedAt,
2773
+ metadata: input.reason === undefined
2774
+ ? record.metadata
2775
+ : { ...record.metadata, revokeReason: input.reason },
2776
+ });
2777
+ recordsByKey.set(key, updated);
2778
+ return Option.some(cloneOAuthConsentRecord(updated));
2779
+ }),
2780
+ deleteInactive: (input) => Effect.sync(() => {
2781
+ let deleted = 0;
2782
+ for (const [key, record] of recordsByKey) {
2783
+ const expired = input.beforeExpiresAt !== undefined &&
2784
+ record.expiresAt !== undefined &&
2785
+ Number(record.expiresAt) < Number(input.beforeExpiresAt);
2786
+ const revoked = input.beforeRevokedAt !== undefined &&
2787
+ record.revokedAt !== undefined &&
2788
+ Number(record.revokedAt) < Number(input.beforeRevokedAt);
2789
+ if (expired || revoked) {
2790
+ recordsByKey.delete(key);
2791
+ deleted += 1;
2792
+ }
2793
+ }
2794
+ return deleted;
2795
+ }),
2796
+ });
2797
+ };
2798
+ export const OAuthConsentStoreMemoryLive = (records = []) => Layer.succeed(OAuthConsentStore)(makeOAuthConsentStoreMemory(records));
2799
+ const oauthClientSecretMemoryKey = (input) => `${input.clientId}\u0000${input.prefix}`;
2800
+ export const makeOAuthClientSecretStoreMemory = () => {
2801
+ const recordsByKey = new Map();
2802
+ return OAuthClientSecretStore.make({
2803
+ upsert: (record) => Effect.sync(() => {
2804
+ recordsByKey.set(oauthClientSecretMemoryKey(record), cloneOAuthClientSecretRecord(record));
2805
+ }),
2806
+ findByClientIdAndPrefix: (input) => Effect.sync(() => {
2807
+ const record = recordsByKey.get(oauthClientSecretMemoryKey(input));
2808
+ return record === undefined
2809
+ ? Option.none()
2810
+ : Option.some(cloneOAuthClientSecretRecord(record));
2811
+ }),
2812
+ markUsed: (input) => Effect.sync(() => {
2813
+ const key = oauthClientSecretMemoryKey(input);
2814
+ const record = recordsByKey.get(key);
2815
+ if (record === undefined || record.revokedAt !== undefined) {
2816
+ return Option.none();
2817
+ }
2818
+ const updated = cloneOAuthClientSecretRecord({
2819
+ ...record,
2820
+ lastUsedAt: input.lastUsedAt,
2821
+ metadata: input.metadata ?? record.metadata,
2822
+ });
2823
+ recordsByKey.set(key, updated);
2824
+ return Option.some(cloneOAuthClientSecretRecord(updated));
2825
+ }),
2826
+ revoke: (input) => Effect.sync(() => {
2827
+ const key = oauthClientSecretMemoryKey(input);
2828
+ const record = recordsByKey.get(key);
2829
+ if (record === undefined) {
2830
+ return Option.none();
2831
+ }
2832
+ const updated = cloneOAuthClientSecretRecord({
2833
+ ...record,
2834
+ revokedAt: input.revokedAt,
2835
+ metadata: input.reason === undefined
2836
+ ? record.metadata
2837
+ : { ...record.metadata, revokeReason: input.reason },
2838
+ });
2839
+ recordsByKey.set(key, updated);
2840
+ return Option.some(cloneOAuthClientSecretRecord(updated));
2841
+ }),
2842
+ });
2843
+ };
2844
+ export const OAuthClientSecretStoreMemoryLive = Layer.sync(OAuthClientSecretStore)(makeOAuthClientSecretStoreMemory);
2845
+ export const makeOAuthAuthorizationCodeStoreMemory = () => {
2846
+ const recordsByHash = new Map();
2847
+ return OAuthAuthorizationCodeStore.make({
2848
+ upsert: (record) => Effect.sync(() => {
2849
+ recordsByHash.set(record.codeHash, cloneAuthorizationCodeRecord(record));
2850
+ }),
2851
+ findByHash: (input) => Effect.sync(() => {
2852
+ const record = recordsByHash.get(input.codeHash);
2853
+ return record === undefined
2854
+ ? Option.none()
2855
+ : Option.some(cloneAuthorizationCodeRecord(record));
2856
+ }),
2857
+ markConsumed: (input) => Effect.sync(() => {
2858
+ const record = recordsByHash.get(input.codeHash);
2859
+ if (record === undefined) {
2860
+ return { status: "missing" };
2861
+ }
2862
+ if (record.consumedAt !== undefined) {
2863
+ return {
2864
+ status: "already_consumed",
2865
+ record: cloneAuthorizationCodeRecord(record),
2866
+ };
2867
+ }
2868
+ const consumed = cloneAuthorizationCodeRecord({
2869
+ ...record,
2870
+ consumedAt: input.consumedAt,
2871
+ });
2872
+ recordsByHash.set(input.codeHash, consumed);
2873
+ return {
2874
+ status: "consumed",
2875
+ record: cloneAuthorizationCodeRecord(consumed),
2876
+ };
2877
+ }),
2878
+ });
2879
+ };
2880
+ export const OAuthAuthorizationCodeStoreMemoryLive = Layer.sync(OAuthAuthorizationCodeStore)(makeOAuthAuthorizationCodeStoreMemory);
2881
+ export const makeOAuthProviderModeTokenStoreMemory = () => {
2882
+ const recordsByHash = new Map();
2883
+ return OAuthProviderModeTokenStore.make({
2884
+ upsert: (record) => Effect.sync(() => {
2885
+ recordsByHash.set(record.tokenHash, cloneOAuthProviderModeTokenRecord(record));
2886
+ }),
2887
+ findByHash: (input) => Effect.sync(() => {
2888
+ const record = recordsByHash.get(input.tokenHash);
2889
+ return record === undefined
2890
+ ? Option.none()
2891
+ : Option.some(cloneOAuthProviderModeTokenRecord(record));
2892
+ }),
2893
+ revokeByHash: (input) => Effect.sync(() => {
2894
+ const record = recordsByHash.get(input.tokenHash);
2895
+ if (record === undefined ||
2896
+ (input.tokenTypeHint !== undefined &&
2897
+ record.tokenType !== input.tokenTypeHint)) {
2898
+ return Option.none();
2899
+ }
2900
+ const revoked = cloneOAuthProviderModeTokenRecord({
2901
+ ...record,
2902
+ revokedAt: record.revokedAt ?? input.revokedAt,
2903
+ revocationReason: record.revocationReason ?? input.reason,
2904
+ });
2905
+ recordsByHash.set(input.tokenHash, revoked);
2906
+ return Option.some(cloneOAuthProviderModeTokenRecord(revoked));
2907
+ }),
2908
+ rotateRefreshToken: (input) => Effect.sync(() => {
2909
+ const record = recordsByHash.get(input.tokenHash);
2910
+ if (record === undefined ||
2911
+ record.tokenType !== "refresh_token" ||
2912
+ record.revokedAt !== undefined ||
2913
+ record.rotatedAt !== undefined) {
2914
+ return Option.none();
2915
+ }
2916
+ const rotated = cloneOAuthProviderModeTokenRecord({
2917
+ ...record,
2918
+ rotatedAt: input.rotatedAt,
2919
+ replacedByTokenHash: input.replacedByTokenHash,
2920
+ });
2921
+ recordsByHash.set(input.tokenHash, rotated);
2922
+ return Option.some(cloneOAuthProviderModeTokenRecord(rotated));
2923
+ }),
2924
+ deleteRetired: (input) => Effect.sync(() => {
2925
+ let deleted = 0;
2926
+ for (const [tokenHash, record] of recordsByHash) {
2927
+ const expired = input.beforeExpiresAt !== undefined &&
2928
+ Number(record.expiresAt) < Number(input.beforeExpiresAt);
2929
+ const revoked = input.beforeRevokedAt !== undefined &&
2930
+ record.revokedAt !== undefined &&
2931
+ Number(record.revokedAt) < Number(input.beforeRevokedAt);
2932
+ const rotated = input.beforeRotatedAt !== undefined &&
2933
+ record.rotatedAt !== undefined &&
2934
+ Number(record.rotatedAt) < Number(input.beforeRotatedAt);
2935
+ if (expired || revoked || rotated) {
2936
+ recordsByHash.delete(tokenHash);
2937
+ deleted += 1;
2938
+ }
2939
+ }
2940
+ return deleted;
2941
+ }),
2942
+ });
2943
+ };
2944
+ export const OAuthProviderModeTokenStoreMemoryLive = Layer.sync(OAuthProviderModeTokenStore)(makeOAuthProviderModeTokenStoreMemory);
2945
+ export const OidcIdTokenVerifierLive = Layer.effect(OidcIdTokenVerifier)(Effect.gen(function* () {
2946
+ const jwtVerifier = yield* JwtVerifier;
2947
+ return makeOidcIdTokenVerifier({ jwtVerifier });
2948
+ }));
2949
+ export const OAuthOidcProfileNormalizerLive = Layer.succeed(OAuthProfileNormalizer, makeOidcOAuthProfileNormalizer());
2950
+ export const OAuthAccountLinkingLive = Layer.effect(OAuthAccountLinking)(Effect.gen(function* () {
2951
+ const accounts = yield* OAuthAccountStore;
2952
+ return makeOAuthAccountLinkingPolicy({ accounts });
2953
+ }));
2954
+ export const OAuthAccountUnlinkingLive = Layer.effect(OAuthAccountUnlinking)(Effect.gen(function* () {
2955
+ const accounts = yield* OAuthAccountStore;
2956
+ return makeOAuthAccountUnlinkingPolicy({ accounts });
2957
+ }));
2958
+ export const OAuthLinkConfirmationLive = Layer.effect(OAuthLinkConfirmation)(Effect.gen(function* () {
2959
+ const challenge = yield* Challenge;
2960
+ const accounts = yield* OAuthAccountStore;
2961
+ const crypto = yield* Crypto;
2962
+ return makeOAuthLinkConfirmation({ challenge, accounts, crypto });
2963
+ }));
2964
+ const accountKey = (input) => `${input.providerId}:${input.providerAccountId}`;
2965
+ const cloneOAuthAccountRecord = (record) => ({
2966
+ ...record,
2967
+ ...(record.metadata === undefined ? {} : { metadata: metadataValue(record.metadata) }),
2968
+ });
2969
+ const cloneProviderTokenVaultRecord = (record) => ({
2970
+ ...record,
2971
+ ...(record.scopes === undefined ? {} : { scopes: [...record.scopes] }),
2972
+ });
2973
+ const providerTokenVaultTokens = (record) => ({
2974
+ accessTokenCiphertext: record.accessTokenCiphertext,
2975
+ ...(record.refreshTokenCiphertext === undefined
2976
+ ? {}
2977
+ : { refreshTokenCiphertext: record.refreshTokenCiphertext }),
2978
+ ...(record.idTokenCiphertext === undefined
2979
+ ? {}
2980
+ : { idTokenCiphertext: record.idTokenCiphertext }),
2981
+ tokenType: record.tokenType,
2982
+ ...(record.scopes === undefined ? {} : { scopes: [...record.scopes] }),
2983
+ ...(record.expiresAt === undefined ? {} : { expiresAt: record.expiresAt }),
2984
+ });
2985
+ const activeProviderTokenRecord = (record) => record === undefined || record.revokedAt !== undefined ? undefined : record;
2986
+ const includeAccount = (record, includeUnlinked) => includeUnlinked === true || record.unlinkedAt === undefined;
2987
+ export const makeOAuthProviderTokenVaultMemory = () => {
2988
+ const recordsByAccountId = new Map();
2989
+ return OAuthProviderTokenVault.make({
2990
+ upsert: (record) => Effect.sync(() => {
2991
+ recordsByAccountId.set(record.accountId, cloneProviderTokenVaultRecord(record));
2992
+ }),
2993
+ refreshTokenForAccount: (input) => Effect.sync(() => {
2994
+ const record = activeProviderTokenRecord(recordsByAccountId.get(input.accountId));
2995
+ return record?.refreshTokenCiphertext === undefined
2996
+ ? Option.none()
2997
+ : Option.some(record.refreshTokenCiphertext);
2998
+ }),
2999
+ tokensForAccount: (input) => Effect.sync(() => {
3000
+ const record = activeProviderTokenRecord(recordsByAccountId.get(input.accountId));
3001
+ return record === undefined
3002
+ ? Option.none()
3003
+ : Option.some(providerTokenVaultTokens(record));
3004
+ }),
3005
+ markRevoked: (input) => Effect.sync(() => {
3006
+ const record = recordsByAccountId.get(input.accountId);
3007
+ if (record === undefined) {
3008
+ return Option.none();
3009
+ }
3010
+ const revoked = cloneProviderTokenVaultRecord({
3011
+ ...record,
3012
+ revokedAt: input.revokedAt,
3013
+ revocationReason: input.reason,
3014
+ });
3015
+ recordsByAccountId.set(input.accountId, revoked);
3016
+ return Option.some(cloneProviderTokenVaultRecord(revoked));
3017
+ }),
3018
+ });
3019
+ };
3020
+ export const OAuthProviderTokenVaultMemoryLive = Layer.sync(OAuthProviderTokenVault)(makeOAuthProviderTokenVaultMemory);
3021
+ export const makeOAuthAccountStoreMemory = () => {
3022
+ const accountsById = new Map();
3023
+ const idByProviderAccount = new Map();
3024
+ return OAuthAccountStore.make({
3025
+ insert: (row) => Effect.sync(() => {
3026
+ const cloned = cloneOAuthAccountRecord(row);
3027
+ accountsById.set(row.id, cloned);
3028
+ idByProviderAccount.set(accountKey(row), row.id);
3029
+ }),
3030
+ findById: (id) => Effect.sync(() => {
3031
+ const row = accountsById.get(id);
3032
+ return row === undefined ? Option.none() : Option.some(cloneOAuthAccountRecord(row));
3033
+ }),
3034
+ findByProviderAccount: (input) => Effect.sync(() => {
3035
+ const id = idByProviderAccount.get(accountKey(input));
3036
+ const row = id === undefined ? undefined : accountsById.get(id);
3037
+ return row === undefined || !includeAccount(row, input.includeUnlinked)
3038
+ ? Option.none()
3039
+ : Option.some(cloneOAuthAccountRecord(row));
3040
+ }),
3041
+ listByUser: (input) => Effect.sync(() => [...accountsById.values()]
3042
+ .filter((row) => row.userId === input.userId && includeAccount(row, input.includeUnlinked))
3043
+ .map(cloneOAuthAccountRecord)),
3044
+ unlink: (input) => Effect.sync(() => {
3045
+ const id = idByProviderAccount.get(accountKey(input));
3046
+ const row = id === undefined ? undefined : accountsById.get(id);
3047
+ if (row === undefined) {
3048
+ return Option.none();
3049
+ }
3050
+ const updated = {
3051
+ ...row,
3052
+ unlinkedAt: input.unlinkedAt,
3053
+ metadata: input.reason === undefined
3054
+ ? row.metadata
3055
+ : { ...row.metadata, unlinkReason: input.reason },
3056
+ };
3057
+ accountsById.set(row.id, updated);
3058
+ return Option.some(cloneOAuthAccountRecord(updated));
3059
+ }),
3060
+ });
3061
+ };
3062
+ export const OAuthAccountStoreMemoryLive = Layer.sync(OAuthAccountStore)(makeOAuthAccountStoreMemory);
3063
+ //# sourceMappingURL=OAuth.js.map