@playcademy/sandbox 0.12.2-beta.1 → 0.12.2-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +511 -155
  2. package/dist/server.js +511 -155
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2517,7 +2517,7 @@ var package_default;
2517
2517
  var init_package = __esm(() => {
2518
2518
  package_default = {
2519
2519
  name: "@playcademy/sandbox",
2520
- version: "0.12.2-beta.1",
2520
+ version: "0.12.2-beta.2",
2521
2521
  description: "Local development server for Playcademy game development",
2522
2522
  type: "module",
2523
2523
  exports: {
@@ -14774,6 +14774,7 @@ var init_schema = __esm(() => {
14774
14774
  assessmentArtifactBucket: stringType().optional(),
14775
14775
  cdnOrigin: stringType().url().startsWith("https://").optional(),
14776
14776
  queueIngressSecret: stringType().optional(),
14777
+ embedAdminToken: stringType().optional(),
14777
14778
  secretsManifestPepper: stringType().optional()
14778
14779
  }).superRefine((config, ctx) => {
14779
14780
  if (config.isLocal && !config.baseUrl) {
@@ -14940,6 +14941,17 @@ var init_table_utils = __esm(() => {
14940
14941
  });
14941
14942
 
14942
14943
  // ../../node_modules/.bun/drizzle-orm@0.45.3+c063d24bf874cbdc/node_modules/drizzle-orm/pg-core/foreign-keys.js
14944
+ function foreignKey(config) {
14945
+ function mappedConfig() {
14946
+ const { name: name2, columns, foreignColumns } = config;
14947
+ return {
14948
+ name: name2,
14949
+ columns,
14950
+ foreignColumns
14951
+ };
14952
+ }
14953
+ return new ForeignKeyBuilder(mappedConfig);
14954
+ }
14943
14955
  var ForeignKeyBuilder, ForeignKey;
14944
14956
  var init_foreign_keys = __esm(() => {
14945
14957
  init_entity();
@@ -17812,6 +17824,16 @@ function ilike(column, value) {
17812
17824
  function notIlike(column, value) {
17813
17825
  return sql`${column} not ilike ${value}`;
17814
17826
  }
17827
+ function arrayContains(column, values) {
17828
+ if (Array.isArray(values)) {
17829
+ if (values.length === 0) {
17830
+ throw new Error("arrayContains requires at least one value");
17831
+ }
17832
+ const array = sql`${bindIfParam(values, column)}`;
17833
+ return sql`${column} @> ${array}`;
17834
+ }
17835
+ return sql`${column} @> ${bindIfParam(values, column)}`;
17836
+ }
17815
17837
  var eq = (left, right) => {
17816
17838
  return sql`${left} = ${bindIfParam(right, left)}`;
17817
17839
  }, ne = (left, right) => {
@@ -20742,7 +20764,7 @@ var init_table6 = __esm(() => {
20742
20764
  });
20743
20765
 
20744
20766
  // ../data/src/domains/timeback/table.ts
20745
- var gameTimebackIntegrationStatusEnum, gameTimebackAssessmentPurposeEnum, gameTimebackAssessmentStatusEnum, gameTimebackIntegrations, gameTimebackAssessmentTests, gameTimebackMetricDiscrepancyVerifications, gameTimebackActivityCompletions;
20767
+ var gameTimebackIntegrationStatusEnum, gameTimebackIntegrationKindEnum, gameTimebackAssessmentPurposeEnum, gameTimebackAssessmentStatusEnum, gameTimebackIntegrations, gameTimebackAssessmentTests, gameTimebackMetricDiscrepancyVerifications, gameTimebackActivityCompletions;
20746
20768
  var init_table7 = __esm(() => {
20747
20769
  init_drizzle_orm();
20748
20770
  init_pg_core();
@@ -20753,6 +20775,10 @@ var init_table7 = __esm(() => {
20753
20775
  "active",
20754
20776
  "deactivated"
20755
20777
  ]);
20778
+ gameTimebackIntegrationKindEnum = pgEnum("game_timeback_integration_kind", [
20779
+ "base",
20780
+ "hole_filling"
20781
+ ]);
20756
20782
  gameTimebackAssessmentPurposeEnum = pgEnum("game_timeback_assessment_purpose", ASSESSMENT_PURPOSES);
20757
20783
  gameTimebackAssessmentStatusEnum = pgEnum("game_timeback_assessment_status", [
20758
20784
  "draft",
@@ -20766,6 +20792,8 @@ var init_table7 = __esm(() => {
20766
20792
  grade: integer("grade").notNull(),
20767
20793
  subject: text("subject").notNull(),
20768
20794
  status: gameTimebackIntegrationStatusEnum("status"),
20795
+ kind: gameTimebackIntegrationKindEnum("kind").notNull().default("base"),
20796
+ baseIntegrationId: uuid("base_integration_id"),
20769
20797
  totalXp: integer("total_xp"),
20770
20798
  lastVerifiedAt: timestamp("last_verified_at", { withTimezone: true }),
20771
20799
  deactivatedAt: timestamp("deactivated_at", { withTimezone: true }),
@@ -20773,7 +20801,19 @@ var init_table7 = __esm(() => {
20773
20801
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
20774
20802
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
20775
20803
  }, (table) => [
20776
- uniqueIndex("game_timeback_integrations_game_grade_subject_idx").on(table.gameId, table.grade, table.subject).where(sql`${table.status} IS DISTINCT FROM 'deactivated'`)
20804
+ uniqueIndex("game_timeback_integrations_game_grade_subject_idx").on(table.gameId, table.grade, table.subject).where(sql`${table.status} IS DISTINCT FROM 'deactivated' AND ${table.kind} = 'base'`),
20805
+ uniqueIndex("game_timeback_integrations_hole_filling_course_idx").on(table.gameId, table.courseId).where(sql`${table.kind} = 'hole_filling'`),
20806
+ index("game_timeback_integrations_base_integration_idx").on(table.baseIntegrationId),
20807
+ foreignKey({
20808
+ name: "game_timeback_integrations_base_integration_id_fk",
20809
+ columns: [table.baseIntegrationId],
20810
+ foreignColumns: [table.id]
20811
+ }),
20812
+ check("game_timeback_integrations_kind_base_check", sql`(
20813
+ (${table.kind}::text = 'base' AND ${table.baseIntegrationId} IS NULL)
20814
+ OR
20815
+ (${table.kind}::text = 'hole_filling' AND ${table.baseIntegrationId} IS NOT NULL)
20816
+ )`)
20777
20817
  ]);
20778
20818
  gameTimebackAssessmentTests = pgTable("game_timeback_assessment_tests", {
20779
20819
  id: uuid("id").primaryKey().defaultRandom(),
@@ -20855,7 +20895,7 @@ var init_table7 = __esm(() => {
20855
20895
  });
20856
20896
 
20857
20897
  // ../data/src/domains/embed/table.ts
20858
- var embedPartners, embedPartnerGames, embedLaunches, embedPartnersRelations, embedPartnerGamesRelations, embedLaunchesRelations;
20898
+ var embedPartners, embedLaunches, embedLaunchesRelations;
20859
20899
  var init_table8 = __esm(() => {
20860
20900
  init_drizzle_orm();
20861
20901
  init_pg_core();
@@ -20870,15 +20910,6 @@ var init_table8 = __esm(() => {
20870
20910
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
20871
20911
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
20872
20912
  });
20873
- embedPartnerGames = pgTable("embed_partner_games", {
20874
- id: uuid("id").primaryKey().defaultRandom(),
20875
- partnerId: uuid("partner_id").notNull().references(() => embedPartners.id, { onDelete: "cascade" }),
20876
- gameId: uuid("game_id").notNull().references(() => games.id, { onDelete: "cascade" }),
20877
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
20878
- }, (table) => [
20879
- uniqueIndex("embed_partner_games_partner_game_idx").on(table.partnerId, table.gameId),
20880
- index("embed_partner_games_game_idx").on(table.gameId)
20881
- ]);
20882
20913
  embedLaunches = pgTable("embed_launches", {
20883
20914
  id: uuid("id").primaryKey().defaultRandom(),
20884
20915
  partnerId: uuid("partner_id").notNull().references(() => embedPartners.id, { onDelete: "restrict" }),
@@ -20897,19 +20928,6 @@ var init_table8 = __esm(() => {
20897
20928
  index("embed_launches_user_idx").on(table.userId),
20898
20929
  index("embed_launches_game_idx").on(table.gameId)
20899
20930
  ]);
20900
- embedPartnersRelations = relations(embedPartners, ({ many }) => ({
20901
- games: many(embedPartnerGames)
20902
- }));
20903
- embedPartnerGamesRelations = relations(embedPartnerGames, ({ one }) => ({
20904
- partner: one(embedPartners, {
20905
- fields: [embedPartnerGames.partnerId],
20906
- references: [embedPartners.id]
20907
- }),
20908
- game: one(games, {
20909
- fields: [embedPartnerGames.gameId],
20910
- references: [games.id]
20911
- })
20912
- }));
20913
20931
  embedLaunchesRelations = relations(embedLaunches, ({ one }) => ({
20914
20932
  partner: one(embedPartners, {
20915
20933
  fields: [embedLaunches.partnerId],
@@ -20940,10 +20958,7 @@ __export(exports_tables_index, {
20940
20958
  developerStatusEnum: () => developerStatusEnum,
20941
20959
  embedLaunches: () => embedLaunches,
20942
20960
  embedLaunchesRelations: () => embedLaunchesRelations,
20943
- embedPartnerGames: () => embedPartnerGames,
20944
- embedPartnerGamesRelations: () => embedPartnerGamesRelations,
20945
20961
  embedPartners: () => embedPartners,
20946
- embedPartnersRelations: () => embedPartnersRelations,
20947
20962
  gameCustomHostnames: () => gameCustomHostnames,
20948
20963
  gameDashboardUserRoleEnum: () => gameDashboardUserRoleEnum,
20949
20964
  gameDashboardUsers: () => gameDashboardUsers,
@@ -20962,6 +20977,7 @@ __export(exports_tables_index, {
20962
20977
  gameTimebackAssessmentPurposeEnum: () => gameTimebackAssessmentPurposeEnum,
20963
20978
  gameTimebackAssessmentStatusEnum: () => gameTimebackAssessmentStatusEnum,
20964
20979
  gameTimebackAssessmentTests: () => gameTimebackAssessmentTests,
20980
+ gameTimebackIntegrationKindEnum: () => gameTimebackIntegrationKindEnum,
20965
20981
  gameTimebackIntegrationStatusEnum: () => gameTimebackIntegrationStatusEnum,
20966
20982
  gameTimebackIntegrations: () => gameTimebackIntegrations,
20967
20983
  gameTimebackMetricDiscrepancyVerifications: () => gameTimebackMetricDiscrepancyVerifications,
@@ -22869,10 +22885,16 @@ var init_helpers = __esm(() => {
22869
22885
  });
22870
22886
 
22871
22887
  // ../data/src/domains/timeback/helpers.ts
22888
+ function isBaseGameTimebackIntegration() {
22889
+ return sql`${gameTimebackIntegrations.kind} = ${BASE_GAME_TIMEBACK_INTEGRATION_KIND}`;
22890
+ }
22872
22891
  function isActiveGameTimebackIntegrationStatus(statusColumn = gameTimebackIntegrations.status) {
22873
22892
  return sql`${statusColumn} IS DISTINCT FROM ${DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS}`;
22874
22893
  }
22875
- var ACTIVE_GAME_TIMEBACK_INTEGRATION_STATUS = "active", DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS = "deactivated";
22894
+ function isActiveBaseGameTimebackIntegration() {
22895
+ return sql`(${isActiveGameTimebackIntegrationStatus()} AND ${isBaseGameTimebackIntegration()})`;
22896
+ }
22897
+ var ACTIVE_GAME_TIMEBACK_INTEGRATION_STATUS = "active", DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS = "deactivated", BASE_GAME_TIMEBACK_INTEGRATION_KIND = "base", HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND = "hole_filling";
22876
22898
  var init_helpers2 = __esm(() => {
22877
22899
  init_drizzle_orm();
22878
22900
  init_table7();
@@ -39449,12 +39471,12 @@ function resolveIntegrationSubject(patchSubject, liveSubject, storedSubject) {
39449
39471
  function filterEnrollmentsByGame(enrollments, gameId) {
39450
39472
  return enrollments.filter((enrollment) => enrollment.gameId === gameId).map(({ gameId: _, ...enrollment }) => enrollment);
39451
39473
  }
39452
- function mapEnrollmentsToUserEnrollments(enrollments, integrations) {
39474
+ function mapEnrollmentsToUserEnrollments(enrollments, integrations, baseCourseIdByIntegrationId = new Map) {
39453
39475
  const enrollmentByCourse = new Map(enrollments.map((enrollment) => [enrollment.courseId, enrollment]));
39454
39476
  const courseToSchool = new Map(enrollments.filter((enrollment) => enrollment.school?.id).map((enrollment) => [enrollment.courseId, enrollment.school.id]));
39455
39477
  return integrations.map((integration) => {
39456
39478
  const enrollment = enrollmentByCourse.get(integration.courseId);
39457
- return {
39479
+ const userEnrollment = {
39458
39480
  gameId: integration.gameId,
39459
39481
  grade: integration.grade,
39460
39482
  subject: integration.subject,
@@ -39462,6 +39484,17 @@ function mapEnrollmentsToUserEnrollments(enrollments, integrations) {
39462
39484
  orgId: courseToSchool.get(integration.courseId),
39463
39485
  ...enrollment ? { id: enrollment.sourcedId } : {}
39464
39486
  };
39487
+ if (integration.kind !== HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND) {
39488
+ return userEnrollment;
39489
+ }
39490
+ const baseCourseId = integration.baseIntegrationId ? baseCourseIdByIntegrationId.get(integration.baseIntegrationId) : undefined;
39491
+ return {
39492
+ ...userEnrollment,
39493
+ kind: HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND,
39494
+ ...baseCourseId && { baseCourseId },
39495
+ ...enrollment?.beginDate !== undefined && { beginDate: enrollment.beginDate },
39496
+ ...enrollment?.metadata && { enrollmentMetadata: enrollment.metadata }
39497
+ };
39465
39498
  });
39466
39499
  }
39467
39500
  function buildGameTimebackSummaries(integrations) {
@@ -39944,7 +39977,7 @@ var init_game_service = __esm(() => {
39944
39977
  const db = this.deps.db;
39945
39978
  const integrations = await db.query.gameTimebackIntegrations.findMany({
39946
39979
  columns: { gameId: true, subject: true },
39947
- where: isActiveGameTimebackIntegrationStatus(),
39980
+ where: isActiveBaseGameTimebackIntegration(),
39948
39981
  orderBy: [asc(gameTimebackIntegrations.createdAt)]
39949
39982
  });
39950
39983
  const subjectMap = {};
@@ -39968,9 +40001,7 @@ var init_game_service = __esm(() => {
39968
40001
  }
39969
40002
  const integrations = await db.query.gameTimebackIntegrations.findMany({
39970
40003
  columns: { gameId: true, subject: true, status: true },
39971
- ...accessibleGameIds && {
39972
- where: inArray(gameTimebackIntegrations.gameId, [...accessibleGameIds])
39973
- },
40004
+ where: and(accessibleGameIds ? inArray(gameTimebackIntegrations.gameId, [...accessibleGameIds]) : undefined, isBaseGameTimebackIntegration()),
39974
40005
  orderBy: [asc(gameTimebackIntegrations.createdAt)]
39975
40006
  });
39976
40007
  return buildGameTimebackSummaries(integrations);
@@ -50401,7 +50432,7 @@ function rejectSystemManagedReview(input, context) {
50401
50432
  });
50402
50433
  }
50403
50434
  }
50404
- var TimebackGradeSchema, TimebackSubjectSchema, CourseGoalsSchema, UpdateGameTimebackIntegrationRequestSchema, CreateGameTimebackIntegrationRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema, GameRunMetricsSchema, GameCourseMetricsSchema, GameMetricsResponseSchema, AdvanceCourseRequestSchema, UnenrollCourseRequestSchema, HeartbeatRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, ADMIN_GRANT_XP_MIN = -1e5, ADMIN_GRANT_XP_MAX = 1e5, ADMIN_GRANT_XP_AMOUNT_RANGE_MESSAGE, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema, ReconcileMasteryForConfigChangeSchema, EnrollStudentRequestSchema, UnenrollStudentRequestSchema, ReactivateEnrollmentRequestSchema, VerifyTimebackMetricDiscrepancyRequestSchema, AssessmentPurposeSchema, AssessmentStatusSchema, AssessmentKeySchema, DiagnosticKeySchema, DiagnosticRoutingManifestSchema, DiagnosticAssessmentDefinitionInputSchema, AssessmentStandardRefSchema3, CreateAssessmentRequestSchema, UpdateAssessmentRequestSchema, CopyAssessmentRequestSchema, ReviseAssessmentRequestSchema, AssessmentAssociationImportEntrySchema, AttachExistingAssessmentsRequestSchema, ReorderQuestionsRequestSchema;
50435
+ var TimebackGradeSchema, TimebackSubjectSchema, CourseGoalsSchema, UpdateGameTimebackIntegrationRequestSchema, CreateGameTimebackIntegrationRequestSchema, RESERVED_TIMEBACK_COURSE_SOURCED_IDS, TimebackCourseSourcedIdSchema, CreateGameTimebackHoleFillingCourseRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema, GameRunMetricsSchema, GameCourseMetricsSchema, GameMetricsResponseSchema, AdvanceCourseRequestSchema, UnenrollCourseRequestSchema, HeartbeatRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, ADMIN_GRANT_XP_MIN = -1e5, ADMIN_GRANT_XP_MAX = 1e5, ADMIN_GRANT_XP_AMOUNT_RANGE_MESSAGE, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema, ReconcileMasteryForConfigChangeSchema, EnrollStudentRequestSchema, UnenrollStudentRequestSchema, ReactivateEnrollmentRequestSchema, VerifyTimebackMetricDiscrepancyRequestSchema, AssessmentPurposeSchema, AssessmentStatusSchema, AssessmentKeySchema, DiagnosticKeySchema, DiagnosticRoutingManifestSchema, DiagnosticAssessmentDefinitionInputSchema, AssessmentStandardRefSchema3, CreateAssessmentRequestSchema, UpdateAssessmentRequestSchema, CopyAssessmentRequestSchema, ReviseAssessmentRequestSchema, AssessmentAssociationImportEntrySchema, AttachExistingAssessmentsRequestSchema, ReorderQuestionsRequestSchema;
50405
50436
  var init_schemas4 = __esm(() => {
50406
50437
  init_esm();
50407
50438
  init_src();
@@ -50437,6 +50468,26 @@ var init_schemas4 = __esm(() => {
50437
50468
  masterableUnits: numberType().int().nonnegative(),
50438
50469
  level: stringType().trim().min(1).optional()
50439
50470
  });
50471
+ RESERVED_TIMEBACK_COURSE_SOURCED_IDS = new Set([
50472
+ "hole-filling",
50473
+ "removed",
50474
+ "components",
50475
+ "component-resources",
50476
+ "structure"
50477
+ ]);
50478
+ TimebackCourseSourcedIdSchema = stringType().trim().min(3).max(100).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, {
50479
+ message: "Use letters, numbers, dots, dashes, or underscores, starting with a letter or number"
50480
+ }).refine((value) => !RESERVED_TIMEBACK_COURSE_SOURCED_IDS.has(value.toLowerCase()), {
50481
+ message: "This course ID is reserved"
50482
+ });
50483
+ CreateGameTimebackHoleFillingCourseRequestSchema = objectType({
50484
+ baseCourseId: stringType().trim().min(1),
50485
+ title: stringType().trim().min(1),
50486
+ courseCode: stringType().trim().min(1),
50487
+ totalXp: numberType().int().positive(),
50488
+ masterableUnits: numberType().int().nonnegative(),
50489
+ courseId: TimebackCourseSourcedIdSchema.optional()
50490
+ });
50440
50491
  TimebackActivityDataSchema = objectType({
50441
50492
  activityId: stringType().min(1),
50442
50493
  activityName: stringType().optional(),
@@ -50768,21 +50819,20 @@ function isCanonicalOrigin(value) {
50768
50819
  }
50769
50820
  return url.protocol === "https:" || url.protocol === "http:" && isLocalHostname(url.hostname);
50770
50821
  }
50771
- var LOCAL_HOSTNAMES, EmbedOriginSchema, CreateEmbedPartnerSchema, UpdateEmbedPartnerSchema, CreateEmbedLaunchSchema, RedeemEmbedPassSchema;
50822
+ var LOCAL_HOSTNAMES, EmbedOriginSchema, EmbedPartnerClientIdSchema, EnsureEmbedPartnerSchema, AddEmbedPartnerOriginSchema, CreateEmbedLaunchSchema, RedeemEmbedPassSchema;
50772
50823
  var init_schemas5 = __esm(() => {
50773
50824
  init_esm();
50774
50825
  LOCAL_HOSTNAMES = new Set(["localhost", "127.0.0.1"]);
50775
50826
  EmbedOriginSchema = stringType().refine(isCanonicalOrigin, {
50776
50827
  message: "Origin must be a bare https origin like https://example.com, with no path or slash"
50777
50828
  });
50778
- CreateEmbedPartnerSchema = objectType({
50779
- name: stringType().trim().min(1).max(100),
50780
- clientId: stringType().trim().min(1).max(200),
50781
- allowedOrigins: arrayType(EmbedOriginSchema).max(20).default([]),
50782
- allowedGameIds: arrayType(stringType().uuid().transform((id) => id.toLowerCase())).max(200).default([]),
50783
- enabled: booleanType().default(true)
50829
+ EmbedPartnerClientIdSchema = stringType().trim().min(1).max(200);
50830
+ EnsureEmbedPartnerSchema = objectType({
50831
+ name: stringType().trim().min(1).max(100)
50832
+ }).strict();
50833
+ AddEmbedPartnerOriginSchema = objectType({
50834
+ origin: EmbedOriginSchema
50784
50835
  }).strict();
50785
- UpdateEmbedPartnerSchema = CreateEmbedPartnerSchema.omit({ clientId: true }).partial();
50786
50836
  CreateEmbedLaunchSchema = objectType({
50787
50837
  timebackId: stringType().trim().min(1).max(200),
50788
50838
  gameSlug: stringType().trim().min(1).max(100),
@@ -77940,6 +77990,7 @@ class CourseAssessments {
77940
77990
  }
77941
77991
  function buildCoursePayload(config) {
77942
77992
  return {
77993
+ ...config.course.sourcedId && { sourcedId: config.course.sourcedId },
77943
77994
  status: "active",
77944
77995
  title: config.course.title,
77945
77996
  courseCode: config.course.courseCode,
@@ -77986,6 +78037,16 @@ function buildComponentResourcePayload(config, sourcedIds) {
77986
78037
  lessonType: config.componentResource.lessonType
77987
78038
  };
77988
78039
  }
78040
+ async function findOrUndefined(request) {
78041
+ try {
78042
+ return await request;
78043
+ } catch (error) {
78044
+ if (isApiError(error) && error.statusCode === 404) {
78045
+ return;
78046
+ }
78047
+ throw error;
78048
+ }
78049
+ }
77989
78050
 
77990
78051
  class CourseIntegration {
77991
78052
  core;
@@ -78024,6 +78085,32 @@ class CourseIntegration {
78024
78085
  ...verboseData && { verboseData }
78025
78086
  };
78026
78087
  }
78088
+ async findCourse(courseId) {
78089
+ return findOrUndefined(this.core.api.oneroster.courses.get(courseId));
78090
+ }
78091
+ async resumeSetup(courseId, config) {
78092
+ const oneroster = this.core.api.oneroster;
78093
+ const sourcedIds = deriveSourcedIds(courseId);
78094
+ if (!await findOrUndefined(oneroster.courses.getComponent(sourcedIds.component))) {
78095
+ await oneroster.courses.createComponent(buildComponentPayload(config, sourcedIds));
78096
+ addEvent("timeback.component_created", {
78097
+ "app.timeback.component_id": sourcedIds.component
78098
+ });
78099
+ }
78100
+ if (!await findOrUndefined(oneroster.resources.get(sourcedIds.resource))) {
78101
+ await oneroster.resources.create(buildResourcePayload(config, sourcedIds.resource));
78102
+ addEvent("timeback.resource_created", {
78103
+ "app.timeback.resource_id": sourcedIds.resource
78104
+ });
78105
+ }
78106
+ if (!await findOrUndefined(oneroster.courses.getComponentResource(sourcedIds.componentResource))) {
78107
+ await oneroster.courses.createComponentResource(buildComponentResourcePayload(config, sourcedIds));
78108
+ addEvent("timeback.component_resource_link_created", {
78109
+ "app.timeback.component_resource_id": sourcedIds.componentResource
78110
+ });
78111
+ }
78112
+ await this.update(courseId, config);
78113
+ }
78027
78114
  async update(courseId, config) {
78028
78115
  const oneroster = this.core.api.oneroster;
78029
78116
  const sourcedIds = deriveSourcedIds(courseId);
@@ -78163,6 +78250,8 @@ function createCourseNamespace(core) {
78163
78250
  update: (courseId, config) => integration.update(courseId, config),
78164
78251
  verify: (courseId) => integration.verify(courseId),
78165
78252
  getConfig: (courseId) => integration.getConfig(courseId),
78253
+ findCourse: (courseId) => integration.findCourse(courseId),
78254
+ resumeSetup: (courseId, config) => integration.resumeSetup(courseId, config),
78166
78255
  cleanup: (courseId) => integration.cleanup(courseId),
78167
78256
  deactivateCourse: (courseId) => integration.updateCourseStatus(courseId, "tobedeleted"),
78168
78257
  reactivateCourse: (courseId) => integration.updateCourseStatus(courseId, "active"),
@@ -78247,7 +78336,9 @@ class RosterEnrollments {
78247
78336
  status: "active",
78248
78337
  grades,
78249
78338
  subjects,
78250
- school: enrollment.school
78339
+ school: enrollment.school,
78340
+ beginDate: enrollment.beginDate,
78341
+ ...enrollment.metadata && { metadata: enrollment.metadata }
78251
78342
  };
78252
78343
  });
78253
78344
  this.core.caches.enrollments.set(studentId, enrollments);
@@ -89051,7 +89142,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
89051
89142
  }) {
89052
89143
  await this.deps.validateDeveloperAccess(user, gameId);
89053
89144
  const integrations = await this.deps.db.query.gameTimebackIntegrations.findMany({
89054
- where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveGameTimebackIntegrationStatus())
89145
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration())
89055
89146
  });
89056
89147
  const matchingIntegrations = new Map;
89057
89148
  for (const integration of integrations) {
@@ -89440,7 +89531,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
89440
89531
  }) {
89441
89532
  await this.deps.validateDeveloperAccess(user, gameId);
89442
89533
  const integrations = await this.deps.db.query.gameTimebackIntegrations.findMany({
89443
- where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveGameTimebackIntegrationStatus())
89534
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration())
89444
89535
  });
89445
89536
  const orderedIntegrations = integrations.toSorted((left, right) => {
89446
89537
  const subjectDifference = left.subject.localeCompare(right.subject);
@@ -89519,7 +89610,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
89519
89610
  async candidateIntegrations(gameId, studentId, input, db = this.deps.db) {
89520
89611
  const filters = [
89521
89612
  eq(gameTimebackIntegrations.gameId, gameId),
89522
- isActiveGameTimebackIntegrationStatus()
89613
+ isActiveBaseGameTimebackIntegration()
89523
89614
  ];
89524
89615
  if (input.subject) {
89525
89616
  filters.push(eq(gameTimebackIntegrations.subject, input.subject));
@@ -92461,6 +92552,50 @@ function timebackConfigMatchesCreateIntegrationRequest(config, request) {
92461
92552
  const requestedLevel = request.level ?? deriveTimebackCourseLevelFromGrade(request.grade);
92462
92553
  return subject === request.subject && grade === request.grade && config.course.title === request.title && config.course.courseCode === request.courseCode && config.course.level === requestedLevel && (getTotalXpFromTimebackConfig(config) ?? null) === request.totalXp && (getMasterableUnitsFromTimebackConfig(config) ?? null) === request.masterableUnits;
92463
92554
  }
92555
+ function buildHoleFillingCourseConfig(baseConfig, request) {
92556
+ const baseCourseMetadata = isRecord4(baseConfig.course.metadata) ? baseConfig.course.metadata : {};
92557
+ const baseMetrics = isRecord4(baseCourseMetadata.metrics) ? baseCourseMetadata.metrics : {};
92558
+ const baseResourceMetadata = isRecord4(baseConfig.resource.metadata) ? baseConfig.resource.metadata : {};
92559
+ const basePlaycademy = isPlaycademyResourceMetadata(baseResourceMetadata.playcademy) ? baseResourceMetadata.playcademy : undefined;
92560
+ return {
92561
+ organization: baseConfig.organization,
92562
+ course: {
92563
+ ...baseConfig.course,
92564
+ ...request.courseId && { sourcedId: request.courseId },
92565
+ title: request.title,
92566
+ courseCode: request.courseCode,
92567
+ metadata: {
92568
+ ...baseCourseMetadata,
92569
+ courseType: HOLE_FILLING_COURSE_TYPE,
92570
+ baseCourseSourcedId: request.baseCourseId,
92571
+ metrics: {
92572
+ ...baseMetrics,
92573
+ totalXp: request.totalXp,
92574
+ totalLessons: request.masterableUnits,
92575
+ totalGrades: 1
92576
+ }
92577
+ }
92578
+ },
92579
+ component: { ...baseConfig.component, title: `${request.title} Activities` },
92580
+ resource: {
92581
+ ...baseConfig.resource,
92582
+ title: `${request.title} Game`,
92583
+ metadata: {
92584
+ ...baseResourceMetadata,
92585
+ xp: request.totalXp,
92586
+ playcademy: {
92587
+ ...basePlaycademy,
92588
+ mastery: {
92589
+ ...basePlaycademy?.mastery,
92590
+ masterableUnits: request.masterableUnits
92591
+ }
92592
+ }
92593
+ }
92594
+ },
92595
+ componentResource: { ...baseConfig.componentResource, title: `${request.title} Lesson` }
92596
+ };
92597
+ }
92598
+ var HOLE_FILLING_COURSE_TYPE = "hole-filling";
92464
92599
  var init_timeback_create_integration_util = __esm(() => {
92465
92600
  init_constants2();
92466
92601
  init_types2();
@@ -92476,7 +92611,7 @@ async function promoteCompletedCourse({
92476
92611
  enrollments: prefetchedEnrollments
92477
92612
  }) {
92478
92613
  const subjectIntegrations = await db.query.gameTimebackIntegrations.findMany({
92479
- where: and(eq(gameTimebackIntegrations.gameId, currentIntegration.gameId), eq(gameTimebackIntegrations.subject, currentIntegration.subject), isActiveGameTimebackIntegrationStatus())
92614
+ where: and(eq(gameTimebackIntegrations.gameId, currentIntegration.gameId), eq(gameTimebackIntegrations.subject, currentIntegration.subject), isActiveBaseGameTimebackIntegration())
92480
92615
  });
92481
92616
  const nextIntegration = subjectIntegrations.filter((integration) => integration.grade > currentIntegration.grade).toSorted((left, right) => left.grade - right.grade)[0];
92482
92617
  if (!nextIntegration) {
@@ -92543,6 +92678,8 @@ function toGameTimebackIntegration(integration) {
92543
92678
  courseId: integration.courseId,
92544
92679
  grade: integration.grade,
92545
92680
  subject: integration.subject,
92681
+ kind: integration.kind,
92682
+ baseIntegrationId: integration.baseIntegrationId,
92546
92683
  totalXp: integration.totalXp ?? null,
92547
92684
  status: integration.status ?? ACTIVE_GAME_TIMEBACK_INTEGRATION_STATUS,
92548
92685
  deactivatedAt: integration.deactivatedAt ?? null,
@@ -92580,6 +92717,9 @@ var init_timeback_removed_integration_util = __esm(() => {
92580
92717
 
92581
92718
  // ../api-core/src/services/timeback.service.ts
92582
92719
  import { createHash as createHash3 } from "node:crypto";
92720
+ function isUnregisteredHoleFillingOf(course, base) {
92721
+ return course.metadata?.courseType === HOLE_FILLING_COURSE_TYPE && course.metadata.baseCourseSourcedId === base.courseId;
92722
+ }
92583
92723
  async function findGameTimebackIntegrationForUpdate(db, condition) {
92584
92724
  const [integration] = await db.select().from(gameTimebackIntegrations).where(condition).limit(1).for("update");
92585
92725
  return integration;
@@ -92986,13 +93126,29 @@ var init_timeback_service = __esm(async () => {
92986
93126
  const integrations = await db.query.gameTimebackIntegrations.findMany({
92987
93127
  where: and(inArray(gameTimebackIntegrations.courseId, courseIds), isActiveGameTimebackIntegrationStatus())
92988
93128
  });
92989
- return mapEnrollmentsToUserEnrollments(enrollments, integrations);
93129
+ return mapEnrollmentsToUserEnrollments(enrollments, integrations, await this.loadBaseCourseIds(integrations));
92990
93130
  } catch (error) {
92991
93131
  setAttribute("app.timeback.enrollment_fetch", "failed");
92992
93132
  setAttribute("app.timeback.enrollment_fetch_error", errorMessage(error));
92993
93133
  return [];
92994
93134
  }
92995
93135
  }
93136
+ async loadBaseCourseIds(integrations) {
93137
+ const courseIdById = new Map(integrations.map((row) => [row.id, row.courseId]));
93138
+ const missingBaseIds = [
93139
+ ...new Set(integrations.map((row) => row.baseIntegrationId).filter((id) => id !== null && !courseIdById.has(id)))
93140
+ ];
93141
+ if (missingBaseIds.length > 0) {
93142
+ const baseRows = await this.deps.db.query.gameTimebackIntegrations.findMany({
93143
+ columns: { id: true, courseId: true },
93144
+ where: inArray(gameTimebackIntegrations.id, missingBaseIds)
93145
+ });
93146
+ for (const row of baseRows) {
93147
+ courseIdById.set(row.id, row.courseId);
93148
+ }
93149
+ }
93150
+ return courseIdById;
93151
+ }
92996
93152
  async setupIntegration(gameId, request, user) {
92997
93153
  return this.withClientTelemetry(async () => {
92998
93154
  const client = this.requireClient();
@@ -93003,7 +93159,7 @@ var init_timeback_service = __esm(async () => {
93003
93159
  "app.timeback.course_count": courses.length
93004
93160
  });
93005
93161
  const existing = await db.query.gameTimebackIntegrations.findMany({
93006
- where: eq(gameTimebackIntegrations.gameId, gameId)
93162
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), isBaseGameTimebackIntegration())
93007
93163
  });
93008
93164
  const integrations = [];
93009
93165
  const verboseData = [];
@@ -93151,13 +93307,13 @@ var init_timeback_service = __esm(async () => {
93151
93307
  "app.timeback.subject": request.subject
93152
93308
  });
93153
93309
  const activeConflict = await db.query.gameTimebackIntegrations.findFirst({
93154
- where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, request.grade), eq(gameTimebackIntegrations.subject, request.subject), isActiveGameTimebackIntegrationStatus())
93310
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, request.grade), eq(gameTimebackIntegrations.subject, request.subject), isActiveBaseGameTimebackIntegration())
93155
93311
  });
93156
93312
  if (activeConflict) {
93157
93313
  throw new ConflictError(`A TimeBack integration already exists for ${request.subject} Grade ${request.grade}`);
93158
93314
  }
93159
93315
  const removedCandidates = await db.query.gameTimebackIntegrations.findMany({
93160
- where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, request.grade), eq(gameTimebackIntegrations.subject, request.subject), eq(gameTimebackIntegrations.status, DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS))
93316
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, request.grade), eq(gameTimebackIntegrations.subject, request.subject), eq(gameTimebackIntegrations.status, DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS), isBaseGameTimebackIntegration())
93161
93317
  });
93162
93318
  for (const candidate of removedCandidates) {
93163
93319
  const config = await client.course.getConfig(candidate.courseId).catch((error) => {
@@ -93195,10 +93351,10 @@ var init_timeback_service = __esm(async () => {
93195
93351
  }
93196
93352
  }
93197
93353
  const reference = await db.query.gameTimebackIntegrations.findFirst({
93198
- where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveGameTimebackIntegrationStatus()),
93354
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration()),
93199
93355
  orderBy: (table, { asc }) => [asc(table.createdAt)]
93200
93356
  }) ?? await db.query.gameTimebackIntegrations.findFirst({
93201
- where: eq(gameTimebackIntegrations.gameId, gameId),
93357
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), isBaseGameTimebackIntegration()),
93202
93358
  orderBy: (table, { asc }) => [asc(table.createdAt)]
93203
93359
  });
93204
93360
  if (!reference) {
@@ -93228,10 +93384,77 @@ var init_timeback_service = __esm(async () => {
93228
93384
  return integration;
93229
93385
  });
93230
93386
  }
93387
+ async createHoleFillingCourse(gameId, user, request) {
93388
+ return this.withClientTelemetry(async () => {
93389
+ const client = this.requireClient();
93390
+ const db = this.deps.db;
93391
+ await this.deps.validateDeveloperAccess(user, gameId);
93392
+ TimebackService.recordIntegrationOperation("create_hole_filling_course", {
93393
+ "app.timeback.base_course_id": request.baseCourseId,
93394
+ "app.timeback.custom_course_id": Boolean(request.courseId)
93395
+ });
93396
+ const integration = await db.transaction(async (tx) => {
93397
+ const database = tx;
93398
+ const base = await findGameTimebackIntegrationForUpdate(database, and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, request.baseCourseId), isActiveBaseGameTimebackIntegration()));
93399
+ if (!base) {
93400
+ throw new NotFoundError("Active base course", `${gameId}:${request.baseCourseId}`);
93401
+ }
93402
+ const config = buildHoleFillingCourseConfig(await client.course.getConfig(base.courseId), request);
93403
+ let courseId = request.courseId;
93404
+ if (courseId) {
93405
+ const takenLocally = await database.query.gameTimebackIntegrations.findFirst({
93406
+ columns: { id: true },
93407
+ where: eq(gameTimebackIntegrations.courseId, courseId)
93408
+ });
93409
+ const remote = takenLocally ? undefined : await client.course.findCourse(courseId);
93410
+ if (takenLocally || remote && !isUnregisteredHoleFillingOf(remote, base)) {
93411
+ throw new ConflictError(`A Timeback course with ID "${courseId}" already exists`);
93412
+ }
93413
+ if (remote) {
93414
+ setAttribute("app.timeback.course_create_mode", "resumed");
93415
+ await client.course.resumeSetup(courseId, config);
93416
+ } else {
93417
+ courseId = undefined;
93418
+ }
93419
+ }
93420
+ if (!courseId) {
93421
+ const created = await client.course.setup(config);
93422
+ courseId = created.courseId;
93423
+ }
93424
+ setAttribute("app.timeback.course_id", courseId);
93425
+ const [inserted] = await database.insert(gameTimebackIntegrations).values({
93426
+ gameId,
93427
+ courseId,
93428
+ grade: base.grade,
93429
+ subject: base.subject,
93430
+ totalXp: request.totalXp,
93431
+ kind: HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND,
93432
+ baseIntegrationId: base.id
93433
+ }).returning();
93434
+ return inserted;
93435
+ });
93436
+ if (!integration) {
93437
+ throw new InternalError("Hole-filling course creation did not return an integration");
93438
+ }
93439
+ return toGameTimebackIntegration(integration);
93440
+ });
93441
+ }
93442
+ async getHoleFillingCourses(gameId, user) {
93443
+ await this.deps.validateGameManagementAccess(user, gameId);
93444
+ const rows = await this.deps.db.query.gameTimebackIntegrations.findMany({
93445
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.kind, HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND), isActiveGameTimebackIntegrationStatus()),
93446
+ orderBy: (table, { asc }) => [
93447
+ asc(table.subject),
93448
+ asc(table.grade),
93449
+ asc(table.createdAt)
93450
+ ]
93451
+ });
93452
+ return rows.map((row) => toGameTimebackIntegration(row));
93453
+ }
93231
93454
  async getIntegrations(gameId, user) {
93232
93455
  await this.deps.validateGameManagementAccess(user, gameId);
93233
93456
  const rows = await this.deps.db.query.gameTimebackIntegrations.findMany({
93234
- where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveGameTimebackIntegrationStatus())
93457
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration())
93235
93458
  });
93236
93459
  return rows.map((row) => toGameTimebackIntegration(row));
93237
93460
  }
@@ -93240,7 +93463,7 @@ var init_timeback_service = __esm(async () => {
93240
93463
  const client = this.requireClient();
93241
93464
  await this.deps.validateGameManagementAccess(user, gameId);
93242
93465
  const rows = await this.deps.db.query.gameTimebackIntegrations.findMany({
93243
- where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.status, DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS)),
93466
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.status, DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS), isBaseGameTimebackIntegration()),
93244
93467
  orderBy: (table, { asc }) => [asc(table.subject), asc(table.grade)]
93245
93468
  });
93246
93469
  setAttribute("app.timeback.removed_integration_count", rows.length);
@@ -93271,6 +93494,17 @@ var init_timeback_service = __esm(async () => {
93271
93494
  if (!integration) {
93272
93495
  throw new NotFoundError("Timeback course", `${gameId}:${courseId}`);
93273
93496
  }
93497
+ if (integration.kind === HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND) {
93498
+ throw new ConflictError("This is a hole-filling course and may be referenced by Timeback, so it can't be removed.");
93499
+ }
93500
+ const activeHoleFilling = await database.query.gameTimebackIntegrations.findFirst({
93501
+ columns: { id: true },
93502
+ where: and(eq(gameTimebackIntegrations.baseIntegrationId, integration.id), isActiveGameTimebackIntegrationStatus())
93503
+ });
93504
+ if (activeHoleFilling) {
93505
+ setAttribute("app.timeback.removal_blocked", "hole_filling_courses");
93506
+ throw new ConflictError("This course has hole-filling courses that may be referenced by Timeback, so it can't be removed.");
93507
+ }
93274
93508
  const activeEnrollments = await client.roster.getCourseRoster(courseId, {
93275
93509
  role: "student",
93276
93510
  includeUsers: false
@@ -93308,11 +93542,18 @@ var init_timeback_service = __esm(async () => {
93308
93542
  if (!integration) {
93309
93543
  throw new NotFoundError("Removed Timeback course", `${gameId}:${courseId}`);
93310
93544
  }
93311
- const activeConflict = await database.query.gameTimebackIntegrations.findFirst({
93312
- where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, integration.grade), eq(gameTimebackIntegrations.subject, integration.subject), isActiveGameTimebackIntegrationStatus())
93313
- });
93314
- if (activeConflict) {
93315
- throw new ConflictError(`An active TimeBack course already exists for ${integration.subject} Grade ${integration.grade}`);
93545
+ if (integration.kind === HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND) {
93546
+ const base = integration.baseIntegrationId ? await findGameTimebackIntegrationForUpdate(database, and(eq(gameTimebackIntegrations.id, integration.baseIntegrationId), isActiveBaseGameTimebackIntegration())) : undefined;
93547
+ if (!base) {
93548
+ throw new ConflictError("Restore the base course first: a hole-filling course can only be active beside its base course.");
93549
+ }
93550
+ } else {
93551
+ const activeConflict = await database.query.gameTimebackIntegrations.findFirst({
93552
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, integration.grade), eq(gameTimebackIntegrations.subject, integration.subject), isActiveBaseGameTimebackIntegration())
93553
+ });
93554
+ if (activeConflict) {
93555
+ throw new ConflictError(`An active TimeBack course already exists for ${integration.subject} Grade ${integration.grade}`);
93556
+ }
93316
93557
  }
93317
93558
  const now = new Date;
93318
93559
  const [updatedIntegration] = await database.update(gameTimebackIntegrations).set({
@@ -93351,52 +93592,66 @@ var init_timeback_service = __esm(async () => {
93351
93592
  TimebackService.recordIntegrationOperation("update_integration", {
93352
93593
  "app.timeback.course_id": courseId
93353
93594
  });
93354
- const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
93355
- where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId), isActiveGameTimebackIntegrationStatus())
93356
- });
93357
- if (!integration) {
93358
- throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
93359
- }
93360
- const timebackConfig = await client.course.getConfig(courseId);
93361
- const liveSubject = timebackConfig.course.subjects[0];
93362
- const subject = resolveIntegrationSubject(patch.subject, liveSubject, integration.subject);
93363
- if (!isTimebackSubject(subject)) {
93364
- throw new ValidationError(`Invalid subject "${subject}"`);
93365
- }
93366
- setAttributes({
93367
- "app.timeback.grade": integration.grade,
93368
- "app.timeback.previous_subject": integration.subject,
93369
- "app.timeback.next_subject": subject
93370
- });
93371
- if (subject !== integration.subject) {
93372
- const subjectConflict = await this.deps.db.query.gameTimebackIntegrations.findFirst({
93373
- where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, integration.grade), eq(gameTimebackIntegrations.subject, subject), isActiveGameTimebackIntegrationStatus())
93595
+ const updated = await this.deps.db.transaction(async (tx) => {
93596
+ const database = tx;
93597
+ const integration = await findGameTimebackIntegrationForUpdate(database, and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId), isActiveGameTimebackIntegrationStatus()));
93598
+ if (!integration) {
93599
+ throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
93600
+ }
93601
+ const timebackConfig = await client.course.getConfig(courseId);
93602
+ const liveSubject = timebackConfig.course.subjects[0];
93603
+ const subject = resolveIntegrationSubject(patch.subject, liveSubject, integration.subject);
93604
+ if (!isTimebackSubject(subject)) {
93605
+ throw new ValidationError(`Invalid subject "${subject}"`);
93606
+ }
93607
+ setAttributes({
93608
+ "app.timeback.grade": integration.grade,
93609
+ "app.timeback.previous_subject": integration.subject,
93610
+ "app.timeback.next_subject": subject
93374
93611
  });
93375
- if (subjectConflict && subjectConflict.id !== integration.id) {
93376
- throw new ValidationError(`A TimeBack integration already exists for ${subject} Grade ${integration.grade}`);
93612
+ if (subject !== integration.subject) {
93613
+ if (integration.kind === HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND) {
93614
+ throw new ValidationError("A hole-filling course must keep the subject of its base course");
93615
+ }
93616
+ const linkedHoleFilling = await database.query.gameTimebackIntegrations.findFirst({
93617
+ columns: { id: true },
93618
+ where: eq(gameTimebackIntegrations.baseIntegrationId, integration.id)
93619
+ });
93620
+ if (linkedHoleFilling) {
93621
+ throw new ValidationError("This course has hole-filling courses, so its subject cannot change");
93622
+ }
93377
93623
  }
93378
- }
93379
- const totalXp = "totalXp" in patch ? patch.totalXp ?? null : getTotalXpFromTimebackConfig(timebackConfig) ?? integration.totalXp ?? null;
93380
- const masterableUnits = "masterableUnits" in patch ? patch.masterableUnits ?? null : getMasterableUnitsFromTimebackConfig(timebackConfig);
93381
- setAttributes({
93382
- "app.timeback.total_xp": totalXp,
93383
- "app.timeback.masterable_units": masterableUnits
93624
+ if (subject !== integration.subject) {
93625
+ const subjectConflict = await database.query.gameTimebackIntegrations.findFirst({
93626
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, integration.grade), eq(gameTimebackIntegrations.subject, subject), isActiveBaseGameTimebackIntegration())
93627
+ });
93628
+ if (subjectConflict && subjectConflict.id !== integration.id) {
93629
+ throw new ValidationError(`A TimeBack integration already exists for ${subject} Grade ${integration.grade}`);
93630
+ }
93631
+ }
93632
+ const totalXp = "totalXp" in patch ? patch.totalXp ?? null : getTotalXpFromTimebackConfig(timebackConfig) ?? integration.totalXp ?? null;
93633
+ const masterableUnits = "masterableUnits" in patch ? patch.masterableUnits ?? null : getMasterableUnitsFromTimebackConfig(timebackConfig);
93634
+ setAttributes({
93635
+ "app.timeback.total_xp": totalXp,
93636
+ "app.timeback.masterable_units": masterableUnits
93637
+ });
93638
+ await client.course.update(courseId, TimebackService.patchTimebackConfig(timebackConfig, {
93639
+ ...patch,
93640
+ subject,
93641
+ totalXp,
93642
+ masterableUnits,
93643
+ grade: integration.grade
93644
+ }));
93645
+ const [row] = await database.update(gameTimebackIntegrations).set({
93646
+ subject,
93647
+ totalXp,
93648
+ updatedAt: new Date
93649
+ }).where(eq(gameTimebackIntegrations.id, integration.id)).returning();
93650
+ if (!row) {
93651
+ throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
93652
+ }
93653
+ return row;
93384
93654
  });
93385
- await client.course.update(courseId, TimebackService.patchTimebackConfig(timebackConfig, {
93386
- ...patch,
93387
- subject,
93388
- totalXp,
93389
- masterableUnits,
93390
- grade: integration.grade
93391
- }));
93392
- const [updated] = await this.deps.db.update(gameTimebackIntegrations).set({
93393
- subject,
93394
- totalXp,
93395
- updatedAt: new Date
93396
- }).where(eq(gameTimebackIntegrations.id, integration.id)).returning();
93397
- if (!updated) {
93398
- throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
93399
- }
93400
93655
  return toGameTimebackIntegration(updated);
93401
93656
  });
93402
93657
  }
@@ -93407,7 +93662,7 @@ var init_timeback_service = __esm(async () => {
93407
93662
  await this.deps.validateDeveloperAccess(user, gameId);
93408
93663
  TimebackService.recordIntegrationOperation("verify_integration");
93409
93664
  const integrations = await db.query.gameTimebackIntegrations.findMany({
93410
- where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveGameTimebackIntegrationStatus())
93665
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration())
93411
93666
  });
93412
93667
  if (integrations.length === 0) {
93413
93668
  throw new NotFoundError("Timeback integration", gameId);
@@ -93445,7 +93700,7 @@ var init_timeback_service = __esm(async () => {
93445
93700
  const client = this.requireClient();
93446
93701
  await this.deps.validateDeveloperAccess(user, gameId);
93447
93702
  const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
93448
- where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveGameTimebackIntegrationStatus())
93703
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration())
93449
93704
  });
93450
93705
  if (!integration) {
93451
93706
  throw new NotFoundError("Timeback integration", gameId);
@@ -93459,17 +93714,24 @@ var init_timeback_service = __esm(async () => {
93459
93714
  const db = this.deps.db;
93460
93715
  await this.deps.validateDeveloperAccess(user, gameId);
93461
93716
  TimebackService.recordIntegrationOperation("delete_integrations");
93462
- const integrations = await db.query.gameTimebackIntegrations.findMany({
93463
- where: eq(gameTimebackIntegrations.gameId, gameId)
93717
+ await db.transaction(async (tx) => {
93718
+ const database = tx;
93719
+ await database.select({ id: gameTimebackIntegrations.id }).from(gameTimebackIntegrations).where(eq(gameTimebackIntegrations.gameId, gameId)).for("update");
93720
+ const integrations = await database.select().from(gameTimebackIntegrations).where(eq(gameTimebackIntegrations.gameId, gameId));
93721
+ if (integrations.length === 0) {
93722
+ throw new NotFoundError("Timeback integration", gameId);
93723
+ }
93724
+ setAttribute("app.timeback.integration_count", integrations.length);
93725
+ const holeFillingCount = integrations.filter((integration) => integration.kind === HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND).length;
93726
+ if (holeFillingCount > 0) {
93727
+ setAttribute("app.timeback.hole_filling_count", holeFillingCount);
93728
+ throw new ConflictError(`This game has ${holeFillingCount} hole-filling course(s) that may be referenced by Timeback, so its Timeback courses can't be removed automatically. Contact a Playcademy admin.`);
93729
+ }
93730
+ for (const integration of integrations) {
93731
+ await client.course.cleanup(integration.courseId);
93732
+ }
93733
+ await database.delete(gameTimebackIntegrations).where(eq(gameTimebackIntegrations.gameId, gameId));
93464
93734
  });
93465
- if (integrations.length === 0) {
93466
- throw new NotFoundError("Timeback integration", gameId);
93467
- }
93468
- setAttribute("app.timeback.integration_count", integrations.length);
93469
- for (const integration of integrations) {
93470
- await client.course.cleanup(integration.courseId);
93471
- }
93472
- await db.delete(gameTimebackIntegrations).where(eq(gameTimebackIntegrations.gameId, gameId));
93473
93735
  });
93474
93736
  }
93475
93737
  static patchCourseMetadata(metadata2, totalXp, options) {
@@ -93649,7 +93911,7 @@ var init_timeback_service = __esm(async () => {
93649
93911
  });
93650
93912
  await this.deps.validateDeveloperAccess(user, gameId);
93651
93913
  const integration = await db.query.gameTimebackIntegrations.findFirst({
93652
- where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject), isActiveGameTimebackIntegrationStatus())
93914
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject), isActiveBaseGameTimebackIntegration())
93653
93915
  });
93654
93916
  if (!integration) {
93655
93917
  throw new NotFoundError(`Timeback integration for game (grade ${activityData.grade}, subject ${activityData.subject})`);
@@ -93792,7 +94054,7 @@ var init_timeback_service = __esm(async () => {
93792
94054
  const client = this.requireClient();
93793
94055
  const db = this.deps.db;
93794
94056
  const integrations = await db.query.gameTimebackIntegrations.findMany({
93795
- where: subject ? and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.subject, subject), isActiveGameTimebackIntegrationStatus()) : and(eq(gameTimebackIntegrations.gameId, gameId), isActiveGameTimebackIntegrationStatus())
94057
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), subject ? eq(gameTimebackIntegrations.subject, subject) : undefined, isActiveBaseGameTimebackIntegration())
93796
94058
  });
93797
94059
  if (integrations.length === 0) {
93798
94060
  throw new NotFoundError(subject ? `Timeback integrations for game (subject ${subject})` : "Timeback integrations for game");
@@ -93981,7 +94243,7 @@ var init_timeback_service = __esm(async () => {
93981
94243
  }
93982
94244
  const pendingHeartbeat = (async () => {
93983
94245
  const integration = await db.query.gameTimebackIntegrations.findFirst({
93984
- where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject), isActiveGameTimebackIntegrationStatus())
94246
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject), isActiveBaseGameTimebackIntegration())
93985
94247
  });
93986
94248
  if (!integration) {
93987
94249
  throw new NotFoundError(`Timeback integration for game (grade ${activityData.grade}, subject ${activityData.subject})`);
@@ -94111,7 +94373,7 @@ var init_timeback_service = __esm(async () => {
94111
94373
  const db = this.deps.db;
94112
94374
  await this.deps.validateDeveloperAccess(user, options.gameId);
94113
94375
  const integration = await db.query.gameTimebackIntegrations.findFirst({
94114
- where: and(eq(gameTimebackIntegrations.gameId, options.gameId), eq(gameTimebackIntegrations.subject, options.subject), isActiveGameTimebackIntegrationStatus())
94376
+ where: and(eq(gameTimebackIntegrations.gameId, options.gameId), eq(gameTimebackIntegrations.subject, options.subject), isActiveBaseGameTimebackIntegration())
94115
94377
  });
94116
94378
  if (!integration) {
94117
94379
  throw new ValidationError(`Subject "${options.subject}" is not configured for game ${options.gameId}`);
@@ -94980,9 +95242,47 @@ var init_account_service = __esm(async () => {
94980
95242
  await init_errors8();
94981
95243
  });
94982
95244
 
95245
+ // ../api-core/src/services/embed-partner.service.ts
95246
+ class EmbedPartnerService {
95247
+ deps;
95248
+ constructor(deps) {
95249
+ this.deps = deps;
95250
+ }
95251
+ async ensurePartner(clientId, name2) {
95252
+ const [partner] = await this.deps.db.insert(embedPartners).values({ clientId, name: name2 }).onConflictDoUpdate({
95253
+ target: embedPartners.clientId,
95254
+ set: { name: name2, updatedAt: new Date }
95255
+ }).returning();
95256
+ if (!partner) {
95257
+ throw new InternalError("Failed to ensure embed partner");
95258
+ }
95259
+ return partner;
95260
+ }
95261
+ addOrigin(clientId, origin) {
95262
+ return this.setOrigins(clientId, sql`CASE WHEN ${origin} = ANY(${embedPartners.allowedOrigins})
95263
+ THEN ${embedPartners.allowedOrigins}
95264
+ ELSE array_append(${embedPartners.allowedOrigins}, ${origin}) END`);
95265
+ }
95266
+ removeOrigin(clientId, origin) {
95267
+ return this.setOrigins(clientId, sql`array_remove(${embedPartners.allowedOrigins}, ${origin})`);
95268
+ }
95269
+ async setOrigins(clientId, origins) {
95270
+ const [partner] = await this.deps.db.update(embedPartners).set({ allowedOrigins: origins, updatedAt: new Date }).where(eq(embedPartners.clientId, clientId)).returning();
95271
+ if (!partner) {
95272
+ throw new NotFoundError("Embed partner", clientId);
95273
+ }
95274
+ return partner;
95275
+ }
95276
+ }
95277
+ var init_embed_partner_service = __esm(() => {
95278
+ init_drizzle_orm();
95279
+ init_tables_index();
95280
+ init_errors2();
95281
+ });
95282
+
94983
95283
  // ../api-core/src/utils/embed.util.ts
94984
- function canPartnerLaunch(partner, origin, gameId) {
94985
- return partner.enabled && partner.allowedOrigins.includes(origin) && partner.games.some((grant) => grant.gameId === gameId);
95284
+ function canPartnerLaunch(partner, origin) {
95285
+ return partner.enabled && partner.allowedOrigins.includes(origin);
94986
95286
  }
94987
95287
  function embedPageUrl(hubOrigin, slug, parentOrigin) {
94988
95288
  const url = new URL(`${EMBED_PAGE_PATH}/${encodeURIComponent(slug)}`, hubOrigin);
@@ -95010,12 +95310,9 @@ class EmbedService {
95010
95310
  return partner;
95011
95311
  }
95012
95312
  async isFrameOriginAllowed(origin, slug) {
95013
- const grants = await this.deps.db.select({
95014
- enabled: embedPartners.enabled,
95015
- allowedOrigins: embedPartners.allowedOrigins,
95016
- gameId: embedPartnerGames.gameId
95017
- }).from(embedPartnerGames).innerJoin(embedPartners, eq(embedPartners.id, embedPartnerGames.partnerId)).innerJoin(games, eq(games.id, embedPartnerGames.gameId)).where(eq(games.slug, slug));
95018
- return grants.some((grant) => canPartnerLaunch({ ...grant, games: [grant] }, origin, grant.gameId));
95313
+ const game = this.deps.db.select({ id: games.id }).from(games).where(eq(games.slug, slug));
95314
+ const [partner] = await this.deps.db.select({ id: embedPartners.id }).from(embedPartners).where(and(eq(embedPartners.enabled, true), arrayContains(embedPartners.allowedOrigins, [origin]), exists(game))).limit(1);
95315
+ return partner !== undefined;
95019
95316
  }
95020
95317
  async createLaunch(partner, input) {
95021
95318
  const game = await this.deps.db.query.games.findFirst({
@@ -95026,9 +95323,8 @@ class EmbedService {
95026
95323
  throw new NotFoundError("Game", input.gameSlug);
95027
95324
  }
95028
95325
  setAttributes({ "app.game.id": game.id, "app.game.slug": game.slug });
95029
- const grants = await this.grantsFor(partner.id, game.id);
95030
- if (!canPartnerLaunch({ ...partner, games: grants }, input.parentOrigin, game.id)) {
95031
- throw new AccessDeniedError("This partner may not launch that game into that origin");
95326
+ if (!canPartnerLaunch(partner, input.parentOrigin)) {
95327
+ throw new AccessDeniedError("This partner may not launch into that origin");
95032
95328
  }
95033
95329
  setAttributes({ "app.user.timeback_id": input.timebackId });
95034
95330
  const user = await this.deps.resolveTimebackStudent(input.timebackId);
@@ -95090,20 +95386,13 @@ class EmbedService {
95090
95386
  throw new NotFoundError("Embed launch", launchId);
95091
95387
  }
95092
95388
  setAttributes({ "app.game.id": launch.gameId, "app.user.id": launch.userId });
95093
- const grants = await this.grantsFor(partner.id, launch.gameId);
95094
- const outcome = renewalRefusal(launch, () => canPartnerLaunch({ ...partner, games: grants }, launch.parentOrigin, launch.gameId));
95389
+ const outcome = renewalRefusal(launch, () => canPartnerLaunch(partner, launch.parentOrigin));
95095
95390
  setAttributes({ "app.embed.outcome": outcome ?? "renewed" });
95096
95391
  if (outcome) {
95097
95392
  throw new AccessDeniedError("This launch has ended");
95098
95393
  }
95099
95394
  return this.deps.mintGameToken(launch.gameId, launch.userId);
95100
95395
  }
95101
- grantsFor(partnerId, gameId) {
95102
- return this.deps.db.query.embedPartnerGames.findMany({
95103
- where: and(eq(embedPartnerGames.partnerId, partnerId), eq(embedPartnerGames.gameId, gameId)),
95104
- columns: { gameId: true }
95105
- });
95106
- }
95107
95396
  async endLaunch(launchId) {
95108
95397
  await this.deps.db.update(embedLaunches).set({ endedAt: sql`coalesce(${embedLaunches.endedAt}, now())`, passHash: null }).where(eq(embedLaunches.id, launchId));
95109
95398
  }
@@ -95120,15 +95409,12 @@ class EmbedService {
95120
95409
  "app.embed.partner_id": launch.partnerId,
95121
95410
  "app.user.id": launch.userId
95122
95411
  });
95123
- const [partner, grants] = await Promise.all([
95124
- this.deps.db.query.embedPartners.findFirst({
95125
- where: eq(embedPartners.id, launch.partnerId),
95126
- columns: { enabled: true, allowedOrigins: true }
95127
- }),
95128
- this.grantsFor(launch.partnerId, launch.gameId)
95129
- ]);
95412
+ const partner = await this.deps.db.query.embedPartners.findFirst({
95413
+ where: eq(embedPartners.id, launch.partnerId),
95414
+ columns: { enabled: true, allowedOrigins: true }
95415
+ });
95130
95416
  const originMatches = launch.parentOrigin === input.parentOrigin;
95131
- const stillAllowed = partner !== undefined && canPartnerLaunch({ ...partner, games: grants }, launch.parentOrigin, launch.gameId);
95417
+ const stillAllowed = partner !== undefined && canPartnerLaunch(partner, launch.parentOrigin);
95132
95418
  if (!originMatches || !stillAllowed) {
95133
95419
  await this.endLaunch(launch.id);
95134
95420
  setAttributes({
@@ -95546,14 +95832,16 @@ function createStandaloneServices(deps) {
95546
95832
  return { ...profile, timeback: timebackData };
95547
95833
  }
95548
95834
  });
95835
+ const embedPartner = new EmbedPartnerService({ db });
95549
95836
  const verify = new VerifyService({
95550
95837
  db,
95551
95838
  validateGameToken: auth.validateGameToken.bind(auth)
95552
95839
  });
95553
- return { account, embed, lti, session, user, verify };
95840
+ return { account, embed, embedPartner, lti, session, user, verify };
95554
95841
  }
95555
95842
  var init_standalone = __esm(async () => {
95556
95843
  init_spans();
95844
+ init_embed_partner_service();
95557
95845
  init_embed_service();
95558
95846
  init_lti_service();
95559
95847
  init_session_service();
@@ -111998,7 +112286,7 @@ function ilike2(column6, value) {
111998
112286
  function notIlike2(column6, value) {
111999
112287
  return sql4`${column6} not ilike ${value}`;
112000
112288
  }
112001
- function arrayContains(column6, values) {
112289
+ function arrayContains2(column6, values) {
112002
112290
  if (Array.isArray(values)) {
112003
112291
  if (values.length === 0) {
112004
112292
  throw new Error("arrayContains requires at least one value");
@@ -135317,7 +135605,7 @@ params: ${params}`);
135317
135605
  and: () => and2,
135318
135606
  applyMixins: () => applyMixins2,
135319
135607
  arrayContained: () => arrayContained,
135320
- arrayContains: () => arrayContains,
135608
+ arrayContains: () => arrayContains2,
135321
135609
  arrayOverlaps: () => arrayOverlaps,
135322
135610
  asc: () => asc2,
135323
135611
  avg: () => avg,
@@ -151887,15 +152175,36 @@ function requireUuidParam(value, name2) {
151887
152175
  }
151888
152176
  return value;
151889
152177
  }
152178
+ function bearerToken(request) {
152179
+ const header = request.headers.get("Authorization");
152180
+ return header?.startsWith("Bearer ") ? header.slice("Bearer ".length) : "";
152181
+ }
151890
152182
  async function requirePartner(ctx) {
151891
- const header = ctx.request.headers.get("Authorization");
151892
- const token = header?.startsWith("Bearer ") ? header.slice("Bearer ".length) : "";
152183
+ const token = bearerToken(ctx.request);
151893
152184
  const verified = token ? await ctx.providers.auth.verifyEmbedPartnerToken(token) : null;
151894
152185
  if (!verified) {
151895
152186
  throw ApiError.unauthorized("A valid embed partner access token is required");
151896
152187
  }
151897
152188
  return ctx.services.embed.getPartnerByClientId(verified.clientId);
151898
152189
  }
152190
+ async function requireEmbedAdmin(ctx) {
152191
+ const expected = ctx.config.embedAdminToken;
152192
+ if (!expected) {
152193
+ throw ApiError.serviceUnavailable("Embed partner management is not configured");
152194
+ }
152195
+ const token = bearerToken(ctx.request);
152196
+ const [given, wanted] = token ? await Promise.all([sha256Hex(token), sha256Hex(expected)]) : [];
152197
+ if (!given || given !== wanted) {
152198
+ throw ApiError.unauthorized("A valid embed admin token is required");
152199
+ }
152200
+ }
152201
+ function requireClientIdParam(ctx) {
152202
+ const clientId = EmbedPartnerClientIdSchema.safeParse(ctx.params.clientId);
152203
+ if (!clientId.success) {
152204
+ throw ApiError.badRequest("Missing or invalid clientId");
152205
+ }
152206
+ return clientId.data;
152207
+ }
151899
152208
  async function createLaunch(ctx) {
151900
152209
  const partner = await requirePartner(ctx);
151901
152210
  const body2 = await parseRequestBody(ctx.request, CreateEmbedLaunchSchema);
@@ -151925,6 +152234,27 @@ async function redeemPass(ctx) {
151925
152234
  timeback: launch.profile.timeback
151926
152235
  };
151927
152236
  }
152237
+ async function ensurePartner(ctx) {
152238
+ await requireEmbedAdmin(ctx);
152239
+ const clientId = requireClientIdParam(ctx);
152240
+ const body2 = await parseRequestBody(ctx.request, EnsureEmbedPartnerSchema);
152241
+ return ctx.services.embedPartner.ensurePartner(clientId, body2.name);
152242
+ }
152243
+ async function addPartnerOrigin(ctx) {
152244
+ await requireEmbedAdmin(ctx);
152245
+ const clientId = requireClientIdParam(ctx);
152246
+ const body2 = await parseRequestBody(ctx.request, AddEmbedPartnerOriginSchema);
152247
+ return ctx.services.embedPartner.addOrigin(clientId, body2.origin);
152248
+ }
152249
+ async function removePartnerOrigin(ctx) {
152250
+ await requireEmbedAdmin(ctx);
152251
+ const clientId = requireClientIdParam(ctx);
152252
+ const origin = EmbedOriginSchema.safeParse(ctx.url.searchParams.get("origin"));
152253
+ if (!origin.success) {
152254
+ throw ApiError.badRequest("Missing or invalid origin");
152255
+ }
152256
+ return ctx.services.embedPartner.removeOrigin(clientId, origin.data);
152257
+ }
151928
152258
  var embed2;
151929
152259
  var init_embed_controller = __esm(() => {
151930
152260
  init_src();
@@ -151934,8 +152264,11 @@ var init_embed_controller = __esm(() => {
151934
152264
  init_utils7();
151935
152265
  init_embed_util();
151936
152266
  embed2 = defineControllerNames("embed", {
152267
+ addPartnerOrigin,
151937
152268
  createLaunch,
152269
+ ensurePartner,
151938
152270
  redeemPass,
152271
+ removePartnerOrigin,
151939
152272
  renewToken
151940
152273
  });
151941
152274
  });
@@ -152542,7 +152875,7 @@ function parseQtiLibraryParams(searchParams) {
152542
152875
  limit
152543
152876
  };
152544
152877
  }
152545
- var populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, getRemovedIntegrations, createIntegration, updateIntegration, deactivateCourse, reactivateCourse, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, prepareRuntimeAssessment, startRuntimeAssessment, getRuntimeAssessment, getLatestRuntimeAssessment, resumeRuntimeAssessment, saveRuntimeAssessment, recordRuntimeAssessmentProgress, submitRuntimeAssessmentItem, submitRuntimeAssessment, finalizeRuntimeAssessment, exportRuntimeAssessmentFixture, getStudentXp, getStudentMastery, getStudentHighestGradeMastered, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getGradeLevelTestResults, getGradeLevelTestReview, getActivityDetail, listMetricDiscrepancies, verifyMetricDiscrepancy, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, attachExistingAssessments, updateAssessment, reviseAssessment, synchronizeReviewBank, reorderQuestions, removeAssessment, listQuestions, listQuestionLibrary, listTestLibrary, copyAssessment, createQuestion, updateQuestion, removeQuestion, timeback2;
152878
+ var populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, getRemovedIntegrations, createIntegration, getHoleFillingCourses, createHoleFillingCourse, updateIntegration, deactivateCourse, reactivateCourse, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, prepareRuntimeAssessment, startRuntimeAssessment, getRuntimeAssessment, getLatestRuntimeAssessment, resumeRuntimeAssessment, saveRuntimeAssessment, recordRuntimeAssessmentProgress, submitRuntimeAssessmentItem, submitRuntimeAssessment, finalizeRuntimeAssessment, exportRuntimeAssessmentFixture, getStudentXp, getStudentMastery, getStudentHighestGradeMastered, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getGradeLevelTestResults, getGradeLevelTestReview, getActivityDetail, listMetricDiscrepancies, verifyMetricDiscrepancy, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, attachExistingAssessments, updateAssessment, reviseAssessment, synchronizeReviewBank, reorderQuestions, removeAssessment, listQuestions, listQuestionLibrary, listTestLibrary, copyAssessment, createQuestion, updateQuestion, removeQuestion, timeback2;
152546
152879
  var init_timeback_controller = __esm(() => {
152547
152880
  init_esm();
152548
152881
  init_src();
@@ -152621,6 +152954,27 @@ var init_timeback_controller = __esm(() => {
152621
152954
  const body2 = await parseRequestBody(ctx.request, CreateGameTimebackIntegrationRequestSchema);
152622
152955
  return ctx.services.timeback.createIntegration(gameId, ctx.user, body2);
152623
152956
  });
152957
+ getHoleFillingCourses = requireGameManagementAccess(async (ctx) => {
152958
+ const gameId = ctx.params.gameId;
152959
+ if (!gameId) {
152960
+ throw ApiError.badRequest("Missing gameId");
152961
+ }
152962
+ if (!isValidUUID(gameId)) {
152963
+ throw ApiError.unprocessableEntity("Invalid gameId format");
152964
+ }
152965
+ return ctx.services.timeback.getHoleFillingCourses(gameId, ctx.user);
152966
+ });
152967
+ createHoleFillingCourse = requireDeveloper(async (ctx) => {
152968
+ const gameId = ctx.params.gameId;
152969
+ if (!gameId) {
152970
+ throw ApiError.badRequest("Missing gameId");
152971
+ }
152972
+ if (!isValidUUID(gameId)) {
152973
+ throw ApiError.unprocessableEntity("Invalid gameId format");
152974
+ }
152975
+ const body2 = await parseRequestBody(ctx.request, CreateGameTimebackHoleFillingCourseRequestSchema);
152976
+ return ctx.services.timeback.createHoleFillingCourse(gameId, ctx.user, body2);
152977
+ });
152624
152978
  updateIntegration = requireDeveloper(async (ctx) => {
152625
152979
  const { gameId, courseId } = ctx.params;
152626
152980
  if (!gameId || !courseId) {
@@ -153405,6 +153759,8 @@ var init_timeback_controller = __esm(() => {
153405
153759
  getIntegrations,
153406
153760
  getRemovedIntegrations,
153407
153761
  createIntegration,
153762
+ getHoleFillingCourses,
153763
+ createHoleFillingCourse,
153408
153764
  updateIntegration,
153409
153765
  deactivateCourse,
153410
153766
  reactivateCourse,