@effect-auth/core 0.1.0-alpha.10 → 0.1.0-alpha.14

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 (93) hide show
  1. package/README.md +65 -4
  2. package/dist/AuditLog.d.ts +60 -0
  3. package/dist/AuditLog.d.ts.map +1 -1
  4. package/dist/AuditLog.js +105 -0
  5. package/dist/AuditLog.js.map +1 -1
  6. package/dist/AuthKernel.d.ts +1 -1
  7. package/dist/Client.d.ts +72 -1
  8. package/dist/Client.d.ts.map +1 -1
  9. package/dist/Client.js +218 -1
  10. package/dist/Client.js.map +1 -1
  11. package/dist/DomainVerification.d.ts +200 -0
  12. package/dist/DomainVerification.d.ts.map +1 -0
  13. package/dist/DomainVerification.js +304 -0
  14. package/dist/DomainVerification.js.map +1 -0
  15. package/dist/EffectQbSqliteStorage.d.ts +389 -4
  16. package/dist/EffectQbSqliteStorage.d.ts.map +1 -1
  17. package/dist/EffectQbSqliteStorage.js +1191 -4
  18. package/dist/EffectQbSqliteStorage.js.map +1 -1
  19. package/dist/EmailOtp.d.ts +2 -2
  20. package/dist/HttpApi/Api.d.ts +477 -23
  21. package/dist/HttpApi/Api.d.ts.map +1 -1
  22. package/dist/HttpApi/Api.js +705 -4
  23. package/dist/HttpApi/Api.js.map +1 -1
  24. package/dist/HttpApi/Endpoints.d.ts +133 -5
  25. package/dist/HttpApi/Endpoints.d.ts.map +1 -1
  26. package/dist/HttpApi/Endpoints.js +57 -2
  27. package/dist/HttpApi/Endpoints.js.map +1 -1
  28. package/dist/HttpApi/Errors.d.ts +32 -0
  29. package/dist/HttpApi/Errors.d.ts.map +1 -1
  30. package/dist/HttpApi/Errors.js +46 -0
  31. package/dist/HttpApi/Errors.js.map +1 -1
  32. package/dist/HttpApi/Schemas.d.ts +149 -8
  33. package/dist/HttpApi/Schemas.d.ts.map +1 -1
  34. package/dist/HttpApi/Schemas.js +128 -0
  35. package/dist/HttpApi/Schemas.js.map +1 -1
  36. package/dist/Identifiers.d.ts +3 -0
  37. package/dist/Identifiers.d.ts.map +1 -1
  38. package/dist/Identifiers.js +2 -0
  39. package/dist/Identifiers.js.map +1 -1
  40. package/dist/IncidentAction.d.ts +80 -0
  41. package/dist/IncidentAction.d.ts.map +1 -0
  42. package/dist/IncidentAction.js +158 -0
  43. package/dist/IncidentAction.js.map +1 -0
  44. package/dist/Jwt.d.ts +10 -0
  45. package/dist/Jwt.d.ts.map +1 -1
  46. package/dist/Jwt.js +209 -0
  47. package/dist/Jwt.js.map +1 -1
  48. package/dist/LoginNotification.d.ts +1 -1
  49. package/dist/LoginRisk.d.ts +272 -1
  50. package/dist/LoginRisk.d.ts.map +1 -1
  51. package/dist/LoginRisk.js +427 -0
  52. package/dist/LoginRisk.js.map +1 -1
  53. package/dist/MachineAuth.d.ts +73 -0
  54. package/dist/MachineAuth.d.ts.map +1 -0
  55. package/dist/MachineAuth.js +147 -0
  56. package/dist/MachineAuth.js.map +1 -0
  57. package/dist/MagicLink.d.ts +2 -2
  58. package/dist/OAuth.d.ts +957 -1
  59. package/dist/OAuth.d.ts.map +1 -1
  60. package/dist/OAuth.js +1403 -1
  61. package/dist/OAuth.js.map +1 -1
  62. package/dist/Password.d.ts +2 -2
  63. package/dist/Retention.d.ts +98 -0
  64. package/dist/Retention.d.ts.map +1 -0
  65. package/dist/Retention.js +204 -0
  66. package/dist/Retention.js.map +1 -0
  67. package/dist/SecurityTimeline.d.ts +8 -2
  68. package/dist/SecurityTimeline.d.ts.map +1 -1
  69. package/dist/SecurityTimeline.js +75 -0
  70. package/dist/SecurityTimeline.js.map +1 -1
  71. package/dist/StorageMigrations.d.ts +11 -1
  72. package/dist/StorageMigrations.d.ts.map +1 -1
  73. package/dist/StorageMigrations.js +206 -0
  74. package/dist/StorageMigrations.js.map +1 -1
  75. package/dist/Webhook.d.ts +310 -2
  76. package/dist/Webhook.d.ts.map +1 -1
  77. package/dist/Webhook.js +511 -1
  78. package/dist/Webhook.js.map +1 -1
  79. package/dist/index.d.ts +4 -0
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +4 -0
  82. package/dist/index.js.map +1 -1
  83. package/migrations/0014_auth_webhook_outbox.sql +16 -0
  84. package/migrations/0015_auth_webhook_replay.sql +7 -0
  85. package/migrations/0016_auth_login_risk_history.sql +23 -0
  86. package/migrations/0017_auth_audit_log.sql +17 -0
  87. package/migrations/0018_auth_domain_verification.sql +18 -0
  88. package/migrations/0019_auth_oauth_authorization_code.sql +17 -0
  89. package/migrations/0020_auth_oauth_provider_mode_token.sql +24 -0
  90. package/migrations/0021_auth_oauth_client_secret.sql +14 -0
  91. package/migrations/0022_auth_oauth_client.sql +15 -0
  92. package/migrations/0023_auth_oauth_consent.sql +15 -0
  93. package/package.json +18 -2
