@playcademy/better-auth 0.0.1-alpha.1 → 0.0.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/dist/client.d.ts +88 -14
- package/dist/client.js +18 -6
- package/dist/server.d.ts +120 -6
- package/dist/server.js +54 -39
- package/package.json +2 -2
package/dist/client.d.ts
CHANGED
|
@@ -19,48 +19,122 @@
|
|
|
19
19
|
* ```
|
|
20
20
|
*/
|
|
21
21
|
import { playcademy as playcademyServerPlugin } from './server';
|
|
22
|
+
/**
|
|
23
|
+
* Configuration options for the Playcademy Better Auth client plugin
|
|
24
|
+
*
|
|
25
|
+
* Configures how the client handles platform authentication and Safari
|
|
26
|
+
* Storage Access API integration.
|
|
27
|
+
*/
|
|
22
28
|
export interface PlaycademyClientOptions {
|
|
23
29
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* When true (default), Safari users in iframes will see an automatic
|
|
27
|
-
* prompt to grant storage access (required for cookies in Safari).
|
|
28
|
-
*
|
|
29
|
-
* Set to false if you want to handle Safari storage access yourself.
|
|
30
|
+
* Safari browser configuration.
|
|
30
31
|
*
|
|
31
|
-
*
|
|
32
|
+
* Controls how the plugin handles Safari's Storage Access API requirements
|
|
33
|
+
* for cross-origin cookies in iframes.
|
|
32
34
|
*/
|
|
33
35
|
safari?: {
|
|
36
|
+
/**
|
|
37
|
+
* Automatically show storage access prompt for Safari users.
|
|
38
|
+
*
|
|
39
|
+
* When `true` (default), Safari users running the game in an iframe
|
|
40
|
+
* (platform mode) will automatically see a prompt to grant cookie
|
|
41
|
+
* access. This is required for authentication to work in Safari iframes.
|
|
42
|
+
*
|
|
43
|
+
* Set to `false` if you want to handle Safari storage access manually
|
|
44
|
+
* or have a custom flow.
|
|
45
|
+
*
|
|
46
|
+
* **Why this is needed**: Safari blocks third-party cookies in iframes
|
|
47
|
+
* by default. The Storage Access API allows users to explicitly grant
|
|
48
|
+
* cookie access after a user interaction.
|
|
49
|
+
*
|
|
50
|
+
* @default true
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* {
|
|
54
|
+
* safari: {
|
|
55
|
+
* autoPrompt: true // Show automatic prompt (recommended)
|
|
56
|
+
* }
|
|
57
|
+
* }
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* {
|
|
63
|
+
* safari: {
|
|
64
|
+
* autoPrompt: false // Handle Safari auth manually
|
|
65
|
+
* }
|
|
66
|
+
* }
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
34
69
|
autoPrompt?: boolean;
|
|
35
70
|
};
|
|
36
71
|
}
|
|
37
72
|
/**
|
|
38
73
|
* Playcademy client plugin for Better Auth
|
|
39
74
|
*
|
|
40
|
-
*
|
|
41
|
-
* -
|
|
42
|
-
* -
|
|
43
|
-
*
|
|
75
|
+
* Enables seamless authentication for Playcademy games that run both:
|
|
76
|
+
* - In the Playcademy platform (iframe with JWT token exchange)
|
|
77
|
+
* - As standalone games (standard Better Auth with cookies)
|
|
78
|
+
*
|
|
79
|
+
* **Key Features**:
|
|
80
|
+
* - Automatic platform vs standalone mode detection
|
|
81
|
+
* - JWT token exchange for platform authentication
|
|
82
|
+
* - Safari Storage Access API handling (automatic prompt)
|
|
83
|
+
* - Cookie-based auth with CHIPS support for Chrome/Edge
|
|
84
|
+
* - Zero configuration required
|
|
85
|
+
*
|
|
86
|
+
* **How it works**:
|
|
87
|
+
* 1. Platform mode: Exchanges platform JWT for Better Auth session
|
|
88
|
+
* 2. Standalone mode: Uses standard Better Auth providers (email/OAuth)
|
|
89
|
+
* 3. Safari: Automatically requests storage access for iframe cookies
|
|
90
|
+
*
|
|
91
|
+
* @param options - Configuration options (optional)
|
|
92
|
+
* @returns Better Auth client plugin
|
|
44
93
|
*
|
|
45
94
|
* @example
|
|
46
95
|
* ```typescript
|
|
47
|
-
* //
|
|
96
|
+
* // Recommended: Use defaults (handles everything automatically)
|
|
97
|
+
* import { createAuthClient } from 'better-auth/react'
|
|
98
|
+
* import { playcademy } from '@playcademy/better-auth/client'
|
|
99
|
+
*
|
|
48
100
|
* const auth = createAuthClient({
|
|
101
|
+
* baseURL: 'http://localhost:8788',
|
|
49
102
|
* plugins: [playcademy()]
|
|
50
103
|
* })
|
|
104
|
+
*
|
|
105
|
+
* // Access auth in your components
|
|
106
|
+
* export const { useSession } = auth
|
|
51
107
|
* ```
|
|
52
108
|
*
|
|
53
109
|
* @example
|
|
54
110
|
* ```typescript
|
|
55
|
-
* // Custom: Disable
|
|
111
|
+
* // Custom: Disable Safari auto-prompt
|
|
56
112
|
* const auth = createAuthClient({
|
|
113
|
+
* baseURL: 'http://localhost:8788',
|
|
57
114
|
* plugins: [
|
|
58
115
|
* playcademy({
|
|
59
|
-
* safari: {
|
|
116
|
+
* safari: {
|
|
117
|
+
* autoPrompt: false // Handle Safari manually
|
|
118
|
+
* }
|
|
60
119
|
* })
|
|
61
120
|
* ]
|
|
62
121
|
* })
|
|
63
122
|
* ```
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```typescript
|
|
126
|
+
* // Use session in React component
|
|
127
|
+
* import { useSession } from './lib/auth'
|
|
128
|
+
*
|
|
129
|
+
* function App() {
|
|
130
|
+
* const { data: session, isPending } = useSession()
|
|
131
|
+
*
|
|
132
|
+
* if (isPending) return <div>Loading...</div>
|
|
133
|
+
* if (!session) return <div>Not logged in</div>
|
|
134
|
+
*
|
|
135
|
+
* return <div>Welcome, {session.user.name}!</div>
|
|
136
|
+
* }
|
|
137
|
+
* ```
|
|
64
138
|
*/
|
|
65
139
|
export declare function playcademy(options?: PlaycademyClientOptions): {
|
|
66
140
|
id: "playcademy-client";
|
package/dist/client.js
CHANGED
|
@@ -118,12 +118,24 @@ function playcademyExchangePlugin(_opts) {
|
|
|
118
118
|
if (!canProceed) {
|
|
119
119
|
return;
|
|
120
120
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
121
|
+
try {
|
|
122
|
+
const headers = { Authorization: `Bearer ${token}` };
|
|
123
|
+
if (isPlatformMode()) {
|
|
124
|
+
headers["X-Playcademy-Mode"] = "platform";
|
|
125
|
+
}
|
|
126
|
+
const response = await fetch("/api/auth/playcademy", {
|
|
127
|
+
method: "POST",
|
|
128
|
+
headers,
|
|
129
|
+
credentials: "include"
|
|
130
|
+
});
|
|
131
|
+
if (response.status === 204) {
|
|
132
|
+
exchangeComplete = true;
|
|
133
|
+
} else if (response.status !== 200 && response.status !== 500) {
|
|
134
|
+
console.warn(`[Playcademy Auth] Unexpected response status: ${response.status}`);
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.warn("[Playcademy Auth] Network error during token exchange:", error);
|
|
138
|
+
}
|
|
127
139
|
})();
|
|
128
140
|
await exchangePromise;
|
|
129
141
|
return { url, options };
|
package/dist/server.d.ts
CHANGED
|
@@ -1,30 +1,144 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Playcademy
|
|
2
|
+
* Playcademy Platform Authentication Plugin (Server)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
4
|
+
* Enables seamless authentication for Playcademy games that run both:
|
|
5
|
+
* - In the Playcademy platform (iframe with JWT token exchange)
|
|
6
|
+
* - As standalone games (standard Better Auth with cookies)
|
|
7
|
+
*
|
|
8
|
+
* **Key Features**:
|
|
9
|
+
* - Platform JWT token exchange endpoint (`POST /api/auth/playcademy`)
|
|
7
10
|
* - Automatic account linking between platform and standalone modes
|
|
8
|
-
* - Cross-site cookie support with CHIPS
|
|
11
|
+
* - Cross-site cookie support with CHIPS (Partitioned cookies)
|
|
12
|
+
* - Schema extension: adds `playcademyUserId` to user model
|
|
13
|
+
* - Safari Storage Access API compatibility
|
|
14
|
+
*
|
|
15
|
+
* **How it works**:
|
|
16
|
+
*
|
|
17
|
+
* 1. **Platform Mode (iframe)**:
|
|
18
|
+
* - Game receives JWT token from Playcademy platform
|
|
19
|
+
* - Client exchanges token via `/api/auth/playcademy` endpoint
|
|
20
|
+
* - Server verifies token with platform API (uses PLAYCADEMY_BASE_URL env var)
|
|
21
|
+
* - Creates or links Better Auth user by `playcademyUserId`
|
|
22
|
+
* - Returns session cookie with CHIPS support
|
|
23
|
+
*
|
|
24
|
+
* 2. **Standalone Mode**:
|
|
25
|
+
* - Users authenticate via Better Auth providers (email, OAuth, etc.)
|
|
26
|
+
* - If they later access via platform, accounts are linked by `playcademyUserId`
|
|
27
|
+
*
|
|
28
|
+
* 3. **Account Linking**:
|
|
29
|
+
* - Platform users get `playcademyUserId` set automatically
|
|
30
|
+
* - If a user exists with that `playcademyUserId`, sessions merge
|
|
31
|
+
* - Enables seamless switching between platform and standalone
|
|
32
|
+
*
|
|
33
|
+
* **Development/Testing**:
|
|
34
|
+
*
|
|
35
|
+
* To test deployed games against a local platform API, set the `PLAYCADEMY_AUTH_API_URL`
|
|
36
|
+
* environment variable to override the platform URL (e.g., using a tunnel service):
|
|
37
|
+
*
|
|
38
|
+
* ```bash
|
|
39
|
+
* PLAYCADEMY_AUTH_API_URL=https://your-tunnel.dev
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* **Cookie Configuration**:
|
|
43
|
+
*
|
|
44
|
+
* For cross-origin iframe authentication to work, you MUST configure cookies:
|
|
45
|
+
*
|
|
46
|
+
* ```typescript
|
|
47
|
+
* advanced: {
|
|
48
|
+
* defaultCookieAttributes: {
|
|
49
|
+
* sameSite: 'none', // Allow cross-site cookies
|
|
50
|
+
* secure: true, // HTTPS only (required for sameSite: none)
|
|
51
|
+
* path: '/', // Available across entire site
|
|
52
|
+
* partitioned: true, // CHIPS for Chrome/Edge
|
|
53
|
+
* }
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* **Safari Compatibility**:
|
|
58
|
+
*
|
|
59
|
+
* Safari doesn't support CHIPS. The client plugin automatically handles
|
|
60
|
+
* Safari Storage Access API to request cookie permission from users.
|
|
61
|
+
*
|
|
62
|
+
* @returns Better Auth server plugin
|
|
9
63
|
*
|
|
10
64
|
* @example
|
|
11
65
|
* ```typescript
|
|
66
|
+
* // Basic setup
|
|
12
67
|
* import { betterAuth } from 'better-auth'
|
|
68
|
+
* import { drizzleAdapter } from 'better-auth/adapters/drizzle'
|
|
13
69
|
* import { playcademy } from '@playcademy/better-auth/server'
|
|
70
|
+
* import { db } from './db'
|
|
14
71
|
*
|
|
15
72
|
* export const auth = betterAuth({
|
|
16
73
|
* database: drizzleAdapter(db, { provider: 'sqlite' }),
|
|
74
|
+
*
|
|
75
|
+
* // Required: Platform integration
|
|
17
76
|
* plugins: [playcademy()],
|
|
77
|
+
*
|
|
78
|
+
* // Required: Cross-site cookie support
|
|
18
79
|
* advanced: {
|
|
19
80
|
* defaultCookieAttributes: {
|
|
20
81
|
* sameSite: 'none',
|
|
21
82
|
* secure: true,
|
|
22
83
|
* path: '/',
|
|
23
84
|
* partitioned: true, // CHIPS for Chrome/Edge
|
|
24
|
-
* }
|
|
85
|
+
* }
|
|
86
|
+
* }
|
|
87
|
+
* })
|
|
88
|
+
* ```
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```typescript
|
|
92
|
+
* // With email/password and OAuth
|
|
93
|
+
* export const auth = betterAuth({
|
|
94
|
+
* database: drizzleAdapter(db, { provider: 'sqlite' }),
|
|
95
|
+
*
|
|
96
|
+
* // Platform auth (always included)
|
|
97
|
+
* plugins: [playcademy()],
|
|
98
|
+
*
|
|
99
|
+
* // Optional: Email/password for standalone
|
|
100
|
+
* emailAndPassword: {
|
|
101
|
+
* enabled: true
|
|
102
|
+
* },
|
|
103
|
+
*
|
|
104
|
+
* // Optional: OAuth for standalone
|
|
105
|
+
* socialProviders: {
|
|
106
|
+
* github: {
|
|
107
|
+
* clientId: process.env.GITHUB_CLIENT_ID!,
|
|
108
|
+
* clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
109
|
+
* }
|
|
25
110
|
* },
|
|
111
|
+
*
|
|
112
|
+
* advanced: {
|
|
113
|
+
* defaultCookieAttributes: {
|
|
114
|
+
* sameSite: 'none',
|
|
115
|
+
* secure: true,
|
|
116
|
+
* path: '/',
|
|
117
|
+
* partitioned: true,
|
|
118
|
+
* }
|
|
119
|
+
* }
|
|
26
120
|
* })
|
|
27
121
|
* ```
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```typescript
|
|
125
|
+
* // Use in API route handler
|
|
126
|
+
* import { Context } from 'hono'
|
|
127
|
+
* import { auth } from './lib/auth'
|
|
128
|
+
*
|
|
129
|
+
* export async function GET(c: Context) {
|
|
130
|
+
* const session = await auth.api.getSession({
|
|
131
|
+
* headers: c.req.raw.headers
|
|
132
|
+
* })
|
|
133
|
+
*
|
|
134
|
+
* if (!session) {
|
|
135
|
+
* return c.json({ error: 'Unauthorized' }, 401)
|
|
136
|
+
* }
|
|
137
|
+
*
|
|
138
|
+
* // session.user.playcademyUserId available if from platform
|
|
139
|
+
* return c.json({ user: session.user })
|
|
140
|
+
* }
|
|
141
|
+
* ```
|
|
28
142
|
*/
|
|
29
143
|
export declare const playcademy: () => {
|
|
30
144
|
id: "playcademy";
|
package/dist/server.js
CHANGED
|
@@ -20473,7 +20473,6 @@ Please set the PLAYCADEMY_BASE_URL environment variable`);
|
|
|
20473
20473
|
// src/server.ts
|
|
20474
20474
|
var DEFAULT_SESSION_MAX_AGE = 60 * 60 * 24 * 7;
|
|
20475
20475
|
var DEFAULT_COOKIE_PATH = "/";
|
|
20476
|
-
var DEV_TUNNEL_URL = "https://hbauer.playcademy.dev";
|
|
20477
20476
|
function extractGameToken(authHeader) {
|
|
20478
20477
|
if (!authHeader?.startsWith("Bearer ")) {
|
|
20479
20478
|
throw new Error("Missing or invalid Authorization header");
|
|
@@ -20602,50 +20601,66 @@ var playcademy = () => {
|
|
|
20602
20601
|
}, async (ctx) => {
|
|
20603
20602
|
const authHeader = ctx.request?.headers.get("authorization");
|
|
20604
20603
|
const gameToken = extractGameToken(authHeader ?? "");
|
|
20605
|
-
const
|
|
20606
|
-
const platformApiUrl = isLocalDev ? undefined : DEV_TUNNEL_URL;
|
|
20604
|
+
const authApiUrl = process.env.PLAYCADEMY_AUTH_API_URL || undefined;
|
|
20607
20605
|
let verified = null;
|
|
20608
20606
|
try {
|
|
20609
|
-
verified = await verifyGameToken(gameToken, {
|
|
20610
|
-
|
|
20607
|
+
verified = await verifyGameToken(gameToken, { baseUrl: authApiUrl });
|
|
20608
|
+
} catch (error4) {
|
|
20609
|
+
const errorMessage = error4 instanceof Error ? error4.message : String(error4);
|
|
20610
|
+
const platformMode = ctx.request?.headers.get("x-playcademy-mode");
|
|
20611
|
+
if (platformMode === "platform") {
|
|
20612
|
+
console.error("[Playcademy Auth] Token verification failed:", {
|
|
20613
|
+
error: errorMessage,
|
|
20614
|
+
hasAuthApiOverride: !!authApiUrl
|
|
20615
|
+
});
|
|
20616
|
+
}
|
|
20617
|
+
return new Response(JSON.stringify({ ok: false, code: "invalid_token" }), {
|
|
20618
|
+
status: 200,
|
|
20619
|
+
headers: { "Content-Type": "application/json" }
|
|
20611
20620
|
});
|
|
20612
|
-
} catch {
|
|
20613
|
-
return new Response(null, { status: 204 });
|
|
20614
|
-
}
|
|
20615
|
-
const user = await findOrCreateUser(ctx.context.adapter, verified.user);
|
|
20616
|
-
if (!user) {
|
|
20617
|
-
throw new Error("Failed to find or create user");
|
|
20618
20621
|
}
|
|
20619
|
-
|
|
20620
|
-
|
|
20621
|
-
|
|
20622
|
-
|
|
20623
|
-
if (existingSession) {
|
|
20624
|
-
const isValid = await hasValidSession(ctx.context.adapter, existingSession.token, user.id);
|
|
20625
|
-
if (isValid) {
|
|
20626
|
-
return new Response(null, { status: 204 });
|
|
20622
|
+
try {
|
|
20623
|
+
const user = await findOrCreateUser(ctx.context.adapter, verified.user);
|
|
20624
|
+
if (!user) {
|
|
20625
|
+
throw new Error("Failed to find or create user");
|
|
20627
20626
|
}
|
|
20628
|
-
|
|
20629
|
-
|
|
20630
|
-
|
|
20631
|
-
|
|
20632
|
-
|
|
20633
|
-
|
|
20634
|
-
|
|
20635
|
-
|
|
20636
|
-
|
|
20637
|
-
secret: ctxSecret,
|
|
20638
|
-
maxAge: cookieOpts.maxAge ?? DEFAULT_SESSION_MAX_AGE,
|
|
20639
|
-
path: cookieOpts.path ?? DEFAULT_COOKIE_PATH,
|
|
20640
|
-
userAgent: ctx.request?.headers.get("user-agent") ?? null
|
|
20641
|
-
});
|
|
20642
|
-
return new Response(null, {
|
|
20643
|
-
status: 200,
|
|
20644
|
-
headers: {
|
|
20645
|
-
"Set-Cookie": setCookieValue,
|
|
20646
|
-
"Content-Type": "application/json"
|
|
20627
|
+
const cookieName = ctx.context.authCookies.sessionToken.name;
|
|
20628
|
+
const cookieHeader = ctx.request?.headers.get("cookie") ?? null;
|
|
20629
|
+
const ctxSecret = ctx.context.secret;
|
|
20630
|
+
const existingSession = await readSignedSessionCookie(cookieHeader, cookieName, ctxSecret);
|
|
20631
|
+
if (existingSession) {
|
|
20632
|
+
const isValid = await hasValidSession(ctx.context.adapter, existingSession.token, user.id);
|
|
20633
|
+
if (isValid) {
|
|
20634
|
+
return new Response(null, { status: 204 });
|
|
20635
|
+
}
|
|
20647
20636
|
}
|
|
20648
|
-
|
|
20637
|
+
const session = await ctx.context.internalAdapter.createSession(user.id, ctx);
|
|
20638
|
+
if (!session) {
|
|
20639
|
+
throw new Error("Failed to create session");
|
|
20640
|
+
}
|
|
20641
|
+
const cookieOpts = ctx.context.authCookies.sessionToken.options;
|
|
20642
|
+
const setCookieValue = await buildCrossSiteCookie({
|
|
20643
|
+
cookieName,
|
|
20644
|
+
sessionToken: session.token,
|
|
20645
|
+
secret: ctxSecret,
|
|
20646
|
+
maxAge: cookieOpts.maxAge ?? DEFAULT_SESSION_MAX_AGE,
|
|
20647
|
+
path: cookieOpts.path ?? DEFAULT_COOKIE_PATH,
|
|
20648
|
+
userAgent: ctx.request?.headers.get("user-agent") ?? null
|
|
20649
|
+
});
|
|
20650
|
+
return new Response(null, {
|
|
20651
|
+
status: 204,
|
|
20652
|
+
headers: {
|
|
20653
|
+
"Set-Cookie": setCookieValue
|
|
20654
|
+
}
|
|
20655
|
+
});
|
|
20656
|
+
} catch (error4) {
|
|
20657
|
+
const errorMessage = error4 instanceof Error ? error4.message : String(error4);
|
|
20658
|
+
console.error("[Playcademy Auth] Internal error during token exchange/session setup:", { error: errorMessage });
|
|
20659
|
+
return new Response(JSON.stringify({ ok: false, code: "internal_error" }), {
|
|
20660
|
+
status: 500,
|
|
20661
|
+
headers: { "Content-Type": "application/json" }
|
|
20662
|
+
});
|
|
20663
|
+
}
|
|
20649
20664
|
})
|
|
20650
20665
|
}
|
|
20651
20666
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcademy/better-auth",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./server": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@inquirer/prompts": "^7.9.0",
|
|
31
|
-
"@playcademy/sdk": "0.1.
|
|
31
|
+
"@playcademy/sdk": "0.1.18",
|
|
32
32
|
"@playcademy/utils": "0.0.1",
|
|
33
33
|
"@types/bun": "latest",
|
|
34
34
|
"typescript": "^5.7.2"
|