@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 +191 -102
- package/dist/client/auth/browser.d.ts +4 -14
- package/dist/client/auth/browser.d.ts.map +1 -1
- package/dist/client/auth/hosted-popup.d.ts +0 -2
- package/dist/client/auth/hosted-popup.d.ts.map +1 -1
- package/dist/client/auth/provider.d.ts +13 -8
- package/dist/client/auth/provider.d.ts.map +1 -1
- package/dist/client/auth-state.d.ts +14 -0
- package/dist/client/auth-state.d.ts.map +1 -0
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +103 -172
- package/dist/client/index.js.map +10 -11
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/start.d.ts +10 -4
- package/dist/client/start.d.ts.map +1 -1
- package/dist/client/types.d.ts +29 -8
- package/dist/client/types.d.ts.map +1 -1
- package/dist/contracts/validation.d.ts +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +2 -2
- package/dist/client/auth/callback.d.ts +0 -10
- package/dist/client/auth/callback.d.ts.map +0 -1
- package/dist/client/auth/storage.d.ts +0 -10
- package/dist/client/auth/storage.d.ts.map +0 -1
- package/dist/client/unauthenticated-state.d.ts +0 -10
- package/dist/client/unauthenticated-state.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
TypeScript SDK primitives for the Primer adaptive learning runtime.
|
|
4
4
|
|
|
5
|
-
The public lifecycle starts with one async call and one
|
|
5
|
+
The public lifecycle starts with one async call and, when hosted auth is needed, one user-gesture auth transition:
|
|
6
6
|
|
|
7
7
|
```txt
|
|
8
|
-
start(options) -> Promise<
|
|
9
|
-
|
|
8
|
+
start(options with accessToken) -> Promise<AccessTokenStartState>
|
|
9
|
+
start(options without accessToken) -> Promise<ManagedStartState>
|
|
10
|
+
SignInRequiredState.login() -> Promise<ManagedStartState>
|
|
11
|
+
SignInFailedState.login() -> Promise<ManagedStartState>
|
|
10
12
|
```
|
|
11
13
|
|
|
12
|
-
`start`
|
|
14
|
+
`start` enters the first Primer learning state when learner auth is ready and returns the live state machine object your renderer drives. When learner sign-in is needed, managed-auth `start` returns `SignInRequiredState`; render a sign-in button and call `state.login()` directly from that button's click or tap handler.
|
|
13
15
|
|
|
14
16
|
```sh
|
|
15
17
|
bun add @superbuilders/primer-tives
|
|
@@ -17,7 +19,7 @@ bun add @superbuilders/primer-tives
|
|
|
17
19
|
|
|
18
20
|
## Version
|
|
19
21
|
|
|
20
|
-
The current SDK version is `4.0.
|
|
22
|
+
The current SDK version is `4.0.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 {
|
|
44
|
+
import {
|
|
45
|
+
start,
|
|
46
|
+
type PrimerOptionsWithAccessToken,
|
|
47
|
+
type PrimerOptionsWithManagedAuth
|
|
48
|
+
} from "@superbuilders/primer-tives/client"
|
|
43
49
|
|
|
44
50
|
const options = {
|
|
45
51
|
publishableKey: "pk_...",
|
|
46
52
|
subject: "math",
|
|
47
53
|
supportedPcis: ["urn:primer:pci:fraction-input"],
|
|
48
54
|
logger
|
|
49
|
-
} satisfies
|
|
55
|
+
} satisfies PrimerOptionsWithManagedAuth<"math", readonly ["urn:primer:pci:fraction-input"]>
|
|
50
56
|
|
|
51
57
|
let state = await start(options)
|
|
52
58
|
```
|
|
@@ -60,23 +66,23 @@ const options = {
|
|
|
60
66
|
subject: "math",
|
|
61
67
|
supportedPcis: ["urn:primer:pci:fraction-input"],
|
|
62
68
|
logger
|
|
63
|
-
} satisfies
|
|
69
|
+
} satisfies PrimerOptionsWithAccessToken<"math", readonly ["urn:primer:pci:fraction-input"]>
|
|
64
70
|
|
|
65
71
|
let state = await start(options)
|
|
66
72
|
```
|
|
67
73
|
|
|
68
|
-
When `start` returns `
|
|
74
|
+
When managed-auth `start` returns `SignInRequiredState`, render sign-in UI and call `login()` directly from a user gesture:
|
|
69
75
|
|
|
70
76
|
```ts
|
|
71
|
-
import type {
|
|
77
|
+
import type { ManagedStartState, SignInRequiredState } from "@superbuilders/primer-tives/client"
|
|
72
78
|
|
|
73
|
-
let state:
|
|
79
|
+
let state: ManagedStartState = await start(options)
|
|
74
80
|
|
|
75
|
-
if (state.phase === "
|
|
81
|
+
if (state.phase === "sign-in-required") {
|
|
76
82
|
renderSignInButton(state)
|
|
77
83
|
}
|
|
78
84
|
|
|
79
|
-
function handleSignInClick(authState:
|
|
85
|
+
function handleSignInClick(authState: SignInRequiredState): void {
|
|
80
86
|
void authState.login().then(function continueAfterLogin(nextState) {
|
|
81
87
|
state = nextState
|
|
82
88
|
renderPrimer(state)
|
|
@@ -93,7 +99,7 @@ const options = {
|
|
|
93
99
|
publishableKey: "pk_...",
|
|
94
100
|
subject: "vocabulary",
|
|
95
101
|
logger
|
|
96
|
-
} satisfies
|
|
102
|
+
} satisfies PrimerOptionsWithManagedAuth<"vocabulary">
|
|
97
103
|
|
|
98
104
|
let state = await start(options)
|
|
99
105
|
```
|
|
@@ -105,7 +111,7 @@ const options = {
|
|
|
105
111
|
publishableKey: "pk_...",
|
|
106
112
|
supportedPcis: ["urn:primer:pci:fraction-input"],
|
|
107
113
|
logger
|
|
108
|
-
} satisfies
|
|
114
|
+
} satisfies PrimerOptionsWithManagedAuth<undefined, readonly ["urn:primer:pci:fraction-input"]>
|
|
109
115
|
|
|
110
116
|
let state = await start(options)
|
|
111
117
|
```
|
|
@@ -116,14 +122,22 @@ let state = await start(options)
|
|
|
116
122
|
function start<
|
|
117
123
|
const S extends Subject | undefined = undefined,
|
|
118
124
|
const Supported extends readonly PciId[] = []
|
|
119
|
-
>(options:
|
|
125
|
+
>(options: PrimerOptionsWithAccessToken<S, Supported>): Promise<AccessTokenStartState>
|
|
126
|
+
|
|
127
|
+
function start<
|
|
128
|
+
const S extends Subject | undefined = undefined,
|
|
129
|
+
const Supported extends readonly PciId[] = []
|
|
130
|
+
>(options: PrimerOptionsWithManagedAuth<S, Supported>): Promise<ManagedStartState>
|
|
120
131
|
```
|
|
121
132
|
|
|
122
|
-
`start` is the first SDK lifecycle operation.
|
|
133
|
+
`start` is the first SDK lifecycle operation. Its return type depends on whether `accessToken` is present:
|
|
123
134
|
|
|
124
135
|
| Result | Meaning |
|
|
125
136
|
| --- | --- |
|
|
126
|
-
| `
|
|
137
|
+
| `SignInRequiredState` | Learner sign-in is needed before runtime learning can begin. Managed-auth mode only. |
|
|
138
|
+
| `SignInFailedState` | Hosted sign-in failed but can be retried. Managed-auth mode only. |
|
|
139
|
+
| `AuthUnavailableState` | Browser-hosted auth cannot run in the current runtime. Managed-auth mode only. |
|
|
140
|
+
| `AuthConfigInvalidState` | Hosted-auth configuration is invalid and cannot be retried. Managed-auth mode only. |
|
|
127
141
|
| `ObservationState`, `InteractionState`, or `FeedbackState` | A learning state is ready to render. |
|
|
128
142
|
| `CompletedState` | The runtime scope is already complete. |
|
|
129
143
|
| `ErroredState` | Startup or runtime communication failed but may be retriable. |
|
|
@@ -134,7 +148,7 @@ Always switch on `state.phase`. Do not assume the first state is renderable lear
|
|
|
134
148
|
```ts
|
|
135
149
|
import * as errors from "@superbuilders/errors"
|
|
136
150
|
import { logger } from "@/logger"
|
|
137
|
-
import { start, type
|
|
151
|
+
import { start, type PrimerOptionsWithAccessToken } from "@superbuilders/primer-tives/client"
|
|
138
152
|
import { ErrAuthUnavailable, ErrMalformedAccessToken } from "@superbuilders/primer-tives/errors"
|
|
139
153
|
|
|
140
154
|
const options = {
|
|
@@ -143,24 +157,15 @@ const options = {
|
|
|
143
157
|
subject: "math",
|
|
144
158
|
supportedPcis: ["urn:primer:pci:fraction-input"],
|
|
145
159
|
logger
|
|
146
|
-
} satisfies
|
|
160
|
+
} satisfies PrimerOptionsWithAccessToken<"math", readonly ["urn:primer:pci:fraction-input"]>
|
|
147
161
|
|
|
148
162
|
let state = await start(options)
|
|
149
|
-
if (state.phase === "unauthenticated") {
|
|
150
|
-
if (errors.is(state.error, ErrAuthUnavailable)) {
|
|
151
|
-
renderUnsupportedBrowserMessage()
|
|
152
|
-
return
|
|
153
|
-
}
|
|
154
|
-
renderSignInButton(state)
|
|
155
|
-
return
|
|
156
|
-
}
|
|
157
|
-
|
|
158
163
|
if (state.phase === "fatal") {
|
|
159
164
|
if (errors.is(state.error, ErrMalformedAccessToken)) {
|
|
160
165
|
renderSignInAgain()
|
|
161
166
|
return
|
|
162
167
|
}
|
|
163
|
-
logger.error(
|
|
168
|
+
logger.error({ error: state.error }, "primer fatal state")
|
|
164
169
|
throw state.error
|
|
165
170
|
}
|
|
166
171
|
```
|
|
@@ -170,19 +175,28 @@ if (state.phase === "fatal") {
|
|
|
170
175
|
```ts
|
|
171
176
|
type PrimerOptions<S extends Subject | undefined = undefined, Supported extends readonly PciId[] = []> = {
|
|
172
177
|
readonly publishableKey: string
|
|
173
|
-
readonly
|
|
178
|
+
readonly origin?: string
|
|
174
179
|
readonly subject?: S
|
|
175
180
|
readonly supportedPcis: subject-dependent
|
|
176
181
|
readonly fetch?: typeof globalThis.fetch
|
|
177
182
|
readonly abort?: AbortController
|
|
178
183
|
readonly logger: PrimerLogger
|
|
179
184
|
}
|
|
185
|
+
|
|
186
|
+
type PrimerOptionsWithAccessToken<S, Supported> = PrimerOptions<S, Supported> & {
|
|
187
|
+
readonly accessToken: string
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
type PrimerOptionsWithManagedAuth<S, Supported> = PrimerOptions<S, Supported> & {
|
|
191
|
+
readonly accessToken?: undefined
|
|
192
|
+
}
|
|
180
193
|
```
|
|
181
194
|
|
|
182
195
|
| Field | Required | Meaning |
|
|
183
196
|
| --- | --- | --- |
|
|
184
197
|
| `publishableKey` | Yes | Public key identifying the Primer frontend your runtime belongs to. |
|
|
185
|
-
| `
|
|
198
|
+
| `origin` | No | Primer origin. Defaults to `https://primerlearn.dev`. |
|
|
199
|
+
| `accessToken` | Mode-dependent | Learner access token. When present, `start` uses access-token mode. When absent, `start` uses managed hosted-auth mode. |
|
|
186
200
|
| `subject` | No | Public content scope: `"math"`, `"vocabulary"`, or `"science"`. Omitted means all-subject scope. |
|
|
187
201
|
| `supportedPcis` | Subject-dependent | Renderer capabilities for Portable Custom Interactions. Required when the chosen scope can emit required PCIs. |
|
|
188
202
|
| `fetch` | No | Fetch override for tests, instrumentation, or host runtime integration. |
|
|
@@ -195,18 +209,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
|
|
199
|
-
| `accessToken` absent | `start`
|
|
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. `
|
|
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
|
|
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
|
|
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 === "
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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(
|
|
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(
|
|
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
|
-
| `
|
|
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
|
-
| `
|
|
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
|
-
|
|
|
374
|
-
|
|
|
375
|
-
|
|
|
376
|
-
|
|
|
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
|
-
## `
|
|
424
|
+
## `SignInRequiredState`
|
|
387
425
|
|
|
388
426
|
```ts
|
|
389
|
-
interface
|
|
390
|
-
readonly phase: "
|
|
391
|
-
|
|
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
|
-
`
|
|
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:
|
|
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:
|
|
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()`
|
|
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
|
-
|
|
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:
|
|
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`,
|
|
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(
|
|
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(
|
|
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 === "
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 `
|
|
1319
|
-
| auth login fails | `login()` resolves to `
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 === "
|
|
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
|
|
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
|
-
|
|
|
1405
|
-
|
|
|
1406
|
-
| client wrapper object | `start` returning
|
|
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
|
|
1503
|
+
start returns AccessTokenStartState or ManagedStartState based on accessToken presence
|
|
1418
1504
|
accessToken present is used for learner runtime auth
|
|
1419
|
-
accessToken
|
|
1420
|
-
|
|
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
|
-
|
|
3
|
-
|
|
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,
|
|
15
|
-
export {
|
|
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":"
|
|
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"}
|