@superbuilders/primer-tives 1.1.2 → 1.1.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.
Files changed (78) hide show
  1. package/README.md +407 -359
  2. package/dist/client/choice-state.d.ts +9 -0
  3. package/dist/client/choice-state.d.ts.map +1 -0
  4. package/dist/client/consumed.d.ts +3 -0
  5. package/dist/client/consumed.d.ts.map +1 -0
  6. package/dist/client/create.d.ts +20 -0
  7. package/dist/client/create.d.ts.map +1 -0
  8. package/dist/client/extended-text-state.d.ts +9 -0
  9. package/dist/client/extended-text-state.d.ts.map +1 -0
  10. package/dist/client/feedback-state.d.ts +9 -0
  11. package/dist/client/feedback-state.d.ts.map +1 -0
  12. package/dist/client/index.d.ts +4 -0
  13. package/dist/client/index.d.ts.map +1 -0
  14. package/dist/client/index.js +1074 -0
  15. package/dist/client/index.js.map +25 -0
  16. package/dist/client/match-state.d.ts +9 -0
  17. package/dist/client/match-state.d.ts.map +1 -0
  18. package/dist/client/observation-state.d.ts +7 -0
  19. package/dist/client/observation-state.d.ts.map +1 -0
  20. package/dist/client/order-state.d.ts +9 -0
  21. package/dist/client/order-state.d.ts.map +1 -0
  22. package/dist/client/pci-state.d.ts +7 -0
  23. package/dist/client/pci-state.d.ts.map +1 -0
  24. package/dist/client/session-context.d.ts +20 -0
  25. package/dist/client/session-context.d.ts.map +1 -0
  26. package/dist/client/session.d.ts +18 -0
  27. package/dist/client/session.d.ts.map +1 -0
  28. package/dist/client/text-entry-state.d.ts +9 -0
  29. package/dist/client/text-entry-state.d.ts.map +1 -0
  30. package/dist/client/transport.d.ts +47 -0
  31. package/dist/client/transport.d.ts.map +1 -0
  32. package/dist/client/types.d.ts +144 -0
  33. package/dist/client/types.d.ts.map +1 -0
  34. package/dist/contracts/content.d.ts +20 -0
  35. package/dist/contracts/content.d.ts.map +1 -0
  36. package/dist/contracts/index.d.ts +8 -0
  37. package/dist/contracts/index.d.ts.map +1 -0
  38. package/dist/contracts/index.js +326 -0
  39. package/dist/contracts/index.js.map +12 -0
  40. package/dist/contracts/pci-schemas.d.ts +25 -0
  41. package/dist/contracts/pci-schemas.d.ts.map +1 -0
  42. package/dist/contracts/pci.d.ts +38 -0
  43. package/dist/contracts/pci.d.ts.map +1 -0
  44. package/dist/contracts/review.d.ts +55 -0
  45. package/dist/contracts/review.d.ts.map +1 -0
  46. package/dist/contracts/types.d.ts +118 -0
  47. package/dist/contracts/types.d.ts.map +1 -0
  48. package/dist/contracts/validation.d.ts +92 -0
  49. package/dist/contracts/validation.d.ts.map +1 -0
  50. package/dist/errors.d.ts +23 -0
  51. package/dist/errors.d.ts.map +1 -0
  52. package/dist/errors.js +48 -0
  53. package/dist/errors.js.map +10 -0
  54. package/dist/grade-level.d.ts +5 -0
  55. package/dist/grade-level.d.ts.map +1 -0
  56. package/dist/grade-level.js +7 -0
  57. package/dist/grade-level.js.map +10 -0
  58. package/dist/logger.d.ts +8 -0
  59. package/dist/logger.d.ts.map +1 -0
  60. package/dist/logger.js +2 -0
  61. package/dist/logger.js.map +9 -0
  62. package/dist/server/create-server.d.ts +44 -0
  63. package/dist/server/create-server.d.ts.map +1 -0
  64. package/dist/server/exchange.d.ts +22 -0
  65. package/dist/server/exchange.d.ts.map +1 -0
  66. package/dist/server/hints.d.ts +25 -0
  67. package/dist/server/hints.d.ts.map +1 -0
  68. package/dist/server/index.d.ts +5 -0
  69. package/dist/server/index.d.ts.map +1 -0
  70. package/dist/server/index.js +516 -0
  71. package/dist/server/index.js.map +15 -0
  72. package/dist/server/students.d.ts +12 -0
  73. package/dist/server/students.d.ts.map +1 -0
  74. package/dist/subject.d.ts +6 -0
  75. package/dist/subject.d.ts.map +1 -0
  76. package/dist/subject.js +7 -0
  77. package/dist/subject.js.map +10 -0
  78. package/package.json +14 -6
