@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 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
- * Automatically show storage access prompt for Safari users
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
- * @default true
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
- * Automatically handles:
41
- * - Platform vs standalone mode detection
42
- * - Token exchange for platform mode
43
- * - Safari Storage Access API (auto-prompt)
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
- * // Default: Auto-handles everything including Safari
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 auto Safari prompt
111
+ * // Custom: Disable Safari auto-prompt
56
112
  * const auth = createAuthClient({
113
+ * baseURL: 'http://localhost:8788',
57
114
  * plugins: [
58
115
  * playcademy({
59
- * safari: { autoPrompt: false }
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
- await fetch("/api/auth/playcademy", {
122
- method: "POST",
123
- headers: { Authorization: `Bearer ${token}` },
124
- credentials: "include"
125
- });
126
- exchangeComplete = true;
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 plugin for Better Auth
2
+ * Playcademy Platform Authentication Plugin (Server)
3
3
  *
4
- * This plugin provides:
5
- * - Platform user schema field (playcademyUserId)
6
- * - Token exchange endpoint (POST /api/auth/playcademy)
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 isLocalDev = ctx.request?.url.includes("localhost");
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
- baseUrl: platformApiUrl
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
- const cookieName = ctx.context.authCookies.sessionToken.name;
20620
- const cookieHeader = ctx.request?.headers.get("cookie") ?? null;
20621
- const ctxSecret = ctx.context.secret;
20622
- const existingSession = await readSignedSessionCookie(cookieHeader, cookieName, ctxSecret);
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
- const session = await ctx.context.internalAdapter.createSession(user.id, ctx);
20630
- if (!session) {
20631
- throw new Error("Failed to create session");
20632
- }
20633
- const cookieOpts = ctx.context.authCookies.sessionToken.options;
20634
- const setCookieValue = await buildCrossSiteCookie({
20635
- cookieName,
20636
- sessionToken: session.token,
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.1-alpha.1",
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.13",
31
+ "@playcademy/sdk": "0.1.18",
32
32
  "@playcademy/utils": "0.0.1",
33
33
  "@types/bun": "latest",
34
34
  "typescript": "^5.7.2"