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.
Files changed (60) hide show
  1. package/README.md +325 -51
  2. package/dist/auth/StartGGOAuth2.d.ts +85 -0
  3. package/dist/auth/StartGGOAuth2.js +306 -0
  4. package/dist/constants.d.ts +10 -0
  5. package/dist/constants.js +16 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +2 -0
  8. package/package.json +63 -22
  9. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -18
  10. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -13
  11. package/.github/pull_request_template.md +0 -30
  12. package/.github/workflows/ci.yml +0 -23
  13. package/.github/workflows/release.yml +0 -60
  14. package/AGENTS.md +0 -46
  15. package/CONTRIBUTING.md +0 -36
  16. package/STARTGG_OAUTH_SETUP.md +0 -41
  17. package/__tests__/authorize-url.test.ts +0 -62
  18. package/__tests__/bearer-token.test.ts +0 -21
  19. package/__tests__/handler.test.ts +0 -111
  20. package/__tests__/pkce.test.ts +0 -17
  21. package/examples/browser/README.md +0 -20
  22. package/examples/browser/index.html +0 -55
  23. package/examples/browser/package.json +0 -17
  24. package/examples/browser/src/main.ts +0 -105
  25. package/examples/browser/tsconfig.json +0 -11
  26. package/examples/browser/vite.config.ts +0 -8
  27. package/examples/discordjs/.env.example +0 -9
  28. package/examples/discordjs/README.md +0 -36
  29. package/examples/discordjs/package.json +0 -23
  30. package/examples/discordjs/src/bot.ts +0 -202
  31. package/examples/discordjs/tsconfig.json +0 -12
  32. package/examples/nextjs/.env.example +0 -7
  33. package/examples/nextjs/README.md +0 -33
  34. package/examples/nextjs/app/api/startgg/auth-url/route.ts +0 -36
  35. package/examples/nextjs/app/api/startgg/callback/route.ts +0 -55
  36. package/examples/nextjs/app/globals.css +0 -48
  37. package/examples/nextjs/app/layout.tsx +0 -15
  38. package/examples/nextjs/app/page.tsx +0 -93
  39. package/examples/nextjs/lib/pendingStore.ts +0 -37
  40. package/examples/nextjs/lib/startgg.ts +0 -28
  41. package/examples/nextjs/next-env.d.ts +0 -5
  42. package/examples/nextjs/next.config.mjs +0 -6
  43. package/examples/nextjs/package.json +0 -25
  44. package/examples/nextjs/tsconfig.json +0 -21
  45. package/examples/node/.env.example +0 -4
  46. package/examples/node/README.md +0 -27
  47. package/examples/node/package.json +0 -17
  48. package/examples/node/src/index.ts +0 -57
  49. package/examples/node/src/server.ts +0 -120
  50. package/examples/node/tsconfig.json +0 -12
  51. package/examples/vite/README.md +0 -22
  52. package/examples/vite/index.html +0 -41
  53. package/examples/vite/package.json +0 -18
  54. package/examples/vite/src/main.ts +0 -38
  55. package/examples/vite/tsconfig.json +0 -11
  56. package/examples/vite/vite.config.ts +0 -8
  57. package/jest.config.ts +0 -15
  58. package/jest.setup.ts +0 -29
  59. package/src/auth/StartGGOAuth2.ts +0 -378
  60. package/tsconfig.json +0 -27