@@ -0,0 +1,1074 @@
1
+ // src/errors.ts
2
+ import * as errors from "@superbuilders/errors";
3
+ var ErrNetwork = errors.new("network");
4
+ var ErrJsonParse = errors.new("json parse");
5
+ var ErrUnsupportedPci = errors.new("unsupported pci");
6
+ var ErrInvalidAccessToken = errors.new("invalid access token");
7
+ var ErrMalformedAccessToken = errors.new("malformed access token");
8
+ var ErrTokenExpired = errors.new("access token expired");
9
+ var ErrBadRequest = errors.new("bad request");
10
+ var ErrServerError = errors.new("server error");
11
+ var ErrTimeout = errors.new("timeout");
12
+ var ErrForbidden = errors.new("forbidden");
13
+ var ErrNotFound = errors.new("not found");
14
+ var ErrConflict = errors.new("conflict");
15
+ var ErrExternalAuthorityRequired = errors.new("external authority required");
16
+ var ErrRateLimited = errors.new("rate limited");
17
+ var ErrServiceUnavailable = errors.new("service unavailable");
18
+ var ErrNotSerializable = errors.new("PrimerState is live in-memory state and must not be serialized or stored");
19
+ var ErrInvalidSubmission = errors.new("invalid submission");
20
+ var ErrInvalidSecretKey = errors.new("invalid secret key");
21
+ var ErrStudentNotFound = errors.new("student not found");
22
+ var ErrUnsupportedGrade = errors.new("unsupported grade");
23
+ var ErrTimebackUnavailable = errors.new("timeback unavailable");
24
+ // src/contracts/content.ts
25
+ function inlinesToPlainText(nodes) {
26
+ const parts = [];
27
+ for (const node of nodes) {
28
+ switch (node.type) {
29
+ case "text":
30
+ parts.push(node.value);
31
+ break;
32
+ case "italic":
33
+ parts.push(node.value);
34
+ break;
35
+ case "latex":
36
+ parts.push(node.value);
37
+ break;
38
+ }
39
+ }
40
+ return parts.join("");
41
+ }
42
+ function blocksToPlainText(blocks) {
43
+ return blocks.map(function blockText(block) {
44
+ return inlinesToPlainText(block.children);
45
+ }).join(`
46
+ `);
47
+ }
48
+ // src/contracts/validation.ts
49
+ import { z as z2 } from "zod";
50
+
51
+ // src/contracts/pci-schemas.ts
52
+ import { z } from "zod";
53
+ var DivisionRemainderPropsSchema = z.object({
54
+ dividend: z.number(),
55
+ divisor: z.number()
56
+ });
57
+ var DivisionRemainderSubmissionSchema = z.object({
58
+ quotient: z.string(),
59
+ remainder: z.string()
60
+ });
61
+ var FractionOperandSchema = z.object({
62
+ numerator: z.number(),
63
+ denominator: z.number()
64
+ });
65
+ var FractionAdditionPropsSchema = z.object({
66
+ left: FractionOperandSchema,
67
+ right: FractionOperandSchema
68
+ });
69
+ var FractionAdditionSubmissionSchema = z.object({
70
+ numerator: z.string(),
71
+ denominator: z.string()
72
+ });
73
+
74
+ // src/contracts/validation.ts
75
+ var MatchPairSchema = z2.object({
76
+ source: z2.string(),
77
+ target: z2.string()
78
+ });
79
+ var ChoiceSubmissionSchema = z2.object({
80
+ type: z2.literal("choice"),
81
+ selectedKeys: z2.array(z2.string())
82
+ });
83
+ var TextEntrySubmissionSchema = z2.object({
84
+ type: z2.literal("text-entry"),
85
+ value: z2.string()
86
+ });
87
+ var ExtendedTextSubmissionSchema = z2.object({
88
+ type: z2.literal("extended-text"),
89
+ values: z2.array(z2.string()).min(1)
90
+ });
91
+ var OrderSubmissionSchema = z2.object({
92
+ type: z2.literal("order"),
93
+ orderedKeys: z2.array(z2.string())
94
+ });
95
+ var MatchSubmissionSchema = z2.object({
96
+ type: z2.literal("match"),
97
+ pairs: z2.array(MatchPairSchema)
98
+ });
99
+ var DivisionRemainderPciSubmissionSchema = z2.object({
100
+ type: z2.literal("portable-custom"),
101
+ pciId: z2.literal("urn:primer:pci:division-remainder"),
102
+ value: DivisionRemainderSubmissionSchema
103
+ });
104
+ var FractionAdditionPciSubmissionSchema = z2.object({
105
+ type: z2.literal("portable-custom"),
106
+ pciId: z2.literal("urn:primer:pci:fraction-addition"),
107
+ value: FractionAdditionSubmissionSchema
108
+ });
109
+ var RendererSubmissionSchema = z2.union([
110
+ ChoiceSubmissionSchema,
111
+ TextEntrySubmissionSchema,
112
+ ExtendedTextSubmissionSchema,
113
+ OrderSubmissionSchema,
114
+ MatchSubmissionSchema,
115
+ DivisionRemainderPciSubmissionSchema,
116
+ FractionAdditionPciSubmissionSchema
117
+ ]);
118
+ function duplicates(values, keyOf) {
119
+ const seen = new Set;
120
+ const duplicated = new Set;
121
+ for (const value of values) {
122
+ const key = keyOf(value);
123
+ if (seen.has(key)) {
124
+ duplicated.add(key);
125
+ continue;
126
+ }
127
+ seen.add(key);
128
+ }
129
+ return [...duplicated];
130
+ }
131
+ function findUnknownIds(values, choices) {
132
+ const ids = new Set(choices.map(function getId(choice) {
133
+ return choice.identifier;
134
+ }));
135
+ return values.filter(function isUnknown(value) {
136
+ return !ids.has(value);
137
+ });
138
+ }
139
+ function countByIdentifier(pairs, side) {
140
+ const counts = new Map;
141
+ for (const pair of pairs) {
142
+ const key = pair[side];
143
+ const currentCount = counts.get(key);
144
+ if (currentCount === undefined) {
145
+ counts.set(key, 1);
146
+ continue;
147
+ }
148
+ counts.set(key, currentCount + 1);
149
+ }
150
+ return counts;
151
+ }
152
+ function validateUsageBounds(choices, counts, side) {
153
+ const issues = [];
154
+ for (const choice of choices) {
155
+ const maybeCount = counts.get(choice.identifier);
156
+ const count = maybeCount === undefined ? 0 : maybeCount;
157
+ if (choice.matchMax !== 0 && count > choice.matchMax) {
158
+ issues.push(`${side} '${choice.identifier}' used ${count} times, max ${choice.matchMax}`);
159
+ }
160
+ if (count < choice.matchMin) {
161
+ issues.push(`${side} '${choice.identifier}' used ${count} times, min ${choice.matchMin}`);
162
+ }
163
+ }
164
+ return issues;
165
+ }
166
+ function pciSubmissionSchema(pciId) {
167
+ switch (pciId) {
168
+ case "urn:primer:pci:division-remainder":
169
+ return DivisionRemainderSubmissionSchema;
170
+ case "urn:primer:pci:fraction-addition":
171
+ return FractionAdditionSubmissionSchema;
172
+ }
173
+ const exhaustiveCheck = pciId;
174
+ return exhaustiveCheck;
175
+ }
176
+ function typeMismatch(interactionType, submissionType) {
177
+ return `submission type '${submissionType}' does not match interaction type '${interactionType}'`;
178
+ }
179
+ function buildResult(submission, issues) {
180
+ if (issues.length > 0) {
181
+ return { ok: false, issues };
182
+ }
183
+ return { ok: true, value: submission };
184
+ }
185
+ function validateChoice(interaction, submission) {
186
+ const issues = [];
187
+ if (submission.selectedKeys.length < interaction.minChoices) {
188
+ issues.push(`need at least ${interaction.minChoices} selections`);
189
+ }
190
+ if (submission.selectedKeys.length > interaction.maxChoices) {
191
+ issues.push(`at most ${interaction.maxChoices} selections`);
192
+ }
193
+ const duplicateKeys = duplicates(submission.selectedKeys, function keyOf(value) {
194
+ return value;
195
+ });
196
+ if (duplicateKeys.length > 0) {
197
+ issues.push("duplicate selections");
198
+ }
199
+ const unknownKeys = findUnknownIds(submission.selectedKeys, interaction.options);
200
+ if (unknownKeys.length > 0) {
201
+ issues.push(`unknown options: ${unknownKeys.join(", ")}`);
202
+ }
203
+ return issues;
204
+ }
205
+ function validateExtendedText(interaction, submission) {
206
+ const issues = [];
207
+ if (interaction.cardinality === "single") {
208
+ if (submission.values.length !== 1) {
209
+ issues.push("single-cardinality extended-text requires exactly one value");
210
+ }
211
+ return issues;
212
+ }
213
+ if (submission.values.length < interaction.minStrings) {
214
+ issues.push(`need at least ${interaction.minStrings} values`);
215
+ }
216
+ if (submission.values.length > interaction.maxStrings) {
217
+ issues.push(`at most ${interaction.maxStrings} values`);
218
+ }
219
+ const duplicateValues = duplicates(submission.values, function keyOf(value) {
220
+ return value;
221
+ });
222
+ if (duplicateValues.length > 0) {
223
+ issues.push("duplicate values are not allowed for multiple-cardinality extended-text");
224
+ }
225
+ return issues;
226
+ }
227
+ function validateOrder(interaction, submission) {
228
+ const issues = [];
229
+ if (submission.orderedKeys.length < interaction.minChoices) {
230
+ issues.push(`need at least ${interaction.minChoices} selections`);
231
+ }
232
+ if (submission.orderedKeys.length > interaction.maxChoices) {
233
+ issues.push(`at most ${interaction.maxChoices} selections`);
234
+ }
235
+ const duplicateKeys = duplicates(submission.orderedKeys, function keyOf(value) {
236
+ return value;
237
+ });
238
+ if (duplicateKeys.length > 0) {
239
+ issues.push("duplicate selections");
240
+ }
241
+ const unknownKeys = findUnknownIds(submission.orderedKeys, interaction.choices);
242
+ if (unknownKeys.length > 0) {
243
+ issues.push(`unknown choices: ${unknownKeys.join(", ")}`);
244
+ }
245
+ return issues;
246
+ }
247
+ function validateMatch(interaction, submission) {
248
+ const issues = [];
249
+ if (submission.pairs.length < interaction.minAssociations) {
250
+ issues.push(`need at least ${interaction.minAssociations} associations`);
251
+ }
252
+ if (submission.pairs.length > interaction.maxAssociations) {
253
+ issues.push(`at most ${interaction.maxAssociations} associations`);
254
+ }
255
+ const duplicatePairs = duplicates(submission.pairs, function keyOf(pair) {
256
+ return `${pair.source}->${pair.target}`;
257
+ });
258
+ if (duplicatePairs.length > 0) {
259
+ issues.push("duplicate associations are not allowed");
260
+ }
261
+ const sourceIds = new Set(interaction.sourceChoices.map(function getId(choice) {
262
+ return choice.identifier;
263
+ }));
264
+ const targetIds = new Set(interaction.targetChoices.map(function getId(choice) {
265
+ return choice.identifier;
266
+ }));
267
+ for (const pair of submission.pairs) {
268
+ if (!sourceIds.has(pair.source)) {
269
+ issues.push(`unknown source '${pair.source}'`);
270
+ }
271
+ if (!targetIds.has(pair.target)) {
272
+ issues.push(`unknown target '${pair.target}'`);
273
+ }
274
+ }
275
+ const sourceCounts = countByIdentifier(submission.pairs, "source");
276
+ const targetCounts = countByIdentifier(submission.pairs, "target");
277
+ issues.push(...validateUsageBounds(interaction.sourceChoices, sourceCounts, "source"));
278
+ issues.push(...validateUsageBounds(interaction.targetChoices, targetCounts, "target"));
279
+ return issues;
280
+ }
281
+ function validatePortableCustom(interaction, submission) {
282
+ if (submission.pciId !== interaction.pciId) {
283
+ return [
284
+ `submission PCI '${submission.pciId}' does not match interaction PCI '${interaction.pciId}'`
285
+ ];
286
+ }
287
+ const schema = pciSubmissionSchema(interaction.pciId);
288
+ const result = schema.safeParse(submission.value);
289
+ if (!result.success) {
290
+ return result.error.issues.map(function toIssue(issue) {
291
+ return issue.message;
292
+ });
293
+ }
294
+ return [];
295
+ }
296
+ function validateSubmissionForInteraction(interaction, submission) {
297
+ switch (interaction.type) {
298
+ case "choice":
299
+ if (submission.type !== "choice") {
300
+ return { ok: false, issues: [typeMismatch(interaction.type, submission.type)] };
301
+ }
302
+ return buildResult(submission, validateChoice(interaction, submission));
303
+ case "text-entry":
304
+ if (submission.type !== "text-entry") {
305
+ return { ok: false, issues: [typeMismatch(interaction.type, submission.type)] };
306
+ }
307
+ return { ok: true, value: submission };
308
+ case "extended-text":
309
+ if (submission.type !== "extended-text") {
310
+ return { ok: false, issues: [typeMismatch(interaction.type, submission.type)] };
311
+ }
312
+ return buildResult(submission, validateExtendedText(interaction, submission));
313
+ case "order":
314
+ if (submission.type !== "order") {
315
+ return { ok: false, issues: [typeMismatch(interaction.type, submission.type)] };
316
+ }
317
+ return buildResult(submission, validateOrder(interaction, submission));
318
+ case "match":
319
+ if (submission.type !== "match") {
320
+ return { ok: false, issues: [typeMismatch(interaction.type, submission.type)] };
321
+ }
322
+ return buildResult(submission, validateMatch(interaction, submission));
323
+ case "portable-custom":
324
+ if (submission.type !== "portable-custom") {
325
+ return { ok: false, issues: [typeMismatch(interaction.type, submission.type)] };
326
+ }
327
+ return buildResult(submission, validatePortableCustom(interaction, submission));
328
+ }
329
+ }
330
+ function submissionValidationMessage(result) {
331
+ return result.issues.join("; ");
332
+ }
333
+ // src/client/create.ts
334
+ import * as errors10 from "@superbuilders/errors";
335
+
336
+ // src/client/transport.ts
337
+ import * as errors2 from "@superbuilders/errors";
338
+ var ADVANCE_PATH = "/api/v0/advance";
339
+ function isAdvanceErrorBody(value) {
340
+ if (typeof value !== "object" || value === null) {
341
+ return false;
342
+ }
343
+ if ("error" in value && value.error !== undefined && typeof value.error !== "string") {
344
+ return false;
345
+ }
346
+ if ("detail" in value && value.detail !== undefined && typeof value.detail !== "string") {
347
+ return false;
348
+ }
349
+ return true;
350
+ }
351
+ function parseAdvanceErrorBody(body) {
352
+ if (body.length === 0) {
353
+ return null;
354
+ }
355
+ const parsed = errors2.trySync(function parseJson() {
356
+ return JSON.parse(body);
357
+ });
358
+ if (parsed.error) {
359
+ return null;
360
+ }
361
+ if (!isAdvanceErrorBody(parsed.data)) {
362
+ return null;
363
+ }
364
+ return parsed.data;
365
+ }
366
+ function httpSentinel(status, body) {
367
+ if (status === 400) {
368
+ return ErrBadRequest;
369
+ }
370
+ if (status === 401) {
371
+ const parsed = parseAdvanceErrorBody(body);
372
+ if (parsed?.detail === "token_expired") {
373
+ return ErrTokenExpired;
374
+ }
375
+ return ErrInvalidAccessToken;
376
+ }
377
+ if (status === 403) {
378
+ return ErrForbidden;
379
+ }
380
+ if (status === 404) {
381
+ return ErrNotFound;
382
+ }
383
+ if (status === 409) {
384
+ return ErrConflict;
385
+ }
386
+ if (status === 429) {
387
+ return ErrRateLimited;
388
+ }
389
+ if (status === 502 || status === 503 || status === 504) {
390
+ return ErrServiceUnavailable;
391
+ }
392
+ return ErrServerError;
393
+ }
394
+ function isAbortError(err) {
395
+ if (err instanceof DOMException && err.name === "AbortError") {
396
+ return true;
397
+ }
398
+ if (err instanceof DOMException && err.name === "TimeoutError") {
399
+ return true;
400
+ }
401
+ return false;
402
+ }
403
+ function createTransport(tc) {
404
+ const fetchFn = tc.fetch ? tc.fetch : globalThis.fetch;
405
+ const log = tc.log;
406
+ function transportSignal() {
407
+ if (tc.abort) {
408
+ return tc.abort.signal;
409
+ }
410
+ return;
411
+ }
412
+ async function transport(body) {
413
+ log?.debug("transport request", {
414
+ intentKind: body.intent.kind,
415
+ subject: body.subject
416
+ });
417
+ const url = `${tc.origin}${ADVANCE_PATH}`;
418
+ const fetchResult = await fetchFn(url, {
419
+ method: "POST",
420
+ headers: {
421
+ "Content-Type": "application/json",
422
+ Authorization: `Bearer ${tc.accessToken}`
423
+ },
424
+ body: JSON.stringify(body),
425
+ signal: transportSignal()
426
+ }).then(function ok(r) {
427
+ return { ok: true, response: r };
428
+ }, function fail(err) {
429
+ return { ok: false, error: err };
430
+ });
431
+ if (!fetchResult.ok) {
432
+ if (isAbortError(fetchResult.error)) {
433
+ log?.error("transport timeout", {
434
+ intentKind: body.intent.kind
435
+ });
436
+ return { ok: false, error: errors2.wrap(ErrTimeout, fetchResult.error.message) };
437
+ }
438
+ log?.error("transport network error", {
439
+ error: fetchResult.error
440
+ });
441
+ return { ok: false, error: errors2.wrap(ErrNetwork, fetchResult.error.message) };
442
+ }
443
+ const res = fetchResult.response;
444
+ if (!res.ok) {
445
+ const text = await res.text().catch(function fallback() {
446
+ return "";
447
+ });
448
+ const sentinel = res.status === 422 ? ErrUnsupportedPci : httpSentinel(res.status, text);
449
+ log?.error("transport http error", {
450
+ status: res.status
451
+ });
452
+ return { ok: false, error: errors2.wrap(sentinel, text) };
453
+ }
454
+ const jsonResult = await res.json().then(function ok(data) {
455
+ return { ok: true, data };
456
+ }, function fail(err) {
457
+ return { ok: false, error: err };
458
+ });
459
+ if (!jsonResult.ok) {
460
+ log?.error("transport json parse failed", {
461
+ error: jsonResult.error
462
+ });
463
+ return { ok: false, error: errors2.wrap(ErrJsonParse, jsonResult.error.message) };
464
+ }
465
+ log?.debug("transport success", {
466
+ intentKind: body.intent.kind
467
+ });
468
+ return { ok: true, data: jsonResult.data };
469
+ }
470
+ return transport;
471
+ }
472
+
473
+ // src/client/session.ts
474
+ import * as errors9 from "@superbuilders/errors";
475
+
476
+ // src/client/consumed.ts
477
+ function poisonToJSON() {
478
+ throw ErrNotSerializable;
479
+ }
480
+
481
+ // src/client/choice-state.ts
482
+ import * as errors3 from "@superbuilders/errors";
483
+ function choiceState(ctx, stimulus, interaction, options, maxChoices, minChoices) {
484
+ let submitPending;
485
+ let submitKey;
486
+ let timeoutPending;
487
+ function beginSubmit(selectedKeys) {
488
+ const submission = { type: "choice", selectedKeys };
489
+ const key = JSON.stringify(submission);
490
+ if (timeoutPending) {
491
+ return Promise.resolve(ctx.errored(errors3.wrap(ErrConflict, "cannot submit while timeout is in flight"), "interaction", { kind: "interaction", submission }));
492
+ }
493
+ if (submitPending) {
494
+ if (submitKey === key) {
495
+ return submitPending;
496
+ }
497
+ return Promise.resolve(ctx.errored(errors3.wrap(ErrConflict, "cannot submit a different choice payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
498
+ }
499
+ const validation = validateSubmissionForInteraction(interaction, submission);
500
+ if (!validation.ok) {
501
+ ctx.log?.error("choice submit invalid", { selectedKeys, issues: validation.issues });
502
+ return Promise.resolve(ctx.errored(errors3.wrap(ErrInvalidSubmission, submissionValidationMessage(validation)), "interaction", { kind: "interaction", submission }));
503
+ }
504
+ submitKey = key;
505
+ submitPending = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
506
+ submitPending = undefined;
507
+ submitKey = undefined;
508
+ });
509
+ return submitPending;
510
+ }
511
+ function beginTimeout() {
512
+ const intent = { kind: "timeout" };
513
+ if (submitPending) {
514
+ return Promise.resolve(ctx.errored(errors3.wrap(ErrConflict, "cannot timeout while submission is in flight"), "timeout", intent));
515
+ }
516
+ if (timeoutPending) {
517
+ return timeoutPending;
518
+ }
519
+ timeoutPending = ctx.execute(intent, "timeout").finally(function clearPending() {
520
+ timeoutPending = undefined;
521
+ });
522
+ return timeoutPending;
523
+ }
524
+ return {
525
+ phase: "interaction",
526
+ kind: "choice",
527
+ stimulus,
528
+ interaction,
529
+ options,
530
+ maxChoices,
531
+ minChoices,
532
+ submitChoice: beginSubmit,
533
+ timeout: beginTimeout,
534
+ toJSON: poisonToJSON
535
+ };
536
+ }
537
+
538
+ // src/client/extended-text-state.ts
539
+ import * as errors4 from "@superbuilders/errors";
540
+ function extendedTextState(ctx, stimulus, interaction) {
541
+ if (interaction.cardinality === "single") {
542
+ let submitText = function(value) {
543
+ const submission = { type: "extended-text", values: [value] };
544
+ const key = JSON.stringify(submission);
545
+ if (timeoutPending2) {
546
+ return Promise.resolve(ctx.errored(errors4.wrap(ErrConflict, "cannot submit while timeout is in flight"), "interaction", { kind: "interaction", submission }));
547
+ }
548
+ if (submitPending2) {
549
+ if (submitKey2 === key) {
550
+ return submitPending2;
551
+ }
552
+ return Promise.resolve(ctx.errored(errors4.wrap(ErrConflict, "cannot submit a different extended-text payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
553
+ }
554
+ const validation = validateSubmissionForInteraction(interaction, submission);
555
+ if (!validation.ok) {
556
+ ctx.log?.error("extended-text submit invalid", { value, issues: validation.issues });
557
+ return Promise.resolve(ctx.errored(errors4.wrap(ErrInvalidSubmission, submissionValidationMessage(validation)), "interaction", { kind: "interaction", submission }));
558
+ }
559
+ submitKey2 = key;
560
+ submitPending2 = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
561
+ submitPending2 = undefined;
562
+ submitKey2 = undefined;
563
+ });
564
+ return submitPending2;
565
+ }, timeout2 = function() {
566
+ const intent = { kind: "timeout" };
567
+ if (submitPending2) {
568
+ return Promise.resolve(ctx.errored(errors4.wrap(ErrConflict, "cannot timeout while submission is in flight"), "timeout", intent));
569
+ }
570
+ if (timeoutPending2) {
571
+ return timeoutPending2;
572
+ }
573
+ timeoutPending2 = ctx.execute(intent, "timeout").finally(function clearPending() {
574
+ timeoutPending2 = undefined;
575
+ });
576
+ return timeoutPending2;
577
+ };
578
+ let submitPending2;
579
+ let submitKey2;
580
+ let timeoutPending2;
581
+ return {
582
+ phase: "interaction",
583
+ kind: "extended-text",
584
+ cardinality: "single",
585
+ stimulus,
586
+ interaction,
587
+ submitText,
588
+ timeout: timeout2,
589
+ toJSON: poisonToJSON
590
+ };
591
+ }
592
+ const multi = interaction;
593
+ let submitPending;
594
+ let submitKey;
595
+ let timeoutPending;
596
+ function submitTexts(values) {
597
+ const submission = { type: "extended-text", values };
598
+ const key = JSON.stringify(submission);
599
+ if (timeoutPending) {
600
+ return Promise.resolve(ctx.errored(errors4.wrap(ErrConflict, "cannot submit while timeout is in flight"), "interaction", { kind: "interaction", submission }));
601
+ }
602
+ if (submitPending) {
603
+ if (submitKey === key) {
604
+ return submitPending;
605
+ }
606
+ return Promise.resolve(ctx.errored(errors4.wrap(ErrConflict, "cannot submit a different extended-text payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
607
+ }
608
+ const validation = validateSubmissionForInteraction(multi, submission);
609
+ if (!validation.ok) {
610
+ ctx.log?.error("extended-text submit invalid", { values, issues: validation.issues });
611
+ return Promise.resolve(ctx.errored(errors4.wrap(ErrInvalidSubmission, submissionValidationMessage(validation)), "interaction", { kind: "interaction", submission }));
612
+ }
613
+ submitKey = key;
614
+ submitPending = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
615
+ submitPending = undefined;
616
+ submitKey = undefined;
617
+ });
618
+ return submitPending;
619
+ }
620
+ function timeout() {
621
+ const intent = { kind: "timeout" };
622
+ if (submitPending) {
623
+ return Promise.resolve(ctx.errored(errors4.wrap(ErrConflict, "cannot timeout while submission is in flight"), "timeout", intent));
624
+ }
625
+ if (timeoutPending) {
626
+ return timeoutPending;
627
+ }
628
+ timeoutPending = ctx.execute(intent, "timeout").finally(function clearPending() {
629
+ timeoutPending = undefined;
630
+ });
631
+ return timeoutPending;
632
+ }
633
+ return {
634
+ phase: "interaction",
635
+ kind: "extended-text",
636
+ cardinality: "multiple",
637
+ stimulus,
638
+ interaction: multi,
639
+ maxStrings: multi.maxStrings,
640
+ minStrings: multi.minStrings,
641
+ submitTexts,
642
+ timeout,
643
+ toJSON: poisonToJSON
644
+ };
645
+ }
646
+
647
+ // src/client/feedback-state.ts
648
+ function feedbackState(ctx, stimulus, interaction, submission, isCorrect, feedbackContent, review) {
649
+ let pending;
650
+ return {
651
+ phase: "feedback",
652
+ stimulus,
653
+ interaction,
654
+ submission,
655
+ isCorrect,
656
+ feedbackContent,
657
+ review,
658
+ advance: function advance() {
659
+ if (pending) {
660
+ return pending;
661
+ }
662
+ pending = ctx.execute({ kind: "observation" }, "observation");
663
+ return pending;
664
+ },
665
+ toJSON: poisonToJSON
666
+ };
667
+ }
668
+
669
+ // src/client/match-state.ts
670
+ import * as errors5 from "@superbuilders/errors";
671
+ function matchState(ctx, stimulus, interaction) {
672
+ let submitPending;
673
+ let submitKey;
674
+ let timeoutPending;
675
+ function submitMatch(pairs) {
676
+ const submission = { type: "match", pairs };
677
+ const key = JSON.stringify(submission);
678
+ if (timeoutPending) {
679
+ return Promise.resolve(ctx.errored(errors5.wrap(ErrConflict, "cannot submit while timeout is in flight"), "interaction", { kind: "interaction", submission }));
680
+ }
681
+ if (submitPending) {
682
+ if (submitKey === key) {
683
+ return submitPending;
684
+ }
685
+ return Promise.resolve(ctx.errored(errors5.wrap(ErrConflict, "cannot submit a different match payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
686
+ }
687
+ const validation = validateSubmissionForInteraction(interaction, submission);
688
+ if (!validation.ok) {
689
+ ctx.log?.error("match submit invalid", { pairs, issues: validation.issues });
690
+ return Promise.resolve(ctx.errored(errors5.wrap(ErrInvalidSubmission, submissionValidationMessage(validation)), "interaction", { kind: "interaction", submission }));
691
+ }
692
+ submitKey = key;
693
+ submitPending = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
694
+ submitPending = undefined;
695
+ submitKey = undefined;
696
+ });
697
+ return submitPending;
698
+ }
699
+ function timeout() {
700
+ const intent = { kind: "timeout" };
701
+ if (submitPending) {
702
+ return Promise.resolve(ctx.errored(errors5.wrap(ErrConflict, "cannot timeout while submission is in flight"), "timeout", intent));
703
+ }
704
+ if (timeoutPending) {
705
+ return timeoutPending;
706
+ }
707
+ timeoutPending = ctx.execute(intent, "timeout").finally(function clearPending() {
708
+ timeoutPending = undefined;
709
+ });
710
+ return timeoutPending;
711
+ }
712
+ return {
713
+ phase: "interaction",
714
+ kind: "match",
715
+ stimulus,
716
+ interaction,
717
+ sourceChoices: interaction.sourceChoices,
718
+ targetChoices: interaction.targetChoices,
719
+ minAssociations: interaction.minAssociations,
720
+ maxAssociations: interaction.maxAssociations,
721
+ submitMatch,
722
+ timeout,
723
+ toJSON: poisonToJSON
724
+ };
725
+ }
726
+
727
+ // src/client/observation-state.ts
728
+ function observationState(ctx, stimulus) {
729
+ let pending;
730
+ return {
731
+ phase: "observation",
732
+ stimulus,
733
+ advance: function advance() {
734
+ if (pending) {
735
+ return pending;
736
+ }
737
+ pending = ctx.execute({ kind: "observation" }, "observation");
738
+ return pending;
739
+ },
740
+ toJSON: poisonToJSON
741
+ };
742
+ }
743
+
744
+ // src/client/order-state.ts
745
+ import * as errors6 from "@superbuilders/errors";
746
+ function orderState(ctx, stimulus, interaction) {
747
+ let submitPending;
748
+ let submitKey;
749
+ let timeoutPending;
750
+ function submitOrder(orderedKeys) {
751
+ const submission = { type: "order", orderedKeys };
752
+ const key = JSON.stringify(submission);
753
+ if (timeoutPending) {
754
+ return Promise.resolve(ctx.errored(errors6.wrap(ErrConflict, "cannot submit while timeout is in flight"), "interaction", { kind: "interaction", submission }));
755
+ }
756
+ if (submitPending) {
757
+ if (submitKey === key) {
758
+ return submitPending;
759
+ }
760
+ return Promise.resolve(ctx.errored(errors6.wrap(ErrConflict, "cannot submit a different order payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
761
+ }
762
+ const validation = validateSubmissionForInteraction(interaction, submission);
763
+ if (!validation.ok) {
764
+ ctx.log?.error("order submit invalid", { orderedKeys, issues: validation.issues });
765
+ return Promise.resolve(ctx.errored(errors6.wrap(ErrInvalidSubmission, submissionValidationMessage(validation)), "interaction", { kind: "interaction", submission }));
766
+ }
767
+ submitKey = key;
768
+ submitPending = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
769
+ submitPending = undefined;
770
+ submitKey = undefined;
771
+ });
772
+ return submitPending;
773
+ }
774
+ function timeout() {
775
+ const intent = { kind: "timeout" };
776
+ if (submitPending) {
777
+ return Promise.resolve(ctx.errored(errors6.wrap(ErrConflict, "cannot timeout while submission is in flight"), "timeout", intent));
778
+ }
779
+ if (timeoutPending) {
780
+ return timeoutPending;
781
+ }
782
+ timeoutPending = ctx.execute(intent, "timeout").finally(function clearPending() {
783
+ timeoutPending = undefined;
784
+ });
785
+ return timeoutPending;
786
+ }
787
+ return {
788
+ phase: "interaction",
789
+ kind: "order",
790
+ stimulus,
791
+ interaction,
792
+ choices: interaction.choices,
793
+ minChoices: interaction.minChoices,
794
+ maxChoices: interaction.maxChoices,
795
+ submitOrder,
796
+ timeout,
797
+ toJSON: poisonToJSON
798
+ };
799
+ }
800
+
801
+ // src/client/pci-state.ts
802
+ import * as errors7 from "@superbuilders/errors";
803
+ function pciInteractionState(ctx, stimulus, interaction) {
804
+ let submitPending;
805
+ let submitKey;
806
+ let timeoutPending;
807
+ const { pciId, properties } = interaction;
808
+ function submit(value) {
809
+ const submission = { type: "portable-custom", pciId, value };
810
+ const key = JSON.stringify(submission);
811
+ if (timeoutPending) {
812
+ return Promise.resolve(ctx.errored(errors7.wrap(ErrConflict, "cannot submit while timeout is in flight"), "interaction", { kind: "interaction", submission }));
813
+ }
814
+ if (submitPending) {
815
+ if (submitKey === key) {
816
+ return submitPending;
817
+ }
818
+ return Promise.resolve(ctx.errored(errors7.wrap(ErrConflict, "cannot submit a different pci payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
819
+ }
820
+ ctx.log?.debug("pci submit", { pciId });
821
+ submitKey = key;
822
+ submitPending = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
823
+ submitPending = undefined;
824
+ submitKey = undefined;
825
+ });
826
+ return submitPending;
827
+ }
828
+ function timeout() {
829
+ const intent = { kind: "timeout" };
830
+ if (submitPending) {
831
+ return Promise.resolve(ctx.errored(errors7.wrap(ErrConflict, "cannot timeout while submission is in flight"), "timeout", intent));
832
+ }
833
+ if (timeoutPending) {
834
+ return timeoutPending;
835
+ }
836
+ ctx.log?.debug("pci timeout", { pciId });
837
+ timeoutPending = ctx.execute(intent, "timeout").finally(function clearPending() {
838
+ timeoutPending = undefined;
839
+ });
840
+ return timeoutPending;
841
+ }
842
+ return {
843
+ phase: "interaction",
844
+ kind: "portable-custom",
845
+ stimulus,
846
+ interaction,
847
+ pciId,
848
+ properties,
849
+ submit,
850
+ timeout,
851
+ toJSON: poisonToJSON
852
+ };
853
+ }
854
+
855
+ // src/client/text-entry-state.ts
856
+ import * as errors8 from "@superbuilders/errors";
857
+ function textEntryState(ctx, stimulus, interaction) {
858
+ let submitPending;
859
+ let submitKey;
860
+ let timeoutPending;
861
+ function submitText(value) {
862
+ const submission = { type: "text-entry", value };
863
+ const key = JSON.stringify(submission);
864
+ if (timeoutPending) {
865
+ return Promise.resolve(ctx.errored(errors8.wrap(ErrConflict, "cannot submit while timeout is in flight"), "interaction", { kind: "interaction", submission }));
866
+ }
867
+ if (submitPending) {
868
+ if (submitKey === key) {
869
+ return submitPending;
870
+ }
871
+ return Promise.resolve(ctx.errored(errors8.wrap(ErrConflict, "cannot submit a different text payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
872
+ }
873
+ const validation = validateSubmissionForInteraction(interaction, submission);
874
+ if (!validation.ok) {
875
+ ctx.log?.error("text-entry submit invalid", { value, issues: validation.issues });
876
+ return Promise.resolve(ctx.errored(errors8.wrap(ErrInvalidSubmission, submissionValidationMessage(validation)), "interaction", { kind: "interaction", submission }));
877
+ }
878
+ submitKey = key;
879
+ submitPending = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
880
+ submitPending = undefined;
881
+ submitKey = undefined;
882
+ });
883
+ return submitPending;
884
+ }
885
+ function timeout() {
886
+ const intent = { kind: "timeout" };
887
+ if (submitPending) {
888
+ return Promise.resolve(ctx.errored(errors8.wrap(ErrConflict, "cannot timeout while submission is in flight"), "timeout", intent));
889
+ }
890
+ if (timeoutPending) {
891
+ return timeoutPending;
892
+ }
893
+ timeoutPending = ctx.execute(intent, "timeout").finally(function clearPending() {
894
+ timeoutPending = undefined;
895
+ });
896
+ return timeoutPending;
897
+ }
898
+ return {
899
+ phase: "interaction",
900
+ kind: "text-entry",
901
+ stimulus,
902
+ interaction,
903
+ submitText,
904
+ timeout,
905
+ toJSON: poisonToJSON
906
+ };
907
+ }
908
+
909
+ // src/client/session.ts
910
+ var FATAL_SENTINELS = [
911
+ ErrBadRequest,
912
+ ErrInvalidAccessToken,
913
+ ErrTokenExpired,
914
+ ErrForbidden,
915
+ ErrNotFound,
916
+ ErrUnsupportedPci
917
+ ];
918
+ function isFatalError(err) {
919
+ for (const sentinel of FATAL_SENTINELS) {
920
+ if (errors9.is(err, sentinel)) {
921
+ return true;
922
+ }
923
+ }
924
+ return false;
925
+ }
926
+ function isRetriableError(err) {
927
+ return !errors9.is(err, ErrInvalidSubmission);
928
+ }
929
+ function makeSession(sc) {
930
+ const log = sc.log;
931
+ function resolve(result) {
932
+ switch (result.outcome) {
933
+ case "advanced":
934
+ return fromAdvanced(result.stimulus, result.interaction);
935
+ case "submitted":
936
+ return feedbackState(ctx, result.stimulus, result.interaction, result.submission, result.isCorrect, result.feedbackContent, result.review);
937
+ case "completed":
938
+ return { phase: "completed", toJSON: poisonToJSON };
939
+ }
940
+ }
941
+ function errored(error, failedPhase, intent) {
942
+ let pending;
943
+ const retriable = isRetriableError(error);
944
+ const state = {
945
+ phase: "errored",
946
+ error,
947
+ retriable,
948
+ retry: function retry() {
949
+ if (!retriable) {
950
+ log?.debug("retry ignored for non-retriable error", { failedPhase });
951
+ return Promise.resolve(state);
952
+ }
953
+ if (pending) {
954
+ return pending;
955
+ }
956
+ log?.debug("retrying from errored state", { failedPhase });
957
+ pending = execute(intent, failedPhase);
958
+ return pending;
959
+ },
960
+ toJSON: poisonToJSON
961
+ };
962
+ return state;
963
+ }
964
+ async function execute(intent, phase) {
965
+ const body = {
966
+ supportedPcis: sc.supportedPcis,
967
+ intent,
968
+ subject: sc.subject
969
+ };
970
+ const result = await sc.transport(body);
971
+ if (!result.ok) {
972
+ if (isFatalError(result.error)) {
973
+ log?.error("fatal transport error", { error: result.error, phase });
974
+ return { phase: "fatal", error: result.error, retriable: false, toJSON: poisonToJSON };
975
+ }
976
+ return errored(result.error, phase, intent);
977
+ }
978
+ return resolve(result.data);
979
+ }
980
+ function isPciSupported(pciId) {
981
+ for (const id of sc.supportedPcis) {
982
+ if (id === pciId) {
983
+ return true;
984
+ }
985
+ }
986
+ return false;
987
+ }
988
+ function fromAdvanced(stimulus, interaction) {
989
+ if (interaction === null) {
990
+ return observationState(ctx, stimulus);
991
+ }
992
+ if (interaction.type === "portable-custom") {
993
+ if (!isPciSupported(interaction.pciId)) {
994
+ log?.error("unsupported pci in frame", { pciId: interaction.pciId });
995
+ return {
996
+ phase: "fatal",
997
+ error: errors9.wrap(ErrUnsupportedPci, `pci '${interaction.pciId}'`),
998
+ retriable: false,
999
+ toJSON: poisonToJSON
1000
+ };
1001
+ }
1002
+ }
1003
+ return pendingInteractionState(stimulus, interaction);
1004
+ }
1005
+ function pendingInteractionState(stimulus, interaction) {
1006
+ switch (interaction.type) {
1007
+ case "choice":
1008
+ return choiceState(ctx, stimulus, interaction, interaction.options, interaction.maxChoices, interaction.minChoices);
1009
+ case "text-entry":
1010
+ return textEntryState(ctx, stimulus, interaction);
1011
+ case "extended-text":
1012
+ return extendedTextState(ctx, stimulus, interaction);
1013
+ case "order":
1014
+ return orderState(ctx, stimulus, interaction);
1015
+ case "match":
1016
+ return matchState(ctx, stimulus, interaction);
1017
+ case "portable-custom":
1018
+ return pciInteractionState(ctx, stimulus, interaction);
1019
+ }
1020
+ }
1021
+ const ctx = { log, execute, errored };
1022
+ return { execute };
1023
+ }
1024
+
1025
+ // src/client/create.ts
1026
+ var ACCESS_TOKEN_PREFIX = "eyJ";
1027
+ function isMalformedJws(token) {
1028
+ if (!token.startsWith(ACCESS_TOKEN_PREFIX)) {
1029
+ return true;
1030
+ }
1031
+ const dotCount = token.split(".").length - 1;
1032
+ return dotCount !== 2;
1033
+ }
1034
+ function create(config) {
1035
+ const log = config.logger;
1036
+ if (isMalformedJws(config.accessToken)) {
1037
+ log?.error("malformed access token", { prefix: ACCESS_TOKEN_PREFIX });
1038
+ throw errors10.wrap(ErrMalformedAccessToken, `token must start with '${ACCESS_TOKEN_PREFIX}' and contain two dots`);
1039
+ }
1040
+ const transport = createTransport({
1041
+ accessToken: config.accessToken,
1042
+ supportedPcis: config.supportedPcis,
1043
+ origin: config.origin,
1044
+ fetch: config.fetch,
1045
+ abort: config.abort,
1046
+ log
1047
+ });
1048
+ let startPromise;
1049
+ async function doStart() {
1050
+ log?.debug("start", { subject: config.subject });
1051
+ const s = makeSession({
1052
+ supportedPcis: config.supportedPcis,
1053
+ subject: config.subject,
1054
+ log,
1055
+ transport
1056
+ });
1057
+ return s.execute({ kind: "observation" }, "observation");
1058
+ }
1059
+ return {
1060
+ start() {
1061
+ if (startPromise) {
1062
+ log?.debug("start already called");
1063
+ return startPromise;
1064
+ }
1065
+ startPromise = doStart();
1066
+ return startPromise;
1067
+ }
1068
+ };
1069
+ }
1070
+ export {
1071
+ create
1072
+ };
1073
+
1074
+ //# debugId=FDB6BAAD7743405564756E2164756E21