startgg-oauth2-full 0.2.0 → 0.2.1
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 +14 -5
- package/dist/auth/StartGGOAuth2.d.ts +84 -0
- package/dist/auth/StartGGOAuth2.js +291 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +15 -2
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -18
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -13
- package/.github/pull_request_template.md +0 -30
- package/.github/workflows/ci.yml +0 -23
- package/.github/workflows/release.yml +0 -60
- package/AGENTS.md +0 -46
- package/CONTRIBUTING.md +0 -36
- package/STARTGG_OAUTH_SETUP.md +0 -41
- package/__tests__/authorize-url.test.ts +0 -62
- package/__tests__/bearer-token.test.ts +0 -21
- package/__tests__/handler.test.ts +0 -111
- package/__tests__/pkce.test.ts +0 -17
- package/examples/browser/README.md +0 -20
- package/examples/browser/index.html +0 -55
- package/examples/browser/package.json +0 -17
- package/examples/browser/src/main.ts +0 -105
- package/examples/browser/tsconfig.json +0 -11
- package/examples/browser/vite.config.ts +0 -8
- package/examples/discordjs/.env.example +0 -9
- package/examples/discordjs/README.md +0 -36
- package/examples/discordjs/package.json +0 -23
- package/examples/discordjs/src/bot.ts +0 -202
- package/examples/discordjs/tsconfig.json +0 -12
- package/examples/nextjs/.env.example +0 -7
- package/examples/nextjs/README.md +0 -33
- package/examples/nextjs/app/api/startgg/auth-url/route.ts +0 -36
- package/examples/nextjs/app/api/startgg/callback/route.ts +0 -55
- package/examples/nextjs/app/globals.css +0 -48
- package/examples/nextjs/app/layout.tsx +0 -15
- package/examples/nextjs/app/page.tsx +0 -93
- package/examples/nextjs/lib/pendingStore.ts +0 -37
- package/examples/nextjs/lib/startgg.ts +0 -28
- package/examples/nextjs/next-env.d.ts +0 -5
- package/examples/nextjs/next.config.mjs +0 -6
- package/examples/nextjs/package.json +0 -25
- package/examples/nextjs/tsconfig.json +0 -21
- package/examples/node/.env.example +0 -4
- package/examples/node/README.md +0 -27
- package/examples/node/package.json +0 -17
- package/examples/node/src/index.ts +0 -57
- package/examples/node/src/server.ts +0 -120
- package/examples/node/tsconfig.json +0 -12
- package/examples/vite/README.md +0 -22
- package/examples/vite/index.html +0 -41
- package/examples/vite/package.json +0 -18
- package/examples/vite/src/main.ts +0 -38
- package/examples/vite/tsconfig.json +0 -11
- package/examples/vite/vite.config.ts +0 -8
- package/jest.config.ts +0 -15
- package/jest.setup.ts +0 -29
- package/src/auth/StartGGOAuth2.ts +0 -378
- package/tsconfig.json +0 -27
package/CONTRIBUTING.md
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# Contributing to startgg-oauth2-full
|
|
2
|
-
|
|
3
|
-
## Prerequisites
|
|
4
|
-
- Node 18+ and npm 9+
|
|
5
|
-
- Git
|
|
6
|
-
|
|
7
|
-
## Layout
|
|
8
|
-
- `src` — library (TypeScript)
|
|
9
|
-
- `__tests__` — unit tests
|
|
10
|
-
- `examples` — browser + node demos (includes local redirect catcher)
|
|
11
|
-
- `.github/workflows/ci.yml` — CI (build + tests)
|
|
12
|
-
|
|
13
|
-
## Setup
|
|
14
|
-
```bash
|
|
15
|
-
npm i
|
|
16
|
-
npm run build
|
|
17
|
-
npm test
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
## Run examples
|
|
21
|
-
```bash
|
|
22
|
-
npm run dev:browser
|
|
23
|
-
npm run dev:node
|
|
24
|
-
STARTGG_CLIENT_ID=your_id \
|
|
25
|
-
STARTGG_AUTH_URL=https://api.start.gg/oauth/authorize \
|
|
26
|
-
STARTGG_TOKEN_URL=https://api.start.gg/oauth/token \
|
|
27
|
-
npm run dev:node:server
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## Branch & PR
|
|
31
|
-
- Branch naming: `feat/*`, `fix/*`, `docs/*`, `chore/*`
|
|
32
|
-
- Before PR:
|
|
33
|
-
- [ ] `npm run build` passes
|
|
34
|
-
- [ ] `npm test` passes
|
|
35
|
-
- [ ] Tests added/updated when changing behavior
|
|
36
|
-
- [ ] README/docs updated if API or flows changed
|
package/STARTGG_OAUTH_SETUP.md
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
# Start.gg OAuth Setup
|
|
2
|
-
|
|
3
|
-
## 1) Create an OAuth app
|
|
4
|
-
- In Start.gg developer console, create a **Public** OAuth app (PKCE).
|
|
5
|
-
- Copy **Client ID**.
|
|
6
|
-
|
|
7
|
-
## 2) Register redirect URIs
|
|
8
|
-
Add exact matches you will use:
|
|
9
|
-
- Browser demo: `http://localhost:5174/index.html` (or the exact URL you open)
|
|
10
|
-
- Node catcher: `http://localhost:3000/callback`
|
|
11
|
-
|
|
12
|
-
> Redirect URIs must match exactly (scheme/host/port/path).
|
|
13
|
-
|
|
14
|
-
## 3) Scopes
|
|
15
|
-
Common scopes:
|
|
16
|
-
- `user.identity`
|
|
17
|
-
- `user.email` (requires `user.identity`)
|
|
18
|
-
- `tournament.manager`
|
|
19
|
-
- `tournament.reporter`
|
|
20
|
-
|
|
21
|
-
## 4) Environment variables (Node catcher)
|
|
22
|
-
```bash
|
|
23
|
-
export STARTGG_CLIENT_ID=your_client_id
|
|
24
|
-
export STARTGG_AUTH_URL=https://api.start.gg/oauth/authorize
|
|
25
|
-
export STARTGG_TOKEN_URL=https://api.start.gg/oauth/token
|
|
26
|
-
npm run dev:node:server
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## 5) Test the flow
|
|
30
|
-
1. Ensure `http://localhost:3000/callback` is registered.
|
|
31
|
-
2. Run the server script above.
|
|
32
|
-
3. Browser opens Start.gg consent.
|
|
33
|
-
4. Approve; you’re redirected to `/callback`.
|
|
34
|
-
5. Terminal prints masked tokens + `Authorization` header.
|
|
35
|
-
|
|
36
|
-
## 6) Production notes
|
|
37
|
-
- Always HTTPS for redirects.
|
|
38
|
-
- Persist and validate `state` (CSRF).
|
|
39
|
-
- Keep `code_verifier` private (session/server).
|
|
40
|
-
- Never log raw tokens in prod.
|
|
41
|
-
- Plan for refresh token storage/rotation.
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { buildAuthorizeUrl, StartGGScope } from '../src/auth/StartGGOAuth2';
|
|
2
|
-
|
|
3
|
-
describe('Authorize URL', () => {
|
|
4
|
-
test('includes required params and scopes', async () => {
|
|
5
|
-
const cfg = {
|
|
6
|
-
clientId: 'abc',
|
|
7
|
-
authEndpoint: 'https://example.com/authorize',
|
|
8
|
-
redirectUri: 'https://app/callback',
|
|
9
|
-
};
|
|
10
|
-
const { url, codeVerifier, codeChallenge } = await buildAuthorizeUrl(cfg, {
|
|
11
|
-
scopes: [StartGGScope.USER_IDENTITY, StartGGScope.USER_EMAIL],
|
|
12
|
-
state: 'xyz',
|
|
13
|
-
prompt: 'consent',
|
|
14
|
-
extras: { access_type: 'offline' },
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
const u = new URL(url);
|
|
18
|
-
expect(u.searchParams.get('response_type')).toBe('code');
|
|
19
|
-
expect(u.searchParams.get('client_id')).toBe('abc');
|
|
20
|
-
expect(u.searchParams.get('redirect_uri')).toBe('https://app/callback');
|
|
21
|
-
expect(u.searchParams.get('scope')).toContain('user.identity');
|
|
22
|
-
expect(u.searchParams.get('scope')).toContain('user.email');
|
|
23
|
-
expect(u.searchParams.get('state')).toBe('xyz');
|
|
24
|
-
expect(u.searchParams.get('prompt')).toBe('consent');
|
|
25
|
-
expect(u.searchParams.get('access_type')).toBe('offline');
|
|
26
|
-
expect(u.searchParams.get('code_challenge_method')).toBe('S256');
|
|
27
|
-
expect(codeVerifier.length).toBeGreaterThanOrEqual(43);
|
|
28
|
-
expect(codeChallenge.length).toBeGreaterThan(20);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test('throws when codeChallenge provided without matching codeVerifier', async () => {
|
|
32
|
-
const cfg = {
|
|
33
|
-
clientId: 'abc',
|
|
34
|
-
authEndpoint: 'https://example.com/authorize',
|
|
35
|
-
redirectUri: 'https://app/callback',
|
|
36
|
-
};
|
|
37
|
-
await expect(
|
|
38
|
-
buildAuthorizeUrl(cfg, {
|
|
39
|
-
scopes: [],
|
|
40
|
-
codeChallenge: 'custom-challenge',
|
|
41
|
-
})
|
|
42
|
-
).rejects.toThrow(/codeVerifier is required/);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test('accepts externally provided PKCE pair when consistent', async () => {
|
|
46
|
-
const cfg = {
|
|
47
|
-
clientId: 'abc',
|
|
48
|
-
authEndpoint: 'https://example.com/authorize',
|
|
49
|
-
redirectUri: 'https://app/callback',
|
|
50
|
-
};
|
|
51
|
-
const verifier = 'test-verifier-1234567890_-=.~';
|
|
52
|
-
const expectedChallenge = 'xPCddgBLJpr4TYXpjK5OM51rAX0xrMyIOiVyy18DPQ8';
|
|
53
|
-
const result = await buildAuthorizeUrl(cfg, {
|
|
54
|
-
scopes: [],
|
|
55
|
-
codeVerifier: verifier,
|
|
56
|
-
codeChallenge: expectedChallenge,
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
expect(result.codeVerifier).toBe(verifier);
|
|
60
|
-
expect(result.codeChallenge).toBe(expectedChallenge);
|
|
61
|
-
});
|
|
62
|
-
});
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { BearerToken } from '../src/auth/StartGGOAuth2';
|
|
2
|
-
|
|
3
|
-
describe('BearerToken', () => {
|
|
4
|
-
test('applies skew and expiry checks', () => {
|
|
5
|
-
const now = 1_000_000;
|
|
6
|
-
const res = {
|
|
7
|
-
access_token: 'at',
|
|
8
|
-
token_type: 'Bearer',
|
|
9
|
-
expires_in: 120,
|
|
10
|
-
};
|
|
11
|
-
const token = BearerToken.fromOAuthResponse(res as any, now, 60);
|
|
12
|
-
expect(token.isExpired(now)).toBe(false);
|
|
13
|
-
expect(token.willExpireWithin(120, now)).toBe(true);
|
|
14
|
-
expect(token.toAuthHeader()).toEqual({ Authorization: 'Bearer at' });
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
test('non-expiring when expires_in absent', () => {
|
|
18
|
-
const token = BearerToken.fromOAuthResponse({ access_token: 'at', token_type: 'Bearer' } as any, 0, 60);
|
|
19
|
-
expect(token.isExpired()).toBe(false);
|
|
20
|
-
});
|
|
21
|
-
});
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createStartGGAuth2Handler,
|
|
3
|
-
StartGGScope,
|
|
4
|
-
OAuth2Error,
|
|
5
|
-
} from '../src/auth/StartGGOAuth2';
|
|
6
|
-
|
|
7
|
-
describe('StartGGOAuth2Handler', () => {
|
|
8
|
-
const cfg = {
|
|
9
|
-
clientId: 'client',
|
|
10
|
-
redirectUri: 'https://app/callback',
|
|
11
|
-
authEndpoint: 'https://example.com/authorize',
|
|
12
|
-
tokenEndpoint: 'https://example.com/token',
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
jest.restoreAllMocks();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test('exchangeToken success with scope present', async () => {
|
|
20
|
-
const mockBody = {
|
|
21
|
-
access_token: 'at',
|
|
22
|
-
token_type: 'Bearer',
|
|
23
|
-
expires_in: 3600,
|
|
24
|
-
scope: 'user.identity user.email',
|
|
25
|
-
refresh_token: 'rt',
|
|
26
|
-
};
|
|
27
|
-
jest.spyOn(global, 'fetch' as any).mockResolvedValueOnce(
|
|
28
|
-
new Response(JSON.stringify(mockBody), { status: 200, headers: { 'Content-Type': 'application/json' } })
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
const handler = createStartGGAuth2Handler(cfg);
|
|
32
|
-
const res = await handler.exchangeToken('code', 'verifier', [StartGGScope.USER_IDENTITY, StartGGScope.USER_EMAIL]);
|
|
33
|
-
expect(res.access_token).toBe('at');
|
|
34
|
-
expect(res.refresh_token).toBe('rt');
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test('exchangeToken accepts missing scope (assume unchanged)', async () => {
|
|
38
|
-
const mockBody = {
|
|
39
|
-
access_token: 'at',
|
|
40
|
-
token_type: 'Bearer',
|
|
41
|
-
expires_in: 3600,
|
|
42
|
-
refresh_token: 'rt',
|
|
43
|
-
};
|
|
44
|
-
jest.spyOn(global, 'fetch' as any).mockResolvedValueOnce(
|
|
45
|
-
new Response(JSON.stringify(mockBody), { status: 200, headers: { 'Content-Type': 'application/json' } })
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
const handler = createStartGGAuth2Handler(cfg);
|
|
49
|
-
const res = await handler.exchangeToken('code', 'verifier', [StartGGScope.USER_IDENTITY]);
|
|
50
|
-
expect(res.access_token).toBe('at');
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test('exchangeToken fails when token_type not Bearer', async () => {
|
|
54
|
-
const mockBody = {
|
|
55
|
-
access_token: 'at',
|
|
56
|
-
token_type: 'MAC',
|
|
57
|
-
expires_in: 3600,
|
|
58
|
-
scope: 'user.identity',
|
|
59
|
-
};
|
|
60
|
-
jest.spyOn(global, 'fetch' as any).mockResolvedValueOnce(
|
|
61
|
-
new Response(JSON.stringify(mockBody), { status: 200, headers: { 'Content-Type': 'application/json' } })
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
const handler = createStartGGAuth2Handler(cfg);
|
|
65
|
-
await expect(handler.exchangeToken('code', 'verifier', [StartGGScope.USER_IDENTITY]))
|
|
66
|
-
.rejects.toThrow(OAuth2Error);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test('refreshToken preserves previous refresh_token when omitted', async () => {
|
|
70
|
-
const mockBody = {
|
|
71
|
-
access_token: 'at2',
|
|
72
|
-
token_type: 'Bearer',
|
|
73
|
-
expires_in: 3600,
|
|
74
|
-
scope: 'user.identity',
|
|
75
|
-
};
|
|
76
|
-
jest.spyOn(global, 'fetch' as any).mockResolvedValueOnce(
|
|
77
|
-
new Response(JSON.stringify(mockBody), { status: 200, headers: { 'Content-Type': 'application/json' } })
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
const handler = createStartGGAuth2Handler(cfg);
|
|
81
|
-
const res = await handler.refreshToken('rt-old', [StartGGScope.USER_IDENTITY]);
|
|
82
|
-
expect(res.refresh_token).toBe('rt-old');
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test('refreshToken throws when scope reduced', async () => {
|
|
86
|
-
const mockBody = {
|
|
87
|
-
access_token: 'at3',
|
|
88
|
-
token_type: 'Bearer',
|
|
89
|
-
scope: 'user.identity',
|
|
90
|
-
};
|
|
91
|
-
jest.spyOn(global, 'fetch' as any).mockResolvedValueOnce(
|
|
92
|
-
new Response(JSON.stringify(mockBody), { status: 200, headers: { 'Content-Type': 'application/json' } })
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
const handler = createStartGGAuth2Handler(cfg);
|
|
96
|
-
await expect(
|
|
97
|
-
handler.refreshToken('rt', [StartGGScope.USER_IDENTITY, StartGGScope.USER_EMAIL])
|
|
98
|
-
).rejects.toThrow('Missing required scopes: user.email');
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test('handles non-JSON error body safely', async () => {
|
|
102
|
-
jest.spyOn(global, 'fetch' as any).mockResolvedValueOnce(
|
|
103
|
-
// @ts-ignore
|
|
104
|
-
new Response('<html>bad request</html>', { status: 400, headers: { 'Content-Type': 'text/html' } })
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
const handler = createStartGGAuth2Handler(cfg);
|
|
108
|
-
await expect(handler.exchangeToken('code', 'verifier', [StartGGScope.USER_IDENTITY]))
|
|
109
|
-
.rejects.toThrow('Token exchange failed');
|
|
110
|
-
});
|
|
111
|
-
});
|
package/__tests__/pkce.test.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { computeCodeChallengeS256, generateCodeVerifier } from '../src/auth/StartGGOAuth2';
|
|
2
|
-
|
|
3
|
-
describe('PKCE helpers', () => {
|
|
4
|
-
test('generateCodeVerifier length bounds', () => {
|
|
5
|
-
expect(generateCodeVerifier(10).length).toBeGreaterThanOrEqual(43);
|
|
6
|
-
expect(generateCodeVerifier(200).length).toBeLessThanOrEqual(128);
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
test('computeCodeChallengeS256 deterministic', async () => {
|
|
10
|
-
const verifier = 'test-verifier-1234567890_-=.~';
|
|
11
|
-
const ch1 = await computeCodeChallengeS256(verifier);
|
|
12
|
-
const ch2 = await computeCodeChallengeS256(verifier);
|
|
13
|
-
expect(ch1).toEqual(ch2);
|
|
14
|
-
expect(typeof ch1).toBe('string');
|
|
15
|
-
expect(ch1.length).toBeGreaterThan(20);
|
|
16
|
-
});
|
|
17
|
-
});
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# Browser Demo
|
|
2
|
-
|
|
3
|
-
This example runs the Start.gg OAuth helpers in a plain browser application with no framework. It uses Vite for bundling and serves a single page that builds PKCE authorize URLs, displays the verifier/challenge pair, and exchanges the authorization code after Start.gg redirects back.
|
|
4
|
-
|
|
5
|
-
## Getting Started
|
|
6
|
-
```bash
|
|
7
|
-
cd examples/browser
|
|
8
|
-
npm install
|
|
9
|
-
npm run dev
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
The dev server opens at `http://localhost:5173`. Fill in your Start.gg OAuth client details and click **Generate Authorize URL**. Inspect the generated verifier/challenge values, open the authorize link in a new tab, and complete the Start.gg flow. When Start.gg redirects back, the page exchanges the code and prints a token summary.
|
|
13
|
-
|
|
14
|
-
## Production Build
|
|
15
|
-
```bash
|
|
16
|
-
npm run build
|
|
17
|
-
npm run preview
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
`npm run build` writes static assets to `dist/`, and `npm run preview` serves them locally.
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>Start.gg OAuth2 Browser Demo</title>
|
|
7
|
-
</head>
|
|
8
|
-
<body>
|
|
9
|
-
<main>
|
|
10
|
-
<h1>Start.gg OAuth2 (Vanilla Browser)</h1>
|
|
11
|
-
<p>
|
|
12
|
-
This demo uses the <code>startgg-oauth2-full</code> helpers to produce a PKCE authorize URL and exchange the
|
|
13
|
-
resulting authorization code inside a plain browser app.
|
|
14
|
-
</p>
|
|
15
|
-
|
|
16
|
-
<form id="config">
|
|
17
|
-
<label>
|
|
18
|
-
Client ID
|
|
19
|
-
<input name="clientId" value="YOUR_CLIENT_ID" required />
|
|
20
|
-
</label>
|
|
21
|
-
<label>
|
|
22
|
-
Auth Endpoint
|
|
23
|
-
<input name="authEndpoint" value="https://api.start.gg/oauth/authorize" required />
|
|
24
|
-
</label>
|
|
25
|
-
<label>
|
|
26
|
-
Token Endpoint
|
|
27
|
-
<input name="tokenEndpoint" value="https://api.start.gg/oauth/token" required />
|
|
28
|
-
</label>
|
|
29
|
-
<label>
|
|
30
|
-
Redirect URI
|
|
31
|
-
<input name="redirectUri" value="http://localhost:5173/" required />
|
|
32
|
-
</label>
|
|
33
|
-
<button type="submit">Generate Authorize URL</button>
|
|
34
|
-
</form>
|
|
35
|
-
|
|
36
|
-
<section>
|
|
37
|
-
<h2>Generated Values</h2>
|
|
38
|
-
<h3>Authorize URL</h3>
|
|
39
|
-
<pre id="output-url">Fill the form to generate a URL.</pre>
|
|
40
|
-
<h3>Verifier</h3>
|
|
41
|
-
<pre id="output-verifier">Verifier will be shown here.</pre>
|
|
42
|
-
<h3>Challenge</h3>
|
|
43
|
-
<pre id="output-challenge">Challenge will be shown here.</pre>
|
|
44
|
-
</section>
|
|
45
|
-
|
|
46
|
-
<section>
|
|
47
|
-
<h2>Authorization Callback</h2>
|
|
48
|
-
<p id="callback-status">Waiting for callback…</p>
|
|
49
|
-
<pre id="token-preview">Token preview appears here after callback.</pre>
|
|
50
|
-
</section>
|
|
51
|
-
</main>
|
|
52
|
-
|
|
53
|
-
<script type="module" src="/src/main.ts"></script>
|
|
54
|
-
</body>
|
|
55
|
-
</html>
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "startgg-oauth2-browser-example",
|
|
3
|
-
"version": "0.0.0",
|
|
4
|
-
"private": true,
|
|
5
|
-
"scripts": {
|
|
6
|
-
"dev": "vite",
|
|
7
|
-
"build": "vite build",
|
|
8
|
-
"preview": "vite preview"
|
|
9
|
-
},
|
|
10
|
-
"dependencies": {
|
|
11
|
-
"startgg-oauth2-full": "file:../.."
|
|
12
|
-
},
|
|
13
|
-
"devDependencies": {
|
|
14
|
-
"typescript": "^5.6.3",
|
|
15
|
-
"vite": "^5.4.8"
|
|
16
|
-
}
|
|
17
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
BearerToken,
|
|
3
|
-
StartGGScope,
|
|
4
|
-
buildAuthorizeUrl,
|
|
5
|
-
createStartGGAuth2Handler,
|
|
6
|
-
} from 'startgg-oauth2-full/src/auth/StartGGOAuth2';
|
|
7
|
-
|
|
8
|
-
const form = document.querySelector<HTMLFormElement>('#config')!;
|
|
9
|
-
const outputUrl = document.querySelector<HTMLPreElement>('#output-url')!;
|
|
10
|
-
const outputVerifier = document.querySelector<HTMLPreElement>('#output-verifier')!;
|
|
11
|
-
const outputChallenge = document.querySelector<HTMLPreElement>('#output-challenge')!;
|
|
12
|
-
const callbackStatus = document.querySelector<HTMLParagraphElement>('#callback-status')!;
|
|
13
|
-
const tokenPreview = document.querySelector<HTMLPreElement>('#token-preview')!;
|
|
14
|
-
|
|
15
|
-
let lastVerifier: string | null = null;
|
|
16
|
-
let lastConfig: {
|
|
17
|
-
clientId: string;
|
|
18
|
-
authEndpoint: string;
|
|
19
|
-
tokenEndpoint: string;
|
|
20
|
-
redirectUri: string;
|
|
21
|
-
} | null = null;
|
|
22
|
-
|
|
23
|
-
form.addEventListener('submit', async event => {
|
|
24
|
-
event.preventDefault();
|
|
25
|
-
const data = new FormData(form);
|
|
26
|
-
|
|
27
|
-
const cfg = {
|
|
28
|
-
clientId: String(data.get('clientId') ?? ''),
|
|
29
|
-
authEndpoint: String(data.get('authEndpoint') ?? ''),
|
|
30
|
-
tokenEndpoint: String(data.get('tokenEndpoint') ?? ''),
|
|
31
|
-
redirectUri: String(data.get('redirectUri') ?? ''),
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
const { url, codeVerifier, codeChallenge } = await buildAuthorizeUrl(cfg, {
|
|
36
|
-
scopes: [StartGGScope.USER_IDENTITY, StartGGScope.USER_EMAIL],
|
|
37
|
-
state: crypto.randomUUID(),
|
|
38
|
-
prompt: 'consent',
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
lastVerifier = codeVerifier;
|
|
42
|
-
lastConfig = cfg;
|
|
43
|
-
|
|
44
|
-
sessionStorage.setItem('startgg:lastVerifier', codeVerifier);
|
|
45
|
-
sessionStorage.setItem('startgg:lastConfig', JSON.stringify(cfg));
|
|
46
|
-
|
|
47
|
-
outputUrl.textContent = url;
|
|
48
|
-
outputVerifier.textContent = codeVerifier;
|
|
49
|
-
outputChallenge.textContent = codeChallenge;
|
|
50
|
-
callbackStatus.textContent = 'Waiting for authorization code…';
|
|
51
|
-
tokenPreview.textContent = 'Token preview appears here after callback.';
|
|
52
|
-
} catch (error) {
|
|
53
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
54
|
-
outputUrl.textContent = `Failed to generate authorize URL: ${message}`;
|
|
55
|
-
outputVerifier.textContent = '—';
|
|
56
|
-
outputChallenge.textContent = '—';
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
async function handleCallbackIfPresent() {
|
|
61
|
-
const params = new URLSearchParams(window.location.search);
|
|
62
|
-
const code = params.get('code');
|
|
63
|
-
const state = params.get('state');
|
|
64
|
-
if (!code || !state) return;
|
|
65
|
-
|
|
66
|
-
if (!lastVerifier || !lastConfig) {
|
|
67
|
-
lastVerifier = sessionStorage.getItem('startgg:lastVerifier');
|
|
68
|
-
const stored = sessionStorage.getItem('startgg:lastConfig');
|
|
69
|
-
lastConfig = stored ? JSON.parse(stored) : null;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (!lastVerifier || !lastConfig) {
|
|
73
|
-
callbackStatus.textContent = 'Missing verifier/config in session storage; restart the flow.';
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
const handler = createStartGGAuth2Handler(lastConfig);
|
|
79
|
-
const tokenResponse = await handler.exchangeToken(code, lastVerifier, [
|
|
80
|
-
StartGGScope.USER_IDENTITY,
|
|
81
|
-
StartGGScope.USER_EMAIL,
|
|
82
|
-
]);
|
|
83
|
-
|
|
84
|
-
const bearer = BearerToken.fromOAuthResponse(tokenResponse);
|
|
85
|
-
callbackStatus.textContent = 'Authorization complete!';
|
|
86
|
-
tokenPreview.textContent = JSON.stringify(
|
|
87
|
-
{
|
|
88
|
-
accessTokenPreview: `${bearer.accessToken.slice(0, 8)}…`,
|
|
89
|
-
expiresIn: tokenResponse.expires_in ?? null,
|
|
90
|
-
scope: tokenResponse.scope ?? null,
|
|
91
|
-
refreshTokenPresent: Boolean(tokenResponse.refresh_token),
|
|
92
|
-
},
|
|
93
|
-
null,
|
|
94
|
-
2
|
|
95
|
-
);
|
|
96
|
-
} catch (error) {
|
|
97
|
-
console.error('[browser example] Token exchange failed', error);
|
|
98
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
99
|
-
callbackStatus.textContent = `Token exchange failed: ${message}`;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
handleCallbackIfPresent().catch(err => {
|
|
104
|
-
console.error('[browser example] Failed to process callback', err);
|
|
105
|
-
});
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
DISCORD_TOKEN=YOUR_DISCORD_BOT_TOKEN
|
|
2
|
-
DISCORD_CLIENT_ID=YOUR_DISCORD_APPLICATION_ID
|
|
3
|
-
# Optional: restrict slash command registration to a single guild for faster propagation.
|
|
4
|
-
# DISCORD_GUILD_ID=YOUR_TEST_GUILD_ID
|
|
5
|
-
|
|
6
|
-
STARTGG_CLIENT_ID=YOUR_STARTGG_CLIENT_ID
|
|
7
|
-
# STARTGG_AUTH_ENDPOINT=https://api.start.gg/oauth/authorize
|
|
8
|
-
# STARTGG_TOKEN_ENDPOINT=https://api.start.gg/oauth/token
|
|
9
|
-
STARTGG_REDIRECT_URI=http://localhost:5175/oauth/callback
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# Discord.js Bot Demo
|
|
2
|
-
|
|
3
|
-
This example shows how to generate Start.gg OAuth authorize links and exchange tokens from a Discord slash command using `discord.js` v14.
|
|
4
|
-
|
|
5
|
-
## Prerequisites
|
|
6
|
-
- Node.js 18+ (for global `fetch` and WebCrypto).
|
|
7
|
-
- A Discord application with a bot token and the `applications.commands` scope.
|
|
8
|
-
- A Start.gg OAuth client configured with a redirect URI like `http://localhost:5175/oauth/callback`.
|
|
9
|
-
|
|
10
|
-
## Setup
|
|
11
|
-
```bash
|
|
12
|
-
cd examples/discordjs
|
|
13
|
-
cp .env.example .env
|
|
14
|
-
# Edit .env with your Discord IDs and Start.gg client info
|
|
15
|
-
npm install
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
Required `.env` values:
|
|
19
|
-
- `DISCORD_TOKEN` – bot token.
|
|
20
|
-
- `DISCORD_CLIENT_ID` – application/client ID.
|
|
21
|
-
- `STARTGG_CLIENT_ID` – Start.gg OAuth client ID.
|
|
22
|
-
- Optional: `DISCORD_GUILD_ID` for faster guild-only slash command registration.
|
|
23
|
-
- Optional overrides: `STARTGG_AUTH_ENDPOINT`, `STARTGG_TOKEN_ENDPOINT`, `STARTGG_REDIRECT_URI`.
|
|
24
|
-
|
|
25
|
-
## Running the Bot
|
|
26
|
-
```bash
|
|
27
|
-
npm run dev
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
The script registers the `/startgg-auth` command, logs the bot in, and starts an Express callback server that listens on the redirect URI's port/path (defaults to `http://localhost:5175/oauth/callback`).
|
|
31
|
-
|
|
32
|
-
1. Invoke `/startgg-auth` in a guild where the bot is present.
|
|
33
|
-
2. Follow the generated authorize link; after approving Start.gg, the callback exchanges the code for tokens.
|
|
34
|
-
3. The bot logs the full token response and DMs you a confirmation with truncated credentials for demonstration.
|
|
35
|
-
|
|
36
|
-
Store tokens securely in real applications—persist them in encrypted storage instead of sending them via DM.
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "startgg-oauth2-discordjs-example",
|
|
3
|
-
"version": "0.0.0",
|
|
4
|
-
"private": true,
|
|
5
|
-
"type": "module",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"dev": "tsx watch src/bot.ts",
|
|
8
|
-
"start": "tsx src/bot.ts",
|
|
9
|
-
"build": "tsc --outDir dist --project tsconfig.json && node dist/bot.js"
|
|
10
|
-
},
|
|
11
|
-
"dependencies": {
|
|
12
|
-
"discord.js": "^14.15.3",
|
|
13
|
-
"dotenv": "^16.4.5",
|
|
14
|
-
"express": "^4.19.2",
|
|
15
|
-
"startgg-oauth2-full": "file:../.."
|
|
16
|
-
},
|
|
17
|
-
"devDependencies": {
|
|
18
|
-
"@types/express": "^4.17.21",
|
|
19
|
-
"@types/node": "^22.7.5",
|
|
20
|
-
"tsx": "^4.19.0",
|
|
21
|
-
"typescript": "^5.6.3"
|
|
22
|
-
}
|
|
23
|
-
}
|