@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/server.js CHANGED
@@ -697,7 +697,7 @@ var package_default;
697
697
  var init_package = __esm(() => {
698
698
  package_default = {
699
699
  name: "@playcademy/sandbox",
700
- version: "0.12.2-beta.1",
700
+ version: "0.12.2-beta.2",
701
701
  description: "Local development server for Playcademy game development",
702
702
  type: "module",
703
703
  exports: {
@@ -12954,6 +12954,7 @@ var init_schema = __esm(() => {
12954
12954
  assessmentArtifactBucket: stringType().optional(),
12955
12955
  cdnOrigin: stringType().url().startsWith("https://").optional(),
12956
12956
  queueIngressSecret: stringType().optional(),
12957
+ embedAdminToken: stringType().optional(),
12957
12958
  secretsManifestPepper: stringType().optional()
12958
12959
  }).superRefine((config, ctx) => {
12959
12960
  if (config.isLocal && !config.baseUrl) {
@@ -13120,6 +13121,17 @@ var init_table_utils = __esm(() => {
13120
13121
  });
13121
13122
 
13122
13123
  // ../../node_modules/.bun/drizzle-orm@0.45.3+c063d24bf874cbdc/node_modules/drizzle-orm/pg-core/foreign-keys.js
13124
+ function foreignKey(config) {
13125
+ function mappedConfig() {
13126
+ const { name: name2, columns, foreignColumns } = config;
13127
+ return {
13128
+ name: name2,
13129
+ columns,
13130
+ foreignColumns
13131
+ };
13132
+ }
13133
+ return new ForeignKeyBuilder(mappedConfig);
13134
+ }
13123
13135
  var ForeignKeyBuilder, ForeignKey;
13124
13136
  var init_foreign_keys = __esm(() => {
13125
13137
  init_entity();
@@ -15992,6 +16004,16 @@ function ilike(column, value) {
15992
16004
  function notIlike(column, value) {
15993
16005
  return sql`${column} not ilike ${value}`;
15994
16006
  }
16007
+ function arrayContains(column, values) {
16008
+ if (Array.isArray(values)) {
16009
+ if (values.length === 0) {
16010
+ throw new Error("arrayContains requires at least one value");
16011
+ }
16012
+ const array = sql`${bindIfParam(values, column)}`;
16013
+ return sql`${column} @> ${array}`;
16014
+ }
16015
+ return sql`${column} @> ${bindIfParam(values, column)}`;
16016
+ }
15995
16017
  var eq = (left, right) => {
15996
16018
  return sql`${left} = ${bindIfParam(right, left)}`;
15997
16019
  }, ne = (left, right) => {
@@ -18922,7 +18944,7 @@ var init_table6 = __esm(() => {
18922
18944
  });
18923
18945
 
18924
18946
  // ../data/src/domains/timeback/table.ts
18925
- var gameTimebackIntegrationStatusEnum, gameTimebackAssessmentPurposeEnum, gameTimebackAssessmentStatusEnum, gameTimebackIntegrations, gameTimebackAssessmentTests, gameTimebackMetricDiscrepancyVerifications, gameTimebackActivityCompletions;
18947
+ var gameTimebackIntegrationStatusEnum, gameTimebackIntegrationKindEnum, gameTimebackAssessmentPurposeEnum, gameTimebackAssessmentStatusEnum, gameTimebackIntegrations, gameTimebackAssessmentTests, gameTimebackMetricDiscrepancyVerifications, gameTimebackActivityCompletions;
18926
18948
  var init_table7 = __esm(() => {
18927
18949
  init_drizzle_orm();
18928
18950
  init_pg_core();
@@ -18933,6 +18955,10 @@ var init_table7 = __esm(() => {
18933
18955
  "active",
18934
18956
  "deactivated"
18935
18957
  ]);
18958
+ gameTimebackIntegrationKindEnum = pgEnum("game_timeback_integration_kind", [
18959
+ "base",
18960
+ "hole_filling"
18961
+ ]);
18936
18962
  gameTimebackAssessmentPurposeEnum = pgEnum("game_timeback_assessment_purpose", ASSESSMENT_PURPOSES);
18937
18963
  gameTimebackAssessmentStatusEnum = pgEnum("game_timeback_assessment_status", [
18938
18964
  "draft",
@@ -18946,6 +18972,8 @@ var init_table7 = __esm(() => {
18946
18972
  grade: integer("grade").notNull(),
18947
18973
  subject: text("subject").notNull(),
18948
18974
  status: gameTimebackIntegrationStatusEnum("status"),
18975
+ kind: gameTimebackIntegrationKindEnum("kind").notNull().default("base"),
18976
+ baseIntegrationId: uuid("base_integration_id"),
18949
18977
  totalXp: integer("total_xp"),
18950
18978
  lastVerifiedAt: timestamp("last_verified_at", { withTimezone: true }),
18951
18979
  deactivatedAt: timestamp("deactivated_at", { withTimezone: true }),
@@ -18953,7 +18981,19 @@ var init_table7 = __esm(() => {
18953
18981
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
18954
18982
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
18955
18983
  }, (table) => [
18956
- uniqueIndex("game_timeback_integrations_game_grade_subject_idx").on(table.gameId, table.grade, table.subject).where(sql`${table.status} IS DISTINCT FROM 'deactivated'`)
18984
+ 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'`),
18985
+ uniqueIndex("game_timeback_integrations_hole_filling_course_idx").on(table.gameId, table.courseId).where(sql`${table.kind} = 'hole_filling'`),
18986
+ index("game_timeback_integrations_base_integration_idx").on(table.baseIntegrationId),
18987
+ foreignKey({
18988
+ name: "game_timeback_integrations_base_integration_id_fk",
18989
+ columns: [table.baseIntegrationId],
18990
+ foreignColumns: [table.id]
18991
+ }),
18992
+ check("game_timeback_integrations_kind_base_check", sql`(
18993
+ (${table.kind}::text = 'base' AND ${table.baseIntegrationId} IS NULL)
18994
+ OR
18995
+ (${table.kind}::text = 'hole_filling' AND ${table.baseIntegrationId} IS NOT NULL)
18996
+ )`)
18957
18997
  ]);
18958
18998
  gameTimebackAssessmentTests = pgTable("game_timeback_assessment_tests", {
18959
18999
  id: uuid("id").primaryKey().defaultRandom(),
@@ -19035,7 +19075,7 @@ var init_table7 = __esm(() => {
19035
19075
  });
19036
19076
 
19037
19077
  // ../data/src/domains/embed/table.ts
19038
- var embedPartners, embedPartnerGames, embedLaunches, embedPartnersRelations, embedPartnerGamesRelations, embedLaunchesRelations;
19078
+ var embedPartners, embedLaunches, embedLaunchesRelations;
19039
19079
  var init_table8 = __esm(() => {
19040
19080
  init_drizzle_orm();
19041
19081
  init_pg_core();
@@ -19050,15 +19090,6 @@ var init_table8 = __esm(() => {
19050
19090
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
19051
19091
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
19052
19092
  });
19053
- embedPartnerGames = pgTable("embed_partner_games", {
19054
- id: uuid("id").primaryKey().defaultRandom(),
19055
- partnerId: uuid("partner_id").notNull().references(() => embedPartners.id, { onDelete: "cascade" }),
19056
- gameId: uuid("game_id").notNull().references(() => games.id, { onDelete: "cascade" }),
19057
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
19058
- }, (table) => [
19059
- uniqueIndex("embed_partner_games_partner_game_idx").on(table.partnerId, table.gameId),
19060
- index("embed_partner_games_game_idx").on(table.gameId)
19061
- ]);
19062
19093
  embedLaunches = pgTable("embed_launches", {
19063
19094
  id: uuid("id").primaryKey().defaultRandom(),
19064
19095
  partnerId: uuid("partner_id").notNull().references(() => embedPartners.id, { onDelete: "restrict" }),
@@ -19077,19 +19108,6 @@ var init_table8 = __esm(() => {
19077
19108
  index("embed_launches_user_idx").on(table.userId),
19078
19109
  index("embed_launches_game_idx").on(table.gameId)
19079
19110
  ]);
19080
- embedPartnersRelations = relations(embedPartners, ({ many }) => ({
19081
- games: many(embedPartnerGames)
19082
- }));
19083
- embedPartnerGamesRelations = relations(embedPartnerGames, ({ one }) => ({
19084
- partner: one(embedPartners, {
19085
- fields: [embedPartnerGames.partnerId],
19086
- references: [embedPartners.id]
19087
- }),
19088
- game: one(games, {
19089
- fields: [embedPartnerGames.gameId],
19090
- references: [games.id]
19091
- })
19092
- }));
19093
19111
  embedLaunchesRelations = relations(embedLaunches, ({ one }) => ({
19094
19112
  partner: one(embedPartners, {
19095
19113
  fields: [embedLaunches.partnerId],
@@ -19120,10 +19138,7 @@ __export(exports_tables_index, {
19120
19138
  developerStatusEnum: () => developerStatusEnum,
19121
19139
  embedLaunches: () => embedLaunches,
19122
19140
  embedLaunchesRelations: () => embedLaunchesRelations,
19123
- embedPartnerGames: () => embedPartnerGames,
19124
- embedPartnerGamesRelations: () => embedPartnerGamesRelations,
19125
19141
  embedPartners: () => embedPartners,
19126
- embedPartnersRelations: () => embedPartnersRelations,
19127
19142
  gameCustomHostnames: () => gameCustomHostnames,
19128
19143
  gameDashboardUserRoleEnum: () => gameDashboardUserRoleEnum,
19129
19144
  gameDashboardUsers: () => gameDashboardUsers,
@@ -19142,6 +19157,7 @@ __export(exports_tables_index, {
19142
19157
  gameTimebackAssessmentPurposeEnum: () => gameTimebackAssessmentPurposeEnum,
19143
19158
  gameTimebackAssessmentStatusEnum: () => gameTimebackAssessmentStatusEnum,
19144
19159
  gameTimebackAssessmentTests: () => gameTimebackAssessmentTests,
19160
+ gameTimebackIntegrationKindEnum: () => gameTimebackIntegrationKindEnum,
19145
19161
  gameTimebackIntegrationStatusEnum: () => gameTimebackIntegrationStatusEnum,
19146
19162
  gameTimebackIntegrations: () => gameTimebackIntegrations,
19147
19163
  gameTimebackMetricDiscrepancyVerifications: () => gameTimebackMetricDiscrepancyVerifications,
@@ -21049,10 +21065,16 @@ var init_helpers = __esm(() => {
21049
21065
  });
21050
21066
 
21051
21067
  // ../data/src/domains/timeback/helpers.ts
21068
+ function isBaseGameTimebackIntegration() {
21069
+ return sql`${gameTimebackIntegrations.kind} = ${BASE_GAME_TIMEBACK_INTEGRATION_KIND}`;
21070
+ }
21052
21071
  function isActiveGameTimebackIntegrationStatus(statusColumn = gameTimebackIntegrations.status) {
21053
21072
  return sql`${statusColumn} IS DISTINCT FROM ${DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS}`;
21054
21073
  }
21055
- var ACTIVE_GAME_TIMEBACK_INTEGRATION_STATUS = "active", DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS = "deactivated";
21074
+ function isActiveBaseGameTimebackIntegration() {
21075
+ return sql`(${isActiveGameTimebackIntegrationStatus()} AND ${isBaseGameTimebackIntegration()})`;
21076
+ }
21077
+ 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";
21056
21078
  var init_helpers2 = __esm(() => {
21057
21079
  init_drizzle_orm();
21058
21080
  init_table7();
@@ -37629,12 +37651,12 @@ function resolveIntegrationSubject(patchSubject, liveSubject, storedSubject) {
37629
37651
  function filterEnrollmentsByGame(enrollments, gameId) {
37630
37652
  return enrollments.filter((enrollment) => enrollment.gameId === gameId).map(({ gameId: _, ...enrollment }) => enrollment);
37631
37653
  }
37632
- function mapEnrollmentsToUserEnrollments(enrollments, integrations) {
37654
+ function mapEnrollmentsToUserEnrollments(enrollments, integrations, baseCourseIdByIntegrationId = new Map) {
37633
37655
  const enrollmentByCourse = new Map(enrollments.map((enrollment) => [enrollment.courseId, enrollment]));
37634
37656
  const courseToSchool = new Map(enrollments.filter((enrollment) => enrollment.school?.id).map((enrollment) => [enrollment.courseId, enrollment.school.id]));
37635
37657
  return integrations.map((integration) => {
37636
37658
  const enrollment = enrollmentByCourse.get(integration.courseId);
37637
- return {
37659
+ const userEnrollment = {
37638
37660
  gameId: integration.gameId,
37639
37661
  grade: integration.grade,
37640
37662
  subject: integration.subject,
@@ -37642,6 +37664,17 @@ function mapEnrollmentsToUserEnrollments(enrollments, integrations) {
37642
37664
  orgId: courseToSchool.get(integration.courseId),
37643
37665
  ...enrollment ? { id: enrollment.sourcedId } : {}
37644
37666
  };
37667
+ if (integration.kind !== HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND) {
37668
+ return userEnrollment;
37669
+ }
37670
+ const baseCourseId = integration.baseIntegrationId ? baseCourseIdByIntegrationId.get(integration.baseIntegrationId) : undefined;
37671
+ return {
37672
+ ...userEnrollment,
37673
+ kind: HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND,
37674
+ ...baseCourseId && { baseCourseId },
37675
+ ...enrollment?.beginDate !== undefined && { beginDate: enrollment.beginDate },
37676
+ ...enrollment?.metadata && { enrollmentMetadata: enrollment.metadata }
37677
+ };
37645
37678
  });
37646
37679
  }
37647
37680
  function buildGameTimebackSummaries(integrations) {
@@ -38124,7 +38157,7 @@ var init_game_service = __esm(() => {
38124
38157
  const db = this.deps.db;
38125
38158
  const integrations = await db.query.gameTimebackIntegrations.findMany({
38126
38159
  columns: { gameId: true, subject: true },
38127
- where: isActiveGameTimebackIntegrationStatus(),
38160
+ where: isActiveBaseGameTimebackIntegration(),
38128
38161
  orderBy: [asc(gameTimebackIntegrations.createdAt)]
38129
38162
  });
38130
38163
  const subjectMap = {};
@@ -38148,9 +38181,7 @@ var init_game_service = __esm(() => {
38148
38181
  }
38149
38182
  const integrations = await db.query.gameTimebackIntegrations.findMany({
38150
38183
  columns: { gameId: true, subject: true, status: true },
38151
- ...accessibleGameIds && {
38152
- where: inArray(gameTimebackIntegrations.gameId, [...accessibleGameIds])
38153
- },
38184
+ where: and(accessibleGameIds ? inArray(gameTimebackIntegrations.gameId, [...accessibleGameIds]) : undefined, isBaseGameTimebackIntegration()),
38154
38185
  orderBy: [asc(gameTimebackIntegrations.createdAt)]
38155
38186
  });
38156
38187
  return buildGameTimebackSummaries(integrations);
@@ -48581,7 +48612,7 @@ function rejectSystemManagedReview(input, context) {
48581
48612
  });
48582
48613
  }
48583
48614
  }
48584
- 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;
48615
+ 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;
48585
48616
  var init_schemas4 = __esm(() => {
48586
48617
  init_esm();
48587
48618
  init_src();
@@ -48617,6 +48648,26 @@ var init_schemas4 = __esm(() => {
48617
48648
  masterableUnits: numberType().int().nonnegative(),
48618
48649
  level: stringType().trim().min(1).optional()
48619
48650
  });
48651
+ RESERVED_TIMEBACK_COURSE_SOURCED_IDS = new Set([
48652
+ "hole-filling",
48653
+ "removed",
48654
+ "components",
48655
+ "component-resources",
48656
+ "structure"
48657
+ ]);
48658
+ TimebackCourseSourcedIdSchema = stringType().trim().min(3).max(100).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, {
48659
+ message: "Use letters, numbers, dots, dashes, or underscores, starting with a letter or number"
48660
+ }).refine((value) => !RESERVED_TIMEBACK_COURSE_SOURCED_IDS.has(value.toLowerCase()), {
48661
+ message: "This course ID is reserved"
48662
+ });
48663
+ CreateGameTimebackHoleFillingCourseRequestSchema = objectType({
48664
+ baseCourseId: stringType().trim().min(1),
48665
+ title: stringType().trim().min(1),
48666
+ courseCode: stringType().trim().min(1),
48667
+ totalXp: numberType().int().positive(),
48668
+ masterableUnits: numberType().int().nonnegative(),
48669
+ courseId: TimebackCourseSourcedIdSchema.optional()
48670
+ });
48620
48671
  TimebackActivityDataSchema = objectType({
48621
48672
  activityId: stringType().min(1),
48622
48673
  activityName: stringType().optional(),
@@ -48948,21 +48999,20 @@ function isCanonicalOrigin(value) {
48948
48999
  }
48949
49000
  return url.protocol === "https:" || url.protocol === "http:" && isLocalHostname(url.hostname);
48950
49001
  }
48951
- var LOCAL_HOSTNAMES, EmbedOriginSchema, CreateEmbedPartnerSchema, UpdateEmbedPartnerSchema, CreateEmbedLaunchSchema, RedeemEmbedPassSchema;
49002
+ var LOCAL_HOSTNAMES, EmbedOriginSchema, EmbedPartnerClientIdSchema, EnsureEmbedPartnerSchema, AddEmbedPartnerOriginSchema, CreateEmbedLaunchSchema, RedeemEmbedPassSchema;
48952
49003
  var init_schemas5 = __esm(() => {
48953
49004
  init_esm();
48954
49005
  LOCAL_HOSTNAMES = new Set(["localhost", "127.0.0.1"]);
48955
49006
  EmbedOriginSchema = stringType().refine(isCanonicalOrigin, {
48956
49007
  message: "Origin must be a bare https origin like https://example.com, with no path or slash"
48957
49008
  });
48958
- CreateEmbedPartnerSchema = objectType({
48959
- name: stringType().trim().min(1).max(100),
48960
- clientId: stringType().trim().min(1).max(200),
48961
- allowedOrigins: arrayType(EmbedOriginSchema).max(20).default([]),
48962
- allowedGameIds: arrayType(stringType().uuid().transform((id) => id.toLowerCase())).max(200).default([]),
48963
- enabled: booleanType().default(true)
49009
+ EmbedPartnerClientIdSchema = stringType().trim().min(1).max(200);
49010
+ EnsureEmbedPartnerSchema = objectType({
49011
+ name: stringType().trim().min(1).max(100)
49012
+ }).strict();
49013
+ AddEmbedPartnerOriginSchema = objectType({
49014
+ origin: EmbedOriginSchema
48964
49015
  }).strict();
48965
- UpdateEmbedPartnerSchema = CreateEmbedPartnerSchema.omit({ clientId: true }).partial();
48966
49016
  CreateEmbedLaunchSchema = objectType({
48967
49017
  timebackId: stringType().trim().min(1).max(200),
48968
49018
  gameSlug: stringType().trim().min(1).max(100),
@@ -76120,6 +76170,7 @@ class CourseAssessments {
76120
76170
  }
76121
76171
  function buildCoursePayload(config) {
76122
76172
  return {
76173
+ ...config.course.sourcedId && { sourcedId: config.course.sourcedId },
76123
76174
  status: "active",
76124
76175
  title: config.course.title,
76125
76176
  courseCode: config.course.courseCode,
@@ -76166,6 +76217,16 @@ function buildComponentResourcePayload(config, sourcedIds) {
76166
76217
  lessonType: config.componentResource.lessonType
76167
76218
  };
76168
76219
  }
76220
+ async function findOrUndefined(request) {
76221
+ try {
76222
+ return await request;
76223
+ } catch (error) {
76224
+ if (isApiError(error) && error.statusCode === 404) {
76225
+ return;
76226
+ }
76227
+ throw error;
76228
+ }
76229
+ }
76169
76230
 
76170
76231
  class CourseIntegration {
76171
76232
  core;
@@ -76204,6 +76265,32 @@ class CourseIntegration {
76204
76265
  ...verboseData && { verboseData }
76205
76266
  };
76206
76267
  }
76268
+ async findCourse(courseId) {
76269
+ return findOrUndefined(this.core.api.oneroster.courses.get(courseId));
76270
+ }
76271
+ async resumeSetup(courseId, config) {
76272
+ const oneroster = this.core.api.oneroster;
76273
+ const sourcedIds = deriveSourcedIds(courseId);
76274
+ if (!await findOrUndefined(oneroster.courses.getComponent(sourcedIds.component))) {
76275
+ await oneroster.courses.createComponent(buildComponentPayload(config, sourcedIds));
76276
+ addEvent("timeback.component_created", {
76277
+ "app.timeback.component_id": sourcedIds.component
76278
+ });
76279
+ }
76280
+ if (!await findOrUndefined(oneroster.resources.get(sourcedIds.resource))) {
76281
+ await oneroster.resources.create(buildResourcePayload(config, sourcedIds.resource));
76282
+ addEvent("timeback.resource_created", {
76283
+ "app.timeback.resource_id": sourcedIds.resource
76284
+ });
76285
+ }
76286
+ if (!await findOrUndefined(oneroster.courses.getComponentResource(sourcedIds.componentResource))) {
76287
+ await oneroster.courses.createComponentResource(buildComponentResourcePayload(config, sourcedIds));
76288
+ addEvent("timeback.component_resource_link_created", {
76289
+ "app.timeback.component_resource_id": sourcedIds.componentResource
76290
+ });
76291
+ }
76292
+ await this.update(courseId, config);
76293
+ }
76207
76294
  async update(courseId, config) {
76208
76295
  const oneroster = this.core.api.oneroster;
76209
76296
  const sourcedIds = deriveSourcedIds(courseId);
@@ -76343,6 +76430,8 @@ function createCourseNamespace(core) {
76343
76430
  update: (courseId, config) => integration.update(courseId, config),
76344
76431
  verify: (courseId) => integration.verify(courseId),
76345
76432
  getConfig: (courseId) => integration.getConfig(courseId),
76433
+ findCourse: (courseId) => integration.findCourse(courseId),
76434
+ resumeSetup: (courseId, config) => integration.resumeSetup(courseId, config),
76346
76435
  cleanup: (courseId) => integration.cleanup(courseId),
76347
76436
  deactivateCourse: (courseId) => integration.updateCourseStatus(courseId, "tobedeleted"),
76348
76437
  reactivateCourse: (courseId) => integration.updateCourseStatus(courseId, "active"),
@@ -76427,7 +76516,9 @@ class RosterEnrollments {
76427
76516
  status: "active",
76428
76517
  grades,
76429
76518
  subjects,
76430
- school: enrollment.school
76519
+ school: enrollment.school,
76520
+ beginDate: enrollment.beginDate,
76521
+ ...enrollment.metadata && { metadata: enrollment.metadata }
76431
76522
  };
76432
76523
  });
76433
76524
  this.core.caches.enrollments.set(studentId, enrollments);
@@ -87231,7 +87322,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
87231
87322
  }) {
87232
87323
  await this.deps.validateDeveloperAccess(user, gameId);
87233
87324
  const integrations = await this.deps.db.query.gameTimebackIntegrations.findMany({
87234
- where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveGameTimebackIntegrationStatus())
87325
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration())
87235
87326
  });
87236
87327
  const matchingIntegrations = new Map;
87237
87328
  for (const integration of integrations) {
@@ -87620,7 +87711,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
87620
87711
  }) {
87621
87712
  await this.deps.validateDeveloperAccess(user, gameId);
87622
87713
  const integrations = await this.deps.db.query.gameTimebackIntegrations.findMany({
87623
- where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveGameTimebackIntegrationStatus())
87714
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration())
87624
87715
  });
87625
87716
  const orderedIntegrations = integrations.toSorted((left, right) => {
87626
87717
  const subjectDifference = left.subject.localeCompare(right.subject);
@@ -87699,7 +87790,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
87699
87790
  async candidateIntegrations(gameId, studentId, input, db = this.deps.db) {
87700
87791
  const filters = [
87701
87792
  eq(gameTimebackIntegrations.gameId, gameId),
87702
- isActiveGameTimebackIntegrationStatus()
87793
+ isActiveBaseGameTimebackIntegration()
87703
87794
  ];
87704
87795
  if (input.subject) {
87705
87796
  filters.push(eq(gameTimebackIntegrations.subject, input.subject));
@@ -90641,6 +90732,50 @@ function timebackConfigMatchesCreateIntegrationRequest(config, request) {
90641
90732
  const requestedLevel = request.level ?? deriveTimebackCourseLevelFromGrade(request.grade);
90642
90733
  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;
90643
90734
  }
90735
+ function buildHoleFillingCourseConfig(baseConfig, request) {
90736
+ const baseCourseMetadata = isRecord4(baseConfig.course.metadata) ? baseConfig.course.metadata : {};
90737
+ const baseMetrics = isRecord4(baseCourseMetadata.metrics) ? baseCourseMetadata.metrics : {};
90738
+ const baseResourceMetadata = isRecord4(baseConfig.resource.metadata) ? baseConfig.resource.metadata : {};
90739
+ const basePlaycademy = isPlaycademyResourceMetadata(baseResourceMetadata.playcademy) ? baseResourceMetadata.playcademy : undefined;
90740
+ return {
90741
+ organization: baseConfig.organization,
90742
+ course: {
90743
+ ...baseConfig.course,
90744
+ ...request.courseId && { sourcedId: request.courseId },
90745
+ title: request.title,
90746
+ courseCode: request.courseCode,
90747
+ metadata: {
90748
+ ...baseCourseMetadata,
90749
+ courseType: HOLE_FILLING_COURSE_TYPE,
90750
+ baseCourseSourcedId: request.baseCourseId,
90751
+ metrics: {
90752
+ ...baseMetrics,
90753
+ totalXp: request.totalXp,
90754
+ totalLessons: request.masterableUnits,
90755
+ totalGrades: 1
90756
+ }
90757
+ }
90758
+ },
90759
+ component: { ...baseConfig.component, title: `${request.title} Activities` },
90760
+ resource: {
90761
+ ...baseConfig.resource,
90762
+ title: `${request.title} Game`,
90763
+ metadata: {
90764
+ ...baseResourceMetadata,
90765
+ xp: request.totalXp,
90766
+ playcademy: {
90767
+ ...basePlaycademy,
90768
+ mastery: {
90769
+ ...basePlaycademy?.mastery,
90770
+ masterableUnits: request.masterableUnits
90771
+ }
90772
+ }
90773
+ }
90774
+ },
90775
+ componentResource: { ...baseConfig.componentResource, title: `${request.title} Lesson` }
90776
+ };
90777
+ }
90778
+ var HOLE_FILLING_COURSE_TYPE = "hole-filling";
90644
90779
  var init_timeback_create_integration_util = __esm(() => {
90645
90780
  init_constants2();
90646
90781
  init_types2();
@@ -90656,7 +90791,7 @@ async function promoteCompletedCourse({
90656
90791
  enrollments: prefetchedEnrollments
90657
90792
  }) {
90658
90793
  const subjectIntegrations = await db.query.gameTimebackIntegrations.findMany({
90659
- where: and(eq(gameTimebackIntegrations.gameId, currentIntegration.gameId), eq(gameTimebackIntegrations.subject, currentIntegration.subject), isActiveGameTimebackIntegrationStatus())
90794
+ where: and(eq(gameTimebackIntegrations.gameId, currentIntegration.gameId), eq(gameTimebackIntegrations.subject, currentIntegration.subject), isActiveBaseGameTimebackIntegration())
90660
90795
  });
90661
90796
  const nextIntegration = subjectIntegrations.filter((integration) => integration.grade > currentIntegration.grade).toSorted((left, right) => left.grade - right.grade)[0];
90662
90797
  if (!nextIntegration) {
@@ -90723,6 +90858,8 @@ function toGameTimebackIntegration(integration) {
90723
90858
  courseId: integration.courseId,
90724
90859
  grade: integration.grade,
90725
90860
  subject: integration.subject,
90861
+ kind: integration.kind,
90862
+ baseIntegrationId: integration.baseIntegrationId,
90726
90863
  totalXp: integration.totalXp ?? null,
90727
90864
  status: integration.status ?? ACTIVE_GAME_TIMEBACK_INTEGRATION_STATUS,
90728
90865
  deactivatedAt: integration.deactivatedAt ?? null,
@@ -90760,6 +90897,9 @@ var init_timeback_removed_integration_util = __esm(() => {
90760
90897
 
90761
90898
  // ../api-core/src/services/timeback.service.ts
90762
90899
  import { createHash as createHash3 } from "node:crypto";
90900
+ function isUnregisteredHoleFillingOf(course, base) {
90901
+ return course.metadata?.courseType === HOLE_FILLING_COURSE_TYPE && course.metadata.baseCourseSourcedId === base.courseId;
90902
+ }
90763
90903
  async function findGameTimebackIntegrationForUpdate(db, condition) {
90764
90904
  const [integration] = await db.select().from(gameTimebackIntegrations).where(condition).limit(1).for("update");
90765
90905
  return integration;
@@ -91166,13 +91306,29 @@ var init_timeback_service = __esm(async () => {
91166
91306
  const integrations = await db.query.gameTimebackIntegrations.findMany({
91167
91307
  where: and(inArray(gameTimebackIntegrations.courseId, courseIds), isActiveGameTimebackIntegrationStatus())
91168
91308
  });
91169
- return mapEnrollmentsToUserEnrollments(enrollments, integrations);
91309
+ return mapEnrollmentsToUserEnrollments(enrollments, integrations, await this.loadBaseCourseIds(integrations));
91170
91310
  } catch (error) {
91171
91311
  setAttribute("app.timeback.enrollment_fetch", "failed");
91172
91312
  setAttribute("app.timeback.enrollment_fetch_error", errorMessage(error));
91173
91313
  return [];
91174
91314
  }
91175
91315
  }
91316
+ async loadBaseCourseIds(integrations) {
91317
+ const courseIdById = new Map(integrations.map((row) => [row.id, row.courseId]));
91318
+ const missingBaseIds = [
91319
+ ...new Set(integrations.map((row) => row.baseIntegrationId).filter((id) => id !== null && !courseIdById.has(id)))
91320
+ ];
91321
+ if (missingBaseIds.length > 0) {
91322
+ const baseRows = await this.deps.db.query.gameTimebackIntegrations.findMany({
91323
+ columns: { id: true, courseId: true },
91324
+ where: inArray(gameTimebackIntegrations.id, missingBaseIds)
91325
+ });
91326
+ for (const row of baseRows) {
91327
+ courseIdById.set(row.id, row.courseId);
91328
+ }
91329
+ }
91330
+ return courseIdById;
91331
+ }
91176
91332
  async setupIntegration(gameId, request, user) {
91177
91333
  return this.withClientTelemetry(async () => {
91178
91334
  const client = this.requireClient();
@@ -91183,7 +91339,7 @@ var init_timeback_service = __esm(async () => {
91183
91339
  "app.timeback.course_count": courses.length
91184
91340
  });
91185
91341
  const existing = await db.query.gameTimebackIntegrations.findMany({
91186
- where: eq(gameTimebackIntegrations.gameId, gameId)
91342
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), isBaseGameTimebackIntegration())
91187
91343
  });
91188
91344
  const integrations = [];
91189
91345
  const verboseData = [];
@@ -91331,13 +91487,13 @@ var init_timeback_service = __esm(async () => {
91331
91487
  "app.timeback.subject": request.subject
91332
91488
  });
91333
91489
  const activeConflict = await db.query.gameTimebackIntegrations.findFirst({
91334
- where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, request.grade), eq(gameTimebackIntegrations.subject, request.subject), isActiveGameTimebackIntegrationStatus())
91490
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, request.grade), eq(gameTimebackIntegrations.subject, request.subject), isActiveBaseGameTimebackIntegration())
91335
91491
  });
91336
91492
  if (activeConflict) {
91337
91493
  throw new ConflictError(`A TimeBack integration already exists for ${request.subject} Grade ${request.grade}`);
91338
91494
  }
91339
91495
  const removedCandidates = await db.query.gameTimebackIntegrations.findMany({
91340
- where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, request.grade), eq(gameTimebackIntegrations.subject, request.subject), eq(gameTimebackIntegrations.status, DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS))
91496
+ 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())
91341
91497
  });
91342
91498
  for (const candidate of removedCandidates) {
91343
91499
  const config = await client.course.getConfig(candidate.courseId).catch((error) => {
@@ -91375,10 +91531,10 @@ var init_timeback_service = __esm(async () => {
91375
91531
  }
91376
91532
  }
91377
91533
  const reference = await db.query.gameTimebackIntegrations.findFirst({
91378
- where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveGameTimebackIntegrationStatus()),
91534
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration()),
91379
91535
  orderBy: (table, { asc }) => [asc(table.createdAt)]
91380
91536
  }) ?? await db.query.gameTimebackIntegrations.findFirst({
91381
- where: eq(gameTimebackIntegrations.gameId, gameId),
91537
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), isBaseGameTimebackIntegration()),
91382
91538
  orderBy: (table, { asc }) => [asc(table.createdAt)]
91383
91539
  });
91384
91540
  if (!reference) {
@@ -91408,10 +91564,77 @@ var init_timeback_service = __esm(async () => {
91408
91564
  return integration;
91409
91565
  });
91410
91566
  }
91567
+ async createHoleFillingCourse(gameId, user, request) {
91568
+ return this.withClientTelemetry(async () => {
91569
+ const client = this.requireClient();
91570
+ const db = this.deps.db;
91571
+ await this.deps.validateDeveloperAccess(user, gameId);
91572
+ TimebackService.recordIntegrationOperation("create_hole_filling_course", {
91573
+ "app.timeback.base_course_id": request.baseCourseId,
91574
+ "app.timeback.custom_course_id": Boolean(request.courseId)
91575
+ });
91576
+ const integration = await db.transaction(async (tx) => {
91577
+ const database = tx;
91578
+ const base = await findGameTimebackIntegrationForUpdate(database, and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, request.baseCourseId), isActiveBaseGameTimebackIntegration()));
91579
+ if (!base) {
91580
+ throw new NotFoundError("Active base course", `${gameId}:${request.baseCourseId}`);
91581
+ }
91582
+ const config = buildHoleFillingCourseConfig(await client.course.getConfig(base.courseId), request);
91583
+ let courseId = request.courseId;
91584
+ if (courseId) {
91585
+ const takenLocally = await database.query.gameTimebackIntegrations.findFirst({
91586
+ columns: { id: true },
91587
+ where: eq(gameTimebackIntegrations.courseId, courseId)
91588
+ });
91589
+ const remote = takenLocally ? undefined : await client.course.findCourse(courseId);
91590
+ if (takenLocally || remote && !isUnregisteredHoleFillingOf(remote, base)) {
91591
+ throw new ConflictError(`A Timeback course with ID "${courseId}" already exists`);
91592
+ }
91593
+ if (remote) {
91594
+ setAttribute("app.timeback.course_create_mode", "resumed");
91595
+ await client.course.resumeSetup(courseId, config);
91596
+ } else {
91597
+ courseId = undefined;
91598
+ }
91599
+ }
91600
+ if (!courseId) {
91601
+ const created = await client.course.setup(config);
91602
+ courseId = created.courseId;
91603
+ }
91604
+ setAttribute("app.timeback.course_id", courseId);
91605
+ const [inserted] = await database.insert(gameTimebackIntegrations).values({
91606
+ gameId,
91607
+ courseId,
91608
+ grade: base.grade,
91609
+ subject: base.subject,
91610
+ totalXp: request.totalXp,
91611
+ kind: HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND,
91612
+ baseIntegrationId: base.id
91613
+ }).returning();
91614
+ return inserted;
91615
+ });
91616
+ if (!integration) {
91617
+ throw new InternalError("Hole-filling course creation did not return an integration");
91618
+ }
91619
+ return toGameTimebackIntegration(integration);
91620
+ });
91621
+ }
91622
+ async getHoleFillingCourses(gameId, user) {
91623
+ await this.deps.validateGameManagementAccess(user, gameId);
91624
+ const rows = await this.deps.db.query.gameTimebackIntegrations.findMany({
91625
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.kind, HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND), isActiveGameTimebackIntegrationStatus()),
91626
+ orderBy: (table, { asc }) => [
91627
+ asc(table.subject),
91628
+ asc(table.grade),
91629
+ asc(table.createdAt)
91630
+ ]
91631
+ });
91632
+ return rows.map((row) => toGameTimebackIntegration(row));
91633
+ }
91411
91634
  async getIntegrations(gameId, user) {
91412
91635
  await this.deps.validateGameManagementAccess(user, gameId);
91413
91636
  const rows = await this.deps.db.query.gameTimebackIntegrations.findMany({
91414
- where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveGameTimebackIntegrationStatus())
91637
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration())
91415
91638
  });
91416
91639
  return rows.map((row) => toGameTimebackIntegration(row));
91417
91640
  }
@@ -91420,7 +91643,7 @@ var init_timeback_service = __esm(async () => {
91420
91643
  const client = this.requireClient();
91421
91644
  await this.deps.validateGameManagementAccess(user, gameId);
91422
91645
  const rows = await this.deps.db.query.gameTimebackIntegrations.findMany({
91423
- where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.status, DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS)),
91646
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.status, DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS), isBaseGameTimebackIntegration()),
91424
91647
  orderBy: (table, { asc }) => [asc(table.subject), asc(table.grade)]
91425
91648
  });
91426
91649
  setAttribute("app.timeback.removed_integration_count", rows.length);
@@ -91451,6 +91674,17 @@ var init_timeback_service = __esm(async () => {
91451
91674
  if (!integration) {
91452
91675
  throw new NotFoundError("Timeback course", `${gameId}:${courseId}`);
91453
91676
  }
91677
+ if (integration.kind === HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND) {
91678
+ throw new ConflictError("This is a hole-filling course and may be referenced by Timeback, so it can't be removed.");
91679
+ }
91680
+ const activeHoleFilling = await database.query.gameTimebackIntegrations.findFirst({
91681
+ columns: { id: true },
91682
+ where: and(eq(gameTimebackIntegrations.baseIntegrationId, integration.id), isActiveGameTimebackIntegrationStatus())
91683
+ });
91684
+ if (activeHoleFilling) {
91685
+ setAttribute("app.timeback.removal_blocked", "hole_filling_courses");
91686
+ throw new ConflictError("This course has hole-filling courses that may be referenced by Timeback, so it can't be removed.");
91687
+ }
91454
91688
  const activeEnrollments = await client.roster.getCourseRoster(courseId, {
91455
91689
  role: "student",
91456
91690
  includeUsers: false
@@ -91488,11 +91722,18 @@ var init_timeback_service = __esm(async () => {
91488
91722
  if (!integration) {
91489
91723
  throw new NotFoundError("Removed Timeback course", `${gameId}:${courseId}`);
91490
91724
  }
91491
- const activeConflict = await database.query.gameTimebackIntegrations.findFirst({
91492
- where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, integration.grade), eq(gameTimebackIntegrations.subject, integration.subject), isActiveGameTimebackIntegrationStatus())
91493
- });
91494
- if (activeConflict) {
91495
- throw new ConflictError(`An active TimeBack course already exists for ${integration.subject} Grade ${integration.grade}`);
91725
+ if (integration.kind === HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND) {
91726
+ const base = integration.baseIntegrationId ? await findGameTimebackIntegrationForUpdate(database, and(eq(gameTimebackIntegrations.id, integration.baseIntegrationId), isActiveBaseGameTimebackIntegration())) : undefined;
91727
+ if (!base) {
91728
+ throw new ConflictError("Restore the base course first: a hole-filling course can only be active beside its base course.");
91729
+ }
91730
+ } else {
91731
+ const activeConflict = await database.query.gameTimebackIntegrations.findFirst({
91732
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, integration.grade), eq(gameTimebackIntegrations.subject, integration.subject), isActiveBaseGameTimebackIntegration())
91733
+ });
91734
+ if (activeConflict) {
91735
+ throw new ConflictError(`An active TimeBack course already exists for ${integration.subject} Grade ${integration.grade}`);
91736
+ }
91496
91737
  }
91497
91738
  const now = new Date;
91498
91739
  const [updatedIntegration] = await database.update(gameTimebackIntegrations).set({
@@ -91531,52 +91772,66 @@ var init_timeback_service = __esm(async () => {
91531
91772
  TimebackService.recordIntegrationOperation("update_integration", {
91532
91773
  "app.timeback.course_id": courseId
91533
91774
  });
91534
- const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
91535
- where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId), isActiveGameTimebackIntegrationStatus())
91536
- });
91537
- if (!integration) {
91538
- throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
91539
- }
91540
- const timebackConfig = await client.course.getConfig(courseId);
91541
- const liveSubject = timebackConfig.course.subjects[0];
91542
- const subject = resolveIntegrationSubject(patch.subject, liveSubject, integration.subject);
91543
- if (!isTimebackSubject(subject)) {
91544
- throw new ValidationError(`Invalid subject "${subject}"`);
91545
- }
91546
- setAttributes({
91547
- "app.timeback.grade": integration.grade,
91548
- "app.timeback.previous_subject": integration.subject,
91549
- "app.timeback.next_subject": subject
91550
- });
91551
- if (subject !== integration.subject) {
91552
- const subjectConflict = await this.deps.db.query.gameTimebackIntegrations.findFirst({
91553
- where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, integration.grade), eq(gameTimebackIntegrations.subject, subject), isActiveGameTimebackIntegrationStatus())
91775
+ const updated = await this.deps.db.transaction(async (tx) => {
91776
+ const database = tx;
91777
+ const integration = await findGameTimebackIntegrationForUpdate(database, and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId), isActiveGameTimebackIntegrationStatus()));
91778
+ if (!integration) {
91779
+ throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
91780
+ }
91781
+ const timebackConfig = await client.course.getConfig(courseId);
91782
+ const liveSubject = timebackConfig.course.subjects[0];
91783
+ const subject = resolveIntegrationSubject(patch.subject, liveSubject, integration.subject);
91784
+ if (!isTimebackSubject(subject)) {
91785
+ throw new ValidationError(`Invalid subject "${subject}"`);
91786
+ }
91787
+ setAttributes({
91788
+ "app.timeback.grade": integration.grade,
91789
+ "app.timeback.previous_subject": integration.subject,
91790
+ "app.timeback.next_subject": subject
91554
91791
  });
91555
- if (subjectConflict && subjectConflict.id !== integration.id) {
91556
- throw new ValidationError(`A TimeBack integration already exists for ${subject} Grade ${integration.grade}`);
91792
+ if (subject !== integration.subject) {
91793
+ if (integration.kind === HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND) {
91794
+ throw new ValidationError("A hole-filling course must keep the subject of its base course");
91795
+ }
91796
+ const linkedHoleFilling = await database.query.gameTimebackIntegrations.findFirst({
91797
+ columns: { id: true },
91798
+ where: eq(gameTimebackIntegrations.baseIntegrationId, integration.id)
91799
+ });
91800
+ if (linkedHoleFilling) {
91801
+ throw new ValidationError("This course has hole-filling courses, so its subject cannot change");
91802
+ }
91557
91803
  }
91558
- }
91559
- const totalXp = "totalXp" in patch ? patch.totalXp ?? null : getTotalXpFromTimebackConfig(timebackConfig) ?? integration.totalXp ?? null;
91560
- const masterableUnits = "masterableUnits" in patch ? patch.masterableUnits ?? null : getMasterableUnitsFromTimebackConfig(timebackConfig);
91561
- setAttributes({
91562
- "app.timeback.total_xp": totalXp,
91563
- "app.timeback.masterable_units": masterableUnits
91804
+ if (subject !== integration.subject) {
91805
+ const subjectConflict = await database.query.gameTimebackIntegrations.findFirst({
91806
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, integration.grade), eq(gameTimebackIntegrations.subject, subject), isActiveBaseGameTimebackIntegration())
91807
+ });
91808
+ if (subjectConflict && subjectConflict.id !== integration.id) {
91809
+ throw new ValidationError(`A TimeBack integration already exists for ${subject} Grade ${integration.grade}`);
91810
+ }
91811
+ }
91812
+ const totalXp = "totalXp" in patch ? patch.totalXp ?? null : getTotalXpFromTimebackConfig(timebackConfig) ?? integration.totalXp ?? null;
91813
+ const masterableUnits = "masterableUnits" in patch ? patch.masterableUnits ?? null : getMasterableUnitsFromTimebackConfig(timebackConfig);
91814
+ setAttributes({
91815
+ "app.timeback.total_xp": totalXp,
91816
+ "app.timeback.masterable_units": masterableUnits
91817
+ });
91818
+ await client.course.update(courseId, TimebackService.patchTimebackConfig(timebackConfig, {
91819
+ ...patch,
91820
+ subject,
91821
+ totalXp,
91822
+ masterableUnits,
91823
+ grade: integration.grade
91824
+ }));
91825
+ const [row] = await database.update(gameTimebackIntegrations).set({
91826
+ subject,
91827
+ totalXp,
91828
+ updatedAt: new Date
91829
+ }).where(eq(gameTimebackIntegrations.id, integration.id)).returning();
91830
+ if (!row) {
91831
+ throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
91832
+ }
91833
+ return row;
91564
91834
  });
91565
- await client.course.update(courseId, TimebackService.patchTimebackConfig(timebackConfig, {
91566
- ...patch,
91567
- subject,
91568
- totalXp,
91569
- masterableUnits,
91570
- grade: integration.grade
91571
- }));
91572
- const [updated] = await this.deps.db.update(gameTimebackIntegrations).set({
91573
- subject,
91574
- totalXp,
91575
- updatedAt: new Date
91576
- }).where(eq(gameTimebackIntegrations.id, integration.id)).returning();
91577
- if (!updated) {
91578
- throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
91579
- }
91580
91835
  return toGameTimebackIntegration(updated);
91581
91836
  });
91582
91837
  }
@@ -91587,7 +91842,7 @@ var init_timeback_service = __esm(async () => {
91587
91842
  await this.deps.validateDeveloperAccess(user, gameId);
91588
91843
  TimebackService.recordIntegrationOperation("verify_integration");
91589
91844
  const integrations = await db.query.gameTimebackIntegrations.findMany({
91590
- where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveGameTimebackIntegrationStatus())
91845
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration())
91591
91846
  });
91592
91847
  if (integrations.length === 0) {
91593
91848
  throw new NotFoundError("Timeback integration", gameId);
@@ -91625,7 +91880,7 @@ var init_timeback_service = __esm(async () => {
91625
91880
  const client = this.requireClient();
91626
91881
  await this.deps.validateDeveloperAccess(user, gameId);
91627
91882
  const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
91628
- where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveGameTimebackIntegrationStatus())
91883
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveBaseGameTimebackIntegration())
91629
91884
  });
91630
91885
  if (!integration) {
91631
91886
  throw new NotFoundError("Timeback integration", gameId);
@@ -91639,17 +91894,24 @@ var init_timeback_service = __esm(async () => {
91639
91894
  const db = this.deps.db;
91640
91895
  await this.deps.validateDeveloperAccess(user, gameId);
91641
91896
  TimebackService.recordIntegrationOperation("delete_integrations");
91642
- const integrations = await db.query.gameTimebackIntegrations.findMany({
91643
- where: eq(gameTimebackIntegrations.gameId, gameId)
91897
+ await db.transaction(async (tx) => {
91898
+ const database = tx;
91899
+ await database.select({ id: gameTimebackIntegrations.id }).from(gameTimebackIntegrations).where(eq(gameTimebackIntegrations.gameId, gameId)).for("update");
91900
+ const integrations = await database.select().from(gameTimebackIntegrations).where(eq(gameTimebackIntegrations.gameId, gameId));
91901
+ if (integrations.length === 0) {
91902
+ throw new NotFoundError("Timeback integration", gameId);
91903
+ }
91904
+ setAttribute("app.timeback.integration_count", integrations.length);
91905
+ const holeFillingCount = integrations.filter((integration) => integration.kind === HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND).length;
91906
+ if (holeFillingCount > 0) {
91907
+ setAttribute("app.timeback.hole_filling_count", holeFillingCount);
91908
+ 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.`);
91909
+ }
91910
+ for (const integration of integrations) {
91911
+ await client.course.cleanup(integration.courseId);
91912
+ }
91913
+ await database.delete(gameTimebackIntegrations).where(eq(gameTimebackIntegrations.gameId, gameId));
91644
91914
  });
91645
- if (integrations.length === 0) {
91646
- throw new NotFoundError("Timeback integration", gameId);
91647
- }
91648
- setAttribute("app.timeback.integration_count", integrations.length);
91649
- for (const integration of integrations) {
91650
- await client.course.cleanup(integration.courseId);
91651
- }
91652
- await db.delete(gameTimebackIntegrations).where(eq(gameTimebackIntegrations.gameId, gameId));
91653
91915
  });
91654
91916
  }
91655
91917
  static patchCourseMetadata(metadata2, totalXp, options) {
@@ -91829,7 +92091,7 @@ var init_timeback_service = __esm(async () => {
91829
92091
  });
91830
92092
  await this.deps.validateDeveloperAccess(user, gameId);
91831
92093
  const integration = await db.query.gameTimebackIntegrations.findFirst({
91832
- where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject), isActiveGameTimebackIntegrationStatus())
92094
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject), isActiveBaseGameTimebackIntegration())
91833
92095
  });
91834
92096
  if (!integration) {
91835
92097
  throw new NotFoundError(`Timeback integration for game (grade ${activityData.grade}, subject ${activityData.subject})`);
@@ -91972,7 +92234,7 @@ var init_timeback_service = __esm(async () => {
91972
92234
  const client = this.requireClient();
91973
92235
  const db = this.deps.db;
91974
92236
  const integrations = await db.query.gameTimebackIntegrations.findMany({
91975
- where: subject ? and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.subject, subject), isActiveGameTimebackIntegrationStatus()) : and(eq(gameTimebackIntegrations.gameId, gameId), isActiveGameTimebackIntegrationStatus())
92237
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), subject ? eq(gameTimebackIntegrations.subject, subject) : undefined, isActiveBaseGameTimebackIntegration())
91976
92238
  });
91977
92239
  if (integrations.length === 0) {
91978
92240
  throw new NotFoundError(subject ? `Timeback integrations for game (subject ${subject})` : "Timeback integrations for game");
@@ -92161,7 +92423,7 @@ var init_timeback_service = __esm(async () => {
92161
92423
  }
92162
92424
  const pendingHeartbeat = (async () => {
92163
92425
  const integration = await db.query.gameTimebackIntegrations.findFirst({
92164
- where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject), isActiveGameTimebackIntegrationStatus())
92426
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject), isActiveBaseGameTimebackIntegration())
92165
92427
  });
92166
92428
  if (!integration) {
92167
92429
  throw new NotFoundError(`Timeback integration for game (grade ${activityData.grade}, subject ${activityData.subject})`);
@@ -92291,7 +92553,7 @@ var init_timeback_service = __esm(async () => {
92291
92553
  const db = this.deps.db;
92292
92554
  await this.deps.validateDeveloperAccess(user, options.gameId);
92293
92555
  const integration = await db.query.gameTimebackIntegrations.findFirst({
92294
- where: and(eq(gameTimebackIntegrations.gameId, options.gameId), eq(gameTimebackIntegrations.subject, options.subject), isActiveGameTimebackIntegrationStatus())
92556
+ where: and(eq(gameTimebackIntegrations.gameId, options.gameId), eq(gameTimebackIntegrations.subject, options.subject), isActiveBaseGameTimebackIntegration())
92295
92557
  });
92296
92558
  if (!integration) {
92297
92559
  throw new ValidationError(`Subject "${options.subject}" is not configured for game ${options.gameId}`);
@@ -93160,9 +93422,47 @@ var init_account_service = __esm(async () => {
93160
93422
  await init_errors8();
93161
93423
  });
93162
93424
 
93425
+ // ../api-core/src/services/embed-partner.service.ts
93426
+ class EmbedPartnerService {
93427
+ deps;
93428
+ constructor(deps) {
93429
+ this.deps = deps;
93430
+ }
93431
+ async ensurePartner(clientId, name2) {
93432
+ const [partner] = await this.deps.db.insert(embedPartners).values({ clientId, name: name2 }).onConflictDoUpdate({
93433
+ target: embedPartners.clientId,
93434
+ set: { name: name2, updatedAt: new Date }
93435
+ }).returning();
93436
+ if (!partner) {
93437
+ throw new InternalError("Failed to ensure embed partner");
93438
+ }
93439
+ return partner;
93440
+ }
93441
+ addOrigin(clientId, origin) {
93442
+ return this.setOrigins(clientId, sql`CASE WHEN ${origin} = ANY(${embedPartners.allowedOrigins})
93443
+ THEN ${embedPartners.allowedOrigins}
93444
+ ELSE array_append(${embedPartners.allowedOrigins}, ${origin}) END`);
93445
+ }
93446
+ removeOrigin(clientId, origin) {
93447
+ return this.setOrigins(clientId, sql`array_remove(${embedPartners.allowedOrigins}, ${origin})`);
93448
+ }
93449
+ async setOrigins(clientId, origins) {
93450
+ const [partner] = await this.deps.db.update(embedPartners).set({ allowedOrigins: origins, updatedAt: new Date }).where(eq(embedPartners.clientId, clientId)).returning();
93451
+ if (!partner) {
93452
+ throw new NotFoundError("Embed partner", clientId);
93453
+ }
93454
+ return partner;
93455
+ }
93456
+ }
93457
+ var init_embed_partner_service = __esm(() => {
93458
+ init_drizzle_orm();
93459
+ init_tables_index();
93460
+ init_errors2();
93461
+ });
93462
+
93163
93463
  // ../api-core/src/utils/embed.util.ts
93164
- function canPartnerLaunch(partner, origin, gameId) {
93165
- return partner.enabled && partner.allowedOrigins.includes(origin) && partner.games.some((grant) => grant.gameId === gameId);
93464
+ function canPartnerLaunch(partner, origin) {
93465
+ return partner.enabled && partner.allowedOrigins.includes(origin);
93166
93466
  }
93167
93467
  function embedPageUrl(hubOrigin, slug, parentOrigin) {
93168
93468
  const url = new URL(`${EMBED_PAGE_PATH}/${encodeURIComponent(slug)}`, hubOrigin);
@@ -93190,12 +93490,9 @@ class EmbedService {
93190
93490
  return partner;
93191
93491
  }
93192
93492
  async isFrameOriginAllowed(origin, slug) {
93193
- const grants = await this.deps.db.select({
93194
- enabled: embedPartners.enabled,
93195
- allowedOrigins: embedPartners.allowedOrigins,
93196
- gameId: embedPartnerGames.gameId
93197
- }).from(embedPartnerGames).innerJoin(embedPartners, eq(embedPartners.id, embedPartnerGames.partnerId)).innerJoin(games, eq(games.id, embedPartnerGames.gameId)).where(eq(games.slug, slug));
93198
- return grants.some((grant) => canPartnerLaunch({ ...grant, games: [grant] }, origin, grant.gameId));
93493
+ const game = this.deps.db.select({ id: games.id }).from(games).where(eq(games.slug, slug));
93494
+ 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);
93495
+ return partner !== undefined;
93199
93496
  }
93200
93497
  async createLaunch(partner, input) {
93201
93498
  const game = await this.deps.db.query.games.findFirst({
@@ -93206,9 +93503,8 @@ class EmbedService {
93206
93503
  throw new NotFoundError("Game", input.gameSlug);
93207
93504
  }
93208
93505
  setAttributes({ "app.game.id": game.id, "app.game.slug": game.slug });
93209
- const grants = await this.grantsFor(partner.id, game.id);
93210
- if (!canPartnerLaunch({ ...partner, games: grants }, input.parentOrigin, game.id)) {
93211
- throw new AccessDeniedError("This partner may not launch that game into that origin");
93506
+ if (!canPartnerLaunch(partner, input.parentOrigin)) {
93507
+ throw new AccessDeniedError("This partner may not launch into that origin");
93212
93508
  }
93213
93509
  setAttributes({ "app.user.timeback_id": input.timebackId });
93214
93510
  const user = await this.deps.resolveTimebackStudent(input.timebackId);
@@ -93270,20 +93566,13 @@ class EmbedService {
93270
93566
  throw new NotFoundError("Embed launch", launchId);
93271
93567
  }
93272
93568
  setAttributes({ "app.game.id": launch.gameId, "app.user.id": launch.userId });
93273
- const grants = await this.grantsFor(partner.id, launch.gameId);
93274
- const outcome = renewalRefusal(launch, () => canPartnerLaunch({ ...partner, games: grants }, launch.parentOrigin, launch.gameId));
93569
+ const outcome = renewalRefusal(launch, () => canPartnerLaunch(partner, launch.parentOrigin));
93275
93570
  setAttributes({ "app.embed.outcome": outcome ?? "renewed" });
93276
93571
  if (outcome) {
93277
93572
  throw new AccessDeniedError("This launch has ended");
93278
93573
  }
93279
93574
  return this.deps.mintGameToken(launch.gameId, launch.userId);
93280
93575
  }
93281
- grantsFor(partnerId, gameId) {
93282
- return this.deps.db.query.embedPartnerGames.findMany({
93283
- where: and(eq(embedPartnerGames.partnerId, partnerId), eq(embedPartnerGames.gameId, gameId)),
93284
- columns: { gameId: true }
93285
- });
93286
- }
93287
93576
  async endLaunch(launchId) {
93288
93577
  await this.deps.db.update(embedLaunches).set({ endedAt: sql`coalesce(${embedLaunches.endedAt}, now())`, passHash: null }).where(eq(embedLaunches.id, launchId));
93289
93578
  }
@@ -93300,15 +93589,12 @@ class EmbedService {
93300
93589
  "app.embed.partner_id": launch.partnerId,
93301
93590
  "app.user.id": launch.userId
93302
93591
  });
93303
- const [partner, grants] = await Promise.all([
93304
- this.deps.db.query.embedPartners.findFirst({
93305
- where: eq(embedPartners.id, launch.partnerId),
93306
- columns: { enabled: true, allowedOrigins: true }
93307
- }),
93308
- this.grantsFor(launch.partnerId, launch.gameId)
93309
- ]);
93592
+ const partner = await this.deps.db.query.embedPartners.findFirst({
93593
+ where: eq(embedPartners.id, launch.partnerId),
93594
+ columns: { enabled: true, allowedOrigins: true }
93595
+ });
93310
93596
  const originMatches = launch.parentOrigin === input.parentOrigin;
93311
- const stillAllowed = partner !== undefined && canPartnerLaunch({ ...partner, games: grants }, launch.parentOrigin, launch.gameId);
93597
+ const stillAllowed = partner !== undefined && canPartnerLaunch(partner, launch.parentOrigin);
93312
93598
  if (!originMatches || !stillAllowed) {
93313
93599
  await this.endLaunch(launch.id);
93314
93600
  setAttributes({
@@ -93726,14 +94012,16 @@ function createStandaloneServices(deps) {
93726
94012
  return { ...profile, timeback: timebackData };
93727
94013
  }
93728
94014
  });
94015
+ const embedPartner = new EmbedPartnerService({ db });
93729
94016
  const verify = new VerifyService({
93730
94017
  db,
93731
94018
  validateGameToken: auth.validateGameToken.bind(auth)
93732
94019
  });
93733
- return { account, embed, lti, session, user, verify };
94020
+ return { account, embed, embedPartner, lti, session, user, verify };
93734
94021
  }
93735
94022
  var init_standalone = __esm(async () => {
93736
94023
  init_spans();
94024
+ init_embed_partner_service();
93737
94025
  init_embed_service();
93738
94026
  init_lti_service();
93739
94027
  init_session_service();
@@ -110178,7 +110466,7 @@ function ilike2(column6, value) {
110178
110466
  function notIlike2(column6, value) {
110179
110467
  return sql4`${column6} not ilike ${value}`;
110180
110468
  }
110181
- function arrayContains(column6, values) {
110469
+ function arrayContains2(column6, values) {
110182
110470
  if (Array.isArray(values)) {
110183
110471
  if (values.length === 0) {
110184
110472
  throw new Error("arrayContains requires at least one value");
@@ -133497,7 +133785,7 @@ params: ${params}`);
133497
133785
  and: () => and2,
133498
133786
  applyMixins: () => applyMixins2,
133499
133787
  arrayContained: () => arrayContained,
133500
- arrayContains: () => arrayContains,
133788
+ arrayContains: () => arrayContains2,
133501
133789
  arrayOverlaps: () => arrayOverlaps,
133502
133790
  asc: () => asc2,
133503
133791
  avg: () => avg,
@@ -150067,15 +150355,36 @@ function requireUuidParam(value, name2) {
150067
150355
  }
150068
150356
  return value;
150069
150357
  }
150358
+ function bearerToken(request) {
150359
+ const header = request.headers.get("Authorization");
150360
+ return header?.startsWith("Bearer ") ? header.slice("Bearer ".length) : "";
150361
+ }
150070
150362
  async function requirePartner(ctx) {
150071
- const header = ctx.request.headers.get("Authorization");
150072
- const token = header?.startsWith("Bearer ") ? header.slice("Bearer ".length) : "";
150363
+ const token = bearerToken(ctx.request);
150073
150364
  const verified = token ? await ctx.providers.auth.verifyEmbedPartnerToken(token) : null;
150074
150365
  if (!verified) {
150075
150366
  throw ApiError.unauthorized("A valid embed partner access token is required");
150076
150367
  }
150077
150368
  return ctx.services.embed.getPartnerByClientId(verified.clientId);
150078
150369
  }
150370
+ async function requireEmbedAdmin(ctx) {
150371
+ const expected = ctx.config.embedAdminToken;
150372
+ if (!expected) {
150373
+ throw ApiError.serviceUnavailable("Embed partner management is not configured");
150374
+ }
150375
+ const token = bearerToken(ctx.request);
150376
+ const [given, wanted] = token ? await Promise.all([sha256Hex(token), sha256Hex(expected)]) : [];
150377
+ if (!given || given !== wanted) {
150378
+ throw ApiError.unauthorized("A valid embed admin token is required");
150379
+ }
150380
+ }
150381
+ function requireClientIdParam(ctx) {
150382
+ const clientId = EmbedPartnerClientIdSchema.safeParse(ctx.params.clientId);
150383
+ if (!clientId.success) {
150384
+ throw ApiError.badRequest("Missing or invalid clientId");
150385
+ }
150386
+ return clientId.data;
150387
+ }
150079
150388
  async function createLaunch(ctx) {
150080
150389
  const partner = await requirePartner(ctx);
150081
150390
  const body2 = await parseRequestBody(ctx.request, CreateEmbedLaunchSchema);
@@ -150105,6 +150414,27 @@ async function redeemPass(ctx) {
150105
150414
  timeback: launch.profile.timeback
150106
150415
  };
150107
150416
  }
150417
+ async function ensurePartner(ctx) {
150418
+ await requireEmbedAdmin(ctx);
150419
+ const clientId = requireClientIdParam(ctx);
150420
+ const body2 = await parseRequestBody(ctx.request, EnsureEmbedPartnerSchema);
150421
+ return ctx.services.embedPartner.ensurePartner(clientId, body2.name);
150422
+ }
150423
+ async function addPartnerOrigin(ctx) {
150424
+ await requireEmbedAdmin(ctx);
150425
+ const clientId = requireClientIdParam(ctx);
150426
+ const body2 = await parseRequestBody(ctx.request, AddEmbedPartnerOriginSchema);
150427
+ return ctx.services.embedPartner.addOrigin(clientId, body2.origin);
150428
+ }
150429
+ async function removePartnerOrigin(ctx) {
150430
+ await requireEmbedAdmin(ctx);
150431
+ const clientId = requireClientIdParam(ctx);
150432
+ const origin = EmbedOriginSchema.safeParse(ctx.url.searchParams.get("origin"));
150433
+ if (!origin.success) {
150434
+ throw ApiError.badRequest("Missing or invalid origin");
150435
+ }
150436
+ return ctx.services.embedPartner.removeOrigin(clientId, origin.data);
150437
+ }
150108
150438
  var embed2;
150109
150439
  var init_embed_controller = __esm(() => {
150110
150440
  init_src();
@@ -150114,8 +150444,11 @@ var init_embed_controller = __esm(() => {
150114
150444
  init_utils7();
150115
150445
  init_embed_util();
150116
150446
  embed2 = defineControllerNames("embed", {
150447
+ addPartnerOrigin,
150117
150448
  createLaunch,
150449
+ ensurePartner,
150118
150450
  redeemPass,
150451
+ removePartnerOrigin,
150119
150452
  renewToken
150120
150453
  });
150121
150454
  });
@@ -150722,7 +151055,7 @@ function parseQtiLibraryParams(searchParams) {
150722
151055
  limit
150723
151056
  };
150724
151057
  }
150725
- 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;
151058
+ 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;
150726
151059
  var init_timeback_controller = __esm(() => {
150727
151060
  init_esm();
150728
151061
  init_src();
@@ -150801,6 +151134,27 @@ var init_timeback_controller = __esm(() => {
150801
151134
  const body2 = await parseRequestBody(ctx.request, CreateGameTimebackIntegrationRequestSchema);
150802
151135
  return ctx.services.timeback.createIntegration(gameId, ctx.user, body2);
150803
151136
  });
151137
+ getHoleFillingCourses = requireGameManagementAccess(async (ctx) => {
151138
+ const gameId = ctx.params.gameId;
151139
+ if (!gameId) {
151140
+ throw ApiError.badRequest("Missing gameId");
151141
+ }
151142
+ if (!isValidUUID(gameId)) {
151143
+ throw ApiError.unprocessableEntity("Invalid gameId format");
151144
+ }
151145
+ return ctx.services.timeback.getHoleFillingCourses(gameId, ctx.user);
151146
+ });
151147
+ createHoleFillingCourse = requireDeveloper(async (ctx) => {
151148
+ const gameId = ctx.params.gameId;
151149
+ if (!gameId) {
151150
+ throw ApiError.badRequest("Missing gameId");
151151
+ }
151152
+ if (!isValidUUID(gameId)) {
151153
+ throw ApiError.unprocessableEntity("Invalid gameId format");
151154
+ }
151155
+ const body2 = await parseRequestBody(ctx.request, CreateGameTimebackHoleFillingCourseRequestSchema);
151156
+ return ctx.services.timeback.createHoleFillingCourse(gameId, ctx.user, body2);
151157
+ });
150804
151158
  updateIntegration = requireDeveloper(async (ctx) => {
150805
151159
  const { gameId, courseId } = ctx.params;
150806
151160
  if (!gameId || !courseId) {
@@ -151585,6 +151939,8 @@ var init_timeback_controller = __esm(() => {
151585
151939
  getIntegrations,
151586
151940
  getRemovedIntegrations,
151587
151941
  createIntegration,
151942
+ getHoleFillingCourses,
151943
+ createHoleFillingCourse,
151588
151944
  updateIntegration,
151589
151945
  deactivateCourse,
151590
151946
  reactivateCourse,