@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.
Files changed (41) hide show
  1. package/README.md +298 -63
  2. package/dist/client/choice-state.d.ts.map +1 -1
  3. package/dist/client/extended-text-state.d.ts.map +1 -1
  4. package/dist/client/index.d.ts.map +1 -1
  5. package/dist/client/index.js +338 -179
  6. package/dist/client/index.js.map +13 -11
  7. package/dist/client/match-state.d.ts.map +1 -1
  8. package/dist/client/order-state.d.ts.map +1 -1
  9. package/dist/client/session.d.ts.map +1 -1
  10. package/dist/client/text-entry-state.d.ts.map +1 -1
  11. package/dist/client/transport.d.ts +1 -1
  12. package/dist/client/transport.d.ts.map +1 -1
  13. package/dist/client/types.d.ts +2 -115
  14. package/dist/client/types.d.ts.map +1 -1
  15. package/dist/contracts/index.d.ts +4 -0
  16. package/dist/contracts/index.d.ts.map +1 -0
  17. package/dist/contracts/index.js +305 -0
  18. package/dist/contracts/index.js.map +11 -0
  19. package/dist/contracts/pci-schemas.d.ts +25 -0
  20. package/dist/contracts/pci-schemas.d.ts.map +1 -0
  21. package/dist/contracts/types.d.ts +118 -0
  22. package/dist/contracts/types.d.ts.map +1 -0
  23. package/dist/contracts/validation.d.ts +132 -0
  24. package/dist/contracts/validation.d.ts.map +1 -0
  25. package/dist/errors.d.ts +2 -1
  26. package/dist/errors.d.ts.map +1 -1
  27. package/dist/errors.js +48 -0
  28. package/dist/errors.js.map +10 -0
  29. package/dist/server/create-server.d.ts +21 -19
  30. package/dist/server/create-server.d.ts.map +1 -1
  31. package/dist/server/exchange.d.ts +10 -5
  32. package/dist/server/exchange.d.ts.map +1 -1
  33. package/dist/server/hints.d.ts +25 -0
  34. package/dist/server/hints.d.ts.map +1 -0
  35. package/dist/server/index.d.ts +4 -3
  36. package/dist/server/index.d.ts.map +1 -1
  37. package/dist/server/index.js +355 -83
  38. package/dist/server/index.js.map +9 -8
  39. package/dist/server/students.d.ts +3 -5
  40. package/dist/server/students.d.ts.map +1 -1
  41. 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. Wraps the raw HTTP protocol in two tiny, fully typed surfaces — one for your backend, one for the browser — so it's mechanically impossible to misuse.
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
- The package ships two subpaths. There is no root export — you must pick the side of the wire you're on:
18
+ ## Entrypoints
14
19
 
15
20
  | Import | Runs on | Wraps |
16
21
  |---|---|---|
17
- | `@superbuilders/primer-tives/server` | your backend | `POST /api/v0/auth/exchange`, `POST /api/v0/students`, `PATCH /api/v0/students/{id}` |
18
- | `@superbuilders/primer-tives/client` | the browser | `POST /api/v0/advance` (the lesson state machine) |
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
- No route strings, no `fetch()` calls, no snake_case wire bodies on your side. Both surfaces normalize transport failures into sentinel errors from `@superbuilders/errors`.
31
+ Use this when **your system** owns the learner identity.
21
32
 
22
- ## Round trip
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
- Your backend provisions a student (once), exchanges a `sk_` for a short-lived access token (each session), and hands the token to your frontend. Your frontend passes the token to `create()` and drives the returned state machine.
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 { createPrimerServer } from "@superbuilders/primer-tives/server"
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.PRIMER_CLIENT_SECRET_KEY_DEV
68
+ secretKey: process.env.PRIMER_DEMO_SECRET_KEY!,
69
+ logger: console
34
70
  })
35
71
 
