@happyvertical/smrt-education 0.60.3 → 0.60.5

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.
@@ -125,6 +125,15 @@ skipped) or `memoryClassCrunchLabsSource(files)`. It never reads video files.
125
125
  number), so `attachCourse` merges the parts in lesson order whatever order they
126
126
  arrive in.
127
127
 
128
+ - Lesson steps (#4656): the slides are step `launch` (order 0, "Launch"); each
129
+ matched video is a step keyed by its real step code (`6`, `1b`) with the
130
+ video's title as label, ordered numerically (`classCrunchLabsStepOrder`: `1` <
131
+ `1a` < `1b` < `2`; videos sharing a code share the step, offset by 0.001 each);
132
+ the status check assessment (printable PDF and the editable master) is step
133
+ `check` (order 9999, "Status check"). The mission log, handouts, printables, the
134
+ teacher manual and the assessment guidance carry no step: the first three are the
135
+ learner's setup, the others stay guardian material.
136
+
128
137
  The `report` lists lessons, members, parts, pages, matched and unmatched videos,
129
138
  duplicates, ignored files and warnings. Fixtures in tests are synthetic files
130
139
  with the same naming; no real download is ever copied into the repository.
@@ -221,10 +221,17 @@ the same way: an unknown key anywhere is `manifest_unknown_key` (with its path i
221
221
  once), `role` (required) one of `teacher-guide`, `slides`, `learner-pages`,
222
222
  `assessment`, `assessment-guidance`, `handout`, `printable`, `answer-key`,
223
223
  `attachment` (`manifest_role` otherwise; a video is a reference, never a file),
224
- optional `audience` and `title`. The audience is the role default
224
+ optional `audience`, `title` and (lesson members only) `step`. The audience is the role default
225
225
  (`LESSON_ASSET_DEFAULT_AUDIENCE`) unless stated; `answer-key` and
226
226
  `assessment-guidance` are always guardian (a learner audience is
227
227
  `manifest_audience`). Members not listed stay recorded evidence with no role.
