@q32/core 0.1.13 → 0.1.15

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/oauth.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { FetchLike } from "./http.js";
1
2
  export type OAuthMetadataOptions = {
2
3
  issuer: string;
3
4
  authorizationPath?: string;
@@ -14,4 +15,56 @@ export declare function mcpServerCard(options: {
14
15
  description?: string;
15
16
  url: string;
16
17
  }): Record<string, unknown>;
18
+ export type OAuthAuthorizationUrlInput = {
19
+ clientId: string;
20
+ redirectUri: string;
21
+ state: string;
22
+ scope?: string;
23
+ extraParams?: Record<string, string | undefined>;
24
+ };
25
+ export type OAuthCodeExchangeInput = {
26
+ clientId: string;
27
+ clientSecret: string;
28
+ code: string;
29
+ redirectUri?: string;
30
+ };
31
+ export type GoogleUserProfile = {
32
+ sub: string;
33
+ email: string;
34
+ email_verified?: boolean;
35
+ name?: string;
36
+ picture?: string;
37
+ };
38
+ export type GitHubUserProfile = {
39
+ id: number;
40
+ login: string;
41
+ email: string | null;
42
+ name: string | null;
43
+ avatar_url: string | null;
44
+ };
45
+ export declare class GoogleOAuthClient {
46
+ private readonly fetchImpl;
47
+ constructor(fetchImpl?: FetchLike);
48
+ buildAuthorizationUrl(input: OAuthAuthorizationUrlInput): string;
49
+ exchangeCode(input: Required<OAuthCodeExchangeInput>): Promise<{
50
+ accessToken: string;
51
+ }>;
52
+ fetchUserProfile(accessToken: string, options?: {
53
+ requireVerifiedEmail?: boolean;
54
+ }): Promise<GoogleUserProfile>;
55
+ }
56
+ export declare class GitHubOAuthClient {
57
+ private readonly options;
58
+ constructor(options?: {
59
+ fetch?: FetchLike;
60
+ userAgent?: string;
61
+ });
62
+ buildAuthorizationUrl(input: OAuthAuthorizationUrlInput): string;
63
+ exchangeCode(input: OAuthCodeExchangeInput): Promise<{
64
+ accessToken: string;
65
+ }>;
66
+ fetchUserProfile(accessToken: string): Promise<GitHubUserProfile>;
67
+ fetchPrimaryEmail(accessToken: string): Promise<string | null>;
68
+ private githubHeaders;
69
+ }
17
70
  //# sourceMappingURL=oauth.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC,CAAC;AAEF,wBAAgB,gCAAgC,CAAC,OAAO,EAAE,oBAAoB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAevG;AAED,wBAAgB,8BAA8B,CAAC,QAAQ,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAOxI;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAOnH"}
1
+ {"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"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"}
package/dist/oauth.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { defaultFetch } from "./http.js";
1
2
  export function oauthAuthorizationServerMetadata(options) {
2
3
  const issuer = options.issuer.replace(/\/$/, "");
3
4
  return {
@@ -30,4 +31,127 @@ export function mcpServerCard(options) {
30
31
  transport: "http",
31
32
  };
32
33
  }
34
+ export class GoogleOAuthClient {
35
+ fetchImpl;
36
+ constructor(fetchImpl = defaultFetch) {
37
+ this.fetchImpl = fetchImpl;
38
+ }
39
+ buildAuthorizationUrl(input) {
40
+ const url = new URL("https://accounts.google.com/o/oauth2/v2/auth");
41
+ url.searchParams.set("client_id", input.clientId);
42
+ url.searchParams.set("redirect_uri", input.redirectUri);
43
+ url.searchParams.set("response_type", "code");
44
+ url.searchParams.set("scope", input.scope ?? "openid email profile");
45
+ url.searchParams.set("state", input.state);
46
+ setExtraSearchParams(url, input.extraParams);
47
+ return url.toString();
48
+ }
49
+ async exchangeCode(input) {
50
+ const response = await this.fetchImpl("https://oauth2.googleapis.com/token", {
51
+ method: "POST",
52
+ headers: {
53
+ accept: "application/json",
54
+ "content-type": "application/x-www-form-urlencoded",
55
+ },
56
+ body: new URLSearchParams({
57
+ client_id: input.clientId,
58
+ client_secret: input.clientSecret,
59
+ code: input.code,
60
+ redirect_uri: input.redirectUri,
61
+ grant_type: "authorization_code",
62
+ }),
63
+ });
64
+ if (!response.ok)
65
+ throw new Error(`Google token exchange failed: ${response.status}`);
66
+ const payload = (await response.json());
67
+ if (!payload.access_token)
68
+ throw new Error(payload.error ?? "Google token exchange missing access token");
69
+ return { accessToken: payload.access_token };
70
+ }
71
+ async fetchUserProfile(accessToken, options = {}) {
72
+ const response = await this.fetchImpl("https://openidconnect.googleapis.com/v1/userinfo", {
73
+ headers: {
74
+ accept: "application/json",
75
+ authorization: `Bearer ${accessToken}`,
76
+ },
77
+ });
78
+ if (!response.ok)
79
+ throw new Error(`Google profile fetch failed: ${response.status}`);
80
+ const profile = (await response.json());
81
+ if (!profile.sub || !profile.email)
82
+ throw new Error("Google profile missing identity fields");
83
+ if (options.requireVerifiedEmail && profile.email_verified === false)
84
+ throw new Error("Google profile missing verified identity fields");
85
+ return profile;
86
+ }
87
+ }
88
+ export class GitHubOAuthClient {
89
+ options;
90
+ constructor(options = {}) {
91
+ this.options = options;
92
+ }
93
+ buildAuthorizationUrl(input) {
94
+ const url = new URL("https://github.com/login/oauth/authorize");
95
+ url.searchParams.set("client_id", input.clientId);
96
+ url.searchParams.set("redirect_uri", input.redirectUri);
97
+ url.searchParams.set("scope", input.scope ?? "read:user user:email");
98
+ url.searchParams.set("state", input.state);
99
+ setExtraSearchParams(url, input.extraParams);
100
+ return url.toString();
101
+ }
102
+ async exchangeCode(input) {
103
+ const response = await (this.options.fetch ?? defaultFetch)("https://github.com/login/oauth/access_token", {
104
+ method: "POST",
105
+ headers: {
106
+ accept: "application/json",
107
+ "content-type": "application/json",
108
+ },
109
+ body: JSON.stringify({
110
+ client_id: input.clientId,
111
+ client_secret: input.clientSecret,
112
+ code: input.code,
113
+ ...(input.redirectUri ? { redirect_uri: input.redirectUri } : {}),
114
+ }),
115
+ });
116
+ if (!response.ok)
117
+ throw new Error(`GitHub token exchange failed: ${response.status}`);
118
+ const payload = (await response.json());
119
+ if (!payload.access_token)
120
+ throw new Error(payload.error ?? "GitHub token exchange missing access token");
121
+ return { accessToken: payload.access_token };
122
+ }
123
+ async fetchUserProfile(accessToken) {
124
+ const response = await (this.options.fetch ?? defaultFetch)("https://api.github.com/user", {
125
+ headers: this.githubHeaders(accessToken),
126
+ });
127
+ if (!response.ok)
128
+ throw new Error(`GitHub profile fetch failed: ${response.status}`);
129
+ const profile = (await response.json());
130
+ if (!profile.id || !profile.login)
131
+ throw new Error("GitHub profile missing identity fields");
132
+ return profile;
133
+ }
134
+ async fetchPrimaryEmail(accessToken) {
135
+ const response = await (this.options.fetch ?? defaultFetch)("https://api.github.com/user/emails", {
136
+ headers: this.githubHeaders(accessToken),
137
+ });
138
+ if (!response.ok)
139
+ return null;
140
+ const emails = (await response.json());
141
+ return emails.find((email) => email.primary && email.verified)?.email ?? emails.find((email) => email.verified)?.email ?? null;
142
+ }
143
+ githubHeaders(accessToken) {
144
+ return {
145
+ accept: "application/vnd.github+json",
146
+ authorization: `Bearer ${accessToken}`,
147
+ "user-agent": this.options.userAgent ?? "@q32/core",
148
+ };
149
+ }
150
+ }
151
+ function setExtraSearchParams(url, params) {
152
+ for (const [key, value] of Object.entries(params ?? {})) {
153
+ if (value !== undefined)
154
+ url.searchParams.set(key, value);
155
+ }
156
+ }
33
157
  //# sourceMappingURL=oauth.js.map
package/dist/oauth.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"oauth.js","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"AAUA,MAAM,UAAU,gCAAgC,CAAC,OAA6B;IAC5E,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACjD,OAAO;QACL,MAAM;QACN,sBAAsB,EAAE,GAAG,MAAM,GAAG,OAAO,CAAC,iBAAiB,IAAI,YAAY,EAAE;QAC/E,cAAc,EAAE,GAAG,MAAM,GAAG,OAAO,CAAC,SAAS,IAAI,QAAQ,EAAE;QAC3D,qBAAqB,EAAE,GAAG,MAAM,GAAG,OAAO,CAAC,gBAAgB,IAAI,WAAW,EAAE;QAC5E,mBAAmB,EAAE,GAAG,MAAM,GAAG,OAAO,CAAC,cAAc,IAAI,SAAS,EAAE;QACtE,wBAAwB,EAAE,CAAC,MAAM,CAAC;QAClC,qBAAqB,EAAE,CAAC,oBAAoB,EAAE,eAAe,CAAC;QAC9D,qCAAqC,EAAE,CAAC,oBAAoB,EAAE,MAAM,CAAC;QACrE,gCAAgC,EAAE,CAAC,MAAM,CAAC;QAC1C,gBAAgB,EAAE,OAAO,CAAC,MAAM,IAAI,CAAC,UAAU,EAAE,WAAW,CAAC;QAC7D,sBAAsB,EAAE,OAAO,CAAC,qBAAqB,IAAI,GAAG,MAAM,MAAM;KACzE,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,8BAA8B,CAAC,QAAgB,EAAE,mBAA2B,EAAE,MAAiB;IAC7G,OAAO;QACL,QAAQ;QACR,qBAAqB,EAAE,CAAC,mBAAmB,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QAC/D,gBAAgB,EAAE,MAAM,IAAI,CAAC,UAAU,EAAE,WAAW,CAAC;QACrD,wBAAwB,EAAE,CAAC,QAAQ,CAAC;KACrC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,OAA4D;IACxF,OAAO;QACL,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,WAAW,EAAE,OAAO,CAAC,WAAW;QAChC,GAAG,EAAE,OAAO,CAAC,GAAG;QAChB,SAAS,EAAE,MAAM;KAClB,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"oauth.js","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@q32/core",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Shared TypeScript primitives for Q32 Cloudflare Worker projects.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/oauth.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import type { FetchLike } from "./http.js";
2
+ import { defaultFetch } from "./http.js";
3
+
1
4
  export type OAuthMetadataOptions = {
2
5
  issuer: string;
3
6
  authorizationPath?: string;
@@ -42,3 +45,160 @@ export function mcpServerCard(options: { name: string; description?: string; url
42
45
  transport: "http",
43
46
  };
44
47
  }
48
+
49
+ export type OAuthAuthorizationUrlInput = {
50
+ clientId: string;
51
+ redirectUri: string;
52
+ state: string;
53
+ scope?: string;
54
+ extraParams?: Record<string, string | undefined>;
55
+ };
56
+
57
+ export type OAuthCodeExchangeInput = {
58
+ clientId: string;
59
+ clientSecret: string;
60
+ code: string;
61
+ redirectUri?: string;
62
+ };
63
+
64
+ export type GoogleUserProfile = {
65
+ sub: string;
66
+ email: string;
67
+ email_verified?: boolean;
68
+ name?: string;
69
+ picture?: string;
70
+ };
71
+
72
+ export type GitHubUserProfile = {
73
+ id: number;
74
+ login: string;
75
+ email: string | null;
76
+ name: string | null;
77
+ avatar_url: string | null;
78
+ };
79
+
80
+ export class GoogleOAuthClient {
81
+ constructor(private readonly fetchImpl: FetchLike = defaultFetch) {}
82
+
83
+ buildAuthorizationUrl(input: OAuthAuthorizationUrlInput): string {
84
+ const url = new URL("https://accounts.google.com/o/oauth2/v2/auth");
85
+ url.searchParams.set("client_id", input.clientId);
86
+ url.searchParams.set("redirect_uri", input.redirectUri);
87
+ url.searchParams.set("response_type", "code");
88
+ url.searchParams.set("scope", input.scope ?? "openid email profile");
89
+ url.searchParams.set("state", input.state);
90
+ setExtraSearchParams(url, input.extraParams);
91
+ return url.toString();
92
+ }
93
+
94
+ async exchangeCode(input: Required<OAuthCodeExchangeInput>): Promise<{ accessToken: string }> {
95
+ const response = await this.fetchImpl("https://oauth2.googleapis.com/token", {
96
+ method: "POST",
97
+ headers: {
98
+ accept: "application/json",
99
+ "content-type": "application/x-www-form-urlencoded",
100
+ },
101
+ body: new URLSearchParams({
102
+ client_id: input.clientId,
103
+ client_secret: input.clientSecret,
104
+ code: input.code,
105
+ redirect_uri: input.redirectUri,
106
+ grant_type: "authorization_code",
107
+ }),
108
+ });
109
+ if (!response.ok) throw new Error(`Google token exchange failed: ${response.status}`);
110
+ const payload = (await response.json()) as { access_token?: string; error?: string };
111
+ if (!payload.access_token) throw new Error(payload.error ?? "Google token exchange missing access token");
112
+ return { accessToken: payload.access_token };
113
+ }
114
+
115
+ async fetchUserProfile(accessToken: string, options: { requireVerifiedEmail?: boolean } = {}): Promise<GoogleUserProfile> {
116
+ const response = await this.fetchImpl("https://openidconnect.googleapis.com/v1/userinfo", {
117
+ headers: {
118
+ accept: "application/json",
119
+ authorization: `Bearer ${accessToken}`,
120
+ },
121
+ });
122
+ if (!response.ok) throw new Error(`Google profile fetch failed: ${response.status}`);
123
+ const profile = (await response.json()) as GoogleUserProfile;
124
+ if (!profile.sub || !profile.email) throw new Error("Google profile missing identity fields");
125
+ if (options.requireVerifiedEmail && profile.email_verified === false) throw new Error("Google profile missing verified identity fields");
126
+ return profile;
127
+ }
128
+ }
129
+
130
+ export class GitHubOAuthClient {
131
+ constructor(
132
+ private readonly options: {
133
+ fetch?: FetchLike;
134
+ userAgent?: string;
135
+ } = {},
136
+ ) {}
137
+
138
+ buildAuthorizationUrl(input: OAuthAuthorizationUrlInput): string {
139
+ const url = new URL("https://github.com/login/oauth/authorize");
140
+ url.searchParams.set("client_id", input.clientId);
141
+ url.searchParams.set("redirect_uri", input.redirectUri);
142
+ url.searchParams.set("scope", input.scope ?? "read:user user:email");
143
+ url.searchParams.set("state", input.state);
144
+ setExtraSearchParams(url, input.extraParams);
145
+ return url.toString();
146
+ }
147
+
148
+ async exchangeCode(input: OAuthCodeExchangeInput): Promise<{ accessToken: string }> {
149
+ const response = await (this.options.fetch ?? defaultFetch)("https://github.com/login/oauth/access_token", {
150
+ method: "POST",
151
+ headers: {
152
+ accept: "application/json",
153
+ "content-type": "application/json",
154
+ },
155
+ body: JSON.stringify({
156
+ client_id: input.clientId,
157
+ client_secret: input.clientSecret,
158
+ code: input.code,
159
+ ...(input.redirectUri ? { redirect_uri: input.redirectUri } : {}),
160
+ }),
161
+ });
162
+ if (!response.ok) throw new Error(`GitHub token exchange failed: ${response.status}`);
163
+ const payload = (await response.json()) as { access_token?: string; error?: string };
164
+ if (!payload.access_token) throw new Error(payload.error ?? "GitHub token exchange missing access token");
165
+ return { accessToken: payload.access_token };
166
+ }
167
+
168
+ async fetchUserProfile(accessToken: string): Promise<GitHubUserProfile> {
169
+ const response = await (this.options.fetch ?? defaultFetch)("https://api.github.com/user", {
170
+ headers: this.githubHeaders(accessToken),
171
+ });
172
+ if (!response.ok) throw new Error(`GitHub profile fetch failed: ${response.status}`);
173
+ const profile = (await response.json()) as GitHubUserProfile;
174
+ if (!profile.id || !profile.login) throw new Error("GitHub profile missing identity fields");
175
+ return profile;
176
+ }
177
+
178
+ async fetchPrimaryEmail(accessToken: string): Promise<string | null> {
179
+ const response = await (this.options.fetch ?? defaultFetch)("https://api.github.com/user/emails", {
180
+ headers: this.githubHeaders(accessToken),
181
+ });
182
+ if (!response.ok) return null;
183
+ const emails = (await response.json()) as Array<{
184
+ email?: string;
185
+ primary?: boolean;
186
+ verified?: boolean;
187
+ }>;
188
+ return emails.find((email) => email.primary && email.verified)?.email ?? emails.find((email) => email.verified)?.email ?? null;
189
+ }
190
+
191
+ private githubHeaders(accessToken: string): Record<string, string> {
192
+ return {
193
+ accept: "application/vnd.github+json",
194
+ authorization: `Bearer ${accessToken}`,
195
+ "user-agent": this.options.userAgent ?? "@q32/core",
196
+ };
197
+ }
198
+ }
199
+
200
+ function setExtraSearchParams(url: URL, params: Record<string, string | undefined> | undefined): void {
201
+ for (const [key, value] of Object.entries(params ?? {})) {
202
+ if (value !== undefined) url.searchParams.set(key, value);
203
+ }
204
+ }