startgg-oauth2-full 0.2.0 → 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 +325 -51
- package/dist/auth/StartGGOAuth2.d.ts +85 -0
- package/dist/auth/StartGGOAuth2.js +306 -0
- package/dist/constants.d.ts +10 -0
- package/dist/constants.js +16 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/package.json +63 -22
- 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
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
import 'dotenv/config';
|
|
2
|
-
import express from 'express';
|
|
3
|
-
import { randomUUID } from 'node:crypto';
|
|
4
|
-
import { Client, GatewayIntentBits, Partials, REST, Routes, SlashCommandBuilder } from 'discord.js';
|
|
5
|
-
import {
|
|
6
|
-
BearerToken,
|
|
7
|
-
StartGGScope,
|
|
8
|
-
buildAuthorizeUrl,
|
|
9
|
-
createStartGGAuth2Handler,
|
|
10
|
-
} from 'startgg-oauth2-full/src/auth/StartGGOAuth2';
|
|
11
|
-
|
|
12
|
-
type PendingAuth = {
|
|
13
|
-
userId: string;
|
|
14
|
-
codeVerifier: string;
|
|
15
|
-
scopes: StartGGScope[];
|
|
16
|
-
timer: NodeJS.Timeout;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const pendingAuthorizations = new Map<string, PendingAuth>();
|
|
20
|
-
const AUTH_TIMEOUT_MS = 10 * 60_000;
|
|
21
|
-
|
|
22
|
-
const requiredEnv = ['DISCORD_TOKEN', 'DISCORD_CLIENT_ID', 'STARTGG_CLIENT_ID'] as const;
|
|
23
|
-
const missing = requiredEnv.filter(key => !process.env[key]);
|
|
24
|
-
if (missing.length) {
|
|
25
|
-
throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const DISCORD_TOKEN = process.env.DISCORD_TOKEN!;
|
|
29
|
-
const DISCORD_CLIENT_ID = process.env.DISCORD_CLIENT_ID!;
|
|
30
|
-
const DISCORD_GUILD_ID = process.env.DISCORD_GUILD_ID;
|
|
31
|
-
|
|
32
|
-
const startggConfig = {
|
|
33
|
-
clientId: process.env.STARTGG_CLIENT_ID!,
|
|
34
|
-
authEndpoint: process.env.STARTGG_AUTH_ENDPOINT ?? 'https://api.start.gg/oauth/authorize',
|
|
35
|
-
tokenEndpoint: process.env.STARTGG_TOKEN_ENDPOINT ?? 'https://api.start.gg/oauth/token',
|
|
36
|
-
redirectUri: process.env.STARTGG_REDIRECT_URI ?? 'http://localhost:5175/oauth/callback',
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const redirectUrl = new URL(startggConfig.redirectUri);
|
|
40
|
-
if (redirectUrl.protocol !== 'http:') {
|
|
41
|
-
console.warn(
|
|
42
|
-
`[discordjs example] Redirect URI ${startggConfig.redirectUri} is non-HTTP; ` +
|
|
43
|
-
'this demo only spins up an HTTP callback server. Adjust as needed.'
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const callbackPath = redirectUrl.pathname || '/';
|
|
48
|
-
const callbackPort = Number(redirectUrl.port || 5175);
|
|
49
|
-
|
|
50
|
-
const startggHandler = createStartGGAuth2Handler({
|
|
51
|
-
clientId: startggConfig.clientId,
|
|
52
|
-
redirectUri: startggConfig.redirectUri,
|
|
53
|
-
authEndpoint: startggConfig.authEndpoint,
|
|
54
|
-
tokenEndpoint: startggConfig.tokenEndpoint,
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const client = new Client({
|
|
58
|
-
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.DirectMessages],
|
|
59
|
-
partials: [Partials.Channel],
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
client.once('ready', readyClient => {
|
|
63
|
-
console.log(`[discordjs example] Logged in as ${readyClient.user.tag}`);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
const scopes = [StartGGScope.USER_IDENTITY, StartGGScope.USER_EMAIL];
|
|
67
|
-
|
|
68
|
-
async function registerSlashCommand(): Promise<void> {
|
|
69
|
-
const command = new SlashCommandBuilder()
|
|
70
|
-
.setName('startgg-auth')
|
|
71
|
-
.setDescription('Generate a Start.gg OAuth authorize URL with PKCE support.')
|
|
72
|
-
.setDMPermission(false);
|
|
73
|
-
|
|
74
|
-
const rest = new REST({ version: '10' }).setToken(DISCORD_TOKEN);
|
|
75
|
-
if (DISCORD_GUILD_ID) {
|
|
76
|
-
await rest.put(Routes.applicationGuildCommands(DISCORD_CLIENT_ID, DISCORD_GUILD_ID), {
|
|
77
|
-
body: [command.toJSON()],
|
|
78
|
-
});
|
|
79
|
-
console.log(
|
|
80
|
-
`[discordjs example] Registered /startgg-auth for guild ${DISCORD_GUILD_ID}. ` +
|
|
81
|
-
'Guild commands update instantly.'
|
|
82
|
-
);
|
|
83
|
-
} else {
|
|
84
|
-
await rest.put(Routes.applicationCommands(DISCORD_CLIENT_ID), {
|
|
85
|
-
body: [command.toJSON()],
|
|
86
|
-
});
|
|
87
|
-
console.log('[discordjs example] Registered /startgg-auth globally. Propagation may take up to an hour.');
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
client.on('interactionCreate', async interaction => {
|
|
92
|
-
if (!interaction.isChatInputCommand() || interaction.commandName !== 'startgg-auth') return;
|
|
93
|
-
|
|
94
|
-
await interaction.deferReply({ ephemeral: true });
|
|
95
|
-
|
|
96
|
-
const state = `${interaction.user.id}-${randomUUID()}`;
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
const { url, codeVerifier } = await buildAuthorizeUrl(
|
|
100
|
-
{
|
|
101
|
-
clientId: startggConfig.clientId,
|
|
102
|
-
authEndpoint: startggConfig.authEndpoint,
|
|
103
|
-
redirectUri: startggConfig.redirectUri,
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
scopes,
|
|
107
|
-
state,
|
|
108
|
-
prompt: 'consent',
|
|
109
|
-
}
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
const timer = setTimeout(() => {
|
|
113
|
-
pendingAuthorizations.delete(state);
|
|
114
|
-
}, AUTH_TIMEOUT_MS);
|
|
115
|
-
|
|
116
|
-
pendingAuthorizations.set(state, {
|
|
117
|
-
userId: interaction.user.id,
|
|
118
|
-
codeVerifier,
|
|
119
|
-
scopes,
|
|
120
|
-
timer,
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
await interaction.editReply({
|
|
124
|
-
content: [
|
|
125
|
-
'Authorize Start.gg with PKCE:',
|
|
126
|
-
url,
|
|
127
|
-
'',
|
|
128
|
-
`This link is tied to your Discord user and expires in ${Math.round(AUTH_TIMEOUT_MS / 60000)} minutes.`,
|
|
129
|
-
'After authorizing, the bot will DM you once the callback succeeds.',
|
|
130
|
-
].join('\n'),
|
|
131
|
-
});
|
|
132
|
-
} catch (error) {
|
|
133
|
-
console.error('[discordjs example] Failed to build authorize URL', error);
|
|
134
|
-
pendingAuthorizations.delete(state);
|
|
135
|
-
await interaction.editReply('Failed to generate authorize URL. Check the bot logs for details.');
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
const app = express();
|
|
140
|
-
|
|
141
|
-
app.get(callbackPath, async (req, res) => {
|
|
142
|
-
const { state, code, error, error_description: errorDescription } = req.query as Record<string, string | undefined>;
|
|
143
|
-
|
|
144
|
-
if (error) {
|
|
145
|
-
console.warn(`[discordjs example] Authorization error: ${error} ${errorDescription ?? ''}`.trim());
|
|
146
|
-
res.status(400).send('Authorization failed. Check the bot logs for details.');
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (!state || !code) {
|
|
151
|
-
res.status(400).send('Missing code or state.');
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const pending = pendingAuthorizations.get(state);
|
|
156
|
-
if (!pending) {
|
|
157
|
-
res.status(400).send('State no longer valid. Start a new authorization from Discord.');
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
clearTimeout(pending.timer);
|
|
162
|
-
pendingAuthorizations.delete(state);
|
|
163
|
-
|
|
164
|
-
try {
|
|
165
|
-
const tokenResponse = await startggHandler.exchangeToken(code, pending.codeVerifier, pending.scopes);
|
|
166
|
-
const bearer = BearerToken.fromOAuthResponse(tokenResponse);
|
|
167
|
-
console.log('[discordjs example] OAuth success for user', pending.userId, tokenResponse);
|
|
168
|
-
|
|
169
|
-
try {
|
|
170
|
-
const user = await client.users.fetch(pending.userId);
|
|
171
|
-
const expires = bearer.expiresAt ? new Date(bearer.expiresAt).toISOString() : 'unknown';
|
|
172
|
-
await user.send(
|
|
173
|
-
[
|
|
174
|
-
'✅ Start.gg authorization complete!',
|
|
175
|
-
`Access token (truncated): ${bearer.accessToken.slice(0, 8)}…`,
|
|
176
|
-
`Expires at: ${expires}`,
|
|
177
|
-
'Check the bot logs for the full token payload and handle it securely.',
|
|
178
|
-
].join('\n')
|
|
179
|
-
);
|
|
180
|
-
} catch (dmError) {
|
|
181
|
-
console.warn('[discordjs example] Unable to DM user with result', dmError);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
res.send('Authorization complete! Return to Discord to continue.');
|
|
185
|
-
} catch (exchangeError) {
|
|
186
|
-
console.error('[discordjs example] Token exchange failed', exchangeError);
|
|
187
|
-
res.status(500).send('Token exchange failed. Check the bot logs for details.');
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
async function main(): Promise<void> {
|
|
192
|
-
await registerSlashCommand();
|
|
193
|
-
await client.login(DISCORD_TOKEN);
|
|
194
|
-
app.listen(callbackPort, () => {
|
|
195
|
-
console.log(`[discordjs example] Listening for Start.gg callbacks on http://localhost:${callbackPort}${callbackPath}`);
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
main().catch(err => {
|
|
200
|
-
console.error('[discordjs example] Fatal startup error', err);
|
|
201
|
-
process.exit(1);
|
|
202
|
-
});
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
STARTGG_CLIENT_ID=YOUR_STARTGG_CLIENT_ID
|
|
2
|
-
# STARTGG_AUTH_ENDPOINT=https://api.start.gg/oauth/authorize
|
|
3
|
-
# STARTGG_TOKEN_ENDPOINT=https://api.start.gg/oauth/token
|
|
4
|
-
STARTGG_REDIRECT_URI=http://localhost:3000/api/startgg/callback
|
|
5
|
-
|
|
6
|
-
NEXT_PUBLIC_STARTGG_CLIENT_ID=YOUR_STARTGG_CLIENT_ID
|
|
7
|
-
NEXT_PUBLIC_STARTGG_REDIRECT_URI=http://localhost:3000/api/startgg/callback
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# Next.js OAuth Demo
|
|
2
|
-
|
|
3
|
-
This example integrates `startgg-oauth2-full` into a Next.js App Router project. It renders a page that generates a Start.gg authorize URL with PKCE, stores the verifier server-side, and exchanges the authorization code in an API route.
|
|
4
|
-
|
|
5
|
-
## Setup
|
|
6
|
-
```bash
|
|
7
|
-
cd examples/nextjs
|
|
8
|
-
cp .env.example .env.local
|
|
9
|
-
# Edit the file with your Start.gg client settings
|
|
10
|
-
npm install
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
Required values:
|
|
14
|
-
- `STARTGG_CLIENT_ID` – Start.gg OAuth client ID (public).
|
|
15
|
-
- `STARTGG_REDIRECT_URI` – defaults to `http://localhost:3000/api/startgg/callback`.
|
|
16
|
-
- Optional overrides: `STARTGG_AUTH_ENDPOINT`, `STARTGG_TOKEN_ENDPOINT`.
|
|
17
|
-
- `NEXT_PUBLIC_STARTGG_CLIENT_ID` mirrors the client ID for client-side display.
|
|
18
|
-
- `NEXT_PUBLIC_STARTGG_REDIRECT_URI` mirrors the redirect URI for helpful UI messaging.
|
|
19
|
-
|
|
20
|
-
## Development
|
|
21
|
-
```bash
|
|
22
|
-
npm run dev
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
Navigate to `http://localhost:3000`, click **Authorize with Start.gg**, and approve in the Start.gg window. The callback route exchanges the authorization code and displays the token payload for demo purposes.
|
|
26
|
-
|
|
27
|
-
## Production Build
|
|
28
|
-
```bash
|
|
29
|
-
npm run build
|
|
30
|
-
npm start
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
`npm run build` compiles the app, and `npm start` serves the production bundle. Adapt the in-memory PKCE store to persistent storage before deploying to serverless environments.
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { NextResponse } from 'next/server';
|
|
3
|
-
import { StartGGScope, buildAuthorizeUrl } from 'startgg-oauth2-full/src/auth/StartGGOAuth2';
|
|
4
|
-
import { startggConfig } from '../../../../lib/startgg';
|
|
5
|
-
import { savePending } from '../../../../lib/pendingStore';
|
|
6
|
-
|
|
7
|
-
export async function POST() {
|
|
8
|
-
try {
|
|
9
|
-
const state = randomUUID();
|
|
10
|
-
const scopes = [StartGGScope.USER_IDENTITY];
|
|
11
|
-
|
|
12
|
-
const { url, codeVerifier } = await buildAuthorizeUrl(startggConfig, {
|
|
13
|
-
scopes,
|
|
14
|
-
state,
|
|
15
|
-
prompt: 'consent',
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
savePending(state, { codeVerifier, scopes });
|
|
19
|
-
|
|
20
|
-
return NextResponse.json<AuthUrlResponse>({
|
|
21
|
-
url,
|
|
22
|
-
});
|
|
23
|
-
} catch (error) {
|
|
24
|
-
console.error('[nextjs example] Failed to generate authorize URL', error);
|
|
25
|
-
return NextResponse.json(
|
|
26
|
-
{
|
|
27
|
-
error: 'Unable to generate authorize URL. Check server logs for details.',
|
|
28
|
-
},
|
|
29
|
-
{ status: 500 }
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
type AuthUrlResponse = {
|
|
35
|
-
url: string;
|
|
36
|
-
};
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server';
|
|
2
|
-
import { BearerToken } from 'startgg-oauth2-full/src/auth/StartGGOAuth2';
|
|
3
|
-
import { consumePending } from '../../../../lib/pendingStore';
|
|
4
|
-
import { startggHandler } from '../../../../lib/startgg';
|
|
5
|
-
|
|
6
|
-
export async function GET(request: Request) {
|
|
7
|
-
const url = new URL(request.url);
|
|
8
|
-
const state = url.searchParams.get('state');
|
|
9
|
-
const code = url.searchParams.get('code');
|
|
10
|
-
const error = url.searchParams.get('error');
|
|
11
|
-
const errorDescription = url.searchParams.get('error_description');
|
|
12
|
-
|
|
13
|
-
if (error) {
|
|
14
|
-
console.warn('[nextjs example] Authorization error from Start.gg', error, errorDescription);
|
|
15
|
-
return NextResponse.json(
|
|
16
|
-
{ error, errorDescription: errorDescription ?? null },
|
|
17
|
-
{ status: 400 }
|
|
18
|
-
);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (!state || !code) {
|
|
22
|
-
return NextResponse.json(
|
|
23
|
-
{ error: 'Missing state or code.' },
|
|
24
|
-
{ status: 400 }
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const pending = consumePending(state);
|
|
29
|
-
if (!pending) {
|
|
30
|
-
return NextResponse.json(
|
|
31
|
-
{ error: 'State has expired or is invalid. Generate a new authorize URL.' },
|
|
32
|
-
{ status: 400 }
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
try {
|
|
37
|
-
const tokenResponse = await startggHandler.exchangeToken(code, pending.codeVerifier, pending.scopes);
|
|
38
|
-
const bearer = BearerToken.fromOAuthResponse(tokenResponse);
|
|
39
|
-
|
|
40
|
-
console.log('[nextjs example] Token response', tokenResponse);
|
|
41
|
-
|
|
42
|
-
return NextResponse.json({
|
|
43
|
-
message: 'Authorization complete!',
|
|
44
|
-
scope: tokenResponse.scope ?? null,
|
|
45
|
-
expiresIn: tokenResponse.expires_in ?? null,
|
|
46
|
-
accessTokenPreview: `${bearer.accessToken.slice(0, 8)}…`,
|
|
47
|
-
});
|
|
48
|
-
} catch (err) {
|
|
49
|
-
console.error('[nextjs example] Token exchange failed', err);
|
|
50
|
-
return NextResponse.json(
|
|
51
|
-
{ error: 'Token exchange failed. Inspect server logs for details.' },
|
|
52
|
-
{ status: 500 }
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
:root {
|
|
2
|
-
color-scheme: light dark;
|
|
3
|
-
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
4
|
-
line-height: 1.6;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
body {
|
|
8
|
-
margin: 0;
|
|
9
|
-
padding: 2rem;
|
|
10
|
-
background: #0f172a;
|
|
11
|
-
color: #e2e8f0;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
a {
|
|
15
|
-
color: #38bdf8;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
button {
|
|
19
|
-
cursor: pointer;
|
|
20
|
-
border: none;
|
|
21
|
-
background: #38bdf8;
|
|
22
|
-
color: #0f172a;
|
|
23
|
-
padding: 0.75rem 1.5rem;
|
|
24
|
-
border-radius: 999px;
|
|
25
|
-
font-weight: 600;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
button:disabled {
|
|
29
|
-
cursor: progress;
|
|
30
|
-
opacity: 0.7;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
main {
|
|
34
|
-
max-width: 720px;
|
|
35
|
-
margin: 0 auto;
|
|
36
|
-
background: rgba(15, 23, 42, 0.6);
|
|
37
|
-
border-radius: 16px;
|
|
38
|
-
padding: 2rem;
|
|
39
|
-
box-shadow: 0 20px 50px rgba(15, 23, 42, 0.6);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
pre {
|
|
43
|
-
padding: 1rem;
|
|
44
|
-
background: rgba(15, 23, 42, 0.9);
|
|
45
|
-
border-radius: 12px;
|
|
46
|
-
overflow-x: auto;
|
|
47
|
-
word-break: break-all;
|
|
48
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { Metadata } from 'next';
|
|
2
|
-
import './globals.css';
|
|
3
|
-
|
|
4
|
-
export const metadata: Metadata = {
|
|
5
|
-
title: 'Start.gg OAuth2 × Next.js Demo',
|
|
6
|
-
description: 'Demonstrates PKCE with startgg-oauth2-full inside a Next.js App Router project.',
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
10
|
-
return (
|
|
11
|
-
<html lang="en">
|
|
12
|
-
<body>{children}</body>
|
|
13
|
-
</html>
|
|
14
|
-
);
|
|
15
|
-
}
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState } from 'react';
|
|
4
|
-
|
|
5
|
-
type AuthResponse = {
|
|
6
|
-
url: string;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
export default function HomePage() {
|
|
10
|
-
const [authUrl, setAuthUrl] = useState<string | null>(null);
|
|
11
|
-
const [error, setError] = useState<string | null>(null);
|
|
12
|
-
const [loading, setLoading] = useState(false);
|
|
13
|
-
|
|
14
|
-
const clientId = process.env.NEXT_PUBLIC_STARTGG_CLIENT_ID ?? 'unset';
|
|
15
|
-
const redirectUri = process.env.NEXT_PUBLIC_STARTGG_REDIRECT_URI ?? 'http://localhost:3000/api/startgg/callback';
|
|
16
|
-
|
|
17
|
-
async function handleAuthorize() {
|
|
18
|
-
setLoading(true);
|
|
19
|
-
setError(null);
|
|
20
|
-
setAuthUrl(null);
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
const res = await fetch('/api/startgg/auth-url', { method: 'POST' });
|
|
24
|
-
if (!res.ok) throw new Error(`Request failed (${res.status})`);
|
|
25
|
-
const data = (await res.json()) as AuthResponse;
|
|
26
|
-
setAuthUrl(data.url);
|
|
27
|
-
} catch (err) {
|
|
28
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
29
|
-
setError(message);
|
|
30
|
-
} finally {
|
|
31
|
-
setLoading(false);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return (
|
|
36
|
-
<main>
|
|
37
|
-
<h1>Start.gg OAuth2 + Next.js</h1>
|
|
38
|
-
<p>
|
|
39
|
-
This demo calls <code>buildAuthorizeUrl</code> on the server, stores the PKCE verifier in memory, and exchanges
|
|
40
|
-
the authorization code in the callback API route. Update <code>.env.local</code> with your Start.gg client
|
|
41
|
-
values before proceeding.
|
|
42
|
-
</p>
|
|
43
|
-
|
|
44
|
-
<section>
|
|
45
|
-
<h2>Environment</h2>
|
|
46
|
-
<ul>
|
|
47
|
-
<li>
|
|
48
|
-
<strong>Client ID:</strong> <code>{clientId}</code>
|
|
49
|
-
</li>
|
|
50
|
-
<li>
|
|
51
|
-
<strong>Redirect URI:</strong> <code>{redirectUri}</code>
|
|
52
|
-
</li>
|
|
53
|
-
</ul>
|
|
54
|
-
</section>
|
|
55
|
-
|
|
56
|
-
<section>
|
|
57
|
-
<h2>Authorize</h2>
|
|
58
|
-
<p>
|
|
59
|
-
Click the button to request a Start.gg authorize URL. The link appears below so you can inspect it before
|
|
60
|
-
visiting. After finishing the Start.gg flow you'll be redirected back here and the callback will exchange
|
|
61
|
-
the code and show a success message.
|
|
62
|
-
</p>
|
|
63
|
-
<button onClick={handleAuthorize} disabled={loading}>
|
|
64
|
-
{loading ? 'Generating…' : 'Authorize with Start.gg'}
|
|
65
|
-
</button>
|
|
66
|
-
{error && (
|
|
67
|
-
<p style={{ color: '#f87171', marginTop: '1rem' }}>
|
|
68
|
-
Failed to request authorize URL: <code>{error}</code>
|
|
69
|
-
</p>
|
|
70
|
-
)}
|
|
71
|
-
{authUrl && (
|
|
72
|
-
<div style={{ marginTop: '1.5rem' }}>
|
|
73
|
-
<p>Open the link below in a new tab:</p>
|
|
74
|
-
<pre>{authUrl}</pre>
|
|
75
|
-
<p>
|
|
76
|
-
<a href={authUrl} target="_blank" rel="noreferrer">
|
|
77
|
-
Launch Start.gg OAuth
|
|
78
|
-
</a>
|
|
79
|
-
</p>
|
|
80
|
-
</div>
|
|
81
|
-
)}
|
|
82
|
-
</section>
|
|
83
|
-
|
|
84
|
-
<section>
|
|
85
|
-
<h2>Callback Output</h2>
|
|
86
|
-
<p>
|
|
87
|
-
The callback endpoint responds with a JSON summary. Keep an eye on the terminal running <code>npm run dev</code>{' '}
|
|
88
|
-
to see the raw token payload. Do not ship the in-memory verifier store or plaintext token output to production.
|
|
89
|
-
</p>
|
|
90
|
-
</section>
|
|
91
|
-
</main>
|
|
92
|
-
);
|
|
93
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import type { StartGGScope } from 'startgg-oauth2-full/src/auth/StartGGOAuth2';
|
|
2
|
-
|
|
3
|
-
type PendingEntry = {
|
|
4
|
-
codeVerifier: string;
|
|
5
|
-
scopes: StartGGScope[];
|
|
6
|
-
createdAt: number;
|
|
7
|
-
timeout: NodeJS.Timeout;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
const TTL_MS = 10 * 60_000;
|
|
11
|
-
const store = new Map<string, PendingEntry>();
|
|
12
|
-
|
|
13
|
-
export function savePending(state: string, data: { codeVerifier: string; scopes: StartGGScope[] }) {
|
|
14
|
-
clearPending(state);
|
|
15
|
-
|
|
16
|
-
const timeout = setTimeout(() => {
|
|
17
|
-
store.delete(state);
|
|
18
|
-
}, TTL_MS);
|
|
19
|
-
|
|
20
|
-
store.set(state, { ...data, createdAt: Date.now(), timeout });
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function consumePending(state: string) {
|
|
24
|
-
const entry = store.get(state);
|
|
25
|
-
if (!entry) return null;
|
|
26
|
-
clearTimeout(entry.timeout);
|
|
27
|
-
store.delete(state);
|
|
28
|
-
return entry;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function clearPending(state: string) {
|
|
32
|
-
const current = store.get(state);
|
|
33
|
-
if (current) {
|
|
34
|
-
clearTimeout(current.timeout);
|
|
35
|
-
store.delete(state);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { createStartGGAuth2Handler } from 'startgg-oauth2-full/src/auth/StartGGOAuth2';
|
|
2
|
-
|
|
3
|
-
function readEnv(name: string, fallback?: string): string {
|
|
4
|
-
const value = process.env[name] ?? fallback;
|
|
5
|
-
if (!value) {
|
|
6
|
-
throw new Error(`[nextjs example] Missing required environment variable: ${name}`);
|
|
7
|
-
}
|
|
8
|
-
return value;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const clientId = readEnv('STARTGG_CLIENT_ID', process.env.NEXT_PUBLIC_STARTGG_CLIENT_ID);
|
|
12
|
-
const authEndpoint = readEnv('STARTGG_AUTH_ENDPOINT', 'https://api.start.gg/oauth/authorize');
|
|
13
|
-
const tokenEndpoint = readEnv('STARTGG_TOKEN_ENDPOINT', 'https://api.start.gg/oauth/token');
|
|
14
|
-
const redirectUri = readEnv('STARTGG_REDIRECT_URI', 'http://localhost:3000/api/startgg/callback');
|
|
15
|
-
|
|
16
|
-
export const startggConfig = {
|
|
17
|
-
clientId,
|
|
18
|
-
authEndpoint,
|
|
19
|
-
tokenEndpoint,
|
|
20
|
-
redirectUri,
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export const startggHandler = createStartGGAuth2Handler({
|
|
24
|
-
clientId,
|
|
25
|
-
authEndpoint,
|
|
26
|
-
tokenEndpoint,
|
|
27
|
-
redirectUri,
|
|
28
|
-
});
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "startgg-oauth2-nextjs-example",
|
|
3
|
-
"version": "0.0.0",
|
|
4
|
-
"private": true,
|
|
5
|
-
"scripts": {
|
|
6
|
-
"dev": "next dev",
|
|
7
|
-
"build": "next build",
|
|
8
|
-
"start": "next start",
|
|
9
|
-
"lint": "next lint"
|
|
10
|
-
},
|
|
11
|
-
"dependencies": {
|
|
12
|
-
"next": "14.2.13",
|
|
13
|
-
"react": "18.2.0",
|
|
14
|
-
"react-dom": "18.2.0",
|
|
15
|
-
"startgg-oauth2-full": "file:../.."
|
|
16
|
-
},
|
|
17
|
-
"devDependencies": {
|
|
18
|
-
"@types/node": "^22.7.5",
|
|
19
|
-
"@types/react": "^18.2.45",
|
|
20
|
-
"@types/react-dom": "^18.2.18",
|
|
21
|
-
"eslint": "^8.57.1",
|
|
22
|
-
"eslint-config-next": "14.2.13",
|
|
23
|
-
"typescript": "^5.6.3"
|
|
24
|
-
}
|
|
25
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "es2017",
|
|
4
|
-
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
-
"allowJs": true,
|
|
6
|
-
"skipLibCheck": true,
|
|
7
|
-
"strict": true,
|
|
8
|
-
"forceConsistentCasingInFileNames": true,
|
|
9
|
-
"noEmit": true,
|
|
10
|
-
"esModuleInterop": true,
|
|
11
|
-
"module": "esnext",
|
|
12
|
-
"moduleResolution": "bundler",
|
|
13
|
-
"resolveJsonModule": true,
|
|
14
|
-
"isolatedModules": true,
|
|
15
|
-
"jsx": "preserve",
|
|
16
|
-
"incremental": true,
|
|
17
|
-
"types": ["node"]
|
|
18
|
-
},
|
|
19
|
-
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
20
|
-
"exclude": ["node_modules"]
|
|
21
|
-
}
|
package/examples/node/README.md
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
# Node CLI Demo
|
|
2
|
-
|
|
3
|
-
This example demonstrates how to use the Start.gg OAuth helpers from a plain Node.js environment. It includes two scripts:
|
|
4
|
-
|
|
5
|
-
- `npm run dev` — prints an authorize URL and is designed for quick, manual testing.
|
|
6
|
-
- `npm run oauth-server` — hosts a callback server, opens the authorize URL in your browser, exchanges the code automatically, and logs masked tokens.
|
|
7
|
-
|
|
8
|
-
## Setup
|
|
9
|
-
```bash
|
|
10
|
-
cd examples/node
|
|
11
|
-
cp .env.example .env # optional
|
|
12
|
-
npm install
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
Populate `STARTGG_CLIENT_ID` in `.env` or your shell. Override the endpoints with `STARTGG_AUTH_URL` and `STARTGG_TOKEN_URL` if you target a non-production Start.gg environment. `PORT` controls the callback server for `npm run oauth-server` (default `3000`).
|
|
16
|
-
|
|
17
|
-
## Manual Flow (`npm run dev`)
|
|
18
|
-
1. Run the script to generate an authorize URL.
|
|
19
|
-
2. Visit the URL, approve Start.gg, and capture the `code` from the redirected address.
|
|
20
|
-
3. Paste the code into the placeholder in `src/index.ts` (or extend the script to prompt for it) to exchange tokens.
|
|
21
|
-
|
|
22
|
-
## Browser-Assisted Flow (`npm run oauth-server`)
|
|
23
|
-
1. Run the script; it starts an HTTP listener on `http://localhost:3000/callback`.
|
|
24
|
-
2. Your default browser opens to Start.gg. Complete the OAuth consent.
|
|
25
|
-
3. The callback handler exchanges the code using `exchangeToken`, logs masked credentials, and exits the process.
|
|
26
|
-
|
|
27
|
-
Persist tokens securely when adapting this approach to production services.
|