package/dist/OAuth.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Brand, Context, Data, Duration, Effect, Encoding, Layer, Option, Redacted, } from "effect";
2
2
  import { Challenge } from "./Challenge.js";
3
3
  import { Crypto } from "./Crypto.js";
4
- import { ChallengeId, Email, OAuthAccountId, OAuthProviderId, UnixMillis, UserId, } from "./Identifiers.js";
4
+ import { ChallengeId, Email, OAuthAccountId, OAuthClientId, OAuthProviderId, UnixMillis, UserId, } from "./Identifiers.js";
5
5
  import { currentUnixMillis } from "./Internal.js";
6
6
  import { JwtAlgorithms, JwtError, JwtKeyId, JwtKeys, JwtVerifier, } from "./Jwt.js";
7
7
  export const OAuthStateToken = Brand.nominal();
@@ -17,8 +17,14 @@ export const defaultOAuthLinkConfirmationTtl = Duration.minutes(15);
17
17
  export const defaultOAuthJwksCacheTtl = Duration.minutes(10);
18
18
  export const defaultOAuthStateSecretBytes = 32;
19
19
  export const defaultOAuthCodeVerifierBytes = 32;
20
+ export const defaultOAuthAuthorizationCodeBytes = 32;
21
+ export const defaultOAuthAuthorizationCodeTtl = Duration.minutes(5);
20
22
  export const defaultOAuthLinkConfirmationSecretBytes = 32;
21
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);
22
28
  export const appleSignInProviderId = OAuthProviderId("apple");
23
29
  export const appleSignInIssuer = "https://appleid.apple.com";
24
30
  export const appleSignInAuthorizationEndpoint = "https://appleid.apple.com/auth/authorize";
