@superbuilders/primer-tives 4.0.3 → 4.0.5
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 +219 -102
- package/dist/client/auth/browser.d.ts +4 -14
- package/dist/client/auth/browser.d.ts.map +1 -1
- package/dist/client/auth/hosted-popup.d.ts +0 -2
- package/dist/client/auth/hosted-popup.d.ts.map +1 -1
- package/dist/client/auth/provider.d.ts +18 -8
- package/dist/client/auth/provider.d.ts.map +1 -1
- package/dist/client/auth/storage.d.ts +7 -9
- package/dist/client/auth/storage.d.ts.map +1 -1
- package/dist/client/auth-state.d.ts +14 -0
- package/dist/client/auth-state.d.ts.map +1 -0
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +154 -159
- package/dist/client/index.js.map +11 -11
- package/dist/client/session.d.ts +1 -1
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/start.d.ts +10 -4
- package/dist/client/start.d.ts.map +1 -1
- package/dist/client/types.d.ts +29 -8
- package/dist/client/types.d.ts.map +1 -1
- package/dist/contracts/validation.d.ts +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/dist/client/auth/callback.d.ts +0 -10
- package/dist/client/auth/callback.d.ts.map +0 -1
- package/dist/client/unauthenticated-state.d.ts +0 -10
- package/dist/client/unauthenticated-state.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
TypeScript SDK primitives for the Primer adaptive learning runtime.
|
|
4
4
|
|
|
5
|
-
The public lifecycle starts with one async call and one
|
|
5
|
+
The public lifecycle starts with one async call and, when hosted auth is needed, one user-gesture auth transition:
|
|
6
6
|
|
|
7
7
|
```txt
|
|
8
|
-
start(options) -> Promise<
|
|
9
|
-
|
|
8
|
+
start(options with accessToken) -> Promise<AccessTokenStartState>
|
|
9
|
+
start(options without accessToken) -> Promise<ManagedStartState>
|
|
10
|
+
SignInRequiredState.login() -> Promise<ManagedStartState>
|
|
11
|
+
SignInFailedState.login() -> Promise<ManagedStartState>
|
|
10
12
|
```
|
|
11
13
|
|
|
12
|
-
`start`
|
|
14
|
+
`start` enters the first Primer learning state when learner auth is ready and returns the live state machine object your renderer drives. When learner sign-in is needed, managed-auth `start` returns `SignInRequiredState`; render a sign-in button and call `state.login()` directly from that button's click or tap handler.
|
|
13
15
|
|
|
14
16
|
```sh
|
|
15
17
|
bun add @superbuilders/primer-tives
|
|
@@ -17,7 +19,7 @@ bun add @superbuilders/primer-tives
|
|
|
17
19
|
|
|
18
20
|
## Version
|
|
19
21
|
|
|
20
|
-
The current SDK version is `4.0.
|
|
22
|
+
The current SDK version is `4.0.5`.
|
|
21
23
|
|
|
22
24
|
## Entrypoints
|
|
23
25
|
|
|
@@ -25,7 +27,7 @@ There is no package-root export. Import from the public subpath that owns the su
|
|
|
25
27
|
|
|
26
28
|
| Subpath | Owns |
|
|
27
29
|
| --- | --- |
|
|
28
|
-
| `@superbuilders/primer-tives/client` | `start`, `PrimerOptions`, `PrimerState`, all state interfaces, PCI render props |
|
|
30
|
+
| `@superbuilders/primer-tives/client` | `start`, `PrimerOptions`, auth-specific start option types, `PrimerState`, all state interfaces, PCI render props |
|
|
29
31
|
| `@superbuilders/primer-tives/contracts` | Content, stimulus, interaction, submission, review, PCI types, schemas, validation helpers |
|
|
30
32
|
| `@superbuilders/primer-tives/errors` | Every SDK error sentinel |
|
|
31
33
|
| `@superbuilders/primer-tives/logger` | `PrimerLogger` interface |
|
|
@@ -39,14 +41,18 @@ Math content can require the fraction-input PCI capability, so a math renderer m
|
|
|
39
41
|
|
|
40
42
|
```ts
|
|
41
43
|
import { logger } from "@/logger"
|
|
42
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
start,
|
|
46
|
+
type PrimerOptionsWithAccessToken,
|
|
47
|
+
type PrimerOptionsWithManagedAuth
|
|
48
|
+
} from "@superbuilders/primer-tives/client"
|
|
43
49
|
|
|
44
50
|
const options = {
|
|
45
51
|
publishableKey: "pk_...",
|
|
46
52
|
subject: "math",
|
|
47
53
|
supportedPcis: ["urn:primer:pci:fraction-input"],
|
|
48
54
|
logger
|
|
49
|
-
} satisfies
|
|
55
|
+
} satisfies PrimerOptionsWithManagedAuth<"math", readonly ["urn:primer:pci:fraction-input"]>
|
|
50
56
|
|
|
51
57
|
let state = await start(options)
|
|
52
58
|
```
|
|
@@ -60,23 +66,23 @@ const options = {
|
|
|
60
66
|
subject: "math",
|
|
61
67
|
supportedPcis: ["urn:primer:pci:fraction-input"],
|
|
62
68
|
logger
|
|
63
|
-
} satisfies
|
|
69
|
+
} satisfies PrimerOptionsWithAccessToken<"math", readonly ["urn:primer:pci:fraction-input"]>
|
|
64
70
|
|
|
65
71
|
let state = await start(options)
|
|
66
72
|
```
|
|
67
73
|
|
|
68
|
-
When `start` returns `
|
|
74
|
+
When managed-auth `start` returns `SignInRequiredState`, render sign-in UI and call `login()` directly from a user gesture:
|
|
69
75
|
|
|
70
76
|
```ts
|
|
71
|
-
import type {
|
|
77
|
+
import type { ManagedStartState, SignInRequiredState } from "@superbuilders/primer-tives/client"
|
|
72
78
|
|
|
73
|
-
let state:
|
|
79
|
+
let state: ManagedStartState = await start(options)
|
|
74
80
|
|
|
75
|
-
if (state.phase === "
|
|
81
|
+
if (state.phase === "sign-in-required") {
|
|
76
82
|
renderSignInButton(state)
|
|
77
83
|
}
|
|
78
84
|
|
|
79
|
-
function handleSignInClick(authState:
|
|
85
|
+
function handleSignInClick(authState: SignInRequiredState): void {
|
|
80
86
|
void authState.login().then(function continueAfterLogin(nextState) {
|
|
81
87
|
state = nextState
|
|
82
88
|
renderPrimer(state)
|
|
@@ -93,7 +99,7 @@ const options = {
|
|
|
93
99
|
publishableKey: "pk_...",
|
|
94
100
|
subject: "vocabulary",
|
|
95
101
|
logger
|
|
96
|
-
} satisfies
|
|
102
|
+
} satisfies PrimerOptionsWithManagedAuth<"vocabulary">
|
|
97
103
|
|
|
98
104
|
let state = await start(options)
|
|
99
105
|
```
|
|
@@ -105,7 +111,7 @@ const options = {
|
|
|
105
111
|
publishableKey: "pk_...",
|
|
106
112
|
supportedPcis: ["urn:primer:pci:fraction-input"],
|
|
107
113
|
logger
|
|
108
|
-
} satisfies
|
|
114
|
+
} satisfies PrimerOptionsWithManagedAuth<undefined, readonly ["urn:primer:pci:fraction-input"]>
|
|
109
115
|
|
|
110
116
|
let state = await start(options)
|
|
111
117
|
```
|
|
@@ -116,14 +122,22 @@ let state = await start(options)
|
|
|
116
122
|
function start<
|
|
117
123
|
const S extends Subject | undefined = undefined,
|
|
118
124
|
const Supported extends readonly PciId[] = []
|
|
119
|
-
>(options:
|
|
125
|
+
>(options: PrimerOptionsWithAccessToken<S, Supported>): Promise<AccessTokenStartState>
|
|
126
|
+
|
|
127
|
+
function start<
|
|
128
|
+
const S extends Subject | undefined = undefined,
|
|
129
|
+
const Supported extends readonly PciId[] = []
|
|
130
|
+
>(options: PrimerOptionsWithManagedAuth<S, Supported>): Promise<ManagedStartState>
|
|
120
131
|
```
|
|
121
132
|
|
|
122
|
-
`start` is the first SDK lifecycle operation.
|
|
133
|
+
`start` is the first SDK lifecycle operation. Its return type depends on whether `accessToken` is present:
|
|
123
134
|
|
|
124
135
|
| Result | Meaning |
|
|
125
136
|
| --- | --- |
|
|
126
|
-
| `
|
|
137
|
+
| `SignInRequiredState` | Learner sign-in is needed before runtime learning can begin. Managed-auth mode only. |
|
|
138
|
+
| `SignInFailedState` | Hosted sign-in failed but can be retried. Managed-auth mode only. |
|
|
139
|
+
| `AuthUnavailableState` | Browser-hosted auth cannot run in the current runtime. Managed-auth mode only. |
|
|
140
|
+
| `AuthConfigInvalidState` | Hosted-auth configuration is invalid and cannot be retried. Managed-auth mode only. |
|
|
127
141
|
| `ObservationState`, `InteractionState`, or `FeedbackState` | A learning state is ready to render. |
|
|
128
142
|
| `CompletedState` | The runtime scope is already complete. |
|
|
129
143
|
| `ErroredState` | Startup or runtime communication failed but may be retriable. |
|
|
@@ -134,7 +148,7 @@ Always switch on `state.phase`. Do not assume the first state is renderable lear
|
|
|
134
148
|
```ts
|
|
135
149
|
import * as errors from "@superbuilders/errors"
|
|
136
150
|
import { logger } from "@/logger"
|
|
137
|
-
import { start, type
|
|
151
|
+
import { start, type PrimerOptionsWithAccessToken } from "@superbuilders/primer-tives/client"
|
|
138
152
|
import { ErrAuthUnavailable, ErrMalformedAccessToken } from "@superbuilders/primer-tives/errors"
|
|
139
153
|
|
|
140
154
|
const options = {
|
|
@@ -143,24 +157,15 @@ const options = {
|
|
|
143
157
|
subject: "math",
|
|
144
158
|
supportedPcis: ["urn:primer:pci:fraction-input"],
|
|
145
159
|
logger
|
|
146
|
-
} satisfies
|
|
160
|
+
} satisfies PrimerOptionsWithAccessToken<"math", readonly ["urn:primer:pci:fraction-input"]>
|
|
147
161
|
|
|
148
162
|
let state = await start(options)
|
|
149
|
-
if (state.phase === "unauthenticated") {
|
|
150
|
-
if (errors.is(state.error, ErrAuthUnavailable)) {
|
|
151
|
-
renderUnsupportedBrowserMessage()
|
|
152
|
-
return
|
|
153
|
-
}
|
|
154
|
-
renderSignInButton(state)
|
|
155
|
-
return
|
|
156
|
-
}
|
|
157
|
-
|
|
158
163
|
if (state.phase === "fatal") {
|
|
159
164
|
if (errors.is(state.error, ErrMalformedAccessToken)) {
|
|
160
165
|
renderSignInAgain()
|
|
161
166
|
return
|
|
162
167
|
}
|
|
163
|
-
logger.error(
|
|
168
|
+
logger.error({ error: state.error }, "primer fatal state")
|
|
164
169
|
throw state.error
|
|
165
170
|
}
|
|
166
171
|
```
|
|
@@ -170,19 +175,28 @@ if (state.phase === "fatal") {
|
|
|
170
175
|
```ts
|
|
171
176
|
type PrimerOptions<S extends Subject | undefined = undefined, Supported extends readonly PciId[] = []> = {
|
|
172
177
|
readonly publishableKey: string
|
|
173
|
-
readonly
|
|
178
|
+
readonly origin?: string
|
|
174
179
|
readonly subject?: S
|
|
175
180
|
readonly supportedPcis: subject-dependent
|
|
176
181
|
readonly fetch?: typeof globalThis.fetch
|
|
177
182
|
readonly abort?: AbortController
|
|
178
183
|
readonly logger: PrimerLogger
|
|
179
184
|
}
|
|
185
|
+
|
|
186
|
+
type PrimerOptionsWithAccessToken<S, Supported> = PrimerOptions<S, Supported> & {
|
|
187
|
+
readonly accessToken: string
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
type PrimerOptionsWithManagedAuth<S, Supported> = PrimerOptions<S, Supported> & {
|
|
191
|
+
readonly accessToken?: undefined
|
|
192
|
+
}
|
|
180
193
|
```
|
|
181
194
|
|
|
182
195
|
| Field | Required | Meaning |
|
|
183
196
|
| --- | --- | --- |
|
|
184
197
|
| `publishableKey` | Yes | Public key identifying the Primer frontend your runtime belongs to. |
|
|
185
|
-
| `
|
|
198
|
+
| `origin` | No | Primer origin. Defaults to `https://primerlearn.dev`. |
|
|
199
|
+
| `accessToken` | Mode-dependent | Learner access token. When present, `start` uses access-token mode. When absent, `start` uses managed hosted-auth mode. |
|
|
186
200
|
| `subject` | No | Public content scope: `"math"`, `"vocabulary"`, or `"science"`. Omitted means all-subject scope. |
|
|
187
201
|
| `supportedPcis` | Subject-dependent | Renderer capabilities for Portable Custom Interactions. Required when the chosen scope can emit required PCIs. |
|
|
188
202
|
| `fetch` | No | Fetch override for tests, instrumentation, or host runtime integration. |
|
|
@@ -195,18 +209,46 @@ The SDK uses Primer's production runtime by default.
|
|
|
195
209
|
|
|
196
210
|
| Shape | Semantics |
|
|
197
211
|
| --- | --- |
|
|
198
|
-
| `accessToken` present | `start` validates the token shape locally and
|
|
199
|
-
| `accessToken` absent | `start` uses
|
|
212
|
+
| `accessToken` present | `start` validates the token shape locally and returns `AccessTokenStartState`. This mode cannot return sign-in states. |
|
|
213
|
+
| `accessToken` absent | `start` uses managed hosted-auth mode. It reads and writes the SDK-managed browser session token cache documented below. |
|
|
214
|
+
|
|
215
|
+
The public API exposes hosted auth as state behavior. `SignInRequiredState.login()` and `SignInFailedState.login()` are the sign-in transitions. `AuthUnavailableState` and `AuthConfigInvalidState` expose no login operation.
|
|
216
|
+
|
|
217
|
+
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 PrimerOptionsWithManagedAuth<"math", ...>`, `satisfies PrimerOptionsWithAccessToken<"math", ...>`, or the corresponding literal subject variant.
|
|
218
|
+
|
|
219
|
+
## Managed Auth Token Persistence
|
|
220
|
+
|
|
221
|
+
Managed hosted auth is designed for browser-only consumers that do not have their own server-side token broker. When `accessToken` is absent, PrimerTives owns a session-scoped browser token cache with one fixed storage location:
|
|
222
|
+
|
|
223
|
+
```txt
|
|
224
|
+
sessionStorage["primer:access-token:<publishableKey>"]
|
|
225
|
+
```
|
|
200
226
|
|
|
201
|
-
|
|
227
|
+
This is intentionally zero config. There is no storage selector and no persistence flag. Managed-auth mode always uses `globalThis.sessionStorage`; if `sessionStorage` is unavailable, `start` returns `AuthUnavailableState`.
|
|
202
228
|
|
|
203
|
-
|
|
229
|
+
The cache stores only the final Primer access token returned by hosted Timeback sign-in. OAuth transaction state, nonce, verifier, and callback validation state are not stored in browser storage; those remain server-managed by Primer.
|
|
230
|
+
|
|
231
|
+
Managed-auth startup behavior is:
|
|
232
|
+
|
|
233
|
+
| Situation | SDK behavior |
|
|
234
|
+
| --- | --- |
|
|
235
|
+
| Valid cached token exists | `start` uses it and enters the runtime without opening sign-in. |
|
|
236
|
+
| No cached token exists | `start` returns `SignInRequiredState`. |
|
|
237
|
+
| Cached token is malformed or expired | SDK clears the key and returns `SignInRequiredState`. |
|
|
238
|
+
| Hosted sign-in succeeds | SDK validates the returned token, stores it at the documented key, then starts the runtime. |
|
|
239
|
+
| Runtime rejects a managed cached token as expired or invalid | SDK clears the key and returns `SignInFailedState` so the app can ask the learner to sign in again. |
|
|
240
|
+
|
|
241
|
+
Access-token mode bypasses this cache entirely. If `accessToken` is present in `start` options, PrimerTives does not read, write, or clear `sessionStorage`.
|
|
242
|
+
|
|
243
|
+
The cached value is a bearer credential. Browser-only consumers get reload convenience from this default, but any script that can read the page can read the token. Host applications must maintain normal XSS protections and should use access-token mode if they need to own token storage themselves.
|
|
244
|
+
|
|
245
|
+
The canonical managed sign-in endpoint is `/api/auth/timeback/start`. Legacy `/auth/timeback` URLs are not part of the PrimerTives 4.0.5 contract.
|
|
204
246
|
|
|
205
247
|
## Auth Semantics
|
|
206
248
|
|
|
207
249
|
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`.
|
|
208
250
|
|
|
209
|
-
SDK-managed auth may require browser capabilities and learner interaction. Auth failures are
|
|
251
|
+
SDK-managed auth may require browser capabilities and learner interaction. Auth failures are represented by explicit auth states:
|
|
210
252
|
|
|
211
253
|
| Sentinel | Meaning |
|
|
212
254
|
| --- | --- |
|
|
@@ -230,7 +272,7 @@ import {
|
|
|
230
272
|
} from "@superbuilders/primer-tives/errors"
|
|
231
273
|
|
|
232
274
|
let state = await start(options)
|
|
233
|
-
if (state.phase === "
|
|
275
|
+
if (state.phase === "sign-in-failed") {
|
|
234
276
|
if (errors.is(state.error, ErrAuthPopupBlocked)) {
|
|
235
277
|
renderPopupInstructions(state)
|
|
236
278
|
return
|
|
@@ -239,13 +281,23 @@ if (state.phase === "unauthenticated") {
|
|
|
239
281
|
renderTryAgain(state)
|
|
240
282
|
return
|
|
241
283
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
284
|
+
logger.error({ error: state.error }, "primer auth failed")
|
|
285
|
+
renderSignInButton(state)
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (state.phase === "auth-unavailable") {
|
|
290
|
+
renderUnsupportedBrowserMessage(state.error)
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (state.phase === "auth-config-invalid") {
|
|
295
|
+
logger.error({ error: state.error }, "primer auth configuration invalid")
|
|
296
|
+
renderIntegrationError(state.error)
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (state.phase === "sign-in-required") {
|
|
249
301
|
renderSignInButton(state)
|
|
250
302
|
}
|
|
251
303
|
```
|
|
@@ -297,7 +349,7 @@ const options = {
|
|
|
297
349
|
subject: "math",
|
|
298
350
|
supportedPcis: ["urn:primer:pci:fraction-input"],
|
|
299
351
|
logger
|
|
300
|
-
} satisfies
|
|
352
|
+
} satisfies PrimerOptionsWithManagedAuth<"math", readonly ["urn:primer:pci:fraction-input"]>
|
|
301
353
|
|
|
302
354
|
await start(options)
|
|
303
355
|
```
|
|
@@ -317,9 +369,13 @@ async function runPrimer(initialState: PrimerState): Promise<void> {
|
|
|
317
369
|
|
|
318
370
|
while (state.phase !== "completed" && state.phase !== "fatal") {
|
|
319
371
|
switch (state.phase) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
372
|
+
case "sign-in-required":
|
|
373
|
+
case "sign-in-failed":
|
|
374
|
+
renderSignInButton(state)
|
|
375
|
+
return
|
|
376
|
+
case "auth-unavailable":
|
|
377
|
+
renderUnsupportedBrowserMessage(state.error)
|
|
378
|
+
return
|
|
323
379
|
case "observation":
|
|
324
380
|
renderFrame(state.body, state.stimulus)
|
|
325
381
|
state = await state.advance()
|
|
@@ -336,13 +392,13 @@ async function runPrimer(initialState: PrimerState): Promise<void> {
|
|
|
336
392
|
state = await state.retry()
|
|
337
393
|
break
|
|
338
394
|
}
|
|
339
|
-
logger.error(
|
|
395
|
+
logger.error({ error: state.error }, "primer state error")
|
|
340
396
|
throw state.error
|
|
341
397
|
}
|
|
342
398
|
}
|
|
343
399
|
|
|
344
400
|
if (state.phase === "fatal") {
|
|
345
|
-
logger.error(
|
|
401
|
+
logger.error({ error: state.error }, "primer fatal state")
|
|
346
402
|
throw state.error
|
|
347
403
|
}
|
|
348
404
|
}
|
|
@@ -352,7 +408,10 @@ State transitions:
|
|
|
352
408
|
|
|
353
409
|
| Current state | Valid operation | Next result |
|
|
354
410
|
| --- | --- | --- |
|
|
355
|
-
| `
|
|
411
|
+
| `SignInRequiredState` | `login()` | `Promise<ManagedStartState>` |
|
|
412
|
+
| `SignInFailedState` | `login()` | `Promise<ManagedStartState>` |
|
|
413
|
+
| `AuthUnavailableState` | none | terminal for hosted auth in this runtime |
|
|
414
|
+
| `AuthConfigInvalidState` | none | terminal for the current configuration |
|
|
356
415
|
| `ObservationState` | `advance()` | `Promise<PrimerState>` |
|
|
357
416
|
| `ChoiceState` | `submitChoice(selectedKeys)` or `timeout()` | `Promise<PrimerState>` |
|
|
358
417
|
| `TextEntryState` | `submitText(value)` or `timeout()` | `Promise<PrimerState>` |
|
|
@@ -363,42 +422,48 @@ State transitions:
|
|
|
363
422
|
| `PciInteractionState` | `submit(value)` or `timeout()` | `Promise<PrimerState>` |
|
|
364
423
|
| `FeedbackState` | `advance()` | `Promise<PrimerState>` |
|
|
365
424
|
| `CompletedState` | none | terminal |
|
|
366
|
-
| `
|
|
425
|
+
| `RetriableErroredState` | `retry()` | `Promise<PrimerState>` |
|
|
426
|
+
| `NonRetriableErroredState` | none | terminal for the failed intent |
|
|
367
427
|
| `FatalState` | none | terminal |
|
|
368
428
|
|
|
369
429
|
## `PrimerState`
|
|
370
430
|
|
|
371
431
|
```ts
|
|
372
432
|
type PrimerState<Pcis extends PciId = PciId> =
|
|
373
|
-
|
|
|
374
|
-
|
|
|
375
|
-
|
|
|
376
|
-
|
|
|
433
|
+
| SignInRequiredState<Pcis>
|
|
434
|
+
| SignInFailedState<Pcis>
|
|
435
|
+
| AuthUnavailableState
|
|
436
|
+
| AuthConfigInvalidState
|
|
437
|
+
| RuntimeState<Pcis>
|
|
377
438
|
| CompletedState
|
|
378
439
|
| ErroredState<Pcis>
|
|
379
440
|
| FatalState
|
|
441
|
+
|
|
442
|
+
type RuntimeState<Pcis extends PciId = PciId> =
|
|
443
|
+
| ObservationState<Pcis>
|
|
444
|
+
| InteractionState<Pcis>
|
|
445
|
+
| FeedbackState<Pcis>
|
|
380
446
|
```
|
|
381
447
|
|
|
382
448
|
`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`.
|
|
383
449
|
|
|
384
450
|
Start a new state by calling `start` again after a reload, remount, account switch, or subject switch.
|
|
385
451
|
|
|
386
|
-
## `
|
|
452
|
+
## `SignInRequiredState`
|
|
387
453
|
|
|
388
454
|
```ts
|
|
389
|
-
interface
|
|
390
|
-
readonly phase: "
|
|
391
|
-
|
|
392
|
-
login(): Promise<PrimerState<Pcis>>
|
|
455
|
+
interface SignInRequiredState<Pcis extends PciId = PciId> {
|
|
456
|
+
readonly phase: "sign-in-required"
|
|
457
|
+
login(): Promise<ManagedStartState<Pcis>>
|
|
393
458
|
}
|
|
394
459
|
```
|
|
395
460
|
|
|
396
|
-
`
|
|
461
|
+
`SignInRequiredState` means learner sign-in is required before learning content can be rendered. It has no `error` field because no sign-in attempt has failed. Render a sign-in button or equivalent learner action. Call `login()` only from that user action.
|
|
397
462
|
|
|
398
463
|
Correct browser-safe pattern:
|
|
399
464
|
|
|
400
465
|
```ts
|
|
401
|
-
function handleSignInClick(state:
|
|
466
|
+
function handleSignInClick(state: SignInRequiredState): void {
|
|
402
467
|
void state.login().then(function continueAfterLogin(nextState) {
|
|
403
468
|
renderPrimer(nextState)
|
|
404
469
|
})
|
|
@@ -408,7 +473,7 @@ function handleSignInClick(state: UnauthenticatedState): void {
|
|
|
408
473
|
React renderers should use the same direct-call rule:
|
|
409
474
|
|
|
410
475
|
```tsx
|
|
411
|
-
function SignInButton({ state }: { state:
|
|
476
|
+
function SignInButton({ state }: { state: SignInRequiredState }) {
|
|
412
477
|
async function handleClick() {
|
|
413
478
|
const nextState = await state.login()
|
|
414
479
|
renderPrimer(nextState)
|
|
@@ -437,7 +502,41 @@ async function handleClick() {
|
|
|
437
502
|
}
|
|
438
503
|
```
|
|
439
504
|
|
|
440
|
-
If `login()`
|
|
505
|
+
If `login()` fails in a retryable hosted-auth way, it resolves to `SignInFailedState`. If hosted auth cannot run in the current runtime, it resolves to `AuthUnavailableState`. If the public hosted-auth configuration is invalid, it resolves to `AuthConfigInvalidState`.
|
|
506
|
+
|
|
507
|
+
## `SignInFailedState`
|
|
508
|
+
|
|
509
|
+
```ts
|
|
510
|
+
interface SignInFailedState<Pcis extends PciId = PciId> {
|
|
511
|
+
readonly phase: "sign-in-failed"
|
|
512
|
+
readonly error: Error
|
|
513
|
+
login(): Promise<ManagedStartState<Pcis>>
|
|
514
|
+
}
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
`SignInFailedState` means a hosted sign-in attempt failed but another user gesture may retry it. Render the error and bind `login()` to a retry button.
|
|
518
|
+
|
|
519
|
+
## `AuthUnavailableState`
|
|
520
|
+
|
|
521
|
+
```ts
|
|
522
|
+
interface AuthUnavailableState {
|
|
523
|
+
readonly phase: "auth-unavailable"
|
|
524
|
+
readonly error: Error
|
|
525
|
+
}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
`AuthUnavailableState` means the browser capabilities needed for hosted auth are unavailable. It does not expose `login()` because retrying the same operation cannot work in that runtime.
|
|
529
|
+
|
|
530
|
+
## `AuthConfigInvalidState`
|
|
531
|
+
|
|
532
|
+
```ts
|
|
533
|
+
interface AuthConfigInvalidState {
|
|
534
|
+
readonly phase: "auth-config-invalid"
|
|
535
|
+
readonly error: Error
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
`AuthConfigInvalidState` means hosted auth cannot run because the public configuration is invalid. It does not expose `login()` because retrying cannot fix invalid configuration.
|
|
441
540
|
|
|
442
541
|
## Common State Fields
|
|
443
542
|
|
|
@@ -730,17 +829,27 @@ Terminal state. There is no transition method.
|
|
|
730
829
|
## `ErroredState`
|
|
731
830
|
|
|
732
831
|
```ts
|
|
733
|
-
|
|
832
|
+
type ErroredState<Pcis extends PciId = PciId> =
|
|
833
|
+
| RetriableErroredState<Pcis>
|
|
834
|
+
| NonRetriableErroredState
|
|
835
|
+
|
|
836
|
+
interface RetriableErroredState<Pcis extends PciId = PciId> {
|
|
734
837
|
readonly phase: "errored"
|
|
735
838
|
readonly error: Error
|
|
736
|
-
readonly retriable:
|
|
839
|
+
readonly retriable: true
|
|
737
840
|
retry(): Promise<PrimerState<Pcis>>
|
|
738
841
|
}
|
|
842
|
+
|
|
843
|
+
interface NonRetriableErroredState {
|
|
844
|
+
readonly phase: "errored"
|
|
845
|
+
readonly error: Error
|
|
846
|
+
readonly retriable: false
|
|
847
|
+
}
|
|
739
848
|
```
|
|
740
849
|
|
|
741
850
|
`ErroredState` means the current learner intent could not complete, but the learning session itself is not necessarily terminal.
|
|
742
851
|
|
|
743
|
-
If `retriable` is `true`, `retry()` repeats the exact failed intent. If `retriable` is `false`,
|
|
852
|
+
If `retriable` is `true`, `retry()` repeats the exact failed intent. If `retriable` is `false`, the state does not expose `retry()`.
|
|
744
853
|
|
|
745
854
|
`ErrInvalidSubmission` is non-retriable because the submitted value was invalid for the active interaction. Renderer code should fix the payload before submitting again from a fresh interaction state.
|
|
746
855
|
|
|
@@ -922,7 +1031,7 @@ import { RendererSubmissionSchema } from "@superbuilders/primer-tives/contracts"
|
|
|
922
1031
|
|
|
923
1032
|
const parsed = RendererSubmissionSchema.parse(payload)
|
|
924
1033
|
if (!parsed.success) {
|
|
925
|
-
logger.error(
|
|
1034
|
+
logger.error({ error: parsed.error }, "submission payload invalid")
|
|
926
1035
|
throw errors.wrap(parsed.error, "submission payload")
|
|
927
1036
|
}
|
|
928
1037
|
|
|
@@ -976,7 +1085,7 @@ import { ErrInvalidSubmission } from "@superbuilders/primer-tives/errors"
|
|
|
976
1085
|
const validation = validateSubmissionForInteraction(interaction, submission)
|
|
977
1086
|
if (!validation.ok) {
|
|
978
1087
|
const message = submissionValidationMessage(validation)
|
|
979
|
-
logger.error(
|
|
1088
|
+
logger.error({ issues: validation.issues }, "submission invalid")
|
|
980
1089
|
throw errors.wrap(ErrInvalidSubmission, message)
|
|
981
1090
|
}
|
|
982
1091
|
```
|
|
@@ -1200,23 +1309,31 @@ Handle auth-needed state before rendering learning content:
|
|
|
1200
1309
|
```ts
|
|
1201
1310
|
let state = await start(options)
|
|
1202
1311
|
|
|
1203
|
-
if (state.phase === "
|
|
1312
|
+
if (state.phase === "sign-in-required") {
|
|
1313
|
+
renderSignInButton(state)
|
|
1314
|
+
return
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
if (state.phase === "sign-in-failed") {
|
|
1204
1318
|
if (errors.is(state.error, ErrAuthCancelled)) {
|
|
1205
1319
|
renderTryAgain(state)
|
|
1206
1320
|
return
|
|
1207
1321
|
}
|
|
1208
|
-
|
|
1209
|
-
logger.error("primer auth needed", { error: state.error })
|
|
1210
|
-
}
|
|
1322
|
+
logger.error({ error: state.error }, "primer auth failed")
|
|
1211
1323
|
renderSignInButton(state)
|
|
1212
1324
|
return
|
|
1213
1325
|
}
|
|
1326
|
+
|
|
1327
|
+
if (state.phase === "auth-unavailable") {
|
|
1328
|
+
renderUnsupportedBrowserMessage(state.error)
|
|
1329
|
+
return
|
|
1330
|
+
}
|
|
1214
1331
|
```
|
|
1215
1332
|
|
|
1216
1333
|
Bind login directly to the sign-in button:
|
|
1217
1334
|
|
|
1218
1335
|
```ts
|
|
1219
|
-
function handleSignInClick(state:
|
|
1336
|
+
function handleSignInClick(state: SignInRequiredState | SignInFailedState): void {
|
|
1220
1337
|
void state.login().then(function continueAfterLogin(nextState) {
|
|
1221
1338
|
renderPrimer(nextState)
|
|
1222
1339
|
})
|
|
@@ -1231,7 +1348,7 @@ if (state.phase === "errored") {
|
|
|
1231
1348
|
state = await state.retry()
|
|
1232
1349
|
return
|
|
1233
1350
|
}
|
|
1234
|
-
logger.error(
|
|
1351
|
+
logger.error({ error: state.error }, "primer non-retriable state")
|
|
1235
1352
|
throw state.error
|
|
1236
1353
|
}
|
|
1237
1354
|
|
|
@@ -1244,7 +1361,7 @@ if (state.phase === "fatal") {
|
|
|
1244
1361
|
renderSdkUpgradeMessage()
|
|
1245
1362
|
return
|
|
1246
1363
|
}
|
|
1247
|
-
logger.error(
|
|
1364
|
+
logger.error({ error: state.error }, "primer fatal state")
|
|
1248
1365
|
throw state.error
|
|
1249
1366
|
}
|
|
1250
1367
|
```
|
|
@@ -1267,25 +1384,20 @@ state = next
|
|
|
1267
1384
|
```ts
|
|
1268
1385
|
import type { PrimerLogger } from "@superbuilders/primer-tives/logger"
|
|
1269
1386
|
|
|
1270
|
-
|
|
1271
|
-
debug(message: string, attributes?: Record<string, unknown>): void
|
|
1272
|
-
info(message: string, attributes?: Record<string, unknown>): void
|
|
1273
|
-
warn(message: string, attributes?: Record<string, unknown>): void
|
|
1274
|
-
error(message: string, attributes?: Record<string, unknown>): void
|
|
1275
|
-
}
|
|
1387
|
+
type PrimerLogger = import("pino").Logger
|
|
1276
1388
|
```
|
|
1277
1389
|
|
|
1278
|
-
Pino
|
|
1390
|
+
Use a Pino-compatible logger. Pino calls are object-first when attributes are present.
|
|
1279
1391
|
|
|
1280
1392
|
```ts
|
|
1281
1393
|
import { logger } from "@/logger"
|
|
1282
|
-
import { start, type
|
|
1394
|
+
import { start, type PrimerOptionsWithManagedAuth } from "@superbuilders/primer-tives/client"
|
|
1283
1395
|
|
|
1284
1396
|
const options = {
|
|
1285
1397
|
publishableKey,
|
|
1286
1398
|
subject: "vocabulary",
|
|
1287
1399
|
logger
|
|
1288
|
-
} satisfies
|
|
1400
|
+
} satisfies PrimerOptionsWithManagedAuth<"vocabulary">
|
|
1289
1401
|
|
|
1290
1402
|
const state = await start(options)
|
|
1291
1403
|
```
|
|
@@ -1315,8 +1427,10 @@ The runtime exchange shape is not public SDK surface. Tests should assert SDK se
|
|
|
1315
1427
|
|
|
1316
1428
|
| Scenario | Assert |
|
|
1317
1429
|
| --- | --- |
|
|
1318
|
-
| auth is needed | `start` resolves to `
|
|
1319
|
-
| auth login fails | `login()` resolves to `
|
|
1430
|
+
| auth is needed | managed-auth `start` resolves to `SignInRequiredState` |
|
|
1431
|
+
| auth login fails | `login()` resolves to `SignInFailedState` with the expected auth sentinel |
|
|
1432
|
+
| auth cannot run | `login()` resolves to `AuthUnavailableState` |
|
|
1433
|
+
| auth config is invalid | `login()` resolves to `AuthConfigInvalidState` |
|
|
1320
1434
|
| first runtime work fails recoverably | `start` resolves to `ErroredState` with `retriable: true` |
|
|
1321
1435
|
| first runtime work fails terminally | `start` resolves to `FatalState` |
|
|
1322
1436
|
| unsupported PCI is presented | `start` resolves to `FatalState` with `ErrUnsupportedPci` |
|
|
@@ -1328,7 +1442,7 @@ Example test shape:
|
|
|
1328
1442
|
|
|
1329
1443
|
```ts
|
|
1330
1444
|
import * as errors from "@superbuilders/errors"
|
|
1331
|
-
import { start, type
|
|
1445
|
+
import { start, type PrimerOptionsWithAccessToken } from "@superbuilders/primer-tives/client"
|
|
1332
1446
|
import { ErrUnsupportedPci } from "@superbuilders/primer-tives/errors"
|
|
1333
1447
|
|
|
1334
1448
|
declare const fetchMock: typeof globalThis.fetch
|
|
@@ -1339,7 +1453,7 @@ const options = {
|
|
|
1339
1453
|
subject: "vocabulary",
|
|
1340
1454
|
fetch: fetchMock,
|
|
1341
1455
|
logger
|
|
1342
|
-
} satisfies
|
|
1456
|
+
} satisfies PrimerOptionsWithAccessToken<"vocabulary">
|
|
1343
1457
|
|
|
1344
1458
|
const state = await start(options)
|
|
1345
1459
|
|
|
@@ -1379,16 +1493,16 @@ grade level
|
|
|
1379
1493
|
|
|
1380
1494
|
1. Import `start` and `PrimerOptions` from `@superbuilders/primer-tives/client`.
|
|
1381
1495
|
2. Import shared renderer contracts from `@superbuilders/primer-tives/contracts`.
|
|
1382
|
-
3. Define options with `satisfies
|
|
1496
|
+
3. Define options with `satisfies PrimerOptionsWithManagedAuth<...>` or `satisfies PrimerOptionsWithAccessToken<...>` so subject and PCI requirements stay visible at the declaration site.
|
|
1383
1497
|
4. Pass `publishableKey` and `logger` to `start`.
|
|
1384
|
-
5. Either pass `accessToken` or handle
|
|
1498
|
+
5. Either pass `accessToken` or handle managed-auth states.
|
|
1385
1499
|
6. Choose a public `subject`, or omit `subject` for all-subject runtime scope.
|
|
1386
1500
|
7. Declare every required renderer PCI in `supportedPcis`.
|
|
1387
1501
|
8. Await `start` and render by switching on `state.phase`.
|
|
1388
|
-
9. If `state.phase === "
|
|
1502
|
+
9. If `state.phase === "sign-in-required"` or `"sign-in-failed"`, render sign-in UI and bind `state.login()` directly to the click or tap handler.
|
|
1389
1503
|
10. For interaction states, render by switching on `state.kind`.
|
|
1390
1504
|
11. Use only the transition methods exposed by the current state.
|
|
1391
|
-
12. Handle `ErroredState` through `retriable
|
|
1505
|
+
12. Handle `ErroredState` through `retriable`; call `retry()` only when `retriable` is `true`.
|
|
1392
1506
|
13. Handle `FatalState` as terminal for the current state object.
|
|
1393
1507
|
14. Never serialize `PrimerState`.
|
|
1394
1508
|
15. Start a new state with `start` after reload, remount, account switch, or subject switch.
|
|
@@ -1401,9 +1515,9 @@ The current SDK intentionally does not expose:
|
|
|
1401
1515
|
| --- | --- |
|
|
1402
1516
|
| package-root exports | explicit public subpaths |
|
|
1403
1517
|
| backend-only SDK surface | browser/client semantic SDK only |
|
|
1404
|
-
|
|
|
1405
|
-
|
|
|
1406
|
-
| client wrapper object | `start` returning
|
|
1518
|
+
| separate auth API object | hosted-auth state variants with `login()` only on retryable sign-in states |
|
|
1519
|
+
| hosted-auth popup configuration | fixed popup defaults and current page redirect URI |
|
|
1520
|
+
| client wrapper object | `start` overloads returning live state |
|
|
1407
1521
|
| `snapshot()` | live `PrimerState` only |
|
|
1408
1522
|
| serializable state | start a new state with `start` |
|
|
1409
1523
|
| public `"all"` subject value | omit `subject` |
|
|
@@ -1414,10 +1528,13 @@ The current SDK intentionally does not expose:
|
|
|
1414
1528
|
|
|
1415
1529
|
```txt
|
|
1416
1530
|
start is the public lifecycle entrypoint
|
|
1417
|
-
start returns
|
|
1531
|
+
start returns AccessTokenStartState or ManagedStartState based on accessToken presence
|
|
1418
1532
|
accessToken present is used for learner runtime auth
|
|
1419
|
-
accessToken
|
|
1420
|
-
|
|
1533
|
+
accessToken present cannot produce hosted-auth states
|
|
1534
|
+
accessToken absent may produce SignInRequiredState
|
|
1535
|
+
SignInRequiredState.login and SignInFailedState.login are hosted-auth user-gesture transitions
|
|
1536
|
+
AuthUnavailableState has no login operation
|
|
1537
|
+
AuthConfigInvalidState has no login operation
|
|
1421
1538
|
subject is optional; omitted means all-subject runtime scope
|
|
1422
1539
|
subject determines required renderer PCI capabilities
|
|
1423
1540
|
supportedPcis declares renderer PCI capabilities
|