@playcademy/better-auth 0.0.1 → 0.0.3
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.js +18 -6
- package/dist/server.d.ts +10 -1
- package/dist/server.js +74 -63
- package/package.json +2 -2
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
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* 1. **Platform Mode (iframe)**:
|
|
18
18
|
* - Game receives JWT token from Playcademy platform
|
|
19
19
|
* - Client exchanges token via `/api/auth/playcademy` endpoint
|
|
20
|
-
* - Server verifies token with platform API
|
|
20
|
+
* - Server verifies token with platform API (uses PLAYCADEMY_BASE_URL env var)
|
|
21
21
|
* - Creates or links Better Auth user by `playcademyUserId`
|
|
22
22
|
* - Returns session cookie with CHIPS support
|
|
23
23
|
*
|
|
@@ -30,6 +30,15 @@
|
|
|
30
30
|
* - If a user exists with that `playcademyUserId`, sessions merge
|
|
31
31
|
* - Enables seamless switching between platform and standalone
|
|
32
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
|
+
*
|
|
33
42
|
* **Cookie Configuration**:
|
|
34
43
|
*
|
|
35
44
|
* For cross-origin iframe authentication to work, you MUST configure cookies:
|
package/dist/server.js
CHANGED
|
@@ -20157,35 +20157,20 @@ import { existsSync, readdirSync, statSync } from "fs";
|
|
|
20157
20157
|
import { readFile } from "fs/promises";
|
|
20158
20158
|
import { dirname, parse as parse9, resolve } from "path";
|
|
20159
20159
|
function createTimebackNamespace(client) {
|
|
20160
|
-
let courseId;
|
|
20161
|
-
async function ensureCourseId() {
|
|
20162
|
-
if (courseId)
|
|
20163
|
-
return courseId;
|
|
20164
|
-
try {
|
|
20165
|
-
const integration = await client["request"](`/api/timeback/integrations/${client.gameId}`, "GET");
|
|
20166
|
-
if (!integration || !integration.courseId) {
|
|
20167
|
-
throw new Error("No TimeBack integration found for this game. Please run TimeBack setup first.");
|
|
20168
|
-
}
|
|
20169
|
-
courseId = integration.courseId;
|
|
20170
|
-
return courseId;
|
|
20171
|
-
} catch (error4) {
|
|
20172
|
-
throw new Error(`Failed to fetch courseId: ${error4 instanceof Error ? error4.message : String(error4)}`);
|
|
20173
|
-
}
|
|
20174
|
-
}
|
|
20175
20160
|
function enrichActivityData(data) {
|
|
20176
20161
|
return {
|
|
20177
20162
|
...data,
|
|
20178
|
-
|
|
20179
|
-
appName: data.appName || client.config.name,
|
|
20180
|
-
courseName: data.courseName || client.config.integrations?.timeback?.course.title
|
|
20163
|
+
appName: data.appName || client.config.name
|
|
20181
20164
|
};
|
|
20182
20165
|
}
|
|
20183
20166
|
return {
|
|
20184
|
-
get courseId() {
|
|
20185
|
-
return courseId;
|
|
20186
|
-
},
|
|
20187
20167
|
endActivity: async (studentId, payload) => {
|
|
20188
|
-
|
|
20168
|
+
if (!payload.activityData.grade) {
|
|
20169
|
+
throw new Error("activityData.grade is required for TimeBack integration");
|
|
20170
|
+
}
|
|
20171
|
+
if (!payload.activityData.subject) {
|
|
20172
|
+
throw new Error("activityData.subject is required for TimeBack integration");
|
|
20173
|
+
}
|
|
20189
20174
|
const enrichedActivityData = enrichActivityData(payload.activityData);
|
|
20190
20175
|
return client["request"]("/api/timeback/end-activity", "POST", {
|
|
20191
20176
|
gameId: client.gameId,
|
|
@@ -20193,7 +20178,8 @@ function createTimebackNamespace(client) {
|
|
|
20193
20178
|
activityData: enrichedActivityData,
|
|
20194
20179
|
scoreData: payload.scoreData,
|
|
20195
20180
|
timingData: payload.timingData,
|
|
20196
|
-
xpEarned: payload.xpEarned
|
|
20181
|
+
xpEarned: payload.xpEarned,
|
|
20182
|
+
masteredUnits: payload.masteredUnits
|
|
20197
20183
|
});
|
|
20198
20184
|
}
|
|
20199
20185
|
};
|
|
@@ -20454,7 +20440,20 @@ Please set the PLAYCADEMY_BASE_URL environment variable`);
|
|
|
20454
20440
|
let errorMessage = "Unknown error";
|
|
20455
20441
|
try {
|
|
20456
20442
|
const data = await response.json();
|
|
20457
|
-
|
|
20443
|
+
const errorField = data.error || data.message;
|
|
20444
|
+
if (typeof errorField === "string") {
|
|
20445
|
+
errorMessage = errorField;
|
|
20446
|
+
} else if (errorField && typeof errorField === "object") {
|
|
20447
|
+
const errorObj = errorField;
|
|
20448
|
+
if ("message" in errorObj && typeof errorObj.message === "string") {
|
|
20449
|
+
errorMessage = errorObj.message;
|
|
20450
|
+
if ("code" in errorObj && typeof errorObj.code === "string") {
|
|
20451
|
+
errorMessage = `[${errorObj.code}] ${errorMessage}`;
|
|
20452
|
+
}
|
|
20453
|
+
} else {
|
|
20454
|
+
errorMessage = JSON.stringify(errorField);
|
|
20455
|
+
}
|
|
20456
|
+
}
|
|
20458
20457
|
} catch {
|
|
20459
20458
|
errorMessage = response.statusText || "Unknown error";
|
|
20460
20459
|
}
|
|
@@ -20473,7 +20472,6 @@ Please set the PLAYCADEMY_BASE_URL environment variable`);
|
|
|
20473
20472
|
// src/server.ts
|
|
20474
20473
|
var DEFAULT_SESSION_MAX_AGE = 60 * 60 * 24 * 7;
|
|
20475
20474
|
var DEFAULT_COOKIE_PATH = "/";
|
|
20476
|
-
var DEV_TUNNEL_URL = "https://hbauer.playcademy.dev";
|
|
20477
20475
|
function extractGameToken(authHeader) {
|
|
20478
20476
|
if (!authHeader?.startsWith("Bearer ")) {
|
|
20479
20477
|
throw new Error("Missing or invalid Authorization header");
|
|
@@ -20602,50 +20600,63 @@ var playcademy = () => {
|
|
|
20602
20600
|
}, async (ctx) => {
|
|
20603
20601
|
const authHeader = ctx.request?.headers.get("authorization");
|
|
20604
20602
|
const gameToken = extractGameToken(authHeader ?? "");
|
|
20605
|
-
const
|
|
20606
|
-
const platformApiUrl = isLocalDev ? undefined : DEV_TUNNEL_URL;
|
|
20603
|
+
const authApiUrl = process.env.PLAYCADEMY_AUTH_API_URL || undefined;
|
|
20607
20604
|
let verified = null;
|
|
20608
20605
|
try {
|
|
20609
|
-
verified = await verifyGameToken(gameToken, {
|
|
20610
|
-
|
|
20606
|
+
verified = await verifyGameToken(gameToken, { baseUrl: authApiUrl });
|
|
20607
|
+
} catch (error4) {
|
|
20608
|
+
const platformMode = ctx.request?.headers.get("x-playcademy-mode");
|
|
20609
|
+
if (platformMode === "platform") {
|
|
20610
|
+
const errorMessage = error4 instanceof Error ? error4.message : String(error4);
|
|
20611
|
+
console.error("[Playcademy Auth] Token verification failed:", errorMessage);
|
|
20612
|
+
}
|
|
20613
|
+
return new Response(JSON.stringify({ ok: false, code: "invalid_token" }), {
|
|
20614
|
+
status: 200,
|
|
20615
|
+
headers: { "Content-Type": "application/json" }
|
|
20611
20616
|
});
|
|
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
20617
|
}
|
|
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 });
|
|
20618
|
+
try {
|
|
20619
|
+
const user = await findOrCreateUser(ctx.context.adapter, verified.user);
|
|
20620
|
+
if (!user) {
|
|
20621
|
+
throw new Error("Failed to find or create user");
|
|
20627
20622
|
}
|
|
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"
|
|
20623
|
+
const cookieName = ctx.context.authCookies.sessionToken.name;
|
|
20624
|
+
const cookieHeader = ctx.request?.headers.get("cookie") ?? null;
|
|
20625
|
+
const ctxSecret = ctx.context.secret;
|
|
20626
|
+
const existingSession = await readSignedSessionCookie(cookieHeader, cookieName, ctxSecret);
|
|
20627
|
+
if (existingSession) {
|
|
20628
|
+
const isValid = await hasValidSession(ctx.context.adapter, existingSession.token, user.id);
|
|
20629
|
+
if (isValid) {
|
|
20630
|
+
return new Response(null, { status: 204 });
|
|
20631
|
+
}
|
|
20647
20632
|
}
|
|
20648
|
-
|
|
20633
|
+
const session = await ctx.context.internalAdapter.createSession(user.id, ctx);
|
|
20634
|
+
if (!session) {
|
|
20635
|
+
throw new Error("Failed to create session");
|
|
20636
|
+
}
|
|
20637
|
+
const cookieOpts = ctx.context.authCookies.sessionToken.options;
|
|
20638
|
+
const setCookieValue = await buildCrossSiteCookie({
|
|
20639
|
+
cookieName,
|
|
20640
|
+
sessionToken: session.token,
|
|
20641
|
+
secret: ctxSecret,
|
|
20642
|
+
maxAge: cookieOpts.maxAge ?? DEFAULT_SESSION_MAX_AGE,
|
|
20643
|
+
path: cookieOpts.path ?? DEFAULT_COOKIE_PATH,
|
|
20644
|
+
userAgent: ctx.request?.headers.get("user-agent") ?? null
|
|
20645
|
+
});
|
|
20646
|
+
return new Response(null, {
|
|
20647
|
+
status: 204,
|
|
20648
|
+
headers: {
|
|
20649
|
+
"Set-Cookie": setCookieValue
|
|
20650
|
+
}
|
|
20651
|
+
});
|
|
20652
|
+
} catch (error4) {
|
|
20653
|
+
const errorMessage = error4 instanceof Error ? error4.message : String(error4);
|
|
20654
|
+
console.error("[Playcademy Auth] Internal error during token exchange/session setup:", { error: errorMessage });
|
|
20655
|
+
return new Response(JSON.stringify({ ok: false, code: "internal_error" }), {
|
|
20656
|
+
status: 500,
|
|
20657
|
+
headers: { "Content-Type": "application/json" }
|
|
20658
|
+
});
|
|
20659
|
+
}
|
|
20649
20660
|
})
|
|
20650
20661
|
}
|
|
20651
20662
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcademy/better-auth",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
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.
|
|
31
|
+
"@playcademy/sdk": "0.2.2",
|
|
32
32
|
"@playcademy/utils": "0.0.1",
|
|
33
33
|
"@types/bun": "latest",
|
|
34
34
|
"typescript": "^5.7.2"
|