tessera-learn 0.0.10 → 0.0.13
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 +1 -0
- package/dist/audit-BBJpQGqb.js +204 -0
- package/dist/audit-BBJpQGqb.js.map +1 -0
- package/dist/plugin/a11y-cli.d.ts +1 -0
- package/dist/plugin/a11y-cli.js +36 -0
- package/dist/plugin/a11y-cli.js.map +1 -0
- package/dist/plugin/cli.js +6 -3
- package/dist/plugin/cli.js.map +1 -1
- package/dist/plugin/index.d.ts +16 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +171 -140
- package/dist/plugin/index.js.map +1 -1
- package/dist/{validation-BxWAMMnJ.js → validation-B-xTvM9B.js} +417 -81
- package/dist/validation-B-xTvM9B.js.map +1 -0
- package/package.json +17 -2
- package/src/components/Accordion.svelte +3 -1
- package/src/components/AccordionItem.svelte +1 -5
- package/src/components/Audio.svelte +22 -5
- package/src/components/Callout.svelte +5 -1
- package/src/components/Carousel.svelte +24 -8
- package/src/components/DefaultLayout.svelte +41 -12
- package/src/components/FillInTheBlank.svelte +75 -103
- package/src/components/Image.svelte +14 -10
- package/src/components/LockedBanner.svelte +5 -5
- package/src/components/Matching.svelte +48 -19
- package/src/components/MediaTracks.svelte +21 -0
- package/src/components/MultipleChoice.svelte +81 -102
- package/src/components/Quiz.svelte +63 -21
- package/src/components/ResultIcon.svelte +20 -4
- package/src/components/RevealModal.svelte +25 -22
- package/src/components/Sorting.svelte +61 -26
- package/src/components/Transcript.svelte +37 -0
- package/src/components/Video.svelte +25 -20
- package/src/components/util.ts +4 -1
- package/src/components/video-embed.ts +25 -0
- package/src/index.ts +2 -7
- package/src/plugin/a11y/audit.ts +299 -0
- package/src/plugin/a11y/contrast.ts +67 -0
- package/src/plugin/a11y-cli.ts +35 -0
- package/src/plugin/cli.ts +6 -8
- package/src/plugin/export.ts +60 -50
- package/src/plugin/index.ts +244 -101
- package/src/plugin/layout.ts +6 -51
- package/src/plugin/manifest.ts +90 -24
- package/src/plugin/override-plugin.ts +68 -0
- package/src/plugin/quiz.ts +9 -54
- package/src/plugin/validation.ts +768 -183
- package/src/runtime/App.svelte +128 -64
- package/src/runtime/LoadingBar.svelte +12 -3
- package/src/runtime/Sidebar.svelte +24 -8
- package/src/runtime/access.ts +15 -3
- package/src/runtime/adapters/cmi5.ts +68 -116
- package/src/runtime/adapters/format.ts +67 -0
- package/src/runtime/adapters/index.ts +45 -34
- package/src/runtime/adapters/retry.ts +25 -84
- package/src/runtime/adapters/scorm-base.ts +19 -15
- package/src/runtime/adapters/scorm12.ts +8 -9
- package/src/runtime/adapters/scorm2004.ts +22 -30
- package/src/runtime/adapters/web.ts +1 -1
- package/src/runtime/hooks.svelte.ts +152 -328
- package/src/runtime/interaction-format.ts +30 -12
- package/src/runtime/interaction.ts +44 -11
- package/src/runtime/navigation.svelte.ts +29 -40
- package/src/runtime/persistence.ts +2 -2
- package/src/runtime/progress.svelte.ts +22 -9
- package/src/runtime/quiz-engine.svelte.ts +361 -0
- package/src/runtime/quiz-policy.ts +28 -179
- package/src/runtime/types.ts +24 -2
- package/src/runtime/xapi/agent-rules.ts +11 -3
- package/src/runtime/xapi/client.ts +5 -5
- package/src/runtime/xapi/derive-actor.ts +2 -2
- package/src/runtime/xapi/publisher.ts +33 -40
- package/src/runtime/xapi/setup.ts +18 -15
- package/src/runtime/xapi/validation.ts +15 -6
- package/src/virtual.d.ts +4 -1
- package/styles/base.css +32 -11
- package/styles/layout.css +39 -18
- package/styles/theme.css +15 -3
- package/dist/validation-BxWAMMnJ.js.map +0 -1
|
@@ -43,7 +43,7 @@ export class XAPIClient {
|
|
|
43
43
|
*/
|
|
44
44
|
sendStatement(
|
|
45
45
|
partial: PartialStatement,
|
|
46
|
-
options?: SendStatementOptions
|
|
46
|
+
options?: SendStatementOptions,
|
|
47
47
|
): Promise<SendStatementResult> {
|
|
48
48
|
try {
|
|
49
49
|
validatePartialStatement(partial);
|
|
@@ -59,8 +59,8 @@ export class XAPIClient {
|
|
|
59
59
|
if (this.#publishers.every(blocked)) {
|
|
60
60
|
return Promise.reject(
|
|
61
61
|
new XAPIConfigError(
|
|
62
|
-
'XAPIClient.sendStatement: page is unloading; author statements queued during unload are dropped to keep Terminated last (cmi5 §9.3.6).'
|
|
63
|
-
)
|
|
62
|
+
'XAPIClient.sendStatement: page is unloading; author statements queued during unload are dropped to keep Terminated last (cmi5 §9.3.6).',
|
|
63
|
+
),
|
|
64
64
|
);
|
|
65
65
|
}
|
|
66
66
|
const id = uuidv4();
|
|
@@ -80,9 +80,9 @@ export class XAPIClient {
|
|
|
80
80
|
endpoint: pub.getEndpoint(),
|
|
81
81
|
ok: false,
|
|
82
82
|
error: new XAPIConfigError(
|
|
83
|
-
'destination skipped: cmi5 publisher is unloading; statement dropped to keep Terminated last (cmi5 §9.3.6).'
|
|
83
|
+
'destination skipped: cmi5 publisher is unloading; statement dropped to keep Terminated last (cmi5 §9.3.6).',
|
|
84
84
|
),
|
|
85
|
-
})
|
|
85
|
+
}),
|
|
86
86
|
);
|
|
87
87
|
continue;
|
|
88
88
|
}
|
|
@@ -39,7 +39,7 @@ export function defaultAccountHomePage(activityId: string): string | null {
|
|
|
39
39
|
export function synthesizeSCORM12Actor(
|
|
40
40
|
api: SCORM12API,
|
|
41
41
|
activityId: string,
|
|
42
|
-
actorAccountHomePage?: string
|
|
42
|
+
actorAccountHomePage?: string,
|
|
43
43
|
): XAPIAgent | null {
|
|
44
44
|
let id = '';
|
|
45
45
|
let name = '';
|
|
@@ -68,7 +68,7 @@ export function synthesizeSCORM12Actor(
|
|
|
68
68
|
export function synthesizeSCORM2004Actor(
|
|
69
69
|
api: SCORM2004API,
|
|
70
70
|
activityId: string,
|
|
71
|
-
actorAccountHomePage?: string
|
|
71
|
+
actorAccountHomePage?: string,
|
|
72
72
|
): XAPIAgent | null {
|
|
73
73
|
let id = '';
|
|
74
74
|
let name = '';
|
|
@@ -12,8 +12,8 @@ import {
|
|
|
12
12
|
validatePartialStatement,
|
|
13
13
|
validateAgent,
|
|
14
14
|
validateAuthCredential,
|
|
15
|
+
joinFieldError,
|
|
15
16
|
XAPIConfigError,
|
|
16
|
-
XAPIStatementError,
|
|
17
17
|
} from './validation.js';
|
|
18
18
|
import { RETRY_ATTEMPTS, backoffMs } from '../adapters/retry.js';
|
|
19
19
|
|
|
@@ -21,16 +21,6 @@ import { RETRY_ATTEMPTS, backoffMs } from '../adapters/retry.js';
|
|
|
21
21
|
const CMI5_SESSIONID_EXT =
|
|
22
22
|
'https://w3id.org/xapi/cmi5/context/extensions/sessionid';
|
|
23
23
|
|
|
24
|
-
/**
|
|
25
|
-
* Combine a field label (e.g. `xapi.actor`) with the prefix-friendly suffix
|
|
26
|
-
* returned by `validateAgent`. Sub-field suffixes start with `.` and chain
|
|
27
|
-
* directly (`xapi.actor.mbox …`); top-level messages get a `: ` separator
|
|
28
|
-
* (`xapi.actor: must be an object`).
|
|
29
|
-
*/
|
|
30
|
-
function joinFieldError(label: string, suffix: string): string {
|
|
31
|
-
return suffix.startsWith('.') ? `${label}${suffix}` : `${label}: ${suffix}`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
24
|
export interface XAPIPublisherOptions {
|
|
35
25
|
/** Resolved http(s) endpoint URL. The 'lms' sentinel is a config-layer concept and never reaches the publisher. */
|
|
36
26
|
endpoint: string;
|
|
@@ -137,7 +127,7 @@ export class XAPIPublisher {
|
|
|
137
127
|
}
|
|
138
128
|
if (!/^https?:\/\//i.test(opts.endpoint)) {
|
|
139
129
|
throw new XAPIConfigError(
|
|
140
|
-
'XAPIPublisher: endpoint must be an absolute http(s) URL'
|
|
130
|
+
'XAPIPublisher: endpoint must be an absolute http(s) URL',
|
|
141
131
|
);
|
|
142
132
|
}
|
|
143
133
|
if (!opts.activityId) {
|
|
@@ -183,7 +173,7 @@ export class XAPIPublisher {
|
|
|
183
173
|
resolved = await this.#actorValue();
|
|
184
174
|
} catch (err) {
|
|
185
175
|
throw new XAPIConfigError(
|
|
186
|
-
`xapi.actor resolver threw: ${err instanceof Error ? err.message : String(err)}
|
|
176
|
+
`xapi.actor resolver threw: ${err instanceof Error ? err.message : String(err)}`,
|
|
187
177
|
);
|
|
188
178
|
}
|
|
189
179
|
const err = validateAgent(resolved);
|
|
@@ -209,7 +199,7 @@ export class XAPIPublisher {
|
|
|
209
199
|
getActor(): XAPIAgent {
|
|
210
200
|
if (!this.#cachedActor) {
|
|
211
201
|
throw new XAPIConfigError(
|
|
212
|
-
'XAPIPublisher.getActor() called before init() resolved. Await publisher.init() before reading the actor.'
|
|
202
|
+
'XAPIPublisher.getActor() called before init() resolved. Await publisher.init() before reading the actor.',
|
|
213
203
|
);
|
|
214
204
|
}
|
|
215
205
|
return this.#cachedActor;
|
|
@@ -241,7 +231,7 @@ export class XAPIPublisher {
|
|
|
241
231
|
buildStatement(partial: PartialStatement, opts?: { id?: string }): Statement {
|
|
242
232
|
if (!this.#cachedActor) {
|
|
243
233
|
throw new XAPIConfigError(
|
|
244
|
-
'XAPIPublisher.buildStatement() called before init() resolved.'
|
|
234
|
+
'XAPIPublisher.buildStatement() called before init() resolved.',
|
|
245
235
|
);
|
|
246
236
|
}
|
|
247
237
|
const userCtx = partial.context ?? {};
|
|
@@ -284,7 +274,8 @@ export class XAPIPublisher {
|
|
|
284
274
|
timestamp: new Date().toISOString(),
|
|
285
275
|
};
|
|
286
276
|
if (partial.result !== undefined) statement.result = partial.result;
|
|
287
|
-
if (partial.attachments !== undefined)
|
|
277
|
+
if (partial.attachments !== undefined)
|
|
278
|
+
statement.attachments = partial.attachments;
|
|
288
279
|
return statement;
|
|
289
280
|
}
|
|
290
281
|
|
|
@@ -299,7 +290,7 @@ export class XAPIPublisher {
|
|
|
299
290
|
*/
|
|
300
291
|
sendStatement(
|
|
301
292
|
partial: PartialStatement,
|
|
302
|
-
options?: SendStatementOptions & { id?: string }
|
|
293
|
+
options?: SendStatementOptions & { id?: string },
|
|
303
294
|
): Promise<SendStatementResult> {
|
|
304
295
|
if (this.#unavailableReason) {
|
|
305
296
|
return Promise.reject(this.#unavailableReason());
|
|
@@ -316,8 +307,8 @@ export class XAPIPublisher {
|
|
|
316
307
|
if (!this.#cachedActor) {
|
|
317
308
|
return Promise.reject(
|
|
318
309
|
new XAPIConfigError(
|
|
319
|
-
'XAPIPublisher.sendStatement() called before init() resolved.'
|
|
320
|
-
)
|
|
310
|
+
'XAPIPublisher.sendStatement() called before init() resolved.',
|
|
311
|
+
),
|
|
321
312
|
);
|
|
322
313
|
}
|
|
323
314
|
const statement = this.buildStatement(partial, { id: options?.id });
|
|
@@ -336,7 +327,7 @@ export class XAPIPublisher {
|
|
|
336
327
|
*/
|
|
337
328
|
enqueueBuilt(
|
|
338
329
|
statementOrBatch: Statement | Statement[],
|
|
339
|
-
options?: SendStatementOptions
|
|
330
|
+
options?: SendStatementOptions,
|
|
340
331
|
): Promise<DestinationOutcome> {
|
|
341
332
|
if (this.#unavailableReason) {
|
|
342
333
|
return Promise.reject(this.#unavailableReason());
|
|
@@ -346,7 +337,7 @@ export class XAPIPublisher {
|
|
|
346
337
|
endpoint: this.#endpoint,
|
|
347
338
|
ok: false,
|
|
348
339
|
error: new XAPIConfigError(
|
|
349
|
-
`XAPIPublisher queue saturated (${this.#queueDepth} in-flight); refusing further sends until the LRS catches up
|
|
340
|
+
`XAPIPublisher queue saturated (${this.#queueDepth} in-flight); refusing further sends until the LRS catches up.`,
|
|
350
341
|
),
|
|
351
342
|
});
|
|
352
343
|
}
|
|
@@ -355,7 +346,7 @@ export class XAPIPublisher {
|
|
|
355
346
|
console.warn(
|
|
356
347
|
`Tessera: xAPI publisher queue depth ${this.#queueDepth} (>= ${QUEUE_DEPTH_WARN}). ` +
|
|
357
348
|
`Each pending statement is retained in the promise chain's closure until it drains; ` +
|
|
358
|
-
`consider rate-limiting authoring sends or batching before sendStatement
|
|
349
|
+
`consider rate-limiting authoring sends or batching before sendStatement.`,
|
|
359
350
|
);
|
|
360
351
|
}
|
|
361
352
|
this.#queueDepth++;
|
|
@@ -377,7 +368,7 @@ export class XAPIPublisher {
|
|
|
377
368
|
status: outcome.status,
|
|
378
369
|
error: outcome.error,
|
|
379
370
|
});
|
|
380
|
-
})
|
|
371
|
+
}),
|
|
381
372
|
);
|
|
382
373
|
return outcomePromise;
|
|
383
374
|
}
|
|
@@ -395,7 +386,9 @@ export class XAPIPublisher {
|
|
|
395
386
|
resolveTask = r;
|
|
396
387
|
});
|
|
397
388
|
this.#queue = this.#queue.then(() =>
|
|
398
|
-
fn()
|
|
389
|
+
fn()
|
|
390
|
+
.catch(() => {})
|
|
391
|
+
.then(() => resolveTask()),
|
|
399
392
|
);
|
|
400
393
|
return taskPromise;
|
|
401
394
|
}
|
|
@@ -423,7 +416,7 @@ export class XAPIPublisher {
|
|
|
423
416
|
|
|
424
417
|
#sendWithRetry(
|
|
425
418
|
statementOrBatch: Statement | Statement[],
|
|
426
|
-
options?: SendStatementOptions
|
|
419
|
+
options?: SendStatementOptions,
|
|
427
420
|
): Promise<SendOutcome> {
|
|
428
421
|
const body = JSON.stringify(statementOrBatch);
|
|
429
422
|
const retry = options?.retry !== false; // default: retry enabled
|
|
@@ -442,7 +435,7 @@ export class XAPIPublisher {
|
|
|
442
435
|
`Tessera: xAPI ${count}-statement batch is ${body.length} bytes, ` +
|
|
443
436
|
`over the 64 KiB keepalive cap. The browser may silently drop this ` +
|
|
444
437
|
`request during unload. Reduce per-statement size or split sends ` +
|
|
445
|
-
`before terminate
|
|
438
|
+
`before terminate.`,
|
|
446
439
|
);
|
|
447
440
|
}
|
|
448
441
|
return this.#sendOnce(body, keepalive).then((outcome) => {
|
|
@@ -457,9 +450,9 @@ export class XAPIPublisher {
|
|
|
457
450
|
return outcome;
|
|
458
451
|
}
|
|
459
452
|
if (isFinal) return outcome;
|
|
460
|
-
return new Promise<void>((r) =>
|
|
461
|
-
|
|
462
|
-
)
|
|
453
|
+
return new Promise<void>((r) => setTimeout(r, backoffMs(n))).then(() =>
|
|
454
|
+
attempt(n + 1),
|
|
455
|
+
);
|
|
463
456
|
});
|
|
464
457
|
};
|
|
465
458
|
|
|
@@ -471,7 +464,7 @@ export class XAPIPublisher {
|
|
|
471
464
|
return Promise.resolve({
|
|
472
465
|
ok: false,
|
|
473
466
|
error: new XAPIConfigError(
|
|
474
|
-
'xapi.auth was rejected by the LRS twice in a row; refusing further sends for the publisher lifetime. Reload the runtime to retry.'
|
|
467
|
+
'xapi.auth was rejected by the LRS twice in a row; refusing further sends for the publisher lifetime. Reload the runtime to retry.',
|
|
475
468
|
),
|
|
476
469
|
});
|
|
477
470
|
}
|
|
@@ -492,7 +485,7 @@ export class XAPIPublisher {
|
|
|
492
485
|
#fetchWithToken(
|
|
493
486
|
token: string,
|
|
494
487
|
body: string,
|
|
495
|
-
keepalive: boolean
|
|
488
|
+
keepalive: boolean,
|
|
496
489
|
): Promise<SendOutcome> {
|
|
497
490
|
const headers = new Headers();
|
|
498
491
|
if (token) headers.set('Authorization', `Basic ${token}`);
|
|
@@ -514,7 +507,7 @@ export class XAPIPublisher {
|
|
|
514
507
|
#handleResponse(
|
|
515
508
|
resp: Response,
|
|
516
509
|
body: string,
|
|
517
|
-
keepalive: boolean
|
|
510
|
+
keepalive: boolean,
|
|
518
511
|
): Promise<SendOutcome> | SendOutcome {
|
|
519
512
|
if (resp.ok || resp.status === 409) {
|
|
520
513
|
return { ok: true, status: resp.status };
|
|
@@ -550,7 +543,7 @@ export class XAPIPublisher {
|
|
|
550
543
|
ok: false,
|
|
551
544
|
status: 401,
|
|
552
545
|
error: new Error(
|
|
553
|
-
'LRS rejected re-resolved auth (consecutive 401s); auth resolver marked dead'
|
|
546
|
+
'LRS rejected re-resolved auth (consecutive 401s); auth resolver marked dead',
|
|
554
547
|
),
|
|
555
548
|
};
|
|
556
549
|
}
|
|
@@ -565,7 +558,7 @@ export class XAPIPublisher {
|
|
|
565
558
|
ok: false,
|
|
566
559
|
status: 401,
|
|
567
560
|
error: err instanceof Error ? err : new Error(String(err)),
|
|
568
|
-
})
|
|
561
|
+
}),
|
|
569
562
|
);
|
|
570
563
|
}
|
|
571
564
|
// Append the LRS body to the error message so callers see the
|
|
@@ -583,14 +576,14 @@ export class XAPIPublisher {
|
|
|
583
576
|
ok: false,
|
|
584
577
|
status: resp.status,
|
|
585
578
|
error: new Error(
|
|
586
|
-
`LRS responded ${resp.status}${respBody ? `: ${respBody.slice(0, 500)}` : ''}
|
|
579
|
+
`LRS responded ${resp.status}${respBody ? `: ${respBody.slice(0, 500)}` : ''}`,
|
|
587
580
|
),
|
|
588
581
|
}),
|
|
589
582
|
(): SendOutcome => ({
|
|
590
583
|
ok: false,
|
|
591
584
|
status: resp.status,
|
|
592
585
|
error: new Error(`LRS responded ${resp.status}`),
|
|
593
|
-
})
|
|
586
|
+
}),
|
|
594
587
|
);
|
|
595
588
|
}
|
|
596
589
|
|
|
@@ -603,7 +596,8 @@ export class XAPIPublisher {
|
|
|
603
596
|
// where the LMS fetch URL produced no token); otherwise revalidate.
|
|
604
597
|
if (this.#authValue.length > 0) {
|
|
605
598
|
const err = validateAuthCredential(this.#authValue);
|
|
606
|
-
if (err)
|
|
599
|
+
if (err)
|
|
600
|
+
return Promise.reject(new XAPIConfigError(`xapi.auth: ${err}`));
|
|
607
601
|
}
|
|
608
602
|
this.#cachedAuth = this.#authValue;
|
|
609
603
|
return Promise.resolve(this.#cachedAuth);
|
|
@@ -613,7 +607,7 @@ export class XAPIPublisher {
|
|
|
613
607
|
.then((resolved) => {
|
|
614
608
|
if (typeof resolved !== 'string' || !resolved) {
|
|
615
609
|
throw new XAPIConfigError(
|
|
616
|
-
'xapi.auth resolver must return a non-empty string'
|
|
610
|
+
'xapi.auth resolver must return a non-empty string',
|
|
617
611
|
);
|
|
618
612
|
}
|
|
619
613
|
const err = validateAuthCredential(resolved);
|
|
@@ -624,9 +618,8 @@ export class XAPIPublisher {
|
|
|
624
618
|
.catch((err) => {
|
|
625
619
|
if (err instanceof XAPIConfigError) throw err;
|
|
626
620
|
throw new XAPIConfigError(
|
|
627
|
-
`xapi.auth resolver threw: ${err instanceof Error ? err.message : String(err)}
|
|
621
|
+
`xapi.auth resolver threw: ${err instanceof Error ? err.message : String(err)}`,
|
|
628
622
|
);
|
|
629
623
|
});
|
|
630
624
|
}
|
|
631
625
|
}
|
|
632
|
-
|
|
@@ -34,8 +34,8 @@ class XAPIDevFallbackError extends Error {
|
|
|
34
34
|
"Tessera xAPI: xapi.endpoint is 'lms' but no cmi5 launch parameters " +
|
|
35
35
|
'(fetch / endpoint / activityId / actor) were present on the URL. ' +
|
|
36
36
|
'Either launch this course from a real LMS / SCORM Cloud, or ' +
|
|
37
|
-
|
|
38
|
-
'local LRS (e.g. http://localhost:8080/data/xAPI/) for dev work.'
|
|
37
|
+
'temporarily change xapi.endpoint to an explicit URL pointed at a ' +
|
|
38
|
+
'local LRS (e.g. http://localhost:8080/data/xAPI/) for dev work.',
|
|
39
39
|
);
|
|
40
40
|
this.name = 'XAPIDevFallbackError';
|
|
41
41
|
}
|
|
@@ -72,14 +72,14 @@ class XAPISCORMDevFallbackError extends Error {
|
|
|
72
72
|
'destination. Either supply xapi.actor explicitly in course.config.js, or launch from ' +
|
|
73
73
|
'a real LMS / SCORM Cloud where ' +
|
|
74
74
|
(standard === 'scorm12' ? 'cmi.core.student_id' : 'cmi.learner_id') +
|
|
75
|
-
' is populated.'
|
|
75
|
+
' is populated.',
|
|
76
76
|
);
|
|
77
77
|
this.name = 'XAPISCORMDevFallbackError';
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
function makeSCORMDevFallbackPublisher(
|
|
82
|
-
standard: 'scorm12' | 'scorm2004'
|
|
82
|
+
standard: 'scorm12' | 'scorm2004',
|
|
83
83
|
): XAPIPublisher {
|
|
84
84
|
return makeRejectingPublisher(() => new XAPISCORMDevFallbackError(standard));
|
|
85
85
|
}
|
|
@@ -93,13 +93,13 @@ function makeSCORMDevFallbackPublisher(
|
|
|
93
93
|
function resolveDestination(
|
|
94
94
|
entry: XAPIConfig,
|
|
95
95
|
config: CourseConfig,
|
|
96
|
-
adapter: PersistenceAdapter | null
|
|
96
|
+
adapter: PersistenceAdapter | null,
|
|
97
97
|
): DestinationSource | null {
|
|
98
98
|
if (entry.endpoint === 'lms') {
|
|
99
99
|
if (config.export?.standard !== 'cmi5') {
|
|
100
100
|
// Build-time validator should reject this; defense in depth at runtime.
|
|
101
101
|
console.warn(
|
|
102
|
-
"Tessera xAPI: ignoring xapi entry with endpoint: 'lms' under non-cmi5 export."
|
|
102
|
+
"Tessera xAPI: ignoring xapi entry with endpoint: 'lms' under non-cmi5 export.",
|
|
103
103
|
);
|
|
104
104
|
return null;
|
|
105
105
|
}
|
|
@@ -121,14 +121,17 @@ function resolveDestination(
|
|
|
121
121
|
(actorOrResolver as { __scormDevFallback?: 'scorm12' | 'scorm2004' })
|
|
122
122
|
.__scormDevFallback
|
|
123
123
|
) {
|
|
124
|
-
const std = (
|
|
125
|
-
|
|
124
|
+
const std = (
|
|
125
|
+
actorOrResolver as { __scormDevFallback: 'scorm12' | 'scorm2004' }
|
|
126
|
+
).__scormDevFallback;
|
|
126
127
|
return { kind: 'explicit', publisher: makeSCORMDevFallbackPublisher(std) };
|
|
127
128
|
}
|
|
128
129
|
const publisher = new XAPIPublisher({
|
|
129
130
|
endpoint: explicit.endpoint,
|
|
130
131
|
auth: explicit.auth,
|
|
131
|
-
actor: actorOrResolver as
|
|
132
|
+
actor: actorOrResolver as
|
|
133
|
+
| XAPIAgent
|
|
134
|
+
| (() => XAPIAgent | Promise<XAPIAgent>),
|
|
132
135
|
activityId: explicit.activityId,
|
|
133
136
|
registration: explicit.registration,
|
|
134
137
|
});
|
|
@@ -145,7 +148,7 @@ function resolveDestination(
|
|
|
145
148
|
function resolveExplicitActor(
|
|
146
149
|
explicit: XAPIExplicitConfig,
|
|
147
150
|
config: CourseConfig,
|
|
148
|
-
adapter: PersistenceAdapter | null
|
|
151
|
+
adapter: PersistenceAdapter | null,
|
|
149
152
|
):
|
|
150
153
|
| XAPIAgent
|
|
151
154
|
| (() => XAPIAgent | Promise<XAPIAgent>)
|
|
@@ -170,7 +173,7 @@ function resolveExplicitActor(
|
|
|
170
173
|
return synthesizeSCORM12Actor(
|
|
171
174
|
adapter.getAPI(),
|
|
172
175
|
explicit.activityId,
|
|
173
|
-
explicit.actorAccountHomePage
|
|
176
|
+
explicit.actorAccountHomePage,
|
|
174
177
|
);
|
|
175
178
|
}
|
|
176
179
|
// Adapter is the WebAdapter dev fallback. Mirror the cmi5 'lms'
|
|
@@ -184,14 +187,14 @@ function resolveExplicitActor(
|
|
|
184
187
|
return synthesizeSCORM2004Actor(
|
|
185
188
|
adapter.getAPI(),
|
|
186
189
|
explicit.activityId,
|
|
187
|
-
explicit.actorAccountHomePage
|
|
190
|
+
explicit.actorAccountHomePage,
|
|
188
191
|
);
|
|
189
192
|
}
|
|
190
193
|
return { __scormDevFallback: 'scorm2004' };
|
|
191
194
|
}
|
|
192
195
|
// Web export with no actor — build-time validator should have errored.
|
|
193
196
|
console.warn(
|
|
194
|
-
'Tessera xAPI: explicit destination has no actor and no derivation source — skipping.'
|
|
197
|
+
'Tessera xAPI: explicit destination has no actor and no derivation source — skipping.',
|
|
195
198
|
);
|
|
196
199
|
return null;
|
|
197
200
|
}
|
|
@@ -206,7 +209,7 @@ function resolveExplicitActor(
|
|
|
206
209
|
*/
|
|
207
210
|
export async function buildXAPIClient(
|
|
208
211
|
config: CourseConfig,
|
|
209
|
-
adapter: PersistenceAdapter | null
|
|
212
|
+
adapter: PersistenceAdapter | null,
|
|
210
213
|
): Promise<XAPIClient | null> {
|
|
211
214
|
const raw = config.xapi;
|
|
212
215
|
if (raw === undefined || raw === null) return null;
|
|
@@ -233,7 +236,7 @@ export async function buildXAPIClient(
|
|
|
233
236
|
} catch (err) {
|
|
234
237
|
console.warn(
|
|
235
238
|
'Tessera xAPI: failed to initialize an explicit destination — skipping.',
|
|
236
|
-
err
|
|
239
|
+
err,
|
|
237
240
|
);
|
|
238
241
|
}
|
|
239
242
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { PartialStatement } from './types.js';
|
|
2
|
-
export {
|
|
2
|
+
export {
|
|
3
|
+
validateAgent,
|
|
4
|
+
validateAuthCredential,
|
|
5
|
+
joinFieldError,
|
|
6
|
+
} from './agent-rules.js';
|
|
3
7
|
|
|
4
8
|
/** Thrown for runtime-validation failures (auth/actor resolver misuse). */
|
|
5
9
|
export class XAPIConfigError extends Error {
|
|
@@ -32,7 +36,7 @@ export function validatePartialStatement(partial: PartialStatement): void {
|
|
|
32
36
|
if (!partial || typeof partial !== 'object') {
|
|
33
37
|
throw new XAPIStatementError(
|
|
34
38
|
'sendStatement: partial statement must be an object',
|
|
35
|
-
partial
|
|
39
|
+
partial,
|
|
36
40
|
);
|
|
37
41
|
}
|
|
38
42
|
if (
|
|
@@ -43,7 +47,7 @@ export function validatePartialStatement(partial: PartialStatement): void {
|
|
|
43
47
|
) {
|
|
44
48
|
throw new XAPIStatementError(
|
|
45
49
|
'sendStatement: verb.id is required and must be a non-empty string',
|
|
46
|
-
partial
|
|
50
|
+
partial,
|
|
47
51
|
);
|
|
48
52
|
}
|
|
49
53
|
if (partial.object !== undefined) {
|
|
@@ -55,16 +59,21 @@ export function validatePartialStatement(partial: PartialStatement): void {
|
|
|
55
59
|
) {
|
|
56
60
|
throw new XAPIStatementError(
|
|
57
61
|
'sendStatement: object.id must be a non-empty string when object is supplied',
|
|
58
|
-
partial
|
|
62
|
+
partial,
|
|
59
63
|
);
|
|
60
64
|
}
|
|
61
65
|
}
|
|
62
66
|
const scaled = partial.result?.score?.scaled;
|
|
63
67
|
if (scaled !== undefined) {
|
|
64
|
-
if (
|
|
68
|
+
if (
|
|
69
|
+
typeof scaled !== 'number' ||
|
|
70
|
+
!Number.isFinite(scaled) ||
|
|
71
|
+
scaled < -1 ||
|
|
72
|
+
scaled > 1
|
|
73
|
+
) {
|
|
65
74
|
throw new XAPIStatementError(
|
|
66
75
|
`sendStatement: result.score.scaled must be a number in [-1, 1], got ${scaled}`,
|
|
67
|
-
partial
|
|
76
|
+
partial,
|
|
68
77
|
);
|
|
69
78
|
}
|
|
70
79
|
}
|
package/src/virtual.d.ts
CHANGED
|
@@ -14,7 +14,10 @@ declare module 'virtual:tessera-xapi-setup' {
|
|
|
14
14
|
import type { CourseConfig } from 'tessera-learn/runtime/types.js';
|
|
15
15
|
import type { PersistenceAdapter } from 'tessera-learn/runtime/persistence.js';
|
|
16
16
|
import type { XAPIClient } from 'tessera-learn/runtime/xapi/client.js';
|
|
17
|
-
export function buildXAPIClient(
|
|
17
|
+
export function buildXAPIClient(
|
|
18
|
+
config: CourseConfig,
|
|
19
|
+
adapter: PersistenceAdapter,
|
|
20
|
+
): Promise<XAPIClient | null>;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
interface ImportMetaEnv {
|
package/styles/base.css
CHANGED
|
@@ -47,7 +47,12 @@ object {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/* ---- Typography ---- */
|
|
50
|
-
h1,
|
|
50
|
+
h1,
|
|
51
|
+
h2,
|
|
52
|
+
h3,
|
|
53
|
+
h4,
|
|
54
|
+
h5,
|
|
55
|
+
h6 {
|
|
51
56
|
font-weight: 700;
|
|
52
57
|
line-height: 1.25;
|
|
53
58
|
color: var(--tessera-text);
|
|
@@ -55,12 +60,24 @@ h1, h2, h3, h4, h5, h6 {
|
|
|
55
60
|
margin-bottom: var(--tessera-spacing-md);
|
|
56
61
|
}
|
|
57
62
|
|
|
58
|
-
h1 {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
h1 {
|
|
64
|
+
font-size: 2rem;
|
|
65
|
+
}
|
|
66
|
+
h2 {
|
|
67
|
+
font-size: 1.625rem;
|
|
68
|
+
}
|
|
69
|
+
h3 {
|
|
70
|
+
font-size: 1.375rem;
|
|
71
|
+
}
|
|
72
|
+
h4 {
|
|
73
|
+
font-size: 1.125rem;
|
|
74
|
+
}
|
|
75
|
+
h5 {
|
|
76
|
+
font-size: 1rem;
|
|
77
|
+
}
|
|
78
|
+
h6 {
|
|
79
|
+
font-size: 0.875rem;
|
|
80
|
+
}
|
|
64
81
|
|
|
65
82
|
h1:first-child,
|
|
66
83
|
h2:first-child,
|
|
@@ -90,16 +107,19 @@ a:focus-visible {
|
|
|
90
107
|
border-radius: 2px;
|
|
91
108
|
}
|
|
92
109
|
|
|
93
|
-
strong,
|
|
110
|
+
strong,
|
|
111
|
+
b {
|
|
94
112
|
font-weight: 700;
|
|
95
113
|
}
|
|
96
114
|
|
|
97
|
-
em,
|
|
115
|
+
em,
|
|
116
|
+
i {
|
|
98
117
|
font-style: italic;
|
|
99
118
|
}
|
|
100
119
|
|
|
101
120
|
/* ---- Lists ---- */
|
|
102
|
-
ul,
|
|
121
|
+
ul,
|
|
122
|
+
ol {
|
|
103
123
|
margin-bottom: var(--tessera-spacing-md);
|
|
104
124
|
padding-left: var(--tessera-spacing-xl);
|
|
105
125
|
}
|
|
@@ -170,7 +190,8 @@ table {
|
|
|
170
190
|
font-size: 0.9375rem;
|
|
171
191
|
}
|
|
172
192
|
|
|
173
|
-
th,
|
|
193
|
+
th,
|
|
194
|
+
td {
|
|
174
195
|
padding: var(--tessera-spacing-sm) var(--tessera-spacing-md);
|
|
175
196
|
border: 1px solid var(--tessera-border);
|
|
176
197
|
text-align: left;
|