@@ -0,0 +1,306 @@
1
+ import { STARTGG_ENDPOINTS } from "../constants";
2
+ // Full RFC-compliant implementation
3
+ // of OAuth2 Authorization Code Flow with PKCE (RFC 6749, RFC 7636)
4
+ // for Start.gg API (https://start.gg/docs/oauth2).
5
+ // Happy to accept PRs for improvements or fixes!
6
+ // (c) 2026 0xabadbabe (jet'aime), Inc. (MIT License)
7
+ // Happy coding! <3
8
+ export var StartGGScope;
9
+ (function (StartGGScope) {
10
+ StartGGScope["USER_IDENTITY"] = "user.identity";
11
+ StartGGScope["USER_EMAIL"] = "user.email";
12
+ StartGGScope["TOURNAMENT_MANAGER"] = "tournament.manager";
13
+ StartGGScope["TOURNAMENT_REPORTER"] = "tournament.reporter";
14
+ })(StartGGScope || (StartGGScope = {}));
15
+ export class OAuth2Error extends Error {
16
+ code;
17
+ details;
18
+ constructor(message, code, details) {
19
+ super(message);
20
+ this.name = "OAuth2Error";
21
+ this.code = code;
22
+ this.details = details;
23
+ }
24
+ }
25
+ export class ScopeValidationError extends OAuth2Error {
26
+ requestedScopes;
27
+ grantedScopes;
28
+ constructor(message, requestedScopes, grantedScopes) {
29
+ super(message, "SCOPE_VALIDATION_FAILED");
30
+ this.requestedScopes = requestedScopes;
31
+ this.grantedScopes = grantedScopes;
32
+ }
33
+ }
34
+ /** WebCrypto subtle (browser + Node >=18). */
35
+ async function getSubtleCrypto() {
36
+ const g = globalThis;
37
+ if (g.crypto?.subtle)
38
+ return g.crypto.subtle;
39
+ throw new OAuth2Error("WebCrypto subtle not available; required for PKCE S256", "CRYPTO_UNAVAILABLE");
40
+ }
41
+ /** Base64 (Buffer if available, else btoa path). */
42
+ function base64Encode(bytes) {
43
+ const g = globalThis;
44
+ if (typeof g.Buffer?.from === "function")
45
+ return g.Buffer.from(bytes).toString("base64");
46
+ let binary = "";
47
+ const chunk = 0x8000;
48
+ for (let i = 0; i < bytes.length; i += chunk)
49
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
50
+ if (typeof g.btoa !== "function")
51
+ throw new OAuth2Error("btoa not available for base64 encoding", "B64_UNAVAILABLE");
52
+ return g.btoa(binary);
53
+ }
54
+ /** URL-safe Base64 (no padding). */
55
+ function base64Url(bytes) {
56
+ const u8 = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
57
+ return base64Encode(u8)
58
+ .replace(/\+/g, "-")
59
+ .replace(/\//g, "_")
60
+ .replace(/=+$/g, "");
61
+ }
62
+ /** Random bytes using WebCrypto. */
63
+ function getRandomBytes(length) {
64
+ const g = globalThis;
65
+ if (!g.crypto?.getRandomValues)
66
+ throw new OAuth2Error("crypto.getRandomValues unavailable", "RNG_UNAVAILABLE");
67
+ const bytes = new Uint8Array(length);
68
+ g.crypto.getRandomValues(bytes);
69
+ return bytes;
70
+ }
71
+ /** x-www-form-urlencoded body. */
72
+ function formBody(params) {
73
+ const sp = new URLSearchParams();
74
+ for (const [k, v] of Object.entries(params))
75
+ sp.set(k, v);
76
+ return sp.toString();
77
+ }
78
+ /** Parse non-JSON error bodies safely. */
79
+ async function parseJsonSafe(res) {
80
+ const txt = await res.text();
81
+ try {
82
+ return JSON.parse(txt);
83
+ }
84
+ catch {
85
+ return { raw: txt };
86
+ }
87
+ }
88
+ /** Validate token response + Bearer type. */
89
+ function validateTokenResponse(tr) {
90
+ if (!tr || typeof tr !== "object")
91
+ throw new OAuth2Error("Invalid token response shape", "INVALID_TOKEN_RESPONSE", tr);
92
+ if (!tr.access_token)
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);
96
+ }
97
+ }
98
+ /** If scope omitted, assume unchanged (RFC 6749). */
99
+ function validateScopesOrAssumePrevious(responseScope, requiredScopes) {
100
+ if (!requiredScopes.length)
101
+ return;
102
+ if (responseScope == null)
103
+ return;
104
+ const granted = new Set(responseScope.split(/\s+/).filter(Boolean));
105
+ const missing = requiredScopes.filter((s) => !granted.has(s));
106
+ if (missing.length > 0) {
107
+ throw new ScopeValidationError(`Missing required scopes: ${missing.join(", ")}`, requiredScopes, Array.from(granted));
108
+ }
109
+ }
110
+ /** Fetch with timeout to avoid hangs. */
111
+ async function fetchWithTimeout(input, init = {}) {
112
+ const { timeoutMs = 15000, ...rest } = init;
113
+ const ac = new AbortController();
114
+ const id = setTimeout(() => ac.abort(), timeoutMs);
115
+ try {
116
+ return await fetch(input, { ...rest, signal: ac.signal });
117
+ }
118
+ finally {
119
+ clearTimeout(id);
120
+ }
121
+ }
122
+ // -------- PKCE (RFC 7636) --------
123
+ const PKCE_VERIFIER_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
124
+ export function generateCodeVerifier(length = 64) {
125
+ const len = Math.min(Math.max(length, 43), 128);
126
+ const alphabet = PKCE_VERIFIER_CHARS;
127
+ const alphabetLength = alphabet.length;
128
+ const maxValue = Math.floor(256 / alphabetLength) * alphabetLength;
129
+ const result = [];
130
+ while (result.length < len) {
131
+ const randomBytes = getRandomBytes(len - result.length);
132
+ for (const byte of randomBytes) {
133
+ if (byte >= maxValue)
134
+ continue; // Skip to avoid modulo bias.
135
+ result.push(alphabet.charAt(byte % alphabetLength));
136
+ if (result.length === len)
137
+ break;
138
+ }
139
+ }
140
+ return result.join("");
141
+ }
142
+ /** S256 challenge for a verifier. */
143
+ export async function computeCodeChallengeS256(codeVerifier) {
144
+ const enc = new TextEncoder();
145
+ const subtle = await getSubtleCrypto();
146
+ const hash = await subtle.digest("SHA-256", enc.encode(codeVerifier));
147
+ return base64Url(hash);
148
+ }
149
+ /** Build an authorization URL with PKCE (S256). */
150
+ export async function buildAuthorizeUrl(cfg, opts) {
151
+ let codeVerifier;
152
+ let codeChallenge;
153
+ if (opts.codeVerifier && opts.codeChallenge) {
154
+ const expectedChallenge = await computeCodeChallengeS256(opts.codeVerifier);
155
+ if (expectedChallenge !== opts.codeChallenge) {
156
+ throw new OAuth2Error("Provided codeChallenge does not match codeVerifier", "INVALID_PKCE_PAIR");
157
+ }
158
+ codeVerifier = opts.codeVerifier;
159
+ codeChallenge = opts.codeChallenge;
160
+ }
161
+ else if (opts.codeVerifier) {
162
+ codeVerifier = opts.codeVerifier;
163
+ codeChallenge = await computeCodeChallengeS256(codeVerifier);
164
+ }
165
+ else if (opts.codeChallenge) {
166
+ throw new OAuth2Error("codeVerifier is required when providing codeChallenge", "INVALID_PKCE_PAIR");
167
+ }
168
+ else {
169
+ codeVerifier = generateCodeVerifier();
170
+ codeChallenge = await computeCodeChallengeS256(codeVerifier);
171
+ }
172
+ const u = new URL(cfg.authEndpoint);
173
+ const params = {
174
+ response_type: "code",
175
+ client_id: cfg.clientId,
176
+ redirect_uri: cfg.redirectUri,
177
+ scope: opts.scopes.map(String).join(" "),
178
+ code_challenge: codeChallenge,
179
+ code_challenge_method: "S256",
180
+ };
181
+ if (opts.state)
182
+ params.state = opts.state;
183
+ if (opts.prompt)
184
+ params.prompt = opts.prompt;
185
+ if (opts.extras) {
186
+ for (const [k, v] of Object.entries(opts.extras))
187
+ if (v != null)
188
+ params[k] = String(v);
189
+ }
190
+ for (const [k, v] of Object.entries(params))
191
+ u.searchParams.set(k, v);
192
+ return { url: u.toString(), codeVerifier, codeChallenge };
193
+ }
194
+ // -------- Bearer Token Helper (RFC 6750) --------
195
+ export class BearerToken {
196
+ accessToken;
197
+ tokenType;
198
+ refreshToken;
199
+ expiresAt; // epoch ms
200
+ constructor(init) {
201
+ this.accessToken = init.accessToken;
202
+ this.tokenType = init.tokenType;
203
+ this.refreshToken = init.refreshToken;
204
+ this.expiresAt = init.expiresAt;
205
+ }
206
+ static fromOAuthResponse(res, nowMs = Date.now(), skewSeconds = 60) {
207
+ validateTokenResponse(res);
208
+ const expiresAt = typeof res.expires_in === "number"
209
+ ? nowMs +
210
+ Math.max(0, (res.expires_in - Math.max(0, skewSeconds)) * 1000)
211
+ : undefined;
212
+ return new BearerToken({
213
+ accessToken: res.access_token,
214
+ tokenType: "Bearer",
215
+ refreshToken: res.refresh_token,
216
+ expiresAt,
217
+ });
218
+ }
219
+ isExpired(nowMs = Date.now()) {
220
+ if (this.expiresAt == null)
221
+ return false; // unknown → treat as non-expiring
222
+ return nowMs >= this.expiresAt;
223
+ }
224
+ willExpireWithin(seconds, nowMs = Date.now()) {
225
+ if (this.expiresAt == null)
226
+ return false;
227
+ return this.expiresAt - nowMs <= seconds * 1000;
228
+ }
229
+ toAuthHeader() {
230
+ return { Authorization: `Bearer ${this.accessToken}` };
231
+ }
232
+ assertUsable(nowMs = Date.now()) {
233
+ if (this.isExpired(nowMs))
234
+ throw new OAuth2Error("Access token expired", "TOKEN_EXPIRED");
235
+ }
236
+ }
237
+ // -------- Handler (RFC 6749 §4.1.3, §6) --------
238
+ export class StartGGOAuth2Handler {
239
+ config;
240
+ constructor(config) {
241
+ this.config = config;
242
+ }
243
+ /** Exchange authorization code for tokens (PKCE). */
244
+ async exchangeToken(code, codeVerifier, expectedScopes) {
245
+ const tokenRequest = {
246
+ grant_type: "authorization_code",
247
+ code,
248
+ redirect_uri: this.config.redirectUri,
249
+ code_verifier: codeVerifier,
250
+ client_id: this.config.clientId,
251
+ };
252
+ const res = await fetchWithTimeout(this.config.tokenEndpoint, {
253
+ method: "POST",
254
+ headers: {
255
+ "Content-Type": "application/x-www-form-urlencoded",
256
+ Accept: "application/json",
257
+ },
258
+ body: formBody(tokenRequest),
259
+ timeoutMs: this.config.fetchTimeoutMs ?? 15000,
260
+ });
261
+ if (!res.ok) {
262
+ const details = await parseJsonSafe(res);
263
+ throw new OAuth2Error("Token exchange failed", "TOKEN_EXCHANGE_FAILED", details);
264
+ }
265
+ const tokenResponse = (await res.json());
266
+ validateTokenResponse(tokenResponse);
267
+ validateScopesOrAssumePrevious(tokenResponse.scope, expectedScopes.map(String));
268
+ return tokenResponse;
269
+ }
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. */
272
+ async refreshToken(refreshToken, originalScopes) {
273
+ const refreshRequest = {
274
+ grant_type: "refresh_token",
275
+ refresh_token: refreshToken,
276
+ client_id: this.config.clientId,
277
+ };
278
+ const res = await fetchWithTimeout(this.config.tokenEndpoint, {
279
+ method: "POST",
280
+ headers: {
281
+ "Content-Type": "application/x-www-form-urlencoded",
282
+ Accept: "application/json",
283
+ },
284
+ body: formBody(Object.fromEntries(Object.entries(refreshRequest).filter(([, v]) => v != null))),
285
+ timeoutMs: this.config.fetchTimeoutMs ?? 15000,
286
+ });
287
+ if (!res.ok) {
288
+ const details = await parseJsonSafe(res);
289
+ throw new OAuth2Error("Token refresh failed", "TOKEN_REFRESH_FAILED", details);
290
+ }
291
+ const tokenResponse = (await res.json());
292
+ validateTokenResponse(tokenResponse);
293
+ validateScopesOrAssumePrevious(tokenResponse.scope, originalScopes.map(String));
294
+ if (!tokenResponse.refresh_token)
295
+ tokenResponse.refresh_token = refreshToken;
296
+ return tokenResponse;
297
+ }
298
+ }
299
+ /** Factory */
300
+ export function createStartGGAuth2Handler(params) {
301
+ return new StartGGOAuth2Handler({
302
+ ...params,
303
+ authEndpoint: params.authEndpoint ?? STARTGG_ENDPOINTS.authorize,
304
+ tokenEndpoint: params.tokenEndpoint ?? STARTGG_ENDPOINTS.token,
305
+ });
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
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./auth/StartGGOAuth2.js";
2
+ export * from "./constants.js";
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./auth/StartGGOAuth2.js";
2
+ export * from "./constants.js";
package/package.json CHANGED
@@ -1,24 +1,65 @@
1
1
  {
2
- "name": "startgg-oauth2-full",
3
- "version": "0.2.0",
4
- "type": "module",
5
- "license": "MIT",
6
- "scripts": {
7
- "dev:browser": "serve examples/browser -p 5174",
8
- "dev:node": "tsx examples/node/index.ts",
9
- "dev:node:server": "tsx examples/node/server.ts",
10
- "test": "jest --runInBand",
11
- "build": "tsc -p tsconfig.json"
12
- },
13
- "devDependencies": {
14
- "@types/jest": "^29.5.12",
15
- "@types/node": "^22.7.5",
16
- "jest": "^29.7.0",
17
- "node-fetch": "^3.3.2",
18
- "serve": "^14.2.1",
19
- "ts-jest": "^29.2.5",
20
- "ts-node": "10.9.2",
21
- "tsx": "^4.19.0",
22
- "typescript": "^5.6.3"
23
- }
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
+ }
24
65
  }
