startgg-oauth2-full 0.2.1 → 0.2.2
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/README.md +317 -52
- package/dist/auth/StartGGOAuth2.d.ts +5 -4
- package/dist/auth/StartGGOAuth2.js +56 -41
- package/dist/constants.d.ts +10 -0
- package/dist/constants.js +16 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/package.json +63 -35
package/README.md
CHANGED
|
@@ -37,24 +37,25 @@ Cooked for *you* by 0xabadbabe - using a lot of 💜 and few lines of code.
|
|
|
37
37
|
|
|
38
38
|
```fish
|
|
39
39
|
┬─[playerone@fedora:~/d/startgg-oauth2-full]─[21:08:50]─[G:main =]
|
|
40
|
-
╰─>$ npm test
|
|
40
|
+
╰─>$ npm test
|
|
41
41
|
|
|
42
|
-
> startgg-oauth2-full@0.2.
|
|
43
|
-
> jest --runInBand
|
|
42
|
+
> startgg-oauth2-full@0.2.2 test
|
|
43
|
+
> jest --runInBand
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
PASS __tests__/pkce.test.ts
|
|
46
|
+
PASS __tests__/handler.test.ts
|
|
47
|
+
PASS __tests__/authorize-url.test.ts
|
|
48
|
+
PASS __tests__/constants.test.ts
|
|
49
|
+
PASS __tests__/bearer-token.test.ts
|
|
49
50
|
|
|
50
|
-
Test Suites:
|
|
51
|
-
Tests:
|
|
51
|
+
Test Suites: 5 passed, 5 total
|
|
52
|
+
Tests: 28 passed, 28 total
|
|
52
53
|
Snapshots: 0 total
|
|
53
|
-
Time: 0.
|
|
54
|
-
Ran all test suites
|
|
54
|
+
Time: 0.859 s, estimated 1 s
|
|
55
|
+
Ran all test suites.
|
|
55
56
|
┬─[playerone@fedora:~/d/startgg-oauth2-full]─[21:10:09]─[G:main =]
|
|
56
|
-
╰─>$
|
|
57
|
-
[0] 0:fish* "~/d/startgg-oauth2-fu" 21:10
|
|
57
|
+
╰─>$
|
|
58
|
+
[0] 0:fish* "~/d/startgg-oauth2-fu" 21:10 26-wrz-26
|
|
58
59
|
```
|
|
59
60
|
|
|
60
61
|
## Installation
|
|
@@ -82,53 +83,245 @@ npm install @0xabadbabe-ops/startgg-oauth2-full
|
|
|
82
83
|
|
|
83
84
|
---
|
|
84
85
|
|
|
86
|
+
## Start.gg Specifics
|
|
87
|
+
|
|
88
|
+
The library itself is spec-pure PKCE (RFC 7636) and never touches a client secret. Start.gg's deployment deviates from that spec in ways you must plan for:
|
|
89
|
+
|
|
90
|
+
- **`client_secret` is required at the token endpoint — even with PKCE.** Every Start.gg OAuth app is a confidential client. Keep the secret server-side and add it to the token request where you run the exchange. Every example in this repo shows the pattern (a small `exchangeTokenWithSecret` helper or a same-origin dev relay).
|
|
91
|
+
- **The token endpoint sends no CORS headers.** A browser page can never exchange the code directly against `api.start.gg`. Exchange on your server (Node, Next.js route handler, bot backend) or through a same-origin relay — see `examples/browser` for the relay approach.
|
|
92
|
+
- **Redirect URIs must match the registered callback exactly** — scheme, host, port, and path.
|
|
93
|
+
- **Authorize host.** The constants use `https://api.start.gg/oauth/authorize`. If that host ever answers the authorize request with a JSON login 404, issue the identical request (same parameters) against `https://start.gg/oauth/authorize` — it renders the login/consent screen and completes the same flow.
|
|
94
|
+
|
|
95
|
+
See [STARTGG_OAUTH_SETUP.md](./STARTGG_OAUTH_SETUP.md) for app registration, per-example redirect URIs, and environment variables.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
85
99
|
## Quick Start
|
|
86
100
|
|
|
87
|
-
###
|
|
101
|
+
### 1. Build the authorize URL (browser or server)
|
|
88
102
|
|
|
89
103
|
```ts
|
|
90
|
-
import { buildAuthorizeUrl, StartGGScope } from 'startgg-oauth2-full';
|
|
104
|
+
import { buildAuthorizeUrl, StartGGScope, STARTGG_ENDPOINTS } from 'startgg-oauth2-full';
|
|
91
105
|
|
|
92
106
|
const cfg = {
|
|
93
107
|
clientId: '<client-id>',
|
|
94
|
-
authEndpoint:
|
|
108
|
+
authEndpoint: STARTGG_ENDPOINTS.authorize,
|
|
95
109
|
redirectUri: 'https://your.app/api/startgg/callback',
|
|
96
110
|
};
|
|
97
111
|
|
|
112
|
+
const state = crypto.randomUUID();
|
|
98
113
|
const { url, codeVerifier } = await buildAuthorizeUrl(cfg, {
|
|
99
114
|
scopes: [StartGGScope.USER_IDENTITY, StartGGScope.USER_EMAIL],
|
|
100
|
-
state
|
|
115
|
+
state,
|
|
101
116
|
});
|
|
102
117
|
|
|
118
|
+
// Keep both tied together for the callback (session, or a server-side state store)
|
|
103
119
|
sessionStorage.setItem('pkce:verifier', codeVerifier);
|
|
104
|
-
sessionStorage.setItem('oauth:state',
|
|
120
|
+
sessionStorage.setItem('oauth:state', state);
|
|
105
121
|
location.href = url;
|
|
106
122
|
```
|
|
107
123
|
|
|
108
|
-
###
|
|
124
|
+
### 2. Exchange the code server-side
|
|
125
|
+
|
|
126
|
+
Start.gg requires `client_secret` and sends no CORS headers, so the exchange happens on your server (the snippet every example in this repo uses):
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
// e.g. app/api/startgg/callback — Node / Next.js route handler
|
|
130
|
+
import { BearerToken } from 'startgg-oauth2-full';
|
|
131
|
+
|
|
132
|
+
export async function GET(req: Request) {
|
|
133
|
+
const { searchParams } = new URL(req.url);
|
|
134
|
+
const code = searchParams.get('code')!;
|
|
135
|
+
// verify `state`, then load the code_verifier you stored for it
|
|
136
|
+
|
|
137
|
+
const body = new URLSearchParams({
|
|
138
|
+
grant_type: 'authorization_code',
|
|
139
|
+
code,
|
|
140
|
+
redirect_uri: process.env.STARTGG_REDIRECT_URI!,
|
|
141
|
+
code_verifier: storedVerifier,
|
|
142
|
+
client_id: process.env.STARTGG_CLIENT_ID!,
|
|
143
|
+
client_secret: process.env.STARTGG_CLIENT_SECRET!, // required by Start.gg even with PKCE
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const res = await fetch('https://api.start.gg/oauth/access_token', {
|
|
147
|
+
method: 'POST',
|
|
148
|
+
headers: {
|
|
149
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
150
|
+
Accept: 'application/json',
|
|
151
|
+
},
|
|
152
|
+
body,
|
|
153
|
+
});
|
|
154
|
+
if (!res.ok) return new Response(`Token exchange failed: HTTP ${res.status}`, { status: 502 });
|
|
155
|
+
|
|
156
|
+
const bearer = BearerToken.fromOAuthResponse(await res.json());
|
|
157
|
+
// store bearer per user; never expose the raw token or your secret to the page
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Against a provider *without* Start.gg's secret requirement, `createStartGGAuth2Handler(cfg).exchangeToken(code, codeVerifier, scopes)` performs the same spec-pure POST for you — see [Advanced Usage](#advanced-usage).
|
|
162
|
+
|
|
163
|
+
### Using Constants for Start.gg Endpoints & Scopes
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import { STARTGG_ENDPOINTS, STARTGG_SCOPES, STARTGG_GQL_AUTH_HEADER, isValidStartGGScope } from 'startgg-oauth2-full';
|
|
167
|
+
|
|
168
|
+
// All official Start.gg endpoints
|
|
169
|
+
console.log(STARTGG_ENDPOINTS.authorize); // https://api.start.gg/oauth/authorize
|
|
170
|
+
console.log(STARTGG_ENDPOINTS.token); // https://api.start.gg/oauth/access_token
|
|
171
|
+
console.log(STARTGG_ENDPOINTS.gql); // https://api.start.gg/gql/alpha
|
|
172
|
+
|
|
173
|
+
// Valid scopes
|
|
174
|
+
console.log(STARTGG_SCOPES); // ['user.identity', 'user.email', 'tournament.manager', 'tournament.reporter']
|
|
175
|
+
console.log(STARTGG_GQL_AUTH_HEADER); // 'Bearer'
|
|
176
|
+
|
|
177
|
+
// Scope validation helper
|
|
178
|
+
const userInput = 'user.identity';
|
|
179
|
+
if (isValidStartGGScope(userInput)) {
|
|
180
|
+
// TypeScript narrows to StartGGScopeValue
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Advanced Usage
|
|
187
|
+
|
|
188
|
+
### Refresh Token Rotation
|
|
109
189
|
|
|
110
190
|
```ts
|
|
111
191
|
import { createStartGGAuth2Handler, BearerToken, StartGGScope } from 'startgg-oauth2-full';
|
|
112
192
|
|
|
113
|
-
const
|
|
114
|
-
const code = params.get('code')!;
|
|
115
|
-
const state = params.get('state')!;
|
|
116
|
-
if (state !== sessionStorage.getItem('oauth:state')) throw new Error('State mismatch');
|
|
193
|
+
const handler = createStartGGAuth2Handler({ clientId, redirectUri });
|
|
117
194
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
195
|
+
// Initial exchange
|
|
196
|
+
const res = await handler.exchangeToken(code, codeVerifier, [StartGGScope.USER_IDENTITY]);
|
|
197
|
+
let bearer = BearerToken.fromOAuthResponse(res);
|
|
198
|
+
|
|
199
|
+
// Later: refresh when expired (skew-aware)
|
|
200
|
+
if (bearer.willExpireWithin(60)) { // expires within 60s
|
|
201
|
+
const newRes = await handler.refreshToken(bearer.refreshToken!, [StartGGScope.USER_IDENTITY]);
|
|
202
|
+
bearer = BearerToken.fromOAuthResponse(newRes); // preserves refresh_token if server omits
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Custom PKCE Pair (Pre-generated Verifier/Challenge)
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
import { buildAuthorizeUrl, computeCodeChallengeS256, generateCodeVerifier } from 'startgg-oauth2-full';
|
|
210
|
+
|
|
211
|
+
// Generate once, store securely
|
|
212
|
+
const codeVerifier = generateCodeVerifier(64);
|
|
213
|
+
const codeChallenge = await computeCodeChallengeS256(codeVerifier);
|
|
214
|
+
|
|
215
|
+
// Later: build URL with pre-computed pair
|
|
216
|
+
const { url } = await buildAuthorizeUrl(cfg, {
|
|
217
|
+
scopes: [StartGGScope.USER_IDENTITY],
|
|
218
|
+
state: crypto.randomUUID(),
|
|
219
|
+
codeVerifier,
|
|
220
|
+
codeChallenge, // validated against verifier
|
|
123
221
|
});
|
|
222
|
+
```
|
|
124
223
|
|
|
125
|
-
|
|
126
|
-
StartGGScope.USER_IDENTITY,
|
|
127
|
-
StartGGScope.USER_EMAIL,
|
|
128
|
-
]);
|
|
224
|
+
### Server-Side (Node/Next.js) with Secure State Store
|
|
129
225
|
|
|
130
|
-
|
|
131
|
-
|
|
226
|
+
```ts
|
|
227
|
+
// lib/startgg.ts (Next.js example)
|
|
228
|
+
import { createStartGGAuth2Handler } from 'startgg-oauth2-full';
|
|
229
|
+
|
|
230
|
+
export function getStartggHandler() {
|
|
231
|
+
return createStartGGAuth2Handler({
|
|
232
|
+
clientId: process.env.STARTGG_CLIENT_ID!,
|
|
233
|
+
redirectUri: process.env.STARTGG_REDIRECT_URI!,
|
|
234
|
+
// authEndpoint and tokenEndpoint default to STARTGG_ENDPOINTS
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// app/api/startgg/callback/route.ts
|
|
239
|
+
import { getStartggHandler } from '@/lib/startgg';
|
|
240
|
+
import { consumePending } from '@/lib/pendingStore'; // your secure store
|
|
241
|
+
|
|
242
|
+
export async function GET(req: Request) {
|
|
243
|
+
const { searchParams } = new URL(req.url);
|
|
244
|
+
const code = searchParams.get('code')!;
|
|
245
|
+
const state = searchParams.get('state')!;
|
|
246
|
+
|
|
247
|
+
const pending = consumePending(state); // delete after use
|
|
248
|
+
if (!pending) return new Response('Invalid state', { status: 400 });
|
|
249
|
+
|
|
250
|
+
const res = await getStartggHandler().exchangeToken(code, pending.codeVerifier, pending.scopes);
|
|
251
|
+
return Response.json({ ok: true, scope: res.scope });
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### GraphQL Calls with Bearer Token
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
import { BearerToken, STARTGG_ENDPOINTS, STARTGG_GQL_AUTH_HEADER } from 'startgg-oauth2-full';
|
|
259
|
+
|
|
260
|
+
const bearer = BearerToken.fromOAuthResponse(tokenResponse);
|
|
261
|
+
|
|
262
|
+
const query = `
|
|
263
|
+
query GetUser { user { id, name, email } }
|
|
264
|
+
`;
|
|
265
|
+
|
|
266
|
+
const response = await fetch(STARTGG_ENDPOINTS.gql, {
|
|
267
|
+
method: 'POST',
|
|
268
|
+
headers: {
|
|
269
|
+
'Content-Type': 'application/json',
|
|
270
|
+
[STARTGG_GQL_AUTH_HEADER]: bearer.toAuthHeader().Authorization,
|
|
271
|
+
},
|
|
272
|
+
body: JSON.stringify({ query }),
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const { data } = await response.json();
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Error Handling
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
import { createStartGGAuth2Handler, OAuth2Error, ScopeValidationError, StartGGScope } from 'startgg-oauth2-full';
|
|
282
|
+
|
|
283
|
+
const handler = createStartGGAuth2Handler(cfg);
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
const res = await handler.exchangeToken(code, verifier, [StartGGScope.USER_IDENTITY]);
|
|
287
|
+
} catch (err) {
|
|
288
|
+
if (err instanceof ScopeValidationError) {
|
|
289
|
+
console.error('Requested scopes:', err.requestedScopes); // ['user.identity', 'user.email']
|
|
290
|
+
console.error('Granted scopes:', err.grantedScopes); // ['user.identity'] (present when server returned scope)
|
|
291
|
+
} else if (err instanceof OAuth2Error) {
|
|
292
|
+
console.error('OAuth error:', err.code); // TOKEN_EXCHANGE_FAILED, INVALID_PKCE_PAIR, etc.
|
|
293
|
+
console.error('Details:', err.details); // parsed JSON or { raw: '...' }
|
|
294
|
+
} else {
|
|
295
|
+
throw err;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Cloudflare Workers / Edge Runtime
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
// Works in Cloudflare Workers, Vercel Edge, Deno, Bun
|
|
304
|
+
import { createStartGGAuth2Handler, StartGGScope } from 'startgg-oauth2-full';
|
|
305
|
+
|
|
306
|
+
export default {
|
|
307
|
+
async fetch(request: Request, env: Env): Promise<Response> {
|
|
308
|
+
const handler = createStartGGAuth2Handler({
|
|
309
|
+
clientId: env.STARTGG_CLIENT_ID,
|
|
310
|
+
redirectUri: new URL('/callback', request.url).href,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const url = new URL(request.url);
|
|
314
|
+
if (url.pathname === '/callback') {
|
|
315
|
+
const code = url.searchParams.get('code')!;
|
|
316
|
+
const state = url.searchParams.get('state')!;
|
|
317
|
+
// validate state from your KV/D1 store...
|
|
318
|
+
const res = await handler.exchangeToken(code, storedVerifier, [StartGGScope.USER_IDENTITY]);
|
|
319
|
+
return Response.redirect('/dashboard');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ... rest of handler
|
|
323
|
+
},
|
|
324
|
+
};
|
|
132
325
|
```
|
|
133
326
|
|
|
134
327
|
---
|
|
@@ -136,11 +329,12 @@ fetch('https://api.start.gg/your-endpoint', { headers: bearer.toAuthHeader() });
|
|
|
136
329
|
## Scripts
|
|
137
330
|
|
|
138
331
|
```bash
|
|
139
|
-
npm run build # tsc
|
|
140
|
-
npm test # Jest
|
|
332
|
+
npm run build # compile TypeScript (tsc)
|
|
333
|
+
npm test # full Jest suite (ts-jest)
|
|
334
|
+
npm test -- pkce # target a single spec
|
|
141
335
|
```
|
|
142
336
|
|
|
143
|
-
Examples ship as their own workspaces—hop into each folder, install once, then use the local scripts:
|
|
337
|
+
Examples ship as their own workspaces — hop into each folder, install once, then use the local scripts. Every example performs a real user login against Start.gg:
|
|
144
338
|
|
|
145
339
|
- Browser (Vite): `cd examples/browser && npm install && npm run dev`
|
|
146
340
|
- Node CLI/server: `cd examples/node && npm install && npm run dev`
|
|
@@ -161,6 +355,8 @@ Examples ship as their own workspaces—hop into each folder, install once, then
|
|
|
161
355
|
- `BearerToken`
|
|
162
356
|
- `fromOAuthResponse(res, nowMs?, skewSeconds?)`
|
|
163
357
|
- `isExpired()`, `willExpireWithin()`, `toAuthHeader()`, `assertUsable()`
|
|
358
|
+
- `STARTGG_ENDPOINTS`, `STARTGG_SCOPES`, `STARTGG_GQL_AUTH_HEADER`
|
|
359
|
+
- `isValidStartGGScope(value): boolean`
|
|
164
360
|
|
|
165
361
|
### Scopes
|
|
166
362
|
|
|
@@ -177,7 +373,7 @@ enum StartGGScope {
|
|
|
177
373
|
|
|
178
374
|
## Scope Semantics
|
|
179
375
|
|
|
180
|
-
- If response **includes** `scope`, it
|
|
376
|
+
- If response **includes** `scope`, it's validated; missing required → `ScopeValidationError`.
|
|
181
377
|
- If response **omits** `scope`, treat as unchanged (RFC 6749).
|
|
182
378
|
- Refresh: preserve prior `refresh_token` if omitted by server.
|
|
183
379
|
|
|
@@ -190,17 +386,73 @@ class OAuth2Error extends Error {
|
|
|
190
386
|
code?: string; // e.g., TOKEN_EXCHANGE_FAILED
|
|
191
387
|
details?: unknown; // parsed JSON or { raw: string }
|
|
192
388
|
}
|
|
389
|
+
|
|
390
|
+
class ScopeValidationError extends Error {
|
|
391
|
+
requestedScopes: string[]; // what you asked for
|
|
392
|
+
grantedScopes?: string[]; // what the server granted (when it returned scope)
|
|
393
|
+
}
|
|
193
394
|
```
|
|
194
395
|
|
|
195
396
|
---
|
|
196
397
|
|
|
197
398
|
## Examples
|
|
198
399
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
400
|
+
| Example | Stack | Token exchange |
|
|
401
|
+
| --- | --- | --- |
|
|
402
|
+
| [`examples/node/`](./examples/node) | Node CLI + local redirect server | Server-side POST with `client_secret` |
|
|
403
|
+
| [`examples/nextjs/`](./examples/nextjs) | Next.js App Router | Route handler POST with `client_secret` |
|
|
404
|
+
| [`examples/discordjs/`](./examples/discordjs) | discord.js v14 bot | Bot backend POST with `client_secret` |
|
|
405
|
+
| [`examples/browser/`](./examples/browser) | Vite SPA | Same-origin dev relay (`/startgg/token`) injects the secret |
|
|
406
|
+
| [`examples/vite/`](./examples/vite) | Minimal Vite scaffold | Same-origin dev relay (`/startgg/token`) injects the secret |
|
|
407
|
+
|
|
408
|
+
Each example documents its redirect URI and environment variables in its own README.
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
## Vercel Connect Integration
|
|
413
|
+
|
|
414
|
+
Use Start.gg with [Vercel Connect](https://vercel.com/docs/connect) for secure, short-lived tokens without storing credentials in your environment.
|
|
415
|
+
|
|
416
|
+
### Quick Setup
|
|
417
|
+
|
|
418
|
+
```bash
|
|
419
|
+
# 1. Create Start.gg OAuth app (redirect: https://connect.vercel.com/callback)
|
|
420
|
+
# 2. Create Custom OAuth connector in Vercel Connect
|
|
421
|
+
vercel connect create https://api.start.gg/oauth/authorize --name startgg
|
|
422
|
+
# 3. Attach to your project
|
|
423
|
+
vercel connect attach oauth/startgg
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### Use in Your Code
|
|
427
|
+
|
|
428
|
+
```bash
|
|
429
|
+
npm install @vercel/connect startgg-vercel-connect
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
```ts
|
|
433
|
+
import { getConnectorUid, getLoginScopes, createTokenParams } from 'startgg-vercel-connect';
|
|
434
|
+
import { getTokenResponse, UserAuthorizationRequiredError } from '@vercel/connect';
|
|
435
|
+
|
|
436
|
+
const token = await getTokenResponse(
|
|
437
|
+
getConnectorUid(),
|
|
438
|
+
createTokenParams({
|
|
439
|
+
subject: { type: 'user', id: 'user_123' },
|
|
440
|
+
scopes: getLoginScopes(true),
|
|
441
|
+
})
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
// Use with Start.gg GraphQL API
|
|
445
|
+
const response = await fetch('https://api.start.gg/gql/alpha', {
|
|
446
|
+
method: 'POST',
|
|
447
|
+
headers: {
|
|
448
|
+
'Content-Type': 'application/json',
|
|
449
|
+
'Authorization': `Bearer ${token.token}`,
|
|
450
|
+
},
|
|
451
|
+
body: JSON.stringify({ query: '{ viewer { id name } }' }),
|
|
452
|
+
});
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
See [VERCEL_CONNECT_STARTGG.md](./VERCEL_CONNECT_STARTGG.md) for the complete guide and [packages/startgg-vercel-connect](./packages/startgg-vercel-connect) for the helper package.
|
|
204
456
|
|
|
205
457
|
---
|
|
206
458
|
|
|
@@ -216,27 +468,33 @@ GitHub Actions runs TypeScript build + Jest on push/PR (Node 18 & 20). See `.git
|
|
|
216
468
|
startgg-oauth2-full/
|
|
217
469
|
├── README.md
|
|
218
470
|
├── AGENTS.md
|
|
471
|
+
├── CONTRIBUTING.md
|
|
219
472
|
├── LICENSE
|
|
473
|
+
├── STARTGG_OAUTH_SETUP.md # Start.gg app registration & env vars
|
|
474
|
+
├── VERCEL_CONNECT_STARTGG.md # Vercel Connect guide
|
|
220
475
|
├── package.json
|
|
221
476
|
├── tsconfig.json
|
|
222
477
|
├── jest.config.ts
|
|
223
478
|
├── jest.setup.ts
|
|
224
|
-
├── .gitignore
|
|
225
|
-
├── .npmrc
|
|
226
479
|
├── src/
|
|
227
|
-
│
|
|
228
|
-
│
|
|
480
|
+
│ ├── auth/
|
|
481
|
+
│ │ └── StartGGOAuth2.ts # PKCE + handler + BearerToken
|
|
482
|
+
│ ├── constants.ts # STARTGG_ENDPOINTS, scopes
|
|
483
|
+
│ └── index.ts
|
|
229
484
|
├── __tests__/
|
|
230
485
|
│ ├── authorize-url.test.ts
|
|
231
486
|
│ ├── bearer-token.test.ts
|
|
487
|
+
│ ├── constants.test.ts
|
|
232
488
|
│ ├── handler.test.ts
|
|
233
489
|
│ └── pkce.test.ts
|
|
234
490
|
├── examples/
|
|
235
|
-
│ ├── browser/
|
|
236
|
-
│ ├── node/
|
|
237
|
-
│ ├── discordjs/
|
|
238
|
-
│ ├── nextjs/
|
|
239
|
-
│ └── vite/
|
|
491
|
+
│ ├── browser/ # Vite SPA — dev relay exchange
|
|
492
|
+
│ ├── node/ # CLI + local redirect server
|
|
493
|
+
│ ├── discordjs/ # Discord bot OAuth flow
|
|
494
|
+
│ ├── nextjs/ # Next.js App Router example
|
|
495
|
+
│ └── vite/ # Minimal Vite SPA scaffold
|
|
496
|
+
├── packages/
|
|
497
|
+
│ └── startgg-vercel-connect/
|
|
240
498
|
└── .github/
|
|
241
499
|
├── ISSUE_TEMPLATE/
|
|
242
500
|
│ ├── bug_report.md
|
|
@@ -252,6 +510,13 @@ startgg-oauth2-full/
|
|
|
252
510
|
- Use and verify `state`.
|
|
253
511
|
- Keep `code_verifier` private.
|
|
254
512
|
- Never log tokens; always HTTPS.
|
|
513
|
+
- Never ship `client_secret` to the browser — Start.gg requires it, so the exchange belongs on a server you control.
|
|
514
|
+
|
|
515
|
+
---
|
|
516
|
+
|
|
517
|
+
## Contributing
|
|
518
|
+
|
|
519
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md). PRs should include Jest coverage for new behaviour and list validation steps (`npm test`, demo transcripts for interactive flows).
|
|
255
520
|
|
|
256
521
|
---
|
|
257
522
|
|
|
@@ -50,7 +50,7 @@ export declare function buildAuthorizeUrl(cfg: {
|
|
|
50
50
|
}, opts: AuthorizeUrlOptions): Promise<BuiltAuthorizeUrl>;
|
|
51
51
|
export declare class BearerToken {
|
|
52
52
|
readonly accessToken: string;
|
|
53
|
-
readonly tokenType:
|
|
53
|
+
readonly tokenType: "Bearer";
|
|
54
54
|
readonly refreshToken?: string;
|
|
55
55
|
readonly expiresAt?: number;
|
|
56
56
|
private constructor();
|
|
@@ -71,14 +71,15 @@ export declare class StartGGOAuth2Handler implements IOAuth2HandlerWithPKCE {
|
|
|
71
71
|
});
|
|
72
72
|
/** Exchange authorization code for tokens (PKCE). */
|
|
73
73
|
exchangeToken(code: string, codeVerifier: string, expectedScopes: StartGGScope[]): Promise<OAuth2TokenResponse>;
|
|
74
|
-
/** Refresh access token; preserve prior refresh token if server omits rotation.
|
|
74
|
+
/** Refresh access token; preserve prior refresh token if server omits rotation.
|
|
75
|
+
* Per RFC 6749, scope should only be included when requesting a subset of original scopes. */
|
|
75
76
|
refreshToken(refreshToken: string, originalScopes: StartGGScope[]): Promise<OAuth2TokenResponse>;
|
|
76
77
|
}
|
|
77
78
|
/** Factory */
|
|
78
79
|
export declare function createStartGGAuth2Handler(params: {
|
|
79
80
|
clientId: string;
|
|
80
81
|
redirectUri: string;
|
|
81
|
-
authEndpoint
|
|
82
|
-
tokenEndpoint
|
|
82
|
+
authEndpoint?: string;
|
|
83
|
+
tokenEndpoint?: string;
|
|
83
84
|
fetchTimeoutMs?: number;
|
|
84
85
|
}): StartGGOAuth2Handler;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { STARTGG_ENDPOINTS } from "../constants";
|
|
2
|
+
// Full RFC-compliant implementation
|
|
2
3
|
// of OAuth2 Authorization Code Flow with PKCE (RFC 6749, RFC 7636)
|
|
3
4
|
// for Start.gg API (https://start.gg/docs/oauth2).
|
|
4
5
|
// Happy to accept PRs for improvements or fixes!
|
|
@@ -16,7 +17,7 @@ export class OAuth2Error extends Error {
|
|
|
16
17
|
details;
|
|
17
18
|
constructor(message, code, details) {
|
|
18
19
|
super(message);
|
|
19
|
-
this.name =
|
|
20
|
+
this.name = "OAuth2Error";
|
|
20
21
|
this.code = code;
|
|
21
22
|
this.details = details;
|
|
22
23
|
}
|
|
@@ -25,7 +26,7 @@ export class ScopeValidationError extends OAuth2Error {
|
|
|
25
26
|
requestedScopes;
|
|
26
27
|
grantedScopes;
|
|
27
28
|
constructor(message, requestedScopes, grantedScopes) {
|
|
28
|
-
super(message,
|
|
29
|
+
super(message, "SCOPE_VALIDATION_FAILED");
|
|
29
30
|
this.requestedScopes = requestedScopes;
|
|
30
31
|
this.grantedScopes = grantedScopes;
|
|
31
32
|
}
|
|
@@ -35,31 +36,34 @@ async function getSubtleCrypto() {
|
|
|
35
36
|
const g = globalThis;
|
|
36
37
|
if (g.crypto?.subtle)
|
|
37
38
|
return g.crypto.subtle;
|
|
38
|
-
throw new OAuth2Error(
|
|
39
|
+
throw new OAuth2Error("WebCrypto subtle not available; required for PKCE S256", "CRYPTO_UNAVAILABLE");
|
|
39
40
|
}
|
|
40
41
|
/** Base64 (Buffer if available, else btoa path). */
|
|
41
42
|
function base64Encode(bytes) {
|
|
42
43
|
const g = globalThis;
|
|
43
|
-
if (typeof g.Buffer?.from ===
|
|
44
|
-
return g.Buffer.from(bytes).toString(
|
|
45
|
-
let binary =
|
|
44
|
+
if (typeof g.Buffer?.from === "function")
|
|
45
|
+
return g.Buffer.from(bytes).toString("base64");
|
|
46
|
+
let binary = "";
|
|
46
47
|
const chunk = 0x8000;
|
|
47
48
|
for (let i = 0; i < bytes.length; i += chunk)
|
|
48
49
|
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
49
|
-
if (typeof g.btoa !==
|
|
50
|
-
throw new OAuth2Error(
|
|
50
|
+
if (typeof g.btoa !== "function")
|
|
51
|
+
throw new OAuth2Error("btoa not available for base64 encoding", "B64_UNAVAILABLE");
|
|
51
52
|
return g.btoa(binary);
|
|
52
53
|
}
|
|
53
54
|
/** URL-safe Base64 (no padding). */
|
|
54
55
|
function base64Url(bytes) {
|
|
55
56
|
const u8 = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
56
|
-
return base64Encode(u8)
|
|
57
|
+
return base64Encode(u8)
|
|
58
|
+
.replace(/\+/g, "-")
|
|
59
|
+
.replace(/\//g, "_")
|
|
60
|
+
.replace(/=+$/g, "");
|
|
57
61
|
}
|
|
58
62
|
/** Random bytes using WebCrypto. */
|
|
59
63
|
function getRandomBytes(length) {
|
|
60
64
|
const g = globalThis;
|
|
61
65
|
if (!g.crypto?.getRandomValues)
|
|
62
|
-
throw new OAuth2Error(
|
|
66
|
+
throw new OAuth2Error("crypto.getRandomValues unavailable", "RNG_UNAVAILABLE");
|
|
63
67
|
const bytes = new Uint8Array(length);
|
|
64
68
|
g.crypto.getRandomValues(bytes);
|
|
65
69
|
return bytes;
|
|
@@ -83,12 +87,12 @@ async function parseJsonSafe(res) {
|
|
|
83
87
|
}
|
|
84
88
|
/** Validate token response + Bearer type. */
|
|
85
89
|
function validateTokenResponse(tr) {
|
|
86
|
-
if (!tr || typeof tr !==
|
|
87
|
-
throw new OAuth2Error(
|
|
90
|
+
if (!tr || typeof tr !== "object")
|
|
91
|
+
throw new OAuth2Error("Invalid token response shape", "INVALID_TOKEN_RESPONSE", tr);
|
|
88
92
|
if (!tr.access_token)
|
|
89
|
-
throw new OAuth2Error(
|
|
90
|
-
if (!tr.token_type || tr.token_type.toLowerCase() !==
|
|
91
|
-
throw new OAuth2Error(
|
|
93
|
+
throw new OAuth2Error("Missing access_token", "INVALID_TOKEN_RESPONSE", tr);
|
|
94
|
+
if (!tr.token_type || tr.token_type.toLowerCase() !== "bearer") {
|
|
95
|
+
throw new OAuth2Error("Unsupported token_type", "UNSUPPORTED_TOKEN_TYPE", tr);
|
|
92
96
|
}
|
|
93
97
|
}
|
|
94
98
|
/** If scope omitted, assume unchanged (RFC 6749). */
|
|
@@ -98,9 +102,9 @@ function validateScopesOrAssumePrevious(responseScope, requiredScopes) {
|
|
|
98
102
|
if (responseScope == null)
|
|
99
103
|
return;
|
|
100
104
|
const granted = new Set(responseScope.split(/\s+/).filter(Boolean));
|
|
101
|
-
const missing = requiredScopes.filter(s => !granted.has(s));
|
|
105
|
+
const missing = requiredScopes.filter((s) => !granted.has(s));
|
|
102
106
|
if (missing.length > 0) {
|
|
103
|
-
throw new ScopeValidationError(`Missing required scopes: ${missing.join(
|
|
107
|
+
throw new ScopeValidationError(`Missing required scopes: ${missing.join(", ")}`, requiredScopes, Array.from(granted));
|
|
104
108
|
}
|
|
105
109
|
}
|
|
106
110
|
/** Fetch with timeout to avoid hangs. */
|
|
@@ -116,7 +120,7 @@ async function fetchWithTimeout(input, init = {}) {
|
|
|
116
120
|
}
|
|
117
121
|
}
|
|
118
122
|
// -------- PKCE (RFC 7636) --------
|
|
119
|
-
const PKCE_VERIFIER_CHARS =
|
|
123
|
+
const PKCE_VERIFIER_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
120
124
|
export function generateCodeVerifier(length = 64) {
|
|
121
125
|
const len = Math.min(Math.max(length, 43), 128);
|
|
122
126
|
const alphabet = PKCE_VERIFIER_CHARS;
|
|
@@ -133,13 +137,13 @@ export function generateCodeVerifier(length = 64) {
|
|
|
133
137
|
break;
|
|
134
138
|
}
|
|
135
139
|
}
|
|
136
|
-
return result.join(
|
|
140
|
+
return result.join("");
|
|
137
141
|
}
|
|
138
142
|
/** S256 challenge for a verifier. */
|
|
139
143
|
export async function computeCodeChallengeS256(codeVerifier) {
|
|
140
144
|
const enc = new TextEncoder();
|
|
141
145
|
const subtle = await getSubtleCrypto();
|
|
142
|
-
const hash = await subtle.digest(
|
|
146
|
+
const hash = await subtle.digest("SHA-256", enc.encode(codeVerifier));
|
|
143
147
|
return base64Url(hash);
|
|
144
148
|
}
|
|
145
149
|
/** Build an authorization URL with PKCE (S256). */
|
|
@@ -149,7 +153,7 @@ export async function buildAuthorizeUrl(cfg, opts) {
|
|
|
149
153
|
if (opts.codeVerifier && opts.codeChallenge) {
|
|
150
154
|
const expectedChallenge = await computeCodeChallengeS256(opts.codeVerifier);
|
|
151
155
|
if (expectedChallenge !== opts.codeChallenge) {
|
|
152
|
-
throw new OAuth2Error(
|
|
156
|
+
throw new OAuth2Error("Provided codeChallenge does not match codeVerifier", "INVALID_PKCE_PAIR");
|
|
153
157
|
}
|
|
154
158
|
codeVerifier = opts.codeVerifier;
|
|
155
159
|
codeChallenge = opts.codeChallenge;
|
|
@@ -159,7 +163,7 @@ export async function buildAuthorizeUrl(cfg, opts) {
|
|
|
159
163
|
codeChallenge = await computeCodeChallengeS256(codeVerifier);
|
|
160
164
|
}
|
|
161
165
|
else if (opts.codeChallenge) {
|
|
162
|
-
throw new OAuth2Error(
|
|
166
|
+
throw new OAuth2Error("codeVerifier is required when providing codeChallenge", "INVALID_PKCE_PAIR");
|
|
163
167
|
}
|
|
164
168
|
else {
|
|
165
169
|
codeVerifier = generateCodeVerifier();
|
|
@@ -167,12 +171,12 @@ export async function buildAuthorizeUrl(cfg, opts) {
|
|
|
167
171
|
}
|
|
168
172
|
const u = new URL(cfg.authEndpoint);
|
|
169
173
|
const params = {
|
|
170
|
-
response_type:
|
|
174
|
+
response_type: "code",
|
|
171
175
|
client_id: cfg.clientId,
|
|
172
176
|
redirect_uri: cfg.redirectUri,
|
|
173
|
-
scope: opts.scopes.map(String).join(
|
|
177
|
+
scope: opts.scopes.map(String).join(" "),
|
|
174
178
|
code_challenge: codeChallenge,
|
|
175
|
-
code_challenge_method:
|
|
179
|
+
code_challenge_method: "S256",
|
|
176
180
|
};
|
|
177
181
|
if (opts.state)
|
|
178
182
|
params.state = opts.state;
|
|
@@ -201,12 +205,13 @@ export class BearerToken {
|
|
|
201
205
|
}
|
|
202
206
|
static fromOAuthResponse(res, nowMs = Date.now(), skewSeconds = 60) {
|
|
203
207
|
validateTokenResponse(res);
|
|
204
|
-
const expiresAt = typeof res.expires_in ===
|
|
205
|
-
? nowMs +
|
|
208
|
+
const expiresAt = typeof res.expires_in === "number"
|
|
209
|
+
? nowMs +
|
|
210
|
+
Math.max(0, (res.expires_in - Math.max(0, skewSeconds)) * 1000)
|
|
206
211
|
: undefined;
|
|
207
212
|
return new BearerToken({
|
|
208
213
|
accessToken: res.access_token,
|
|
209
|
-
tokenType:
|
|
214
|
+
tokenType: "Bearer",
|
|
210
215
|
refreshToken: res.refresh_token,
|
|
211
216
|
expiresAt,
|
|
212
217
|
});
|
|
@@ -226,7 +231,7 @@ export class BearerToken {
|
|
|
226
231
|
}
|
|
227
232
|
assertUsable(nowMs = Date.now()) {
|
|
228
233
|
if (this.isExpired(nowMs))
|
|
229
|
-
throw new OAuth2Error(
|
|
234
|
+
throw new OAuth2Error("Access token expired", "TOKEN_EXPIRED");
|
|
230
235
|
}
|
|
231
236
|
}
|
|
232
237
|
// -------- Handler (RFC 6749 §4.1.3, §6) --------
|
|
@@ -238,44 +243,50 @@ export class StartGGOAuth2Handler {
|
|
|
238
243
|
/** Exchange authorization code for tokens (PKCE). */
|
|
239
244
|
async exchangeToken(code, codeVerifier, expectedScopes) {
|
|
240
245
|
const tokenRequest = {
|
|
241
|
-
grant_type:
|
|
246
|
+
grant_type: "authorization_code",
|
|
242
247
|
code,
|
|
243
248
|
redirect_uri: this.config.redirectUri,
|
|
244
249
|
code_verifier: codeVerifier,
|
|
245
250
|
client_id: this.config.clientId,
|
|
246
251
|
};
|
|
247
252
|
const res = await fetchWithTimeout(this.config.tokenEndpoint, {
|
|
248
|
-
method:
|
|
249
|
-
headers: {
|
|
253
|
+
method: "POST",
|
|
254
|
+
headers: {
|
|
255
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
256
|
+
Accept: "application/json",
|
|
257
|
+
},
|
|
250
258
|
body: formBody(tokenRequest),
|
|
251
259
|
timeoutMs: this.config.fetchTimeoutMs ?? 15000,
|
|
252
260
|
});
|
|
253
261
|
if (!res.ok) {
|
|
254
262
|
const details = await parseJsonSafe(res);
|
|
255
|
-
throw new OAuth2Error(
|
|
263
|
+
throw new OAuth2Error("Token exchange failed", "TOKEN_EXCHANGE_FAILED", details);
|
|
256
264
|
}
|
|
257
265
|
const tokenResponse = (await res.json());
|
|
258
266
|
validateTokenResponse(tokenResponse);
|
|
259
267
|
validateScopesOrAssumePrevious(tokenResponse.scope, expectedScopes.map(String));
|
|
260
268
|
return tokenResponse;
|
|
261
269
|
}
|
|
262
|
-
/** Refresh access token; preserve prior refresh token if server omits rotation.
|
|
270
|
+
/** Refresh access token; preserve prior refresh token if server omits rotation.
|
|
271
|
+
* Per RFC 6749, scope should only be included when requesting a subset of original scopes. */
|
|
263
272
|
async refreshToken(refreshToken, originalScopes) {
|
|
264
273
|
const refreshRequest = {
|
|
265
|
-
grant_type:
|
|
274
|
+
grant_type: "refresh_token",
|
|
266
275
|
refresh_token: refreshToken,
|
|
267
276
|
client_id: this.config.clientId,
|
|
268
|
-
scope: originalScopes.length ? originalScopes.join(' ') : undefined,
|
|
269
277
|
};
|
|
270
278
|
const res = await fetchWithTimeout(this.config.tokenEndpoint, {
|
|
271
|
-
method:
|
|
272
|
-
headers: {
|
|
279
|
+
method: "POST",
|
|
280
|
+
headers: {
|
|
281
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
282
|
+
Accept: "application/json",
|
|
283
|
+
},
|
|
273
284
|
body: formBody(Object.fromEntries(Object.entries(refreshRequest).filter(([, v]) => v != null))),
|
|
274
285
|
timeoutMs: this.config.fetchTimeoutMs ?? 15000,
|
|
275
286
|
});
|
|
276
287
|
if (!res.ok) {
|
|
277
288
|
const details = await parseJsonSafe(res);
|
|
278
|
-
throw new OAuth2Error(
|
|
289
|
+
throw new OAuth2Error("Token refresh failed", "TOKEN_REFRESH_FAILED", details);
|
|
279
290
|
}
|
|
280
291
|
const tokenResponse = (await res.json());
|
|
281
292
|
validateTokenResponse(tokenResponse);
|
|
@@ -287,5 +298,9 @@ export class StartGGOAuth2Handler {
|
|
|
287
298
|
}
|
|
288
299
|
/** Factory */
|
|
289
300
|
export function createStartGGAuth2Handler(params) {
|
|
290
|
-
return new StartGGOAuth2Handler(
|
|
301
|
+
return new StartGGOAuth2Handler({
|
|
302
|
+
...params,
|
|
303
|
+
authEndpoint: params.authEndpoint ?? STARTGG_ENDPOINTS.authorize,
|
|
304
|
+
tokenEndpoint: params.tokenEndpoint ?? STARTGG_ENDPOINTS.token,
|
|
305
|
+
});
|
|
291
306
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const STARTGG_ENDPOINTS: {
|
|
2
|
+
readonly authorize: "https://api.start.gg/oauth/authorize";
|
|
3
|
+
readonly token: "https://api.start.gg/oauth/access_token";
|
|
4
|
+
readonly refresh: "https://api.start.gg/oauth/refresh";
|
|
5
|
+
readonly gql: "https://api.start.gg/gql/alpha";
|
|
6
|
+
};
|
|
7
|
+
export declare const STARTGG_SCOPES: readonly ["user.identity", "user.email", "tournament.manager", "tournament.reporter"];
|
|
8
|
+
export declare const STARTGG_GQL_AUTH_HEADER: "Bearer";
|
|
9
|
+
export type StartGGScopeValue = (typeof STARTGG_SCOPES)[number];
|
|
10
|
+
export declare function isValidStartGGScope(scope: string): scope is StartGGScopeValue;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const STARTGG_ENDPOINTS = {
|
|
2
|
+
authorize: "https://api.start.gg/oauth/authorize",
|
|
3
|
+
token: "https://api.start.gg/oauth/access_token",
|
|
4
|
+
refresh: "https://api.start.gg/oauth/refresh",
|
|
5
|
+
gql: "https://api.start.gg/gql/alpha",
|
|
6
|
+
};
|
|
7
|
+
export const STARTGG_SCOPES = [
|
|
8
|
+
"user.identity",
|
|
9
|
+
"user.email",
|
|
10
|
+
"tournament.manager",
|
|
11
|
+
"tournament.reporter",
|
|
12
|
+
];
|
|
13
|
+
export const STARTGG_GQL_AUTH_HEADER = "Bearer";
|
|
14
|
+
export function isValidStartGGScope(scope) {
|
|
15
|
+
return STARTGG_SCOPES.includes(scope);
|
|
16
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export * from
|
|
1
|
+
export * from "./auth/StartGGOAuth2.js";
|
|
2
|
+
export * from "./constants.js";
|
package/dist/index.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export * from
|
|
1
|
+
export * from "./auth/StartGGOAuth2.js";
|
|
2
|
+
export * from "./constants.js";
|
package/package.json
CHANGED
|
@@ -1,37 +1,65 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
2
|
+
"name": "startgg-oauth2-full",
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./auth": {
|
|
13
|
+
"types": "./dist/auth/StartGGOAuth2.d.ts",
|
|
14
|
+
"import": "./dist/auth/StartGGOAuth2.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/0xABADBABE-ops/startgg-oauth2-full.git"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"dev:browser": "serve examples/browser -p 5174",
|
|
29
|
+
"dev:node": "tsx examples/node/src/index.ts",
|
|
30
|
+
"dev:node:server": "tsx examples/node/src/server.ts",
|
|
31
|
+
"dev:vite": "cd examples/vite && npm run dev",
|
|
32
|
+
"dev:nextjs": "cd examples/nextjs && npm run dev",
|
|
33
|
+
"dev:discord": "cd examples/discordjs && npm run dev",
|
|
34
|
+
"build:vercel-connect": "cd packages/startgg-vercel-connect && npm run build",
|
|
35
|
+
"test": "jest --runInBand",
|
|
36
|
+
"test:watch": "jest --watch",
|
|
37
|
+
"test:coverage": "jest --coverage",
|
|
38
|
+
"lint": "tsc --noEmit",
|
|
39
|
+
"build": "tsc -p tsconfig.json",
|
|
40
|
+
"prepare": "npm run build"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/jest": "^29.5.12",
|
|
44
|
+
"@types/node": "^22.7.5",
|
|
45
|
+
"jest": "^29.7.0",
|
|
46
|
+
"lantern": "0.1.2",
|
|
47
|
+
"node-fetch": "^3.3.2",
|
|
48
|
+
"serve": "^14.2.6",
|
|
49
|
+
"ts-jest": "^29.2.5",
|
|
50
|
+
"ts-node": "10.9.2",
|
|
51
|
+
"tsx": "^4.19.0",
|
|
52
|
+
"typescript": "^5.6.3"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=18.0.0"
|
|
56
|
+
},
|
|
57
|
+
"sideEffects": false,
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"access": "public",
|
|
60
|
+
"registry": "https://registry.npmjs.org/"
|
|
61
|
+
},
|
|
62
|
+
"allowScripts": {
|
|
63
|
+
"esbuild@0.25.11": true
|
|
64
|
+
}
|
|
37
65
|
}
|