@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 +51 -0
- package/README.md +443 -0
- package/dist/client.d.mts +416 -0
- package/dist/client.mjs +287 -0
- package/dist/index.d.mts +430 -0
- package/dist/index.mjs +22 -0
- package/dist/react.d.mts +200 -0
- package/dist/react.mjs +22 -0
- package/dist/umd/index.global.js +1255 -0
- package/package.json +80 -0
- package/src/styles/runtime-utilities.css +1 -0
- package/src/styles/shell.css +2 -0
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
|