36
- // One-time per user: provision a Primer-owned student.
37
- const studentId = await primer.createNativeStudent("4")
38
- // persist `studentId` alongside your user record
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
- // Every session: mint a short-lived access token.
41
- const result = await errors.try(
42
- primer.exchangeNativeStudentForAccessToken(studentId)
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 (result.error) {
45
- // map ErrInvalidSecretKey / ErrStudentNotFound / ErrServerError etc.
46
- throw result.error
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
- const { accessToken, expiresInSeconds } = result.data
49
- // ship `accessToken` to the browser
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, // from your backend
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 // e.g. https://sb-primer.vercel.app (no trailing slash)
116
- readonly secretKey: string // your sk_… key
117
- readonly fetch?: typeof globalThis.fetch // override (defaults to globalThis.fetch)
118
- readonly abort?: AbortController // wired into every request signal
119
- readonly logger?: PrimerLogger // optional structured 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. Each method throws a sentinel-wrapped `Error` — use `errors.try()` from `@superbuilders/errors` at the call site.
253
+ Returns a `PrimerServer` with exactly **four** methods:
126
254
 
127
- ### `createNativeStudent(gradeLevel): Promise<string>`
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
- Provision a new Primer-owned student. Returns the `studentId` string — persist it in your own database keyed by your user. Call this **once per user**.
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.createNativeStudent("5")
273
+ const studentId = await primer.createStudent()
133
274
  ```
134
275
 
135
- **Do not call this for Timeback integrations.** Timeback students are provisioned automatically on first `exchangeTimebackStudentForAccessToken`.
276
+ Notes:
136
277
 
137
- ### `updateNativeStudentGradeLevel(studentId, gradeLevel): Promise<void>`
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
- Change a Primer student's grade level.
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.updateNativeStudentGradeLevel(studentId, "6")
289
+ const { gradeLevel } = await primer.setStudentHints(studentId, {
290
+ gradeLevel: "3"
291
+ })
143
292
  ```
144
293
 
145
- **Do not call this for Timeback students.** Grade-level changes flow from the SIS.
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
- ### `exchangeNativeStudentForAccessToken(studentId): Promise<SessionToken>`
300
+ ### `exchangeStudentForAccessToken(studentId): Promise<SessionToken>`
148
301
 
149
- Mint a short-lived access token for an existing Primer-owned student. Call this **every session start**; tokens expire in 15 minutes by default.
302
+ Mint a short-lived access token for an existing **native/manual** Primer student.
150
303
 
151
304
  ```ts
152
- const { accessToken, expiresInSeconds } = await primer.exchangeNativeStudentForAccessToken(studentId)
305
+ const { accessToken, expiresInSeconds } =
306
+ await primer.exchangeStudentForAccessToken(studentId)
153
307
  ```
154
308
 
155
- ### `exchangeTimebackStudentForAccessToken(sourcedId): Promise<SessionToken>`
309
+ Call this at **every session start**.
310
+
311
+ Important:
156
312
 
157
- Mint a short-lived access token by OneRoster `sourcedId`. The server contacts the Timeback API to verify identity and grade, auto-provisions the Primer student row on first use, and returns the token. Subsequent exchanges for the same `sourcedId` reuse the existing row.
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 } = await primer.exchangeTimebackStudentForAccessToken(sourcedId)
328
+ const { studentId, accessToken, expiresInSeconds } =
329
+ await primer.exchangeTimebackStudentForAccessToken(sourcedId)
161
330
  ```
162
331
 
163
- ## `SessionToken`
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 // HS256 JWS — pass to client SDK as Config.accessToken
168
- readonly expiresInSeconds: number // typically 900 (15 min)
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
- Hand `accessToken` to your frontend over whatever channel you already use (cookie, HTML response, WebSocket, API response). The browser passes it to `create()`.
361
+ ### `PlacementHints`
173
362
 
174
- ## `GradeLevel`
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
- type GradeLevel = "K" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "10" | "11" | "12"
374
+ interface PlacementHintsResult {
375
+ readonly studentId: string
376
+ readonly gradeLevel: GradeLevel | null
377
+ }
178
378
  ```
179
379
 
180
- Exported as both `GradeLevel` and the `GRADE_LEVELS` readonly tuple.
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 — student doesn't exist (native) or sourced ID unknown (timeback) |
188
- | `ErrUnsupportedGrade` | HTTP 400 — Timeback user's grade is outside K–12 |
189
- | `ErrTimebackUnavailable` | HTTP 502 — Timeback OneRoster endpoint failed |
190
- | `ErrBadRequest` | HTTP 400 (non-grade) validation failure |
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 409attempted 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 wasn't valid JSON |
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
- All server methods follow the same pattern:
400
+ ### Recommended error-handling pattern
197
401
 
198
402
  ```ts
199
- const result = await errors.try(primer.createNativeStudent("5"))
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 the sk_ and retry
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
- const studentId = result.data
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":"AAEA,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;AA6CjD,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,CAsFnB;AAED,OAAO,EAAE,WAAW,EAAE,CAAA"}
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":"AAEA,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;AAoBjD,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,CA0JnB;AAED,OAAO,EAAE,iBAAiB,EAAE,CAAA"}
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;AACnD,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"}
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"}