@@ -70,8 +76,160 @@ export const microsoftOidcJwksUri = (tenant) => `${microsoftOidcLoginBaseUrl}/${
70
76
  export const microsoftOidcIssuer = (tenant) => `${microsoftOidcLoginBaseUrl}/${microsoftOidcTenantPathSegment(tenant)}/v2.0`;
71
77
  export class OAuthProviderError extends Data.TaggedError("OAuthProviderError") {
72
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 = {}));
73
224
  export class OAuthUserInfoError extends Data.TaggedError("OAuthUserInfoError") {
74
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 = {}));
75
233
  export class OAuthProviderTokenVaultError extends Data.TaggedError("OAuthProviderTokenVaultError") {
76
234
  }
77
235
  export class OAuthProviderTokenVault extends Context.Service()("auth/OAuthProviderTokenVault") {
@@ -148,6 +306,43 @@ export class OAuthLinkConfirmation extends Context.Service()("auth/OAuthLinkConf
148
306
  OAuthLinkConfirmation.make = (service) => OAuthLinkConfirmation.of(service);
149
307
  })(OAuthLinkConfirmation || (OAuthLinkConfirmation = {}));
150
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 });
151
346
  const oauthTokenExchangeError = (operation, message, cause) => new OAuthTokenExchangeError({ operation, message, cause });
152
347
  const oauthProviderTokenLifecycleError = (operation, message, cause) => new OAuthProviderTokenLifecycleError({ operation, message, cause });
153
348
  const oidcIdTokenVerifierError = (operation, message, cause) => new OidcIdTokenVerifierError({ operation, message, cause });
@@ -187,7 +382,56 @@ const validateByteLength = (value, operation, label) => Number.isInteger(value)
187
382
  const validateOAuthLinkConfirmationSecretBytes = (value) => Number.isInteger(value) && value >= 16 && value <= 128
188
383
  ? Effect.succeed(value)
189
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"));
190
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
+ });
191
435
  export const makeAppleSignInProvider = (input) => ({
192
436
  id: input.id ?? appleSignInProviderId,
193
437
  clientId: input.clientId,
@@ -335,6 +579,142 @@ const cloneProviderConfig = (provider) => ({
335
579
  : { authorizationParams: { ...provider.authorizationParams } }),
336
580
  ...(provider.metadata === undefined ? {} : { metadata: metadataValue(provider.metadata) }),
337
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
+ });
338
718
  const encodeOAuthState = (input) => OAuthStateToken(`${input.challengeId}.${Redacted.value(input.secret)}`);
339
719
  export const parseOAuthState = (state) => {
340
720
  const parts = String(state).split(".");
@@ -1404,6 +1784,707 @@ const makeOAuthPkceWithCrypto = (dependencies, input) => Effect.gen(function* ()
1404
1784
  codeChallengeMethod: "S256",
1405
1785
  };
1406
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));
1407
2488
  export const makeOAuthPkce = (input) => Effect.gen(function* () {
1408
2489
  const crypto = yield* Crypto;
1409
2490
  return yield* makeOAuthPkceWithCrypto({ crypto }, input);
@@ -1528,6 +2609,72 @@ export const OAuthStateLive = Layer.effect(OAuthState)(Effect.gen(function* () {
1528
2609
  const challenge = yield* Challenge;
1529
2610
  return makeOAuthState({ crypto, challenge });
1530
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
+ }));
1531
2678
  export const makeOAuthProviders = (providers) => {
1532
2679
  const providersById = new Map(providers.map((provider) => [provider.id, cloneProviderConfig(provider)]));
1533
2680
  return OAuthProviders.make({
@@ -1540,6 +2687,261 @@ export const makeOAuthProviders = (providers) => {
1540
2687
  });
1541
2688
  };
1542
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);
1543
2945
  export const OidcIdTokenVerifierLive = Layer.effect(OidcIdTokenVerifier)(Effect.gen(function* () {
1544
2946
  const jwtVerifier = yield* JwtVerifier;
1545
2947
  return makeOidcIdTokenVerifier({ jwtVerifier });