@buildrbets/react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ BuildrCallback: () => BuildrCallback,
24
+ LoginWithBuildr: () => LoginWithBuildr
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+
28
+ // src/LoginWithBuildr.tsx
29
+ var import_react = require("react");
30
+
31
+ // src/pkce.ts
32
+ var STORAGE_PREFIX = "buildr_oauth_";
33
+ function generateCodeVerifier() {
34
+ const array = new Uint8Array(32);
35
+ crypto.getRandomValues(array);
36
+ return base64UrlEncode(array);
37
+ }
38
+ async function generateCodeChallenge(verifier) {
39
+ const data = new TextEncoder().encode(verifier);
40
+ const hash = await crypto.subtle.digest("SHA-256", data);
41
+ return base64UrlEncode(new Uint8Array(hash));
42
+ }
43
+ function base64UrlEncode(bytes) {
44
+ return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
45
+ }
46
+ function generateState() {
47
+ const array = new Uint8Array(16);
48
+ crypto.getRandomValues(array);
49
+ return base64UrlEncode(array);
50
+ }
51
+ function storeOAuthParams(state, codeVerifier) {
52
+ sessionStorage.setItem(`${STORAGE_PREFIX}state`, state);
53
+ sessionStorage.setItem(`${STORAGE_PREFIX}code_verifier`, codeVerifier);
54
+ }
55
+ function getStoredOAuthParams() {
56
+ return {
57
+ state: sessionStorage.getItem(`${STORAGE_PREFIX}state`),
58
+ codeVerifier: sessionStorage.getItem(`${STORAGE_PREFIX}code_verifier`)
59
+ };
60
+ }
61
+ function clearStoredOAuthParams() {
62
+ sessionStorage.removeItem(`${STORAGE_PREFIX}state`);
63
+ sessionStorage.removeItem(`${STORAGE_PREFIX}code_verifier`);
64
+ }
65
+
66
+ // src/LoginWithBuildr.tsx
67
+ var import_jsx_runtime = require("react/jsx-runtime");
68
+ var DEFAULT_BASE_URL = "https://buildr.bet";
69
+ var DEFAULT_SCOPES = "openid email profile";
70
+ function LoginWithBuildr({
71
+ clientId,
72
+ redirectUri,
73
+ onError,
74
+ baseUrl = DEFAULT_BASE_URL,
75
+ children,
76
+ ...buttonProps
77
+ }) {
78
+ const handleClick = (0, import_react.useCallback)(async () => {
79
+ try {
80
+ const codeVerifier = generateCodeVerifier();
81
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
82
+ const state = generateState();
83
+ storeOAuthParams(state, codeVerifier);
84
+ const params = new URLSearchParams({
85
+ client_id: clientId,
86
+ redirect_uri: redirectUri,
87
+ response_type: "code",
88
+ scope: DEFAULT_SCOPES,
89
+ code_challenge: codeChallenge,
90
+ code_challenge_method: "S256",
91
+ state
92
+ });
93
+ window.location.href = `${baseUrl}/api/oidc/auth?${params}`;
94
+ } catch (err) {
95
+ onError?.(err instanceof Error ? err : new Error(String(err)));
96
+ }
97
+ }, [clientId, redirectUri, baseUrl, onError]);
98
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { onClick: handleClick, type: "button", ...buttonProps, children: children ?? "Login with Buildr" });
99
+ }
100
+
101
+ // src/BuildrCallback.tsx
102
+ var import_react2 = require("react");
103
+ var import_jsx_runtime2 = require("react/jsx-runtime");
104
+ var DEFAULT_BASE_URL2 = "https://buildr.bet";
105
+ function BuildrCallback({
106
+ clientId,
107
+ redirectUri,
108
+ onSuccess,
109
+ onError,
110
+ baseUrl = DEFAULT_BASE_URL2,
111
+ children
112
+ }) {
113
+ const processedRef = (0, import_react2.useRef)(false);
114
+ (0, import_react2.useEffect)(() => {
115
+ if (processedRef.current) return;
116
+ processedRef.current = true;
117
+ const params = new URLSearchParams(window.location.search);
118
+ const code = params.get("code");
119
+ const returnedState = params.get("state");
120
+ const error = params.get("error");
121
+ if (error) {
122
+ const description = params.get("error_description") ?? error;
123
+ onError?.(new Error(description));
124
+ return;
125
+ }
126
+ if (!code) {
127
+ onError?.(new Error("No authorization code in callback URL"));
128
+ return;
129
+ }
130
+ const stored = getStoredOAuthParams();
131
+ if (!stored.state || stored.state !== returnedState) {
132
+ onError?.(new Error("Invalid state parameter \u2014 possible CSRF attack"));
133
+ return;
134
+ }
135
+ if (!stored.codeVerifier) {
136
+ onError?.(new Error("No code verifier found \u2014 session may have expired"));
137
+ return;
138
+ }
139
+ const exchangeToken = async () => {
140
+ try {
141
+ const tokenRes = await fetch(`${baseUrl}/api/oidc/token`, {
142
+ method: "POST",
143
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
144
+ body: new URLSearchParams({
145
+ grant_type: "authorization_code",
146
+ code,
147
+ redirect_uri: redirectUri,
148
+ client_id: clientId,
149
+ code_verifier: stored.codeVerifier
150
+ })
151
+ });
152
+ if (!tokenRes.ok) {
153
+ const err = await tokenRes.json().catch(() => ({}));
154
+ throw new Error(
155
+ err.error_description ?? err.error ?? `Token exchange failed (${tokenRes.status})`
156
+ );
157
+ }
158
+ const tokens = await tokenRes.json();
159
+ const userinfoRes = await fetch(`${baseUrl}/api/oidc/me`, {
160
+ headers: { Authorization: `Bearer ${tokens.access_token}` }
161
+ });
162
+ if (!userinfoRes.ok) {
163
+ throw new Error(`Failed to fetch user info (${userinfoRes.status})`);
164
+ }
165
+ const user = await userinfoRes.json();
166
+ clearStoredOAuthParams();
167
+ onSuccess(user);
168
+ } catch (err) {
169
+ clearStoredOAuthParams();
170
+ onError?.(err instanceof Error ? err : new Error(String(err)));
171
+ }
172
+ };
173
+ exchangeToken();
174
+ }, [clientId, redirectUri, baseUrl, onSuccess, onError]);
175
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: children ?? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { children: "Completing login..." }) });
176
+ }
177
+ // Annotate the CommonJS export names for ESM import in node:
178
+ 0 && (module.exports = {
179
+ BuildrCallback,
180
+ LoginWithBuildr
181
+ });
182
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/LoginWithBuildr.tsx","../src/pkce.ts","../src/BuildrCallback.tsx"],"sourcesContent":["export { LoginWithBuildr } from \"./LoginWithBuildr\";\nexport { BuildrCallback } from \"./BuildrCallback\";\nexport type {\n BuildrUser,\n LoginWithBuildrProps,\n BuildrCallbackProps,\n} from \"./types\";\n","import { useCallback } from \"react\";\nimport type { LoginWithBuildrProps } from \"./types\";\nimport {\n generateCodeVerifier,\n generateCodeChallenge,\n generateState,\n storeOAuthParams,\n} from \"./pkce\";\n\nconst DEFAULT_BASE_URL = \"https://buildr.bet\";\nconst DEFAULT_SCOPES = \"openid email profile\";\n\nexport function LoginWithBuildr({\n clientId,\n redirectUri,\n onError,\n baseUrl = DEFAULT_BASE_URL,\n children,\n ...buttonProps\n}: LoginWithBuildrProps) {\n const handleClick = useCallback(async () => {\n try {\n const codeVerifier = generateCodeVerifier();\n const codeChallenge = await generateCodeChallenge(codeVerifier);\n const state = generateState();\n\n storeOAuthParams(state, codeVerifier);\n\n const params = new URLSearchParams({\n client_id: clientId,\n redirect_uri: redirectUri,\n response_type: \"code\",\n scope: DEFAULT_SCOPES,\n code_challenge: codeChallenge,\n code_challenge_method: \"S256\",\n state,\n });\n\n window.location.href = `${baseUrl}/api/oidc/auth?${params}`;\n } catch (err) {\n onError?.(err instanceof Error ? err : new Error(String(err)));\n }\n }, [clientId, redirectUri, baseUrl, onError]);\n\n return (\n <button onClick={handleClick} type=\"button\" {...buttonProps}>\n {children ?? \"Login with Buildr\"}\n </button>\n );\n}\n","const STORAGE_PREFIX = \"buildr_oauth_\";\n\nexport function generateCodeVerifier(): string {\n const array = new Uint8Array(32);\n crypto.getRandomValues(array);\n return base64UrlEncode(array);\n}\n\nexport async function generateCodeChallenge(verifier: string): Promise<string> {\n const data = new TextEncoder().encode(verifier);\n const hash = await crypto.subtle.digest(\"SHA-256\", data);\n return base64UrlEncode(new Uint8Array(hash));\n}\n\nfunction base64UrlEncode(bytes: Uint8Array): string {\n return btoa(String.fromCharCode(...bytes))\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/, \"\");\n}\n\nexport function generateState(): string {\n const array = new Uint8Array(16);\n crypto.getRandomValues(array);\n return base64UrlEncode(array);\n}\n\nexport function storeOAuthParams(state: string, codeVerifier: string): void {\n sessionStorage.setItem(`${STORAGE_PREFIX}state`, state);\n sessionStorage.setItem(`${STORAGE_PREFIX}code_verifier`, codeVerifier);\n}\n\nexport function getStoredOAuthParams(): {\n state: string | null;\n codeVerifier: string | null;\n} {\n return {\n state: sessionStorage.getItem(`${STORAGE_PREFIX}state`),\n codeVerifier: sessionStorage.getItem(`${STORAGE_PREFIX}code_verifier`),\n };\n}\n\nexport function clearStoredOAuthParams(): void {\n sessionStorage.removeItem(`${STORAGE_PREFIX}state`);\n sessionStorage.removeItem(`${STORAGE_PREFIX}code_verifier`);\n}\n","import { useEffect, useRef } from \"react\";\nimport type { BuildrCallbackProps, BuildrUser } from \"./types\";\nimport { getStoredOAuthParams, clearStoredOAuthParams } from \"./pkce\";\n\nconst DEFAULT_BASE_URL = \"https://buildr.bet\";\n\nexport function BuildrCallback({\n clientId,\n redirectUri,\n onSuccess,\n onError,\n baseUrl = DEFAULT_BASE_URL,\n children,\n}: BuildrCallbackProps) {\n const processedRef = useRef(false);\n\n useEffect(() => {\n if (processedRef.current) return;\n processedRef.current = true;\n\n const params = new URLSearchParams(window.location.search);\n const code = params.get(\"code\");\n const returnedState = params.get(\"state\");\n const error = params.get(\"error\");\n\n if (error) {\n const description = params.get(\"error_description\") ?? error;\n onError?.(new Error(description));\n return;\n }\n\n if (!code) {\n onError?.(new Error(\"No authorization code in callback URL\"));\n return;\n }\n\n const stored = getStoredOAuthParams();\n\n if (!stored.state || stored.state !== returnedState) {\n onError?.(new Error(\"Invalid state parameter — possible CSRF attack\"));\n return;\n }\n\n if (!stored.codeVerifier) {\n onError?.(new Error(\"No code verifier found — session may have expired\"));\n return;\n }\n\n const exchangeToken = async () => {\n try {\n // Exchange code for tokens (public client — no client_secret)\n const tokenRes = await fetch(`${baseUrl}/api/oidc/token`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({\n grant_type: \"authorization_code\",\n code,\n redirect_uri: redirectUri,\n client_id: clientId,\n code_verifier: stored.codeVerifier!,\n }),\n });\n\n if (!tokenRes.ok) {\n const err = await tokenRes.json().catch(() => ({}));\n throw new Error(\n err.error_description ??\n err.error ??\n `Token exchange failed (${tokenRes.status})`,\n );\n }\n\n const tokens = await tokenRes.json();\n\n // Fetch user info\n const userinfoRes = await fetch(`${baseUrl}/api/oidc/me`, {\n headers: { Authorization: `Bearer ${tokens.access_token}` },\n });\n\n if (!userinfoRes.ok) {\n throw new Error(`Failed to fetch user info (${userinfoRes.status})`);\n }\n\n const user: BuildrUser = await userinfoRes.json();\n clearStoredOAuthParams();\n onSuccess(user);\n } catch (err) {\n clearStoredOAuthParams();\n onError?.(err instanceof Error ? err : new Error(String(err)));\n }\n };\n\n exchangeToken();\n }, [clientId, redirectUri, baseUrl, onSuccess, onError]);\n\n return <>{children ?? <p>Completing login...</p>}</>;\n}\n"],"mappings":"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","names":["import_react","import_jsx_runtime","DEFAULT_BASE_URL"]}
@@ -0,0 +1,41 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ interface BuildrUser {
4
+ sub: string;
5
+ email?: string;
6
+ name?: string;
7
+ picture?: string;
8
+ organization_id?: string;
9
+ membership_role?: string;
10
+ membership_status?: string;
11
+ }
12
+ interface LoginWithBuildrProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onClick" | "onError"> {
13
+ /** Your OAuth app's client ID */
14
+ clientId: string;
15
+ /** The redirect URI registered with your OAuth app */
16
+ redirectUri: string;
17
+ /** Called when an error occurs during authentication */
18
+ onError?: (error: Error) => void;
19
+ /** @internal Base URL of the Buildr instance (default: "https://buildr.bet") */
20
+ baseUrl?: string;
21
+ }
22
+ interface BuildrCallbackProps {
23
+ /** Your OAuth app's client ID */
24
+ clientId: string;
25
+ /** The redirect URI (must match what was used in the auth request) */
26
+ redirectUri: string;
27
+ /** Called with user claims after successful token exchange */
28
+ onSuccess: (user: BuildrUser) => void;
29
+ /** Called when an error occurs */
30
+ onError?: (error: Error) => void;
31
+ /** Base URL of the Buildr instance (default: "https://buildr.bet") */
32
+ baseUrl?: string;
33
+ /** Content to show while processing the callback */
34
+ children?: React.ReactNode;
35
+ }
36
+
37
+ declare function LoginWithBuildr({ clientId, redirectUri, onError, baseUrl, children, ...buttonProps }: LoginWithBuildrProps): react_jsx_runtime.JSX.Element;
38
+
39
+ declare function BuildrCallback({ clientId, redirectUri, onSuccess, onError, baseUrl, children, }: BuildrCallbackProps): react_jsx_runtime.JSX.Element;
40
+
41
+ export { BuildrCallback, type BuildrCallbackProps, type BuildrUser, LoginWithBuildr, type LoginWithBuildrProps };
@@ -0,0 +1,41 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ interface BuildrUser {
4
+ sub: string;
5
+ email?: string;
6
+ name?: string;
7
+ picture?: string;
8
+ organization_id?: string;
9
+ membership_role?: string;
10
+ membership_status?: string;
11
+ }
12
+ interface LoginWithBuildrProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onClick" | "onError"> {
13
+ /** Your OAuth app's client ID */
14
+ clientId: string;
15
+ /** The redirect URI registered with your OAuth app */
16
+ redirectUri: string;
17
+ /** Called when an error occurs during authentication */
18
+ onError?: (error: Error) => void;
19
+ /** @internal Base URL of the Buildr instance (default: "https://buildr.bet") */
20
+ baseUrl?: string;
21
+ }
22
+ interface BuildrCallbackProps {
23
+ /** Your OAuth app's client ID */
24
+ clientId: string;
25
+ /** The redirect URI (must match what was used in the auth request) */
26
+ redirectUri: string;
27
+ /** Called with user claims after successful token exchange */
28
+ onSuccess: (user: BuildrUser) => void;
29
+ /** Called when an error occurs */
30
+ onError?: (error: Error) => void;
31
+ /** Base URL of the Buildr instance (default: "https://buildr.bet") */
32
+ baseUrl?: string;
33
+ /** Content to show while processing the callback */
34
+ children?: React.ReactNode;
35
+ }
36
+
37
+ declare function LoginWithBuildr({ clientId, redirectUri, onError, baseUrl, children, ...buttonProps }: LoginWithBuildrProps): react_jsx_runtime.JSX.Element;
38
+
39
+ declare function BuildrCallback({ clientId, redirectUri, onSuccess, onError, baseUrl, children, }: BuildrCallbackProps): react_jsx_runtime.JSX.Element;
40
+
41
+ export { BuildrCallback, type BuildrCallbackProps, type BuildrUser, LoginWithBuildr, type LoginWithBuildrProps };
package/dist/index.js ADDED
@@ -0,0 +1,154 @@
1
+ // src/LoginWithBuildr.tsx
2
+ import { useCallback } from "react";
3
+
4
+ // src/pkce.ts
5
+ var STORAGE_PREFIX = "buildr_oauth_";
6
+ function generateCodeVerifier() {
7
+ const array = new Uint8Array(32);
8
+ crypto.getRandomValues(array);
9
+ return base64UrlEncode(array);
10
+ }
11
+ async function generateCodeChallenge(verifier) {
12
+ const data = new TextEncoder().encode(verifier);
13
+ const hash = await crypto.subtle.digest("SHA-256", data);
14
+ return base64UrlEncode(new Uint8Array(hash));
15
+ }
16
+ function base64UrlEncode(bytes) {
17
+ return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
18
+ }
19
+ function generateState() {
20
+ const array = new Uint8Array(16);
21
+ crypto.getRandomValues(array);
22
+ return base64UrlEncode(array);
23
+ }
24
+ function storeOAuthParams(state, codeVerifier) {
25
+ sessionStorage.setItem(`${STORAGE_PREFIX}state`, state);
26
+ sessionStorage.setItem(`${STORAGE_PREFIX}code_verifier`, codeVerifier);
27
+ }
28
+ function getStoredOAuthParams() {
29
+ return {
30
+ state: sessionStorage.getItem(`${STORAGE_PREFIX}state`),
31
+ codeVerifier: sessionStorage.getItem(`${STORAGE_PREFIX}code_verifier`)
32
+ };
33
+ }
34
+ function clearStoredOAuthParams() {
35
+ sessionStorage.removeItem(`${STORAGE_PREFIX}state`);
36
+ sessionStorage.removeItem(`${STORAGE_PREFIX}code_verifier`);
37
+ }
38
+
39
+ // src/LoginWithBuildr.tsx
40
+ import { jsx } from "react/jsx-runtime";
41
+ var DEFAULT_BASE_URL = "https://buildr.bet";
42
+ var DEFAULT_SCOPES = "openid email profile";
43
+ function LoginWithBuildr({
44
+ clientId,
45
+ redirectUri,
46
+ onError,
47
+ baseUrl = DEFAULT_BASE_URL,
48
+ children,
49
+ ...buttonProps
50
+ }) {
51
+ const handleClick = useCallback(async () => {
52
+ try {
53
+ const codeVerifier = generateCodeVerifier();
54
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
55
+ const state = generateState();
56
+ storeOAuthParams(state, codeVerifier);
57
+ const params = new URLSearchParams({
58
+ client_id: clientId,
59
+ redirect_uri: redirectUri,
60
+ response_type: "code",
61
+ scope: DEFAULT_SCOPES,
62
+ code_challenge: codeChallenge,
63
+ code_challenge_method: "S256",
64
+ state
65
+ });
66
+ window.location.href = `${baseUrl}/api/oidc/auth?${params}`;
67
+ } catch (err) {
68
+ onError?.(err instanceof Error ? err : new Error(String(err)));
69
+ }
70
+ }, [clientId, redirectUri, baseUrl, onError]);
71
+ return /* @__PURE__ */ jsx("button", { onClick: handleClick, type: "button", ...buttonProps, children: children ?? "Login with Buildr" });
72
+ }
73
+
74
+ // src/BuildrCallback.tsx
75
+ import { useEffect, useRef } from "react";
76
+ import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
77
+ var DEFAULT_BASE_URL2 = "https://buildr.bet";
78
+ function BuildrCallback({
79
+ clientId,
80
+ redirectUri,
81
+ onSuccess,
82
+ onError,
83
+ baseUrl = DEFAULT_BASE_URL2,
84
+ children
85
+ }) {
86
+ const processedRef = useRef(false);
87
+ useEffect(() => {
88
+ if (processedRef.current) return;
89
+ processedRef.current = true;
90
+ const params = new URLSearchParams(window.location.search);
91
+ const code = params.get("code");
92
+ const returnedState = params.get("state");
93
+ const error = params.get("error");
94
+ if (error) {
95
+ const description = params.get("error_description") ?? error;
96
+ onError?.(new Error(description));
97
+ return;
98
+ }
99
+ if (!code) {
100
+ onError?.(new Error("No authorization code in callback URL"));
101
+ return;
102
+ }
103
+ const stored = getStoredOAuthParams();
104
+ if (!stored.state || stored.state !== returnedState) {
105
+ onError?.(new Error("Invalid state parameter \u2014 possible CSRF attack"));
106
+ return;
107
+ }
108
+ if (!stored.codeVerifier) {
109
+ onError?.(new Error("No code verifier found \u2014 session may have expired"));
110
+ return;
111
+ }
112
+ const exchangeToken = async () => {
113
+ try {
114
+ const tokenRes = await fetch(`${baseUrl}/api/oidc/token`, {
115
+ method: "POST",
116
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
117
+ body: new URLSearchParams({
118
+ grant_type: "authorization_code",
119
+ code,
120
+ redirect_uri: redirectUri,
121
+ client_id: clientId,
122
+ code_verifier: stored.codeVerifier
123
+ })
124
+ });
125
+ if (!tokenRes.ok) {
126
+ const err = await tokenRes.json().catch(() => ({}));
127
+ throw new Error(
128
+ err.error_description ?? err.error ?? `Token exchange failed (${tokenRes.status})`
129
+ );
130
+ }
131
+ const tokens = await tokenRes.json();
132
+ const userinfoRes = await fetch(`${baseUrl}/api/oidc/me`, {
133
+ headers: { Authorization: `Bearer ${tokens.access_token}` }
134
+ });
135
+ if (!userinfoRes.ok) {
136
+ throw new Error(`Failed to fetch user info (${userinfoRes.status})`);
137
+ }
138
+ const user = await userinfoRes.json();
139
+ clearStoredOAuthParams();
140
+ onSuccess(user);
141
+ } catch (err) {
142
+ clearStoredOAuthParams();
143
+ onError?.(err instanceof Error ? err : new Error(String(err)));
144
+ }
145
+ };
146
+ exchangeToken();
147
+ }, [clientId, redirectUri, baseUrl, onSuccess, onError]);
148
+ return /* @__PURE__ */ jsx2(Fragment, { children: children ?? /* @__PURE__ */ jsx2("p", { children: "Completing login..." }) });
149
+ }
150
+ export {
151
+ BuildrCallback,
152
+ LoginWithBuildr
153
+ };
154
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/LoginWithBuildr.tsx","../src/pkce.ts","../src/BuildrCallback.tsx"],"sourcesContent":["import { useCallback } from \"react\";\nimport type { LoginWithBuildrProps } from \"./types\";\nimport {\n generateCodeVerifier,\n generateCodeChallenge,\n generateState,\n storeOAuthParams,\n} from \"./pkce\";\n\nconst DEFAULT_BASE_URL = \"https://buildr.bet\";\nconst DEFAULT_SCOPES = \"openid email profile\";\n\nexport function LoginWithBuildr({\n clientId,\n redirectUri,\n onError,\n baseUrl = DEFAULT_BASE_URL,\n children,\n ...buttonProps\n}: LoginWithBuildrProps) {\n const handleClick = useCallback(async () => {\n try {\n const codeVerifier = generateCodeVerifier();\n const codeChallenge = await generateCodeChallenge(codeVerifier);\n const state = generateState();\n\n storeOAuthParams(state, codeVerifier);\n\n const params = new URLSearchParams({\n client_id: clientId,\n redirect_uri: redirectUri,\n response_type: \"code\",\n scope: DEFAULT_SCOPES,\n code_challenge: codeChallenge,\n code_challenge_method: \"S256\",\n state,\n });\n\n window.location.href = `${baseUrl}/api/oidc/auth?${params}`;\n } catch (err) {\n onError?.(err instanceof Error ? err : new Error(String(err)));\n }\n }, [clientId, redirectUri, baseUrl, onError]);\n\n return (\n <button onClick={handleClick} type=\"button\" {...buttonProps}>\n {children ?? \"Login with Buildr\"}\n </button>\n );\n}\n","const STORAGE_PREFIX = \"buildr_oauth_\";\n\nexport function generateCodeVerifier(): string {\n const array = new Uint8Array(32);\n crypto.getRandomValues(array);\n return base64UrlEncode(array);\n}\n\nexport async function generateCodeChallenge(verifier: string): Promise<string> {\n const data = new TextEncoder().encode(verifier);\n const hash = await crypto.subtle.digest(\"SHA-256\", data);\n return base64UrlEncode(new Uint8Array(hash));\n}\n\nfunction base64UrlEncode(bytes: Uint8Array): string {\n return btoa(String.fromCharCode(...bytes))\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/, \"\");\n}\n\nexport function generateState(): string {\n const array = new Uint8Array(16);\n crypto.getRandomValues(array);\n return base64UrlEncode(array);\n}\n\nexport function storeOAuthParams(state: string, codeVerifier: string): void {\n sessionStorage.setItem(`${STORAGE_PREFIX}state`, state);\n sessionStorage.setItem(`${STORAGE_PREFIX}code_verifier`, codeVerifier);\n}\n\nexport function getStoredOAuthParams(): {\n state: string | null;\n codeVerifier: string | null;\n} {\n return {\n state: sessionStorage.getItem(`${STORAGE_PREFIX}state`),\n codeVerifier: sessionStorage.getItem(`${STORAGE_PREFIX}code_verifier`),\n };\n}\n\nexport function clearStoredOAuthParams(): void {\n sessionStorage.removeItem(`${STORAGE_PREFIX}state`);\n sessionStorage.removeItem(`${STORAGE_PREFIX}code_verifier`);\n}\n","import { useEffect, useRef } from \"react\";\nimport type { BuildrCallbackProps, BuildrUser } from \"./types\";\nimport { getStoredOAuthParams, clearStoredOAuthParams } from \"./pkce\";\n\nconst DEFAULT_BASE_URL = \"https://buildr.bet\";\n\nexport function BuildrCallback({\n clientId,\n redirectUri,\n onSuccess,\n onError,\n baseUrl = DEFAULT_BASE_URL,\n children,\n}: BuildrCallbackProps) {\n const processedRef = useRef(false);\n\n useEffect(() => {\n if (processedRef.current) return;\n processedRef.current = true;\n\n const params = new URLSearchParams(window.location.search);\n const code = params.get(\"code\");\n const returnedState = params.get(\"state\");\n const error = params.get(\"error\");\n\n if (error) {\n const description = params.get(\"error_description\") ?? error;\n onError?.(new Error(description));\n return;\n }\n\n if (!code) {\n onError?.(new Error(\"No authorization code in callback URL\"));\n return;\n }\n\n const stored = getStoredOAuthParams();\n\n if (!stored.state || stored.state !== returnedState) {\n onError?.(new Error(\"Invalid state parameter — possible CSRF attack\"));\n return;\n }\n\n if (!stored.codeVerifier) {\n onError?.(new Error(\"No code verifier found — session may have expired\"));\n return;\n }\n\n const exchangeToken = async () => {\n try {\n // Exchange code for tokens (public client — no client_secret)\n const tokenRes = await fetch(`${baseUrl}/api/oidc/token`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({\n grant_type: \"authorization_code\",\n code,\n redirect_uri: redirectUri,\n client_id: clientId,\n code_verifier: stored.codeVerifier!,\n }),\n });\n\n if (!tokenRes.ok) {\n const err = await tokenRes.json().catch(() => ({}));\n throw new Error(\n err.error_description ??\n err.error ??\n `Token exchange failed (${tokenRes.status})`,\n );\n }\n\n const tokens = await tokenRes.json();\n\n // Fetch user info\n const userinfoRes = await fetch(`${baseUrl}/api/oidc/me`, {\n headers: { Authorization: `Bearer ${tokens.access_token}` },\n });\n\n if (!userinfoRes.ok) {\n throw new Error(`Failed to fetch user info (${userinfoRes.status})`);\n }\n\n const user: BuildrUser = await userinfoRes.json();\n clearStoredOAuthParams();\n onSuccess(user);\n } catch (err) {\n clearStoredOAuthParams();\n onError?.(err instanceof Error ? err : new Error(String(err)));\n }\n };\n\n exchangeToken();\n }, [clientId, redirectUri, baseUrl, onSuccess, onError]);\n\n return <>{children ?? <p>Completing login...</p>}</>;\n}\n"],"mappings":"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","names":["jsx","DEFAULT_BASE_URL"]}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@buildrbets/react",
3
+ "version": "0.1.0",
4
+ "description": "Login with Buildr — React SDK for OAuth 2.0 + PKCE",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "scripts": {
23
+ "build": "tsup",
24
+ "dev": "tsup --watch",
25
+ "publish-pkg": "pnpm build && npm publish"
26
+ },
27
+ "peerDependencies": {
28
+ "react": ">=18.0.0",
29
+ "react-dom": ">=18.0.0"
30
+ },
31
+ "devDependencies": {
32
+ "react": "^19.0.0",
33
+ "react-dom": "^19.0.0",
34
+ "tsup": "^8.0.0",
35
+ "typescript": "^5.0.0",
36
+ "@types/react": "^19.0.0"
37
+ }
38
+ }