@superbuilders/primer-tives 1.0.0 → 1.1.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.
@@ -1,6 +1,3 @@
1
- // src/client/create.ts
2
- import * as errors10 from "@superbuilders/errors";
3
-
4
1
  // src/errors.ts
5
2
  import * as errors from "@superbuilders/errors";
6
3
  var ErrNetwork = errors.new("network");
@@ -24,15 +21,323 @@ var ErrInvalidSecretKey = errors.new("invalid secret key");
24
21
  var ErrStudentNotFound = errors.new("student not found");
25
22
  var ErrUnsupportedGrade = errors.new("unsupported grade");
26
23
  var ErrTimebackUnavailable = errors.new("timeback unavailable");
24
+ // src/contracts/validation.ts
25
+ import { z as z2 } from "zod";
26
+
27
+ // src/contracts/pci-schemas.ts
28
+ import { z } from "zod";
29
+ var DivisionRemainderPropsSchema = z.object({
30
+ dividend: z.number(),
31
+ divisor: z.number()
32
+ });
33
+ var DivisionRemainderSubmissionSchema = z.object({
34
+ quotient: z.string(),
35
+ remainder: z.string()
36
+ });
37
+ var FractionOperandSchema = z.object({
38
+ numerator: z.number(),
39
+ denominator: z.number()
40
+ });
41
+ var FractionAdditionPropsSchema = z.object({
42
+ left: FractionOperandSchema,
43
+ right: FractionOperandSchema
44
+ });
45
+ var FractionAdditionSubmissionSchema = z.object({
46
+ numerator: z.string(),
47
+ denominator: z.string()
48
+ });
49
+
50
+ // src/contracts/validation.ts
51
+ var MatchPairSchema = z2.object({
52
+ source: z2.string(),
53
+ target: z2.string()
54
+ });
55
+ var ChoiceSubmissionSchema = z2.object({
56
+ type: z2.literal("choice"),
57
+ selectedKeys: z2.array(z2.string())
58
+ });
59
+ var TextEntrySubmissionSchema = z2.object({
60
+ type: z2.literal("text-entry"),
61
+ value: z2.string()
62
+ });
63
+ var ExtendedTextSubmissionSchema = z2.object({
64
+ type: z2.literal("extended-text"),
65
+ values: z2.array(z2.string()).min(1)
66
+ });
67
+ var OrderSubmissionSchema = z2.object({
68
+ type: z2.literal("order"),
69
+ orderedKeys: z2.array(z2.string())
70
+ });
71
+ var MatchSubmissionSchema = z2.object({
72
+ type: z2.literal("match"),
73
+ pairs: z2.array(MatchPairSchema)
74
+ });
75
+ var DivisionRemainderPciSubmissionSchema = z2.object({
76
+ type: z2.literal("portable-custom"),
77
+ pciId: z2.literal("urn:primer:pci:division-remainder"),
78
+ value: DivisionRemainderSubmissionSchema
79
+ });
80
+ var FractionAdditionPciSubmissionSchema = z2.object({
81
+ type: z2.literal("portable-custom"),
82
+ pciId: z2.literal("urn:primer:pci:fraction-addition"),
83
+ value: FractionAdditionSubmissionSchema
84
+ });
85
+ var RendererSubmissionSchema = z2.union([
86
+ ChoiceSubmissionSchema,
87
+ TextEntrySubmissionSchema,
88
+ ExtendedTextSubmissionSchema,
89
+ OrderSubmissionSchema,
90
+ MatchSubmissionSchema,
91
+ DivisionRemainderPciSubmissionSchema,
92
+ FractionAdditionPciSubmissionSchema
93
+ ]);
94
+ function invalid(issues) {
95
+ return { ok: false, issues: Array.isArray(issues) ? issues : [issues] };
96
+ }
97
+ function valid(submission) {
98
+ return {
99
+ ok: true,
100
+ value: submission
101
+ };
102
+ }
103
+ function duplicates(values, keyOf) {
104
+ const seen = new Set;
105
+ const duplicated = new Set;
106
+ for (const value of values) {
107
+ const key = keyOf(value);
108
+ if (seen.has(key)) {
109
+ duplicated.add(key);
110
+ continue;
111
+ }
112
+ seen.add(key);
113
+ }
114
+ return [...duplicated];
115
+ }
116
+ function findUnknownIds(values, choices) {
117
+ const ids = new Set(choices.map(function getId(choice) {
118
+ return choice.identifier;
119
+ }));
120
+ return values.filter(function isUnknown(value) {
121
+ return !ids.has(value);
122
+ });
123
+ }
124
+ function countByIdentifier(pairs, side) {
125
+ const counts = new Map;
126
+ for (const pair of pairs) {
127
+ const key = pair[side];
128
+ counts.set(key, (counts.get(key) ?? 0) + 1);
129
+ }
130
+ return counts;
131
+ }
132
+ function validateUsageBounds(choices, counts, side) {
133
+ const issues = [];
134
+ for (const choice of choices) {
135
+ const count = counts.get(choice.identifier) ?? 0;
136
+ if (choice.matchMax !== 0 && count > choice.matchMax) {
137
+ issues.push(`${side} '${choice.identifier}' used ${count} times, max ${choice.matchMax}`);
138
+ }
139
+ if (count < choice.matchMin) {
140
+ issues.push(`${side} '${choice.identifier}' used ${count} times, min ${choice.matchMin}`);
141
+ }
142
+ }
143
+ return issues;
144
+ }
145
+ function pciSubmissionSchema(pciId) {
146
+ switch (pciId) {
147
+ case "urn:primer:pci:division-remainder":
148
+ return DivisionRemainderSubmissionSchema;
149
+ case "urn:primer:pci:fraction-addition":
150
+ return FractionAdditionSubmissionSchema;
151
+ }
152
+ }
153
+ function validateChoiceSubmission(interaction, submission) {
154
+ if (submission.type !== "choice") {
155
+ return invalid(`submission type '${submission.type}' does not match interaction type 'choice'`);
156
+ }
157
+ const issues = [];
158
+ if (submission.selectedKeys.length < interaction.minChoices) {
159
+ issues.push(`need at least ${interaction.minChoices} selections`);
160
+ }
161
+ if (submission.selectedKeys.length > interaction.maxChoices) {
162
+ issues.push(`at most ${interaction.maxChoices} selections`);
163
+ }
164
+ const duplicateKeys = duplicates(submission.selectedKeys, function keyOf(value) {
165
+ return value;
166
+ });
167
+ if (duplicateKeys.length > 0) {
168
+ issues.push("duplicate selections");
169
+ }
170
+ const unknownKeys = findUnknownIds(submission.selectedKeys, interaction.options);
171
+ if (unknownKeys.length > 0) {
172
+ issues.push(`unknown options: ${unknownKeys.join(", ")}`);
173
+ }
174
+ if (issues.length > 0) {
175
+ return invalid(issues);
176
+ }
177
+ return valid(submission);
178
+ }
179
+ function validateTextEntrySubmission(interaction, submission) {
180
+ if (submission.type !== "text-entry") {
181
+ return invalid(`submission type '${submission.type}' does not match interaction type 'text-entry'`);
182
+ }
183
+ return valid(submission);
184
+ }
185
+ function validateExtendedTextSubmission(interaction, submission) {
186
+ if (submission.type !== "extended-text") {
187
+ return invalid(`submission type '${submission.type}' does not match interaction type 'extended-text'`);
188
+ }
189
+ const issues = [];
190
+ if (interaction.cardinality === "single") {
191
+ if (submission.values.length !== 1) {
192
+ issues.push("single-cardinality extended-text requires exactly one value");
193
+ }
194
+ } else {
195
+ if (submission.values.length < interaction.minStrings) {
196
+ issues.push(`need at least ${interaction.minStrings} values`);
197
+ }
198
+ if (submission.values.length > interaction.maxStrings) {
199
+ issues.push(`at most ${interaction.maxStrings} values`);
200
+ }
201
+ const duplicateValues = duplicates(submission.values, function keyOf(value) {
202
+ return value;
203
+ });
204
+ if (duplicateValues.length > 0) {
205
+ issues.push("duplicate values are not allowed for multiple-cardinality extended-text");
206
+ }
207
+ }
208
+ if (issues.length > 0) {
209
+ return invalid(issues);
210
+ }
211
+ return valid(submission);
212
+ }
213
+ function validateOrderSubmission(interaction, submission) {
214
+ if (submission.type !== "order") {
215
+ return invalid(`submission type '${submission.type}' does not match interaction type 'order'`);
216
+ }
217
+ const issues = [];
218
+ if (submission.orderedKeys.length < interaction.minChoices) {
219
+ issues.push(`need at least ${interaction.minChoices} selections`);
220
+ }
221
+ if (submission.orderedKeys.length > interaction.maxChoices) {
222
+ issues.push(`at most ${interaction.maxChoices} selections`);
223
+ }
224
+ const duplicateKeys = duplicates(submission.orderedKeys, function keyOf(value) {
225
+ return value;
226
+ });
227
+ if (duplicateKeys.length > 0) {
228
+ issues.push("duplicate selections");
229
+ }
230
+ const unknownKeys = findUnknownIds(submission.orderedKeys, interaction.choices);
231
+ if (unknownKeys.length > 0) {
232
+ issues.push(`unknown choices: ${unknownKeys.join(", ")}`);
233
+ }
234
+ if (issues.length > 0) {
235
+ return invalid(issues);
236
+ }
237
+ return valid(submission);
238
+ }
239
+ function validateMatchSubmission(interaction, submission) {
240
+ if (submission.type !== "match") {
241
+ return invalid(`submission type '${submission.type}' does not match interaction type 'match'`);
242
+ }
243
+ const issues = [];
244
+ if (submission.pairs.length < interaction.minAssociations) {
245
+ issues.push(`need at least ${interaction.minAssociations} associations`);
246
+ }
247
+ if (submission.pairs.length > interaction.maxAssociations) {
248
+ issues.push(`at most ${interaction.maxAssociations} associations`);
249
+ }
250
+ const duplicatePairs = duplicates(submission.pairs, function keyOf(pair) {
251
+ return `${pair.source}->${pair.target}`;
252
+ });
253
+ if (duplicatePairs.length > 0) {
254
+ issues.push("duplicate associations are not allowed");
255
+ }
256
+ const sourceIds = new Set(interaction.sourceChoices.map(function getId(choice) {
257
+ return choice.identifier;
258
+ }));
259
+ const targetIds = new Set(interaction.targetChoices.map(function getId(choice) {
260
+ return choice.identifier;
261
+ }));
262
+ for (const pair of submission.pairs) {
263
+ if (!sourceIds.has(pair.source)) {
264
+ issues.push(`unknown source '${pair.source}'`);
265
+ }
266
+ if (!targetIds.has(pair.target)) {
267
+ issues.push(`unknown target '${pair.target}'`);
268
+ }
269
+ }
270
+ const sourceCounts = countByIdentifier(submission.pairs, "source");
271
+ const targetCounts = countByIdentifier(submission.pairs, "target");
272
+ issues.push(...validateUsageBounds(interaction.sourceChoices, sourceCounts, "source"));
273
+ issues.push(...validateUsageBounds(interaction.targetChoices, targetCounts, "target"));
274
+ if (issues.length > 0) {
275
+ return invalid(issues);
276
+ }
277
+ return valid(submission);
278
+ }
279
+ function validatePortableCustomSubmission(interaction, submission) {
280
+ if (submission.type !== "portable-custom") {
281
+ return invalid(`submission type '${submission.type}' does not match interaction type 'portable-custom'`);
282
+ }
283
+ if (submission.pciId !== interaction.pciId) {
284
+ return invalid(`submission PCI '${submission.pciId}' does not match interaction PCI '${interaction.pciId}'`);
285
+ }
286
+ const schema = pciSubmissionSchema(interaction.pciId);
287
+ const result = schema.safeParse(submission.value);
288
+ if (!result.success) {
289
+ return invalid(result.error.issues.map(function toIssue(issue) {
290
+ return issue.message;
291
+ }));
292
+ }
293
+ return valid(submission);
294
+ }
295
+ function validateSubmissionForInteraction(interaction, submission) {
296
+ switch (interaction.type) {
297
+ case "choice":
298
+ return validateChoiceSubmission(interaction, submission);
299
+ case "text-entry":
300
+ return validateTextEntrySubmission(interaction, submission);
301
+ case "extended-text":
302
+ return validateExtendedTextSubmission(interaction, submission);
303
+ case "order":
304
+ return validateOrderSubmission(interaction, submission);
305
+ case "match":
306
+ return validateMatchSubmission(interaction, submission);
307
+ case "portable-custom":
308
+ return validatePortableCustomSubmission(interaction, submission);
309
+ }
310
+ }
311
+ function submissionValidationMessage(result) {
312
+ return result.issues.join("; ");
313
+ }
314
+ // src/client/create.ts
315
+ import * as errors10 from "@superbuilders/errors";
27
316
 
