@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 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 optional auth transition:
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<PrimerState>
9
- UnauthenticatedState.login() -> Promise<PrimerState>
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` 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.
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.3`.
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 { start, type PrimerOptions } from "@superbuilders/primer-tives/client"
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 PrimerOptions<"math", readonly ["urn:primer:pci:fraction-input"]>
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 PrimerOptions<"math", readonly ["urn:primer:pci:fraction-input"]>
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 `UnauthenticatedState`, render sign-in UI and call `login()` directly from a user gesture:
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 { PrimerState, UnauthenticatedState } from "@superbuilders/primer-tives/client"
77
+ import type { ManagedStartState, SignInRequiredState } from "@superbuilders/primer-tives/client"
72
78
 
73
- let state: PrimerState = await start(options)
79
+ let state: ManagedStartState = await start(options)
74
80
 
75
- if (state.phase === "unauthenticated") {
81
+ if (state.phase === "sign-in-required") {
76
82
  renderSignInButton(state)
77
83
  }
78
84
 
79
- function handleSignInClick(authState: UnauthenticatedState): void {
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 PrimerOptions<"vocabulary">
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 PrimerOptions<undefined, readonly ["urn:primer:pci:fraction-input"]>
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: PrimerOptions<S, Supported>): Promise<PrimerState>
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. It may return any current state the renderer must handle:
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
- | `UnauthenticatedState` | Learner sign-in is needed before runtime learning can begin. |
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 PrimerOptions } from "@superbuilders/primer-tives/client"
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 PrimerOptions<"math", readonly ["urn:primer:pci:fraction-input"]>
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("primer fatal state", { error: state.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 accessToken?: string
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
- | `accessToken` | No | Learner access token. When present, `start` uses it for the learning runtime. |
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 uses it for learning runtime state. |
199
- | `accessToken` absent | `start` uses existing browser auth state when available, or returns `UnauthenticatedState` when learner sign-in is needed. |
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
- The public API exposes auth as state behavior. `UnauthenticatedState.login()` is the sign-in transition.
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
- 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", ...>`.
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 exposed through `UnauthenticatedState.error`:
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 === "unauthenticated") {
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
- if (errors.is(state.error, ErrAuthUnavailable)) {
243
- renderUnsupportedBrowserMessage()
244
- return
245
- }
246
- if (state.error !== null) {
247
- logger.error("primer auth failed", { error: state.error })
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 PrimerOptions<"math", readonly ["urn:primer:pci:fraction-input"]>
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
- case "unauthenticated":
321
- renderSignInButton(state)
322
- return
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("primer state error", { error: state.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("primer fatal state", { error: state.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
- | `UnauthenticatedState` | `login()` | `Promise<PrimerState>` |
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
- | `ErroredState` | `retry()` when `retriable` is `true` | `Promise<PrimerState>` |
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
- | UnauthenticatedState<Pcis>
374
- | ObservationState<Pcis>
375
- | InteractionState<Pcis>
376
- | FeedbackState<Pcis>
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
- ## `UnauthenticatedState`
452
+ ## `SignInRequiredState`
387
453
 
388
454
  ```ts
389
- interface UnauthenticatedState<Pcis extends PciId = PciId> {
390
- readonly phase: "unauthenticated"
391
- readonly error: Error | null
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
- `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.
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: UnauthenticatedState): void {
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: UnauthenticatedState }) {
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()` cannot complete auth, it resolves to another `UnauthenticatedState` with `error` set to the auth sentinel. Render retry or unsupported-browser UI from that state.
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
- interface ErroredState<Pcis extends PciId = PciId> {
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: boolean
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`, `retry()` resolves to the same errored state.
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("submission payload invalid", { error: parsed.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("submission invalid", { issues: validation.issues })
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 === "unauthenticated") {
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
- if (state.error !== null) {
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: UnauthenticatedState): void {
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("primer non-retriable state", { error: state.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("primer fatal state", { error: state.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
- interface PrimerLogger {
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 through the central `@/logger` module matches this interface directly.
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 PrimerOptions } from "@superbuilders/primer-tives/client"
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 PrimerOptions<"vocabulary">
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 `UnauthenticatedState` |
1319
- | auth login fails | `login()` resolves to `UnauthenticatedState` with the expected auth sentinel |
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 PrimerOptions } from "@superbuilders/primer-tives/client"
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 PrimerOptions<"vocabulary">
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 PrimerOptions<...>` so subject and PCI requirements stay visible at the declaration site.
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 `UnauthenticatedState`.
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 === "unauthenticated"`, render sign-in UI and bind `state.login()` directly to the click or tap handler.
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` and `retry()`.
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
- | public auth result union | `UnauthenticatedState` |
1405
- | separate auth API object | `UnauthenticatedState.login()` |
1406
- | client wrapper object | `start` returning `Promise<PrimerState>` |
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 Promise<PrimerState>
1531
+ start returns AccessTokenStartState or ManagedStartState based on accessToken presence
1418
1532
  accessToken present is used for learner runtime auth
1419
- accessToken absent may produce UnauthenticatedState
1420
- UnauthenticatedState.login is the hosted-auth user-gesture transition
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