@edpire/sdk 0.5.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/LICENSE ADDED
@@ -0,0 +1,51 @@
1
+ Edpire SDK License Agreement
2
+
3
+ Copyright (c) 2026 Edpire (Youssef Almia). All rights reserved.
4
+
5
+ This software, including its source code, compiled bundles, and accompanying
6
+ files (the "Software"), is the proprietary property of Edpire. By downloading,
7
+ installing, or using the Software you agree to the terms below.
8
+
9
+ 1. License grant.
10
+ Edpire grants you a non-exclusive, non-transferable, revocable license to
11
+ download, install, and use the Software for the sole purpose of integrating
12
+ your own application or service with the Edpire platform via Edpire's
13
+ official APIs and services.
14
+
15
+ 2. Restrictions.
16
+ Except as expressly permitted in Section 1, you may NOT:
17
+ (a) copy, redistribute, sublicense, sell, rent, or otherwise make the
18
+ Software (in source or compiled form) available to any third party as a
19
+ standalone product;
20
+ (b) modify, adapt, translate, or create derivative works of the Software,
21
+ except for configuration and integration code that calls the Software's
22
+ public interfaces;
23
+ (c) reverse engineer, decompile, or disassemble the Software, or use it or
24
+ any portion of it, to design, develop, or operate a product or service
25
+ that competes with the Edpire platform;
26
+ (d) remove, alter, or obscure any proprietary notices in the Software.
27
+
28
+ The presence of human-readable code in any distributed bundle does not grant
29
+ any right to reuse it beyond the license granted in Section 1.
30
+
31
+ 3. Ownership.
32
+ The Software is licensed, not sold. Edpire retains all right, title, and
33
+ interest in and to the Software, including all intellectual property rights.
34
+
35
+ 4. No warranty.
36
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
37
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
38
+ FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
39
+
40
+ 5. Limitation of liability.
41
+ IN NO EVENT SHALL EDPIRE BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
42
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING
43
+ FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
44
+ DEALINGS IN THE SOFTWARE.
45
+
46
+ 6. Termination.
47
+ This license terminates automatically if you breach any of its terms. Upon
48
+ termination you must cease all use of the Software and destroy all copies in
49
+ your possession.
50
+
51
+ For licensing inquiries beyond the scope of this grant, contact Edpire.
package/README.md ADDED
@@ -0,0 +1,443 @@
1
+ # @edpire/sdk
2
+
3
+ Embeddable assessment SDK for Edpire. Deliver, render, and grade assessments from your own application.
4
+
5
+ > ๐Ÿ“š **Full documentation** lives in the Edpire docs site under [**SDK Reference**](https://docs.edpire.com/developer/sdk/overview) โ€” complete option/prop/type tables for the Embedded Player, React components, Custom Flow, server client, and branding. This README is a condensed quick-start.
6
+
7
+ ---
8
+
9
+ ## Which approach fits you?
10
+
11
+ | Approach | Learner stays on your page? | You build the UI? | Entry point |
12
+ |---|---|---|---|
13
+ | **[Hosted Redirect](#hosted-redirect)** | No | No | Share URL only |
14
+ | **[Embedded Player](#embedded-player)** | Yes | No | `EdpireAssessment.mount()` |
15
+ | **[Custom Flow](#custom-flow)** | Yes | Yes | `renderQuestion` + `flattenAssessment` |
16
+
17
+ > **Server Client** (`EdpireClient`) is a backend utility used alongside any of the above โ€” not a separate approach. [Jump to Server Client โ†’](#server-client)
18
+
19
+ ---
20
+
21
+ ## Installation
22
+
23
+ Published to the public npm registry โ€” no auth token required.
24
+
25
+ ```bash
26
+ npm install @edpire/sdk
27
+ # or
28
+ pnpm add @edpire/sdk
29
+ ```
30
+
31
+ > **License:** the package is free to install but carries a proprietary, integrate-with-Edpire-only license (see `LICENSE`). It is safe to make public because the bundle is only the question renderer โ€” answer keys, grading, and API keys never ship in it; they stay on the server.
32
+
33
+ ---
34
+
35
+ ## Hosted Redirect
36
+
37
+ The simplest integration โ€” no SDK required on the frontend. Redirect learners to Edpire's hosted assessment UI. After completion, Edpire redirects back to your `return_url` with results.
38
+
39
+ ```
40
+ https://{your-slug}.edpire.com/take/{shareCode}?learner_ref={userId}&return_url={yourUrl}
41
+ ```
42
+
43
+ | Parameter | Required | Description |
44
+ |-----------|----------|-------------|
45
+ | `shareCode` | Yes | Assessment share code from the dashboard |
46
+ | `learner_ref` | Recommended | Your stable internal user ID |
47
+ | `return_url` | Recommended | Where to send the learner after completion |
48
+
49
+ Edpire appends `?submission_id=...&score=...&max_score=...` to the return URL.
50
+
51
+ **When to use:** Fast adoption, no custom UI needed, comfortable with a browser redirect.
52
+
53
+ ---
54
+
55
+ ## Embedded Player
56
+
57
+ Mount Edpire's full assessment player inline inside your page. The SDK fetches the assessment, handles all interactions, grades the submission, and shows per-question feedback โ€” all automatically. The learner never leaves your app.
58
+
59
+ **Server (Node.js / API route):**
60
+
61
+ ```typescript
62
+ import { EdpireClient } from "@edpire/sdk/client"
63
+
64
+ const client = new EdpireClient({ apiKey: process.env.EDPIRE_API_KEY! })
65
+
66
+ // Mint a short-lived single-use token for this learner + assessment
67
+ const { token } = await client.mintEmbedToken(assessmentId, userId)
68
+ // Pass `token` to the browser (e.g. via server-rendered props or an API response)
69
+ ```
70
+
71
+ **Browser:**
72
+
73
+ ```typescript
74
+ import { EdpireAssessment } from "@edpire/sdk"
75
+
76
+ const embed = EdpireAssessment.mount({
77
+ token, // from your server
78
+ container: "#assessment-root", // CSS selector or HTMLElement
79
+ onComplete: (result) => {
80
+ console.log(`Score: ${result.score}/${result.max_score}`)
81
+ console.log(`Passed: ${result.passed}`)
82
+ },
83
+ onError: (err) => console.error(err.message),
84
+ })
85
+
86
+ // Later, when done:
87
+ embed.unmount()
88
+ ```
89
+
90
+ **CDN (no bundler):**
91
+
92
+ ```html
93
+ <script src="https://cdn.jsdelivr.net/npm/@edpire/sdk@0.5.0/dist/umd/index.global.js"></script>
94
+ <script>
95
+ EdpireSDK.EdpireAssessment.mount({ token, container: "#root", onComplete: handleDone })
96
+ </script>
97
+ ```
98
+
99
+ > The UMD build is served straight from npm by [jsDelivr](https://www.jsdelivr.com/) (and [unpkg](https://unpkg.com/)) โ€” no CDN to run yourself. Pin a version for production; `@latest` works for prototyping.
100
+
101
+ **When to use:** You want the full assessment experience inside your page with a single function call. Zero UI code required.
102
+
103
+ ---
104
+
105
+ ## Custom Flow
106
+
107
+ Build your own question-by-question UX โ€” Duolingo-style flows, flashcard drills, practice modes, custom navigation. The SDK handles question rendering and answer collection; you own everything else.
108
+
109
+ **How it works:**
110
+ 1. Your server fetches the assessment (keeping your API key server-side)
111
+ 2. `flattenAssessment()` converts it into a flat array of question steps
112
+ 3. `renderQuestion()` (or `<EdpireQuestion>` in React) renders each step into a container you control
113
+ 4. Your server grades each answer via the `/check` endpoint
114
+ 5. After all questions, your server submits the full attempt
115
+
116
+ **Server โ€” fetch the assessment:**
117
+
118
+ ```typescript
119
+ // GET /api/edpire/assessment/[id]/route.ts
120
+ import { EdpireClient } from "@edpire/sdk/client"
121
+
122
+ const client = new EdpireClient({ apiKey: process.env.EDPIRE_API_KEY! })
123
+ const assessment = await client.getAssessment(assessmentId)
124
+ return NextResponse.json({ data: assessment })
125
+ ```
126
+
127
+ **Browser โ€” flatten and render:**
128
+
129
+ ```tsx
130
+ // React
131
+ import { flattenAssessment } from "@edpire/sdk"
132
+ import { EdpireQuestion } from "@edpire/sdk/react"
133
+ import type { RuntimeAnswer } from "@edpire/sdk"
134
+
135
+ const steps = flattenAssessment(assessment)
136
+ // steps[i] = { exerciseId, questionId, content, points, sequenceNumber, index }
137
+
138
+ const [currentIndex, setCurrentIndex] = useState(0)
139
+ const [answers, setAnswers] = useState<RuntimeAnswer[]>([])
140
+ const [feedback, setFeedback] = useState(null)
141
+ const step = steps[currentIndex]
142
+
143
+ <EdpireQuestion
144
+ content={step.content}
145
+ onAnswersChange={setAnswers}
146
+ feedback={feedback} // null = no feedback shown; pass result of /check to show it
147
+ dir="ltr" // or "rtl" for Arabic
148
+ />
149
+ ```
150
+
151
+ ```typescript
152
+ // Vanilla JS
153
+ import { flattenAssessment, renderQuestion } from "@edpire/sdk"
154
+
155
+ const steps = flattenAssessment(assessment)
156
+ const step = steps[currentIndex]
157
+
158
+ const question = renderQuestion({
159
+ container: document.getElementById("question-root"),
160
+ content: step.content,
161
+ onAnswersChange: (answers) => { /* store answers */ },
162
+ })
163
+
164
+ // Later, update feedback:
165
+ question.setFeedback(feedbackFromCheckEndpoint)
166
+
167
+ // Move to next question:
168
+ question.setContent(steps[nextIndex].content)
169
+ ```
170
+
171
+ **Server โ€” grade each question via your proxy:**
172
+
173
+ ```typescript
174
+ // POST /api/edpire/check/[id]/route.ts (recommended โ€” type-safe, handles URL construction)
175
+ import { EdpireClient } from "@edpire/sdk/client"
176
+
177
+ const client = new EdpireClient({ apiKey: process.env.EDPIRE_API_KEY! })
178
+
179
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
180
+ const { id } = await params
181
+ const body = await req.json()
182
+ // Always set include_correct_answers server-side โ€” never trust the client to send it
183
+ const result = await client.checkQuestion(id, { ...body, include_correct_answers: true })
184
+ return Response.json(result) // result: CheckResult = { correct, score, max_score, feedback }
185
+ }
186
+ ```
187
+
188
+ <details>
189
+ <summary>Alternative โ€” raw fetch without EdpireClient</summary>
190
+
191
+ ```typescript
192
+ // Note: assessmentId goes in the URL path, not the request body
193
+ const result = await fetch(`https://edpire.com/api/v1/assessments/${assessmentId}/check`, {
194
+ method: "POST",
195
+ headers: {
196
+ Authorization: `Bearer ${process.env.EDPIRE_API_KEY}`,
197
+ "Content-Type": "application/json",
198
+ },
199
+ body: JSON.stringify({
200
+ exercise_id: exerciseId,
201
+ question_id: questionId,
202
+ answers,
203
+ learner_ref: userId,
204
+ session_id: sessionId,
205
+ include_correct_answers: true,
206
+ }),
207
+ }).then((r) => r.json())
208
+ // Response envelope: { data: CheckResult, error: null, meta: null }
209
+ ```
210
+
211
+ </details>
212
+
213
+ **Server โ€” submit the full attempt when done:**
214
+
215
+ ```typescript
216
+ import { EdpireClient } from "@edpire/sdk/client"
217
+ import type { StoredAnswer } from "@edpire/sdk"
218
+
219
+ const stored: StoredAnswer[] = []
220
+ // push { exerciseId, questionId, answers } for each step as the learner answers
221
+
222
+ const client = new EdpireClient({ apiKey: process.env.EDPIRE_API_KEY! })
223
+ const result = await client.submit(assessmentId, {
224
+ learner_ref: userId,
225
+ answers: stored, // StoredAnswer[] โ€” the client handles nesting automatically
226
+ })
227
+ console.log(`Final score: ${result.score}/${result.max_score}`)
228
+ ```
229
+
230
+ <details>
231
+ <summary>Rate limiting on /check</summary>
232
+
233
+ Each `(session_id, question_id)` pair is limited to 3 checks per hour (configurable per org).
234
+
235
+ **Always pass a `session_id`** โ€” generate a UUID once at the start of each attempt:
236
+ ```typescript
237
+ const sessionId = crypto.randomUUID() // generate once per attempt, not per question
238
+ ```
239
+ If omitted, rate limiting falls back to `learner_ref`, which accumulates across all of a learner's attempts and can cause unexpected 429s on repeat attempts.
240
+
241
+ </details>
242
+
243
+ **Required API key scopes:** `read:assessments` (for fetching and checking) + `write:submissions` (for the final submit).
244
+
245
+ **When to use:** You want full control over pacing, navigation, animations, or scoring UX. You're building a Duolingo-style flow, a timed drill, a practice mode, or any experience that goes beyond a linear "answer all โ†’ submit" form.
246
+
247
+ ---
248
+
249
+ ## Server Client
250
+
251
+ `EdpireClient` is a backend-only utility for calling the Edpire REST API from your server. It is not a rendering pattern โ€” it is used alongside Embedded Player and Custom Flow.
252
+
253
+ ```typescript
254
+ import { EdpireClient } from "@edpire/sdk/client"
255
+
256
+ const client = new EdpireClient({
257
+ apiKey: process.env.EDPIRE_API_KEY!,
258
+ baseUrl: "https://edpire.com", // optional, defaults to edpire.com
259
+ })
260
+ ```
261
+
262
+ **Common operations:**
263
+
264
+ ```typescript
265
+ // List published assessments
266
+ const { items } = await client.getAssessments({ status: "published" })
267
+
268
+ // Fetch a single assessment with full content
269
+ const assessment = await client.getAssessment(assessmentId)
270
+
271
+ // Mint an embed token (for Embedded Player)
272
+ const { token } = await client.mintEmbedToken(assessmentId, learnerRef)
273
+
274
+ // Submit a full attempt (for Custom Flow)
275
+ const result = await client.submit(assessmentId, { learner_ref: userId, answers: stored })
276
+
277
+ // Grade a single question without recording (for Custom Flow /check)
278
+ const check = await client.checkQuestion(assessmentId, {
279
+ exercise_id, question_id, answers, learner_ref, session_id,
280
+ include_correct_answers: true,
281
+ })
282
+
283
+ // Register a webhook
284
+ const webhook = await client.registerWebhook("https://yourapp.com/webhooks/edpire", [
285
+ "submission.graded",
286
+ ])
287
+ // Store webhook.secret securely โ€” shown only once
288
+ ```
289
+
290
+ ---
291
+
292
+ ## Mobile & WebView integration
293
+
294
+ Flutter, React Native, and native iOS/Android apps cannot import this npm package directly.
295
+ The integration uses two parts:
296
+
297
+ 1. **Your backend** mints an embed token via `client.mintEmbedToken()` (the API key stays server-side).
298
+ 2. **A WebView** in your mobile app loads a minimal HTML page that calls the CDN UMD build with the token.
299
+
300
+ > The UMD build is served from npm via [jsDelivr](https://www.jsdelivr.com/) โ€” no CDN to host yourself.
301
+
302
+ **Universal WebView HTML template:**
303
+
304
+ ```html
305
+ <!DOCTYPE html>
306
+ <html>
307
+ <head>
308
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
309
+ <style>* { margin: 0; padding: 0; box-sizing: border-box; } #root { min-height: 100vh; }</style>
310
+ </head>
311
+ <body>
312
+ <div id="root"></div>
313
+ <script src="https://cdn.jsdelivr.net/npm/@edpire/sdk@0.5.0/dist/umd/index.global.js"></script>
314
+ <script>
315
+ EdpireSDK.EdpireAssessment.mount({
316
+ token: "{{EMBED_TOKEN}}", // replace server-side before loading
317
+ container: "#root",
318
+ onComplete: function(r) {
319
+ if (window.ReactNativeWebView)
320
+ window.ReactNativeWebView.postMessage(JSON.stringify({ type: "complete", result: r }))
321
+ if (window.flutter_inappwebview)
322
+ window.flutter_inappwebview.callHandler("onComplete", r)
323
+ },
324
+ onError: function(e) {
325
+ if (window.ReactNativeWebView)
326
+ window.ReactNativeWebView.postMessage(JSON.stringify({ type: "error", error: e }))
327
+ if (window.flutter_inappwebview)
328
+ window.flutter_inappwebview.callHandler("onError", e)
329
+ }
330
+ })
331
+ </script>
332
+ </body>
333
+ </html>
334
+ ```
335
+
336
+ > **Ionic / Capacitor** apps run in a WebView โ€” you can use the full npm SDK directly. No HTML template needed.
337
+
338
+ For React Native, Flutter, and native iOS/Android code examples, see the [Mobile Integration guide](https://docs.edpire.com/developer/mobile).
339
+
340
+ ---
341
+
342
+ ## CSS
343
+
344
+ **Embedded Player** (`EdpireAssessment.mount()`) โ€” all CSS is injected automatically. No imports needed.
345
+
346
+ **Custom Flow** (`EdpireQuestion` / `renderQuestion`) โ€” styles are bundled into the SDK's JavaScript and injected when the component first renders. In most setups this is also automatic. If styles appear missing (e.g. in a sandboxed iframe or a strict CSP environment that blocks inline styles), import the utility stylesheet explicitly:
347
+
348
+ ```typescript
349
+ import "@edpire/sdk/styles/runtime-utilities.css"
350
+ ```
351
+
352
+ ---
353
+
354
+ ## TypeScript types
355
+
356
+ All commonly needed types are available from the main entry:
357
+
358
+ ```typescript
359
+ import type {
360
+ // Answer types
361
+ RuntimeAnswer,
362
+ ChoiceSetAnswer,
363
+ BlankAnswer,
364
+ TypedBlankAnswer,
365
+ BlankChoiceAnswer,
366
+ MatchingSetAnswer,
367
+ AiAnswer,
368
+ OpenResponseAnswer,
369
+ MathResponseAnswer,
370
+
371
+ // Custom Flow helpers
372
+ StoredAnswer, // { exerciseId, questionId, answers: RuntimeAnswer[] }
373
+ FlatStep, // one step from flattenAssessment()
374
+
375
+ // Embedded Player
376
+ MountOptions,
377
+ EmbedInstance,
378
+ EmbedResult,
379
+ EmbedError,
380
+ } from "@edpire/sdk"
381
+ ```
382
+
383
+ Types specific to the Server Client come from `@edpire/sdk/client`:
384
+
385
+ ```typescript
386
+ import type { Assessment, SubmitResult, CheckResult, EdpireClientOptions } from "@edpire/sdk/client"
387
+ ```
388
+
389
+ ---
390
+
391
+ ## Webhooks
392
+
393
+ ```typescript
394
+ const webhook = await client.registerWebhook("https://yourapp.com/api/webhooks/edpire", [
395
+ "submission.graded",
396
+ "assessment.published",
397
+ ])
398
+ // Store webhook.secret securely โ€” it is only shown once.
399
+ ```
400
+
401
+ **Verify incoming signatures:**
402
+
403
+ ```typescript
404
+ import { createHmac, timingSafeEqual } from "crypto"
405
+
406
+ export async function POST(req: Request) {
407
+ const rawBody = await req.text()
408
+ const signature = req.headers.get("x-edpire-signature") ?? ""
409
+
410
+ const expected = "sha256=" + createHmac("sha256", process.env.EDPIRE_WEBHOOK_SECRET!)
411
+ .update(rawBody)
412
+ .digest("hex")
413
+
414
+ if (!timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
415
+ return new Response("Unauthorized", { status: 401 })
416
+ }
417
+
418
+ const event = JSON.parse(rawBody)
419
+ // event.event, event.submission_id, event.score, event.passed, ...
420
+ }
421
+ ```
422
+
423
+ **Available events:** `submission.graded`, `submission.pending_review`, `assessment.published`, `assessment.archived`
424
+
425
+ ---
426
+
427
+ ## Error handling
428
+
429
+ `EdpireClient` throws `EdpireError` on non-2xx responses:
430
+
431
+ ```typescript
432
+ import { EdpireError } from "@edpire/sdk/client"
433
+
434
+ try {
435
+ await client.submit(assessmentId, options)
436
+ } catch (err) {
437
+ if (err instanceof EdpireError) {
438
+ console.error(err.status, err.message)
439
+ }
440
+ }
441
+ ```
442
+
443
+ Common status codes: `400` bad request ยท `401` invalid API key ยท `404` not found ยท `409` max attempts reached ยท `422` grading failed ยท `429` rate limit exceeded