28
317
  // src/client/transport.ts
29
318
  import * as errors2 from "@superbuilders/errors";
30
319
  var ADVANCE_PATH = "/api/v0/advance";
31
- function httpSentinel(status) {
320
+ function parseAdvanceErrorBody(body) {
321
+ if (body.length === 0) {
322
+ return null;
323
+ }
324
+ const parsed = errors2.trySync(function parseJson() {
325
+ return JSON.parse(body);
326
+ });
327
+ if (parsed.error) {
328
+ return null;
329
+ }
330
+ return parsed.data;
331
+ }
332
+ function httpSentinel(status, body) {
32
333
  if (status === 400) {
33
334
  return ErrBadRequest;
34
335
  }
35
336
  if (status === 401) {
337
+ const parsed = parseAdvanceErrorBody(body);
338
+ if (parsed?.detail === "token_expired") {
339
+ return ErrTokenExpired;
340
+ }
36
341
  return ErrInvalidAccessToken;
37
342
  }
38
343
  if (status === 403) {
@@ -106,7 +411,7 @@ function createTransport(tc) {
106
411
  const text = await res.text().catch(function fallback() {
107
412
  return "";
108
413
  });
109
- const sentinel = res.status === 422 ? ErrUnsupportedPci : httpSentinel(res.status);
414
+ const sentinel = res.status === 422 ? ErrUnsupportedPci : httpSentinel(res.status, text);
110
415
  log?.error("transport http error", {
111
416
  status: res.status
112
417
  });
@@ -141,38 +446,6 @@ function poisonToJSON() {
141
446
 
142
447
  // src/client/choice-state.ts
143
448
  import * as errors3 from "@superbuilders/errors";
144
- function findUnknownKey(selectedKeys, options) {
145
- const optionIds = new Set(options.map(function getId(o) {
146
- return o.identifier;
147
- }));
148
- for (const key of selectedKeys) {
149
- if (!optionIds.has(key)) {
150
- return key;
151
- }
152
- }
153
- return null;
154
- }
155
- function validateChoiceSubmission(log, selectedKeys, options, maxChoices, minChoices) {
156
- if (selectedKeys.length < minChoices) {
157
- log?.error("choice submit below minChoices", { selectedKeys, minChoices });
158
- return errors3.wrap(ErrInvalidSubmission, `need at least ${minChoices} selections`);
159
- }
160
- if (selectedKeys.length > maxChoices) {
161
- log?.error("choice submit above maxChoices", { selectedKeys, maxChoices });
162
- return errors3.wrap(ErrInvalidSubmission, `at most ${maxChoices} selections`);
163
- }
164
- const uniqueKeys = new Set(selectedKeys);
165
- if (uniqueKeys.size !== selectedKeys.length) {
166
- log?.error("choice submit has duplicates", { selectedKeys });
167
- return errors3.wrap(ErrInvalidSubmission, "duplicate selections");
168
- }
169
- const unknownKey = findUnknownKey(selectedKeys, options);
170
- if (unknownKey) {
171
- log?.error("choice submit has unknown key", { key: unknownKey });
172
- return errors3.wrap(ErrInvalidSubmission, `unknown option '${unknownKey}'`);
173
- }
174
- return null;
175
- }
176
449
  function choiceState(ctx, stimulus, interaction, options, maxChoices, minChoices) {
177
450
  let submitPending;
178
451
  let submitKey;
@@ -189,9 +462,10 @@ function choiceState(ctx, stimulus, interaction, options, maxChoices, minChoices
189
462
  }
190
463
  return Promise.resolve(ctx.errored(errors3.wrap(ErrConflict, "cannot submit a different choice payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
191
464
  }
192
- const validationError = validateChoiceSubmission(ctx.log, selectedKeys, options, maxChoices, minChoices);
193
- if (validationError) {
194
- return Promise.resolve(ctx.errored(validationError, "interaction", { kind: "interaction", submission }));
465
+ const validation = validateSubmissionForInteraction(interaction, submission);
466
+ if (!validation.ok) {
467
+ ctx.log?.error("choice submit invalid", { selectedKeys, issues: validation.issues });
468
+ return Promise.resolve(ctx.errored(errors3.wrap(ErrInvalidSubmission, submissionValidationMessage(validation)), "interaction", { kind: "interaction", submission }));
195
469
  }
196
470
  submitKey = key;
197
471
  submitPending = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
@@ -229,17 +503,6 @@ function choiceState(ctx, stimulus, interaction, options, maxChoices, minChoices
229
503
 
230
504
  // src/client/extended-text-state.ts
231
505
  import * as errors4 from "@superbuilders/errors";
232
- function validateExtendedTextSubmission(log, values, minStrings, maxStrings) {
233
- if (values.length < minStrings) {
234
- log?.error("extended-text submit below minStrings", { count: values.length, minStrings });
235
- return errors4.wrap(ErrInvalidSubmission, `need at least ${minStrings} values`);
236
- }
237
- if (values.length > maxStrings) {
238
- log?.error("extended-text submit above maxStrings", { count: values.length, maxStrings });
239
- return errors4.wrap(ErrInvalidSubmission, `at most ${maxStrings} values`);
240
- }
241
- return null;
242
- }
243
506
  function extendedTextState(ctx, stimulus, interaction) {
244
507
  if (interaction.cardinality === "single") {
245
508
  let submitText = function(value) {
@@ -254,6 +517,11 @@ function extendedTextState(ctx, stimulus, interaction) {
254
517
  }
255
518
  return Promise.resolve(ctx.errored(errors4.wrap(ErrConflict, "cannot submit a different extended-text payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
256
519
  }
520
+ const validation = validateSubmissionForInteraction(interaction, submission);
521
+ if (!validation.ok) {
522
+ ctx.log?.error("extended-text submit invalid", { value, issues: validation.issues });
523
+ return Promise.resolve(ctx.errored(errors4.wrap(ErrInvalidSubmission, submissionValidationMessage(validation)), "interaction", { kind: "interaction", submission }));
524
+ }
257
525
  submitKey2 = key;
258
526
  submitPending2 = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
259
527
  submitPending2 = undefined;
@@ -303,9 +571,10 @@ function extendedTextState(ctx, stimulus, interaction) {
303
571
  }
304
572
  return Promise.resolve(ctx.errored(errors4.wrap(ErrConflict, "cannot submit a different extended-text payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
305
573
  }
306
- const validationError = validateExtendedTextSubmission(ctx.log, values, multi.minStrings, multi.maxStrings);
307
- if (validationError) {
308
- return Promise.resolve(ctx.errored(validationError, "interaction", { kind: "interaction", submission }));
574
+ const validation = validateSubmissionForInteraction(multi, submission);
575
+ if (!validation.ok) {
576
+ ctx.log?.error("extended-text submit invalid", { values, issues: validation.issues });
577
+ return Promise.resolve(ctx.errored(errors4.wrap(ErrInvalidSubmission, submissionValidationMessage(validation)), "interaction", { kind: "interaction", submission }));
309
578
  }
310
579
  submitKey = key;
311
580
  submitPending = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
@@ -365,92 +634,6 @@ function feedbackState(ctx, stimulus, interaction, submission, isCorrect, feedba
365
634
 
366
635
  // src/client/match-state.ts
367
636
  import * as errors5 from "@superbuilders/errors";
368
- function bumpCount(counts, key) {
369
- const prev = counts.get(key);
370
- const next = prev === undefined ? 1 : prev + 1;
371
- counts.set(key, next);
372
- }
373
- function getCount(counts, key) {
374
- const recorded = counts.get(key);
375
- return recorded === undefined ? 0 : recorded;
376
- }
377
- function validateAssociationBounds(log, pairs, minAssociations, maxAssociations) {
378
- if (pairs.length < minAssociations) {
379
- log?.error("match submit below minAssociations", { pairs, minAssociations });
380
- return errors5.wrap(ErrInvalidSubmission, `need at least ${minAssociations} associations`);
381
- }
382
- if (pairs.length > maxAssociations) {
383
- log?.error("match submit above maxAssociations", { pairs, maxAssociations });
384
- return errors5.wrap(ErrInvalidSubmission, `at most ${maxAssociations} associations`);
385
- }
386
- return null;
387
- }
388
- function validatePairsReferToKnownChoices(log, pairs, sourceIds, targetIds) {
389
- for (const pair of pairs) {
390
- if (!sourceIds.has(pair.source)) {
391
- log?.error("match submit has unknown source", { source: pair.source });
392
- return errors5.wrap(ErrInvalidSubmission, `unknown source '${pair.source}'`);
393
- }
394
- if (!targetIds.has(pair.target)) {
395
- log?.error("match submit has unknown target", { target: pair.target });
396
- return errors5.wrap(ErrInvalidSubmission, `unknown target '${pair.target}'`);
397
- }
398
- }
399
- return null;
400
- }
401
- function validateChoiceCaps(log, side, choices, counts) {
402
- for (const choice of choices) {
403
- const count = getCount(counts, choice.identifier);
404
- if (choice.matchMax !== 0 && count > choice.matchMax) {
405
- log?.error(`match submit exceeds ${side} matchMax`, {
406
- identifier: choice.identifier,
407
- count,
408
- matchMax: choice.matchMax
409
- });
410
- return errors5.wrap(ErrInvalidSubmission, `${side} '${choice.identifier}' used ${count} times, max ${choice.matchMax}`);
411
- }
412
- if (count < choice.matchMin) {
413
- log?.error(`match submit below ${side} matchMin`, {
414
- identifier: choice.identifier,
415
- count,
416
- matchMin: choice.matchMin
417
- });
418
- return errors5.wrap(ErrInvalidSubmission, `${side} '${choice.identifier}' used ${count} times, min ${choice.matchMin}`);
419
- }
420
- }
421
- return null;
422
- }
423
- function validateMatchSubmission(log, pairs, sourceChoices, targetChoices, minAssociations, maxAssociations) {
424
- const boundsError = validateAssociationBounds(log, pairs, minAssociations, maxAssociations);
425
- if (boundsError) {
426
- return boundsError;
427
- }
428
- const sourceIds = new Set(sourceChoices.map(function getId(c) {
429
- return c.identifier;
430
- }));
431
- const targetIds = new Set(targetChoices.map(function getId(c) {
432
- return c.identifier;
433
- }));
434
- const referenceError = validatePairsReferToKnownChoices(log, pairs, sourceIds, targetIds);
435
- if (referenceError) {
436
- return referenceError;
437
- }
438
- const sourceCounts = new Map;
439
- const targetCounts = new Map;
440
- for (const pair of pairs) {
441
- bumpCount(sourceCounts, pair.source);
442
- bumpCount(targetCounts, pair.target);
443
- }
444
- const sourceCapError = validateChoiceCaps(log, "source", sourceChoices, sourceCounts);
445
- if (sourceCapError) {
446
- return sourceCapError;
447
- }
448
- const targetCapError = validateChoiceCaps(log, "target", targetChoices, targetCounts);
449
- if (targetCapError) {
450
- return targetCapError;
451
- }
452
- return null;
453
- }
454
637
  function matchState(ctx, stimulus, interaction) {
455
638
  let submitPending;
456
639
  let submitKey;
@@ -467,9 +650,10 @@ function matchState(ctx, stimulus, interaction) {
467
650
  }
468
651
  return Promise.resolve(ctx.errored(errors5.wrap(ErrConflict, "cannot submit a different match payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
469
652
  }
470
- const validationError = validateMatchSubmission(ctx.log, pairs, interaction.sourceChoices, interaction.targetChoices, interaction.minAssociations, interaction.maxAssociations);
471
- if (validationError) {
472
- return Promise.resolve(ctx.errored(validationError, "interaction", { kind: "interaction", submission }));
653
+ const validation = validateSubmissionForInteraction(interaction, submission);
654
+ if (!validation.ok) {
655
+ ctx.log?.error("match submit invalid", { pairs, issues: validation.issues });
656
+ return Promise.resolve(ctx.errored(errors5.wrap(ErrInvalidSubmission, submissionValidationMessage(validation)), "interaction", { kind: "interaction", submission }));
473
657
  }
474
658
  submitKey = key;
475
659
  submitPending = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
@@ -525,38 +709,6 @@ function observationState(ctx, stimulus) {
525
709
 
526
710
  // src/client/order-state.ts
527
711
  import * as errors6 from "@superbuilders/errors";
528
- function findUnknownKey2(orderedKeys, choices) {
529
- const choiceIds = new Set(choices.map(function getId(c) {
530
- return c.identifier;
531
- }));
532
- for (const key of orderedKeys) {
533
- if (!choiceIds.has(key)) {
534
- return key;
535
- }
536
- }
537
- return null;
538
- }
539
- function validateOrderSubmission(log, orderedKeys, choices, minChoices, maxChoices) {
540
- if (orderedKeys.length < minChoices) {
541
- log?.error("order submit below minChoices", { orderedKeys, minChoices });
542
- return errors6.wrap(ErrInvalidSubmission, `need at least ${minChoices} selections`);
543
- }
544
- if (orderedKeys.length > maxChoices) {
545
- log?.error("order submit above maxChoices", { orderedKeys, maxChoices });
546
- return errors6.wrap(ErrInvalidSubmission, `at most ${maxChoices} selections`);
547
- }
548
- const uniqueKeys = new Set(orderedKeys);
549
- if (uniqueKeys.size !== orderedKeys.length) {
550
- log?.error("order submit has duplicates", { orderedKeys });
551
- return errors6.wrap(ErrInvalidSubmission, "duplicate selections");
552
- }
553
- const unknownKey = findUnknownKey2(orderedKeys, choices);
554
- if (unknownKey) {
555
- log?.error("order submit has unknown key", { key: unknownKey });
556
- return errors6.wrap(ErrInvalidSubmission, `unknown choice '${unknownKey}'`);
557
- }
558
- return null;
559
- }
560
712
  function orderState(ctx, stimulus, interaction) {
561
713
  let submitPending;
562
714
  let submitKey;
@@ -573,9 +725,10 @@ function orderState(ctx, stimulus, interaction) {
573
725
  }
574
726
  return Promise.resolve(ctx.errored(errors6.wrap(ErrConflict, "cannot submit a different order payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
575
727
  }
576
- const validationError = validateOrderSubmission(ctx.log, orderedKeys, interaction.choices, interaction.minChoices, interaction.maxChoices);
577
- if (validationError) {
578
- return Promise.resolve(ctx.errored(validationError, "interaction", { kind: "interaction", submission }));
728
+ const validation = validateSubmissionForInteraction(interaction, submission);
729
+ if (!validation.ok) {
730
+ ctx.log?.error("order submit invalid", { orderedKeys, issues: validation.issues });
731
+ return Promise.resolve(ctx.errored(errors6.wrap(ErrInvalidSubmission, submissionValidationMessage(validation)), "interaction", { kind: "interaction", submission }));
579
732
  }
580
733
  submitKey = key;
581
734
  submitPending = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
@@ -683,6 +836,11 @@ function textEntryState(ctx, stimulus, interaction) {
683
836
  }
684
837
  return Promise.resolve(ctx.errored(errors8.wrap(ErrConflict, "cannot submit a different text payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
685
838
  }
839
+ const validation = validateSubmissionForInteraction(interaction, submission);
840
+ if (!validation.ok) {
841
+ ctx.log?.error("text-entry submit invalid", { value, issues: validation.issues });
842
+ return Promise.resolve(ctx.errored(errors8.wrap(ErrInvalidSubmission, submissionValidationMessage(validation)), "interaction", { kind: "interaction", submission }));
843
+ }
686
844
  submitKey = key;
687
845
  submitPending = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
688
846
  submitPending = undefined;
@@ -924,4 +1082,4 @@ export {
924
1082
  ErrBadRequest
925
1083
  };
926
1084
 
927
- //# debugId=03C66069BFA6BE2F64756E2164756E21
1085
+ //# debugId=BFF38D60446A9E1864756E2164756E21