@superbuilders/primer-tives 0.9.0 → 1.1.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/README.md +298 -63
- package/dist/client/choice-state.d.ts.map +1 -1
- package/dist/client/extended-text-state.d.ts.map +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +338 -179
- package/dist/client/index.js.map +13 -11
- package/dist/client/match-state.d.ts.map +1 -1
- package/dist/client/order-state.d.ts.map +1 -1
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/text-entry-state.d.ts.map +1 -1
- package/dist/client/transport.d.ts +1 -1
- package/dist/client/transport.d.ts.map +1 -1
- package/dist/client/types.d.ts +2 -115
- package/dist/client/types.d.ts.map +1 -1
- package/dist/contracts/index.d.ts +4 -0
- package/dist/contracts/index.d.ts.map +1 -0
- package/dist/contracts/index.js +305 -0
- package/dist/contracts/index.js.map +11 -0
- package/dist/contracts/pci-schemas.d.ts +25 -0
- package/dist/contracts/pci-schemas.d.ts.map +1 -0
- package/dist/contracts/types.d.ts +118 -0
- package/dist/contracts/types.d.ts.map +1 -0
- package/dist/contracts/validation.d.ts +132 -0
- package/dist/contracts/validation.d.ts.map +1 -0
- package/dist/errors.d.ts +2 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +48 -0
- package/dist/errors.js.map +10 -0
- package/dist/server/create-server.d.ts +21 -19
- package/dist/server/create-server.d.ts.map +1 -1
- package/dist/server/exchange.d.ts +10 -5
- package/dist/server/exchange.d.ts.map +1 -1
- package/dist/server/hints.d.ts +25 -0
- package/dist/server/hints.d.ts.map +1 -0
- package/dist/server/index.d.ts +4 -3
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +355 -83
- package/dist/server/index.js.map +9 -8
- package/dist/server/students.d.ts +3 -5
- package/dist/server/students.d.ts.map +1 -1
- package/package.json +17 -4
package/README.md
CHANGED
|
@@ -1,64 +1,177 @@
|
|
|
1
1
|
# @superbuilders/primer-tives
|
|
2
2
|
|
|
3
|
-
TypeScript SDK for the Primer adaptive learning engine.
|
|
3
|
+
TypeScript SDK for the Primer adaptive learning engine.
|
|
4
|
+
|
|
5
|
+
The package exposes **two explicit subpaths**:
|
|
6
|
+
|
|
7
|
+
- `@superbuilders/primer-tives/server` — runs on your backend, authenticates with your Primer `sk_...` secret key, and starts student sessions.
|
|
8
|
+
- `@superbuilders/primer-tives/client` — runs in the browser and drives the Primer lesson state machine.
|
|
9
|
+
|
|
10
|
+
There is **no root export**. You must choose the side of the wire you are on.
|
|
4
11
|
|
|
5
12
|
```sh
|
|
6
13
|
bun add @superbuilders/primer-tives
|
|
7
14
|
```
|
|
8
15
|
|
|
9
|
-
Dependency: `@superbuilders/errors` is installed automatically.
|
|
10
|
-
|
|
11
|
-
## Two entrypoints
|
|
16
|
+
Dependency note: `@superbuilders/errors` is installed automatically and is used for `errors.try()` / `errors.is()`.
|
|
12
17
|
|
|
13
|
-
|
|
18
|
+
## Entrypoints
|
|
14
19
|
|
|
15
20
|
| Import | Runs on | Wraps |
|
|
16
21
|
|---|---|---|
|
|
17
|
-
| `@superbuilders/primer-tives/server` | your backend | `POST /api/v0/
|
|
18
|
-
| `@superbuilders/primer-tives/client` | the browser | `POST /api/v0/advance`
|
|
22
|
+
| `@superbuilders/primer-tives/server` | your backend | `POST /api/v0/students`, `PATCH /api/v0/students/:id/hints`, `POST /api/v0/auth/exchange`, `POST /api/v0/auth/exchange/timeback` |
|
|
23
|
+
| `@superbuilders/primer-tives/client` | the browser | `POST /api/v0/advance` |
|
|
24
|
+
|
|
25
|
+
## Supported student flows
|
|
26
|
+
|
|
27
|
+
Primer intentionally supports **two** backend-authenticated student flows.
|
|
28
|
+
|
|
29
|
+
### 1. Primer-native / manual students
|
|
19
30
|
|
|
20
|
-
|
|
31
|
+
Use this when **your system** owns the learner identity.
|
|
21
32
|
|
|
22
|
-
|
|
33
|
+
1. Call `createStudent()` once to mint a stable Primer `studentId`.
|
|
34
|
+
2. Persist that `studentId` in your own database alongside your user record.
|
|
35
|
+
3. **Before the student's first session**, call `setStudentHints(studentId, { gradeLevel })` at least once. Native students are created hint-less, and the first `/advance` call fails without a `gradeLevel` on record.
|
|
36
|
+
4. At each session start, call `exchangeStudentForAccessToken(studentId)`.
|
|
37
|
+
5. Hand the returned `accessToken` to the browser SDK.
|
|
23
38
|
|
|
24
|
-
|
|
39
|
+
### 2. Live-authoritative Timeback students
|
|
40
|
+
|
|
41
|
+
Use this when **Timeback / OneRoster** remains the live authority for each login.
|
|
42
|
+
|
|
43
|
+
1. At each session start, call `exchangeTimebackStudentForAccessToken(sourcedId)`.
|
|
44
|
+
2. Primer verifies the learner against Timeback on **every call**.
|
|
45
|
+
3. Primer resolves or provisions the frontend-owned Primer student row behind the scenes.
|
|
46
|
+
4. The call returns both the stable Primer `studentId` and a short-lived `accessToken`.
|
|
47
|
+
5. Hand the returned `accessToken` to the browser SDK.
|
|
48
|
+
|
|
49
|
+
Persisting the returned `studentId` is fine if you want a stable local foreign key. Session startup for this flow still begins from `sourcedId`, and the returned `accessToken` is the session credential you pass to the browser SDK.
|
|
50
|
+
|
|
51
|
+
## End-to-end examples
|
|
52
|
+
|
|
53
|
+
### Native/manual flow
|
|
25
54
|
|
|
26
55
|
```ts
|
|
27
56
|
// ── your backend ─────────────────────────────────────────────────────
|
|
28
57
|
import * as errors from "@superbuilders/errors"
|
|
29
|
-
import {
|
|
58
|
+
import {
|
|
59
|
+
createPrimerServer,
|
|
60
|
+
ErrBadRequest,
|
|
61
|
+
ErrConflict,
|
|
62
|
+
ErrInvalidSecretKey,
|
|
63
|
+
ErrStudentNotFound
|
|
64
|
+
} from "@superbuilders/primer-tives/server"
|
|
30
65
|
|
|
31
66
|
const primer = createPrimerServer({
|
|
32
67
|
origin: "https://sb-primer.vercel.app",
|
|
33
|
-
secretKey: process.env.
|
|
68
|
+
secretKey: process.env.PRIMER_DEMO_SECRET_KEY!,
|
|
69
|
+
logger: console
|
|
34
70
|
})
|
|
35
71
|
|
|
36
|
-
// One
|
|
37
|
-
const
|
|
38
|
-
|
|
72
|
+
// One time per user.
|
|
73
|
+
const createResult = await errors.try(primer.createStudent())
|
|
74
|
+
if (createResult.error) {
|
|
75
|
+
if (errors.is(createResult.error, ErrInvalidSecretKey)) {
|
|
76
|
+
throw new Error("Primer secret key is invalid")
|
|
77
|
+
}
|
|
78
|
+
if (errors.is(createResult.error, ErrConflict)) {
|
|
79
|
+
throw new Error("This Primer frontend is not provisioned with routable content")
|
|
80
|
+
}
|
|
81
|
+
throw createResult.error
|
|
82
|
+
}
|
|
83
|
+
const studentId = createResult.data
|
|
84
|
+
// persist `studentId` alongside your own user record
|
|
39
85
|
|
|
40
|
-
//
|
|
41
|
-
const
|
|
42
|
-
primer.
|
|
86
|
+
// Required before the first session for native students.
|
|
87
|
+
const hintsResult = await errors.try(
|
|
88
|
+
primer.setStudentHints(studentId, { gradeLevel: "3" })
|
|
43
89
|
)
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
|
|
90
|
+
if (hintsResult.error) {
|
|
91
|
+
if (errors.is(hintsResult.error, ErrStudentNotFound)) {
|
|
92
|
+
throw new Error("Stored Primer student id no longer exists on this frontend")
|
|
93
|
+
}
|
|
94
|
+
if (errors.is(hintsResult.error, ErrBadRequest)) {
|
|
95
|
+
throw new Error("Invalid hint payload (e.g. unsupported gradeLevel)")
|
|
96
|
+
}
|
|
97
|
+
throw hintsResult.error
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Every session start.
|
|
101
|
+
const tokenResult = await errors.try(
|
|
102
|
+
primer.exchangeStudentForAccessToken(studentId)
|
|
103
|
+
)
|
|
104
|
+
if (tokenResult.error) {
|
|
105
|
+
if (errors.is(tokenResult.error, ErrInvalidSecretKey)) {
|
|
106
|
+
throw new Error("Primer secret key is invalid")
|
|
107
|
+
}
|
|
108
|
+
if (errors.is(tokenResult.error, ErrStudentNotFound)) {
|
|
109
|
+
throw new Error("Stored Primer student id no longer exists on this frontend")
|
|
110
|
+
}
|
|
111
|
+
throw tokenResult.error
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const { accessToken, expiresInSeconds } = tokenResult.data
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Live-authoritative Timeback flow
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
// ── your backend ─────────────────────────────────────────────────────
|
|
121
|
+
import * as errors from "@superbuilders/errors"
|
|
122
|
+
import {
|
|
123
|
+
createPrimerServer,
|
|
124
|
+
ErrConflict,
|
|
125
|
+
ErrInvalidSecretKey,
|
|
126
|
+
ErrStudentNotFound,
|
|
127
|
+
ErrTimebackUnavailable,
|
|
128
|
+
ErrUnsupportedGrade
|
|
129
|
+
} from "@superbuilders/primer-tives/server"
|
|
130
|
+
|
|
131
|
+
const primer = createPrimerServer({
|
|
132
|
+
origin: "https://sb-primer.vercel.app",
|
|
133
|
+
secretKey: process.env.PRIMER_DEMO_SECRET_KEY!,
|
|
134
|
+
logger: console
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const sessionResult = await errors.try(
|
|
138
|
+
primer.exchangeTimebackStudentForAccessToken("student-123")
|
|
139
|
+
)
|
|
140
|
+
if (sessionResult.error) {
|
|
141
|
+
if (errors.is(sessionResult.error, ErrInvalidSecretKey)) {
|
|
142
|
+
throw new Error("Primer secret key is invalid")
|
|
143
|
+
}
|
|
144
|
+
if (errors.is(sessionResult.error, ErrStudentNotFound)) {
|
|
145
|
+
throw new Error("Timeback sourcedId was not found upstream")
|
|
146
|
+
}
|
|
147
|
+
if (errors.is(sessionResult.error, ErrUnsupportedGrade)) {
|
|
148
|
+
throw new Error("Timeback returned a grade Primer does not support")
|
|
149
|
+
}
|
|
150
|
+
if (errors.is(sessionResult.error, ErrConflict)) {
|
|
151
|
+
throw new Error("This Primer frontend is not provisioned with routable content")
|
|
152
|
+
}
|
|
153
|
+
if (errors.is(sessionResult.error, ErrTimebackUnavailable)) {
|
|
154
|
+
throw new Error("Timeback is temporarily unavailable")
|
|
155
|
+
}
|
|
156
|
+
throw sessionResult.error
|
|
47
157
|
}
|
|
48
|
-
|
|
49
|
-
|
|
158
|
+
|
|
159
|
+
const { studentId, accessToken, expiresInSeconds } = sessionResult.data
|
|
160
|
+
// persist `studentId` if you want a stable Primer foreign key
|
|
161
|
+
// use `accessToken` for this session only
|
|
50
162
|
```
|
|
51
163
|
|
|
164
|
+
### Browser flow
|
|
165
|
+
|
|
52
166
|
```ts
|
|
53
167
|
// ── your frontend ────────────────────────────────────────────────────
|
|
54
168
|
import {
|
|
55
169
|
create,
|
|
56
|
-
ErrRateLimited,
|
|
57
170
|
type PrimerState
|
|
58
171
|
} from "@superbuilders/primer-tives/client"
|
|
59
172
|
|
|
60
173
|
const client = create({
|
|
61
|
-
accessToken,
|
|
174
|
+
accessToken,
|
|
62
175
|
origin: "https://sb-primer.vercel.app",
|
|
63
176
|
subject: "math",
|
|
64
177
|
supportedPcis: [
|
|
@@ -103,8 +216,23 @@ import {
|
|
|
103
216
|
type PrimerServer,
|
|
104
217
|
type PrimerServerConfig,
|
|
105
218
|
type SessionToken,
|
|
219
|
+
type TimebackSession,
|
|
220
|
+
type PlacementHints,
|
|
221
|
+
type PlacementHintsResult,
|
|
106
222
|
type GradeLevel,
|
|
107
|
-
GRADE_LEVELS
|
|
223
|
+
GRADE_LEVELS,
|
|
224
|
+
type PrimerLogger,
|
|
225
|
+
ErrBadRequest,
|
|
226
|
+
ErrConflict,
|
|
227
|
+
ErrExternalAuthorityRequired,
|
|
228
|
+
ErrInvalidSecretKey,
|
|
229
|
+
ErrJsonParse,
|
|
230
|
+
ErrNetwork,
|
|
231
|
+
ErrServerError,
|
|
232
|
+
ErrStudentNotFound,
|
|
233
|
+
ErrTimebackUnavailable,
|
|
234
|
+
ErrTimeout,
|
|
235
|
+
ErrUnsupportedGrade
|
|
108
236
|
} from "@superbuilders/primer-tives/server"
|
|
109
237
|
```
|
|
110
238
|
|
|
@@ -112,100 +240,207 @@ import {
|
|
|
112
240
|
|
|
113
241
|
```ts
|
|
114
242
|
interface PrimerServerConfig {
|
|
115
|
-
readonly origin: string
|
|
116
|
-
readonly secretKey: string
|
|
117
|
-
readonly fetch?: typeof globalThis.fetch
|
|
118
|
-
readonly abort?: AbortController
|
|
119
|
-
readonly logger
|
|
243
|
+
readonly origin: string // e.g. https://sb-primer.vercel.app (no trailing slash)
|
|
244
|
+
readonly secretKey: string // your sk_… key
|
|
245
|
+
readonly fetch?: typeof globalThis.fetch // override (tests, proxies, instrumentation)
|
|
246
|
+
readonly abort?: AbortController // wired into every request signal
|
|
247
|
+
readonly logger: PrimerLogger // required debug/info/warn/error logger (console works)
|
|
120
248
|
}
|
|
121
249
|
|
|
122
250
|
function createPrimerServer(config: PrimerServerConfig): PrimerServer
|
|
123
251
|
```
|
|
124
252
|
|
|
125
|
-
Returns a `PrimerServer` with four methods
|
|
253
|
+
Returns a `PrimerServer` with exactly **four** methods:
|
|
126
254
|
|
|
127
|
-
|
|
255
|
+
```ts
|
|
256
|
+
interface PrimerServer {
|
|
257
|
+
createStudent(): Promise<string>
|
|
258
|
+
setStudentHints(studentId: string, hints: PlacementHints): Promise<PlacementHintsResult>
|
|
259
|
+
exchangeStudentForAccessToken(studentId: string): Promise<SessionToken>
|
|
260
|
+
exchangeTimebackStudentForAccessToken(sourcedId: string): Promise<TimebackSession>
|
|
261
|
+
}
|
|
262
|
+
```
|
|
128
263
|
|
|
129
|
-
|
|
264
|
+
## Method reference
|
|
265
|
+
|
|
266
|
+
### `createStudent(): Promise<string>`
|
|
267
|
+
|
|
268
|
+
Provision a new **frontend-owned Primer student** and return its stable `studentId`.
|
|
269
|
+
|
|
270
|
+
Use this only for the native/manual flow.
|
|
130
271
|
|
|
131
272
|
```ts
|
|
132
|
-
const studentId = await primer.
|
|
273
|
+
const studentId = await primer.createStudent()
|
|
133
274
|
```
|
|
134
275
|
|
|
135
|
-
|
|
276
|
+
Notes:
|
|
136
277
|
|
|
137
|
-
|
|
278
|
+
- Call this when your system is the source of truth for learner identity.
|
|
279
|
+
- Persist the returned `studentId` in your own database.
|
|
280
|
+
- **Before the first session**, call `setStudentHints(studentId, { gradeLevel })`. Native students are created without a `gradeLevel`, and `/advance` requires one.
|
|
281
|
+
- Use that stored `studentId` with `exchangeStudentForAccessToken(studentId)` at each session start.
|
|
282
|
+
- `logger` is required. Pass any object implementing `debug/info/warn/error`; `console` is acceptable for basic integrations.
|
|
138
283
|
|
|
139
|
-
|
|
284
|
+
### `setStudentHints(studentId, hints): Promise<PlacementHintsResult>`
|
|
285
|
+
|
|
286
|
+
Partial upsert of a native/manual student's placement-routing hints. Omitted fields are left untouched; the server returns the persisted state after upsert.
|
|
140
287
|
|
|
141
288
|
```ts
|
|
142
|
-
await primer.
|
|
289
|
+
const { gradeLevel } = await primer.setStudentHints(studentId, {
|
|
290
|
+
gradeLevel: "3"
|
|
291
|
+
})
|
|
143
292
|
```
|
|
144
293
|
|
|
145
|
-
**
|
|
294
|
+
- **Required** for native/manual students before their first session. Without a `gradeLevel` on record, the first `/advance` call fails server-side.
|
|
295
|
+
- `gradeLevel` is the only hint today; future hint kinds (raw context, interests, etc.) will appear as additional optional fields on `PlacementHints`.
|
|
296
|
+
- Safe to call repeatedly — each call is a partial upsert.
|
|
297
|
+
- Not needed for Timeback-linked students: `exchangeTimebackStudentForAccessToken` syncs the grade level from the authority on every call.
|
|
298
|
+
- Throws `ErrStudentNotFound` if `studentId` is unknown; `ErrBadRequest` if the payload fails validation (e.g., unsupported `gradeLevel`).
|
|
146
299
|
|
|
147
|
-
### `
|
|
300
|
+
### `exchangeStudentForAccessToken(studentId): Promise<SessionToken>`
|
|
148
301
|
|
|
149
|
-
Mint a short-lived access token for an existing Primer
|
|
302
|
+
Mint a short-lived access token for an existing **native/manual** Primer student.
|
|
150
303
|
|
|
151
304
|
```ts
|
|
152
|
-
const { accessToken, expiresInSeconds } =
|
|
305
|
+
const { accessToken, expiresInSeconds } =
|
|
306
|
+
await primer.exchangeStudentForAccessToken(studentId)
|
|
153
307
|
```
|
|
154
308
|
|
|
155
|
-
|
|
309
|
+
Call this at **every session start**.
|
|
310
|
+
|
|
311
|
+
Important:
|
|
156
312
|
|
|
157
|
-
|
|
313
|
+
- This is for **native/manual** students only.
|
|
314
|
+
- If `studentId` belongs to a Timeback-linked student, the method throws `ErrExternalAuthorityRequired`.
|
|
315
|
+
- Tokens are short-lived (typically 15 minutes).
|
|
316
|
+
|
|
317
|
+
### `exchangeTimebackStudentForAccessToken(sourcedId): Promise<TimebackSession>`
|
|
318
|
+
|
|
319
|
+
Perform a **live-authoritative Timeback session start**.
|
|
320
|
+
|
|
321
|
+
Primer verifies the learner against Timeback on every call, then resolves or provisions the corresponding frontend-owned Primer student and returns:
|
|
322
|
+
|
|
323
|
+
- the stable Primer `studentId`
|
|
324
|
+
- a short-lived `accessToken`
|
|
325
|
+
- `expiresInSeconds`
|
|
158
326
|
|
|
159
327
|
```ts
|
|
160
|
-
const { accessToken, expiresInSeconds } =
|
|
328
|
+
const { studentId, accessToken, expiresInSeconds } =
|
|
329
|
+
await primer.exchangeTimebackStudentForAccessToken(sourcedId)
|
|
161
330
|
```
|
|
162
331
|
|
|
163
|
-
|
|
332
|
+
Use this at **every Timeback session start**.
|
|
333
|
+
|
|
334
|
+
Operational rule:
|
|
335
|
+
|
|
336
|
+
- Call this at every Timeback session start.
|
|
337
|
+
- Persist `studentId` if you need a stable Primer foreign key in your own system.
|
|
338
|
+
- Use the returned `accessToken` for the active browser session.
|
|
339
|
+
|
|
340
|
+
## Return types
|
|
341
|
+
|
|
342
|
+
### `SessionToken`
|
|
164
343
|
|
|
165
344
|
```ts
|
|
166
345
|
interface SessionToken {
|
|
167
|
-
readonly accessToken: string
|
|
168
|
-
readonly expiresInSeconds: number
|
|
346
|
+
readonly accessToken: string
|
|
347
|
+
readonly expiresInSeconds: number
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### `TimebackSession`
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
interface TimebackSession {
|
|
355
|
+
readonly studentId: string
|
|
356
|
+
readonly accessToken: string
|
|
357
|
+
readonly expiresInSeconds: number
|
|
169
358
|
}
|
|
170
359
|
```
|
|
171
360
|
|
|
172
|
-
|
|
361
|
+
### `PlacementHints`
|
|
173
362
|
|
|
174
|
-
|
|
363
|
+
```ts
|
|
364
|
+
interface PlacementHints {
|
|
365
|
+
readonly gradeLevel?: GradeLevel
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Partial shape: pass only the fields you want to set. Today the only hint is `gradeLevel` (the learner's current grade). Re-export `GRADE_LEVELS` enumerates the supported values.
|
|
370
|
+
|
|
371
|
+
### `PlacementHintsResult`
|
|
175
372
|
|
|
176
373
|
```ts
|
|
177
|
-
|
|
374
|
+
interface PlacementHintsResult {
|
|
375
|
+
readonly studentId: string
|
|
376
|
+
readonly gradeLevel: GradeLevel | null
|
|
377
|
+
}
|
|
178
378
|
```
|
|
179
379
|
|
|
180
|
-
|
|
380
|
+
The persisted state after upsert. `gradeLevel` is `null` only on native students who have never had one set.
|
|
181
381
|
|
|
182
382
|
## Error sentinels (`/server`)
|
|
183
383
|
|
|
384
|
+
All `/server` methods throw sentinel-wrapped `Error`s. Use `errors.try()` and `errors.is()` from `@superbuilders/errors`.
|
|
385
|
+
|
|
184
386
|
| Sentinel | Raised when |
|
|
185
387
|
|---|---|
|
|
186
388
|
| `ErrInvalidSecretKey` | HTTP 401 — missing, malformed, or unknown `sk_` |
|
|
187
|
-
| `ErrStudentNotFound` | HTTP 404 —
|
|
188
|
-
| `ErrUnsupportedGrade` | HTTP 400 — Timeback
|
|
189
|
-
| `ErrTimebackUnavailable` | HTTP 502 — Timeback OneRoster endpoint failed |
|
|
190
|
-
| `
|
|
389
|
+
| `ErrStudentNotFound` | HTTP 404 — native `studentId` unknown on this frontend, or Timeback `sourcedId` unknown upstream |
|
|
390
|
+
| `ErrUnsupportedGrade` | HTTP 400 — Timeback returned a grade outside Primer's supported range |
|
|
391
|
+
| `ErrTimebackUnavailable` | HTTP 502 — Timeback OneRoster endpoint failed during live exchange |
|
|
392
|
+
| `ErrExternalAuthorityRequired` | HTTP 409 — attempted native/manual exchange for a Timeback-linked student |
|
|
393
|
+
| `ErrConflict` | HTTP 409 — frontend is not provisioned for routing/content |
|
|
394
|
+
| `ErrBadRequest` | HTTP 400 — validation failure |
|
|
191
395
|
| `ErrServerError` | HTTP 5xx |
|
|
192
|
-
| `ErrJsonParse` | Success response body
|
|
193
|
-
| `ErrNetwork` | fetch() rejected (DNS, connection, TLS) |
|
|
396
|
+
| `ErrJsonParse` | Success response body was not valid JSON or had the wrong shape |
|
|
397
|
+
| `ErrNetwork` | fetch() rejected (DNS, connection, TLS, etc.) |
|
|
194
398
|
| `ErrTimeout` | fetch() aborted (your `AbortController` or `TimeoutError`) |
|
|
195
399
|
|
|
196
|
-
|
|
400
|
+
### Recommended error-handling pattern
|
|
197
401
|
|
|
198
402
|
```ts
|
|
199
|
-
|
|
403
|
+
import * as errors from "@superbuilders/errors"
|
|
404
|
+
import {
|
|
405
|
+
createPrimerServer,
|
|
406
|
+
ErrExternalAuthorityRequired,
|
|
407
|
+
ErrInvalidSecretKey,
|
|
408
|
+
ErrStudentNotFound
|
|
409
|
+
} from "@superbuilders/primer-tives/server"
|
|
410
|
+
|
|
411
|
+
const primer = createPrimerServer({
|
|
412
|
+
origin: "https://sb-primer.vercel.app",
|
|
413
|
+
secretKey: process.env.PRIMER_DEMO_SECRET_KEY!,
|
|
414
|
+
logger: console
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
const result = await errors.try(
|
|
418
|
+
primer.exchangeStudentForAccessToken(studentId)
|
|
419
|
+
)
|
|
200
420
|
if (result.error) {
|
|
201
421
|
if (errors.is(result.error, ErrInvalidSecretKey)) {
|
|
202
|
-
// rotate
|
|
422
|
+
// rotate or fix your sk_
|
|
423
|
+
}
|
|
424
|
+
if (errors.is(result.error, ErrStudentNotFound)) {
|
|
425
|
+
// your stored studentId is stale or wrong for this frontend
|
|
426
|
+
}
|
|
427
|
+
if (errors.is(result.error, ErrExternalAuthorityRequired)) {
|
|
428
|
+
// this student must authenticate through live Timeback exchange
|
|
203
429
|
}
|
|
204
430
|
throw result.error
|
|
205
431
|
}
|
|
206
|
-
|
|
432
|
+
|
|
433
|
+
const { accessToken } = result.data
|
|
207
434
|
```
|
|
208
435
|
|
|
436
|
+
## Rules of the road
|
|
437
|
+
|
|
438
|
+
- **Native/manual flow:** `createStudent()` once, then `exchangeStudentForAccessToken(studentId)` every session.
|
|
439
|
+
- **Timeback flow:** `exchangeTimebackStudentForAccessToken(sourcedId)` every session.
|
|
440
|
+
- **Student ids are stable identifiers, not browser credentials.** Always hand the browser a fresh `accessToken` from your backend.
|
|
441
|
+
- **Server-only secrets.** Keep `secretKey` on your backend; never ship it to the browser.
|
|
442
|
+
- **Explicit subpaths only.** Import from `/server` or `/client`, never a package root.
|
|
443
|
+
|
|
209
444
|
---
|
|
210
445
|
|
|
211
446
|
# `/client`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"choice-state.d.ts","sourceRoot":"","sources":["../../src/client/choice-state.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"choice-state.d.ts","sourceRoot":"","sources":["../../src/client/choice-state.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oDAAoD,CAAA;AACxF,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,wCAAwC,CAAA;AACnE,OAAO,KAAK,EACX,WAAW,EACX,cAAc,EACd,mBAAmB,EACnB,gBAAgB,EAChB,MAAM,0CAA0C,CAAA;AAGjD,iBAAS,WAAW,CAAC,IAAI,SAAS,KAAK,EACtC,GAAG,EAAE,cAAc,CAAC,IAAI,CAAC,EACzB,QAAQ,EAAE,gBAAgB,GAAG,IAAI,EACjC,WAAW,EAAE,OAAO,CAAC,mBAAmB,CAAC,IAAI,CAAC,EAAE;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC,EACnE,OAAO,EAAE,cAAc,EAAE,EACzB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GAChB,WAAW,CAAC,IAAI,CAAC,CAqFnB;AAED,OAAO,EAAE,WAAW,EAAE,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extended-text-state.d.ts","sourceRoot":"","sources":["../../src/client/extended-text-state.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"extended-text-state.d.ts","sourceRoot":"","sources":["../../src/client/extended-text-state.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oDAAoD,CAAA;AACxF,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,wCAAwC,CAAA;AACnE,OAAO,KAAK,EACX,WAAW,EACX,gBAAgB,EAChB,2BAA2B,EAC3B,MAAM,0CAA0C,CAAA;AAGjD,iBAAS,iBAAiB,CAAC,IAAI,SAAS,KAAK,EAC5C,GAAG,EAAE,cAAc,CAAC,IAAI,CAAC,EACzB,QAAQ,EAAE,gBAAgB,GAAG,IAAI,EACjC,WAAW,EAAE,OAAO,CAAC,2BAA2B,EAAE;IAAE,IAAI,EAAE,eAAe,CAAA;CAAE,CAAC,GAC1E,WAAW,CAAC,IAAI,CAAC,CAqKnB;AAED,OAAO,EAAE,iBAAiB,EAAE,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAA;AAClE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAA;AAE/E,YAAY,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAA;AAEtE,OAAO,EAAE,QAAQ,EAAE,MAAM,qCAAqC,CAAA;AAC9D,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAA;AAEhF,OAAO,EACN,aAAa,EACb,WAAW,EACX,YAAY,EACZ,qBAAqB,EACrB,oBAAoB,EACpB,YAAY,EACZ,uBAAuB,EACvB,UAAU,EACV,WAAW,EACX,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,qBAAqB,EACrB,UAAU,EACV,eAAe,EACf,iBAAiB,EACjB,MAAM,oCAAoC,CAAA;AAE3C,YAAY,EACX,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,cAAc,EACd,YAAY,EACZ,yBAAyB,EACzB,kBAAkB,EAClB,uBAAuB,EACvB,iBAAiB,EACjB,UAAU,EACV,aAAa,EACb,aAAa,EACb,iBAAiB,EACjB,gBAAgB,EAChB,SAAS,EACT,WAAW,EACX,UAAU,EACV,eAAe,EACf,gBAAgB,EAChB,WAAW,EACX,UAAU,EACV,cAAc,EACd,mBAAmB,EACnB,qBAAqB,EACrB,cAAc,EACd,SAAS,EACT,aAAa,EACb,uBAAuB,EACvB,WAAW,EACX,cAAc,EACd,mBAAmB,EACnB,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,yBAAyB,EACzB,iBAAiB,EACjB,2BAA2B,EAC3B,eAAe,EACf,cAAc,EACd,MAAM,0CAA0C,CAAA;AAEjD,YAAY,EACX,YAAY,EACZ,aAAa,EACb,WAAW,EACX,MAAM,4CAA4C,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAA;AAClE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAA;AAE/E,YAAY,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAA;AAEtE,OAAO,EAAE,QAAQ,EAAE,MAAM,qCAAqC,CAAA;AAC9D,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAA;AAEhF,OAAO,EACN,aAAa,EACb,WAAW,EACX,YAAY,EACZ,qBAAqB,EACrB,oBAAoB,EACpB,YAAY,EACZ,uBAAuB,EACvB,UAAU,EACV,WAAW,EACX,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,qBAAqB,EACrB,UAAU,EACV,eAAe,EACf,iBAAiB,EACjB,MAAM,oCAAoC,CAAA;AAE3C,YAAY,EACX,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,cAAc,EACd,YAAY,EACZ,yBAAyB,EACzB,kBAAkB,EAClB,uBAAuB,EACvB,iBAAiB,EACjB,UAAU,EACV,aAAa,EACb,aAAa,EACb,iBAAiB,EACjB,gBAAgB,EAChB,SAAS,EACT,WAAW,EACX,UAAU,EACV,eAAe,EACf,gBAAgB,EAChB,WAAW,EACX,UAAU,EACV,cAAc,EACd,mBAAmB,EACnB,qBAAqB,EACrB,cAAc,EACd,SAAS,EACT,aAAa,EACb,uBAAuB,EACvB,WAAW,EACX,cAAc,EACd,mBAAmB,EACnB,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,yBAAyB,EACzB,iBAAiB,EACjB,2BAA2B,EAC3B,eAAe,EACf,cAAc,EACd,MAAM,0CAA0C,CAAA;AAEjD,YAAY,EACX,YAAY,EACZ,aAAa,EACb,WAAW,EACX,MAAM,4CAA4C,CAAA;AAEnD,OAAO,EACN,iBAAiB,EACjB,kBAAkB,EAClB,MAAM,4CAA4C,CAAA;AAEnD,YAAY,EACX,sBAAsB,EACtB,2BAA2B,EAC3B,qBAAqB,EACrB,0BAA0B,EAC1B,KAAK,EACL,QAAQ,EACR,WAAW,EACX,MAAM,EACN,QAAQ,EACR,MAAM,wCAAwC,CAAA"}
|