228
+ - **Lesson steps** (SMRT #4656): a lesson member or video may declare
229
+ `step: { key, order, label? }` (a closed object; `manifest_invalid` for a key over
230
+ 32 characters or outside `A-Za-z0-9._:-`, a non-finite or non-number `order`
231
+ (|order| up to 1,000,000), or an empty label; `manifest_unknown_key` for anything
232
+ else, and a course member cannot carry one). Materials that share a `key` are
233
+ one step; fractions order `1b` after `1`. A video's `step` is still accepted as a
234
+ whole number (its rank); the object form also ranks it by `floor(order)`.
228
235
  - **Pages**: `{ member, from, to, label? }` ranges over a member of the same lesson
229
236
  (PDF, Word or PowerPoint; the rendered PDF for Office). Each page becomes a
230
237
  `LessonPage` keyed `p<n>`, labelled `<label> <n>` (default `Page <n>`), ordered by
@@ -232,7 +239,7 @@ the same way: an unknown key anywhere is `manifest_unknown_key` (with its path i
232
239
  500 pages per lesson.
233
240
  - **Videos**: `{ reference, title, step, audience? }` where `reference` is a bare id
234
241
  or an allowlisted https YouTube URL (`parseYouTubeReference`; anything else is
235
- `manifest_video`), `step` an integer 0-9999. They become `LessonExternalVideo`
242
+ `manifest_video`), `step` an integer 0-9999 or a lesson step object (see above). They become `LessonExternalVideo`
236
243
  rows; nothing is downloaded.
237
244
  - **Course members** become `CourseAsset` rows, not tied to any lesson.
238
245
  - **Unit and parts** (SMRT #4632): `course.key` (a manifest key) names the unit
@@ -251,7 +258,24 @@ plus `skipped` for declared members that were unsupported, refused or failed
251
258
  rendering. A duplicate member resolves to the earlier identical member's bytes
252
259
  (`assetPath`) and one material per (place, role, content).
253
260
 
254
- `receiveMember` also gets `audience` and `courseLevel`. **Slides** keep the original
261
+ `receiveMember` also gets `audience` and `courseLevel`, and an optional `title`
262
+ (SMRT #4653): the manifest title of the one material the member holds ("Teacher
263
+ Mission Manual"), so a host can name the assets it stores. It is absent when the
264
+ manifest gave none or the member backs several materials with different titles.
265
+ **Material titles**: `attachCourse` records that title on the `lesson_assets` /
266
+ `course_assets` row (`title`; null on a lesson row without one, `''` on a course
267
+ row), the same for the original and rendered halves of a pair. Lesson and course
268
+ material views show the row title and fall back to the stored asset's name when
269
+ there is none. A re-attach fills a missing title; a different one is a
270
+ `conflict` unless `supersede` restates it. **Lesson steps** (#4656): the declared
271
+ step is recorded on the `lesson_assets` row (`step_key`, `step_order`, `step_label`;
272
+ both halves of an original/rendered pair share it) and on `lesson_external_videos`
273
+ (`step_key`, `step_position`, `step_label`; its existing integer `step_order` stays
274
+ the rank, `floor(order)` for an object step). All are additive nullable columns.
275
+ A re-attach fills a row's missing step without a conflict; a different step is a
276
+ `conflict` unless `supersede` restates it (replays are unchanged). Hosts read it
277
+ with `SubmissionService.listLessonSteps` ([lesson materials](lesson-materials.md)).
278
+ **Slides** keep the original
255
279
  deck (hand `bytes` to the host as a private asset: "Open in PowerPoint") and its
256
280
  rendered pages (`rendered`: "Present").
257
281
 
@@ -318,7 +342,8 @@ tenant rules and lock order (import row, then unit row) as any attach.
318
342
  rewritten. Sections and lessons the newer build no longer lists stay.
319
343
  - **Part count change** stays `unit-mismatch`, supersede or not.
320
344
  - **Materials**: the newer build's materials are attached; an existing row
321
- (same lesson, asset, role) is restated in place (order, title, current again),
345
+ (same lesson, asset, role) is restated in place (order, title, current again; a
346
+ renamed material shows its new title though it keeps its earlier asset),
322
347
  not a `conflict`; its audience stays the family's. Pages are restated and
323
348
  never retired (learner work points at page keys).
324
349
  - **Retirement**: file materials and external videos the replaced build wrote
@@ -35,7 +35,7 @@ must be private.
35
35
 
36
36
  `listLessonMaterials({enrollmentId, lessonId})` and `listCourseMaterials({enrollmentId})`
37
37
  return `LessonMaterialView`s (`id`, `kind` file or external-video, `scope`, role,
38
- audience, title, order, `assetId` or provider/videoId/`embedUrl`, and for files
38
+ audience, title (the bundle's material title, else the stored asset's name), order, `assetId` or provider/videoId/`embedUrl`, and for files
39
39
  `mimeType` from the stored asset, `rendition`, `pairedMaterialId` (the other half
40
40
  of an original/rendered pair, only when this actor sees both) and
41
41
  `thumbnailAssetId`). Hosts read these instead of guessing by file extension. The learner
@@ -44,7 +44,7 @@ catalog projection without answer keys (every learner-facing catalog read) appli
44
44
  the same rule. `signLessonMaterial`/`signCourseMaterial` sign only a file in the
45
45
  actor's own list (claim scope `lesson:<id>` / `course:<id>`); a guessed id of
46
46
  guardian material is denied; `thumbnail: true` signs the thumbnail of a visible
47
- material instead. `signLessonPrintable` applies the same audience rule to the learner. Storage URIs never appear.
47
+ material instead. `signLessonPrintable` signs a lesson file for printing: a `printable`, `handout` or `learner-pages` row (handout and learner pages only when the stored asset is `application/pdf`), learner-audience for a learner and any audience for a guardian. A retired row, an `original` rendition (the editable file) and any other role, answer keys included, are refused (SMRT #4657). Storage URIs never appear.
48
48
 
49
49
  `setMaterialAudience({enrollmentId, scope, kind, materialId, audience})` lets a
50
50
  guardian with plan authority change a family-owned row within the role's bounds
@@ -54,6 +54,29 @@ family-owned, so this is the after-attach path; before attach use
54
54
  Authored courses change audience through course authoring (`CourseMaterialInput.audience`,
55
55
  carried to the released join); shared or other families' rows are denied.
56
56
 
57
+ ## Lesson steps (SMRT #4656)
58
+
59
+ `listLessonSteps({enrollmentId, lessonId})` walks a multi-step lesson in order,
60
+ filtered like `listLessonMaterials` (same actor and enrollment checks, learner
61
+ audience only for a learner, retired rows left out, same `LessonMaterialView`s):
62
+
63
+ ```ts
64
+ { setup: LessonMaterialView[]; // stepless printable / handout / learner-pages files
65
+ steps: Array<{ key: string; label: string; // label falls back to the key
66
+ kind: 'present' | 'watch' | 'do' | 'check';
67
+ order: number; // 1b sorts after 1
68
+ materials: LessonMaterialView[] }> }
69
+ ```
70
+
71
+ Steps come from the `step_key`/`step_order`/`step_label` recorded on lesson files and
72
+ (`step_position`) external videos by a course bundle ([manifest steps](course-bundle-import.md)).
73
+ Materials sharing a key are one step (the original and rendered halves of a deck stay
74
+ together); steps sort by order then key; `kind` is `present` if any material is
75
+ `slides`, else `watch` if any is an external video, else `check` if any is an
76
+ `assessment`, else `do`. A lesson with no declared steps returns `steps: []`.
77
+ Course-level materials (supply lists) are not part of a lesson listing and stay with
78
+ `listCourseMaterials`.
79
+
57
80
  ## External videos
58
81
 
59
82
  `LessonExternalVideo` references a video at an allowlisted provider (YouTube only):
@@ -637,6 +637,14 @@ function materialAudience(relationship, audience) {
637
637
  return effective;
638
638
  }
639
639
  var MATERIAL_RENDITIONS = ["original", "rendered"];
640
+ var LESSON_STEP_KEY_MAX = 32;
641
+ var LESSON_STEP_LABEL_MAX = 200;
642
+ var LESSON_STEP_ORDER_MAX = 1e6;
643
+ function checkStep(key, rawOrder, label) {
644
+ if (key === null && rawOrder === null && label === null) return;
645
+ const order = rawOrder === null ? null : Number(rawOrder);
646
+ if (typeof key !== "string" || !key.trim() || key.length > 32 || typeof order !== "number" || !Number.isFinite(order) || Math.abs(order) > 1e6 || label !== null && (typeof label !== "string" || label.length > 200)) throw new Error("Invalid lesson step");
647
+ }
640
648
  async function requirePrivateAsset(row, assetId = row.assetId) {
641
649
  const asset = await (await AssetCollection.create({ db: row.db })).get(assetId, { cache: false });
642
650
  if (!row.tenantId || !asset || asset.tenantId !== row.tenantId || asset.accessMode !== "private" || !asset.ownerProfileId) throw new Error("Private lesson material requires an authorized private asset");
@@ -678,14 +686,20 @@ var LessonAsset = class extends SmrtObject {
678
686
  rendition = "";
679
687
  renditionOfAssetId = null;
680
688
  thumbnailAssetId = null;
689
+ title = null;
681
690
  retiredAt = null;
682
691
  sourceImportId = null;
692
+ stepKey = null;
693
+ stepOrder = null;
694
+ stepLabel = null;
683
695
  constructor(options = {}) {
684
696
  super(options);
685
697
  for (const [key, value] of Object.entries(options)) if (Object.hasOwn(this, key)) this[key] = value;
686
698
  }
687
699
  async save(options) {
688
700
  materialAudience(this.relationship, this.audience);
701
+ if (this.title !== null && (typeof this.title !== "string" || this.title.length > 200)) throw new Error("Invalid lesson material title");
702
+ checkStep(this.stepKey, this.stepOrder, this.stepLabel);
689
703
  const lesson = await this.db.get("lessons", { id: this.lessonId });
690
704
  if (!lesson) throw new Error(`education lesson ${this.lessonId} does not exist`);
691
705
  if (!await this.db.get("lessons", {
@@ -711,6 +725,10 @@ __decorateClass$10([crossPackageRef("@happyvertical/smrt-assets:Asset", { requir
711
725
  __decorateClass$10([field({ required: true })], LessonAsset.prototype, "relationship", 2);
712
726
  __decorateClass$10([crossPackageRef("@happyvertical/smrt-assets:Asset", { nullable: true })], LessonAsset.prototype, "renditionOfAssetId", 2);
713
727
  __decorateClass$10([crossPackageRef("@happyvertical/smrt-assets:Asset", { nullable: true })], LessonAsset.prototype, "thumbnailAssetId", 2);
728
+ __decorateClass$10([field({
729
+ type: "text",
730
+ nullable: true
731
+ })], LessonAsset.prototype, "title", 2);
714
732
  __decorateClass$10([field({
715
733
  type: "datetime",
716
734
  nullable: true
@@ -719,6 +737,18 @@ __decorateClass$10([field({
719
737
  type: "text",
720
738
  nullable: true
721
739
  })], LessonAsset.prototype, "sourceImportId", 2);
740
+ __decorateClass$10([field({
741
+ type: "text",
742
+ nullable: true
743
+ })], LessonAsset.prototype, "stepKey", 2);
744
+ __decorateClass$10([field({
745
+ type: "decimal",
746
+ nullable: true
747
+ })], LessonAsset.prototype, "stepOrder", 2);
748
+ __decorateClass$10([field({
749
+ type: "text",
750
+ nullable: true
751
+ })], LessonAsset.prototype, "stepLabel", 2);
722
752
  LessonAsset = __decorateClass$10([TenantScoped({ mode: "optional" }), smrt({
723
753
  tableName: "lesson_assets",
724
754
  conflictColumns: [
@@ -834,6 +864,9 @@ var LessonExternalVideo = class extends SmrtObject {
834
864
  audience = "";
835
865
  retiredAt = null;
836
866
  sourceImportId = null;
867
+ stepKey = null;
868
+ stepPosition = null;
869
+ stepLabel = null;
837
870
  constructor(options = {}) {
838
871
  super(options);
839
872
  for (const [key, value] of Object.entries(options)) if (Object.hasOwn(this, key)) this[key] = value;
@@ -841,6 +874,7 @@ var LessonExternalVideo = class extends SmrtObject {
841
874
  async save(options) {
842
875
  materialAudience("video", this.audience);
843
876
  if (!EXTERNAL_VIDEO_PROVIDERS.includes(this.provider) || !YOUTUBE_ID.test(this.videoId) || typeof this.title !== "string" || !this.title.trim() || this.title.length > 200 || !Number.isSafeInteger(this.stepOrder) || this.stepOrder < 0) throw new Error("Invalid external video reference");
877
+ checkStep(this.stepKey, this.stepPosition, this.stepLabel);
844
878
  if (!await this.db.get("lessons", {
845
879
  id: this.lessonId,
846
880
  tenant_id: this.tenantId
@@ -859,6 +893,18 @@ __decorateClass$10([field({
859
893
  type: "text",
860
894
  nullable: true
861
895
  })], LessonExternalVideo.prototype, "sourceImportId", 2);
896
+ __decorateClass$10([field({
897
+ type: "text",
898
+ nullable: true
899
+ })], LessonExternalVideo.prototype, "stepKey", 2);
900
+ __decorateClass$10([field({
901
+ type: "decimal",
902
+ nullable: true
903
+ })], LessonExternalVideo.prototype, "stepPosition", 2);
904
+ __decorateClass$10([field({
905
+ type: "text",
906
+ nullable: true
907
+ })], LessonExternalVideo.prototype, "stepLabel", 2);
862
908
  LessonExternalVideo = __decorateClass$10([TenantScoped({ mode: "optional" }), smrt({
863
909
  tableName: "lesson_external_videos",
864
910
  conflictColumns: [
@@ -32461,6 +32507,21 @@ var ExternalVideoReferenceError = class extends LearnerValidationError {
32461
32507
  this.name = "ExternalVideoReferenceError";
32462
32508
  }
32463
32509
  };
32510
+ var PRINTABLE_RELATIONSHIPS = [
32511
+ "printable",
32512
+ "handout",
32513
+ "learner-pages"
32514
+ ];
32515
+ var stepOfRow = (row, orderField) => row.step_key ? {
32516
+ key: String(row.step_key),
32517
+ order: Number(row[orderField]) || 0,
32518
+ label: row.step_label ? String(row.step_label) : ""
32519
+ } : null;
32520
+ var SETUP_RELATIONSHIPS = /* @__PURE__ */ new Set([
32521
+ "printable",
32522
+ "handout",
32523
+ "learner-pages"
32524
+ ]);
32464
32525
  function pairRenditions(rows, views) {
32465
32526
  const out = views.map((v) => ({ ...v }));
32466
32527
  rows.forEach((row, i) => {
@@ -33309,7 +33370,7 @@ var SubmissionService = class SubmissionService extends LearnerService {
33309
33370
  scope,
33310
33371
  relationship: row.relationship,
33311
33372
  audience: materialAudience(String(row.relationship), String(row.audience ?? "")),
33312
- title: scope === "course" && String(row.title ?? "") || asset.name,
33373
+ title: String(row.title ?? "") || asset.name,
33313
33374
  order: Number(row.sort_order) || 0,
33314
33375
  assetId: String(row.asset_id),
33315
33376
  provider: null,
@@ -33327,6 +33388,9 @@ var SubmissionService = class SubmissionService extends LearnerService {
33327
33388
  * learner-audience material; keys and guidance never reach them.
33328
33389
  */
33329
33390
  async listLessonMaterials(request) {
33391
+ return (await this.collectLessonMaterials(request)).map((m) => m.view);
33392
+ }
33393
+ async collectLessonMaterials(request) {
33330
33394
  const enrollment = await this.requireEnrollment(request, "education.learner.read");
33331
33395
  if (!await this.lessonInScope(request, enrollment, request.lessonId)) throw new LearnerAuthorizationError();
33332
33396
  const lesson = await this.db.get("lessons", { id: request.lessonId });
@@ -33334,28 +33398,72 @@ var SubmissionService = class SubmissionService extends LearnerService {
33334
33398
  const tenant = lesson.tenant_id ?? null;
33335
33399
  const learnerOnly = await this.learnerAudienceOnly(request);
33336
33400
  const files = (await this.db.list("lesson_assets", { lesson_id: request.lessonId })).filter((row) => (row.tenant_id ?? null) === tenant && !row.retired_at && this.visibleTo(learnerOnly, String(row.relationship), String(row.audience ?? ""))).sort((a, b) => Number(a.sort_order) - Number(b.sort_order) || String(a.id).localeCompare(String(b.id)));
33337
- const videos = await this.db.tableExists("lesson_external_videos") ? (await this.db.list("lesson_external_videos", { lesson_id: request.lessonId })).filter((row) => (row.tenant_id ?? null) === tenant && !row.retired_at && this.visibleTo(learnerOnly, "video", String(row.audience ?? ""))).sort((a, b) => Number(a.step_order) - Number(b.step_order) || String(a.id).localeCompare(String(b.id))) : [];
33401
+ const videos = await this.db.tableExists("lesson_external_videos") ? (await this.db.list("lesson_external_videos", { lesson_id: request.lessonId })).filter((row) => (row.tenant_id ?? null) === tenant && !row.retired_at && this.visibleTo(learnerOnly, "video", String(row.audience ?? ""))).sort((a, b) => Number(a.step_order) - Number(b.step_order) || (Number(a.step_position ?? 0) || 0) - (Number(b.step_position ?? 0) || 0) || String(a.id).localeCompare(String(b.id))) : [];
33338
33402
  const fileViews = [];
33339
33403
  for (const row of files) fileViews.push(await this.fileView(row, "lesson"));
33340
33404
  const result = pairRenditions(files, fileViews);
33341
- for (const row of videos) result.push({
33342
- id: String(row.id),
33343
- kind: "external-video",
33344
- scope: "lesson",
33345
- relationship: "video",
33346
- audience: materialAudience("video", String(row.audience ?? "")),
33347
- title: String(row.title),
33348
- order: Number(row.step_order) || 0,
33349
- assetId: null,
33350
- provider: row.provider,
33351
- videoId: String(row.video_id),
33352
- embedUrl: externalVideoEmbedUrl(row.provider, String(row.video_id)),
33353
- mimeType: null,
33354
- rendition: null,
33355
- pairedMaterialId: null,
33356
- thumbnailAssetId: null
33357
- });
33358
- return result;
33405
+ const steps = files.map((row) => stepOfRow(row, "step_order"));
33406
+ for (const row of videos) {
33407
+ steps.push(stepOfRow(row, "step_position"));
33408
+ result.push({
33409
+ id: String(row.id),
33410
+ kind: "external-video",
33411
+ scope: "lesson",
33412
+ relationship: "video",
33413
+ audience: materialAudience("video", String(row.audience ?? "")),
33414
+ title: String(row.title),
33415
+ order: Number(row.step_order) || 0,
33416
+ assetId: null,
33417
+ provider: row.provider,
33418
+ videoId: String(row.video_id),
33419
+ embedUrl: externalVideoEmbedUrl(row.provider, String(row.video_id)),
33420
+ mimeType: null,
33421
+ rendition: null,
33422
+ pairedMaterialId: null,
33423
+ thumbnailAssetId: null
33424
+ });
33425
+ }
33426
+ return result.map((view, i) => ({
33427
+ view,
33428
+ step: steps[i] ?? null
33429
+ }));
33430
+ }
33431
+ /**
33432
+ * A lesson walked in order (SMRT #4656), audience-filtered exactly like
33433
+ * `listLessonMaterials`: stepless handouts, learner pages and printables as
33434
+ * `setup`, then the declared steps. A step groups every material sharing its
33435
+ * key (the original and rendered halves of a deck stay together). A lesson
33436
+ * with no declared steps returns `steps: []`. Course-level supply lists are
33437
+ * not part of a lesson's listing and stay with `listCourseMaterials`.
33438
+ */
33439
+ async listLessonSteps(request) {
33440
+ const collected = await this.collectLessonMaterials(request);
33441
+ const setup = collected.filter((m) => !m.step && m.view.kind === "file" && SETUP_RELATIONSHIPS.has(m.view.relationship)).map((m) => m.view);
33442
+ const groups = /* @__PURE__ */ new Map();
33443
+ for (const { view, step } of collected) {
33444
+ if (!step) continue;
33445
+ const group = groups.get(step.key) ?? {
33446
+ key: step.key,
33447
+ order: step.order,
33448
+ label: "",
33449
+ materials: []
33450
+ };
33451
+ group.order = Math.min(group.order, step.order);
33452
+ group.label = group.label || step.label;
33453
+ group.materials.push(view);
33454
+ groups.set(step.key, group);
33455
+ }
33456
+ const kindOf = (materials) => materials.some((m) => m.relationship === "slides") ? "present" : materials.some((m) => m.kind === "external-video") ? "watch" : materials.some((m) => m.relationship === "assessment") ? "check" : "do";
33457
+ return {
33458
+ setup,
33459
+ steps: [...groups.values()].sort((a, b) => a.order - b.order || (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)).map((g) => ({
33460
+ key: g.key,
33461
+ label: g.label || g.key,
33462
+ kind: kindOf(g.materials),
33463
+ order: g.order,
33464
+ materials: g.materials
33465
+ }))
33466
+ };
33359
33467
  }
33360
33468
  /** Course-level materials (supply lists, posters) under the same audience rule. */
33361
33469
  async listCourseMaterials(request) {
@@ -33525,16 +33633,30 @@ var SubmissionService = class SubmissionService extends LearnerService {
33525
33633
  return view;
33526
33634
  });
33527
33635
  }
33636
+ /**
33637
+ * Sign a lesson file for printing: a `printable`, `handout` or `learner-pages`
33638
+ * PDF the actor may see (learner-audience, or any for a guardian). Never an
33639
+ * editable `original` rendition and never an answer key.
33640
+ */
33528
33641
  async signLessonPrintable(request) {
33529
33642
  const enrollment = await this.requireEnrollment(request, "education.learner.read");
33530
33643
  if (!await this.lessonInScope(request, enrollment, request.lessonId)) throw new LearnerAuthorizationError();
33531
- const link = await (await LessonAssetCollection.create({ db: this.db })).get({
33532
- tenantId: request.tenantId,
33533
- lessonId: request.lessonId,
33534
- assetId: request.assetId,
33535
- relationship: "printable"
33536
- }, { cache: false });
33537
- if (!link || !this.options.signAsset) throw new LearnerAuthorizationError();
33644
+ if (!this.options.signAsset) throw new LearnerAuthorizationError();
33645
+ const links = await LessonAssetCollection.create({ db: this.db });
33646
+ let link = null;
33647
+ for (const relationship of PRINTABLE_RELATIONSHIPS) {
33648
+ link = await links.get({
33649
+ tenantId: request.tenantId,
33650
+ lessonId: request.lessonId,
33651
+ assetId: request.assetId,
33652
+ relationship
33653
+ }, { cache: false });
33654
+ if (link?.retiredAt) link = null;
33655
+ if (link) break;
33656
+ }
33657
+ if (!link) throw new LearnerAuthorizationError();
33658
+ if (link.rendition === "original") throw new LearnerAuthorizationError();
33659
+ if (link.relationship !== "printable" && (await this.materialAsset(request.assetId)).mimeType !== "application/pdf") throw new LearnerAuthorizationError();
33538
33660
  if (await this.learnerAudienceOnly(request) && materialAudience(link.relationship, link.audience) !== "learner") throw new LearnerAuthorizationError();
33539
33661
  const expiresAt = new Date(Date.now() + 300 * 1e3);
33540
33662
  return {
@@ -36968,6 +37090,6 @@ var WorkbookPhotoService = class WorkbookPhotoService extends LessonWorkService
36968
37090
  }
36969
37091
  };
36970
37092
  //#endregion
36971
- export { LessonAlreadyCompleteError as $, Curriculum as $i, QUESTION_COMPLETION_MODES as $n, ReleasedLesson as $r, WorkFeedbackRelease as $t, QUESTION_EXTRACTION_VERSION as A, Attestation as Ai, QuestionRevision as An, CourseCoverChangeCollection as Ar, WorkbookPhotoDecision as At, SELF_ATTESTABLE_LESSON_KINDS as B, CourseAsset as Bi, normalizeQuestionContent as Bn, CourseUpgradeChoiceCollection as Br, WorkCompletionDecision as Bt, MAX_EXTRACTED_OMISSIONS as C, ReadingCollection as Ci, LearningObjectiveCollection as Cn, MAX_REFUSAL_EXPLANATION_LENGTH as Cr, canonicalGroupSharingPolicy as Ct, MAX_EXTRACTED_STUDENT_WORK as D, SubmissionIntakeBindingCollection as Di, QuestionCollection as Dn, CourseArchiveDecision as Dr, BookContextRevisionCollection as Dt, MAX_EXTRACTED_SECTIONS as E, SubmissionCollection as Ei, Question as En, readDelegation as Er, BookContextRevision as Et, normalizeWorkObservedLayout as F, Submission as Fi, resolveEnrollmentLessons as Fn, CourseRelease as Fr, FamilyIntakeAsset as Ft, EnrollmentNotActiveError as G, LESSON_ASSET_RELATIONSHIPS as Gi, MAX_PRINTABLE_ITEMS as Gn, EnrollmentAssignmentRecord as Gr, LessonWorkFiling as Gt, SubmissionStorageCleanupError as H, EXTERNAL_VIDEO_PROVIDERS as Hi, GENERATED_PRINTABLE_RELATIONSHIPS as Hn, CourseVisibilityChangeCollection as Hr, readWorkCompletion as Ht, normalizeWorkAnswerRegions as I, SubmissionAsset as Ii, applyProposalFields as In, CourseReleaseCollection as Ir, FamilyIntakeAssetCollection as It, InterestThemeError as J, LessonExternalVideoCollection as Ji, MAX_PRINTABLE_TOTAL_TEXT as Jn, EnrollmentAutoUpgradeSkipCollection as Jr, WorkAssessmentRevisionCollection as Jt, EnrollmentPaceError as K, LessonAsset as Ki, MAX_PRINTABLE_PAGES as Kn, EnrollmentAssignmentRecordCollection as Kr, LessonWorkFilingCollection as Kt, LessonWorkWorkflowRequiredError as L, SubmissionIntakeBinding as Li, draftFromRevision as Ln, CourseRevision as Lr, FamilyIntakeCollection as Lt, QuestionExtractionPendingError as M, Grade as Mi, CourseAuthoringService as Mn, CourseDiscardTombstoneCollection as Mr, WorkbookPhotoIntake as Mt, QuestionExtractionValidationError as N, LessonSelfAttestation as Ni, authoredVisibilityPermitsGroups as Nn, CourseDraftHead as Nr, WorkbookPhotoIntakeCollection as Nt, MAX_EXTRACTED_TEXT as O, VideoCollection as Oi, QuestionResponse as On, CourseArchiveDecisionCollection as Or, WorkbookPhotoAsset as Ot, LessonWorkService as P, ScannedSubmission as Pi, effectiveCourseVisibility as Pn, CourseDraftHeadCollection as Pr, FamilyIntake as Pt, LearnerValidationError as Q, parseYouTubeReference as Qi, PrintableStorageNotConfiguredError as Qn, LessonRevisionCollection as Qr, WorkFeedbackDeliveryCollection as Qt, OwnAttestationConflictError as R, parseQuestionRubric as Ri, planProposal as Rn, CourseRevisionCollection as Rr, FamilyIntakeDecision as Rt, MAX_EXTRACTED_ANCHORS as S, LessonSelfAttestationCollection as Si, LearningObjective as Sn, MAX_REFUSAL_CATEGORY_LENGTH as Sr, GroupSharingPolicyValidationError as St, MAX_EXTRACTED_QUESTIONS as T, SubmissionAssetCollection as Ti, LearningObjectiveRevisionCollection as Tn, delegationRecordFields as Tr, familyIntakeScopeId as Tt, submissionPageKey as U, GUARDIAN_ONLY_RELATIONSHIPS as Ui, LearningObjectiveKeyConflictError as Un, CurriculumCreationRecord as Ur, LessonActivity as Ut, SubmissionService as V, CourseAssetCollection as Vi, GENERATED_PRINTABLE_AUDIENCES as Vn, CourseVisibilityChange as Vr, WorkCompletionDecisionCollection as Vt, ENROLLMENT_STATUS_TRANSITIONS as W, LESSON_ASSET_DEFAULT_AUDIENCE as Wi, LearningObjectiveNotFoundError as Wn, CurriculumCreationRecordCollection as Wr, LessonActivityCollection as Wt, LearnerHistoryUnavailableError as X, externalVideoEmbedUrl as Xi, PrintableOwnerError as Xn, EnrollmentReleasePinCollection as Xr, WorkAttemptCollection as Xt, LearnerAuthorizationError as Y, MATERIAL_RENDITIONS as Yi, MAX_RESOLVE_QUESTION_REVISIONS as Yn, EnrollmentReleasePin as Yr, WorkAttempt as Yt, LearnerService as Z, materialAudience as Zi, PrintableStorageCleanupError as Zn, LessonRevision as Zr, WorkFeedbackDelivery as Zt, EXTRACTED_OMISSION_REASONS as _, GradeCollection as _i, renderGuardianWorksheetPdf as _n, CourseProposalFieldError as _r, LessonProgressTransitionCollection as _t, questionExtractionProposalDigest as a, interestThemeFromColumns as aa, mostRestrictiveLicense as ai, aggregateWorkQuestions as an, QuestionReferenceError as ar, MIN_LESSONS_PER_WEEK as at, EXTRACTED_QUESTION_STATUSES as b, LessonCollection as bi, GeneratedPrintable as bn, MAX_GENERATION_GRADE as br, GroupSharingAuthorizationError as bt, QuestionExtractionRecord as c, COURSE_VISIBILITIES as ca, AttestationCollection as ci, LessonPage as cn, COURSE_GENERATION_STATUSES as cr, EnrollmentPaceChange as ct, extractedQuestionContent as d, Exercise as da, CourseSectionCollection as di, validLessonPageKey as dn, CourseGenerationBriefError as dr, EnrollmentStatusChangeCollection as dt, Enrollment as ea, ReleasedLessonCollection as ei, WorkFeedbackReleaseCollection as en, QUESTION_FIELDS as er, LessonPutBackError as et, matchExtractedQuestions as f, ExternalCourse as fa, CurriculumCollection as fi, QuestionAuthoringService as fn, CourseGenerationInProgressError as fr, LearnerInterestThemeChange as ft, EXTRACTED_MEDIA_KINDS as g, Worksheet as ga, ExternalCourseCollection as gi, worksheetVersionId as gn, CourseGeneratorNotConfiguredError as gr, LessonProgressTransition as gt, EXTRACTED_LEGIBILITY as h, Video as ha, ExternalCompletionCollection as hi, canonical$2 as hn, CourseGenerationRefusal as hr, LessonCompletionRecordCollection as ht, QuestionExtractionService as i, MAX_INTEREST_THEME_LABEL_CHARS as ia, isGroupShareableLicense as ii, WorkCompletionConflictError as in, QuestionNotFoundError as ir, MAX_NEXT_LESSONS_COUNT as it, QuestionExtractionNotFoundError as j, ExternalCompletion as ji, QuestionRevisionCollection as jn, CourseDiscardTombstone as jr, WorkbookPhotoDecisionCollection as jt, QUESTION_EXTRACTION_SOURCE_KINDS as k, WorksheetCollection as ki, QuestionResponseCollection as kn, CourseCoverChange as kr, WorkbookPhotoAssetCollection as kt, QuestionExtractionRecordCollection as l, Course as la, AuthoredCourseCollection as li, LessonPageCollection as ln, COURSE_GENERATION_TARGETS as lr, EnrollmentPaceChangeCollection as lt, questionExtractionSourceIdentity as m, Reading as ma, ExerciseCollection as mi, buildLearnerWorksheet as mn, CourseGenerationOutputError as mr, LessonCompletionRecord as mt, preservePhotoProposal as n, LessonProgress as na, AUTHORED_RELEASE_SOURCE_KEY as ni, WORK_MATH_VOCABULARY as nn, QUESTION_PROVENANCES as nr, LessonWorkOwnedError as nt, ExtractedQuestionLink as o, parseInterestTheme as oa, CatalogAuthorizationError as oi, validateWorkAssessment as on, WorksheetVersionMismatchError as or, enrollmentView as ot, normalizeQuestionExtraction as p, Lesson as pa, EnrollmentCollection as pi, buildGuardianWorksheet as pn, CourseGenerationNotFoundError as pr, LearnerInterestThemeChangeCollection as pt, EnrollmentStatusTransitionError as q, LessonExternalVideo as qi, MAX_PRINTABLE_TEXT_LENGTH as qn, EnrollmentAutoUpgradeSkip as qr, WorkAssessmentRevision as qt, validateWorkbookPhotoIdentification as r, MAX_INTEREST_THEME_KEY_CHARS as ra, isAuthoredReleaseGroupShareable as ri, WorkAssessmentValidationError as rn, QUESTION_RESPONSE_TYPES as rr, MAX_LESSONS_PER_WEEK as rt, ExtractedQuestionLinkCollection as s, AuthoredCourse as sa, CatalogService as si, workAggregateFeedback as sn, groupSharingDigest as sr, requirePolicyBasis as st, WorkbookPhotoService as t, Learner as ta, AUTHORED_IDENTITY_SOURCE_KEY as ti, LessonWorkPlacementError as tn, QUESTION_PROMPT_SUMMARY_LENGTH as tr, LessonSkippedError as tt, QUESTION_EXTRACTION_ARGS_SCHEMA as u, CourseSection as ua, CourseCollection as ui, lessonPageView as un, COURSE_GENERATION_TERMINAL as ur, EnrollmentStatusChange as ut, EXTRACTED_ORIGINS as v, LearnerCollection as vi, renderLearnerWorksheetPdf as vn, CourseProposalNotFoundError as vr, evaluateGroupSharingPolicy as vt, MAX_EXTRACTED_PAGES as w, ScannedSubmissionCollection as wi, LearningObjectiveRevision as wn, delegationActorFields as wr, validateGroupSharingPolicy as wt, EXTRACTED_READABILITY as x, LessonProgressCollection as xi, GeneratedPrintableCollection as xn, MAX_GENERATION_OBJECTIVE_CHARS as xr, GroupSharingConflictError as xt, EXTRACTED_PAGE_ROLES as y, LessonAssetCollection as yi, es_exports as yn, MAX_GENERATION_BRIEF_FIELD_CHARS as yr, summarizeWorkEvidence as yt, OwnAttestationLessonKindError as z, validateRubricResults as zi, validateCourseGenerationOutput as zn, CourseUpgradeChoice as zr, FamilyIntakeDecisionCollection as zt };
37093
+ export { LessonAlreadyCompleteError as $, externalVideoEmbedUrl as $i, QUESTION_COMPLETION_MODES as $n, ReleasedLesson as $r, WorkFeedbackRelease as $t, QUESTION_EXTRACTION_VERSION as A, Attestation as Ai, QuestionRevision as An, CourseCoverChangeCollection as Ar, WorkbookPhotoDecision as At, SELF_ATTESTABLE_LESSON_KINDS as B, CourseAsset as Bi, normalizeQuestionContent as Bn, CourseUpgradeChoiceCollection as Br, WorkCompletionDecision as Bt, MAX_EXTRACTED_OMISSIONS as C, ReadingCollection as Ci, LearningObjectiveCollection as Cn, MAX_REFUSAL_EXPLANATION_LENGTH as Cr, canonicalGroupSharingPolicy as Ct, MAX_EXTRACTED_STUDENT_WORK as D, SubmissionIntakeBindingCollection as Di, QuestionCollection as Dn, CourseArchiveDecision as Dr, BookContextRevisionCollection as Dt, MAX_EXTRACTED_SECTIONS as E, SubmissionCollection as Ei, Question as En, readDelegation as Er, BookContextRevision as Et, normalizeWorkObservedLayout as F, Submission as Fi, resolveEnrollmentLessons as Fn, CourseRelease as Fr, FamilyIntakeAsset as Ft, EnrollmentNotActiveError as G, LESSON_ASSET_RELATIONSHIPS as Gi, MAX_PRINTABLE_ITEMS as Gn, EnrollmentAssignmentRecord as Gr, LessonWorkFiling as Gt, SubmissionStorageCleanupError as H, EXTERNAL_VIDEO_PROVIDERS as Hi, GENERATED_PRINTABLE_RELATIONSHIPS as Hn, CourseVisibilityChangeCollection as Hr, readWorkCompletion as Ht, normalizeWorkAnswerRegions as I, SubmissionAsset as Ii, applyProposalFields as In, CourseReleaseCollection as Ir, FamilyIntakeAssetCollection as It, InterestThemeError as J, LESSON_STEP_ORDER_MAX as Ji, MAX_PRINTABLE_TOTAL_TEXT as Jn, EnrollmentAutoUpgradeSkipCollection as Jr, WorkAssessmentRevisionCollection as Jt, EnrollmentPaceError as K, LESSON_STEP_KEY_MAX as Ki, MAX_PRINTABLE_PAGES as Kn, EnrollmentAssignmentRecordCollection as Kr, LessonWorkFilingCollection as Kt, LessonWorkWorkflowRequiredError as L, SubmissionIntakeBinding as Li, draftFromRevision as Ln, CourseRevision as Lr, FamilyIntakeCollection as Lt, QuestionExtractionPendingError as M, Grade as Mi, CourseAuthoringService as Mn, CourseDiscardTombstoneCollection as Mr, WorkbookPhotoIntake as Mt, QuestionExtractionValidationError as N, LessonSelfAttestation as Ni, authoredVisibilityPermitsGroups as Nn, CourseDraftHead as Nr, WorkbookPhotoIntakeCollection as Nt, MAX_EXTRACTED_TEXT as O, VideoCollection as Oi, QuestionResponse as On, CourseArchiveDecisionCollection as Or, WorkbookPhotoAsset as Ot, LessonWorkService as P, ScannedSubmission as Pi, effectiveCourseVisibility as Pn, CourseDraftHeadCollection as Pr, FamilyIntake as Pt, LearnerValidationError as Q, MATERIAL_RENDITIONS as Qi, PrintableStorageNotConfiguredError as Qn, LessonRevisionCollection as Qr, WorkFeedbackDeliveryCollection as Qt, OwnAttestationConflictError as R, parseQuestionRubric as Ri, planProposal as Rn, CourseRevisionCollection as Rr, FamilyIntakeDecision as Rt, MAX_EXTRACTED_ANCHORS as S, LessonSelfAttestationCollection as Si, LearningObjective as Sn, MAX_REFUSAL_CATEGORY_LENGTH as Sr, GroupSharingPolicyValidationError as St, MAX_EXTRACTED_QUESTIONS as T, SubmissionAssetCollection as Ti, LearningObjectiveRevisionCollection as Tn, delegationRecordFields as Tr, familyIntakeScopeId as Tt, submissionPageKey as U, GUARDIAN_ONLY_RELATIONSHIPS as Ui, LearningObjectiveKeyConflictError as Un, CurriculumCreationRecord as Ur, LessonActivity as Ut, SubmissionService as V, CourseAssetCollection as Vi, GENERATED_PRINTABLE_AUDIENCES as Vn, CourseVisibilityChange as Vr, WorkCompletionDecisionCollection as Vt, ENROLLMENT_STATUS_TRANSITIONS as W, LESSON_ASSET_DEFAULT_AUDIENCE as Wi, LearningObjectiveNotFoundError as Wn, CurriculumCreationRecordCollection as Wr, LessonActivityCollection as Wt, LearnerHistoryUnavailableError as X, LessonExternalVideo as Xi, PrintableOwnerError as Xn, EnrollmentReleasePinCollection as Xr, WorkAttemptCollection as Xt, LearnerAuthorizationError as Y, LessonAsset as Yi, MAX_RESOLVE_QUESTION_REVISIONS as Yn, EnrollmentReleasePin as Yr, WorkAttempt as Yt, LearnerService as Z, LessonExternalVideoCollection as Zi, PrintableStorageCleanupError as Zn, LessonRevision as Zr, WorkFeedbackDelivery as Zt, EXTRACTED_OMISSION_REASONS as _, Reading as _a, GradeCollection as _i, renderGuardianWorksheetPdf as _n, CourseProposalFieldError as _r, LessonProgressTransitionCollection as _t, questionExtractionProposalDigest as a, LessonProgress as aa, mostRestrictiveLicense as ai, aggregateWorkQuestions as an, QuestionReferenceError as ar, MIN_LESSONS_PER_WEEK as at, EXTRACTED_QUESTION_STATUSES as b, LessonCollection as bi, GeneratedPrintable as bn, MAX_GENERATION_GRADE as br, GroupSharingAuthorizationError as bt, QuestionExtractionRecord as c, interestThemeFromColumns as ca, AttestationCollection as ci, LessonPage as cn, COURSE_GENERATION_STATUSES as cr, EnrollmentPaceChange as ct, extractedQuestionContent as d, COURSE_VISIBILITIES as da, CourseSectionCollection as di, validLessonPageKey as dn, CourseGenerationBriefError as dr, EnrollmentStatusChangeCollection as dt, materialAudience as ea, ReleasedLessonCollection as ei, WorkFeedbackReleaseCollection as en, QUESTION_FIELDS as er, LessonPutBackError as et, matchExtractedQuestions as f, Course as fa, CurriculumCollection as fi, QuestionAuthoringService as fn, CourseGenerationInProgressError as fr, LearnerInterestThemeChange as ft, EXTRACTED_MEDIA_KINDS as g, Lesson as ga, ExternalCourseCollection as gi, worksheetVersionId as gn, CourseGeneratorNotConfiguredError as gr, LessonProgressTransition as gt, EXTRACTED_LEGIBILITY as h, ExternalCourse as ha, ExternalCompletionCollection as hi, canonical$2 as hn, CourseGenerationRefusal as hr, LessonCompletionRecordCollection as ht, QuestionExtractionService as i, Learner as ia, isGroupShareableLicense as ii, WorkCompletionConflictError as in, QuestionNotFoundError as ir, MAX_NEXT_LESSONS_COUNT as it, QuestionExtractionNotFoundError as j, ExternalCompletion as ji, QuestionRevisionCollection as jn, CourseDiscardTombstone as jr, WorkbookPhotoDecisionCollection as jt, QUESTION_EXTRACTION_SOURCE_KINDS as k, WorksheetCollection as ki, QuestionResponseCollection as kn, CourseCoverChange as kr, WorkbookPhotoAssetCollection as kt, QuestionExtractionRecordCollection as l, parseInterestTheme as la, AuthoredCourseCollection as li, LessonPageCollection as ln, COURSE_GENERATION_TARGETS as lr, EnrollmentPaceChangeCollection as lt, questionExtractionSourceIdentity as m, Exercise as ma, ExerciseCollection as mi, buildLearnerWorksheet as mn, CourseGenerationOutputError as mr, LessonCompletionRecord as mt, preservePhotoProposal as n, Curriculum as na, AUTHORED_RELEASE_SOURCE_KEY as ni, WORK_MATH_VOCABULARY as nn, QUESTION_PROVENANCES as nr, LessonWorkOwnedError as nt, ExtractedQuestionLink as o, MAX_INTEREST_THEME_KEY_CHARS as oa, CatalogAuthorizationError as oi, validateWorkAssessment as on, WorksheetVersionMismatchError as or, enrollmentView as ot, normalizeQuestionExtraction as p, CourseSection as pa, EnrollmentCollection as pi, buildGuardianWorksheet as pn, CourseGenerationNotFoundError as pr, LearnerInterestThemeChangeCollection as pt, EnrollmentStatusTransitionError as q, LESSON_STEP_LABEL_MAX as qi, MAX_PRINTABLE_TEXT_LENGTH as qn, EnrollmentAutoUpgradeSkip as qr, WorkAssessmentRevision as qt, validateWorkbookPhotoIdentification as r, Enrollment as ra, isAuthoredReleaseGroupShareable as ri, WorkAssessmentValidationError as rn, QUESTION_RESPONSE_TYPES as rr, MAX_LESSONS_PER_WEEK as rt, ExtractedQuestionLinkCollection as s, MAX_INTEREST_THEME_LABEL_CHARS as sa, CatalogService as si, workAggregateFeedback as sn, groupSharingDigest as sr, requirePolicyBasis as st, WorkbookPhotoService as t, parseYouTubeReference as ta, AUTHORED_IDENTITY_SOURCE_KEY as ti, LessonWorkPlacementError as tn, QUESTION_PROMPT_SUMMARY_LENGTH as tr, LessonSkippedError as tt, QUESTION_EXTRACTION_ARGS_SCHEMA as u, AuthoredCourse as ua, CourseCollection as ui, lessonPageView as un, COURSE_GENERATION_TERMINAL as ur, EnrollmentStatusChange as ut, EXTRACTED_ORIGINS as v, Video as va, LearnerCollection as vi, renderLearnerWorksheetPdf as vn, CourseProposalNotFoundError as vr, evaluateGroupSharingPolicy as vt, MAX_EXTRACTED_PAGES as w, ScannedSubmissionCollection as wi, LearningObjectiveRevision as wn, delegationActorFields as wr, validateGroupSharingPolicy as wt, EXTRACTED_READABILITY as x, LessonProgressCollection as xi, GeneratedPrintableCollection as xn, MAX_GENERATION_OBJECTIVE_CHARS as xr, GroupSharingConflictError as xt, EXTRACTED_PAGE_ROLES as y, Worksheet as ya, LessonAssetCollection as yi, es_exports as yn, MAX_GENERATION_BRIEF_FIELD_CHARS as yr, summarizeWorkEvidence as yt, OwnAttestationLessonKindError as z, validateRubricResults as zi, validateCourseGenerationOutput as zn, CourseUpgradeChoice as zr, FamilyIntakeDecisionCollection as zt };
36972
37094
 
36973
- //# sourceMappingURL=workbook-photo-service-DGIkYT6Q.js.map
37095
+ //# sourceMappingURL=workbook-photo-service-DEb2LSC0.js.map