@@ -1,18 +0,0 @@
1
- ---
2
- name: Bug report
3
- about: Report a bug
4
- ---
5
-
6
- **Describe the bug**
7
- …
8
-
9
- **Repro**
10
- 1. …
11
- 2. …
12
-
13
- **Expected**
14
- …
15
-
16
- **Env**
17
- - Node:
18
- - Browser:
@@ -1,13 +0,0 @@
1
- ---
2
- name: Feature request
3
- about: Suggest an idea
4
- ---
5
-
6
- **Problem**
7
- …
8
-
9
- **Proposal**
10
- …
11
-
12
- **Alternatives**
13
- …
@@ -1,30 +0,0 @@
1
- # Pull Request
2
-
3
- ## Summary
4
- Explain the change concisely. What does this PR do and why?
5
-
6
- ## Type
7
- - [ ] feat (new feature)
8
- - [ ] fix (bug fix)
9
- - [ ] refactor (no functional change)
10
- - [ ] docs (docs only)
11
- - [ ] test (tests only)
12
- - [ ] chore (infrastructure/CI/build)
13
-
14
- ## Checklist
15
- - [ ] I ran `npm run build` successfully.
16
- - [ ] I ran `npm test` successfully and added/updated tests where behavior changed.
17
- - [ ] I updated README/docs if public API or flows changed.
18
- - [ ] I did not commit secrets/tokens and scrubbed logs.
19
-
20
- ## Breaking Changes
21
- - [ ] Yes — describe impact and migration steps below
22
- - [ ] No
23
-
24
- If **Yes**, detail migration notes:
25
-
26
- ## Screenshots / Demos (if applicable)
27
- Paste or link minimal repro or demo outputs.
28
-
29
- ## Related Issues
30
- Link to issues (e.g., #123).
@@ -1,23 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- branches: [ main, master ]
6
- pull_request:
7
- branches: [ main, master ]
8
-
9
- jobs:
10
- build-and-test:
11
- runs-on: ubuntu-latest
12
- strategy:
13
- matrix:
14
- node-version: [18.x, 20.x]
15
- steps:
16
- - uses: actions/checkout@v4
17
- - uses: actions/setup-node@v4
18
- with:
19
- node-version: ${{ matrix.node-version }}
20
- cache: 'npm'
21
- - run: npm ci
22
- - run: npm run build
23
- - run: npm test -- --ci
@@ -1,60 +0,0 @@
1
- name: Release Publish
2
-
3
- on:
4
- release:
5
- types: [published]
6
- workflow_dispatch:
7
-
8
- jobs:
9
- publish:
10
- name: Publish to npm
11
- if: github.event.release.draft == false && github.event.release.prerelease == false || github.event_name == 'workflow_dispatch'
12
- runs-on: ubuntu-latest
13
- permissions:
14
- contents: read
15
- packages: write
16
- steps:
17
- - name: Checkout
18
- uses: actions/checkout@v4
19
-
20
- - name: Setup Node
21
- uses: actions/setup-node@v4
22
- with:
23
- node-version: 20
24
- registry-url: https://registry.npmjs.org
25
- cache: npm
26
-
27
- - name: Install dependencies
28
- run: npm ci
29
-
30
- - name: Verify tag matches package version
31
- if: github.event_name == 'release'
32
- run: |
33
- TAG="${{ github.event.release.tag_name }}"
34
- PKG_VERSION=$(node -p "require('./package.json').version")
35
- if [ "$TAG" != "v$PKG_VERSION" ]; then
36
- echo "Release tag $TAG does not match package version $PKG_VERSION"
37
- exit 1
38
- fi
39
-
40
- - name: Build
41
- run: npm run build
42
-
43
- - name: Test
44
- run: npm test -- --ci
45
-
46
- - name: Publish
47
- run: npm publish --access public
48
- env:
49
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
50
-
51
- - name: Publish to GitHub Packages
52
- env:
53
- NODE_AUTH_TOKEN: ${{ github.token }}
54
- run: |
55
- ORIGINAL_NAME=$(node -p "require('./package.json').name")
56
- OWNER_SCOPE=$(echo "${GITHUB_REPOSITORY_OWNER}" | tr '[:upper:]' '[:lower:]')
57
- SCOPED_NAME="@${OWNER_SCOPE}/startgg-oauth2-full"
58
- npm pkg set name="$SCOPED_NAME"
59
- npm publish --registry https://npm.pkg.github.com
60
- npm pkg set name="$ORIGINAL_NAME"
package/AGENTS.md DELETED
@@ -1,46 +0,0 @@
1
- # Repository Guidelines
2
-
3
- ## Project Structure & Module Organization
4
- - `src/` contains the TypeScript OAuth2 helpers (PKCE generation, token exchange, authorize URL builder).
5
- - `__tests__/` mirrors the source with Jest specs (e.g., `pkce.test.ts`, `authorize-url.test.ts`).
6
- - `examples/` includes Node, server, and browser demos; use them to validate end-to-end flows.
7
- - Root config (`tsconfig.json`, `jest.config.ts`, `package.json`) defines tooling standards—change only with consensus.
8
-
9
- ## Build, Test, and Development Commands
10
- - `npm run build` compiles TypeScript via `tsc`; run before opening a PR.
11
- - `npm test` runs the full Jest suite; add `-- pkce` (or similar) to target individual specs.
12
- - `npm run dev:node`, `npm run dev:node:server`, and `npm run dev:browser` start interactive demos for manual OAuth verification.
13
-
14
- ## Coding Style & Naming Conventions
15
- - Write modern TypeScript (ES2022 target) with strict null checks enabled in `tsconfig.json`.
16
- - Use two-space indentation, semicolons, and single quotes to match the existing codebase.
17
- - Functions and variables use `camelCase`, classes and enums use `PascalCase`, constants use `SCREAMING_SNAKE_CASE` where appropriate.
18
- - Prefer small, self-documenting helpers; add concise comments only when logic is non-obvious.
19
-
20
- ## Testing Guidelines
21
- - Jest with `ts-jest` handles unit tests; new behaviour requires corresponding specs in `__tests__/`.
22
- - Name test files after the module under test (e.g., `foo.test.ts`); keep describe blocks aligned with exported functions.
23
- - Ensure deterministic randomness in tests (stub RNGs) and cover error cases like PKCE validation failures.
24
-
25
- ## Commit & Pull Request Guidelines
26
- - Craft commits in the form `type(scope): summary` (examples in history: `fix(pkce): cap verifier length`).
27
- - Each PR should explain the change, list validation steps (e.g., `npm test` output), and link related issues or tickets.
28
- - Include screenshots or curl transcripts when work affects interactive flows or API responses.
29
-
30
- ## Security & Configuration Tips
31
- - PKCE relies on WebCrypto; document fallbacks if new runtimes lack `crypto.subtle`.
32
- - Never log tokens or verifiers in examples; scrub debug statements before merging.
33
- - Keep secrets out of version control and reference sample env files instead.
34
-
35
- ## Agent Workflow Checklist
36
- - Sync `main`, install modules (`npm install`), and confirm `npm run build` passes.
37
- - Add focused Jest coverage for new logic and ensure `npm test` is green.
38
- - Update docs (`README.md`, `AGENTS.md`) when behaviour or workflows shift.
39
- - Capture manual verification steps (demo URLs, curl output) in the PR description.
40
-
41
- ## Release Process
42
- - Bump `package.json` version and update changelog/notes as needed.
43
- - Commit and tag in `vX.Y.Z` form, then push the tag (`git push origin vX.Y.Z`).
44
- - Create a GitHub release using that tag; the `Release Publish` workflow runs automatically (or run it manually via **Run workflow**).
45
- - Ensure `NPM_TOKEN` is configured as a repository secret with publish rights to npm.
46
- - The workflow also publishes to GitHub Packages using `GITHUB_TOKEN`; consumers can install via `npm install @${repo_owner}/startgg-oauth2-full`.
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