@yielded/auth 0.1.0-beta.3 → 0.1.0-beta.5
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.
- package/dist/DrizzleD1.d.mts +51 -2
- package/dist/DrizzleLibsql.d.mts +53 -4
- package/dist/DrizzleMysql2.d.mts +53 -4
- package/dist/DrizzlePglite.d.mts +53 -4
- package/dist/DrizzlePostgres.d.mts +55 -6
- package/dist/DrizzleSqliteBun.d.mts +51 -2
- package/dist/DrizzleSqliteDo.d.mts +53 -4
- package/dist/DrizzleSqliteNode.d.mts +53 -4
- package/dist/DrizzleSqliteWasm.d.mts +55 -6
- package/dist/GitHub.d.mts +2 -1
- package/dist/GitHub.mjs +2 -1
- package/dist/OpenIdClient.d.mts +2 -1
- package/dist/OpenIdClient.mjs +2 -1
- package/dist/drizzle/oauth-connected-state.d.mts +1 -1
- package/dist/drizzle/oauth-registration.d.mts +1 -1
- package/dist/drizzle/oauth-registration.mjs +3 -2
- package/dist/drizzle/oauth-registration.mjs.map +1 -1
- package/dist/oauth/OAuthConnectedRevocations.d.mts +10 -0
- package/dist/oauth/OAuthRegistrationIntents.d.mts +9 -0
- package/dist/oauth/accountsModels.d.mts +5 -0
- package/dist/oauth/connected.d.mts +22 -0
- package/dist/oauth/connectedModels.d.mts +115 -0
- package/dist/oauth/definition.d.mts +111 -3
- package/dist/oauth/github/identity.mjs +11 -9
- package/dist/oauth/github/identity.mjs.map +1 -1
- package/dist/oauth/github/profile.d.mts +61 -0
- package/dist/oauth/github/profile.mjs +72 -0
- package/dist/oauth/github/profile.mjs.map +1 -0
- package/dist/oauth/github/protocol.mjs +2 -2
- package/dist/oauth/github/protocol.mjs.map +1 -1
- package/dist/oauth/openid-client/connected/protocol.mjs +3 -0
- package/dist/oauth/openid-client/connected/protocol.mjs.map +1 -1
- package/dist/oauth/openid-client/profile.d.mts +39 -0
- package/dist/oauth/openid-client/profile.mjs +58 -0
- package/dist/oauth/openid-client/profile.mjs.map +1 -0
- package/dist/oauth/openid-client/protocol.mjs +3 -0
- package/dist/oauth/openid-client/protocol.mjs.map +1 -1
- package/dist/oauth/registrationModels.d.mts +22 -0
- package/dist/oauth/registrationModels.mjs +4 -1
- package/dist/oauth/registrationModels.mjs.map +1 -1
- package/dist/oauth/registrationSecrets.mjs +1 -0
- package/dist/oauth/registrationSecrets.mjs.map +1 -1
- package/dist/oauth/signInModels.d.mts +18 -0
- package/dist/oauth/signInModels.mjs +11 -1
- package/dist/oauth/signInModels.mjs.map +1 -1
- package/dist/oauth/signInModule.d.mts +43 -4
- package/dist/oauth/signInModule.mjs +1 -1
- package/dist/oauth/signInModule.mjs.map +1 -1
- package/dist/oauth/signInSnapshot.mjs +1 -1
- package/dist/oauth/signInSnapshot.mjs.map +1 -1
- package/package.json +1 -1
- package/src/GitHub.ts +1 -0
- package/src/OpenIdClient.ts +2 -0
- package/src/drizzle/oauth-registration.ts +4 -0
- package/src/oauth/github/identity.ts +11 -7
- package/src/oauth/github/profile.ts +64 -0
- package/src/oauth/github/protocol.ts +3 -2
- package/src/oauth/openid-client/connected/protocol.ts +4 -0
- package/src/oauth/openid-client/profile.ts +68 -0
- package/src/oauth/openid-client/protocol.ts +4 -0
- package/src/oauth/registrationModels.ts +4 -0
- package/src/oauth/registrationSecrets.ts +1 -0
- package/src/oauth/signInModels.ts +18 -0
- package/src/oauth/signInModule.ts +4 -0
- package/src/oauth/signInSnapshot.ts +2 -1
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"@yielded/auth","version":"0.1.0-beta.
|
|
1
|
+
{"name":"@yielded/auth","version":"0.1.0-beta.5","dependencies":{"@noble/ciphers":"2.1.1","@noble/hashes":"2.3.0"},"devDependencies":{"@cloudflare/workers-types":"5.20260825.1","@effect/platform-bun":"4.0.0-rc.112","@effect/platform-node":"4.0.0-rc.112","@effect/sql-d1":"4.0.0-rc.112","@effect/sql-libsql":"4.0.0-rc.112","@effect/sql-mysql2":"4.0.0-rc.112","@effect/sql-pg":"4.0.0-rc.112","@effect/sql-pglite":"4.0.0-rc.112","@effect/sql-sqlite-bun":"4.0.0-rc.112","@effect/sql-sqlite-do":"4.0.0-rc.112","@effect/sql-sqlite-node":"4.0.0-rc.112","@effect/sql-sqlite-wasm":"4.0.0-rc.112","@effect/vitest":"4.0.0-rc.112","@simplewebauthn/browser":"14.0.0","@simplewebauthn/server":"14.0.1","drizzle-orm":"1.0.0-rc.5-ab785fc","effect":"4.0.0-rc.112","effect-cf":"0.40.0","openid-client":"6.8.8","tldts":"7.4.11","typescript":"7.0.2","vite-plus":"0.3.0"},"peerDependencies":{"@effect/sql-d1":">=4.0.0-rc.112","@effect/sql-libsql":">=4.0.0-rc.112","@effect/sql-mysql2":">=4.0.0-rc.112","@effect/sql-pg":">=4.0.0-rc.112","@effect/sql-pglite":">=4.0.0-rc.112","@effect/sql-sqlite-bun":">=4.0.0-rc.112","@effect/sql-sqlite-do":">=4.0.0-rc.112","@effect/sql-sqlite-node":">=4.0.0-rc.112","@effect/sql-sqlite-wasm":">=4.0.0-rc.112","@simplewebauthn/browser":">=14.0.0 <15","@simplewebauthn/server":">=14.0.1 <15","drizzle-orm":">=1.0.0-rc.5-ab785fc","effect":"^4.0.0-rc.112","effect-cf":"^0.40.0","openid-client":">=6.8.8 <7","tldts":">=7.4.11 <8"},"exports":{".":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"./Auth":{"types":"./dist/Auth.d.mts","default":"./dist/Auth.mjs"},"./Drizzle":{"types":"./dist/Drizzle.d.mts","default":"./dist/Drizzle.mjs"},"./DrizzleD1":{"types":"./dist/DrizzleD1.d.mts","default":"./dist/DrizzleD1.mjs"},"./DrizzleLibsql":{"types":"./dist/DrizzleLibsql.d.mts","default":"./dist/DrizzleLibsql.mjs"},"./DrizzleMysql2":{"types":"./dist/DrizzleMysql2.d.mts","default":"./dist/DrizzleMysql2.mjs"},"./DrizzlePglite":{"types":"./dist/DrizzlePglite.d.mts","default":"./dist/DrizzlePglite.mjs"},"./DrizzlePostgres":{"types":"./dist/DrizzlePostgres.d.mts","default":"./dist/DrizzlePostgres.mjs"},"./DrizzleSqliteBun":{"types":"./dist/DrizzleSqliteBun.d.mts","default":"./dist/DrizzleSqliteBun.mjs"},"./DrizzleSqliteDo":{"types":"./dist/DrizzleSqliteDo.d.mts","default":"./dist/DrizzleSqliteDo.mjs"},"./DrizzleSqliteNode":{"types":"./dist/DrizzleSqliteNode.d.mts","default":"./dist/DrizzleSqliteNode.mjs"},"./DrizzleSqliteWasm":{"types":"./dist/DrizzleSqliteWasm.d.mts","default":"./dist/DrizzleSqliteWasm.mjs"},"./Cloudflare":{"types":"./dist/Cloudflare.d.mts","default":"./dist/Cloudflare.mjs"},"./Http":{"types":"./dist/Http.d.mts","default":"./dist/Http.mjs"},"./HttpServer":{"types":"./dist/HttpServer.d.mts","default":"./dist/HttpServer.mjs"},"./OperationHttp":{"types":"./dist/OperationHttp.d.mts","default":"./dist/OperationHttp.mjs"},"./OperationHttpClient":{"types":"./dist/OperationHttpClient.d.mts","default":"./dist/OperationHttpClient.mjs"},"./OperationHttpServer":{"types":"./dist/OperationHttpServer.d.mts","default":"./dist/OperationHttpServer.mjs"},"./Atom":{"types":"./dist/Atom.d.mts","default":"./dist/Atom.mjs"},"./Hooks":{"types":"./dist/Hooks.d.mts","default":"./dist/Hooks.mjs"},"./Identity":{"types":"./dist/Identity.d.mts","default":"./dist/Identity.mjs"},"./OAuth":{"types":"./dist/OAuth.d.mts","default":"./dist/OAuth.mjs"},"./Operations":{"types":"./dist/Operations.d.mts","default":"./dist/Operations.mjs"},"./Rpc":{"types":"./dist/Rpc.d.mts","default":"./dist/Rpc.mjs"},"./Schema":{"types":"./dist/Schema.d.mts","default":"./dist/Schema.mjs"},"./Testing":{"types":"./dist/Testing.d.mts","default":"./dist/Testing.mjs"},"./WebCrypto":{"types":"./dist/WebCrypto.d.mts","default":"./dist/WebCrypto.mjs"},"./Sessions":{"types":"./dist/Sessions.d.mts","default":"./dist/Sessions.mjs"},"./SessionContract":{"types":"./dist/SessionContract.d.mts","default":"./dist/SessionContract.mjs"},"./Proofs":{"types":"./dist/Proofs.d.mts","default":"./dist/Proofs.mjs"},"./Password":{"types":"./dist/Password.d.mts","default":"./dist/Password.mjs"},"./Email":{"types":"./dist/Email.d.mts","default":"./dist/Email.mjs"},"./OpenIdClient":{"types":"./dist/OpenIdClient.d.mts","default":"./dist/OpenIdClient.mjs"},"./OpenIdClientConnected":{"types":"./dist/OpenIdClientConnected.d.mts","default":"./dist/OpenIdClientConnected.mjs"},"./Passkey":{"types":"./dist/Passkey.d.mts","default":"./dist/Passkey.mjs"},"./PasskeyPassword":{"types":"./dist/PasskeyPassword.d.mts","default":"./dist/PasskeyPassword.mjs"},"./PasskeySimpleWebAuthn":{"types":"./dist/PasskeySimpleWebAuthn.d.mts","default":"./dist/PasskeySimpleWebAuthn.mjs"},"./PasskeyBrowser":{"types":"./dist/PasskeyBrowser.d.mts","default":"./dist/PasskeyBrowser.mjs"},"./GitHub":{"types":"./dist/GitHub.d.mts","default":"./dist/GitHub.mjs"},"./AuthSession":{"types":"./dist/AuthSession.d.mts","default":"./dist/AuthSession.mjs"},"./Errors":{"types":"./dist/Errors.d.mts","default":"./dist/Errors.mjs"},"./Workflows":{"types":"./dist/Workflows.d.mts","default":"./dist/Workflows.mjs"},"./PasswordHasher":{"types":"./dist/PasswordHasher.d.mts","default":"./dist/PasswordHasher.mjs"},"./EmailOtp":{"types":"./dist/EmailOtp.d.mts","default":"./dist/EmailOtp.mjs"},"./AuthStore":{"types":"./dist/AuthStore.d.mts","default":"./dist/AuthStore.mjs"},"./PasswordAuth":{"types":"./dist/PasswordAuth.d.mts","default":"./dist/PasswordAuth.mjs"},"./Policy":{"types":"./dist/Policy.d.mts","default":"./dist/Policy.mjs"},"./EmailOtpSender":{"types":"./dist/EmailOtpSender.d.mts","default":"./dist/EmailOtpSender.mjs"},"./AuthTokenCodec":{"types":"./dist/AuthTokenCodec.d.mts","default":"./dist/AuthTokenCodec.mjs"},"./IdentityResolver":{"types":"./dist/IdentityResolver.d.mts","default":"./dist/IdentityResolver.mjs"},"./PasswordCredentialStore":{"types":"./dist/PasswordCredentialStore.d.mts","default":"./dist/PasswordCredentialStore.mjs"},"./PhoneOtp":{"types":"./dist/PhoneOtp.d.mts","default":"./dist/PhoneOtp.mjs"},"./Totp":{"types":"./dist/Totp.d.mts","default":"./dist/Totp.mjs"},"./PasskeyContract":{"types":"./dist/PasskeyContract.d.mts","default":"./dist/PasskeyContract.mjs"},"./TotpContract":{"types":"./dist/TotpContract.d.mts","default":"./dist/TotpContract.mjs"},"./AuthContract":{"types":"./dist/AuthContract.d.mts","default":"./dist/AuthContract.mjs"},"./Client":{"types":"./dist/Client.d.mts","default":"./dist/Client.mjs"}},"description":"Composable authentication, sessions, and identity workflows for Effect.","keywords":["authentication","effect","oauth","passkeys","sessions","typescript"],"homepage":"https://yielded.dev/auth/","bugs":{"url":"https://github.com/yielded-dev/auth/issues"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/yielded-dev/auth.git","directory":"packages/effect-auth"},"files":["dist","src"],"type":"module","sideEffects":[],"publishConfig":{"access":"public"},"scripts":{"build":"vp pack","check":"tsc --noEmit -p tsconfig.json","test":"vp test --passWithNoTests","e2e":"bun test/e2e/harness.ts","example:totp":"bun test/fixtures/totp-sqlite-bun.ts"},"peerDependenciesMeta":{"@effect/sql-d1":{"optional":true},"@effect/sql-libsql":{"optional":true},"@effect/sql-mysql2":{"optional":true},"@effect/sql-pg":{"optional":true},"@effect/sql-pglite":{"optional":true},"@effect/sql-sqlite-bun":{"optional":true},"@effect/sql-sqlite-do":{"optional":true},"@effect/sql-sqlite-node":{"optional":true},"@effect/sql-sqlite-wasm":{"optional":true},"@simplewebauthn/browser":{"optional":true},"@simplewebauthn/server":{"optional":true},"drizzle-orm":{"optional":true},"effect-cf":{"optional":true},"openid-client":{"optional":true},"tldts":{"optional":true}}}
|
package/src/GitHub.ts
CHANGED
package/src/OpenIdClient.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "../oauth/registrationModels";
|
|
12
12
|
import {
|
|
13
13
|
OAuthCredentialSnapshot,
|
|
14
|
+
OAuthDisplayProfile,
|
|
14
15
|
OAuthSignInTransactionContext,
|
|
15
16
|
type OAuthExternalIdentity,
|
|
16
17
|
} from "../oauth/signInModels";
|
|
@@ -25,6 +26,7 @@ import { invariant, oauthIdentityKey, sameIdentity, storage } from "./oauth-stat
|
|
|
25
26
|
export const intentStorage = storage(OAuthRegistrationIntent);
|
|
26
27
|
const decisionStorage = storage(OAuthRegistrationDecision);
|
|
27
28
|
const contextStorage = storage(OAuthSignInTransactionContext);
|
|
29
|
+
const profileStorage = storage(Schema.NullOr(OAuthDisplayProfile));
|
|
28
30
|
|
|
29
31
|
const reservationStorage = storage(
|
|
30
32
|
Schema.Struct({
|
|
@@ -174,6 +176,8 @@ export const settleRegistrationIntent = (mapping: any, input: any) =>
|
|
|
174
176
|
|
|
175
177
|
invariant(
|
|
176
178
|
sameIdentity(intent.identity, identity) &&
|
|
179
|
+
profileStorage.encode(intent.profile ?? null) ===
|
|
180
|
+
profileStorage.encode(input.identity.profile ?? null) &&
|
|
177
181
|
intent.claimId === input.claim.claimId &&
|
|
178
182
|
intent.claimedAtMillis === input.claim.claimedAtMillis &&
|
|
179
183
|
contextStorage.encode(c) === contextStorage.encode(context),
|
|
@@ -3,24 +3,28 @@ import { Effect, Schema } from "effect";
|
|
|
3
3
|
import type { PlainOAuthIdentity } from "../openid-client/models";
|
|
4
4
|
import { OAuthProviderKey } from "../schema";
|
|
5
5
|
import { OAuthProtocolRejected } from "../signInErrors";
|
|
6
|
+
import { GitHubUserProfile } from "./profile";
|
|
6
7
|
|
|
7
8
|
export const gitHubOAuthAppProviderKey = OAuthProviderKey.make("github");
|
|
8
9
|
|
|
9
|
-
const identity = Schema.Struct({
|
|
10
|
-
id: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER })),
|
|
11
|
-
login: Schema.optionalKey(Schema.NonEmptyString.check(Schema.isMaxLength(256))),
|
|
12
|
-
});
|
|
13
|
-
|
|
14
10
|
// oxlint-disable-next-line no-restricted-properties -- GitHub /user is an untyped, freshly authenticated JSON boundary.
|
|
15
|
-
const decode = Schema.decodeUnknownEffect(
|
|
11
|
+
const decode = Schema.decodeUnknownEffect(GitHubUserProfile);
|
|
16
12
|
|
|
17
13
|
export const decodeGitHubIdentity = Effect.fn("GitHubOAuthApp.decodeIdentity")(function* (
|
|
18
14
|
body: unknown,
|
|
19
15
|
): Effect.fn.Return<PlainOAuthIdentity, OAuthProtocolRejected> {
|
|
20
16
|
const value = yield* decode(body).pipe(Effect.mapError(() => OAuthProtocolRejected.make({})));
|
|
17
|
+
const displayName = value.name?.trim() || value.login;
|
|
21
18
|
|
|
22
19
|
return {
|
|
23
20
|
subject: String(value.id),
|
|
24
|
-
|
|
21
|
+
profile: {
|
|
22
|
+
...(displayName === undefined ? {} : { displayName }),
|
|
23
|
+
...(value.login === undefined ? {} : { handle: value.login }),
|
|
24
|
+
...(value.avatar_url === undefined ? {} : { avatarUrl: value.avatar_url }),
|
|
25
|
+
...(value.html_url === undefined ? {} : { profileUrl: value.html_url }),
|
|
26
|
+
...(value.email === null || value.email === undefined ? {} : { email: value.email }),
|
|
27
|
+
providerData: value,
|
|
28
|
+
},
|
|
25
29
|
};
|
|
26
30
|
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
const text = Schema.String.check(Schema.isMaxLength(2048));
|
|
4
|
+
const name = Schema.String.check(Schema.isMaxLength(256));
|
|
5
|
+
const count = Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }));
|
|
6
|
+
const optionalText = Schema.optionalKey(text);
|
|
7
|
+
const nullableText = Schema.optionalKey(Schema.NullOr(text));
|
|
8
|
+
|
|
9
|
+
/** All documented GitHub /user fields, with provider spelling and nulls retained.
|
|
10
|
+
* Only the numeric ID establishes identity. Other fields are optional because
|
|
11
|
+
* availability differs by permission and provider response. This does not fetch
|
|
12
|
+
* repositories, email lists, or any additional endpoint. Unknown fields are omitted.
|
|
13
|
+
* URLs are metadata, not approved redirect or server-fetch targets. */
|
|
14
|
+
export const GitHubUserProfile = Schema.Struct({
|
|
15
|
+
id: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER })),
|
|
16
|
+
login: Schema.optionalKey(Schema.NonEmptyString.check(Schema.isMaxLength(256))),
|
|
17
|
+
name: Schema.optionalKey(Schema.NullOr(name)),
|
|
18
|
+
user_view_type: optionalText,
|
|
19
|
+
node_id: optionalText,
|
|
20
|
+
avatar_url: optionalText,
|
|
21
|
+
gravatar_id: nullableText,
|
|
22
|
+
url: optionalText,
|
|
23
|
+
html_url: optionalText,
|
|
24
|
+
followers_url: optionalText,
|
|
25
|
+
following_url: optionalText,
|
|
26
|
+
gists_url: optionalText,
|
|
27
|
+
starred_url: optionalText,
|
|
28
|
+
subscriptions_url: optionalText,
|
|
29
|
+
organizations_url: optionalText,
|
|
30
|
+
repos_url: optionalText,
|
|
31
|
+
events_url: optionalText,
|
|
32
|
+
received_events_url: optionalText,
|
|
33
|
+
type: optionalText,
|
|
34
|
+
site_admin: Schema.optionalKey(Schema.Boolean),
|
|
35
|
+
company: nullableText,
|
|
36
|
+
blog: nullableText,
|
|
37
|
+
location: nullableText,
|
|
38
|
+
email: Schema.optionalKey(Schema.NullOr(Schema.String.check(Schema.isMaxLength(320)))),
|
|
39
|
+
notification_email: Schema.optionalKey(
|
|
40
|
+
Schema.NullOr(Schema.String.check(Schema.isMaxLength(320))),
|
|
41
|
+
),
|
|
42
|
+
hireable: Schema.optionalKey(Schema.NullOr(Schema.Boolean)),
|
|
43
|
+
bio: nullableText,
|
|
44
|
+
twitter_username: nullableText,
|
|
45
|
+
public_repos: Schema.optionalKey(count),
|
|
46
|
+
public_gists: Schema.optionalKey(count),
|
|
47
|
+
followers: Schema.optionalKey(count),
|
|
48
|
+
following: Schema.optionalKey(count),
|
|
49
|
+
created_at: optionalText,
|
|
50
|
+
updated_at: optionalText,
|
|
51
|
+
private_gists: Schema.optionalKey(count),
|
|
52
|
+
total_private_repos: Schema.optionalKey(count),
|
|
53
|
+
owned_private_repos: Schema.optionalKey(count),
|
|
54
|
+
disk_usage: Schema.optionalKey(count),
|
|
55
|
+
collaborators: Schema.optionalKey(count),
|
|
56
|
+
two_factor_authentication: Schema.optionalKey(Schema.Boolean),
|
|
57
|
+
plan: Schema.optionalKey(
|
|
58
|
+
Schema.Struct({ collaborators: count, name: text, space: count, private_repos: count }),
|
|
59
|
+
),
|
|
60
|
+
business_plus: Schema.optionalKey(Schema.Boolean),
|
|
61
|
+
ldap_dn: optionalText,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export type GitHubUserProfile = typeof GitHubUserProfile.Type;
|
|
@@ -29,7 +29,8 @@ import type {
|
|
|
29
29
|
|
|
30
30
|
const unavailable = () => OAuthUnavailable.make({});
|
|
31
31
|
const invalid = () => OpenIdClientConfigurationError.make({ reason: "provider" });
|
|
32
|
-
|
|
32
|
+
// GitHub's OAuth authorization server issuer, including its RFC 9207 callback value.
|
|
33
|
+
const issuer = OAuthIssuer.make("https://github.com/login/oauth");
|
|
33
34
|
|
|
34
35
|
const headers = Object.freeze({
|
|
35
36
|
Accept: "application/vnd.github+json",
|
|
@@ -92,7 +93,7 @@ const provider = (
|
|
|
92
93
|
issuance: registration.issuance,
|
|
93
94
|
issuer,
|
|
94
95
|
protocol: "oauth",
|
|
95
|
-
responseIssuerMode: "
|
|
96
|
+
responseIssuerMode: "required",
|
|
96
97
|
clientId: registration.clientId,
|
|
97
98
|
authentication: { method: "client_secret_post", secret: registration.clientSecret },
|
|
98
99
|
callbacks: registration.callbacks,
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
} from "../compatibility";
|
|
24
24
|
import { clientAuthentication } from "../configuration";
|
|
25
25
|
import { type OpenIdClientConfigurationError } from "../models";
|
|
26
|
+
import { decodeOidcProfile } from "../profile";
|
|
26
27
|
import { ProviderRevocation } from "../ProviderRevocation";
|
|
27
28
|
import { boundedFetch } from "../transport";
|
|
28
29
|
import {
|
|
@@ -372,8 +373,11 @@ export const makeConnectedProtocolWithCompatibility = Effect.fn(
|
|
|
372
373
|
)
|
|
373
374
|
return yield* unavailable();
|
|
374
375
|
|
|
376
|
+
const profile = yield* decodeOidcProfile(raw).pipe(Effect.mapError(unavailable));
|
|
377
|
+
|
|
375
378
|
return {
|
|
376
379
|
identity: { provider: provider.provider, issuer: provider.issuer, subject: claims.sub },
|
|
380
|
+
...(profile === undefined ? {} : { profile }),
|
|
377
381
|
continuation: previous?.continuation ?? {
|
|
378
382
|
_tag: "Oidc" as const,
|
|
379
383
|
clientId: provider.clientId,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
import { OAuthProtocolRejected } from "../signInErrors";
|
|
4
|
+
import type { OAuthDisplayProfile } from "../signInModels";
|
|
5
|
+
|
|
6
|
+
const text = Schema.String.check(Schema.isMaxLength(256));
|
|
7
|
+
const url = Schema.String.check(Schema.isMaxLength(2048));
|
|
8
|
+
|
|
9
|
+
/** Standard OpenID Connect user claims returned in a verified ID token. This
|
|
10
|
+
* does not request additional scopes or fetch UserInfo. Protocol claims and
|
|
11
|
+
* credentials are excluded; values such as email_verified describe the provider's
|
|
12
|
+
* assertion and never authorize local account linking by themselves. */
|
|
13
|
+
export const OidcUserProfile = Schema.Struct({
|
|
14
|
+
name: Schema.optionalKey(text),
|
|
15
|
+
given_name: Schema.optionalKey(text),
|
|
16
|
+
family_name: Schema.optionalKey(text),
|
|
17
|
+
middle_name: Schema.optionalKey(text),
|
|
18
|
+
nickname: Schema.optionalKey(text),
|
|
19
|
+
preferred_username: Schema.optionalKey(text),
|
|
20
|
+
profile: Schema.optionalKey(url),
|
|
21
|
+
picture: Schema.optionalKey(url),
|
|
22
|
+
website: Schema.optionalKey(url),
|
|
23
|
+
email: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(320))),
|
|
24
|
+
email_verified: Schema.optionalKey(Schema.Boolean),
|
|
25
|
+
gender: Schema.optionalKey(text),
|
|
26
|
+
birthdate: Schema.optionalKey(text),
|
|
27
|
+
zoneinfo: Schema.optionalKey(text),
|
|
28
|
+
locale: Schema.optionalKey(text),
|
|
29
|
+
phone_number: Schema.optionalKey(text),
|
|
30
|
+
phone_number_verified: Schema.optionalKey(Schema.Boolean),
|
|
31
|
+
address: Schema.optionalKey(
|
|
32
|
+
Schema.Struct({
|
|
33
|
+
formatted: Schema.optionalKey(url),
|
|
34
|
+
street_address: Schema.optionalKey(url),
|
|
35
|
+
locality: Schema.optionalKey(text),
|
|
36
|
+
region: Schema.optionalKey(text),
|
|
37
|
+
postal_code: Schema.optionalKey(text),
|
|
38
|
+
country: Schema.optionalKey(text),
|
|
39
|
+
}),
|
|
40
|
+
),
|
|
41
|
+
updated_at: Schema.optionalKey(
|
|
42
|
+
Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER })),
|
|
43
|
+
),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export type OidcUserProfile = typeof OidcUserProfile.Type;
|
|
47
|
+
|
|
48
|
+
export const decodeOidcProfile = Effect.fn("OpenIdClient.decodeProfile")(function* (
|
|
49
|
+
claims: unknown,
|
|
50
|
+
): Effect.fn.Return<OAuthDisplayProfile | undefined, OAuthProtocolRejected> {
|
|
51
|
+
// oxlint-disable-next-line no-restricted-properties -- Select standard user claims from an already verified ID token.
|
|
52
|
+
const profile = yield* Schema.decodeUnknownEffect(OidcUserProfile)(claims).pipe(
|
|
53
|
+
Effect.mapError(() => OAuthProtocolRejected.make({})),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
if (Object.keys(profile).length === 0) return undefined;
|
|
57
|
+
const displayName = profile.name?.trim() || profile.preferred_username;
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
...(displayName === undefined ? {} : { displayName }),
|
|
61
|
+
...(profile.preferred_username === undefined ? {} : { handle: profile.preferred_username }),
|
|
62
|
+
...(profile.picture === undefined ? {} : { avatarUrl: profile.picture }),
|
|
63
|
+
...(profile.profile === undefined ? {} : { profileUrl: profile.profile }),
|
|
64
|
+
...(profile.email === undefined ? {} : { email: profile.email }),
|
|
65
|
+
...(profile.email_verified === undefined ? {} : { emailVerified: profile.email_verified }),
|
|
66
|
+
providerData: profile,
|
|
67
|
+
};
|
|
68
|
+
});
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
type OpenIdClientConfigurationError,
|
|
28
28
|
type OpenIdClientOAuthProtocolOptions,
|
|
29
29
|
} from "./models";
|
|
30
|
+
import { decodeOidcProfile } from "./profile";
|
|
30
31
|
import { boundedFetch } from "./transport";
|
|
31
32
|
|
|
32
33
|
const beginInput = Schema.Struct({
|
|
@@ -357,8 +358,11 @@ export const makeOpenIdClientOAuthProtocol = Effect.fn("makeOpenIdClientOAuthPro
|
|
|
357
358
|
)
|
|
358
359
|
return yield* rejected();
|
|
359
360
|
|
|
361
|
+
const profile = yield* decodeOidcProfile(exchanged.claims);
|
|
362
|
+
|
|
360
363
|
return yield* snapshotOAuth(OAuthVerifiedExternalIdentity, {
|
|
361
364
|
identity: { provider: provider.provider, issuer: provider.issuer, subject: claims.sub },
|
|
365
|
+
...(profile === undefined ? {} : { profile }),
|
|
362
366
|
...(claims.auth_time === undefined
|
|
363
367
|
? {}
|
|
364
368
|
: {
|
|
@@ -5,6 +5,7 @@ import { TokenDigest } from "../Schema";
|
|
|
5
5
|
import {
|
|
6
6
|
OAuthClaimId,
|
|
7
7
|
OAuthCommandId,
|
|
8
|
+
OAuthDisplayProfile,
|
|
8
9
|
OAuthExternalIdentity,
|
|
9
10
|
OAuthInstant,
|
|
10
11
|
OAuthModuleId,
|
|
@@ -46,6 +47,9 @@ export const OAuthRegistrationIntent = Schema.Struct({
|
|
|
46
47
|
claimId: OAuthClaimId,
|
|
47
48
|
claimedAtMillis: OAuthInstant,
|
|
48
49
|
identity: OAuthExternalIdentity,
|
|
50
|
+
/** Original authenticated provider snapshot, available only to server-side
|
|
51
|
+
* provisioning authority. It is not caller-supplied registration data. */
|
|
52
|
+
profile: Schema.optionalKey(OAuthDisplayProfile),
|
|
49
53
|
verifiedAtMillis: OAuthInstant,
|
|
50
54
|
credentialDigest: OAuthRegistrationBearerDigest,
|
|
51
55
|
issuedAtMillis: OAuthInstant,
|
|
@@ -100,6 +100,7 @@ export const prepare = Effect.fn("OAuthRegistration.prepareIntent")(function* (
|
|
|
100
100
|
claimId: claim.claimId,
|
|
101
101
|
claimedAtMillis: claim.claimedAtMillis,
|
|
102
102
|
identity: identity.identity,
|
|
103
|
+
...(identity.profile === undefined ? {} : { profile: identity.profile }),
|
|
103
104
|
verifiedAtMillis,
|
|
104
105
|
credentialDigest: yield* credentialDigest(
|
|
105
106
|
claim.flow.context.moduleId,
|
|
@@ -127,11 +127,29 @@ export const OAuthExternalIdentity = Schema.Struct({
|
|
|
127
127
|
subject: Schema.NonEmptyString.check(Schema.isMaxLength(1024)),
|
|
128
128
|
});
|
|
129
129
|
|
|
130
|
+
/** Provider-supplied metadata, never local identity or linking authority. Adapters
|
|
131
|
+
* project identity responses into this bounded JSON snapshot; tokens and protocol
|
|
132
|
+
* secrets do not belong here. Applications select their own public session claims. */
|
|
130
133
|
export const OAuthDisplayProfile = Schema.Struct({
|
|
131
134
|
displayName: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(256))),
|
|
132
135
|
handle: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(256))),
|
|
136
|
+
avatarUrl: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(2048))),
|
|
137
|
+
profileUrl: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(2048))),
|
|
138
|
+
email: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(320))),
|
|
139
|
+
emailVerified: Schema.optionalKey(Schema.Boolean),
|
|
140
|
+
/** Provider-specific fields. Use the provider's exported profile Schema to
|
|
141
|
+
* decode them. Availability follows the existing provider permissions. */
|
|
142
|
+
providerData: Schema.optionalKey(
|
|
143
|
+
Schema.JsonObject.check(
|
|
144
|
+
Schema.makeFilter(
|
|
145
|
+
(value) => new TextEncoder().encode(JSON.stringify(value)).length <= 65_536,
|
|
146
|
+
),
|
|
147
|
+
),
|
|
148
|
+
),
|
|
133
149
|
});
|
|
134
150
|
|
|
151
|
+
export type OAuthDisplayProfile = typeof OAuthDisplayProfile.Type;
|
|
152
|
+
|
|
135
153
|
export const OAuthVerifiedExternalIdentity = Schema.Struct({
|
|
136
154
|
identity: OAuthExternalIdentity,
|
|
137
155
|
upstreamAuthenticatedAt: Schema.optionalKey(Schema.DateTimeUtcFromMillis),
|
|
@@ -139,6 +139,9 @@ export const makeOAuthMethod = <
|
|
|
139
139
|
{
|
|
140
140
|
readonly resolve: (
|
|
141
141
|
credential: OAuthCredentialSnapshot,
|
|
142
|
+
/** Fresh provider metadata, after exact local credential/identity matching.
|
|
143
|
+
* Select application claims explicitly; profile email grants no linking authority. */
|
|
144
|
+
verified: OAuthVerifiedExternalIdentity,
|
|
142
145
|
) => Effect.Effect<Claims["Type"], OAuthUnavailable>;
|
|
143
146
|
}
|
|
144
147
|
>(`effect-auth/oauth/${moduleId.length}:${moduleId}/Claims`);
|
|
@@ -681,6 +684,7 @@ export const makeOAuthMethod = <
|
|
|
681
684
|
|
|
682
685
|
const claims = yield* resolveClaims(
|
|
683
686
|
snapshotOAuthSync(OAuthCredentialSnapshot, credential),
|
|
687
|
+
snapshotOAuthSync(OAuthVerifiedExternalIdentity, identity),
|
|
684
688
|
);
|
|
685
689
|
|
|
686
690
|
const established = yield* completeAuthentication({
|
|
@@ -16,7 +16,8 @@ export const snapshotOAuthSync = <S extends Schema.Codec<unknown, unknown, never
|
|
|
16
16
|
value: S["Type"],
|
|
17
17
|
): S["Type"] => {
|
|
18
18
|
try {
|
|
19
|
-
|
|
19
|
+
// JSON fields can otherwise retain nested input references through validation.
|
|
20
|
+
const codec = Schema.fromJsonString(Schema.toCodecJson(Schema.toType(schema)));
|
|
20
21
|
const result = Schema.decodeSync(codec)(Schema.encodeSync(codec)(value));
|
|
21
22
|
|
|
22
23
|
freezeOAuth(result);
|