@bounc.ing/next 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/client/provider.d.ts +14 -0
- package/dist/client/provider.js +35 -0
- package/dist/client/sign-in.d.ts +5 -0
- package/dist/client/sign-in.js +17 -0
- package/dist/client/use-user.d.ts +5 -0
- package/dist/client/use-user.js +6 -0
- package/dist/client/user-button.d.ts +5 -0
- package/dist/client/user-button.js +16 -0
- package/dist/client.d.ts +4 -0
- package/dist/client.js +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/middleware/auth.d.ts +19 -0
- package/dist/middleware/auth.js +47 -0
- package/dist/middleware.d.ts +2 -0
- package/dist/middleware.js +1 -0
- package/dist/server/admin.d.ts +8 -0
- package/dist/server/admin.js +22 -0
- package/dist/server/auth.d.ts +8 -0
- package/dist/server/auth.js +50 -0
- package/dist/server/config.d.ts +21 -0
- package/dist/server/config.js +42 -0
- package/dist/types.d.ts +23 -0
- package/dist/types.js +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import type { User } from '../types.js';
|
|
3
|
+
interface BouncingContextValue {
|
|
4
|
+
user: User | null;
|
|
5
|
+
isLoading: boolean;
|
|
6
|
+
domain: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function useBouncingContext(): BouncingContextValue;
|
|
9
|
+
interface BouncingProviderProps {
|
|
10
|
+
domain: string;
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
export declare function BouncingProvider({ domain, children }: BouncingProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useContext, useState, useEffect } from 'react';
|
|
4
|
+
const BouncingContext = createContext({
|
|
5
|
+
user: null, isLoading: true, domain: '',
|
|
6
|
+
});
|
|
7
|
+
export function useBouncingContext() {
|
|
8
|
+
return useContext(BouncingContext);
|
|
9
|
+
}
|
|
10
|
+
export function BouncingProvider({ domain, children }) {
|
|
11
|
+
const [user, setUser] = useState(null);
|
|
12
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
// Try hydration from server-rendered script tag
|
|
15
|
+
const script = document.getElementById('__bouncing');
|
|
16
|
+
if (script) {
|
|
17
|
+
try {
|
|
18
|
+
const data = JSON.parse(script.textContent ?? '');
|
|
19
|
+
if (data.user) {
|
|
20
|
+
setUser(data.user);
|
|
21
|
+
setIsLoading(false);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch { /* fall through to fetch */ }
|
|
26
|
+
}
|
|
27
|
+
// Fetch from /auth/me
|
|
28
|
+
fetch(`https://${domain}/auth/me`, { credentials: 'include' })
|
|
29
|
+
.then((res) => res.ok ? res.json() : null)
|
|
30
|
+
.then((data) => setUser(data))
|
|
31
|
+
.catch(() => setUser(null))
|
|
32
|
+
.finally(() => setIsLoading(false));
|
|
33
|
+
}, [domain]);
|
|
34
|
+
return (_jsx(BouncingContext, { value: { user, isLoading, domain }, children: children }));
|
|
35
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { useBouncingContext } from './provider.js';
|
|
5
|
+
export function SignIn({ className }) {
|
|
6
|
+
const { domain } = useBouncingContext();
|
|
7
|
+
const [providers, setProviders] = useState([]);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
fetch(`https://${domain}/auth/providers`)
|
|
10
|
+
.then((res) => res.json())
|
|
11
|
+
.then((data) => setProviders(data.providers))
|
|
12
|
+
.catch(() => { });
|
|
13
|
+
}, [domain]);
|
|
14
|
+
if (providers.length === 0)
|
|
15
|
+
return null;
|
|
16
|
+
return (_jsx("div", { className: className, children: providers.map((provider) => (_jsxs("a", { href: `https://${domain}/auth/oauth/${provider}`, style: { display: 'inline-block', margin: '0.25rem' }, children: ["Sign in with ", provider.charAt(0).toUpperCase() + provider.slice(1)] }, provider))) }));
|
|
17
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useBouncingContext } from './provider.js';
|
|
4
|
+
export function UserButton({ className }) {
|
|
5
|
+
const { user, isLoading, domain } = useBouncingContext();
|
|
6
|
+
if (isLoading || !user)
|
|
7
|
+
return null;
|
|
8
|
+
const handleSignOut = async () => {
|
|
9
|
+
await fetch(`https://${domain}/auth/logout`, {
|
|
10
|
+
method: 'POST',
|
|
11
|
+
credentials: 'include',
|
|
12
|
+
});
|
|
13
|
+
window.location.reload();
|
|
14
|
+
};
|
|
15
|
+
return (_jsxs("div", { className: className, children: [_jsx("span", { children: user.name ?? user.email }), _jsx("button", { onClick: handleSignOut, type: "button", children: "Sign out" })] }));
|
|
16
|
+
}
|
package/dist/client.d.ts
ADDED
package/dist/client.js
ADDED
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
export interface WithAuthConfig {
|
|
3
|
+
/** The domain of your bouncing app */
|
|
4
|
+
domain: string;
|
|
5
|
+
/** URL to redirect to when not authenticated (default: /auth/oauth/{firstProvider} on the bouncing domain) */
|
|
6
|
+
loginURL?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Next.js middleware that protects routes with bouncing auth.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* // middleware.ts
|
|
14
|
+
* import { withAuth } from '@bounc.ing/next/middleware'
|
|
15
|
+
* export default withAuth({ domain: 'myapp.bounc.ing' })
|
|
16
|
+
* export const config = { matcher: ['/dashboard/:path*'] }
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare function withAuth(config: WithAuthConfig): (request: NextRequest) => Promise<NextResponse<unknown>>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { verifyAccessToken, refreshTokens } from '../server/auth.js';
|
|
3
|
+
/**
|
|
4
|
+
* Next.js middleware that protects routes with bouncing auth.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* // middleware.ts
|
|
9
|
+
* import { withAuth } from '@bounc.ing/next/middleware'
|
|
10
|
+
* export default withAuth({ domain: 'myapp.bounc.ing' })
|
|
11
|
+
* export const config = { matcher: ['/dashboard/:path*'] }
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export function withAuth(config) {
|
|
15
|
+
const baseURL = `https://${config.domain}`;
|
|
16
|
+
return async function middleware(request) {
|
|
17
|
+
const accessToken = request.cookies.get('bouncing_access')?.value;
|
|
18
|
+
if (accessToken) {
|
|
19
|
+
const session = await verifyAccessToken(accessToken, baseURL);
|
|
20
|
+
if (session && session.expiresAt > Date.now()) {
|
|
21
|
+
return NextResponse.next();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// Try refresh
|
|
25
|
+
const rt = request.cookies.get('bouncing_refresh')?.value;
|
|
26
|
+
if (rt) {
|
|
27
|
+
const tokens = await refreshTokens(baseURL, rt);
|
|
28
|
+
if (tokens) {
|
|
29
|
+
const session = await verifyAccessToken(tokens.accessToken, baseURL);
|
|
30
|
+
if (session) {
|
|
31
|
+
const response = NextResponse.next();
|
|
32
|
+
response.cookies.set('bouncing_access', tokens.accessToken, {
|
|
33
|
+
httpOnly: true, secure: true, sameSite: 'lax', path: '/', maxAge: 900,
|
|
34
|
+
});
|
|
35
|
+
response.cookies.set('bouncing_refresh', tokens.refreshToken, {
|
|
36
|
+
httpOnly: true, secure: true, sameSite: 'lax', path: '/auth/refresh', maxAge: 604800,
|
|
37
|
+
});
|
|
38
|
+
return response;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Not authenticated — redirect to login
|
|
43
|
+
const loginURL = config.loginURL ?? `${baseURL}/auth/oauth/github`;
|
|
44
|
+
const redirectURL = new URL(loginURL);
|
|
45
|
+
return NextResponse.redirect(redirectURL);
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { withAuth } from './middleware/auth.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BouncingConfig } from '../types.js';
|
|
2
|
+
export interface BouncingAdminClient {
|
|
3
|
+
listApps(): Promise<unknown[]>;
|
|
4
|
+
getApp(slug: string): Promise<unknown>;
|
|
5
|
+
listUsers(slug: string): Promise<unknown[]>;
|
|
6
|
+
deleteUser(slug: string, userId: string): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
export declare function createAdminClient(config: BouncingConfig): BouncingAdminClient;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function createAdminClient(config) {
|
|
2
|
+
const controlBase = 'https://dev.bounc.ing';
|
|
3
|
+
const headers = {
|
|
4
|
+
'Content-Type': 'application/json',
|
|
5
|
+
};
|
|
6
|
+
if (config.apiKey) {
|
|
7
|
+
headers['Authorization'] = `Bearer ${config.apiKey}`;
|
|
8
|
+
}
|
|
9
|
+
async function api(path, options) {
|
|
10
|
+
const res = await fetch(`${controlBase}${path}`, { ...options, headers: { ...headers, ...options?.headers } });
|
|
11
|
+
const body = await res.json();
|
|
12
|
+
if (!res.ok)
|
|
13
|
+
throw new Error(body.error?.message ?? `API error ${res.status}`);
|
|
14
|
+
return body.data;
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
async listApps() { return api('/api/apps'); },
|
|
18
|
+
async getApp(slug) { return api(`/api/apps/${slug}`); },
|
|
19
|
+
async listUsers(slug) { return api(`/api/apps/${slug}/users`); },
|
|
20
|
+
async deleteUser(slug, userId) { await api(`/api/apps/${slug}/users/${userId}`, { method: 'DELETE' }); },
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Session } from '../types.js';
|
|
2
|
+
/** Verify a JWT access token against the JWKS endpoint. Returns Session or null. */
|
|
3
|
+
export declare function verifyAccessToken(token: string, baseURL: string): Promise<Session | null>;
|
|
4
|
+
/** Exchange a refresh token for new tokens. Returns null on failure. */
|
|
5
|
+
export declare function refreshTokens(baseURL: string, refreshToken: string): Promise<{
|
|
6
|
+
accessToken: string;
|
|
7
|
+
refreshToken: string;
|
|
8
|
+
} | null>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
|
2
|
+
const jwksCache = new Map();
|
|
3
|
+
function getJWKS(baseURL) {
|
|
4
|
+
let jwks = jwksCache.get(baseURL);
|
|
5
|
+
if (!jwks) {
|
|
6
|
+
jwks = createRemoteJWKSet(new URL('/.well-known/jwks.json', baseURL));
|
|
7
|
+
jwksCache.set(baseURL, jwks);
|
|
8
|
+
}
|
|
9
|
+
return jwks;
|
|
10
|
+
}
|
|
11
|
+
/** Verify a JWT access token against the JWKS endpoint. Returns Session or null. */
|
|
12
|
+
export async function verifyAccessToken(token, baseURL) {
|
|
13
|
+
try {
|
|
14
|
+
const jwks = getJWKS(baseURL);
|
|
15
|
+
const { payload } = await jwtVerify(token, jwks, {
|
|
16
|
+
algorithms: ['EdDSA'],
|
|
17
|
+
});
|
|
18
|
+
return {
|
|
19
|
+
user: {
|
|
20
|
+
id: payload.sub ?? '',
|
|
21
|
+
email: payload.email ?? '',
|
|
22
|
+
name: payload.name ?? null,
|
|
23
|
+
roles: payload.roles ?? [],
|
|
24
|
+
permissions: payload.permissions ?? [],
|
|
25
|
+
},
|
|
26
|
+
accessToken: token,
|
|
27
|
+
expiresAt: (payload.exp ?? 0) * 1000,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Exchange a refresh token for new tokens. Returns null on failure. */
|
|
35
|
+
export async function refreshTokens(baseURL, refreshToken) {
|
|
36
|
+
try {
|
|
37
|
+
const res = await fetch(new URL('/auth/refresh', baseURL), {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: { 'Content-Type': 'application/json' },
|
|
40
|
+
body: JSON.stringify({ refresh_token: refreshToken }),
|
|
41
|
+
});
|
|
42
|
+
if (!res.ok)
|
|
43
|
+
return null;
|
|
44
|
+
const data = await res.json();
|
|
45
|
+
return { accessToken: data.access_token, refreshToken: data.refresh_token };
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { BouncingConfig, Session, User } from '../types.js';
|
|
2
|
+
export type { BouncingConfig, Session, User };
|
|
3
|
+
export interface BouncingInstance {
|
|
4
|
+
/** Returns the current session or null. Reads cookies via next/headers. */
|
|
5
|
+
auth(): Promise<Session | null>;
|
|
6
|
+
/** Returns the current user or throws if not authenticated. */
|
|
7
|
+
currentUser(): Promise<User>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Creates a Bouncing auth instance.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* // bouncing.ts
|
|
15
|
+
* import { createBouncing } from '@bounc.ing/next'
|
|
16
|
+
* export const { auth, currentUser } = createBouncing({
|
|
17
|
+
* domain: 'myapp.bounc.ing',
|
|
18
|
+
* })
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare function createBouncing(config: BouncingConfig): BouncingInstance;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { verifyAccessToken, refreshTokens } from './auth.js';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a Bouncing auth instance.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* // bouncing.ts
|
|
8
|
+
* import { createBouncing } from '@bounc.ing/next'
|
|
9
|
+
* export const { auth, currentUser } = createBouncing({
|
|
10
|
+
* domain: 'myapp.bounc.ing',
|
|
11
|
+
* })
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export function createBouncing(config) {
|
|
15
|
+
const baseURL = `https://${config.domain}`;
|
|
16
|
+
async function auth() {
|
|
17
|
+
const { cookies } = await import('next/headers');
|
|
18
|
+
const cookieStore = await cookies();
|
|
19
|
+
const accessToken = cookieStore.get('bouncing_access')?.value;
|
|
20
|
+
if (accessToken) {
|
|
21
|
+
const session = await verifyAccessToken(accessToken, baseURL);
|
|
22
|
+
if (session && session.expiresAt > Date.now()) {
|
|
23
|
+
return session;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// Access token missing or expired — try refresh
|
|
27
|
+
const rt = cookieStore.get('bouncing_refresh')?.value;
|
|
28
|
+
if (!rt)
|
|
29
|
+
return null;
|
|
30
|
+
const tokens = await refreshTokens(baseURL, rt);
|
|
31
|
+
if (!tokens)
|
|
32
|
+
return null;
|
|
33
|
+
return verifyAccessToken(tokens.accessToken, baseURL);
|
|
34
|
+
}
|
|
35
|
+
async function currentUser() {
|
|
36
|
+
const session = await auth();
|
|
37
|
+
if (!session)
|
|
38
|
+
throw new Error('Not authenticated');
|
|
39
|
+
return session.user;
|
|
40
|
+
}
|
|
41
|
+
return { auth, currentUser };
|
|
42
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface BouncingConfig {
|
|
2
|
+
/** The domain of your bouncing app (e.g. "myapp.bounc.ing" or "auth.myapp.com") */
|
|
3
|
+
domain: string;
|
|
4
|
+
/** API key for management operations (optional, server-side only) */
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface User {
|
|
8
|
+
id: string;
|
|
9
|
+
email: string;
|
|
10
|
+
name: string | null;
|
|
11
|
+
roles: string[];
|
|
12
|
+
permissions: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface Session {
|
|
15
|
+
user: User;
|
|
16
|
+
accessToken: string;
|
|
17
|
+
expiresAt: number;
|
|
18
|
+
}
|
|
19
|
+
export interface TokenResponse {
|
|
20
|
+
access_token: string;
|
|
21
|
+
refresh_token: string;
|
|
22
|
+
expires_in: number;
|
|
23
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bounc.ing/next",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Bouncing auth SDK for Next.js — auth for your app in 3 minutes",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./client": {
|
|
12
|
+
"types": "./dist/client.d.ts",
|
|
13
|
+
"import": "./dist/client.js"
|
|
14
|
+
},
|
|
15
|
+
"./middleware": {
|
|
16
|
+
"types": "./dist/middleware.d.ts",
|
|
17
|
+
"import": "./dist/middleware.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"typecheck": "tsc --noEmit"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"next": ">=15.0.0",
|
|
27
|
+
"react": ">=19.0.0",
|
|
28
|
+
"react-dom": ">=19.0.0"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"jose": "^6.0.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/react": "^19.0.0",
|
|
35
|
+
"react": "^19.0.0",
|
|
36
|
+
"next": "^15.0.0",
|
|
37
|
+
"typescript": "^6.0.0",
|
|
38
|
+
"vitest": "^4.1.0"
|
|
39
|
+
},
|
|
40
|
+
"files": ["dist"],
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "https://github.com/scttfrdmn/bouncing-managed",
|
|
45
|
+
"directory": "sdk/next"
|
|
46
|
+
}
|
|
47
|
+
}
|