@superbuilders/primer-tives 4.0.2 → 4.0.4

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.2`.
22
+ The current SDK version is `4.0.4`.
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,18 @@ 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` returns managed hosted-auth state or runtime state as `ManagedStartState`. This mode does not read browser storage and does not persist tokens. |
200
214
 
201
- The public API exposes auth as state behavior. `UnauthenticatedState.login()` is the sign-in transition.
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.
202
216
 
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", ...>`.
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.
204
218
 
205
219
  ## Auth Semantics
206
220
 
207
221
  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
222
 
209
- SDK-managed auth may require browser capabilities and learner interaction. Auth failures are exposed through `UnauthenticatedState.error`:
223
+ SDK-managed auth may require browser capabilities and learner interaction. Auth failures are represented by explicit auth states:
210
224
 
211
225
  | Sentinel | Meaning |
212
226
  | --- | --- |
@@ -230,7 +244,7 @@ import {
230
244
  } from "@superbuilders/primer-tives/errors"
231
245
 
232
246
  let state = await start(options)
233
- if (state.phase === "unauthenticated") {
247
+ if (state.phase === "sign-in-failed") {
234
248
  if (errors.is(state.error, ErrAuthPopupBlocked)) {
235
249
  renderPopupInstructions(state)
236
250
  return
@@ -239,13 +253,23 @@ if (state.phase === "unauthenticated") {
239
253
  renderTryAgain(state)
240
254
  return
241
255
  }
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
- }
256
+ logger.error({ error: state.error }, "primer auth failed")
257
+ renderSignInButton(state)
258
+ return
259
+ }
260
+
261
+ if (state.phase === "auth-unavailable") {
262
+ renderUnsupportedBrowserMessage(state.error)
263
+ return
264
+ }
265
+
266
+ if (state.phase === "auth-config-invalid") {
267
+ logger.error({ error: state.error }, "primer auth configuration invalid")
268
+ renderIntegrationError(state.error)
269
+ return
270
+ }
271
+
272
+ if (state.phase === "sign-in-required") {
249
273
  renderSignInButton(state)
250
274
  }
251
275
  ```
@@ -297,7 +321,7 @@ const options = {
297
321
  subject: "math",
298
322
  supportedPcis: ["urn:primer:pci:fraction-input"],
299
323
  logger
300
- } satisfies PrimerOptions<"math", readonly ["urn:primer:pci:fraction-input"]>
324
+ } satisfies PrimerOptionsWithManagedAuth<"math", readonly ["urn:primer:pci:fraction-input"]>
301
325
 
302
326
  await start(options)
303
327
  ```
@@ -317,9 +341,13 @@ async function runPrimer(initialState: PrimerState): Promise<void> {
317
341
 
318
342
  while (state.phase !== "completed" && state.phase !== "fatal") {
319
343
  switch (state.phase) {
320
- case "unauthenticated":
321
- renderSignInButton(state)
322
- return
344
+ case "sign-in-required":
345
+ case "sign-in-failed":
346
+ renderSignInButton(state)
347
+ return
348
+ case "auth-unavailable":
349
+ renderUnsupportedBrowserMessage(state.error)
350
+ return
323
351
  case "observation":
324
352
  renderFrame(state.body, state.stimulus)
325
353
  state = await state.advance()
@@ -336,13 +364,13 @@ async function runPrimer(initialState: PrimerState): Promise<void> {
336
364
  state = await state.retry()
337
365
  break
338
366
  }
339
- logger.error("primer state error", { error: state.error })
367
+ logger.error({ error: state.error }, "primer state error")
340
368
  throw state.error
341
369
  }
342
370
  }
343
371
 
344
372
  if (state.phase === "fatal") {
345
- logger.error("primer fatal state", { error: state.error })
373
+ logger.error({ error: state.error }, "primer fatal state")
346
374
  throw state.error
347
375
  }
348
376
  }
@@ -352,7 +380,10 @@ State transitions:
352
380
 
353
381
  | Current state | Valid operation | Next result |
354
382
  | --- | --- | --- |
355
- | `UnauthenticatedState` | `login()` | `Promise<PrimerState>` |
383
+ | `SignInRequiredState` | `login()` | `Promise<ManagedStartState>` |
384
+ | `SignInFailedState` | `login()` | `Promise<ManagedStartState>` |
385
+ | `AuthUnavailableState` | none | terminal for hosted auth in this runtime |
386
+ | `AuthConfigInvalidState` | none | terminal for the current configuration |
356
387
  | `ObservationState` | `advance()` | `Promise<PrimerState>` |
357
388
  | `ChoiceState` | `submitChoice(selectedKeys)` or `timeout()` | `Promise<PrimerState>` |
358
389
  | `TextEntryState` | `submitText(value)` or `timeout()` | `Promise<PrimerState>` |
@@ -363,42 +394,48 @@ State transitions:
363
394
  | `PciInteractionState` | `submit(value)` or `timeout()` | `Promise<PrimerState>` |
364
395
  | `FeedbackState` | `advance()` | `Promise<PrimerState>` |
365
396
  | `CompletedState` | none | terminal |
366
- | `ErroredState` | `retry()` when `retriable` is `true` | `Promise<PrimerState>` |
397
+ | `RetriableErroredState` | `retry()` | `Promise<PrimerState>` |
398
+ | `NonRetriableErroredState` | none | terminal for the failed intent |
367
399
  | `FatalState` | none | terminal |
368
400
 
369
401
  ## `PrimerState`
370
402
 
371
403
  ```ts
372
404
  type PrimerState<Pcis extends PciId = PciId> =
373
- | UnauthenticatedState<Pcis>
374
- | ObservationState<Pcis>
375
- | InteractionState<Pcis>
376
- | FeedbackState<Pcis>
405
+ | SignInRequiredState<Pcis>
406
+ | SignInFailedState<Pcis>
407
+ | AuthUnavailableState
408
+ | AuthConfigInvalidState
409
+ | RuntimeState<Pcis>
377
410
  | CompletedState
378
411
  | ErroredState<Pcis>
379
412
  | FatalState
413
+
414
+ type RuntimeState<Pcis extends PciId = PciId> =
415
+ | ObservationState<Pcis>
416
+ | InteractionState<Pcis>
417
+ | FeedbackState<Pcis>
380
418
  ```
381
419
 
382
420
  `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
421
 
384
422
  Start a new state by calling `start` again after a reload, remount, account switch, or subject switch.
385
423
 
386
- ## `UnauthenticatedState`
424
+ ## `SignInRequiredState`
387
425
 
388
426
  ```ts
389
- interface UnauthenticatedState<Pcis extends PciId = PciId> {
390
- readonly phase: "unauthenticated"
391
- readonly error: Error | null
392
- login(): Promise<PrimerState<Pcis>>
427
+ interface SignInRequiredState<Pcis extends PciId = PciId> {
428
+ readonly phase: "sign-in-required"
429
+ login(): Promise<ManagedStartState<Pcis>>
393
430
  }
394
431
  ```
395
432
 
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.
433
+ `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
434
 
398
435
  Correct browser-safe pattern:
399
436
 
400
437
  ```ts
401
- function handleSignInClick(state: UnauthenticatedState): void {
438
+ function handleSignInClick(state: SignInRequiredState): void {
402
439
  void state.login().then(function continueAfterLogin(nextState) {
403
440
  renderPrimer(nextState)
404
441
  })
@@ -408,7 +445,7 @@ function handleSignInClick(state: UnauthenticatedState): void {
408
445
  React renderers should use the same direct-call rule:
409
446
 
410
447
  ```tsx
411
- function SignInButton({ state }: { state: UnauthenticatedState }) {
448
+ function SignInButton({ state }: { state: SignInRequiredState }) {
412
449
  async function handleClick() {
413
450
  const nextState = await state.login()
414
451
  renderPrimer(nextState)
@@ -437,7 +474,41 @@ async function handleClick() {
437
474
  }
438
475
  ```
439
476
 
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.
477
+ 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`.
478
+
479
+ ## `SignInFailedState`
480
+
481
+ ```ts
482
+ interface SignInFailedState<Pcis extends PciId = PciId> {
483
+ readonly phase: "sign-in-failed"
484
+ readonly error: Error
485
+ login(): Promise<ManagedStartState<Pcis>>
486
+ }
487
+ ```
488
+
489
+ `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.
490
+
491
+ ## `AuthUnavailableState`
492
+
493
+ ```ts
494
+ interface AuthUnavailableState {
495
+ readonly phase: "auth-unavailable"
496
+ readonly error: Error
497
+ }
498
+ ```
499
+
500
+ `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.
501
+
502
+ ## `AuthConfigInvalidState`
503
+
504
+ ```ts
505
+ interface AuthConfigInvalidState {
506
+ readonly phase: "auth-config-invalid"
507
+ readonly error: Error
508
+ }
509
+ ```
510
+
511
+ `AuthConfigInvalidState` means hosted auth cannot run because the public configuration is invalid. It does not expose `login()` because retrying cannot fix invalid configuration.
441
512
 
442
513
  ## Common State Fields
443
514
 
@@ -730,17 +801,27 @@ Terminal state. There is no transition method.
730
801
  ## `ErroredState`
731
802
 
732
803
  ```ts
733
- interface ErroredState<Pcis extends PciId = PciId> {
804
+ type ErroredState<Pcis extends PciId = PciId> =
805
+ | RetriableErroredState<Pcis>
806
+ | NonRetriableErroredState
807
+
808
+ interface RetriableErroredState<Pcis extends PciId = PciId> {
734
809
  readonly phase: "errored"
735
810
  readonly error: Error
736
- readonly retriable: boolean
811
+ readonly retriable: true
737
812
  retry(): Promise<PrimerState<Pcis>>
738
813
  }
814
+
815
+ interface NonRetriableErroredState {
816
+ readonly phase: "errored"
817
+ readonly error: Error
818
+ readonly retriable: false
819
+ }
739
820
  ```
740
821
 
741
822
  `ErroredState` means the current learner intent could not complete, but the learning session itself is not necessarily terminal.
742
823
 
743
- If `retriable` is `true`, `retry()` repeats the exact failed intent. If `retriable` is `false`, `retry()` resolves to the same errored state.
824
+ If `retriable` is `true`, `retry()` repeats the exact failed intent. If `retriable` is `false`, the state does not expose `retry()`.
744
825
 
745
826
  `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
827
 
@@ -922,7 +1003,7 @@ import { RendererSubmissionSchema } from "@superbuilders/primer-tives/contracts"
922
1003
 
923
1004
  const parsed = RendererSubmissionSchema.parse(payload)
924
1005
  if (!parsed.success) {
925
- logger.error("submission payload invalid", { error: parsed.error })
1006
+ logger.error({ error: parsed.error }, "submission payload invalid")
926
1007
  throw errors.wrap(parsed.error, "submission payload")
927
1008
  }
928
1009
 
@@ -976,7 +1057,7 @@ import { ErrInvalidSubmission } from "@superbuilders/primer-tives/errors"
976
1057
  const validation = validateSubmissionForInteraction(interaction, submission)
977
1058
  if (!validation.ok) {
978
1059
  const message = submissionValidationMessage(validation)
979
- logger.error("submission invalid", { issues: validation.issues })
1060
+ logger.error({ issues: validation.issues }, "submission invalid")
980
1061
  throw errors.wrap(ErrInvalidSubmission, message)
981
1062
  }
982
1063
  ```
@@ -1200,23 +1281,31 @@ Handle auth-needed state before rendering learning content:
1200
1281
  ```ts
1201
1282
  let state = await start(options)
1202
1283
 
1203
- if (state.phase === "unauthenticated") {
1284
+ if (state.phase === "sign-in-required") {
1285
+ renderSignInButton(state)
1286
+ return
1287
+ }
1288
+
1289
+ if (state.phase === "sign-in-failed") {
1204
1290
  if (errors.is(state.error, ErrAuthCancelled)) {
1205
1291
  renderTryAgain(state)
1206
1292
  return
1207
1293
  }
1208
- if (state.error !== null) {
1209
- logger.error("primer auth needed", { error: state.error })
1210
- }
1294
+ logger.error({ error: state.error }, "primer auth failed")
1211
1295
  renderSignInButton(state)
1212
1296
  return
1213
1297
  }
1298
+
1299
+ if (state.phase === "auth-unavailable") {
1300
+ renderUnsupportedBrowserMessage(state.error)
1301
+ return
1302
+ }
1214
1303
  ```
1215
1304
 
1216
1305
  Bind login directly to the sign-in button:
1217
1306
 
1218
1307
  ```ts
1219
- function handleSignInClick(state: UnauthenticatedState): void {
1308
+ function handleSignInClick(state: SignInRequiredState | SignInFailedState): void {
1220
1309
  void state.login().then(function continueAfterLogin(nextState) {
1221
1310
  renderPrimer(nextState)
1222
1311
  })
@@ -1231,7 +1320,7 @@ if (state.phase === "errored") {
1231
1320
  state = await state.retry()
1232
1321
  return
1233
1322
  }
1234
- logger.error("primer non-retriable state", { error: state.error })
1323
+ logger.error({ error: state.error }, "primer non-retriable state")
1235
1324
  throw state.error
1236
1325
  }
1237
1326
 
@@ -1244,7 +1333,7 @@ if (state.phase === "fatal") {
1244
1333
  renderSdkUpgradeMessage()
1245
1334
  return
1246
1335
  }
1247
- logger.error("primer fatal state", { error: state.error })
1336
+ logger.error({ error: state.error }, "primer fatal state")
1248
1337
  throw state.error
1249
1338
  }
1250
1339
  ```
@@ -1267,25 +1356,20 @@ state = next
1267
1356
  ```ts
1268
1357
  import type { PrimerLogger } from "@superbuilders/primer-tives/logger"
1269
1358
 
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
- }
1359
+ type PrimerLogger = import("pino").Logger
1276
1360
  ```
1277
1361
 
1278
- Pino through the central `@/logger` module matches this interface directly.
1362
+ Use a Pino-compatible logger. Pino calls are object-first when attributes are present.
1279
1363
 
1280
1364
  ```ts
1281
1365
  import { logger } from "@/logger"
1282
- import { start, type PrimerOptions } from "@superbuilders/primer-tives/client"
1366
+ import { start, type PrimerOptionsWithManagedAuth } from "@superbuilders/primer-tives/client"
1283
1367
 
1284
1368
  const options = {
1285
1369
  publishableKey,
1286
1370
  subject: "vocabulary",
1287
1371
  logger
1288
- } satisfies PrimerOptions<"vocabulary">
1372
+ } satisfies PrimerOptionsWithManagedAuth<"vocabulary">
1289
1373
 
1290
1374
  const state = await start(options)
1291
1375
  ```
@@ -1315,8 +1399,10 @@ The runtime exchange shape is not public SDK surface. Tests should assert SDK se
1315
1399
 
1316
1400
  | Scenario | Assert |
1317
1401
  | --- | --- |
1318
- | auth is needed | `start` resolves to `UnauthenticatedState` |
1319
- | auth login fails | `login()` resolves to `UnauthenticatedState` with the expected auth sentinel |
1402
+ | auth is needed | managed-auth `start` resolves to `SignInRequiredState` |
1403
+ | auth login fails | `login()` resolves to `SignInFailedState` with the expected auth sentinel |
1404
+ | auth cannot run | `login()` resolves to `AuthUnavailableState` |
1405
+ | auth config is invalid | `login()` resolves to `AuthConfigInvalidState` |
1320
1406
  | first runtime work fails recoverably | `start` resolves to `ErroredState` with `retriable: true` |
1321
1407
  | first runtime work fails terminally | `start` resolves to `FatalState` |
1322
1408
  | unsupported PCI is presented | `start` resolves to `FatalState` with `ErrUnsupportedPci` |
@@ -1328,7 +1414,7 @@ Example test shape:
1328
1414
 
1329
1415
  ```ts
1330
1416
  import * as errors from "@superbuilders/errors"
1331
- import { start, type PrimerOptions } from "@superbuilders/primer-tives/client"
1417
+ import { start, type PrimerOptionsWithAccessToken } from "@superbuilders/primer-tives/client"
1332
1418
  import { ErrUnsupportedPci } from "@superbuilders/primer-tives/errors"
1333
1419
 
1334
1420
  declare const fetchMock: typeof globalThis.fetch
@@ -1339,7 +1425,7 @@ const options = {
1339
1425
  subject: "vocabulary",
1340
1426
  fetch: fetchMock,
1341
1427
  logger
1342
- } satisfies PrimerOptions<"vocabulary">
1428
+ } satisfies PrimerOptionsWithAccessToken<"vocabulary">
1343
1429
 
1344
1430
  const state = await start(options)
1345
1431
 
@@ -1379,16 +1465,16 @@ grade level
1379
1465
 
1380
1466
  1. Import `start` and `PrimerOptions` from `@superbuilders/primer-tives/client`.
1381
1467
  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.
1468
+ 3. Define options with `satisfies PrimerOptionsWithManagedAuth<...>` or `satisfies PrimerOptionsWithAccessToken<...>` so subject and PCI requirements stay visible at the declaration site.
1383
1469
  4. Pass `publishableKey` and `logger` to `start`.
1384
- 5. Either pass `accessToken` or handle `UnauthenticatedState`.
1470
+ 5. Either pass `accessToken` or handle managed-auth states.
1385
1471
  6. Choose a public `subject`, or omit `subject` for all-subject runtime scope.
1386
1472
  7. Declare every required renderer PCI in `supportedPcis`.
1387
1473
  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.
1474
+ 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
1475
  10. For interaction states, render by switching on `state.kind`.
1390
1476
  11. Use only the transition methods exposed by the current state.
1391
- 12. Handle `ErroredState` through `retriable` and `retry()`.
1477
+ 12. Handle `ErroredState` through `retriable`; call `retry()` only when `retriable` is `true`.
1392
1478
  13. Handle `FatalState` as terminal for the current state object.
1393
1479
  14. Never serialize `PrimerState`.
1394
1480
  15. Start a new state with `start` after reload, remount, account switch, or subject switch.
@@ -1401,9 +1487,9 @@ The current SDK intentionally does not expose:
1401
1487
  | --- | --- |
1402
1488
  | package-root exports | explicit public subpaths |
1403
1489
  | 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>` |
1490
+ | separate auth API object | hosted-auth state variants with `login()` only on retryable sign-in states |
1491
+ | hosted-auth popup configuration | fixed popup defaults and current page redirect URI |
1492
+ | client wrapper object | `start` overloads returning live state |
1407
1493
  | `snapshot()` | live `PrimerState` only |
1408
1494
  | serializable state | start a new state with `start` |
1409
1495
  | public `"all"` subject value | omit `subject` |
@@ -1414,10 +1500,13 @@ The current SDK intentionally does not expose:
1414
1500
 
1415
1501
  ```txt
1416
1502
  start is the public lifecycle entrypoint
1417
- start returns Promise<PrimerState>
1503
+ start returns AccessTokenStartState or ManagedStartState based on accessToken presence
1418
1504
  accessToken present is used for learner runtime auth
1419
- accessToken absent may produce UnauthenticatedState
1420
- UnauthenticatedState.login is the hosted-auth user-gesture transition
1505
+ accessToken present cannot produce hosted-auth states
1506
+ accessToken absent may produce SignInRequiredState
1507
+ SignInRequiredState.login and SignInFailedState.login are hosted-auth user-gesture transitions
1508
+ AuthUnavailableState has no login operation
1509
+ AuthConfigInvalidState has no login operation
1421
1510
  subject is optional; omitted means all-subject runtime scope
1422
1511
  subject determines required renderer PCI capabilities
1423
1512
  supportedPcis declares renderer PCI capabilities
@@ -1,17 +1,7 @@
1
1
  import type { PrimerLogger } from "../../logger";
2
- type HostedAuthOptions = {
3
- readonly redirectUri?: string;
4
- readonly storage?: Storage;
5
- readonly currentUrl?: string;
6
- readonly popupTarget?: string;
7
- readonly popupFeatures?: string;
8
- readonly popupTimeoutMs?: number;
9
- };
10
- declare function browserStorage(options: HostedAuthOptions | undefined, logger: PrimerLogger): Storage;
11
- declare function currentUrl(options: HostedAuthOptions | undefined, logger: PrimerLogger): URL;
12
- declare function redirectUri(options: HostedAuthOptions | undefined, url: URL, logger: PrimerLogger): string;
2
+ declare function currentUrl(logger: PrimerLogger): URL;
3
+ declare function redirectUri(url: URL): string;
13
4
  declare function randomClientState(logger: PrimerLogger): string;
14
- declare function openAuthPopup(url: string, options: HostedAuthOptions | undefined, logger: PrimerLogger): Window;
15
- export { browserStorage, currentUrl, openAuthPopup, randomClientState, redirectUri };
16
- export type { HostedAuthOptions };
5
+ declare function openAuthPopup(url: string, logger: PrimerLogger): Window;
6
+ export { currentUrl, openAuthPopup, randomClientState, redirectUri };
17
7
  //# sourceMappingURL=browser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../../src/client/auth/browser.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAA;AAEtE,KAAK,iBAAiB,GAAG;IACxB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;IAC1B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;IAC/B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAChC,CAAA;AAED,iBAAS,cAAc,CAAC,OAAO,EAAE,iBAAiB,GAAG,SAAS,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAS7F;AAED,iBAAS,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,SAAS,EAAE,MAAM,EAAE,YAAY,GAAG,GAAG,CAarF;AAED,iBAAS,WAAW,CACnB,OAAO,EAAE,iBAAiB,GAAG,SAAS,EACtC,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,YAAY,GAClB,MAAM,CASR;AAED,iBAAS,iBAAiB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAYvD;AAED,iBAAS,aAAa,CACrB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,iBAAiB,GAAG,SAAS,EACtC,MAAM,EAAE,YAAY,GAClB,MAAM,CAmBR;AAED,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,aAAa,EAAE,iBAAiB,EAAE,WAAW,EAAE,CAAA;AACpF,YAAY,EAAE,iBAAiB,EAAE,CAAA"}
1
+ {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../../src/client/auth/browser.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAA;AAKtE,iBAAS,UAAU,CAAC,MAAM,EAAE,YAAY,GAAG,GAAG,CAM7C;AAED,iBAAS,WAAW,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAErC;AAED,iBAAS,iBAAiB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAYvD;AAED,iBAAS,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,MAAM,CAWhE;AAED,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,iBAAiB,EAAE,WAAW,EAAE,CAAA"}