@edpire/sdk 0.6.0 → 0.6.1

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.
@@ -0,0 +1,6 @@
1
+ # Your Edpire API key — get one at https://edpire.com/org/integrations
2
+ # NEVER prefix this with VITE_ or NEXT_PUBLIC_ — it must stay server-side only.
3
+ EDPIRE_API_KEY=edp_live_your_key_here
4
+
5
+ # The assessment UUID to embed (find it in the Edpire dashboard)
6
+ NEXT_PUBLIC_ASSESSMENT_ID=your-assessment-uuid-here
@@ -0,0 +1,66 @@
1
+ # Edpire SDK — Next.js example
2
+
3
+ Embeds an Edpire assessment player inside a Next.js App Router page using:
4
+ - `createEdpireTokenHandler()` for the server token endpoint (one call, one file)
5
+ - `<EdpireAssessmentPlayer>` for the browser component (no useRef/useEffect needed)
6
+
7
+ ```
8
+ Your browser → POST /api/edpire/token
9
+ Next.js server → mintEmbedToken(assessmentId, session.userId) ← API key never leaves the server
10
+ Browser → EdpireAssessmentPlayer renders the full assessment UI
11
+ ```
12
+
13
+ ## Quickstart
14
+
15
+ ```bash
16
+ cp .env.example .env.local
17
+ # Fill in EDPIRE_API_KEY (from edpire.com/org/integrations) and NEXT_PUBLIC_ASSESSMENT_ID
18
+ npm install
19
+ npm run dev
20
+ ```
21
+
22
+ Open http://localhost:3000 — the player loads.
23
+
24
+ ## How the token endpoint works
25
+
26
+ `app/api/edpire/token/route.ts` exports `createEdpireTokenHandler()` directly as the `POST` handler. In production you replace the cookie-reading `resolveLearner` stub with your real auth session:
27
+
28
+ ```typescript
29
+ // With NextAuth
30
+ resolveLearner: async (req) => {
31
+ const session = await getServerSession(authOptions)
32
+ return session?.user.id ?? null
33
+ }
34
+
35
+ // With Better Auth
36
+ resolveLearner: async (req) => {
37
+ const session = await auth.api.getSession({ headers: req.headers })
38
+ return session?.user.id ?? null
39
+ }
40
+ ```
41
+
42
+ Returning `null` rejects with 401 — the player never receives a token.
43
+
44
+ ## Allowed Origins
45
+
46
+ If you see a blank player (no error, no content), your app's origin is not in Edpire's allow-list:
47
+
48
+ 1. Go to **edpire.com → Settings → Integrations → Allowed Origins**
49
+ 2. Add `http://localhost:3000` for dev, your production domain for prod
50
+ 3. Reload — the player should appear
51
+
52
+ See [SDK Troubleshooting](https://docs.edpire.com/developer/sdk/troubleshooting) for more.
53
+
54
+ ## Production
55
+
56
+ This example works in both `next dev` and `next build && next start`. The `/api/edpire/token` route is a real server-side endpoint in both modes — not a dev-only workaround.
57
+
58
+ ## Project structure
59
+
60
+ ```
61
+ app/
62
+ api/edpire/token/route.ts ← server: mints embed tokens
63
+ layout.tsx
64
+ page.tsx ← client: renders <EdpireAssessmentPlayer>
65
+ .env.example
66
+ ```
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Token-minting endpoint for the Edpire Embedded Player.
3
+ *
4
+ * The browser POSTs { assessmentId } here; this handler resolves the learner
5
+ * from the session and returns a short-lived, single-use embed token.
6
+ *
7
+ * Powered by createEdpireTokenHandler() — one call handles auth checking,
8
+ * token minting, and error mapping.
9
+ */
10
+ import { createEdpireTokenHandler } from "@edpire/sdk/client"
11
+
12
+ export const POST = createEdpireTokenHandler({
13
+ apiKey: process.env.EDPIRE_API_KEY!,
14
+
15
+ /**
16
+ * Resolve the learner ID from YOUR auth system.
17
+ *
18
+ * This example reads a `userId` cookie set by a login flow.
19
+ * In a real app replace this with your session/auth library:
20
+ *
21
+ * @example Next.js + NextAuth
22
+ * const session = await getServerSession(req, res, authOptions)
23
+ * return session?.user?.id ?? null
24
+ *
25
+ * @example Next.js + Better Auth
26
+ * const session = await auth.api.getSession({ headers: req.headers })
27
+ * return session?.user.id ?? null
28
+ *
29
+ * Returning null → 401 Unauthorized (no token minted).
30
+ * Never read the learner ID from the request body — that's an open relay.
31
+ */
32
+ resolveLearner: (req) => {
33
+ // Demo: read userId from a signed cookie. Replace with your auth session.
34
+ const cookie = req.headers.get("cookie") ?? ""
35
+ const match = /(?:^|;\s*)userId=([^;]+)/.exec(cookie)
36
+ return match ? decodeURIComponent(match[1]) : null
37
+ },
38
+ })
@@ -0,0 +1,9 @@
1
+ /* Reset body margin so the player fills the viewport without gaps. */
2
+ * {
3
+ box-sizing: border-box;
4
+ }
5
+
6
+ body {
7
+ margin: 0;
8
+ padding: 0;
9
+ }
@@ -0,0 +1,13 @@
1
+ import type { Metadata } from "next"
2
+
3
+ export const metadata: Metadata = {
4
+ title: "Edpire SDK — Next.js example",
5
+ }
6
+
7
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
8
+ return (
9
+ <html lang="en">
10
+ <body style={{ margin: 0 }}>{children}</body>
11
+ </html>
12
+ )
13
+ }
@@ -0,0 +1,51 @@
1
+ "use client"
2
+
3
+ /**
4
+ * Example page: embed an Edpire assessment player.
5
+ *
6
+ * How it works:
7
+ * 1. EdpireAssessmentPlayer POSTs { assessmentId } to /api/edpire/token.
8
+ * 2. The route handler resolves the learner from the session, mints a token,
9
+ * and returns { token }.
10
+ * 3. The player fetches the assessment content from Edpire using the token,
11
+ * renders the full UI, and calls onComplete when the learner submits.
12
+ *
13
+ * You never see the API key in the browser.
14
+ */
15
+ import { EdpireAssessmentPlayer } from "@edpire/sdk/react"
16
+
17
+ const ASSESSMENT_ID = process.env.NEXT_PUBLIC_ASSESSMENT_ID
18
+
19
+ export default function Page() {
20
+ if (!ASSESSMENT_ID) {
21
+ return (
22
+ <div style={{ padding: 32, fontFamily: "sans-serif" }}>
23
+ <strong>Set NEXT_PUBLIC_ASSESSMENT_ID in .env.local</strong>
24
+ <p>Copy .env.example → .env.local and fill in your assessment ID.</p>
25
+ </div>
26
+ )
27
+ }
28
+
29
+ return (
30
+ <EdpireAssessmentPlayer
31
+ tokenEndpoint="/api/edpire/token"
32
+ assessmentId={ASSESSMENT_ID}
33
+ onComplete={(result) => {
34
+ console.log("Assessment complete!", {
35
+ score: result.score,
36
+ max: result.max_score,
37
+ passed: result.passed,
38
+ })
39
+ // TODO: redirect to results page, update progress, etc.
40
+ }}
41
+ onError={(err) => {
42
+ console.error("Embed error:", err.code, err.message)
43
+ // err.code is one of: TOKEN_INVALID | TOKEN_EXPIRED | TOKEN_USED |
44
+ // ORIGIN_NOT_ALLOWED | ASSESSMENT_NOT_FOUND | MAX_ATTEMPTS_REACHED |
45
+ // NETWORK_ERROR | UNKNOWN
46
+ }}
47
+ // The player fills its container. style/className controls the height.
48
+ style={{ width: "100%", height: "100vh" }}
49
+ />
50
+ )
51
+ }
@@ -0,0 +1,8 @@
1
+ import type { NextConfig } from "next"
2
+
3
+ const nextConfig: NextConfig = {
4
+ // No special config needed for @edpire/sdk.
5
+ // The SDK is a standard ESM package — Next.js resolves it from node_modules.
6
+ }
7
+
8
+ export default nextConfig
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "edpire-sdk-example-nextjs",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start"
9
+ },
10
+ "dependencies": {
11
+ "@edpire/sdk": "^0.6.0",
12
+ "next": "^15.0.0",
13
+ "react": "^19.0.0",
14
+ "react-dom": "^19.0.0"
15
+ },
16
+ "devDependencies": {
17
+ "@types/node": "^20.0.0",
18
+ "@types/react": "^19.0.0",
19
+ "@types/react-dom": "^19.0.0",
20
+ "typescript": "^5.0.0"
21
+ }
22
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "plugins": [{ "name": "next" }]
17
+ },
18
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
19
+ "exclude": ["node_modules"]
20
+ }
@@ -0,0 +1,10 @@
1
+ # Your Edpire API key — get one at https://edpire.com/org/integrations
2
+ # NEVER prefix with VITE_ — that would expose the key in the browser bundle.
3
+ EDPIRE_API_KEY=edp_live_your_key_here
4
+
5
+ # The assessment UUID to embed (find it in the Edpire dashboard).
6
+ # VITE_ prefix is safe here because this is a public ID, not a secret.
7
+ VITE_ASSESSMENT_ID=your-assessment-uuid-here
8
+
9
+ # Port for the Express server
10
+ PORT=3001
@@ -0,0 +1,69 @@
1
+ # Edpire SDK — Vite + Express example
2
+
3
+ Embeds an Edpire assessment player using:
4
+ - **Vite** for the React frontend (browser)
5
+ - **Express** for the token endpoint (server) — works in both dev and production
6
+
7
+ ```
8
+ Browser → POST /api/edpire/token (via Vite proxy in dev, Express directly in prod)
9
+ Express → mintEmbedToken(assessmentId, session.userId) ← API key never leaves the server
10
+ Browser → <EdpireAssessmentPlayer> renders the full assessment UI
11
+ ```
12
+
13
+ This is the production-correct version of the "put it in vite.config.ts" pattern. The Express server runs separately and survives `vite build` + `vite preview`.
14
+
15
+ ## Quickstart
16
+
17
+ ```bash
18
+ cp .env.example .env
19
+ # Fill in EDPIRE_API_KEY (from edpire.com/org/integrations) and VITE_ASSESSMENT_ID
20
+ npm install
21
+ npm run dev
22
+ ```
23
+
24
+ Two processes start:
25
+ - **Express** on port 3001 (token endpoint)
26
+ - **Vite** on port 5173 (dev server, proxies /api → port 3001)
27
+
28
+ Open http://localhost:5173 — the player loads.
29
+
30
+ ## How it differs from vite.config.ts middleware
31
+
32
+ The test project at `edpire-sdk-test` put the token endpoint inside a Vite dev-server middleware. That's clean for local dev, but it only runs during `vite dev` — a production build has no `/api/edpire/token` and the embed silently fails.
33
+
34
+ This example runs a real Express server that handles the token endpoint in both dev and production. The Vite proxy in dev routes `/api` requests to Express; in production, Express serves everything.
35
+
36
+ ## Env variables
37
+
38
+ | Variable | Where used | VITE_ prefix? |
39
+ |---|---|---|
40
+ | `EDPIRE_API_KEY` | Express server | **No** — prefixing it would leak it to the browser bundle |
41
+ | `VITE_ASSESSMENT_ID` | Vite/browser | **Yes** — this is a public ID, not a secret |
42
+
43
+ This is the key insight: `VITE_` prefixed variables are compiled into the browser bundle. The API key must never be prefixed.
44
+
45
+ ## Allowed Origins
46
+
47
+ If you see a blank player, your app's origin is not in Edpire's allow-list:
48
+
49
+ 1. Go to **edpire.com → Settings → Integrations → Allowed Origins**
50
+ 2. Add `http://localhost:5173` for dev, your production domain for prod
51
+ 3. Reload — the player should appear
52
+
53
+ ## Production
54
+
55
+ ```bash
56
+ npm run build # builds Vite frontend → dist/ + compiles server.ts
57
+ NODE_ENV=production npm run preview # starts Express serving dist/ + /api/edpire/token
58
+ ```
59
+
60
+ ## Project structure
61
+
62
+ ```
63
+ server.ts ← Express: /api/edpire/token endpoint + static files in prod
64
+ vite.config.ts ← Vite: dev server, proxies /api → Express
65
+ src/
66
+ App.tsx ← React: <EdpireAssessmentPlayer>
67
+ main.tsx
68
+ .env.example
69
+ ```
@@ -0,0 +1,16 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Edpire SDK — Vite + Express example</title>
7
+ <style>
8
+ * { box-sizing: border-box; }
9
+ body { margin: 0; }
10
+ </style>
11
+ </head>
12
+ <body>
13
+ <div id="root"></div>
14
+ <script type="module" src="/src/main.tsx"></script>
15
+ </body>
16
+ </html>
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "edpire-sdk-example-vite-express",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "concurrently -k -n SERVER,VITE -c cyan,magenta \"tsx server.ts\" \"vite\"",
7
+ "build": "vite build && tsc -p tsconfig.server.json",
8
+ "preview": "tsx server.ts",
9
+ "type-check": "tsc --noEmit"
10
+ },
11
+ "dependencies": {
12
+ "@edpire/sdk": "^0.6.0",
13
+ "cors": "^2.8.5",
14
+ "express": "^4.18.2",
15
+ "react": "^19.0.0",
16
+ "react-dom": "^19.0.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/cors": "^2.8.17",
20
+ "@types/express": "^4.17.21",
21
+ "@types/node": "^20.0.0",
22
+ "@types/react": "^19.0.0",
23
+ "@types/react-dom": "^19.0.0",
24
+ "@vitejs/plugin-react": "^4.0.0",
25
+ "concurrently": "^8.2.2",
26
+ "dotenv": "^16.3.1",
27
+ "tsx": "^4.7.0",
28
+ "typescript": "^5.0.0",
29
+ "vite": "^5.0.0"
30
+ }
31
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Express server — serves the token endpoint + static files in production.
3
+ *
4
+ * In development: Vite dev server runs separately (port 5173) and proxies
5
+ * /api requests here (port 3001). Run `npm run dev` to start both.
6
+ *
7
+ * In production: this server serves the Vite build output (dist/) and the
8
+ * /api/edpire/token endpoint. Run `npm run build` then `npm run preview`.
9
+ */
10
+ import "dotenv/config"
11
+ import express from "express"
12
+ import cors from "cors"
13
+ import path from "node:path"
14
+ import { fileURLToPath } from "node:url"
15
+ import { createEdpireTokenHandler, toNodeHandler } from "@edpire/sdk/client"
16
+
17
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
18
+ const PORT = Number(process.env.PORT ?? 3001)
19
+ const isDev = process.env.NODE_ENV !== "production"
20
+
21
+ const app = express()
22
+
23
+ // In dev, the Vite dev server (port 5173) calls this server for /api requests.
24
+ // Allow cross-origin requests from localhost in development.
25
+ if (isDev) {
26
+ app.use(cors({ origin: /localhost/ }))
27
+ }
28
+
29
+ // ── Token endpoint ─────────────────────────────────────────────────────────────
30
+ //
31
+ // The browser POSTs { assessmentId } here and receives { token }.
32
+ // The learner ID is resolved from the server session — never from the body.
33
+ //
34
+ // toNodeHandler() adapts the Web-standard createEdpireTokenHandler() response
35
+ // to Express's (req, res) signature.
36
+ const edpireTokenHandler = toNodeHandler(
37
+ createEdpireTokenHandler({
38
+ apiKey: process.env.EDPIRE_API_KEY!,
39
+
40
+ /**
41
+ * Resolve the learner ID from YOUR session.
42
+ *
43
+ * This example reads a `userId` cookie set by your login flow.
44
+ * Replace with your real session middleware in production:
45
+ *
46
+ * @example express-session
47
+ * resolveLearner: (req) => req.session?.userId ?? null
48
+ *
49
+ * @example JWT
50
+ * resolveLearner: (req) => {
51
+ * const { userId } = jwt.verify(req.cookies.token, JWT_SECRET)
52
+ * return userId ?? null
53
+ * }
54
+ *
55
+ * Returning null → 401 (no token minted).
56
+ */
57
+ resolveLearner: (req) => {
58
+ const cookie = req.headers.get("cookie") ?? ""
59
+ const match = /(?:^|;\s*)userId=([^;]+)/.exec(cookie)
60
+ return match ? decodeURIComponent(match[1]) : null
61
+ },
62
+ }),
63
+ )
64
+
65
+ app.post("/api/edpire/token", edpireTokenHandler)
66
+
67
+ // ── Static files (production only) ────────────────────────────────────────────
68
+ if (!isDev) {
69
+ const distPath = path.join(__dirname, "dist")
70
+ app.use(express.static(distPath))
71
+ // SPA fallback — send index.html for all non-API routes
72
+ app.get(/^(?!\/api).*$/, (_req, res) => {
73
+ res.sendFile(path.join(distPath, "index.html"))
74
+ })
75
+ }
76
+
77
+ app.listen(PORT, () => {
78
+ if (isDev) {
79
+ console.log(`[server] Token endpoint ready at http://localhost:${PORT}/api/edpire/token`)
80
+ console.log(`[server] Start Vite dev server with: npm run dev`)
81
+ } else {
82
+ console.log(`[server] Production server at http://localhost:${PORT}`)
83
+ }
84
+ })
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Edpire SDK — Vite + Express example
3
+ *
4
+ * How it works:
5
+ * 1. App.tsx renders <EdpireAssessmentPlayer> which POSTs { assessmentId }
6
+ * to /api/edpire/token (proxied to the Express server in dev).
7
+ * 2. The Express server (server.ts) resolves the learner from the session,
8
+ * mints a token via EdpireClient, and returns { token }.
9
+ * 3. The player fetches assessment content from Edpire using the token and
10
+ * renders the full UI.
11
+ *
12
+ * The secret EDPIRE_API_KEY never touches the browser.
13
+ */
14
+ import { EdpireAssessmentPlayer } from "@edpire/sdk/react"
15
+
16
+ // VITE_ASSESSMENT_ID is safe to expose in the browser (it's a public ID, not a secret).
17
+ const ASSESSMENT_ID = import.meta.env.VITE_ASSESSMENT_ID as string | undefined
18
+
19
+ export default function App() {
20
+ if (!ASSESSMENT_ID) {
21
+ return (
22
+ <div style={{ padding: 32, fontFamily: "sans-serif" }}>
23
+ <strong>Set VITE_ASSESSMENT_ID in .env</strong>
24
+ <p>Copy .env.example → .env and fill in your assessment ID.</p>
25
+ <p>
26
+ <em>Note: EDPIRE_API_KEY must NOT have the VITE_ prefix — that would leak it to the
27
+ browser. Only the assessment ID is prefixed.</em>
28
+ </p>
29
+ </div>
30
+ )
31
+ }
32
+
33
+ return (
34
+ <EdpireAssessmentPlayer
35
+ tokenEndpoint="/api/edpire/token"
36
+ assessmentId={ASSESSMENT_ID}
37
+ onComplete={(result) => {
38
+ console.log("Assessment complete!", {
39
+ score: result.score,
40
+ max: result.max_score,
41
+ passed: result.passed,
42
+ })
43
+ // TODO: redirect to results page, update progress, etc.
44
+ }}
45
+ onError={(err) => {
46
+ console.error("Embed error:", err.code, err.message)
47
+ // Common errors:
48
+ // ORIGIN_NOT_ALLOWED → add localhost to Allowed Origins in the dashboard
49
+ // TOKEN_INVALID → check EDPIRE_API_KEY in .env
50
+ // ASSESSMENT_NOT_FOUND → check VITE_ASSESSMENT_ID in .env
51
+ }}
52
+ // The player fills its container. Set a height here or via CSS.
53
+ style={{ width: "100%", height: "100vh" }}
54
+ />
55
+ )
56
+ }
@@ -0,0 +1,9 @@
1
+ import React from "react"
2
+ import { createRoot } from "react-dom/client"
3
+ import App from "./App"
4
+
5
+ createRoot(document.getElementById("root")!).render(
6
+ <React.StrictMode>
7
+ <App />
8
+ </React.StrictMode>,
9
+ )
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": true,
10
+ "isolatedModules": true,
11
+ "moduleDetection": "force",
12
+ "noEmit": true,
13
+ "jsx": "react-jsx",
14
+ "strict": true
15
+ },
16
+ "include": ["src"]
17
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "noEmit": false,
5
+ "outDir": "dist-server",
6
+ "module": "ESNext",
7
+ "moduleResolution": "node",
8
+ "target": "node18",
9
+ "allowImportingTsExtensions": false
10
+ },
11
+ "include": ["server.ts"]
12
+ }
@@ -0,0 +1,18 @@
1
+ import { defineConfig } from "vite"
2
+ import react from "@vitejs/plugin-react"
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+
7
+ server: {
8
+ port: 5173,
9
+ proxy: {
10
+ // In development, proxy /api requests to the Express server.
11
+ // This keeps the secret EDPIRE_API_KEY server-side (never in the browser bundle).
12
+ "/api": {
13
+ target: "http://localhost:3001",
14
+ changeOrigin: true,
15
+ },
16
+ },
17
+ },
18
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edpire/sdk",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Embeddable JS SDK for Edpire assessments",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://docs.edpire.com/developer/sdk/overview",
@@ -40,7 +40,8 @@
40
40
  },
41
41
  "files": [
42
42
  "dist",
43
- "src/styles"
43
+ "src/styles",
44
+ "examples"
44
45
  ],
45
46
  "dependencies": {
46
47
  "@dnd-kit/core": "^6.3.1",