@superbuilders/primer-tives 3.5.1 → 3.7.0

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