@superbuilders/primer-tives 3.5.1 → 3.6.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 +234 -126
- package/dist/client/auth/provider.d.ts +18 -3
- package/dist/client/auth/provider.d.ts.map +1 -1
- package/dist/client/auth/storage.d.ts +2 -1
- package/dist/client/auth/storage.d.ts.map +1 -1
- package/dist/client/index.d.ts +3 -3
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +201 -45
- package/dist/client/index.js.map +8 -7
- package/dist/client/session-context.d.ts +1 -1
- package/dist/client/session-context.d.ts.map +1 -1
- package/dist/client/{create.d.ts → start.d.ts} +7 -5
- package/dist/client/start.d.ts.map +1 -0
- package/dist/client/start.type-test.d.ts +2 -0
- package/dist/client/start.type-test.d.ts.map +1 -0
- package/dist/client/types.d.ts +7 -2
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client/unauthenticated-state.d.ts +10 -0
- package/dist/client/unauthenticated-state.d.ts.map +1 -0
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/dist/client/create.d.ts.map +0 -1
- package/dist/client/create.type-test.d.ts +0 -2
- package/dist/client/create.type-test.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
TypeScript SDK primitives for the Primer adaptive learning runtime.
|
|
4
4
|
|
|
5
|
-
The public lifecycle
|
|
5
|
+
The public lifecycle starts with one async call and one optional auth transition:
|
|
6
6
|
|
|
7
7
|
```txt
|
|
8
|
-
|
|
8
|
+
start(options) -> Promise<PrimerState>
|
|
9
|
+
UnauthenticatedState.login() -> Promise<PrimerState>
|
|
9
10
|
```
|
|
10
11
|
|
|
11
|
-
`
|
|
12
|
+
`start` resolves existing learner auth when available, enters the first Primer learning state when the learner is ready, and returns the live state machine object your renderer drives. When learner sign-in is needed, `start` returns `UnauthenticatedState`; render a sign-in button and call `state.login()` directly from that button's click or tap handler.
|
|
12
13
|
|
|
13
14
|
```sh
|
|
14
15
|
bun add @superbuilders/primer-tives
|
|
@@ -16,7 +17,7 @@ bun add @superbuilders/primer-tives
|
|
|
16
17
|
|
|
17
18
|
## Version
|
|
18
19
|
|
|
19
|
-
The current SDK version is `3.
|
|
20
|
+
The current SDK version is `3.6.0`.
|
|
20
21
|
|
|
21
22
|
## Entrypoints
|
|
22
23
|
|
|
@@ -24,7 +25,7 @@ There is no package-root export. Import from the public subpath that owns the su
|
|
|
24
25
|
|
|
25
26
|
| Subpath | Owns |
|
|
26
27
|
| --- | --- |
|
|
27
|
-
| `@superbuilders/primer-tives/client` | `
|
|
28
|
+
| `@superbuilders/primer-tives/client` | `start`, `PrimerOptions`, `PrimerState`, all state interfaces, PCI render props |
|
|
28
29
|
| `@superbuilders/primer-tives/contracts` | Content, stimulus, interaction, submission, review, PCI types, schemas, validation helpers |
|
|
29
30
|
| `@superbuilders/primer-tives/errors` | Every SDK error sentinel |
|
|
30
31
|
| `@superbuilders/primer-tives/logger` | `PrimerLogger` interface |
|
|
@@ -38,100 +39,135 @@ Math content can require the fraction-input PCI capability, so a math renderer m
|
|
|
38
39
|
|
|
39
40
|
```ts
|
|
40
41
|
import * as logger from "@superbuilders/slog"
|
|
41
|
-
import {
|
|
42
|
+
import { start, type PrimerOptions } from "@superbuilders/primer-tives/client"
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
const options = {
|
|
44
45
|
origin: "https://primerlearn.dev",
|
|
45
46
|
publishableKey: "pk_...",
|
|
46
47
|
subject: "math",
|
|
47
48
|
supportedPcis: ["urn:primer:pci:fraction-input"],
|
|
48
49
|
logger
|
|
49
|
-
}
|
|
50
|
+
} satisfies PrimerOptions<"math", readonly ["urn:primer:pci:fraction-input"]>
|
|
51
|
+
|
|
52
|
+
let state = await start(options)
|
|
50
53
|
```
|
|
51
54
|
|
|
52
|
-
If your application already has a learner access token, pass it directly.
|
|
55
|
+
If your application already has a learner access token, pass it directly. `start` uses that token for the learning runtime.
|
|
53
56
|
|
|
54
57
|
```ts
|
|
55
|
-
|
|
58
|
+
const options = {
|
|
56
59
|
origin: "https://primerlearn.dev",
|
|
57
60
|
publishableKey: "pk_...",
|
|
58
61
|
accessToken,
|
|
59
62
|
subject: "math",
|
|
60
63
|
supportedPcis: ["urn:primer:pci:fraction-input"],
|
|
61
64
|
logger
|
|
62
|
-
}
|
|
65
|
+
} satisfies PrimerOptions<"math", readonly ["urn:primer:pci:fraction-input"]>
|
|
66
|
+
|
|
67
|
+
let state = await start(options)
|
|
63
68
|
```
|
|
64
69
|
|
|
70
|
+
When `start` returns `UnauthenticatedState`, render sign-in UI and call `login()` directly from a user gesture:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import type { PrimerState, UnauthenticatedState } from "@superbuilders/primer-tives/client"
|
|
74
|
+
|
|
75
|
+
let state: PrimerState = await start(options)
|
|
76
|
+
|
|
77
|
+
if (state.phase === "unauthenticated") {
|
|
78
|
+
renderSignInButton(state)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function handleSignInClick(authState: UnauthenticatedState): void {
|
|
82
|
+
void authState.login().then(function continueAfterLogin(nextState) {
|
|
83
|
+
state = nextState
|
|
84
|
+
renderPrimer(state)
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
For Chrome and other popup blockers, `login()` must be called directly from the click or tap handler. Do not put `await`, `setTimeout`, dynamic imports, analytics calls, or other async work before `state.login()`.
|
|
90
|
+
|
|
65
91
|
Vocabulary and science currently have no required PCI capabilities, so `supportedPcis` can be omitted for those subjects.
|
|
66
92
|
|
|
67
93
|
```ts
|
|
68
|
-
|
|
94
|
+
const options = {
|
|
69
95
|
origin: "https://primerlearn.dev",
|
|
70
96
|
publishableKey: "pk_...",
|
|
71
97
|
subject: "vocabulary",
|
|
72
98
|
logger
|
|
73
|
-
}
|
|
99
|
+
} satisfies PrimerOptions<"vocabulary">
|
|
100
|
+
|
|
101
|
+
let state = await start(options)
|
|
74
102
|
```
|
|
75
103
|
|
|
76
104
|
Omitting `subject` means the SDK asks Primer for the all-subject runtime scope. Because that scope can include math, it requires the union of all subject-required PCIs.
|
|
77
105
|
|
|
78
106
|
```ts
|
|
79
|
-
|
|
107
|
+
const options = {
|
|
80
108
|
origin: "https://primerlearn.dev",
|
|
81
109
|
publishableKey: "pk_...",
|
|
82
110
|
supportedPcis: ["urn:primer:pci:fraction-input"],
|
|
83
111
|
logger
|
|
84
|
-
}
|
|
112
|
+
} satisfies PrimerOptions<undefined, readonly ["urn:primer:pci:fraction-input"]>
|
|
113
|
+
|
|
114
|
+
let state = await start(options)
|
|
85
115
|
```
|
|
86
116
|
|
|
87
|
-
## `
|
|
117
|
+
## `start(options)`
|
|
88
118
|
|
|
89
119
|
```ts
|
|
90
|
-
function
|
|
120
|
+
function start<
|
|
91
121
|
const S extends Subject | undefined = undefined,
|
|
92
122
|
const Supported extends readonly PciId[] = []
|
|
93
123
|
>(options: PrimerOptions<S, Supported>): Promise<PrimerState>
|
|
94
124
|
```
|
|
95
125
|
|
|
96
|
-
`
|
|
126
|
+
`start` is the first SDK lifecycle operation. It may return any current state the renderer must handle:
|
|
97
127
|
|
|
98
|
-
|
|
|
128
|
+
| Result | Meaning |
|
|
99
129
|
| --- | --- |
|
|
100
|
-
|
|
|
101
|
-
|
|
|
130
|
+
| `UnauthenticatedState` | Learner sign-in is needed before runtime learning can begin. |
|
|
131
|
+
| `ObservationState`, `InteractionState`, or `FeedbackState` | A learning state is ready to render. |
|
|
132
|
+
| `CompletedState` | The runtime scope is already complete. |
|
|
133
|
+
| `ErroredState` | Startup or runtime communication failed but may be retriable. |
|
|
134
|
+
| `FatalState` | Startup or runtime communication failed terminally for this state object. |
|
|
102
135
|
|
|
103
|
-
|
|
136
|
+
Always switch on `state.phase`. Do not assume the first state is renderable learning content.
|
|
104
137
|
|
|
105
138
|
```ts
|
|
106
139
|
import * as errors from "@superbuilders/errors"
|
|
107
140
|
import * as logger from "@superbuilders/slog"
|
|
108
|
-
import {
|
|
109
|
-
import {
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
)
|
|
121
|
-
if (
|
|
122
|
-
if (errors.is(
|
|
123
|
-
|
|
141
|
+
import { start, type PrimerOptions } from "@superbuilders/primer-tives/client"
|
|
142
|
+
import { ErrAuthUnavailable, ErrMalformedAccessToken } from "@superbuilders/primer-tives/errors"
|
|
143
|
+
|
|
144
|
+
const options = {
|
|
145
|
+
origin,
|
|
146
|
+
publishableKey,
|
|
147
|
+
accessToken,
|
|
148
|
+
subject: "math",
|
|
149
|
+
supportedPcis: ["urn:primer:pci:fraction-input"],
|
|
150
|
+
logger
|
|
151
|
+
} satisfies PrimerOptions<"math", readonly ["urn:primer:pci:fraction-input"]>
|
|
152
|
+
|
|
153
|
+
let state = await start(options)
|
|
154
|
+
if (state.phase === "unauthenticated") {
|
|
155
|
+
if (errors.is(state.error, ErrAuthUnavailable)) {
|
|
156
|
+
renderUnsupportedBrowserMessage()
|
|
124
157
|
return
|
|
125
158
|
}
|
|
126
|
-
|
|
159
|
+
renderSignInButton(state)
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (state.phase === "fatal") {
|
|
164
|
+
if (errors.is(state.error, ErrMalformedAccessToken)) {
|
|
127
165
|
renderSignInAgain()
|
|
128
166
|
return
|
|
129
167
|
}
|
|
130
|
-
logger.error("primer
|
|
131
|
-
throw
|
|
168
|
+
logger.error("primer fatal state", { error: state.error })
|
|
169
|
+
throw state.error
|
|
132
170
|
}
|
|
133
|
-
|
|
134
|
-
let state = result.data
|
|
135
171
|
```
|
|
136
172
|
|
|
137
173
|
## `PrimerOptions`
|
|
@@ -153,27 +189,29 @@ type PrimerOptions<S extends Subject | undefined = undefined, Supported extends
|
|
|
153
189
|
| --- | --- | --- |
|
|
154
190
|
| `origin` | Yes | Primer deployment origin. |
|
|
155
191
|
| `publishableKey` | Yes | Public key identifying the Primer frontend your runtime belongs to. |
|
|
156
|
-
| `accessToken` | No | Learner access token. When present,
|
|
192
|
+
| `accessToken` | No | Learner access token. When present, `start` uses it for the learning runtime. |
|
|
157
193
|
| `subject` | No | Public content scope: `"math"`, `"vocabulary"`, or `"science"`. Omitted means all-subject scope. |
|
|
158
194
|
| `supportedPcis` | Subject-dependent | Renderer capabilities for Portable Custom Interactions. Required when the chosen scope can emit required PCIs. |
|
|
159
195
|
| `fetch` | No | Fetch override for tests, instrumentation, or host runtime integration. |
|
|
160
196
|
| `abort` | No | Abort controller for SDK runtime work. |
|
|
161
197
|
| `logger` | Yes | Structured logger implementing `debug`, `info`, `warn`, and `error`. |
|
|
162
198
|
|
|
163
|
-
The presence or absence of `accessToken` selects auth semantics.
|
|
199
|
+
The presence or absence of `accessToken` selects startup auth semantics.
|
|
164
200
|
|
|
165
201
|
| Shape | Semantics |
|
|
166
202
|
| --- | --- |
|
|
167
|
-
| `accessToken` present |
|
|
168
|
-
| `accessToken` absent |
|
|
203
|
+
| `accessToken` present | `start` validates the token shape locally and uses it for learning runtime state. |
|
|
204
|
+
| `accessToken` absent | `start` uses existing browser auth state when available, or returns `UnauthenticatedState` when learner sign-in is needed. |
|
|
205
|
+
|
|
206
|
+
The public API exposes auth as state behavior. `UnauthenticatedState.login()` is the sign-in transition.
|
|
169
207
|
|
|
170
|
-
|
|
208
|
+
When `subject` is present, it must be a concrete literal subject at the options declaration site. Do not pass a broad `Subject` value into `PrimerOptions`; narrow it in host control flow, then construct options with `satisfies PrimerOptions<"math", ...>`, `satisfies PrimerOptions<"vocabulary", ...>`, or `satisfies PrimerOptions<"science", ...>`.
|
|
171
209
|
|
|
172
210
|
## Auth Semantics
|
|
173
211
|
|
|
174
|
-
An access token is expected to be JWS-shaped: it starts with `eyJ` and contains exactly two dots. If a provided
|
|
212
|
+
An access token is expected to be JWS-shaped: it starts with `eyJ` and contains exactly two dots. If a provided token does not match that shape, `start` returns `FatalState` with `ErrMalformedAccessToken`.
|
|
175
213
|
|
|
176
|
-
SDK-managed auth may require browser capabilities and learner interaction.
|
|
214
|
+
SDK-managed auth may require browser capabilities and learner interaction. Auth failures are exposed through `UnauthenticatedState.error`:
|
|
177
215
|
|
|
178
216
|
| Sentinel | Meaning |
|
|
179
217
|
| --- | --- |
|
|
@@ -196,22 +234,24 @@ import {
|
|
|
196
234
|
ErrAuthUnavailable
|
|
197
235
|
} from "@superbuilders/primer-tives/errors"
|
|
198
236
|
|
|
199
|
-
|
|
200
|
-
if (
|
|
201
|
-
if (errors.is(
|
|
202
|
-
renderPopupInstructions()
|
|
237
|
+
let state = await start(options)
|
|
238
|
+
if (state.phase === "unauthenticated") {
|
|
239
|
+
if (errors.is(state.error, ErrAuthPopupBlocked)) {
|
|
240
|
+
renderPopupInstructions(state)
|
|
203
241
|
return
|
|
204
242
|
}
|
|
205
|
-
if (errors.is(
|
|
206
|
-
renderTryAgain()
|
|
243
|
+
if (errors.is(state.error, ErrAuthCancelled)) {
|
|
244
|
+
renderTryAgain(state)
|
|
207
245
|
return
|
|
208
246
|
}
|
|
209
|
-
if (errors.is(
|
|
247
|
+
if (errors.is(state.error, ErrAuthUnavailable)) {
|
|
210
248
|
renderUnsupportedBrowserMessage()
|
|
211
249
|
return
|
|
212
250
|
}
|
|
213
|
-
|
|
214
|
-
|
|
251
|
+
if (state.error !== null) {
|
|
252
|
+
logger.error("primer auth failed", { error: state.error })
|
|
253
|
+
}
|
|
254
|
+
renderSignInButton(state)
|
|
215
255
|
}
|
|
216
256
|
```
|
|
217
257
|
|
|
@@ -247,7 +287,7 @@ Order does not matter. Extra supported PCIs are allowed. `supportedPcis` is a re
|
|
|
247
287
|
This fails at compile time because math can emit a required PCI:
|
|
248
288
|
|
|
249
289
|
```ts
|
|
250
|
-
await
|
|
290
|
+
await start({
|
|
251
291
|
origin,
|
|
252
292
|
publishableKey,
|
|
253
293
|
subject: "math",
|
|
@@ -258,27 +298,15 @@ await create({
|
|
|
258
298
|
This passes:
|
|
259
299
|
|
|
260
300
|
```ts
|
|
261
|
-
|
|
301
|
+
const options = {
|
|
262
302
|
origin,
|
|
263
303
|
publishableKey,
|
|
264
304
|
subject: "math",
|
|
265
305
|
supportedPcis: ["urn:primer:pci:fraction-input"],
|
|
266
306
|
logger
|
|
267
|
-
}
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
If `subject` is a broad dynamic `Subject` value, TypeScript requires support for the union of all PCIs required by the possible subjects.
|
|
307
|
+
} satisfies PrimerOptions<"math", readonly ["urn:primer:pci:fraction-input"]>
|
|
271
308
|
|
|
272
|
-
|
|
273
|
-
declare const subject: Subject
|
|
274
|
-
|
|
275
|
-
await create({
|
|
276
|
-
origin,
|
|
277
|
-
publishableKey,
|
|
278
|
-
subject,
|
|
279
|
-
supportedPcis: ["urn:primer:pci:fraction-input"],
|
|
280
|
-
logger
|
|
281
|
-
})
|
|
309
|
+
await start(options)
|
|
282
310
|
```
|
|
283
311
|
|
|
284
312
|
The runtime also protects the trust boundary. If Primer presents a portable custom interaction that is not declared in `supportedPcis`, the SDK returns `FatalState` with `ErrUnsupportedPci`.
|
|
@@ -296,6 +324,9 @@ async function runPrimer(initialState: PrimerState): Promise<void> {
|
|
|
296
324
|
|
|
297
325
|
while (state.phase !== "completed" && state.phase !== "fatal") {
|
|
298
326
|
switch (state.phase) {
|
|
327
|
+
case "unauthenticated":
|
|
328
|
+
renderSignInButton(state)
|
|
329
|
+
return
|
|
299
330
|
case "observation":
|
|
300
331
|
renderFrame(state.body, state.stimulus)
|
|
301
332
|
state = await state.advance()
|
|
@@ -328,6 +359,7 @@ State transitions:
|
|
|
328
359
|
|
|
329
360
|
| Current state | Valid operation | Next result |
|
|
330
361
|
| --- | --- | --- |
|
|
362
|
+
| `UnauthenticatedState` | `login()` | `Promise<PrimerState>` |
|
|
331
363
|
| `ObservationState` | `advance()` | `Promise<PrimerState>` |
|
|
332
364
|
| `ChoiceState` | `submitChoice(selectedKeys)` or `timeout()` | `Promise<PrimerState>` |
|
|
333
365
|
| `TextEntryState` | `submitText(value)` or `timeout()` | `Promise<PrimerState>` |
|
|
@@ -345,6 +377,7 @@ State transitions:
|
|
|
345
377
|
|
|
346
378
|
```ts
|
|
347
379
|
type PrimerState<Pcis extends PciId = PciId> =
|
|
380
|
+
| UnauthenticatedState<Pcis>
|
|
348
381
|
| ObservationState<Pcis>
|
|
349
382
|
| InteractionState<Pcis>
|
|
350
383
|
| FeedbackState<Pcis>
|
|
@@ -355,7 +388,63 @@ type PrimerState<Pcis extends PciId = PciId> =
|
|
|
355
388
|
|
|
356
389
|
`PrimerState` is live in-memory state. It contains transition closures, pending-operation guards, and retry behavior. Do not serialize it, store it, clone it through JSON, or pass it through host data. Calling `JSON.stringify(state)` throws `ErrNotSerializable`.
|
|
357
390
|
|
|
358
|
-
|
|
391
|
+
Start a new state by calling `start` again after a reload, remount, account switch, or subject switch.
|
|
392
|
+
|
|
393
|
+
## `UnauthenticatedState`
|
|
394
|
+
|
|
395
|
+
```ts
|
|
396
|
+
interface UnauthenticatedState<Pcis extends PciId = PciId> {
|
|
397
|
+
readonly phase: "unauthenticated"
|
|
398
|
+
readonly error: Error | null
|
|
399
|
+
login(): Promise<PrimerState<Pcis>>
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
`UnauthenticatedState` means learner sign-in is required before learning content can be rendered. Render a sign-in button or equivalent learner action. Call `login()` only from that user action.
|
|
404
|
+
|
|
405
|
+
Correct browser-safe pattern:
|
|
406
|
+
|
|
407
|
+
```ts
|
|
408
|
+
function handleSignInClick(state: UnauthenticatedState): void {
|
|
409
|
+
void state.login().then(function continueAfterLogin(nextState) {
|
|
410
|
+
renderPrimer(nextState)
|
|
411
|
+
})
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
React renderers should use the same direct-call rule:
|
|
416
|
+
|
|
417
|
+
```tsx
|
|
418
|
+
function SignInButton({ state }: { state: UnauthenticatedState }) {
|
|
419
|
+
async function handleClick() {
|
|
420
|
+
const nextState = await state.login()
|
|
421
|
+
renderPrimer(nextState)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return <button type="button" onClick={handleClick}>Sign in to continue</button>
|
|
425
|
+
}
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
For Chrome popup blocking, `state.login()` must be the first async-producing operation in the click or tap handler. This is correct:
|
|
429
|
+
|
|
430
|
+
```ts
|
|
431
|
+
async function handleClick() {
|
|
432
|
+
const nextState = await state.login()
|
|
433
|
+
renderPrimer(nextState)
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
This is not browser-safe:
|
|
438
|
+
|
|
439
|
+
```ts
|
|
440
|
+
async function handleClick() {
|
|
441
|
+
await recordAnalyticsClick()
|
|
442
|
+
const nextState = await state.login()
|
|
443
|
+
renderPrimer(nextState)
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
If `login()` cannot complete auth, it resolves to another `UnauthenticatedState` with `error` set to the auth sentinel. Render retry or unsupported-browser UI from that state.
|
|
359
448
|
|
|
360
449
|
## Common State Fields
|
|
361
450
|
|
|
@@ -672,7 +761,7 @@ interface FatalState {
|
|
|
672
761
|
}
|
|
673
762
|
```
|
|
674
763
|
|
|
675
|
-
Fatal state means the SDK cannot recover by retrying the current learner intent. Render a terminal error UI,
|
|
764
|
+
Fatal state means the SDK cannot recover by retrying the current learner intent. Render a terminal error UI, call `start` again with valid options, or send the learner through auth again depending on the sentinel.
|
|
676
765
|
|
|
677
766
|
Fatal sentinels:
|
|
678
767
|
|
|
@@ -1010,7 +1099,7 @@ type PciRenderProps<K extends PciId> = PciPendingRenderProps<K> | PciSubmittedRe
|
|
|
1010
1099
|
|
|
1011
1100
|
## Subject PCI Helpers
|
|
1012
1101
|
|
|
1013
|
-
Use `@superbuilders/primer-tives/subject-pcis` when renderer tooling needs the same subject-to-PCI contract as `
|
|
1102
|
+
Use `@superbuilders/primer-tives/subject-pcis` when renderer tooling needs the same subject-to-PCI contract as `start`.
|
|
1014
1103
|
|
|
1015
1104
|
```ts
|
|
1016
1105
|
import {
|
|
@@ -1073,17 +1162,17 @@ import {
|
|
|
1073
1162
|
} from "@superbuilders/primer-tives/errors"
|
|
1074
1163
|
```
|
|
1075
1164
|
|
|
1076
|
-
##
|
|
1165
|
+
## Auth And Startup Errors
|
|
1077
1166
|
|
|
1078
|
-
|
|
1167
|
+
Auth and startup failures are represented as state whenever possible.
|
|
1079
1168
|
|
|
1080
1169
|
| Sentinel | Meaning | Typical handling |
|
|
1081
1170
|
| --- | --- | --- |
|
|
1082
|
-
| `ErrAuthUnavailable` | SDK-managed auth cannot run in the current host environment. |
|
|
1171
|
+
| `ErrAuthUnavailable` | SDK-managed auth cannot run in the current host environment. | Render unsupported-runtime or externally managed sign-in UI. |
|
|
1083
1172
|
| `ErrAuthConfigInvalid` | Public auth configuration is invalid. | Log and treat as integration error. |
|
|
1084
1173
|
| `ErrAuthCallbackInvalid` | Learner auth did not complete with an acceptable result. | Offer sign-in retry; log if unexpected. |
|
|
1085
|
-
| `ErrAuthStateMismatch` | Auth result does not match the initiated auth attempt. |
|
|
1086
|
-
| `ErrAuthPopupBlocked` | Browser blocked learner auth UI. |
|
|
1174
|
+
| `ErrAuthStateMismatch` | Auth result does not match the initiated auth attempt. | Offer sign-in retry. |
|
|
1175
|
+
| `ErrAuthPopupBlocked` | Browser blocked learner auth UI. | Render sign-in instructions and retry from a direct user gesture. |
|
|
1087
1176
|
| `ErrAuthCancelled` | Learner auth was closed or timed out. | Offer retry. |
|
|
1088
1177
|
| `ErrMalformedAccessToken` | Provided or resolved token is not shaped like a learner access token. | Re-authenticate learner or fix token source. |
|
|
1089
1178
|
|
|
@@ -1112,20 +1201,32 @@ Runtime errors are represented as `ErroredState` or `FatalState`.
|
|
|
1112
1201
|
|
|
1113
1202
|
## Error-Handling Recipes
|
|
1114
1203
|
|
|
1115
|
-
Handle
|
|
1204
|
+
Handle auth-needed state before rendering learning content:
|
|
1116
1205
|
|
|
1117
1206
|
```ts
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1207
|
+
let state = await start(options)
|
|
1208
|
+
|
|
1209
|
+
if (state.phase === "unauthenticated") {
|
|
1210
|
+
if (errors.is(state.error, ErrAuthCancelled)) {
|
|
1211
|
+
renderTryAgain(state)
|
|
1122
1212
|
return
|
|
1123
1213
|
}
|
|
1124
|
-
|
|
1125
|
-
|
|
1214
|
+
if (state.error !== null) {
|
|
1215
|
+
logger.error("primer auth needed", { error: state.error })
|
|
1216
|
+
}
|
|
1217
|
+
renderSignInButton(state)
|
|
1218
|
+
return
|
|
1126
1219
|
}
|
|
1220
|
+
```
|
|
1221
|
+
|
|
1222
|
+
Bind login directly to the sign-in button:
|
|
1127
1223
|
|
|
1128
|
-
|
|
1224
|
+
```ts
|
|
1225
|
+
function handleSignInClick(state: UnauthenticatedState): void {
|
|
1226
|
+
void state.login().then(function continueAfterLogin(nextState) {
|
|
1227
|
+
renderPrimer(nextState)
|
|
1228
|
+
})
|
|
1229
|
+
}
|
|
1129
1230
|
```
|
|
1130
1231
|
|
|
1131
1232
|
Handle runtime errors through the state machine:
|
|
@@ -1184,13 +1285,16 @@ interface PrimerLogger {
|
|
|
1184
1285
|
|
|
1185
1286
|
```ts
|
|
1186
1287
|
import * as logger from "@superbuilders/slog"
|
|
1288
|
+
import { start, type PrimerOptions } from "@superbuilders/primer-tives/client"
|
|
1187
1289
|
|
|
1188
|
-
const
|
|
1290
|
+
const options = {
|
|
1189
1291
|
origin,
|
|
1190
1292
|
publishableKey,
|
|
1191
1293
|
subject: "vocabulary",
|
|
1192
1294
|
logger
|
|
1193
|
-
}
|
|
1295
|
+
} satisfies PrimerOptions<"vocabulary">
|
|
1296
|
+
|
|
1297
|
+
const state = await start(options)
|
|
1194
1298
|
```
|
|
1195
1299
|
|
|
1196
1300
|
## Grade Levels
|
|
@@ -1208,20 +1312,21 @@ const GRADE_LEVELS = ["K", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "1
|
|
|
1208
1312
|
type GradeLevel = (typeof GRADE_LEVELS)[number]
|
|
1209
1313
|
```
|
|
1210
1314
|
|
|
1211
|
-
Grade level is not a `
|
|
1315
|
+
Grade level is not a `start` option. Treat grade-level identifiers as content/runtime data when Primer presents them, not as an SDK lifecycle input.
|
|
1212
1316
|
|
|
1213
1317
|
## Testing
|
|
1214
1318
|
|
|
1215
|
-
`PrimerOptions.fetch` exists so tests, host runtimes, and instrumentation can provide a fetch-compatible function. The SDK treats it exactly as the runtime communication function for `
|
|
1319
|
+
`PrimerOptions.fetch` exists so tests, host runtimes, and instrumentation can provide a fetch-compatible function. The SDK treats it exactly as the runtime communication function for `start`, transitions, submissions, retries, and timeouts.
|
|
1216
1320
|
|
|
1217
|
-
The runtime exchange shape is not public SDK surface. Tests should assert SDK semantics after `
|
|
1321
|
+
The runtime exchange shape is not public SDK surface. Tests should assert SDK semantics after `start` resolves:
|
|
1218
1322
|
|
|
1219
1323
|
| Scenario | Assert |
|
|
1220
1324
|
| --- | --- |
|
|
1221
|
-
| auth
|
|
1222
|
-
|
|
|
1223
|
-
| first runtime work fails
|
|
1224
|
-
|
|
|
1325
|
+
| auth is needed | `start` resolves to `UnauthenticatedState` |
|
|
1326
|
+
| auth login fails | `login()` resolves to `UnauthenticatedState` with the expected auth sentinel |
|
|
1327
|
+
| first runtime work fails recoverably | `start` resolves to `ErroredState` with `retriable: true` |
|
|
1328
|
+
| first runtime work fails terminally | `start` resolves to `FatalState` |
|
|
1329
|
+
| unsupported PCI is presented | `start` resolves to `FatalState` with `ErrUnsupportedPci` |
|
|
1225
1330
|
| standard submission is invalid | submit method resolves to `ErroredState` with `ErrInvalidSubmission` |
|
|
1226
1331
|
| concurrent submit/timeout conflict occurs | transition resolves to `ErroredState` with `ErrConflict` |
|
|
1227
1332
|
| state is serialized | serialization throws `ErrNotSerializable` |
|
|
@@ -1230,19 +1335,21 @@ Example test shape:
|
|
|
1230
1335
|
|
|
1231
1336
|
```ts
|
|
1232
1337
|
import * as errors from "@superbuilders/errors"
|
|
1233
|
-
import {
|
|
1338
|
+
import { start, type PrimerOptions } from "@superbuilders/primer-tives/client"
|
|
1234
1339
|
import { ErrUnsupportedPci } from "@superbuilders/primer-tives/errors"
|
|
1235
1340
|
|
|
1236
1341
|
declare const fetchMock: typeof globalThis.fetch
|
|
1237
1342
|
|
|
1238
|
-
const
|
|
1343
|
+
const options = {
|
|
1239
1344
|
origin: "https://primer.test",
|
|
1240
1345
|
publishableKey: "pk_test",
|
|
1241
1346
|
accessToken: "eyJ.test.token",
|
|
1242
|
-
subject: "
|
|
1347
|
+
subject: "vocabulary",
|
|
1243
1348
|
fetch: fetchMock,
|
|
1244
1349
|
logger
|
|
1245
|
-
}
|
|
1350
|
+
} satisfies PrimerOptions<"vocabulary">
|
|
1351
|
+
|
|
1352
|
+
const state = await start(options)
|
|
1246
1353
|
|
|
1247
1354
|
if (state.phase === "fatal") {
|
|
1248
1355
|
if (errors.is(state.error, ErrUnsupportedPci)) {
|
|
@@ -1278,20 +1385,21 @@ grade level
|
|
|
1278
1385
|
|
|
1279
1386
|
## Integration Checklist
|
|
1280
1387
|
|
|
1281
|
-
1. Import `
|
|
1388
|
+
1. Import `start` and `PrimerOptions` from `@superbuilders/primer-tives/client`.
|
|
1282
1389
|
2. Import shared renderer contracts from `@superbuilders/primer-tives/contracts`.
|
|
1283
|
-
3.
|
|
1284
|
-
4.
|
|
1285
|
-
5.
|
|
1286
|
-
6.
|
|
1287
|
-
7.
|
|
1288
|
-
8.
|
|
1289
|
-
9.
|
|
1290
|
-
10.
|
|
1291
|
-
11.
|
|
1292
|
-
12. Handle `
|
|
1293
|
-
13.
|
|
1294
|
-
14.
|
|
1390
|
+
3. Define options with `satisfies PrimerOptions<...>` so subject and PCI requirements stay visible at the declaration site.
|
|
1391
|
+
4. Pass `origin`, `publishableKey`, and `logger` to `start`.
|
|
1392
|
+
5. Either pass `accessToken` or handle `UnauthenticatedState`.
|
|
1393
|
+
6. Choose a public `subject`, or omit `subject` for all-subject runtime scope.
|
|
1394
|
+
7. Declare every required renderer PCI in `supportedPcis`.
|
|
1395
|
+
8. Await `start` and render by switching on `state.phase`.
|
|
1396
|
+
9. If `state.phase === "unauthenticated"`, render sign-in UI and bind `state.login()` directly to the click or tap handler.
|
|
1397
|
+
10. For interaction states, render by switching on `state.kind`.
|
|
1398
|
+
11. Use only the transition methods exposed by the current state.
|
|
1399
|
+
12. Handle `ErroredState` through `retriable` and `retry()`.
|
|
1400
|
+
13. Handle `FatalState` as terminal for the current state object.
|
|
1401
|
+
14. Never serialize `PrimerState`.
|
|
1402
|
+
15. Start a new state with `start` after reload, remount, account switch, or subject switch.
|
|
1295
1403
|
|
|
1296
1404
|
## What This SDK Does Not Expose
|
|
1297
1405
|
|
|
@@ -1301,12 +1409,11 @@ The current SDK intentionally does not expose:
|
|
|
1301
1409
|
| --- | --- |
|
|
1302
1410
|
| package-root exports | explicit public subpaths |
|
|
1303
1411
|
| backend-only SDK surface | browser/client semantic SDK only |
|
|
1304
|
-
| public auth result union | `
|
|
1305
|
-
| separate auth API | `
|
|
1306
|
-
| client wrapper object | `
|
|
1307
|
-
| `start()` | `create` already starts the first state |
|
|
1412
|
+
| public auth result union | `UnauthenticatedState` |
|
|
1413
|
+
| separate auth API object | `UnauthenticatedState.login()` |
|
|
1414
|
+
| client wrapper object | `start` returning `Promise<PrimerState>` |
|
|
1308
1415
|
| `snapshot()` | live `PrimerState` only |
|
|
1309
|
-
| serializable state |
|
|
1416
|
+
| serializable state | start a new state with `start` |
|
|
1310
1417
|
| public `"all"` subject value | omit `subject` |
|
|
1311
1418
|
| implicit PCI negotiation | explicit `supportedPcis` |
|
|
1312
1419
|
| grade-level lifecycle option | runtime content/state handling |
|
|
@@ -1314,10 +1421,11 @@ The current SDK intentionally does not expose:
|
|
|
1314
1421
|
## Final Invariants
|
|
1315
1422
|
|
|
1316
1423
|
```txt
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
accessToken present
|
|
1320
|
-
accessToken absent
|
|
1424
|
+
start is the public lifecycle entrypoint
|
|
1425
|
+
start returns Promise<PrimerState>
|
|
1426
|
+
accessToken present is used for learner runtime auth
|
|
1427
|
+
accessToken absent may produce UnauthenticatedState
|
|
1428
|
+
UnauthenticatedState.login is the hosted-auth user-gesture transition
|
|
1321
1429
|
subject is optional; omitted means all-subject runtime scope
|
|
1322
1430
|
subject determines required renderer PCI capabilities
|
|
1323
1431
|
supportedPcis declares renderer PCI capabilities
|
|
@@ -8,7 +8,22 @@ type AccessTokenResolverOptions = {
|
|
|
8
8
|
readonly hostedAuth?: HostedAuthOptions;
|
|
9
9
|
readonly logger: PrimerLogger;
|
|
10
10
|
};
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
type ResolvedAccessTokenResult = {
|
|
12
|
+
readonly kind: "resolved";
|
|
13
|
+
readonly accessToken: ResolvedAccessToken;
|
|
14
|
+
};
|
|
15
|
+
type UnauthenticatedAccessTokenResult = {
|
|
16
|
+
readonly kind: "unauthenticated";
|
|
17
|
+
readonly error: Error | null;
|
|
18
|
+
};
|
|
19
|
+
type FatalAccessTokenResult = {
|
|
20
|
+
readonly kind: "fatal";
|
|
21
|
+
readonly error: Error;
|
|
22
|
+
};
|
|
23
|
+
type ExistingAccessTokenResult = ResolvedAccessTokenResult | UnauthenticatedAccessTokenResult | FatalAccessTokenResult;
|
|
24
|
+
type HostedLoginResult = ResolvedAccessTokenResult | UnauthenticatedAccessTokenResult;
|
|
25
|
+
declare function resolveExistingAccessToken(options: AccessTokenResolverOptions): ExistingAccessTokenResult;
|
|
26
|
+
declare function beginHostedLogin(options: AccessTokenResolverOptions): Promise<HostedLoginResult>;
|
|
27
|
+
export { beginHostedLogin, resolveExistingAccessToken };
|
|
28
|
+
export type { AccessTokenResolverOptions, ExistingAccessTokenResult, HostedLoginResult };
|
|
14
29
|
//# sourceMappingURL=provider.d.ts.map
|