@cedarjs/auth-dbauth-oauth 7.0.0-canary.3075

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.
@@ -0,0 +1,26 @@
1
+ import type { OAuthIdentityFields, OAuthUserInfo, UserType } from './types.js';
2
+ export declare function resolveIdentityFields(fields: Partial<OAuthIdentityFields> | undefined): OAuthIdentityFields;
3
+ /**
4
+ * Thin wrapper around the identity model accessor (`db[oauthModelAccessor]`)
5
+ * that reads/writes through the configured field names, so the rest of the
6
+ * handler can talk about `provider`/`providerUserId`/`userId` without caring
7
+ * how the app named its Prisma columns.
8
+ */
9
+ export declare class IdentityModel {
10
+ private accessor;
11
+ private fields;
12
+ constructor(accessor: any, fields: OAuthIdentityFields);
13
+ findByProviderUserId(provider: string, providerUserId: string): Promise<UserType | null>;
14
+ findByUserAndProvider(userId: unknown, provider: string): Promise<UserType | null>;
15
+ findAllForUser(userId: unknown): Promise<UserType[]>;
16
+ create(userId: unknown, provider: string, profile: OAuthUserInfo): Promise<UserType>;
17
+ delete(userId: unknown, provider: string): Promise<void>;
18
+ userIdOf(identity: UserType): unknown;
19
+ /**
20
+ * Reads a fetched identity row back into the `OAuthUserInfo` shape
21
+ * `create` accepts, so a row can be recreated from a snapshot taken
22
+ * before it was deleted.
23
+ */
24
+ profileOf(identity: UserType): OAuthUserInfo;
25
+ }
26
+ //# sourceMappingURL=identity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../src/identity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAG9E,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,OAAO,CAAC,mBAAmB,CAAC,GAAG,SAAS,GAC/C,mBAAmB,CAUrB;AAED;;;;;GAKG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,MAAM,CAAqB;gBAEvB,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,mBAAmB;IAKhD,oBAAoB,CACxB,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAWrB,qBAAqB,CACzB,MAAM,EAAE,OAAO,EACf,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAWrB,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAMpD,MAAM,CACV,MAAM,EAAE,OAAO,EACf,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,QAAQ,CAAC;IAgBd,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAY9D,QAAQ,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO;IAIrC;;;;OAIG;IACH,SAAS,CAAC,QAAQ,EAAE,QAAQ,GAAG,aAAa;CAO7C"}
@@ -0,0 +1,76 @@
1
+ import { DEFAULT_OAUTH_IDENTITY_FIELDS } from "./types.js";
2
+ function resolveIdentityFields(fields) {
3
+ const definedFields = Object.fromEntries(
4
+ Object.entries(fields ?? {}).filter(([, value]) => value !== void 0)
5
+ );
6
+ return { ...DEFAULT_OAUTH_IDENTITY_FIELDS, ...definedFields };
7
+ }
8
+ class IdentityModel {
9
+ accessor;
10
+ fields;
11
+ constructor(accessor, fields) {
12
+ this.accessor = accessor;
13
+ this.fields = fields;
14
+ }
15
+ async findByProviderUserId(provider, providerUserId) {
16
+ const record = await this.accessor.findFirst({
17
+ where: {
18
+ [this.fields.provider]: provider,
19
+ [this.fields.providerUserId]: providerUserId
20
+ }
21
+ });
22
+ return record ?? null;
23
+ }
24
+ async findByUserAndProvider(userId, provider) {
25
+ const record = await this.accessor.findFirst({
26
+ where: {
27
+ [this.fields.userId]: userId,
28
+ [this.fields.provider]: provider
29
+ }
30
+ });
31
+ return record ?? null;
32
+ }
33
+ async findAllForUser(userId) {
34
+ return this.accessor.findMany({
35
+ where: { [this.fields.userId]: userId }
36
+ });
37
+ }
38
+ async create(userId, provider, profile) {
39
+ return this.accessor.create({
40
+ data: {
41
+ [this.fields.userId]: userId,
42
+ [this.fields.provider]: provider,
43
+ [this.fields.providerUserId]: profile.providerUserId,
44
+ ...profile.username ? { [this.fields.providerUsername]: profile.username } : {},
45
+ ...profile.email ? { [this.fields.providerEmail]: profile.email } : {}
46
+ }
47
+ });
48
+ }
49
+ async delete(userId, provider) {
50
+ await this.accessor.deleteMany({
51
+ where: {
52
+ [this.fields.userId]: userId,
53
+ [this.fields.provider]: provider
54
+ }
55
+ });
56
+ }
57
+ userIdOf(identity) {
58
+ return identity[this.fields.userId];
59
+ }
60
+ /**
61
+ * Reads a fetched identity row back into the `OAuthUserInfo` shape
62
+ * `create` accepts, so a row can be recreated from a snapshot taken
63
+ * before it was deleted.
64
+ */
65
+ profileOf(identity) {
66
+ return {
67
+ providerUserId: identity[this.fields.providerUserId],
68
+ username: identity[this.fields.providerUsername] ?? void 0,
69
+ email: identity[this.fields.providerEmail] ?? void 0
70
+ };
71
+ }
72
+ }
73
+ export {
74
+ IdentityModel,
75
+ resolveIdentityFields
76
+ };
@@ -0,0 +1,10 @@
1
+ export { OAuthHandler } from './OAuthHandler.js';
2
+ export * from './errors.js';
3
+ export * from './types.js';
4
+ export { createOidcStrategy } from './oidc.js';
5
+ export { GOOGLE_PRESET, googleProvider } from './providers/google.js';
6
+ export { githubProvider } from './strategies/github.js';
7
+ export { parseOAuthRoute, parseAuthorizeFlow, normalizeOAuthRequest, } from './request.js';
8
+ export type { NormalizedOAuthRequest, OAuthRoute } from './request.js';
9
+ export { TRANSACTION_COOKIE_NAME, DEFAULT_TRANSACTION_EXPIRES_SECONDS, } from './transactionCookie.js';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAA;AAC9C,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AACrE,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAA;AACvD,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,qBAAqB,GACtB,MAAM,cAAc,CAAA;AACrB,YAAY,EAAE,sBAAsB,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACtE,OAAO,EACL,uBAAuB,EACvB,mCAAmC,GACpC,MAAM,wBAAwB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,27 @@
1
+ import { OAuthHandler } from "./OAuthHandler.js";
2
+ export * from "./errors.js";
3
+ export * from "./types.js";
4
+ import { createOidcStrategy } from "./oidc.js";
5
+ import { GOOGLE_PRESET, googleProvider } from "./providers/google.js";
6
+ import { githubProvider } from "./strategies/github.js";
7
+ import {
8
+ parseOAuthRoute,
9
+ parseAuthorizeFlow,
10
+ normalizeOAuthRequest
11
+ } from "./request.js";
12
+ import {
13
+ TRANSACTION_COOKIE_NAME,
14
+ DEFAULT_TRANSACTION_EXPIRES_SECONDS
15
+ } from "./transactionCookie.js";
16
+ export {
17
+ DEFAULT_TRANSACTION_EXPIRES_SECONDS,
18
+ GOOGLE_PRESET,
19
+ OAuthHandler,
20
+ TRANSACTION_COOKIE_NAME,
21
+ createOidcStrategy,
22
+ githubProvider,
23
+ googleProvider,
24
+ normalizeOAuthRequest,
25
+ parseAuthorizeFlow,
26
+ parseOAuthRoute
27
+ };
package/dist/oidc.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import type { OAuthProviderCredentials, OAuthStrategy, ProviderPreset } from './types.js';
2
+ /**
3
+ * Turns a data-only `ProviderPreset` (issuer, default scope) plus per-app
4
+ * credentials into a full `OAuthStrategy`, using `oauth4webapi` for OIDC
5
+ * discovery, the authorization-code + PKCE + nonce flow, and id_token
6
+ * verification. `oauth4webapi` is an optional peer dependency and is only
7
+ * ever lazy-imported here, so apps that don't configure any OIDC provider
8
+ * never need it installed.
9
+ */
10
+ export declare function createOidcStrategy(preset: ProviderPreset, credentials: OAuthProviderCredentials): OAuthStrategy;
11
+ //# sourceMappingURL=oidc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oidc.d.ts","sourceRoot":"","sources":["../src/oidc.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAGV,wBAAwB,EACxB,aAAa,EAEb,cAAc,EACf,MAAM,YAAY,CAAA;AAEnB;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,cAAc,EACtB,WAAW,EAAE,wBAAwB,GACpC,aAAa,CA2If"}
package/dist/oidc.js ADDED
@@ -0,0 +1,99 @@
1
+ import { ProviderError } from "./errors.js";
2
+ function createOidcStrategy(preset, credentials) {
3
+ let discoveryPromise;
4
+ async function discover() {
5
+ const oauth = await import("oauth4webapi");
6
+ const issuer = new URL(preset.issuer);
7
+ const insecureOptions = credentials.allowInsecureRequests ? { [oauth.allowInsecureRequests]: true } : {};
8
+ const response = await oauth.discoveryRequest(issuer, insecureOptions);
9
+ const as = await oauth.processDiscoveryResponse(issuer, response);
10
+ const client = {
11
+ client_id: credentials.clientId
12
+ };
13
+ const clientAuth = oauth.ClientSecretPost(credentials.clientSecret);
14
+ return { oauth, as, client, clientAuth };
15
+ }
16
+ function getDiscovery() {
17
+ if (!discoveryPromise) {
18
+ discoveryPromise = discover();
19
+ discoveryPromise.catch(() => {
20
+ discoveryPromise = void 0;
21
+ });
22
+ }
23
+ return discoveryPromise;
24
+ }
25
+ return {
26
+ name: preset.name,
27
+ redirectUri: credentials.redirectUri,
28
+ usesOidc: true,
29
+ async getAuthorizationUrl(ctx) {
30
+ const { as } = await getDiscovery();
31
+ if (!as.authorization_endpoint) {
32
+ throw new ProviderError(
33
+ `${preset.name} discovery document has no authorization_endpoint`
34
+ );
35
+ }
36
+ const url = new URL(as.authorization_endpoint);
37
+ url.searchParams.set("client_id", credentials.clientId);
38
+ url.searchParams.set("redirect_uri", ctx.redirectUri);
39
+ url.searchParams.set("response_type", "code");
40
+ url.searchParams.set("scope", credentials.scope ?? preset.scope);
41
+ url.searchParams.set("state", ctx.state);
42
+ url.searchParams.set("code_challenge", ctx.codeChallenge);
43
+ url.searchParams.set("code_challenge_method", "S256");
44
+ if (ctx.nonce) {
45
+ url.searchParams.set("nonce", ctx.nonce);
46
+ }
47
+ return url;
48
+ },
49
+ async handleCallback(ctx) {
50
+ const { oauth, as, client, clientAuth } = await getDiscovery();
51
+ const rawParams = { ...ctx.query, ...ctx.form };
52
+ const params = oauth.validateAuthResponse(
53
+ as,
54
+ client,
55
+ new URLSearchParams(rawParams),
56
+ ctx.state
57
+ );
58
+ const insecureOptions = credentials.allowInsecureRequests ? { [oauth.allowInsecureRequests]: true } : {};
59
+ const response = await oauth.authorizationCodeGrantRequest(
60
+ as,
61
+ client,
62
+ clientAuth,
63
+ params,
64
+ ctx.redirectUri,
65
+ ctx.codeVerifier,
66
+ insecureOptions
67
+ );
68
+ const result = await oauth.processAuthorizationCodeResponse(
69
+ as,
70
+ client,
71
+ response,
72
+ {
73
+ expectedNonce: ctx.nonce ?? oauth.expectNoNonce,
74
+ requireIdToken: true
75
+ }
76
+ );
77
+ const claims = oauth.getValidatedIdTokenClaims(result);
78
+ if (!claims) {
79
+ throw new ProviderError(`${preset.name} did not return an id_token`);
80
+ }
81
+ const emailVerified = typeof claims.email_verified === "boolean" ? claims.email_verified : void 0;
82
+ return {
83
+ providerUserId: claims.sub,
84
+ // An unverified email must never seed a username or feed the
85
+ // duplicate-account (`email_in_use`) check -- anyone can put an
86
+ // arbitrary address in an OIDC profile the provider hasn't
87
+ // confirmed they control, so only a claim with
88
+ // `email_verified: true` is passed through.
89
+ email: typeof claims.email === "string" && emailVerified === true ? claims.email : void 0,
90
+ emailVerified,
91
+ username: typeof claims.name === "string" ? claims.name : void 0,
92
+ raw: claims
93
+ };
94
+ }
95
+ };
96
+ }
97
+ export {
98
+ createOidcStrategy
99
+ };
@@ -0,0 +1,24 @@
1
+ import type { OAuthProviderCredentials, OAuthStrategy, ProviderPreset } from '../types.js';
2
+ /**
3
+ * Data-only Google OIDC preset. Google publishes discovery at
4
+ * `https://accounts.google.com/.well-known/openid-configuration`.
5
+ *
6
+ * @see https://developers.google.com/identity/openid-connect/openid-connect
7
+ */
8
+ export declare const GOOGLE_PRESET: ProviderPreset;
9
+ /**
10
+ * Convenience factory: `createOidcStrategy(GOOGLE_PRESET, credentials)`.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * providers: {
15
+ * google: googleProvider({
16
+ * clientId: process.env.GOOGLE_CLIENT_ID,
17
+ * clientSecret: process.env.GOOGLE_CLIENT_SECRET,
18
+ * redirectUri: `${apiUrl}/auth/oauth/google/callback`,
19
+ * }),
20
+ * }
21
+ * ```
22
+ */
23
+ export declare function googleProvider(credentials: OAuthProviderCredentials): OAuthStrategy;
24
+ //# sourceMappingURL=google.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google.d.ts","sourceRoot":"","sources":["../../src/providers/google.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,wBAAwB,EACxB,aAAa,EACb,cAAc,EACf,MAAM,aAAa,CAAA;AAEpB;;;;;GAKG;AACH,eAAO,MAAM,aAAa,EAAE,cAI3B,CAAA;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAC5B,WAAW,EAAE,wBAAwB,GACpC,aAAa,CAEf"}
@@ -0,0 +1,13 @@
1
+ import { createOidcStrategy } from "../oidc.js";
2
+ const GOOGLE_PRESET = {
3
+ name: "Google",
4
+ issuer: "https://accounts.google.com",
5
+ scope: "openid email profile"
6
+ };
7
+ function googleProvider(credentials) {
8
+ return createOidcStrategy(GOOGLE_PRESET, credentials);
9
+ }
10
+ export {
11
+ GOOGLE_PRESET,
12
+ googleProvider
13
+ };
@@ -0,0 +1,36 @@
1
+ import type { APIGatewayProxyEvent } from 'aws-lambda';
2
+ import type { OAuthFlow } from './types.js';
3
+ export interface NormalizedOAuthRequest {
4
+ method: string;
5
+ /** URL pathname, e.g. `/auth/oauth/google/callback`. */
6
+ path: string;
7
+ headers: Headers;
8
+ query: Record<string, string>;
9
+ /** Parsed `application/x-www-form-urlencoded` body, present on POST callbacks. */
10
+ form: Record<string, string>;
11
+ }
12
+ /**
13
+ * Normalizes a Lambda-style event or a Fetch `Request` into the subset of
14
+ * fields the OAuth handler needs. Unlike `@cedarjs/api`'s `normalizeRequest`
15
+ * (which always JSON-parses the body), this also handles
16
+ * `application/x-www-form-urlencoded` bodies — required for `form_post`
17
+ * callbacks (Apple-shaped providers POST the callback as a form, not JSON).
18
+ */
19
+ export declare function normalizeOAuthRequest(event: APIGatewayProxyEvent | Request): Promise<NormalizedOAuthRequest>;
20
+ export interface OAuthRoute {
21
+ provider: string;
22
+ action: 'authorize' | 'callback' | 'unlink';
23
+ }
24
+ /**
25
+ * Parses `{basePath}/{provider}/{action}` out of a request path. Returns
26
+ * `null` when the path doesn't match a recognized OAuth route (the caller
27
+ * should treat that as a 404).
28
+ */
29
+ export declare function parseOAuthRoute(path: string, basePath: string): OAuthRoute | null;
30
+ /**
31
+ * Reads the `flow` query param off an `/authorize` request. Defaults to
32
+ * `'login'`. Anything other than `login`/`signup`/`link` is treated as
33
+ * `login` too — `authorize` never accepts `unlink` as a flow.
34
+ */
35
+ export declare function parseAuthorizeFlow(query: Record<string, string>): Extract<OAuthFlow, 'login' | 'signup' | 'link'>;
36
+ //# sourceMappingURL=request.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAItD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAE3C,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAA;IACd,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC7B,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC7B;AAqFD;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,oBAAoB,GAAG,OAAO,GACpC,OAAO,CAAC,sBAAsB,CAAC,CAMjC;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,WAAW,GAAG,UAAU,GAAG,QAAQ,CAAA;CAC5C;AA4BD;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GACf,UAAU,GAAG,IAAI,CAgCnB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC5B,OAAO,CAAC,SAAS,EAAE,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC,CAQjD"}
@@ -0,0 +1,113 @@
1
+ import { isFetchApiRequest } from "@cedarjs/api";
2
+ function parseFormBody(text) {
3
+ const form = {};
4
+ new URLSearchParams(text).forEach((value, key) => {
5
+ form[key] = value;
6
+ });
7
+ return form;
8
+ }
9
+ function isFormUrlEncoded(headers) {
10
+ const contentType = headers.get("content-type") ?? "";
11
+ return contentType.toLowerCase().includes("application/x-www-form-urlencoded");
12
+ }
13
+ async function normalizeFetchRequest(event) {
14
+ const url = new URL(event.url);
15
+ const query = {};
16
+ url.searchParams.forEach((value, key) => {
17
+ query[key] = value;
18
+ });
19
+ let form = {};
20
+ if (isFormUrlEncoded(event.headers)) {
21
+ const text = await event.text();
22
+ form = text ? parseFormBody(text) : {};
23
+ }
24
+ return {
25
+ method: event.method,
26
+ path: url.pathname,
27
+ headers: event.headers,
28
+ query,
29
+ form
30
+ };
31
+ }
32
+ function decodeLambdaBody(event) {
33
+ if (!event.body) {
34
+ return "";
35
+ }
36
+ return event.isBase64Encoded ? Buffer.from(event.body, "base64").toString("utf-8") : event.body;
37
+ }
38
+ function compactQueryParams(params) {
39
+ const query = {};
40
+ for (const [key, value] of Object.entries(params ?? {})) {
41
+ if (value !== void 0) {
42
+ query[key] = value;
43
+ }
44
+ }
45
+ return query;
46
+ }
47
+ function normalizeLambdaRequest(event) {
48
+ const headers = new Headers(event.headers);
49
+ let form = {};
50
+ if (isFormUrlEncoded(headers)) {
51
+ const text = decodeLambdaBody(event);
52
+ form = text ? parseFormBody(text) : {};
53
+ }
54
+ return {
55
+ method: event.httpMethod,
56
+ path: event.path,
57
+ headers,
58
+ query: compactQueryParams(event.queryStringParameters),
59
+ form
60
+ };
61
+ }
62
+ async function normalizeOAuthRequest(event) {
63
+ if (isFetchApiRequest(event)) {
64
+ return normalizeFetchRequest(event);
65
+ }
66
+ return normalizeLambdaRequest(event);
67
+ }
68
+ function trimTrailingSlashes(value) {
69
+ let end = value.length;
70
+ while (end > 0 && value[end - 1] === "/") {
71
+ end--;
72
+ }
73
+ return value.slice(0, end);
74
+ }
75
+ function trimLeadingSlashes(value) {
76
+ let start = 0;
77
+ while (start < value.length && value[start] === "/") {
78
+ start++;
79
+ }
80
+ return value.slice(start);
81
+ }
82
+ function parseOAuthRoute(path, basePath) {
83
+ const normalizedBase = trimTrailingSlashes(basePath);
84
+ if (!path.startsWith(normalizedBase)) {
85
+ return null;
86
+ }
87
+ const boundaryChar = path[normalizedBase.length];
88
+ if (boundaryChar !== void 0 && boundaryChar !== "/") {
89
+ return null;
90
+ }
91
+ const rest = trimLeadingSlashes(path.slice(normalizedBase.length));
92
+ const segments = rest.split("/").filter(Boolean);
93
+ if (segments.length !== 2) {
94
+ return null;
95
+ }
96
+ const [provider, action] = segments;
97
+ if (action !== "authorize" && action !== "callback" && action !== "unlink") {
98
+ return null;
99
+ }
100
+ return { provider, action };
101
+ }
102
+ function parseAuthorizeFlow(query) {
103
+ const flow = query.flow;
104
+ if (flow === "signup" || flow === "link") {
105
+ return flow;
106
+ }
107
+ return "login";
108
+ }
109
+ export {
110
+ normalizeOAuthRequest,
111
+ parseAuthorizeFlow,
112
+ parseOAuthRoute
113
+ };
@@ -0,0 +1,21 @@
1
+ import type { OAuthProviderCredentials, OAuthStrategy } from '../types.js';
2
+ /**
3
+ * GitHub's standard user OAuth flow issues no id_token (its OIDC discovery
4
+ * document is a preview limited to MCP clients), so this strategy is built
5
+ * entirely through the public `OAuthStrategy` interface — the same one
6
+ * available to userland strategy packages — rather than the OIDC path in
7
+ * `oidc.ts`.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * providers: {
12
+ * github: githubProvider({
13
+ * clientId: process.env.GITHUB_CLIENT_ID,
14
+ * clientSecret: process.env.GITHUB_CLIENT_SECRET,
15
+ * redirectUri: `${apiUrl}/auth/oauth/github/callback`,
16
+ * }),
17
+ * }
18
+ * ```
19
+ */
20
+ export declare function githubProvider(credentials: OAuthProviderCredentials): OAuthStrategy;
21
+ //# sourceMappingURL=github.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github.d.ts","sourceRoot":"","sources":["../../src/strategies/github.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAGV,wBAAwB,EACxB,aAAa,EAEd,MAAM,aAAa,CAAA;AAoBpB;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC5B,WAAW,EAAE,wBAAwB,GACpC,aAAa,CAwGf"}
@@ -0,0 +1,104 @@
1
+ import { ProviderError } from "../errors.js";
2
+ const AUTHORIZATION_ENDPOINT = "https://github.com/login/oauth/authorize";
3
+ const TOKEN_ENDPOINT = "https://github.com/login/oauth/access_token";
4
+ const PROFILE_ENDPOINT = "https://api.github.com/user";
5
+ const EMAILS_ENDPOINT = "https://api.github.com/user/emails";
6
+ function githubProvider(credentials) {
7
+ return {
8
+ name: "GitHub",
9
+ redirectUri: credentials.redirectUri,
10
+ usesOidc: false,
11
+ getAuthorizationUrl(ctx) {
12
+ const url = new URL(AUTHORIZATION_ENDPOINT);
13
+ url.searchParams.set("client_id", credentials.clientId);
14
+ url.searchParams.set("redirect_uri", ctx.redirectUri);
15
+ url.searchParams.set("scope", credentials.scope ?? "read:user user:email");
16
+ url.searchParams.set("state", ctx.state);
17
+ url.searchParams.set("code_challenge", ctx.codeChallenge);
18
+ url.searchParams.set("code_challenge_method", "S256");
19
+ return url;
20
+ },
21
+ async handleCallback(ctx) {
22
+ const oauth = await import("oauth4webapi");
23
+ const as = {
24
+ issuer: "https://github.com",
25
+ token_endpoint: TOKEN_ENDPOINT
26
+ };
27
+ const client = {
28
+ client_id: credentials.clientId
29
+ };
30
+ const clientAuth = oauth.ClientSecretPost(credentials.clientSecret);
31
+ const rawParams = { ...ctx.query, ...ctx.form };
32
+ const params = oauth.validateAuthResponse(
33
+ as,
34
+ client,
35
+ new URLSearchParams(rawParams),
36
+ ctx.state
37
+ );
38
+ const response = await oauth.authorizationCodeGrantRequest(
39
+ as,
40
+ client,
41
+ clientAuth,
42
+ params,
43
+ ctx.redirectUri,
44
+ ctx.codeVerifier,
45
+ // GitHub's token endpoint returns form-urlencoded by default; ask
46
+ // for JSON explicitly, which is what oauth4webapi expects to parse.
47
+ { headers: { Accept: "application/json" } }
48
+ );
49
+ const result = await oauth.processAuthorizationCodeResponse(
50
+ as,
51
+ client,
52
+ response,
53
+ { requireIdToken: false }
54
+ );
55
+ const profile = await fetchJson(
56
+ PROFILE_ENDPOINT,
57
+ result.access_token
58
+ );
59
+ if (typeof profile.id !== "number") {
60
+ throw new ProviderError(
61
+ "GitHub profile response is missing a numeric id"
62
+ );
63
+ }
64
+ let email = profile.email ?? void 0;
65
+ let emailVerified;
66
+ if (!email) {
67
+ const emails = await fetchJson(
68
+ EMAILS_ENDPOINT,
69
+ result.access_token
70
+ );
71
+ const primary = emails.find((e) => e.primary && e.verified);
72
+ email = primary?.email;
73
+ emailVerified = primary ? true : void 0;
74
+ }
75
+ return {
76
+ // GitHub's numeric `id` is immutable; `login` (the username) can
77
+ // change, so it's never used to key account lookup.
78
+ providerUserId: String(profile.id),
79
+ email,
80
+ emailVerified,
81
+ username: profile.login,
82
+ raw: profile
83
+ };
84
+ }
85
+ };
86
+ }
87
+ async function fetchJson(url, accessToken) {
88
+ const response = await fetch(url, {
89
+ headers: {
90
+ Authorization: `Bearer ${accessToken}`,
91
+ Accept: "application/vnd.github+json",
92
+ "User-Agent": "CedarJS"
93
+ }
94
+ });
95
+ if (!response.ok) {
96
+ throw new ProviderError(
97
+ `GitHub API request to ${url} failed with status ${response.status}`
98
+ );
99
+ }
100
+ return response.json();
101
+ }
102
+ export {
103
+ githubProvider
104
+ };
@@ -0,0 +1,55 @@
1
+ import type { DbAuthCookieConfig } from '@cedarjs/auth-dbauth-api';
2
+ import type { OAuthFlow } from './types.js';
3
+ export declare const TRANSACTION_COOKIE_NAME = "oauth-transaction";
4
+ /** Default lifetime of the OAuth transaction cookie: 10 minutes. */
5
+ export declare const DEFAULT_TRANSACTION_EXPIRES_SECONDS: number;
6
+ export interface OAuthTransactionData {
7
+ provider: string;
8
+ flow: OAuthFlow;
9
+ state: string;
10
+ codeVerifier: string;
11
+ nonce?: string;
12
+ /** Epoch milliseconds the transaction was created at, checked independently of the cookie's `Expires` attribute (which the browser, not the server, enforces). */
13
+ createdAt: number;
14
+ }
15
+ /**
16
+ * Encodes transaction data for the cookie value. Reuses `encryptSession`
17
+ * (AES-256-CBC keyed by `SESSION_SECRET`, same as the dbAuth session cookie)
18
+ * for the encryption, but not `decryptSession`'s `data;csrf` string
19
+ * convention — the payload is wrapped in a base64url string first so it can
20
+ * never collide with the `;` that convention splits on.
21
+ */
22
+ export declare function encodeTransactionCookie(data: OAuthTransactionData): string;
23
+ /**
24
+ * Decrypts and decodes a transaction cookie value produced by
25
+ * `encodeTransactionCookie`. Returns `null` when the cookie is missing,
26
+ * tampered with, or otherwise unreadable — the caller should treat that as
27
+ * an expired/invalid transaction rather than a crash.
28
+ */
29
+ export declare function decodeTransactionCookie(cookieValue: string | null | undefined): OAuthTransactionData | null;
30
+ /**
31
+ * Returns `true` when the transaction was created more than
32
+ * `expiresSeconds` ago. The cookie's own `Expires` attribute is enforced by
33
+ * the browser, not the server, so this is checked independently.
34
+ */
35
+ export declare function isTransactionExpired(data: OAuthTransactionData, expiresSeconds: number): boolean;
36
+ /**
37
+ * Builds the `Set-Cookie` header string for the OAuth transaction cookie.
38
+ */
39
+ export declare function createTransactionCookieString({ data, cookieConfig, expiresSeconds, }: {
40
+ data: OAuthTransactionData;
41
+ cookieConfig?: DbAuthCookieConfig;
42
+ expiresSeconds: number;
43
+ }): string;
44
+ /**
45
+ * Builds the `Set-Cookie` header string that clears the OAuth transaction
46
+ * cookie (used once the callback has consumed it, on both success and
47
+ * failure — clearing on failure prevents a stale transaction from being
48
+ * replayed).
49
+ */
50
+ export declare function clearTransactionCookieString(cookieConfig?: DbAuthCookieConfig): string;
51
+ /**
52
+ * Extracts the raw transaction cookie value out of a `Cookie` header string.
53
+ */
54
+ export declare function getTransactionCookieValue(cookieHeader: string | null | undefined): string | null;
55
+ //# sourceMappingURL=transactionCookie.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transactionCookie.d.ts","sourceRoot":"","sources":["../src/transactionCookie.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAEV,kBAAkB,EACnB,MAAM,0BAA0B,CAAA;AAEjC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAE3C,eAAO,MAAM,uBAAuB,sBAAsB,CAAA;AAE1D,oEAAoE;AACpE,eAAO,MAAM,mCAAmC,QAAU,CAAA;AAiB1D,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,SAAS,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,kKAAkK;IAClK,SAAS,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,oBAAoB,GAAG,MAAM,CAI1E;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACrC,oBAAoB,GAAG,IAAI,CAyB7B;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,oBAAoB,EAC1B,cAAc,EAAE,MAAM,GACrB,OAAO,CAET;AAED;;GAEG;AACH,wBAAgB,6BAA6B,CAAC,EAC5C,IAAI,EACJ,YAAY,EACZ,cAAc,GACf,EAAE;IACD,IAAI,EAAE,oBAAoB,CAAA;IAC1B,YAAY,CAAC,EAAE,kBAAkB,CAAA;IACjC,cAAc,EAAE,MAAM,CAAA;CACvB,GAAG,MAAM,CAsBT;AAED;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAC1C,YAAY,CAAC,EAAE,kBAAkB,GAChC,MAAM,CAKR;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACtC,MAAM,GAAG,IAAI,CAcf"}