@playcademy/vite-plugin 1.1.1-beta.2 → 1.1.1-beta.3

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 (2) hide show
  1. package/dist/index.js +1776 -1175
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -23746,7 +23746,7 @@ import path from "node:path";
23746
23746
  // package.json
23747
23747
  var package_default = {
23748
23748
  name: "@playcademy/vite-plugin",
23749
- version: "1.1.1-beta.2",
23749
+ version: "1.1.1-beta.3",
23750
23750
  type: "module",
23751
23751
  exports: {
23752
23752
  ".": {
@@ -24244,6 +24244,7 @@ import { join as join2 } from "path";
24244
24244
  import { Buffer as Buffer2 } from "node:buffer";
24245
24245
  import { gzipSync } from "node:zlib";
24246
24246
  import { stdout } from "process";
24247
+ import { createHash } from "node:crypto";
24247
24248
  import { AsyncLocalStorage } from "node:async_hooks";
24248
24249
  import crypto3 from "node:crypto";
24249
24250
  import * as s3 from "fs";
@@ -25342,7 +25343,7 @@ var package_default2;
25342
25343
  var init_package = __esm(() => {
25343
25344
  package_default2 = {
25344
25345
  name: "@playcademy/sandbox",
25345
- version: "0.5.2-beta.2",
25346
+ version: "0.5.2-beta.3",
25346
25347
  description: "Local development server for Playcademy game development",
25347
25348
  type: "module",
25348
25349
  exports: {
@@ -35608,25 +35609,34 @@ var init_table6 = __esm(() => {
35608
35609
  })
35609
35610
  }));
35610
35611
  });
35612
+ var gameTimebackIntegrationStatusEnum;
35611
35613
  var gameTimebackIntegrations;
35612
35614
  var gameTimebackAssessmentTests;
35613
35615
  var gameTimebackMetricDiscrepancyVerifications;
35614
35616
  var init_table7 = __esm(() => {
35617
+ init_drizzle_orm();
35615
35618
  init_pg_core();
35616
35619
  init_table5();
35617
35620
  init_table3();
35621
+ gameTimebackIntegrationStatusEnum = pgEnum("game_timeback_integration_status", [
35622
+ "active",
35623
+ "deactivated"
35624
+ ]);
35618
35625
  gameTimebackIntegrations = pgTable("game_timeback_integrations", {
35619
35626
  id: uuid("id").primaryKey().defaultRandom(),
35620
35627
  gameId: uuid("game_id").notNull().references(() => games.id, { onDelete: "cascade" }),
35621
35628
  courseId: text("course_id").notNull(),
35622
35629
  grade: integer("grade").notNull(),
35623
35630
  subject: text("subject").notNull(),
35631
+ status: gameTimebackIntegrationStatusEnum("status"),
35624
35632
  totalXp: integer("total_xp"),
35625
35633
  lastVerifiedAt: timestamp("last_verified_at", { withTimezone: true }),
35634
+ deactivatedAt: timestamp("deactivated_at", { withTimezone: true }),
35635
+ reactivatedAt: timestamp("reactivated_at", { withTimezone: true }),
35626
35636
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
35627
35637
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
35628
35638
  }, (table3) => [
35629
- uniqueIndex("game_timeback_integrations_game_grade_subject_idx").on(table3.gameId, table3.grade, table3.subject)
35639
+ uniqueIndex("game_timeback_integrations_game_grade_subject_idx").on(table3.gameId, table3.grade, table3.subject).where(sql`${table3.status} IS DISTINCT FROM 'deactivated'`)
35630
35640
  ]);
35631
35641
  gameTimebackAssessmentTests = pgTable("game_timeback_assessment_tests", {
35632
35642
  id: uuid("id").primaryKey().defaultRandom(),
@@ -35667,6 +35677,7 @@ __export(exports_tables_index, {
35667
35677
  gameTypeEnum: () => gameTypeEnum,
35668
35678
  gameTimebackMetricDiscrepancyVerifications: () => gameTimebackMetricDiscrepancyVerifications,
35669
35679
  gameTimebackIntegrations: () => gameTimebackIntegrations,
35680
+ gameTimebackIntegrationStatusEnum: () => gameTimebackIntegrationStatusEnum,
35670
35681
  gameTimebackAssessmentTests: () => gameTimebackAssessmentTests,
35671
35682
  gameScoresRelations: () => gameScoresRelations,
35672
35683
  gameScores: () => gameScores,
@@ -48677,6 +48688,7 @@ var init_helpers = __esm(() => {
48677
48688
  init_mime();
48678
48689
  });
48679
48690
  var init_provider = __esm(() => {
48691
+ init_src();
48680
48692
  init_src();
48681
48693
  init_core();
48682
48694
  init_constants2();
@@ -51748,21 +51760,626 @@ var init_game_member_service = __esm(() => {
51748
51760
  init_spans();
51749
51761
  init_errors();
51750
51762
  });
51763
+ function isActiveGameTimebackIntegrationStatus(statusColumn = gameTimebackIntegrations.status) {
51764
+ return sql`${statusColumn} IS DISTINCT FROM ${DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS}`;
51765
+ }
51766
+ var ACTIVE_GAME_TIMEBACK_INTEGRATION_STATUS = "active";
51767
+ var DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS = "deactivated";
51768
+ var init_helpers2 = __esm(() => {
51769
+ init_drizzle_orm();
51770
+ init_table7();
51771
+ });
51772
+ var init_helpers_index = __esm(() => {
51773
+ init_helpers2();
51774
+ });
51751
51775
  function sleep(ms) {
51752
51776
  if (ms <= 0) {
51753
51777
  return Promise.resolve();
51754
51778
  }
51755
51779
  return new Promise((resolve2) => setTimeout(resolve2, ms));
51756
51780
  }
51781
+ function isObject(value) {
51782
+ return typeof value === "object" && value !== null;
51783
+ }
51784
+ function isCourseMetadata(value) {
51785
+ return isObject(value);
51786
+ }
51787
+ function isResourceMetadata(value) {
51788
+ return isObject(value);
51789
+ }
51790
+ function isPlaycademyResourceMetadata(value) {
51791
+ if (!isObject(value)) {
51792
+ return false;
51793
+ }
51794
+ if (!("mastery" in value) || value.mastery === undefined) {
51795
+ return true;
51796
+ }
51797
+ return isObject(value.mastery);
51798
+ }
51799
+ function isTimebackSubject(value) {
51800
+ return typeof value === "string" && SUBJECT_VALUES.includes(value);
51801
+ }
51802
+ function isTimebackGrade(value) {
51803
+ return typeof value === "number" && Number.isInteger(value) && GRADE_VALUES.includes(value);
51804
+ }
51805
+ var __esm2 = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
51806
+ var TIMEBACK_API_URLS;
51807
+ var TIMEBACK_AUTH_URLS;
51808
+ var CALIPER_API_URLS;
51809
+ var ONEROSTER_ENDPOINTS;
51810
+ var QTI_ENDPOINTS;
51811
+ var CALIPER_ENDPOINTS;
51812
+ var CALIPER_CONSTANTS;
51813
+ var TIMEBACK_EVENT_TYPES;
51814
+ var TIMEBACK_ACTIONS;
51815
+ var TIMEBACK_TYPES;
51816
+ var ACTIVITY_METRIC_TYPES;
51817
+ var TIME_METRIC_TYPES;
51818
+ var TIMEBACK_SUBJECTS;
51819
+ var TIMEBACK_GRADE_LEVELS;
51820
+ var TIMEBACK_GRADE_LEVEL_LABELS;
51821
+ var CALIPER_SUBJECTS;
51822
+ var ONEROSTER_STATUS;
51823
+ var SCORE_STATUS;
51824
+ var ENV_VARS;
51825
+ var HTTP_DEFAULTS;
51826
+ var AUTH_DEFAULTS;
51827
+ var CACHE_DEFAULTS;
51828
+ var CONFIG_DEFAULTS;
51829
+ var PLAYCADEMY_DEFAULTS;
51830
+ var RESOURCE_DEFAULTS;
51831
+ var HTTP_STATUS;
51832
+ var ERROR_NAMES;
51833
+ var init_constants3;
51834
+ var SUBJECT_VALUES;
51835
+ var GRADE_VALUES;
51836
+ var init_types2 = __esm(() => {
51837
+ init_src();
51838
+ init_constants3 = __esm2(() => {
51839
+ TIMEBACK_API_URLS = {
51840
+ production: "https://api.alpha-1edtech.ai",
51841
+ staging: "https://api.staging.alpha-1edtech.com"
51842
+ };
51843
+ TIMEBACK_AUTH_URLS = {
51844
+ production: "https://prod-beyond-timeback-api-2-idp.auth.us-east-1.amazoncognito.com",
51845
+ staging: "https://alpha-auth-development-idp.auth.us-west-2.amazoncognito.com"
51846
+ };
51847
+ CALIPER_API_URLS = {
51848
+ production: "https://caliper.alpha-1edtech.ai",
51849
+ staging: "https://caliper-staging.alpha-1edtech.com"
51850
+ };
51851
+ ONEROSTER_ENDPOINTS = {
51852
+ organizations: "/ims/oneroster/rostering/v1p2/orgs",
51853
+ courses: "/ims/oneroster/rostering/v1p2/courses",
51854
+ courseComponents: "/ims/oneroster/rostering/v1p2/courses/components",
51855
+ resources: "/ims/oneroster/resources/v1p2/resources",
51856
+ componentResources: "/ims/oneroster/rostering/v1p2/courses/component-resources",
51857
+ classes: "/ims/oneroster/rostering/v1p2/classes",
51858
+ enrollments: "/ims/oneroster/rostering/v1p2/enrollments",
51859
+ assessmentLineItems: "/ims/oneroster/gradebook/v1p2/assessmentLineItems",
51860
+ assessmentResults: "/ims/oneroster/gradebook/v1p2/assessmentResults",
51861
+ users: "/ims/oneroster/rostering/v1p2/users"
51862
+ };
51863
+ QTI_ENDPOINTS = {
51864
+ assessmentTests: "/assessment-tests",
51865
+ assessmentItems: "/assessment-items"
51866
+ };
51867
+ CALIPER_ENDPOINTS = {
51868
+ event: "/caliper/event",
51869
+ events: "/caliper/events",
51870
+ validate: "/caliper/event/validate"
51871
+ };
51872
+ CALIPER_CONSTANTS = {
51873
+ context: "http://purl.imsglobal.org/ctx/caliper/v1p2",
51874
+ profile: "TimebackProfile",
51875
+ dataVersion: "http://purl.imsglobal.org/ctx/caliper/v1p2"
51876
+ };
51877
+ TIMEBACK_EVENT_TYPES = {
51878
+ activityEvent: "ActivityEvent",
51879
+ timeSpentEvent: "TimeSpentEvent"
51880
+ };
51881
+ TIMEBACK_ACTIONS = {
51882
+ completed: "Completed",
51883
+ spentTime: "SpentTime"
51884
+ };
51885
+ TIMEBACK_TYPES = {
51886
+ user: "TimebackUser",
51887
+ activityContext: "TimebackActivityContext",
51888
+ activityMetricsCollection: "TimebackActivityMetricsCollection",
51889
+ timeSpentMetricsCollection: "TimebackTimeSpentMetricsCollection"
51890
+ };
51891
+ ACTIVITY_METRIC_TYPES = {
51892
+ totalQuestions: "totalQuestions",
51893
+ correctQuestions: "correctQuestions",
51894
+ xpEarned: "xpEarned",
51895
+ masteredUnits: "masteredUnits"
51896
+ };
51897
+ TIME_METRIC_TYPES = {
51898
+ active: "active",
51899
+ inactive: "inactive",
51900
+ waste: "waste",
51901
+ unknown: "unknown",
51902
+ antiPattern: "anti-pattern"
51903
+ };
51904
+ TIMEBACK_SUBJECTS = [
51905
+ "Math",
51906
+ "FastMath",
51907
+ "Science",
51908
+ "Social Studies",
51909
+ "Language",
51910
+ "Reading",
51911
+ "Vocabulary",
51912
+ "Writing"
51913
+ ];
51914
+ TIMEBACK_GRADE_LEVELS = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
51915
+ TIMEBACK_GRADE_LEVEL_LABELS = {
51916
+ "-1": "pre-k",
51917
+ "0": "kindergarten",
51918
+ "1": "1st grade",
51919
+ "2": "2nd grade",
51920
+ "3": "3rd grade",
51921
+ "4": "4th grade",
51922
+ "5": "5th grade",
51923
+ "6": "6th grade",
51924
+ "7": "7th grade",
51925
+ "8": "8th grade",
51926
+ "9": "9th grade",
51927
+ "10": "10th grade",
51928
+ "11": "11th grade",
51929
+ "12": "12th grade",
51930
+ "13": "AP"
51931
+ };
51932
+ CALIPER_SUBJECTS = {
51933
+ Reading: "Reading",
51934
+ Language: "Language",
51935
+ Vocabulary: "Vocabulary",
51936
+ SocialStudies: "Social Studies",
51937
+ Writing: "Writing",
51938
+ Science: "Science",
51939
+ FastMath: "FastMath",
51940
+ Math: "Math",
51941
+ None: "None"
51942
+ };
51943
+ ONEROSTER_STATUS = {
51944
+ active: "active",
51945
+ toBeDeleted: "tobedeleted"
51946
+ };
51947
+ SCORE_STATUS = {
51948
+ exempt: "exempt",
51949
+ fullyGraded: "fully graded",
51950
+ notSubmitted: "not submitted",
51951
+ partiallyGraded: "partially graded",
51952
+ submitted: "submitted"
51953
+ };
51954
+ ENV_VARS = {
51955
+ clientId: "TIMEBACK_CLIENT_ID",
51956
+ clientSecret: "TIMEBACK_CLIENT_SECRET",
51957
+ baseUrl: "TIMEBACK_BASE_URL",
51958
+ environment: "TIMEBACK_ENVIRONMENT",
51959
+ vendorResourceId: "TIMEBACK_VENDOR_RESOURCE_ID",
51960
+ launchBaseUrl: "GAME_URL",
51961
+ qtiClientId: "QTI_CLIENT_ID",
51962
+ qtiClientSecret: "QTI_CLIENT_SECRET",
51963
+ qtiAuthUrl: "QTI_AUTH_URL"
51964
+ };
51965
+ HTTP_DEFAULTS = {
51966
+ timeout: 30000,
51967
+ retries: 3,
51968
+ retryBackoffBase: 2
51969
+ };
51970
+ AUTH_DEFAULTS = {
51971
+ tokenCacheDuration: 50000
51972
+ };
51973
+ CACHE_DEFAULTS = {
51974
+ defaultTTL: 600000,
51975
+ defaultMaxSize: 500,
51976
+ defaultName: "TimebackCache",
51977
+ studentTTL: 600000,
51978
+ studentMaxSize: 500,
51979
+ assessmentTTL: 1800000,
51980
+ assessmentMaxSize: 200,
51981
+ enrollmentTTL: 5000,
51982
+ enrollmentMaxSize: 100
51983
+ };
51984
+ CONFIG_DEFAULTS = {
51985
+ fileNames: ["timeback.config.js", "timeback.config.json"]
51986
+ };
51987
+ PLAYCADEMY_DEFAULTS = {
51988
+ organization: TIMEBACK_ORG_SOURCED_ID,
51989
+ launchBaseUrls: PLAYCADEMY_BASE_URLS
51990
+ };
51991
+ RESOURCE_DEFAULTS = {
51992
+ organization: {
51993
+ name: TIMEBACK_ORG_NAME,
51994
+ type: TIMEBACK_ORG_TYPE
51995
+ },
51996
+ course: {
51997
+ gradingScheme: TIMEBACK_COURSE_DEFAULTS.gradingScheme,
51998
+ level: TIMEBACK_COURSE_DEFAULTS.level,
51999
+ metadata: {
52000
+ goals: TIMEBACK_COURSE_DEFAULTS.goals,
52001
+ metrics: TIMEBACK_COURSE_DEFAULTS.metrics
52002
+ }
52003
+ },
52004
+ component: TIMEBACK_COMPONENT_DEFAULTS,
52005
+ resource: TIMEBACK_RESOURCE_DEFAULTS,
52006
+ componentResource: TIMEBACK_COMPONENT_RESOURCE_DEFAULTS
52007
+ };
52008
+ HTTP_STATUS = {
52009
+ CLIENT_ERROR_MIN: 400,
52010
+ CLIENT_ERROR_MAX: 500,
52011
+ SERVER_ERROR_MIN: 500
52012
+ };
52013
+ ERROR_NAMES = {
52014
+ timebackAuth: "TimebackAuthError",
52015
+ timebackApi: "TimebackApiError",
52016
+ timebackConfig: "TimebackConfigError",
52017
+ timebackSdk: "TimebackSDKError"
52018
+ };
52019
+ });
52020
+ init_constants3();
52021
+ SUBJECT_VALUES = TIMEBACK_SUBJECTS;
52022
+ GRADE_VALUES = TIMEBACK_GRADE_LEVELS;
52023
+ });
52024
+ function kebabToTitleCase(kebabStr) {
52025
+ return kebabStr.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
52026
+ }
52027
+ function isRecord2(value) {
52028
+ return typeof value === "object" && value !== null;
52029
+ }
52030
+ function filterEnrollmentsByGame(enrollments, gameId) {
52031
+ return enrollments.filter((enrollment) => enrollment.gameId === gameId).map(({ gameId: _2, ...enrollment }) => enrollment);
52032
+ }
52033
+ function mapEnrollmentsToUserEnrollments(enrollments, integrations) {
52034
+ const enrollmentByCourse = new Map(enrollments.map((enrollment) => [enrollment.courseId, enrollment]));
52035
+ const courseToSchool = new Map(enrollments.filter((enrollment) => enrollment.school?.id).map((enrollment) => [enrollment.courseId, enrollment.school.id]));
52036
+ return integrations.map((integration) => {
52037
+ const enrollment = enrollmentByCourse.get(integration.courseId);
52038
+ return {
52039
+ gameId: integration.gameId,
52040
+ grade: integration.grade,
52041
+ subject: integration.subject,
52042
+ courseId: integration.courseId,
52043
+ orgId: courseToSchool.get(integration.courseId),
52044
+ ...enrollment ? { id: enrollment.sourcedId } : {}
52045
+ };
52046
+ });
52047
+ }
52048
+ function buildGameTimebackSummaries(integrations) {
52049
+ const summaries = {};
52050
+ for (const integration of integrations) {
52051
+ const summary = summaries[integration.gameId] ?? {
52052
+ subject: null,
52053
+ hasActiveIntegration: false,
52054
+ hasRemovedIntegration: false
52055
+ };
52056
+ const isRemoved = integration.status === DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS;
52057
+ if (isRemoved) {
52058
+ summary.hasRemovedIntegration = true;
52059
+ } else {
52060
+ summary.hasActiveIntegration = true;
52061
+ summary.subject ??= integration.subject;
52062
+ }
52063
+ summaries[integration.gameId] = summary;
52064
+ }
52065
+ return summaries;
52066
+ }
52067
+ function getStringValue(value) {
52068
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
52069
+ }
52070
+ function parseSourcedIdFromUrl(url) {
52071
+ if (!url) {
52072
+ return;
52073
+ }
52074
+ const trimmed = url.trim().replace(/\/$/, "");
52075
+ if (!trimmed) {
52076
+ return;
52077
+ }
52078
+ const segments = trimmed.split("/");
52079
+ const lastSegment = segments.at(-1);
52080
+ return lastSegment ? decodeURIComponent(lastSegment) : undefined;
52081
+ }
52082
+ function getGeneratedMetricValue(event, type) {
52083
+ const items = event.generated?.items;
52084
+ if (!Array.isArray(items)) {
52085
+ return;
52086
+ }
52087
+ const metric = items.find((item) => item?.type === type);
52088
+ if (!metric) {
52089
+ return;
52090
+ }
52091
+ const value = typeof metric.value === "number" ? metric.value : Number(metric.value);
52092
+ return Number.isFinite(value) ? value : undefined;
52093
+ }
52094
+ function getMergedCaliperExtensions(event) {
52095
+ const objectActivityExtensions = isRecord2(event.object.activity?.extensions) ? event.object.activity.extensions : undefined;
52096
+ const generatedExtensions = isRecord2(event.generated?.extensions) ? event.generated.extensions : undefined;
52097
+ const eventExtensions = isRecord2(event.extensions) ? event.extensions : undefined;
52098
+ return {
52099
+ ...objectActivityExtensions,
52100
+ ...generatedExtensions,
52101
+ ...eventExtensions
52102
+ };
52103
+ }
52104
+ function getPlaycademyMetadata(event) {
52105
+ const extensions = getMergedCaliperExtensions(event);
52106
+ return isRecord2(extensions.playcademy) ? extensions.playcademy : undefined;
52107
+ }
52108
+ function getActivityId(event, playcademy) {
52109
+ const metadataActivityId = getStringValue(playcademy?.activityId);
52110
+ if (metadataActivityId) {
52111
+ return metadataActivityId;
52112
+ }
52113
+ const activityId = getStringValue(event.object.activity?.id);
52114
+ if (activityId) {
52115
+ return activityId;
52116
+ }
52117
+ const objectId = getStringValue(event.object.id);
52118
+ if (!objectId) {
52119
+ return;
52120
+ }
52121
+ const trimmed = objectId.replace(/\/$/, "");
52122
+ const segments = trimmed.split("/");
52123
+ const activityIndex = segments.lastIndexOf("activities");
52124
+ if (activityIndex !== -1 && segments.length >= activityIndex + 3) {
52125
+ const candidate = segments[activityIndex + 2];
52126
+ return candidate ? decodeURIComponent(candidate) : undefined;
52127
+ }
52128
+ return;
52129
+ }
52130
+ function buildResourceMetadata({
52131
+ baseMetadata,
52132
+ subject,
52133
+ grade,
52134
+ totalXp,
52135
+ masterableUnits
52136
+ }) {
52137
+ const normalizedBaseMetadata = isResourceMetadata(baseMetadata) ? baseMetadata : undefined;
52138
+ const metadata2 = {
52139
+ ...normalizedBaseMetadata
52140
+ };
52141
+ metadata2.subject = subject;
52142
+ metadata2.grades = [grade];
52143
+ metadata2.xp = totalXp;
52144
+ if (masterableUnits !== undefined && masterableUnits !== null) {
52145
+ const existingPlaycademy = isPlaycademyResourceMetadata(metadata2.playcademy) ? metadata2.playcademy : undefined;
52146
+ metadata2.playcademy = {
52147
+ ...existingPlaycademy,
52148
+ mastery: {
52149
+ ...existingPlaycademy?.mastery,
52150
+ masterableUnits
52151
+ }
52152
+ };
52153
+ }
52154
+ return metadata2;
52155
+ }
52156
+ function getDurationSecondsFromExtensions(event) {
52157
+ const extensions = getMergedCaliperExtensions(event);
52158
+ const playcademy = isRecord2(extensions.playcademy) ? extensions.playcademy : undefined;
52159
+ const rawValue = extensions.durationSeconds ?? playcademy?.durationSeconds;
52160
+ const value = typeof rawValue === "number" ? rawValue : Number(rawValue);
52161
+ return Number.isFinite(value) ? value : undefined;
52162
+ }
52163
+ function getCanonicalRunId(session2) {
52164
+ const sessionId = getStringValue(session2?.id);
52165
+ if (!sessionId) {
52166
+ return;
52167
+ }
52168
+ return sessionId.replace(/^urn:uuid:/, "");
52169
+ }
52170
+ function getResumeId(event) {
52171
+ const playcademy = getPlaycademyMetadata(event);
52172
+ return getStringValue(playcademy?.resumeId);
52173
+ }
52174
+ function isCaliperRemediationOrCompletionEvent(event) {
52175
+ const playcademy = getPlaycademyMetadata(event);
52176
+ return REMEDIATION_OR_COMPLETION_EVENT_KINDS.has(getStringValue(playcademy?.eventKind) || "");
52177
+ }
52178
+ function groupCaliperEventsByRun(events) {
52179
+ const groups = new Map;
52180
+ for (const event of events) {
52181
+ const objectId = getStringValue(event.object.id) || "unknown-activity";
52182
+ const groupKey = `${objectId}::${getStringValue(event.session?.id) || event.externalId}`;
52183
+ const existing = groups.get(groupKey);
52184
+ if (existing) {
52185
+ existing.push(event);
52186
+ } else {
52187
+ groups.set(groupKey, [event]);
52188
+ }
52189
+ }
52190
+ return groups;
52191
+ }
52192
+ function findCaliperEventGroupContainingExternalId(events, externalId) {
52193
+ const targetExternalId = externalId.trim();
52194
+ if (!targetExternalId) {
52195
+ return;
52196
+ }
52197
+ return [...groupCaliperEventsByRun(events).values()].find((group) => group.some((event) => event.externalId === targetExternalId));
52198
+ }
52199
+ function mapCaliperEventGroupToActivity(events, relevantCourseIds) {
52200
+ if (events.length === 0) {
52201
+ return null;
52202
+ }
52203
+ const sortedEvents = events.toSorted((a, b) => a.eventTime.localeCompare(b.eventTime));
52204
+ const activityEvent = [...sortedEvents].toReversed().find((event) => event.type === "ActivityEvent");
52205
+ const contextSource = activityEvent || sortedEvents.at(-1);
52206
+ if (!contextSource) {
52207
+ return null;
52208
+ }
52209
+ const ctx = parseCaliperEventContext(contextSource, relevantCourseIds);
52210
+ if (!ctx) {
52211
+ return null;
52212
+ }
52213
+ const score = activityEvent !== undefined ? (() => {
52214
+ const totalQuestions = getGeneratedMetricValue(activityEvent, "totalQuestions");
52215
+ const correctQuestions = getGeneratedMetricValue(activityEvent, "correctQuestions");
52216
+ if (totalQuestions === undefined || correctQuestions === undefined || totalQuestions <= 0) {
52217
+ return;
52218
+ }
52219
+ return correctQuestions / totalQuestions * 100;
52220
+ })() : undefined;
52221
+ const xpEarned = activityEvent !== undefined ? getGeneratedMetricValue(activityEvent, "xpEarned") : undefined;
52222
+ const masteredUnits = activityEvent !== undefined ? getGeneratedMetricValue(activityEvent, "masteredUnits") : undefined;
52223
+ const timeSpentEvents = sortedEvents.filter((event) => event.type === "TimeSpentEvent");
52224
+ let totalActiveTimeSeconds;
52225
+ if (timeSpentEvents.length > 0) {
52226
+ totalActiveTimeSeconds = timeSpentEvents.reduce((sum, event) => sum + (getGeneratedMetricValue(event, "active") ?? 0), 0);
52227
+ } else if (activityEvent !== undefined) {
52228
+ totalActiveTimeSeconds = getDurationSecondsFromExtensions(activityEvent);
52229
+ }
52230
+ const fallbackActivityId = getActivityId(contextSource, getPlaycademyMetadata(contextSource));
52231
+ const occurredAt = getStringValue(activityEvent?.eventTime) || getStringValue(sortedEvents.at(-1)?.eventTime);
52232
+ const runId = getCanonicalRunId(contextSource.session);
52233
+ const resumeIds = new Set(sortedEvents.map((event) => getResumeId(event)).filter((resumeId) => resumeId !== undefined));
52234
+ const sessionCount = resumeIds.size > 0 ? resumeIds.size : 1;
52235
+ const kind = activityEvent !== undefined ? "activity" : "activity-in-progress";
52236
+ if (!occurredAt) {
52237
+ return null;
52238
+ }
52239
+ return {
52240
+ id: activityEvent?.externalId || sortedEvents.at(-1)?.externalId || events[0].externalId,
52241
+ kind,
52242
+ occurredAt,
52243
+ courseId: ctx.courseId,
52244
+ title: getStringValue(activityEvent?.object.activity?.name) || ctx.titleFromEvent || (fallbackActivityId ? kebabToTitleCase(fallbackActivityId) : "Activity completed"),
52245
+ ...ctx.activityId ? { activityId: ctx.activityId } : {},
52246
+ ...ctx.appName ? { appName: ctx.appName } : {},
52247
+ ...score !== undefined ? { score } : {},
52248
+ ...xpEarned !== undefined ? { xpDelta: xpEarned } : {},
52249
+ ...masteredUnits !== undefined ? { masteredUnitsDelta: masteredUnits } : {},
52250
+ ...totalActiveTimeSeconds !== undefined ? { timeDeltaSeconds: totalActiveTimeSeconds } : {},
52251
+ ...runId ? { runId } : {},
52252
+ ...sessionCount > 0 ? { sessionCount } : {}
52253
+ };
52254
+ }
52255
+ function parseCaliperEventContext(event, relevantCourseIds) {
52256
+ const playcademy = getPlaycademyMetadata(event);
52257
+ const courseId = getStringValue(playcademy?.courseId) || parseSourcedIdFromUrl(event.object.course?.id);
52258
+ if (!courseId || !relevantCourseIds.has(courseId)) {
52259
+ return null;
52260
+ }
52261
+ const occurredAt = getStringValue(event.eventTime);
52262
+ if (!occurredAt) {
52263
+ return null;
52264
+ }
52265
+ return {
52266
+ courseId,
52267
+ occurredAt,
52268
+ eventKind: getStringValue(playcademy?.eventKind),
52269
+ source: getStringValue(playcademy?.source),
52270
+ reason: getStringValue(playcademy?.reason),
52271
+ titleFromEvent: getStringValue(event.object.activity?.name),
52272
+ appName: getStringValue(event.object.app?.name),
52273
+ activityId: getActivityId(event, playcademy)
52274
+ };
52275
+ }
52276
+ function mapTimeSpentRemediation(event, ctx) {
52277
+ if (ctx.eventKind !== "remediation-time") {
52278
+ return null;
52279
+ }
52280
+ return {
52281
+ id: event.externalId,
52282
+ kind: "remediation-time",
52283
+ occurredAt: ctx.occurredAt,
52284
+ courseId: ctx.courseId,
52285
+ title: "Time Adjustment",
52286
+ activityId: ctx.activityId,
52287
+ appName: ctx.appName,
52288
+ reason: ctx.reason,
52289
+ timeDeltaSeconds: getGeneratedMetricValue(event, "active")
52290
+ };
52291
+ }
52292
+ function mapActivityRemediation(event, ctx) {
52293
+ if (ctx.eventKind === "remediation-xp") {
52294
+ return {
52295
+ id: event.externalId,
52296
+ kind: "remediation-xp",
52297
+ occurredAt: ctx.occurredAt,
52298
+ courseId: ctx.courseId,
52299
+ title: "XP Adjustment",
52300
+ activityId: ctx.activityId,
52301
+ appName: ctx.appName,
52302
+ reason: ctx.reason,
52303
+ xpDelta: getGeneratedMetricValue(event, "xpEarned"),
52304
+ masteredUnitsDelta: getGeneratedMetricValue(event, "masteredUnits")
52305
+ };
52306
+ }
52307
+ if (ctx.eventKind === "remediation-mastery") {
52308
+ return {
52309
+ id: event.externalId,
52310
+ kind: "remediation-mastery",
52311
+ occurredAt: ctx.occurredAt,
52312
+ courseId: ctx.courseId,
52313
+ title: "Mastery Adjustment",
52314
+ activityId: ctx.activityId,
52315
+ appName: ctx.appName,
52316
+ reason: ctx.reason,
52317
+ xpDelta: getGeneratedMetricValue(event, "xpEarned"),
52318
+ masteredUnitsDelta: getGeneratedMetricValue(event, "masteredUnits")
52319
+ };
52320
+ }
52321
+ if (ctx.eventKind === "course-completed") {
52322
+ return {
52323
+ id: event.externalId,
52324
+ kind: "course-completed",
52325
+ occurredAt: ctx.occurredAt,
52326
+ courseId: ctx.courseId,
52327
+ title: ctx.source === "admin" ? "Course marked complete" : "Course completed",
52328
+ activityId: ctx.activityId,
52329
+ appName: ctx.appName,
52330
+ reason: ctx.reason
52331
+ };
52332
+ }
52333
+ if (ctx.eventKind === "course-resumed") {
52334
+ return {
52335
+ id: event.externalId,
52336
+ kind: "course-resumed",
52337
+ occurredAt: ctx.occurredAt,
52338
+ courseId: ctx.courseId,
52339
+ title: "Course resumed",
52340
+ activityId: ctx.activityId,
52341
+ appName: ctx.appName,
52342
+ reason: ctx.reason
52343
+ };
52344
+ }
52345
+ return null;
52346
+ }
52347
+ function mapCaliperEventToRemediationActivity(event, relevantCourseIds) {
52348
+ const ctx = parseCaliperEventContext(event, relevantCourseIds);
52349
+ if (!ctx) {
52350
+ return null;
52351
+ }
52352
+ if (event.type === "TimeSpentEvent") {
52353
+ return mapTimeSpentRemediation(event, ctx);
52354
+ }
52355
+ if (event.type === "ActivityEvent") {
52356
+ return mapActivityRemediation(event, ctx);
52357
+ }
52358
+ return null;
52359
+ }
52360
+ var REMEDIATION_OR_COMPLETION_EVENT_KINDS;
52361
+ var init_timeback_util = __esm(() => {
52362
+ init_helpers_index();
52363
+ init_types2();
52364
+ REMEDIATION_OR_COMPLETION_EVENT_KINDS = new Set([
52365
+ "remediation-xp",
52366
+ "remediation-time",
52367
+ "remediation-mastery",
52368
+ "course-completed",
52369
+ "course-resumed"
52370
+ ]);
52371
+ });
51757
52372
  var inFlightManifestFetches;
51758
52373
  var GameService;
51759
52374
  var init_game_service = __esm(() => {
51760
52375
  init_drizzle_orm();
51761
52376
  init_src();
52377
+ init_helpers_index();
51762
52378
  init_tables_index();
51763
52379
  init_spans();
51764
52380
  init_errors();
51765
52381
  init_deployment_util();
52382
+ init_timeback_util();
51766
52383
  inFlightManifestFetches = new Map;
51767
52384
  GameService = class GameService2 {
51768
52385
  deps;
@@ -51906,6 +52523,7 @@ var init_game_service = __esm(() => {
51906
52523
  const db2 = this.deps.db;
51907
52524
  const integrations = await db2.query.gameTimebackIntegrations.findMany({
51908
52525
  columns: { gameId: true, subject: true },
52526
+ where: isActiveGameTimebackIntegrationStatus(),
51909
52527
  orderBy: [asc(gameTimebackIntegrations.createdAt)]
51910
52528
  });
51911
52529
  const subjectMap = {};
@@ -51916,6 +52534,26 @@ var init_game_service = __esm(() => {
51916
52534
  }
51917
52535
  return subjectMap;
51918
52536
  }
52537
+ async getTimebackSummaries(user) {
52538
+ const db2 = this.deps.db;
52539
+ const canSeeAllGames = user.role === "admin" || user.role === "teacher";
52540
+ let accessibleGameIds = null;
52541
+ if (!canSeeAllGames) {
52542
+ const accessibleGames = await this.listAccessible(user);
52543
+ accessibleGameIds = new Set(accessibleGames.map((game2) => game2.id));
52544
+ }
52545
+ if (accessibleGameIds?.size === 0) {
52546
+ return {};
52547
+ }
52548
+ const integrations = await db2.query.gameTimebackIntegrations.findMany({
52549
+ columns: { gameId: true, subject: true, status: true },
52550
+ ...accessibleGameIds && {
52551
+ where: inArray(gameTimebackIntegrations.gameId, [...accessibleGameIds])
52552
+ },
52553
+ orderBy: [asc(gameTimebackIntegrations.createdAt)]
52554
+ });
52555
+ return buildGameTimebackSummaries(integrations);
52556
+ }
51919
52557
  async getById(gameId, caller) {
51920
52558
  const db2 = this.deps.db;
51921
52559
  const game2 = await db2.query.games.findFirst({
@@ -52545,7 +53183,7 @@ class DiscordEmbedBuilder {
52545
53183
  }
52546
53184
  var init_client2 = () => {};
52547
53185
  var DiscordColors;
52548
- var init_types2 = __esm(() => {
53186
+ var init_types3 = __esm(() => {
52549
53187
  DiscordColors = {
52550
53188
  DEFAULT: 0,
52551
53189
  WHITE: 16777215,
@@ -52581,7 +53219,7 @@ var init_types2 = __esm(() => {
52581
53219
  });
52582
53220
  var init_discord = __esm(() => {
52583
53221
  init_client2();
52584
- init_types2();
53222
+ init_types3();
52585
53223
  });
52586
53224
  function truncateField(value, maxLength = DISCORD_FIELD_LIMIT) {
52587
53225
  if (value.length <= maxLength) {
@@ -53702,7 +54340,7 @@ var init_secrets_service = __esm(() => {
53702
54340
  });
53703
54341
  var ASSET_ROUTE_PREFIX = "/api/assets/";
53704
54342
  var ROUTES;
53705
- var init_constants3 = __esm(() => {
54343
+ var init_constants4 = __esm(() => {
53706
54344
  init_src();
53707
54345
  ROUTES = {
53708
54346
  INDEX: "/api",
@@ -53728,7 +54366,7 @@ function prefixSecrets(secrets) {
53728
54366
  }
53729
54367
  var init_setup2 = __esm(() => {
53730
54368
  init_src();
53731
- init_constants3();
54369
+ init_constants4();
53732
54370
  });
53733
54371
 
53734
54372
  class SeedService {
@@ -54469,11 +55107,11 @@ var init_schemas3 = __esm(() => {
54469
55107
  metadata: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
54470
55108
  });
54471
55109
  });
54472
- function isTimebackGrade(value) {
55110
+ function isTimebackGrade2(value) {
54473
55111
  return Number.isInteger(value) && TIMEBACK_GRADES.includes(value);
54474
55112
  }
54475
- function isTimebackSubject(value) {
54476
- return TIMEBACK_SUBJECTS.includes(value);
55113
+ function isTimebackSubject2(value) {
55114
+ return TIMEBACK_SUBJECTS2.includes(value);
54477
55115
  }
54478
55116
  function isValidAdminAttributionDate(value) {
54479
55117
  const match = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
@@ -54488,11 +55126,12 @@ function isValidAdminAttributionDate(value) {
54488
55126
  return date3.getUTCFullYear() === year && date3.getUTCMonth() + 1 === month && date3.getUTCDate() === day;
54489
55127
  }
54490
55128
  var TIMEBACK_GRADES;
54491
- var TIMEBACK_SUBJECTS;
55129
+ var TIMEBACK_SUBJECTS2;
54492
55130
  var TimebackGradeSchema;
54493
55131
  var TimebackSubjectSchema;
54494
55132
  var CourseGoalsSchema;
54495
55133
  var UpdateGameTimebackIntegrationRequestSchema;
55134
+ var CreateGameTimebackIntegrationRequestSchema;
54496
55135
  var TimebackActivityDataSchema;
54497
55136
  var EndActivityRequestSchema;
54498
55137
  var GameRunMetricsSchema;
@@ -54527,7 +55166,7 @@ var init_schemas4 = __esm(() => {
54527
55166
  init_esm();
54528
55167
  init_table7();
54529
55168
  TIMEBACK_GRADES = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
54530
- TIMEBACK_SUBJECTS = [
55169
+ TIMEBACK_SUBJECTS2 = [
54531
55170
  "Reading",
54532
55171
  "Language",
54533
55172
  "Vocabulary",
@@ -54541,7 +55180,7 @@ var init_schemas4 = __esm(() => {
54541
55180
  TimebackGradeSchema = exports_external.number().int().refine((val) => TIMEBACK_GRADES.includes(val), {
54542
55181
  message: `Grade must be one of: ${TIMEBACK_GRADES.join(", ")}`
54543
55182
  });
54544
- TimebackSubjectSchema = exports_external.enum(TIMEBACK_SUBJECTS);
55183
+ TimebackSubjectSchema = exports_external.enum(TIMEBACK_SUBJECTS2);
54545
55184
  CourseGoalsSchema = exports_external.object({
54546
55185
  dailyXp: exports_external.number().int().nonnegative().nullable().optional(),
54547
55186
  dailyLessons: exports_external.number().int().nonnegative().nullable().optional(),
@@ -54560,6 +55199,15 @@ var init_schemas4 = __esm(() => {
54560
55199
  isSupplemental: exports_external.boolean().optional(),
54561
55200
  timebackVisible: exports_external.boolean().nullable().optional()
54562
55201
  });
55202
+ CreateGameTimebackIntegrationRequestSchema = exports_external.object({
55203
+ title: exports_external.string().trim().min(1),
55204
+ courseCode: exports_external.string().trim().min(1),
55205
+ subject: TimebackSubjectSchema,
55206
+ grade: TimebackGradeSchema,
55207
+ totalXp: exports_external.number().int().positive(),
55208
+ masterableUnits: exports_external.number().int().nonnegative(),
55209
+ level: exports_external.string().trim().min(1).optional()
55210
+ });
54563
55211
  TimebackActivityDataSchema = exports_external.object({
54564
55212
  activityId: exports_external.string().min(1),
54565
55213
  activityName: exports_external.string().optional(),
@@ -55001,9 +55649,6 @@ var init_log = __esm(() => {
55001
55649
  init_ansi();
55002
55650
  init_spinner();
55003
55651
  });
55004
- function kebabToTitleCase(kebabStr) {
55005
- return kebabStr.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
55006
- }
55007
55652
  function formatDateYMDInTimezone(timeZone, date3 = new Date) {
55008
55653
  const parts2 = new Intl.DateTimeFormat("en-US", {
55009
55654
  timeZone,
@@ -55106,14 +55751,42 @@ var init_src4 = __esm(() => {
55106
55751
  init_pure();
55107
55752
  });
55108
55753
  function createOneRosterUrls(baseUrl) {
55109
- const effective = baseUrl || TIMEBACK_API_URLS.production;
55754
+ const effective = baseUrl || TIMEBACK_API_URLS2.production;
55110
55755
  const base = effective.replace(/\/$/, "");
55111
55756
  return {
55112
- user: (userId) => `${base}${ONEROSTER_ENDPOINTS.users}/${userId}`,
55113
- course: (courseId) => `${base}${ONEROSTER_ENDPOINTS.courses}/${courseId}`,
55114
- componentResource: (resourceId) => `${base}${ONEROSTER_ENDPOINTS.componentResources}/${resourceId}`
55757
+ user: (userId) => `${base}${ONEROSTER_ENDPOINTS2.users}/${userId}`,
55758
+ course: (courseId) => `${base}${ONEROSTER_ENDPOINTS2.courses}/${courseId}`,
55759
+ componentResource: (resourceId) => `${base}${ONEROSTER_ENDPOINTS2.componentResources}/${resourceId}`
55115
55760
  };
55116
55761
  }
55762
+ function resolveCourseIdFromCourseUrl(url2) {
55763
+ try {
55764
+ const parts2 = new URL(url2).pathname.split("/").filter(Boolean);
55765
+ const maybeCourses = parts2.at(-2);
55766
+ const maybeId = parts2.at(-1);
55767
+ if (maybeCourses !== "courses" || !maybeId) {
55768
+ return null;
55769
+ }
55770
+ if (UUID_REGEX2.test(maybeId)) {
55771
+ return maybeId;
55772
+ }
55773
+ if (maybeId.length > 2 && !maybeId.includes("/")) {
55774
+ return maybeId;
55775
+ }
55776
+ return null;
55777
+ } catch {
55778
+ return null;
55779
+ }
55780
+ }
55781
+ function computeCaliperLineItemId(objectId, courseSourcedId, courseUrl) {
55782
+ const recovered = resolveCourseIdFromCourseUrl(courseUrl);
55783
+ if (recovered !== courseSourcedId) {
55784
+ throw new ConfigurationError("courseId", `Course id "${courseSourcedId}" is not URL-safe: Timeback Platform would parse it back as ${recovered === null ? "null" : `"${recovered}"`} from the Caliper event, producing a different gradebook line item. Use a UUID or an id with no spaces, slashes, or non-ASCII characters.`);
55785
+ }
55786
+ const idParts = [objectId, courseSourcedId].join("_");
55787
+ const hashedId = createHash("sha256").update(idParts).digest("hex");
55788
+ return `caliper_${hashedId}`;
55789
+ }
55117
55790
  function deriveSourcedIds(courseId) {
55118
55791
  return {
55119
55792
  course: courseId,
@@ -55125,7 +55798,7 @@ function deriveSourcedIds(courseId) {
55125
55798
  async function fetchTimebackConfig(client, courseId) {
55126
55799
  const sourcedIds = deriveSourcedIds(courseId);
55127
55800
  const [org, course, component, resource, componentResource] = await Promise.all([
55128
- client.oneroster.organizations.get(PLAYCADEMY_DEFAULTS.organization),
55801
+ client.oneroster.organizations.get(PLAYCADEMY_DEFAULTS2.organization),
55129
55802
  client.oneroster.courses.get(sourcedIds.course),
55130
55803
  client.oneroster.courseComponents.get(sourcedIds.component),
55131
55804
  client.oneroster.resources.get(sourcedIds.resource),
@@ -55135,7 +55808,7 @@ async function fetchTimebackConfig(client, courseId) {
55135
55808
  organization: {
55136
55809
  name: org.name,
55137
55810
  type: org.type,
55138
- identifier: org.identifier || PLAYCADEMY_DEFAULTS.organization
55811
+ identifier: org.identifier || PLAYCADEMY_DEFAULTS2.organization
55139
55812
  },
55140
55813
  course: {
55141
55814
  title: course.title || "",
@@ -55195,23 +55868,23 @@ async function verifyTimebackResources(client, courseId) {
55195
55868
  }
55196
55869
  };
55197
55870
  }
55198
- function isObject(value) {
55871
+ function isObject2(value) {
55199
55872
  return typeof value === "object" && value !== null;
55200
55873
  }
55201
- function isPlaycademyResourceMetadata(value) {
55202
- if (!isObject(value)) {
55874
+ function isPlaycademyResourceMetadata2(value) {
55875
+ if (!isObject2(value)) {
55203
55876
  return false;
55204
55877
  }
55205
55878
  if (!("mastery" in value) || value.mastery === undefined) {
55206
55879
  return true;
55207
55880
  }
55208
- return isObject(value.mastery);
55881
+ return isObject2(value.mastery);
55209
55882
  }
55210
- function isTimebackSubject2(value) {
55211
- return typeof value === "string" && SUBJECT_VALUES.includes(value);
55883
+ function isTimebackSubject3(value) {
55884
+ return typeof value === "string" && SUBJECT_VALUES2.includes(value);
55212
55885
  }
55213
- function isTimebackGrade2(value) {
55214
- return typeof value === "number" && Number.isInteger(value) && GRADE_VALUES.includes(value);
55886
+ function isTimebackGrade3(value) {
55887
+ return typeof value === "number" && Number.isInteger(value) && GRADE_VALUES2.includes(value);
55215
55888
  }
55216
55889
  async function deleteTimebackResources(client, courseId) {
55217
55890
  const sourcedIds = deriveSourcedIds(courseId);
@@ -55262,6 +55935,15 @@ async function deleteTimebackResources(client, courseId) {
55262
55935
  ]);
55263
55936
  setAttribute("app.timeback.resources_deleted", true);
55264
55937
  }
55938
+ async function updateTimebackCourseStatus(client, courseId, status) {
55939
+ const course = await client.oneroster.courses.get(courseId);
55940
+ await client.oneroster.courses.update(courseId, {
55941
+ ...course,
55942
+ sourcedId: courseId,
55943
+ status
55944
+ });
55945
+ setAttribute("app.timeback.course_status", status);
55946
+ }
55265
55947
  async function createCourse(client, config2) {
55266
55948
  const courseData = {
55267
55949
  status: "active",
@@ -55510,14 +56192,14 @@ async function getTimebackTokenResponse(config2) {
55510
56192
  }
55511
56193
  }
55512
56194
  function getAuthUrl(environment = "production") {
55513
- return TIMEBACK_AUTH_URLS[environment];
56195
+ return TIMEBACK_AUTH_URLS2[environment];
55514
56196
  }
55515
56197
  function parseEduBridgeGrade(value) {
55516
56198
  if (value === null || value === undefined || value.trim() === "") {
55517
56199
  return null;
55518
56200
  }
55519
56201
  const parsed = Number(value);
55520
- return isTimebackGrade2(parsed) ? parsed : null;
56202
+ return isTimebackGrade3(parsed) ? parsed : null;
55521
56203
  }
55522
56204
  function normalizeHighestGradeMastered(response, subject) {
55523
56205
  const grades = {
@@ -55594,12 +56276,12 @@ async function withTimebackClientTelemetry(fn) {
55594
56276
  }
55595
56277
  function handleHttpError(res, errorBody, attempt, retries, context2) {
55596
56278
  const error = new TimebackApiError(res.status, res.statusText, errorBody);
55597
- if (res.status >= HTTP_STATUS.CLIENT_ERROR_MIN && res.status < HTTP_STATUS.CLIENT_ERROR_MAX) {
56279
+ if (res.status >= HTTP_STATUS2.CLIENT_ERROR_MIN && res.status < HTTP_STATUS2.CLIENT_ERROR_MAX) {
55598
56280
  recordTimebackHttpFailure();
55599
56281
  throw error;
55600
56282
  }
55601
56283
  if (attempt < retries) {
55602
- const delay = HTTP_DEFAULTS.retryBackoffBase ** attempt * 1000;
56284
+ const delay = HTTP_DEFAULTS2.retryBackoffBase ** attempt * 1000;
55603
56285
  recordTimebackRetry();
55604
56286
  addEvent("timeback.request_retry", {
55605
56287
  "app.timeback.attempt": attempt + 1,
@@ -55676,7 +56358,7 @@ async function request({
55676
56358
  const result = handleHttpError(res, errorBody, attempt, retries, { method, path: path2 });
55677
56359
  lastError = result.error;
55678
56360
  if (result.retry) {
55679
- const delay = HTTP_DEFAULTS.retryBackoffBase ** attempt * 1000;
56361
+ const delay = HTTP_DEFAULTS2.retryBackoffBase ** attempt * 1000;
55680
56362
  await new Promise((resolve2) => setTimeout(resolve2, delay));
55681
56363
  }
55682
56364
  } else {
@@ -55688,7 +56370,7 @@ async function request({
55688
56370
  }
55689
56371
  lastError = error instanceof Error ? error : new Error(String(error));
55690
56372
  if (attempt < retries) {
55691
- const delay = HTTP_DEFAULTS.retryBackoffBase ** attempt * 1000;
56373
+ const delay = HTTP_DEFAULTS2.retryBackoffBase ** attempt * 1000;
55692
56374
  recordTimebackRetry();
55693
56375
  addEvent("timeback.network_retry", {
55694
56376
  "app.timeback.attempt": attempt + 1,
@@ -55737,10 +56419,10 @@ function createCaliperNamespace(client) {
55737
56419
  const envelope = {
55738
56420
  sensor: sensorUrl,
55739
56421
  sendTime: new Date().toISOString(),
55740
- dataVersion: CALIPER_CONSTANTS.dataVersion,
56422
+ dataVersion: CALIPER_CONSTANTS2.dataVersion,
55741
56423
  data: [event]
55742
56424
  };
55743
- return client["requestCaliper"](CALIPER_ENDPOINTS.event, "POST", envelope);
56425
+ return client["requestCaliper"](CALIPER_ENDPOINTS2.event, "POST", envelope);
55744
56426
  },
55745
56427
  emitBatch: async (events, sensorUrl) => {
55746
56428
  if (events.length === 0) {
@@ -55749,10 +56431,10 @@ function createCaliperNamespace(client) {
55749
56431
  const envelope = {
55750
56432
  sensor: sensorUrl,
55751
56433
  sendTime: new Date().toISOString(),
55752
- dataVersion: CALIPER_CONSTANTS.dataVersion,
56434
+ dataVersion: CALIPER_CONSTANTS2.dataVersion,
55753
56435
  data: events
55754
56436
  };
55755
- return client["requestCaliper"](CALIPER_ENDPOINTS.event, "POST", envelope);
56437
+ return client["requestCaliper"](CALIPER_ENDPOINTS2.event, "POST", envelope);
55756
56438
  },
55757
56439
  events: {
55758
56440
  list: async (params = {}) => {
@@ -55782,7 +56464,7 @@ function createCaliperNamespace(client) {
55782
56464
  query.set(`extensions.${key}`, value);
55783
56465
  }
55784
56466
  }
55785
- const requestPath = `${CALIPER_ENDPOINTS.events}?${query.toString()}`;
56467
+ const requestPath = `${CALIPER_ENDPOINTS2.events}?${query.toString()}`;
55786
56468
  return client["requestCaliper"](requestPath, "GET");
55787
56469
  }
55788
56470
  },
@@ -55792,21 +56474,21 @@ function createCaliperNamespace(client) {
55792
56474
  gameId: data.gameId
55793
56475
  });
55794
56476
  const event = {
55795
- "@context": CALIPER_CONSTANTS.context,
56477
+ "@context": CALIPER_CONSTANTS2.context,
55796
56478
  id: `urn:uuid:${crypto.randomUUID()}`,
55797
- type: TIMEBACK_EVENT_TYPES.activityEvent,
56479
+ type: TIMEBACK_EVENT_TYPES2.activityEvent,
55798
56480
  eventTime: data.eventTime || new Date().toISOString(),
55799
- profile: CALIPER_CONSTANTS.profile,
56481
+ profile: CALIPER_CONSTANTS2.profile,
55800
56482
  actor: {
55801
56483
  id: urls.user(data.studentId),
55802
- type: TIMEBACK_TYPES.user,
56484
+ type: TIMEBACK_TYPES2.user,
55803
56485
  email: data.studentEmail
55804
56486
  },
55805
- action: TIMEBACK_ACTIONS.completed,
56487
+ action: TIMEBACK_ACTIONS2.completed,
55806
56488
  ...data.runId ? { session: `urn:uuid:${data.runId}` } : {},
55807
56489
  object: {
55808
56490
  id: data.objectId || caliper.buildActivityUrl(data),
55809
- type: TIMEBACK_TYPES.activityContext,
56491
+ type: TIMEBACK_TYPES2.activityContext,
55810
56492
  subject: data.subject,
55811
56493
  app: {
55812
56494
  name: data.appName
@@ -55820,25 +56502,25 @@ function createCaliperNamespace(client) {
55820
56502
  },
55821
56503
  generated: {
55822
56504
  id: data.generatedId || `urn:timeback:metrics:activity-completion-${crypto.randomUUID()}`,
55823
- type: TIMEBACK_TYPES.activityMetricsCollection,
56505
+ type: TIMEBACK_TYPES2.activityMetricsCollection,
55824
56506
  ...data.includeAttempt === false ? {} : { attempt: data.attemptNumber || 1 },
55825
56507
  items: [
55826
56508
  ...data.totalQuestions !== undefined ? [
55827
56509
  {
55828
- type: ACTIVITY_METRIC_TYPES.totalQuestions,
56510
+ type: ACTIVITY_METRIC_TYPES2.totalQuestions,
55829
56511
  value: data.totalQuestions
55830
56512
  }
55831
56513
  ] : [],
55832
56514
  ...data.correctQuestions !== undefined ? [
55833
56515
  {
55834
- type: ACTIVITY_METRIC_TYPES.correctQuestions,
56516
+ type: ACTIVITY_METRIC_TYPES2.correctQuestions,
55835
56517
  value: data.correctQuestions
55836
56518
  }
55837
56519
  ] : [],
55838
- ...data.xpEarned !== undefined ? [{ type: ACTIVITY_METRIC_TYPES.xpEarned, value: data.xpEarned }] : [],
56520
+ ...data.xpEarned !== undefined ? [{ type: ACTIVITY_METRIC_TYPES2.xpEarned, value: data.xpEarned }] : [],
55839
56521
  ...data.masteredUnits !== undefined ? [
55840
56522
  {
55841
- type: ACTIVITY_METRIC_TYPES.masteredUnits,
56523
+ type: ACTIVITY_METRIC_TYPES2.masteredUnits,
55842
56524
  value: data.masteredUnits
55843
56525
  }
55844
56526
  ] : []
@@ -55860,21 +56542,21 @@ function createCaliperNamespace(client) {
55860
56542
  gameId: data.gameId
55861
56543
  });
55862
56544
  const event = {
55863
- "@context": CALIPER_CONSTANTS.context,
56545
+ "@context": CALIPER_CONSTANTS2.context,
55864
56546
  id: `urn:uuid:${crypto.randomUUID()}`,
55865
- type: TIMEBACK_EVENT_TYPES.timeSpentEvent,
56547
+ type: TIMEBACK_EVENT_TYPES2.timeSpentEvent,
55866
56548
  eventTime: data.eventTime || new Date().toISOString(),
55867
- profile: CALIPER_CONSTANTS.profile,
56549
+ profile: CALIPER_CONSTANTS2.profile,
55868
56550
  actor: {
55869
56551
  id: urls.user(data.studentId),
55870
- type: TIMEBACK_TYPES.user,
56552
+ type: TIMEBACK_TYPES2.user,
55871
56553
  email: data.studentEmail
55872
56554
  },
55873
- action: TIMEBACK_ACTIONS.spentTime,
56555
+ action: TIMEBACK_ACTIONS2.spentTime,
55874
56556
  ...data.runId ? { session: `urn:uuid:${data.runId}` } : {},
55875
56557
  object: {
55876
56558
  id: caliper.buildActivityUrl(data),
55877
- type: TIMEBACK_TYPES.activityContext,
56559
+ type: TIMEBACK_TYPES2.activityContext,
55878
56560
  subject: data.subject,
55879
56561
  app: {
55880
56562
  name: data.appName
@@ -55887,16 +56569,16 @@ function createCaliperNamespace(client) {
55887
56569
  },
55888
56570
  generated: {
55889
56571
  id: `urn:timeback:metrics:time-spent-${crypto.randomUUID()}`,
55890
- type: TIMEBACK_TYPES.timeSpentMetricsCollection,
56572
+ type: TIMEBACK_TYPES2.timeSpentMetricsCollection,
55891
56573
  items: [
55892
- { type: TIME_METRIC_TYPES.active, value: data.activeTimeSeconds },
56574
+ { type: TIME_METRIC_TYPES2.active, value: data.activeTimeSeconds },
55893
56575
  ...data.inactiveTimeSeconds !== undefined ? [
55894
56576
  {
55895
- type: TIME_METRIC_TYPES.inactive,
56577
+ type: TIME_METRIC_TYPES2.inactive,
55896
56578
  value: data.inactiveTimeSeconds
55897
56579
  }
55898
56580
  ] : [],
55899
- ...data.wasteTimeSeconds !== undefined ? [{ type: TIME_METRIC_TYPES.waste, value: data.wasteTimeSeconds }] : []
56581
+ ...data.wasteTimeSeconds !== undefined ? [{ type: TIME_METRIC_TYPES2.waste, value: data.wasteTimeSeconds }] : []
55900
56582
  ],
55901
56583
  ...data.extensions ? { extensions: data.extensions } : {}
55902
56584
  },
@@ -55907,7 +56589,8 @@ function createCaliperNamespace(client) {
55907
56589
  buildActivityUrl: (data) => {
55908
56590
  const base = data.sensorUrl.replace(/\/$/, "");
55909
56591
  return `${base}/activities/${encodeURIComponent(data.courseId)}/${encodeURIComponent(data.activityId)}`;
55910
- }
56592
+ },
56593
+ buildCourseUrl: (courseId) => urls.course(courseId)
55911
56594
  };
55912
56595
  return caliper;
55913
56596
  }
@@ -56030,17 +56713,17 @@ async function listOneRosterCollection(client, endpoint, collectionKey, options)
56030
56713
  function createOneRosterNamespace(client) {
56031
56714
  return {
56032
56715
  classes: {
56033
- create: async (data) => client["request"](ONEROSTER_ENDPOINTS.classes, "POST", { class: data }),
56716
+ create: async (data) => client["request"](ONEROSTER_ENDPOINTS2.classes, "POST", { class: data }),
56034
56717
  get: async (sourcedId) => {
56035
- const res = await client["request"](`${ONEROSTER_ENDPOINTS.classes}/${sourcedId}`, "GET");
56718
+ const res = await client["request"](`${ONEROSTER_ENDPOINTS2.classes}/${sourcedId}`, "GET");
56036
56719
  return res.class;
56037
56720
  },
56038
56721
  update: async (sourcedId, data) => {
56039
- const res = await client["request"](`${ONEROSTER_ENDPOINTS.classes}/${sourcedId}`, "PUT", { class: data });
56722
+ const res = await client["request"](`${ONEROSTER_ENDPOINTS2.classes}/${sourcedId}`, "PUT", { class: data });
56040
56723
  return res.class;
56041
56724
  },
56042
56725
  listByCourse: async (courseSourcedId) => {
56043
- const res = await client["request"](`${ONEROSTER_ENDPOINTS.courses}/${courseSourcedId}/classes`, "GET");
56726
+ const res = await client["request"](`${ONEROSTER_ENDPOINTS2.courses}/${courseSourcedId}/classes`, "GET");
56044
56727
  return res.classes;
56045
56728
  },
56046
56729
  listByStudent: async (userSourcedId, options) => {
@@ -56051,7 +56734,7 @@ function createOneRosterNamespace(client) {
56051
56734
  if (options?.offset) {
56052
56735
  queryParams.set("offset", String(options.offset));
56053
56736
  }
56054
- const endpoint = `${ONEROSTER_ENDPOINTS.users}/${userSourcedId}/classes`;
56737
+ const endpoint = `${ONEROSTER_ENDPOINTS2.users}/${userSourcedId}/classes`;
56055
56738
  const url2 = queryParams.toString() ? `${endpoint}?${queryParams}` : endpoint;
56056
56739
  const res = await client["request"](url2, "GET");
56057
56740
  return res.classes || [];
@@ -56059,7 +56742,7 @@ function createOneRosterNamespace(client) {
56059
56742
  },
56060
56743
  enrollments: {
56061
56744
  get: async (sourcedId) => {
56062
- const response = await client["request"](`${ONEROSTER_ENDPOINTS.enrollments}/${sourcedId}`, "GET");
56745
+ const response = await client["request"](`${ONEROSTER_ENDPOINTS2.enrollments}/${sourcedId}`, "GET");
56063
56746
  return response.enrollment;
56064
56747
  },
56065
56748
  listByClass: async (classSourcedId, options) => {
@@ -56075,7 +56758,7 @@ function createOneRosterNamespace(client) {
56075
56758
  if (options?.offset) {
56076
56759
  queryParams.set("offset", String(options.offset));
56077
56760
  }
56078
- const url2 = `${ONEROSTER_ENDPOINTS.enrollments}?${queryParams}`;
56761
+ const url2 = `${ONEROSTER_ENDPOINTS2.enrollments}?${queryParams}`;
56079
56762
  try {
56080
56763
  const response = await client["request"](url2, "GET");
56081
56764
  return response.enrollments || [];
@@ -56108,7 +56791,7 @@ function createOneRosterNamespace(client) {
56108
56791
  }
56109
56792
  queryParams.set("filter", filters.join(" AND "));
56110
56793
  queryParams.set("limit", "3000");
56111
- const url2 = `${ONEROSTER_ENDPOINTS.enrollments}?${queryParams}`;
56794
+ const url2 = `${ONEROSTER_ENDPOINTS2.enrollments}?${queryParams}`;
56112
56795
  const response = await client["request"](url2, "GET");
56113
56796
  return response.enrollments || [];
56114
56797
  }));
@@ -56138,72 +56821,72 @@ function createOneRosterNamespace(client) {
56138
56821
  throw error;
56139
56822
  }
56140
56823
  },
56141
- create: async (data) => client["request"](ONEROSTER_ENDPOINTS.enrollments, "POST", { enrollment: data }),
56824
+ create: async (data) => client["request"](ONEROSTER_ENDPOINTS2.enrollments, "POST", { enrollment: data }),
56142
56825
  update: async (sourcedId, data) => {
56143
- await client["request"](`${ONEROSTER_ENDPOINTS.enrollments}/${sourcedId}`, "PUT", {
56826
+ await client["request"](`${ONEROSTER_ENDPOINTS2.enrollments}/${sourcedId}`, "PUT", {
56144
56827
  enrollment: data
56145
56828
  });
56146
56829
  },
56147
56830
  delete: async (sourcedId) => {
56148
- await client["request"](`${ONEROSTER_ENDPOINTS.enrollments}/${sourcedId}`, "DELETE");
56831
+ await client["request"](`${ONEROSTER_ENDPOINTS2.enrollments}/${sourcedId}`, "DELETE");
56149
56832
  }
56150
56833
  },
56151
56834
  organizations: {
56152
- create: async (data) => client["request"](ONEROSTER_ENDPOINTS.organizations, "POST", data),
56153
- get: async (sourcedId) => client["request"](`${ONEROSTER_ENDPOINTS.organizations}/${sourcedId}`, "GET").then((res) => res.org),
56154
- update: async (sourcedId, data) => client["request"](`${ONEROSTER_ENDPOINTS.organizations}/${sourcedId}`, "PUT", data)
56835
+ create: async (data) => client["request"](ONEROSTER_ENDPOINTS2.organizations, "POST", data),
56836
+ get: async (sourcedId) => client["request"](`${ONEROSTER_ENDPOINTS2.organizations}/${sourcedId}`, "GET").then((res) => res.org),
56837
+ update: async (sourcedId, data) => client["request"](`${ONEROSTER_ENDPOINTS2.organizations}/${sourcedId}`, "PUT", data)
56155
56838
  },
56156
56839
  courses: {
56157
- create: async (data) => client["request"](ONEROSTER_ENDPOINTS.courses, "POST", data),
56840
+ create: async (data) => client["request"](ONEROSTER_ENDPOINTS2.courses, "POST", data),
56158
56841
  get: async (sourcedId) => {
56159
- const response = await client["request"](`${ONEROSTER_ENDPOINTS.courses}/${sourcedId}`, "GET");
56842
+ const response = await client["request"](`${ONEROSTER_ENDPOINTS2.courses}/${sourcedId}`, "GET");
56160
56843
  return response.course;
56161
56844
  },
56162
56845
  update: async (sourcedId, data) => {
56163
- await client["request"](`${ONEROSTER_ENDPOINTS.courses}/${sourcedId}`, "PUT", {
56846
+ await client["request"](`${ONEROSTER_ENDPOINTS2.courses}/${sourcedId}`, "PUT", {
56164
56847
  course: data
56165
56848
  });
56166
56849
  }
56167
56850
  },
56168
56851
  courseComponents: {
56169
- create: async (data) => client["request"](ONEROSTER_ENDPOINTS.courseComponents, "POST", data),
56852
+ create: async (data) => client["request"](ONEROSTER_ENDPOINTS2.courseComponents, "POST", data),
56170
56853
  get: async (sourcedId) => {
56171
- const response = await client["request"](`${ONEROSTER_ENDPOINTS.courseComponents}/${sourcedId}`, "GET");
56854
+ const response = await client["request"](`${ONEROSTER_ENDPOINTS2.courseComponents}/${sourcedId}`, "GET");
56172
56855
  return response.courseComponent;
56173
56856
  },
56174
56857
  update: async (sourcedId, data) => {
56175
- await client["request"](`${ONEROSTER_ENDPOINTS.courseComponents}/${sourcedId}`, "PUT", { courseComponent: data });
56858
+ await client["request"](`${ONEROSTER_ENDPOINTS2.courseComponents}/${sourcedId}`, "PUT", { courseComponent: data });
56176
56859
  }
56177
56860
  },
56178
56861
  resources: {
56179
- create: async (data) => client["request"](ONEROSTER_ENDPOINTS.resources, "POST", data),
56862
+ create: async (data) => client["request"](ONEROSTER_ENDPOINTS2.resources, "POST", data),
56180
56863
  get: async (sourcedId) => {
56181
- const response = await client["request"](`${ONEROSTER_ENDPOINTS.resources}/${sourcedId}`, "GET");
56864
+ const response = await client["request"](`${ONEROSTER_ENDPOINTS2.resources}/${sourcedId}`, "GET");
56182
56865
  return response.resource;
56183
56866
  },
56184
56867
  update: async (sourcedId, data) => {
56185
- await client["request"](`${ONEROSTER_ENDPOINTS.resources}/${sourcedId}`, "PUT", {
56868
+ await client["request"](`${ONEROSTER_ENDPOINTS2.resources}/${sourcedId}`, "PUT", {
56186
56869
  resource: data
56187
56870
  });
56188
56871
  },
56189
56872
  delete: async (sourcedId) => {
56190
- await client["request"](`${ONEROSTER_ENDPOINTS.resources}/${sourcedId}`, "DELETE");
56873
+ await client["request"](`${ONEROSTER_ENDPOINTS2.resources}/${sourcedId}`, "DELETE");
56191
56874
  }
56192
56875
  },
56193
56876
  componentResources: {
56194
- create: async (data) => client["request"](ONEROSTER_ENDPOINTS.componentResources, "POST", data),
56877
+ create: async (data) => client["request"](ONEROSTER_ENDPOINTS2.componentResources, "POST", data),
56195
56878
  get: async (sourcedId) => {
56196
- const response = await client["request"](`${ONEROSTER_ENDPOINTS.componentResources}/${sourcedId}`, "GET");
56879
+ const response = await client["request"](`${ONEROSTER_ENDPOINTS2.componentResources}/${sourcedId}`, "GET");
56197
56880
  return response.componentResource;
56198
56881
  },
56199
56882
  update: async (sourcedId, data) => {
56200
- await client["request"](`${ONEROSTER_ENDPOINTS.componentResources}/${sourcedId}`, "PUT", { componentResource: data });
56883
+ await client["request"](`${ONEROSTER_ENDPOINTS2.componentResources}/${sourcedId}`, "PUT", { componentResource: data });
56201
56884
  }
56202
56885
  },
56203
56886
  assessmentLineItems: {
56204
56887
  list: async (options) => {
56205
56888
  try {
56206
- return await listOneRosterCollection(client, ONEROSTER_ENDPOINTS.assessmentLineItems, "assessmentLineItems", options);
56889
+ return await listOneRosterCollection(client, ONEROSTER_ENDPOINTS2.assessmentLineItems, "assessmentLineItems", options);
56207
56890
  } catch (error) {
56208
56891
  logTimebackError("list assessment line items", error, {
56209
56892
  filter: options?.filter
@@ -56211,14 +56894,14 @@ function createOneRosterNamespace(client) {
56211
56894
  return [];
56212
56895
  }
56213
56896
  },
56214
- create: async (data) => client["request"](ONEROSTER_ENDPOINTS.assessmentLineItems, "POST", { assessmentLineItem: data }),
56897
+ create: async (data) => client["request"](ONEROSTER_ENDPOINTS2.assessmentLineItems, "POST", { assessmentLineItem: data }),
56215
56898
  get: async (sourcedId) => {
56216
- const response = await client["request"](`${ONEROSTER_ENDPOINTS.assessmentLineItems}/${sourcedId}`, "GET");
56899
+ const response = await client["request"](`${ONEROSTER_ENDPOINTS2.assessmentLineItems}/${sourcedId}`, "GET");
56217
56900
  return response.assessmentLineItem;
56218
56901
  },
56219
56902
  findOrCreate: async (sourcedId, data) => {
56220
56903
  try {
56221
- const response = await client["request"](`${ONEROSTER_ENDPOINTS.assessmentLineItems}/${sourcedId}`, "GET");
56904
+ const response = await client["request"](`${ONEROSTER_ENDPOINTS2.assessmentLineItems}/${sourcedId}`, "GET");
56222
56905
  return response.assessmentLineItem;
56223
56906
  } catch {
56224
56907
  const createData = {
@@ -56227,7 +56910,7 @@ function createOneRosterNamespace(client) {
56227
56910
  dateLastModified: new Date().toISOString()
56228
56911
  };
56229
56912
  try {
56230
- const response = await client["request"](ONEROSTER_ENDPOINTS.assessmentLineItems, "POST", { assessmentLineItem: createData });
56913
+ const response = await client["request"](ONEROSTER_ENDPOINTS2.assessmentLineItems, "POST", { assessmentLineItem: createData });
56231
56914
  if (!response.sourcedIdPairs?.allocatedSourcedId) {
56232
56915
  throw new Error("Invalid response from OneRoster API - missing allocatedSourcedId");
56233
56916
  }
@@ -56242,7 +56925,7 @@ function createOneRosterNamespace(client) {
56242
56925
  assessmentResults: {
56243
56926
  list: async (options) => {
56244
56927
  try {
56245
- return await listOneRosterCollection(client, ONEROSTER_ENDPOINTS.assessmentResults, "assessmentResults", options);
56928
+ return await listOneRosterCollection(client, ONEROSTER_ENDPOINTS2.assessmentResults, "assessmentResults", options);
56246
56929
  } catch (error) {
56247
56930
  logTimebackError("list assessment results", error, {
56248
56931
  filter: options?.filter
@@ -56250,10 +56933,11 @@ function createOneRosterNamespace(client) {
56250
56933
  return [];
56251
56934
  }
56252
56935
  },
56253
- create: async (data) => client["request"](ONEROSTER_ENDPOINTS.assessmentResults, "POST", { assessmentResult: data }),
56936
+ listOrThrow: async (options) => await listOneRosterCollection(client, ONEROSTER_ENDPOINTS2.assessmentResults, "assessmentResults", options),
56937
+ create: async (data) => client["request"](ONEROSTER_ENDPOINTS2.assessmentResults, "POST", { assessmentResult: data }),
56254
56938
  listByStudent: async (studentSourcedId, options) => {
56255
56939
  try {
56256
- return await listOneRosterCollection(client, ONEROSTER_ENDPOINTS.assessmentResults, "assessmentResults", {
56940
+ return await listOneRosterCollection(client, ONEROSTER_ENDPOINTS2.assessmentResults, "assessmentResults", {
56257
56941
  limit: options?.limit,
56258
56942
  offset: options?.offset,
56259
56943
  fields: options?.fields,
@@ -56272,54 +56956,22 @@ function createOneRosterNamespace(client) {
56272
56956
  sourcedId,
56273
56957
  dateLastModified: new Date().toISOString()
56274
56958
  };
56275
- return client["request"](`${ONEROSTER_ENDPOINTS.assessmentResults}/${sourcedId}`, "PUT", { assessmentResult });
56959
+ return client["request"](`${ONEROSTER_ENDPOINTS2.assessmentResults}/${sourcedId}`, "PUT", { assessmentResult });
56276
56960
  },
56277
56961
  get: async (sourcedId) => {
56278
- const response = await client["request"](`${ONEROSTER_ENDPOINTS.assessmentResults}/${sourcedId}`, "GET");
56962
+ const response = await client["request"](`${ONEROSTER_ENDPOINTS2.assessmentResults}/${sourcedId}`, "GET");
56279
56963
  return response.assessmentResult;
56280
56964
  },
56281
- update: async (sourcedId, data) => client["request"](`${ONEROSTER_ENDPOINTS.assessmentResults}/${sourcedId}`, "PUT", data),
56282
- getAttemptStats: async (studentId, lineItemId) => {
56283
- try {
56284
- const filter = `student.sourcedId='${studentId}' AND assessmentLineItem.sourcedId='${lineItemId}'`;
56285
- const url2 = `${ONEROSTER_ENDPOINTS.assessmentResults}?filter=${encodeURIComponent(filter)}`;
56286
- const response = await client["request"](url2, "GET");
56287
- const results = response.assessmentResults || [];
56288
- const firstResult = results[0];
56289
- if (!firstResult) {
56290
- return null;
56291
- }
56292
- let maxAttemptResult = firstResult;
56293
- let maxAttemptNumber = maxAttemptResult.metadata?.attemptNumber || 0;
56294
- let activeAttemptCount = 0;
56295
- for (const result of results) {
56296
- const attemptNumber = result.metadata?.attemptNumber || 0;
56297
- if (attemptNumber > maxAttemptNumber) {
56298
- maxAttemptNumber = attemptNumber;
56299
- maxAttemptResult = result;
56300
- }
56301
- if (result.status === "active") {
56302
- activeAttemptCount++;
56303
- }
56304
- }
56305
- return { maxAttemptNumber, activeAttemptCount, maxAttemptResult };
56306
- } catch (error) {
56307
- logTimebackError("query attempt stats", error, {
56308
- studentId,
56309
- lineItemId
56310
- });
56311
- return null;
56312
- }
56313
- }
56965
+ update: async (sourcedId, data) => client["request"](`${ONEROSTER_ENDPOINTS2.assessmentResults}/${sourcedId}`, "PUT", data)
56314
56966
  },
56315
56967
  users: {
56316
- create: async (data) => client["request"](ONEROSTER_ENDPOINTS.users, "POST", {
56968
+ create: async (data) => client["request"](ONEROSTER_ENDPOINTS2.users, "POST", {
56317
56969
  user: data
56318
56970
  }),
56319
- get: async (sourcedId) => client["request"](`${ONEROSTER_ENDPOINTS.users}/${sourcedId}`, "GET").then((res) => res.user),
56971
+ get: async (sourcedId) => client["request"](`${ONEROSTER_ENDPOINTS2.users}/${sourcedId}`, "GET").then((res) => res.user),
56320
56972
  findByEmail: async (email) => {
56321
56973
  const params = new URLSearchParams({ filter: `email='${email}'` });
56322
- const response = await client["request"](`${ONEROSTER_ENDPOINTS.users}?${params}`, "GET");
56974
+ const response = await client["request"](`${ONEROSTER_ENDPOINTS2.users}?${params}`, "GET");
56323
56975
  if (!response || !response.users || !Array.isArray(response.users)) {
56324
56976
  throw new Error(`Invalid response format from OneRoster API when searching for user with email: ${email}. Expected { users: [...] } but received: ${JSON.stringify(response)}`);
56325
56977
  }
@@ -56338,7 +56990,7 @@ function createOneRosterNamespace(client) {
56338
56990
  const results = await Promise.all(batches.map(async (batch) => {
56339
56991
  const filter = batch.map((id) => `sourcedId='${escapeFilterValue(id)}'`).join(" OR ");
56340
56992
  const params = new URLSearchParams({ filter });
56341
- const response = await client["request"](`${ONEROSTER_ENDPOINTS.users}?${params}`, "GET");
56993
+ const response = await client["request"](`${ONEROSTER_ENDPOINTS2.users}?${params}`, "GET");
56342
56994
  return response.users || [];
56343
56995
  }));
56344
56996
  return results.flat();
@@ -56355,11 +57007,11 @@ function createOneRosterNamespace(client) {
56355
57007
  function createQtiNamespace(client) {
56356
57008
  return {
56357
57009
  items: {
56358
- create: async (data) => client["requestQti"](QTI_ENDPOINTS.assessmentItems, "POST", data),
56359
- get: async (identifier) => client["requestQti"](`${QTI_ENDPOINTS.assessmentItems}/${identifier}`, "GET"),
56360
- update: async (identifier, data) => client["requestQti"](`${QTI_ENDPOINTS.assessmentItems}/${identifier}`, "PUT", data),
57010
+ create: async (data) => client["requestQti"](QTI_ENDPOINTS2.assessmentItems, "POST", data),
57011
+ get: async (identifier) => client["requestQti"](`${QTI_ENDPOINTS2.assessmentItems}/${identifier}`, "GET"),
57012
+ update: async (identifier, data) => client["requestQti"](`${QTI_ENDPOINTS2.assessmentItems}/${identifier}`, "PUT", data),
56361
57013
  delete: async (identifier) => {
56362
- await client["requestQti"](`${QTI_ENDPOINTS.assessmentItems}/${identifier}`, "DELETE");
57014
+ await client["requestQti"](`${QTI_ENDPOINTS2.assessmentItems}/${identifier}`, "DELETE");
56363
57015
  }
56364
57016
  },
56365
57017
  tests: {
@@ -56381,25 +57033,25 @@ function createQtiNamespace(client) {
56381
57033
  params.set("order", options.order);
56382
57034
  }
56383
57035
  const query = params.toString();
56384
- return client["requestQti"](`${QTI_ENDPOINTS.assessmentTests}${query ? `?${query}` : ""}`, "GET");
57036
+ return client["requestQti"](`${QTI_ENDPOINTS2.assessmentTests}${query ? `?${query}` : ""}`, "GET");
56385
57037
  },
56386
- create: async (data) => client["requestQti"](QTI_ENDPOINTS.assessmentTests, "POST", data),
56387
- get: async (identifier) => client["requestQti"](`${QTI_ENDPOINTS.assessmentTests}/${identifier}`, "GET"),
56388
- getQuestions: async (identifier) => client["requestQti"](`${QTI_ENDPOINTS.assessmentTests}/${identifier}/questions`, "GET"),
56389
- update: async (identifier, data) => client["requestQti"](`${QTI_ENDPOINTS.assessmentTests}/${identifier}`, "PUT", data),
57038
+ create: async (data) => client["requestQti"](QTI_ENDPOINTS2.assessmentTests, "POST", data),
57039
+ get: async (identifier) => client["requestQti"](`${QTI_ENDPOINTS2.assessmentTests}/${identifier}`, "GET"),
57040
+ getQuestions: async (identifier) => client["requestQti"](`${QTI_ENDPOINTS2.assessmentTests}/${identifier}/questions`, "GET"),
57041
+ update: async (identifier, data) => client["requestQti"](`${QTI_ENDPOINTS2.assessmentTests}/${identifier}`, "PUT", data),
56390
57042
  delete: async (identifier) => {
56391
- await client["requestQti"](`${QTI_ENDPOINTS.assessmentTests}/${identifier}`, "DELETE");
57043
+ await client["requestQti"](`${QTI_ENDPOINTS2.assessmentTests}/${identifier}`, "DELETE");
56392
57044
  },
56393
- addItem: async (testId, partId, sectionId, itemIdentifier) => client["requestQti"](`${QTI_ENDPOINTS.assessmentTests}/${testId}/test-parts/${partId}/sections/${sectionId}/items`, "POST", { identifier: itemIdentifier }),
57045
+ addItem: async (testId, partId, sectionId, itemIdentifier) => client["requestQti"](`${QTI_ENDPOINTS2.assessmentTests}/${testId}/test-parts/${partId}/sections/${sectionId}/items`, "POST", { identifier: itemIdentifier }),
56394
57046
  removeItem: async (testId, partId, sectionId, itemIdentifier) => {
56395
- await client["requestQti"](`${QTI_ENDPOINTS.assessmentTests}/${testId}/test-parts/${partId}/sections/${sectionId}/items/${itemIdentifier}`, "DELETE");
57047
+ await client["requestQti"](`${QTI_ENDPOINTS2.assessmentTests}/${testId}/test-parts/${partId}/sections/${sectionId}/items/${itemIdentifier}`, "DELETE");
56396
57048
  },
56397
- reorderItems: async (testId, partId, sectionId, items) => client["requestQti"](`${QTI_ENDPOINTS.assessmentTests}/${testId}/test-parts/${partId}/sections/${sectionId}/items/order`, "PUT", { items })
57049
+ reorderItems: async (testId, partId, sectionId, items) => client["requestQti"](`${QTI_ENDPOINTS2.assessmentTests}/${testId}/test-parts/${partId}/sections/${sectionId}/items/order`, "PUT", { items })
56398
57050
  }
56399
57051
  };
56400
57052
  }
56401
57053
  function toCaliperSubject(subject) {
56402
- return isTimebackSubject2(subject) ? subject : "None";
57054
+ return isTimebackSubject3(subject) ? subject : "None";
56403
57055
  }
56404
57056
  function buildAdminEventMetadata({
56405
57057
  reason,
@@ -56463,7 +57115,7 @@ class AdminEventRecorder {
56463
57115
  defaultActivityId: "playcademy-admin-manual-xp",
56464
57116
  eventKind: "remediation-xp"
56465
57117
  });
56466
- const courseUrl = createOneRosterUrls(TIMEBACK_API_URLS[this.environment]).course(data.courseId);
57118
+ const courseUrl = createOneRosterUrls(TIMEBACK_API_URLS2[this.environment]).course(data.courseId);
56467
57119
  await this.caliper.emitActivityEvent({
56468
57120
  studentId: ctx.student.id,
56469
57121
  studentEmail: ctx.student.email,
@@ -56513,7 +57165,7 @@ class AdminEventRecorder {
56513
57165
  defaultActivityId: "playcademy-admin-mastery-adjustment",
56514
57166
  eventKind: "remediation-mastery"
56515
57167
  });
56516
- const courseUrl = createOneRosterUrls(TIMEBACK_API_URLS[this.environment]).course(data.courseId);
57168
+ const courseUrl = createOneRosterUrls(TIMEBACK_API_URLS2[this.environment]).course(data.courseId);
56517
57169
  await this.caliper.emitActivityEvent({
56518
57170
  studentId: ctx.student.id,
56519
57171
  studentEmail: ctx.student.email,
@@ -56547,9 +57199,9 @@ class TimebackCache {
56547
57199
  maxSize;
56548
57200
  name;
56549
57201
  constructor(options = {}) {
56550
- this.defaultTTL = options.defaultTTL || CACHE_DEFAULTS.defaultTTL;
56551
- this.maxSize = options.maxSize || CACHE_DEFAULTS.defaultMaxSize;
56552
- this.name = options.name || CACHE_DEFAULTS.defaultName;
57202
+ this.defaultTTL = options.defaultTTL || CACHE_DEFAULTS2.defaultTTL;
57203
+ this.maxSize = options.maxSize || CACHE_DEFAULTS2.defaultMaxSize;
57204
+ this.name = options.name || CACHE_DEFAULTS2.defaultName;
56553
57205
  }
56554
57206
  get(key) {
56555
57207
  const entry = this.cache.get(key);
@@ -56640,23 +57292,23 @@ class TimebackCacheManager {
56640
57292
  enrollmentCache;
56641
57293
  constructor() {
56642
57294
  this.studentCache = new TimebackCache({
56643
- defaultTTL: CACHE_DEFAULTS.studentTTL,
56644
- maxSize: CACHE_DEFAULTS.studentMaxSize,
57295
+ defaultTTL: CACHE_DEFAULTS2.studentTTL,
57296
+ maxSize: CACHE_DEFAULTS2.studentMaxSize,
56645
57297
  name: "StudentCache"
56646
57298
  });
56647
57299
  this.assessmentLineItemCache = new TimebackCache({
56648
- defaultTTL: CACHE_DEFAULTS.assessmentTTL,
56649
- maxSize: CACHE_DEFAULTS.assessmentMaxSize,
57300
+ defaultTTL: CACHE_DEFAULTS2.assessmentTTL,
57301
+ maxSize: CACHE_DEFAULTS2.assessmentMaxSize,
56650
57302
  name: "AssessmentLineItemCache"
56651
57303
  });
56652
57304
  this.resourceMasteryCache = new TimebackCache({
56653
- defaultTTL: CACHE_DEFAULTS.assessmentTTL,
56654
- maxSize: CACHE_DEFAULTS.assessmentMaxSize,
57305
+ defaultTTL: CACHE_DEFAULTS2.assessmentTTL,
57306
+ maxSize: CACHE_DEFAULTS2.assessmentMaxSize,
56655
57307
  name: "ResourceMasteryCache"
56656
57308
  });
56657
57309
  this.enrollmentCache = new TimebackCache({
56658
- defaultTTL: CACHE_DEFAULTS.enrollmentTTL,
56659
- maxSize: CACHE_DEFAULTS.enrollmentMaxSize,
57310
+ defaultTTL: CACHE_DEFAULTS2.enrollmentTTL,
57311
+ maxSize: CACHE_DEFAULTS2.enrollmentMaxSize,
56660
57312
  name: "EnrollmentCache"
56661
57313
  });
56662
57314
  }
@@ -56912,18 +57564,18 @@ class MasteryTracker {
56912
57564
  await this.onerosterNamespace.assessmentLineItems.findOrCreate(lineItemId, {
56913
57565
  sourcedId: lineItemId,
56914
57566
  title: "Mastery Completion",
56915
- status: ONEROSTER_STATUS.active,
57567
+ status: ONEROSTER_STATUS2.active,
56916
57568
  ...classId ? { class: { sourcedId: classId } } : { course: { sourcedId: ids.course } },
56917
57569
  ...ids.componentResource ? { componentResource: { sourcedId: ids.componentResource } } : {}
56918
57570
  });
56919
57571
  await this.onerosterNamespace.assessmentResults.upsert(resultId, {
56920
57572
  sourcedId: resultId,
56921
- status: ONEROSTER_STATUS.active,
57573
+ status: ONEROSTER_STATUS2.active,
56922
57574
  assessmentLineItem: { sourcedId: lineItemId },
56923
57575
  student: { sourcedId: studentId },
56924
57576
  score: 100,
56925
57577
  scoreDate: new Date().toISOString(),
56926
- scoreStatus: SCORE_STATUS.fullyGraded,
57578
+ scoreStatus: SCORE_STATUS2.fullyGraded,
56927
57579
  inProgress: "false",
56928
57580
  metadata: {
56929
57581
  isMasteryCompletion: true,
@@ -56935,6 +57587,7 @@ class MasteryTracker {
56935
57587
  "app.timeback.line_item_id": lineItemId,
56936
57588
  "app.timeback.result_id": resultId
56937
57589
  });
57590
+ return true;
56938
57591
  } catch (error) {
56939
57592
  addEvent("timeback.mastery_completion_failed", {
56940
57593
  "app.timeback.student_id": studentId,
@@ -56942,6 +57595,7 @@ class MasteryTracker {
56942
57595
  "exception.type": errorType(error),
56943
57596
  "app.error.message": errorMessage(error)
56944
57597
  });
57598
+ return false;
56945
57599
  }
56946
57600
  }
56947
57601
  async revokeCompletionEntry(studentId, courseId, classId, appName) {
@@ -56952,18 +57606,18 @@ class MasteryTracker {
56952
57606
  await this.onerosterNamespace.assessmentLineItems.findOrCreate(lineItemId, {
56953
57607
  sourcedId: lineItemId,
56954
57608
  title: "Mastery Completion",
56955
- status: ONEROSTER_STATUS.active,
57609
+ status: ONEROSTER_STATUS2.active,
56956
57610
  ...classId ? { class: { sourcedId: classId } } : { course: { sourcedId: ids.course } },
56957
57611
  ...ids.componentResource ? { componentResource: { sourcedId: ids.componentResource } } : {}
56958
57612
  });
56959
57613
  await this.onerosterNamespace.assessmentResults.upsert(resultId, {
56960
57614
  sourcedId: resultId,
56961
- status: ONEROSTER_STATUS.active,
57615
+ status: ONEROSTER_STATUS2.active,
56962
57616
  assessmentLineItem: { sourcedId: lineItemId },
56963
57617
  student: { sourcedId: studentId },
56964
57618
  score: 0,
56965
57619
  scoreDate: new Date().toISOString(),
56966
- scoreStatus: SCORE_STATUS.notSubmitted,
57620
+ scoreStatus: SCORE_STATUS2.notSubmitted,
56967
57621
  inProgress: "true",
56968
57622
  metadata: {
56969
57623
  isMasteryCompletion: true,
@@ -56998,7 +57652,7 @@ class MasteryTracker {
56998
57652
  if (!playcademyMetadata) {
56999
57653
  return;
57000
57654
  }
57001
- const masterableUnits = isPlaycademyResourceMetadata(playcademyMetadata) ? playcademyMetadata.mastery?.masterableUnits : undefined;
57655
+ const masterableUnits = isPlaycademyResourceMetadata2(playcademyMetadata) ? playcademyMetadata.mastery?.masterableUnits : undefined;
57002
57656
  this.cacheManager.setResourceMasterableUnits(resourceId, masterableUnits ?? null);
57003
57657
  return masterableUnits;
57004
57658
  } catch (error) {
@@ -57078,13 +57732,11 @@ function validateSessionData(sessionData) {
57078
57732
 
57079
57733
  class ProgressRecorder {
57080
57734
  studentResolver;
57081
- cacheManager;
57082
57735
  onerosterNamespace;
57083
57736
  caliperNamespace;
57084
57737
  masteryTracker;
57085
- constructor(studentResolver, cacheManager, onerosterNamespace, caliperNamespace, masteryTracker) {
57738
+ constructor(studentResolver, onerosterNamespace, caliperNamespace, masteryTracker) {
57086
57739
  this.studentResolver = studentResolver;
57087
- this.cacheManager = cacheManager;
57088
57740
  this.onerosterNamespace = onerosterNamespace;
57089
57741
  this.caliperNamespace = caliperNamespace;
57090
57742
  this.masteryTracker = masteryTracker;
@@ -57093,26 +57745,37 @@ class ProgressRecorder {
57093
57745
  validateProgressData(progressData);
57094
57746
  const { ids, activityId, activityName, courseName, student } = await this.resolveContext(courseId, studentIdentifier, progressData);
57095
57747
  const { id: studentId, email: studentEmail } = student;
57096
- const {
57097
- score,
57098
- totalQuestions,
57099
- correctQuestions,
57100
- xpEarned = 0,
57101
- attemptNumber
57102
- } = progressData;
57103
- let extensions = progressData.extensions;
57104
- const masteryProgress = await this.masteryTracker.checkProgress({
57105
- studentId,
57106
- courseId,
57107
- resourceId: ids.resource,
57108
- masteredUnits: progressData.masteredUnits ?? 0,
57109
- masteredUnitsAbsolute: progressData.masteredUnitsAbsolute
57748
+ const { totalQuestions, correctQuestions, xpEarned = 0, attemptNumber } = progressData;
57749
+ const activityUrl = this.caliperNamespace.buildActivityUrl({
57750
+ courseId: ids.course,
57751
+ activityId,
57752
+ sensorUrl: progressData.sensorUrl
57110
57753
  });
57754
+ const courseUrl = this.caliperNamespace.buildCourseUrl(ids.course);
57755
+ let caliperLineItemId;
57756
+ try {
57757
+ caliperLineItemId = computeCaliperLineItemId(activityUrl, ids.course, courseUrl);
57758
+ } catch (error) {
57759
+ setAttributes({ "app.timeback.course_id_not_url_safe": true });
57760
+ throw error;
57761
+ }
57762
+ const legacyLineItemId = `${ids.course}-${activityId}-assessment`;
57763
+ const [currentAttemptNumber, masteryProgress] = await Promise.all([
57764
+ this.resolveAttemptNumber(attemptNumber, studentId, caliperLineItemId, legacyLineItemId),
57765
+ this.masteryTracker.checkProgress({
57766
+ studentId,
57767
+ courseId,
57768
+ resourceId: ids.resource,
57769
+ masteredUnits: progressData.masteredUnits ?? 0,
57770
+ masteredUnitsAbsolute: progressData.masteredUnitsAbsolute
57771
+ })
57772
+ ]);
57773
+ setAttributes({ "app.timeback.attempt_number": currentAttemptNumber });
57774
+ let extensions = progressData.extensions;
57111
57775
  const effectiveMasteredUnits = masteryProgress ? masteryProgress.effectiveDelta : progressData.masteredUnits ?? 0;
57112
57776
  let pctCompleteApp;
57113
57777
  let masteryAchieved = false;
57114
- let scoreStatus = SCORE_STATUS.fullyGraded;
57115
- const inProgress = "false";
57778
+ let completionEntryWritten = false;
57116
57779
  const warnings = masteryProgress?.writeWarning ? [masteryProgress.writeWarning] : undefined;
57117
57780
  if (masteryProgress) {
57118
57781
  masteryAchieved = masteryProgress.masteryAchieved;
@@ -57121,32 +57784,12 @@ class ProgressRecorder {
57121
57784
  ...extensions,
57122
57785
  ...pctCompleteApp !== undefined ? { pctCompleteApp } : {}
57123
57786
  };
57124
- if (masteryAchieved) {
57125
- scoreStatus = SCORE_STATUS.fullyGraded;
57126
- }
57127
57787
  }
57128
- const actualLineItemId = await this.resolveAssessmentLineItem(activityId, activityName, progressData.classId, ids);
57129
- const currentAttemptNumber = await this.resolveAttemptNumber(attemptNumber, score, studentId, actualLineItemId);
57130
- if (score !== undefined) {
57131
- await this.createGradebookEntry({
57132
- lineItemId: actualLineItemId,
57133
- studentId,
57134
- attemptNumber: currentAttemptNumber,
57135
- score,
57136
- xp: xpEarned,
57137
- scoreStatus,
57138
- inProgress,
57139
- appName: progressData.appName,
57140
- totalQuestions,
57141
- correctQuestions,
57142
- masteredUnits: effectiveMasteredUnits || undefined,
57143
- pctCompleteApp
57144
- });
57145
- } else {
57146
- setAttribute("app.timeback.score_provided", false);
57788
+ if (masteryAchieved) {
57789
+ setAttributes({ "app.timeback.mastery_achieved": true });
57147
57790
  }
57148
57791
  if (masteryAchieved) {
57149
- await this.masteryTracker.createCompletionEntry(studentId, courseId, progressData.classId, progressData.appName);
57792
+ completionEntryWritten = await this.masteryTracker.createCompletionEntry(studentId, courseId, progressData.classId, progressData.appName);
57150
57793
  await this.emitCourseCompletionHistoryEvent({
57151
57794
  studentId,
57152
57795
  studentEmail,
@@ -57162,30 +57805,36 @@ class ProgressRecorder {
57162
57805
  if (masteryProgress?.masteryRevoked) {
57163
57806
  await this.masteryTracker.revokeCompletionEntry(studentId, courseId, progressData.classId, progressData.appName);
57164
57807
  }
57165
- await this.emitCaliperEvent({
57166
- studentId,
57167
- studentEmail,
57168
- gameId: progressData.gameId,
57169
- activityId,
57170
- activityName,
57171
- courseId: ids.course,
57172
- courseName,
57173
- totalQuestions,
57174
- correctQuestions,
57175
- xpEarned,
57176
- masteredUnits: effectiveMasteredUnits || undefined,
57177
- attemptNumber: currentAttemptNumber,
57178
- progressData,
57179
- extensions,
57180
- runId: progressData.runId
57181
- });
57808
+ try {
57809
+ await this.emitCaliperEvent({
57810
+ studentId,
57811
+ studentEmail,
57812
+ gameId: progressData.gameId,
57813
+ activityId,
57814
+ activityName,
57815
+ courseId: ids.course,
57816
+ courseName,
57817
+ totalQuestions,
57818
+ correctQuestions,
57819
+ xpEarned,
57820
+ masteredUnits: effectiveMasteredUnits || undefined,
57821
+ attemptNumber: currentAttemptNumber,
57822
+ objectId: activityUrl,
57823
+ progressData,
57824
+ extensions,
57825
+ runId: progressData.runId
57826
+ });
57827
+ } catch (error) {
57828
+ if (completionEntryWritten) {
57829
+ setAttributes({ "app.timeback.completion_orphaned": true });
57830
+ }
57831
+ throw error;
57832
+ }
57182
57833
  return {
57183
57834
  xpAwarded: xpEarned,
57184
57835
  attemptNumber: currentAttemptNumber,
57185
57836
  masteredUnitsApplied: effectiveMasteredUnits,
57186
57837
  pctCompleteApp,
57187
- scoreStatus,
57188
- inProgress,
57189
57838
  ...warnings ? { warnings } : {}
57190
57839
  };
57191
57840
  }
@@ -57197,89 +57846,48 @@ class ProgressRecorder {
57197
57846
  const student = await this.studentResolver.resolve(studentIdentifier, progressData.studentEmail);
57198
57847
  return { ids, activityId, activityName, courseName, student };
57199
57848
  }
57200
- async resolveAssessmentLineItem(activityId, activityName, classId, ids) {
57201
- const lineItemId = `${ids.course}-${activityId}-assessment`;
57202
- let actualLineItemId = this.cacheManager.getAssessmentLineItem(lineItemId);
57203
- if (!actualLineItemId) {
57204
- actualLineItemId = await this.getOrCreateLineItem(lineItemId, activityName, classId, ids);
57205
- this.cacheManager.setAssessmentLineItem(lineItemId, actualLineItemId);
57206
- }
57207
- return actualLineItemId;
57208
- }
57209
- async resolveAttemptNumber(providedAttemptNumber, score, studentId, lineItemId) {
57849
+ async resolveAttemptNumber(providedAttemptNumber, studentId, caliperLineItemId, legacyLineItemId) {
57210
57850
  if (providedAttemptNumber) {
57851
+ setAttributes({ "app.timeback.attempt_source": "provided" });
57211
57852
  return providedAttemptNumber;
57212
57853
  }
57213
- if (score !== undefined) {
57214
- return this.determineAttemptNumber(studentId, lineItemId);
57215
- }
57216
- return 1;
57217
- }
57218
- async getOrCreateLineItem(lineItemId, activityName, classId, ids) {
57219
- try {
57220
- const lineItem = await this.onerosterNamespace.assessmentLineItems.findOrCreate(lineItemId, {
57221
- sourcedId: lineItemId,
57222
- title: activityName,
57223
- status: ONEROSTER_STATUS.active,
57224
- ...classId ? { class: { sourcedId: classId } } : { course: { sourcedId: ids.course } }
57854
+ const caliperAttempt = await this.getLatestActivityAttempt(studentId, caliperLineItemId, "attempt");
57855
+ if (caliperAttempt !== null) {
57856
+ const next = caliperAttempt + 1;
57857
+ setAttributes({
57858
+ "app.timeback.attempt_source": caliperAttempt > 0 ? "caliper" : "caliper_unreadable"
57225
57859
  });
57226
- if (!lineItem.sourcedId) {
57227
- throw new TimebackError(`Assessment line item created but has no sourcedId. This should not happen and indicates an upstream API issue.`);
57228
- }
57229
- return lineItem.sourcedId;
57230
- } catch (error) {
57231
- if (error instanceof TimebackApiError && error.status === 404) {
57232
- const errorDetails = error.details;
57233
- const description = errorDetails?.imsx_description || error.message;
57234
- if (description.includes("course") && description.includes("not found") || description.includes("component") && description.includes("not found") || description.includes("resource") && description.includes("not found") || description.includes("componentResource") && description.includes("not found")) {
57235
- throw new ResourceNotFoundError("TimeBack resources", "setup-not-run");
57236
- }
57860
+ if (caliperAttempt === 0) {
57861
+ setAttributes({
57862
+ "app.timeback.attempt_regressed": true,
57863
+ "app.timeback.attempt_emitted": next
57864
+ });
57237
57865
  }
57238
- throw error;
57866
+ return next;
57239
57867
  }
57240
- }
57241
- async determineAttemptNumber(studentId, lineItemId) {
57242
- const stats = await this.onerosterNamespace.assessmentResults.getAttemptStats(studentId, lineItemId);
57243
- if (stats) {
57244
- return stats.activeAttemptCount + 1;
57868
+ const legacyAttempt = await this.getLatestActivityAttempt(studentId, legacyLineItemId, "attemptNumber");
57869
+ if (legacyAttempt !== null) {
57870
+ setAttributes({
57871
+ "app.timeback.attempt_source": "legacy_seed",
57872
+ "app.timeback.legacy_seed_attempt": legacyAttempt
57873
+ });
57874
+ return legacyAttempt + 1;
57245
57875
  }
57876
+ setAttributes({ "app.timeback.attempt_source": "first" });
57246
57877
  return 1;
57247
57878
  }
57248
- async createGradebookEntry({
57249
- lineItemId,
57250
- studentId,
57251
- attemptNumber,
57252
- score,
57253
- xp,
57254
- scoreStatus,
57255
- inProgress,
57256
- appName,
57257
- totalQuestions,
57258
- correctQuestions,
57259
- masteredUnits,
57260
- pctCompleteApp
57261
- }) {
57262
- const timestamp3 = Date.now().toString(36);
57263
- const resultId = `${lineItemId}:${studentId}:${timestamp3}`;
57264
- await this.onerosterNamespace.assessmentResults.upsert(resultId, {
57265
- sourcedId: resultId,
57266
- status: ONEROSTER_STATUS.active,
57267
- assessmentLineItem: { sourcedId: lineItemId },
57268
- student: { sourcedId: studentId },
57269
- score,
57270
- scoreDate: new Date().toISOString(),
57271
- scoreStatus,
57272
- inProgress,
57273
- metadata: {
57274
- xp,
57275
- attemptNumber,
57276
- appName,
57277
- ...totalQuestions !== undefined ? { totalQuestions } : {},
57278
- ...correctQuestions !== undefined ? { correctQuestions } : {},
57279
- ...masteredUnits !== undefined ? { masteredUnits } : {},
57280
- ...pctCompleteApp !== undefined ? { pctCompleteApp } : {}
57281
- }
57879
+ async getLatestActivityAttempt(studentId, lineItemId, metadataField) {
57880
+ const results = await this.onerosterNamespace.assessmentResults.listOrThrow({
57881
+ filter: `student.sourcedId='${escapeFilterValue(studentId)}' AND assessmentLineItem.sourcedId='${escapeFilterValue(lineItemId)}' AND status='active'`,
57882
+ sort: "dateLastModified",
57883
+ orderBy: "desc",
57884
+ limit: 1
57282
57885
  });
57886
+ const latest = results[0];
57887
+ if (!latest) {
57888
+ return null;
57889
+ }
57890
+ return latest.metadata?.[metadataField] || 0;
57283
57891
  }
57284
57892
  async emitCaliperEvent({
57285
57893
  studentId,
@@ -57294,6 +57902,7 @@ class ProgressRecorder {
57294
57902
  xpEarned,
57295
57903
  masteredUnits,
57296
57904
  attemptNumber,
57905
+ objectId,
57297
57906
  progressData,
57298
57907
  extensions,
57299
57908
  runId
@@ -57311,12 +57920,18 @@ class ProgressRecorder {
57311
57920
  xpEarned,
57312
57921
  masteredUnits,
57313
57922
  attemptNumber,
57923
+ objectId,
57924
+ process: true,
57314
57925
  subject: progressData.subject,
57315
57926
  appName: progressData.appName,
57316
57927
  sensorUrl: progressData.sensorUrl,
57317
57928
  extensions: extensions || progressData.extensions,
57318
57929
  ...runId ? { runId } : {}
57319
- }).catch(catchEvent("timeback.caliper_event_failed"));
57930
+ }).catch((error) => {
57931
+ setAttributes({ "app.timeback.caliper_emit_failed": true });
57932
+ catchEvent("timeback.caliper_event_failed")(error);
57933
+ throw error;
57934
+ });
57320
57935
  }
57321
57936
  async emitCourseCompletionHistoryEvent(data) {
57322
57937
  await this.caliperNamespace.emitActivityEvent({
@@ -57494,15 +58109,15 @@ class TimebackClient {
57494
58109
  masteryTracker;
57495
58110
  constructor(config2) {
57496
58111
  this.baseUrl = TimebackClient.resolveBaseUrl(config2?.baseUrl);
57497
- this.environment = process.env[ENV_VARS.environment] === "staging" ? "staging" : "production";
58112
+ this.environment = process.env[ENV_VARS2.environment] === "staging" ? "staging" : "production";
57498
58113
  this.caliperUrl = TimebackClient.resolveCaliperUrl(config2?.caliperUrl, this.environment);
57499
58114
  this.authUrl = config2?.credentials?.authUrl;
57500
58115
  this.credentials = config2?.credentials;
57501
58116
  this.qtiCredentials = config2?.qtiCredentials;
57502
58117
  this.options = {
57503
- retries: config2?.options?.retries ?? HTTP_DEFAULTS.retries,
57504
- cacheDuration: config2?.options?.cacheDuration ?? AUTH_DEFAULTS.tokenCacheDuration,
57505
- timeout: config2?.options?.timeout ?? HTTP_DEFAULTS.timeout,
58118
+ retries: config2?.options?.retries ?? HTTP_DEFAULTS2.retries,
58119
+ cacheDuration: config2?.options?.cacheDuration ?? AUTH_DEFAULTS2.tokenCacheDuration,
58120
+ timeout: config2?.options?.timeout ?? HTTP_DEFAULTS2.timeout,
57506
58121
  sensorUrl: config2?.options?.sensorUrl
57507
58122
  };
57508
58123
  this.oneroster = createOneRosterNamespace(this);
@@ -57512,7 +58127,7 @@ class TimebackClient {
57512
58127
  this.cacheManager = new TimebackCacheManager;
57513
58128
  this.studentResolver = new StudentResolver(this.cacheManager, this.oneroster);
57514
58129
  this.masteryTracker = new MasteryTracker(this.cacheManager, this.oneroster, this.edubridge);
57515
- this.progressRecorder = new ProgressRecorder(this.studentResolver, this.cacheManager, this.oneroster, this.caliper, this.masteryTracker);
58130
+ this.progressRecorder = new ProgressRecorder(this.studentResolver, this.oneroster, this.caliper, this.masteryTracker);
57516
58131
  this.sessionRecorder = new SessionRecorder(this.studentResolver, this.caliper);
57517
58132
  this.adminEventRecorder = new AdminEventRecorder(this.studentResolver, this.oneroster, this.caliper, this.environment);
57518
58133
  if (this.credentials) {
@@ -57521,16 +58136,16 @@ class TimebackClient {
57521
58136
  }
57522
58137
  static async init(config2) {
57523
58138
  let credentials = config2?.credentials;
57524
- if (!credentials && process.env[ENV_VARS.clientId] && process.env[ENV_VARS.clientSecret]) {
58139
+ if (!credentials && process.env[ENV_VARS2.clientId] && process.env[ENV_VARS2.clientSecret]) {
57525
58140
  credentials = {
57526
- clientId: process.env[ENV_VARS.clientId],
57527
- clientSecret: process.env[ENV_VARS.clientSecret]
58141
+ clientId: process.env[ENV_VARS2.clientId],
58142
+ clientSecret: process.env[ENV_VARS2.clientSecret]
57528
58143
  };
57529
58144
  }
57530
- const qtiCredentials = config2?.qtiCredentials ?? (process.env[ENV_VARS.qtiClientId] && process.env[ENV_VARS.qtiClientSecret] && process.env[ENV_VARS.qtiAuthUrl] ? {
57531
- clientId: process.env[ENV_VARS.qtiClientId],
57532
- clientSecret: process.env[ENV_VARS.qtiClientSecret],
57533
- authUrl: process.env[ENV_VARS.qtiAuthUrl]
58145
+ const qtiCredentials = config2?.qtiCredentials ?? (process.env[ENV_VARS2.qtiClientId] && process.env[ENV_VARS2.qtiClientSecret] && process.env[ENV_VARS2.qtiAuthUrl] ? {
58146
+ clientId: process.env[ENV_VARS2.qtiClientId],
58147
+ clientSecret: process.env[ENV_VARS2.qtiClientSecret],
58148
+ authUrl: process.env[ENV_VARS2.qtiAuthUrl]
57534
58149
  } : undefined);
57535
58150
  return new TimebackClient({
57536
58151
  ...config2,
@@ -57543,15 +58158,15 @@ class TimebackClient {
57543
58158
  if (explicit) {
57544
58159
  return explicit;
57545
58160
  }
57546
- const envName = process.env[ENV_VARS.environment] === "staging" ? "staging" : "production";
57547
- return TIMEBACK_API_URLS[envName];
58161
+ const envName = process.env[ENV_VARS2.environment] === "staging" ? "staging" : "production";
58162
+ return TIMEBACK_API_URLS2[envName];
57548
58163
  }
57549
58164
  static resolveCaliperUrl(input, environment = "production") {
57550
58165
  const explicit = (input || "").trim();
57551
58166
  if (explicit) {
57552
58167
  return explicit;
57553
58168
  }
57554
- return CALIPER_API_URLS[environment];
58169
+ return CALIPER_API_URLS2[environment];
57555
58170
  }
57556
58171
  getBaseUrl() {
57557
58172
  return this.baseUrl;
@@ -57672,14 +58287,14 @@ class TimebackClient {
57672
58287
  await this._ensureAuthenticated();
57673
58288
  return this.token;
57674
58289
  }
57675
- throw new TimebackAuthenticationError(`QTI credentials are required. Set ${ENV_VARS.qtiClientId}, ${ENV_VARS.qtiClientSecret}, and ${ENV_VARS.qtiAuthUrl}.`);
58290
+ throw new TimebackAuthenticationError(`QTI credentials are required. Set ${ENV_VARS2.qtiClientId}, ${ENV_VARS2.qtiClientSecret}, and ${ENV_VARS2.qtiAuthUrl}.`);
57676
58291
  }
57677
58292
  async ensureQtiAuthenticated() {
57678
58293
  if (this.isQtiAuthenticated()) {
57679
58294
  return;
57680
58295
  }
57681
58296
  if (!this.qtiCredentials) {
57682
- throw new TimebackAuthenticationError(`QTI credentials are required. Set ${ENV_VARS.qtiClientId}, ${ENV_VARS.qtiClientSecret}, and ${ENV_VARS.qtiAuthUrl}.`);
58297
+ throw new TimebackAuthenticationError(`QTI credentials are required. Set ${ENV_VARS2.qtiClientId}, ${ENV_VARS2.qtiClientSecret}, and ${ENV_VARS2.qtiAuthUrl}.`);
57683
58298
  }
57684
58299
  try {
57685
58300
  const tokenData = await getTimebackTokenResponse({
@@ -57691,11 +58306,11 @@ class TimebackClient {
57691
58306
  this.setQtiToken(tokenData.access_token, tokenData.expires_in);
57692
58307
  } catch (error) {
57693
58308
  const errMsg = errorMessage(error);
57694
- throw new TimebackAuthenticationError(`QTI authentication failed: ${errMsg}. Verify that ${ENV_VARS.qtiClientId}, ${ENV_VARS.qtiClientSecret}, and ${ENV_VARS.qtiAuthUrl} are correct.`);
58309
+ throw new TimebackAuthenticationError(`QTI authentication failed: ${errMsg}. Verify that ${ENV_VARS2.qtiClientId}, ${ENV_VARS2.qtiClientSecret}, and ${ENV_VARS2.qtiAuthUrl} are correct.`);
57695
58310
  }
57696
58311
  }
57697
58312
  canUseCoreCredentialsForQti() {
57698
- return this.baseUrl.replace(/\/$/, "") === TIMEBACK_API_URLS.production;
58313
+ return this.baseUrl.replace(/\/$/, "") === TIMEBACK_API_URLS2.production;
57699
58314
  }
57700
58315
  async resolveStudent(studentIdentifier, providedEmail) {
57701
58316
  return this.studentResolver.resolve(studentIdentifier, providedEmail);
@@ -57716,8 +58331,8 @@ class TimebackClient {
57716
58331
  await this._ensureAuthenticated();
57717
58332
  const edubridgeEnrollments = await this.edubridge.enrollments.listByUser(studentId);
57718
58333
  const enrollments = edubridgeEnrollments.map((enrollment) => {
57719
- const grades = enrollment.course.grades ? enrollment.course.grades.map((g) => parseInt(g, 10)).filter(isTimebackGrade2) : null;
57720
- const subjects = enrollment.course.subjects ? enrollment.course.subjects.filter(isTimebackSubject2) : null;
58334
+ const grades = enrollment.course.grades ? enrollment.course.grades.map((g) => parseInt(g, 10)).filter(isTimebackGrade3) : null;
58335
+ const subjects = enrollment.course.subjects ? enrollment.course.subjects.filter(isTimebackSubject3) : null;
57721
58336
  return {
57722
58337
  sourcedId: enrollment.id,
57723
58338
  title: enrollment.course.title,
@@ -57783,9 +58398,9 @@ class TimebackClient {
57783
58398
  if (options?.include?.perCourse) {
57784
58399
  const gradeStr = enrollment.course.grades?.[0];
57785
58400
  const parsedGrade = gradeStr ? parseInt(gradeStr, 10) : 0;
57786
- const grade = isTimebackGrade2(parsedGrade) ? parsedGrade : 0;
58401
+ const grade = isTimebackGrade3(parsedGrade) ? parsedGrade : 0;
57787
58402
  const subjectStr = enrollment.course.subjects?.[0];
57788
- const subject = subjectStr && isTimebackSubject2(subjectStr) ? subjectStr : "None";
58403
+ const subject = subjectStr && isTimebackSubject3(subjectStr) ? subjectStr : "None";
57789
58404
  courses.push({
57790
58405
  grade,
57791
58406
  subject,
@@ -57859,9 +58474,9 @@ class TimebackClient {
57859
58474
  if (options?.include?.perCourse) {
57860
58475
  const gradeStr = enrollment.course.grades?.[0];
57861
58476
  const parsedGrade = gradeStr ? parseInt(gradeStr, 10) : 0;
57862
- const grade = isTimebackGrade2(parsedGrade) ? parsedGrade : 0;
58477
+ const grade = isTimebackGrade3(parsedGrade) ? parsedGrade : 0;
57863
58478
  const subjectStr = enrollment.course.subjects?.[0];
57864
- const subject = subjectStr && isTimebackSubject2(subjectStr) ? subjectStr : "None";
58479
+ const subject = subjectStr && isTimebackSubject3(subjectStr) ? subjectStr : "None";
57865
58480
  courses.push({
57866
58481
  grade,
57867
58482
  subject,
@@ -57917,6 +58532,12 @@ class TimebackClient {
57917
58532
  async cleanup(courseId) {
57918
58533
  return deleteTimebackResources(this, courseId);
57919
58534
  }
58535
+ async deactivateCourse(courseId) {
58536
+ return updateTimebackCourseStatus(this, courseId, "tobedeleted");
58537
+ }
58538
+ async reactivateCourse(courseId) {
58539
+ return updateTimebackCourseStatus(this, courseId, "active");
58540
+ }
57920
58541
  }
57921
58542
  var __defProp22;
57922
58543
  var __export2 = (target, all) => {
@@ -57928,46 +58549,50 @@ var __export2 = (target, all) => {
57928
58549
  set: (newValue) => all[name3] = () => newValue
57929
58550
  });
57930
58551
  };
57931
- var __esm2 = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
57932
- var TIMEBACK_API_URLS;
58552
+ var __esm3 = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
58553
+ var TIMEBACK_API_URLS2;
57933
58554
  var QTI_API_URL = "https://qti.alpha-1edtech.ai/api";
57934
- var TIMEBACK_AUTH_URLS;
57935
- var CALIPER_API_URLS;
57936
- var ONEROSTER_ENDPOINTS;
57937
- var QTI_ENDPOINTS;
57938
- var CALIPER_ENDPOINTS;
57939
- var CALIPER_CONSTANTS;
57940
- var TIMEBACK_EVENT_TYPES;
57941
- var TIMEBACK_ACTIONS;
57942
- var TIMEBACK_TYPES;
57943
- var ACTIVITY_METRIC_TYPES;
57944
- var TIME_METRIC_TYPES;
57945
- var TIMEBACK_SUBJECTS2;
57946
- var TIMEBACK_GRADE_LEVELS;
57947
- var TIMEBACK_GRADE_LEVEL_LABELS;
57948
- var CALIPER_SUBJECTS;
57949
- var ONEROSTER_STATUS;
57950
- var SCORE_STATUS;
57951
- var ENV_VARS;
57952
- var HTTP_DEFAULTS;
57953
- var AUTH_DEFAULTS;
57954
- var CACHE_DEFAULTS;
57955
- var CONFIG_DEFAULTS;
57956
- var PLAYCADEMY_DEFAULTS;
57957
- var RESOURCE_DEFAULTS;
57958
- var HTTP_STATUS;
57959
- var ERROR_NAMES;
57960
- var init_constants4;
57961
- var exports_verify;
57962
- var init_verify;
58555
+ var TIMEBACK_AUTH_URLS2;
58556
+ var CALIPER_API_URLS2;
58557
+ var ONEROSTER_ENDPOINTS2;
58558
+ var QTI_ENDPOINTS2;
58559
+ var CALIPER_ENDPOINTS2;
58560
+ var CALIPER_CONSTANTS2;
58561
+ var TIMEBACK_EVENT_TYPES2;
58562
+ var TIMEBACK_ACTIONS2;
58563
+ var TIMEBACK_TYPES2;
58564
+ var ACTIVITY_METRIC_TYPES2;
58565
+ var TIME_METRIC_TYPES2;
58566
+ var TIMEBACK_SUBJECTS3;
58567
+ var TIMEBACK_GRADE_LEVELS2;
58568
+ var TIMEBACK_GRADE_LEVEL_LABELS2;
58569
+ var CALIPER_SUBJECTS2;
58570
+ var ONEROSTER_STATUS2;
58571
+ var SCORE_STATUS2;
58572
+ var ENV_VARS2;
58573
+ var HTTP_DEFAULTS2;
58574
+ var AUTH_DEFAULTS2;
58575
+ var CACHE_DEFAULTS2;
58576
+ var CONFIG_DEFAULTS2;
58577
+ var PLAYCADEMY_DEFAULTS2;
58578
+ var RESOURCE_DEFAULTS2;
58579
+ var HTTP_STATUS2;
58580
+ var ERROR_NAMES2;
58581
+ var init_constants5;
57963
58582
  var TimebackError;
57964
58583
  var TimebackApiError;
57965
58584
  var TimebackAuthenticationError;
57966
58585
  var StudentNotFoundError;
57967
58586
  var ConfigurationError;
58587
+ var ResourceAlreadyExistsError;
57968
58588
  var ResourceNotFoundError;
57969
- var SUBJECT_VALUES;
57970
- var GRADE_VALUES;
58589
+ var init_errors4;
58590
+ var UUID_REGEX2;
58591
+ var init_ids;
58592
+ var exports_verify;
58593
+ var init_verify;
58594
+ var SUBJECT_VALUES2;
58595
+ var GRADE_VALUES2;
57971
58596
  var TimebackAuthError;
57972
58597
  var UUID_PATTERN;
57973
58598
  var storage;
@@ -58000,320 +58625,7 @@ var init_dist2 = __esm(() => {
58000
58625
  init_spans();
58001
58626
  init_esm();
58002
58627
  __defProp22 = Object.defineProperty;
58003
- init_constants4 = __esm2(() => {
58004
- TIMEBACK_API_URLS = {
58005
- production: "https://api.alpha-1edtech.ai",
58006
- staging: "https://api.staging.alpha-1edtech.com"
58007
- };
58008
- TIMEBACK_AUTH_URLS = {
58009
- production: "https://prod-beyond-timeback-api-2-idp.auth.us-east-1.amazoncognito.com",
58010
- staging: "https://alpha-auth-development-idp.auth.us-west-2.amazoncognito.com"
58011
- };
58012
- CALIPER_API_URLS = {
58013
- production: "https://caliper.alpha-1edtech.ai",
58014
- staging: "https://caliper-staging.alpha-1edtech.com"
58015
- };
58016
- ONEROSTER_ENDPOINTS = {
58017
- organizations: "/ims/oneroster/rostering/v1p2/orgs",
58018
- courses: "/ims/oneroster/rostering/v1p2/courses",
58019
- courseComponents: "/ims/oneroster/rostering/v1p2/courses/components",
58020
- resources: "/ims/oneroster/resources/v1p2/resources",
58021
- componentResources: "/ims/oneroster/rostering/v1p2/courses/component-resources",
58022
- classes: "/ims/oneroster/rostering/v1p2/classes",
58023
- enrollments: "/ims/oneroster/rostering/v1p2/enrollments",
58024
- assessmentLineItems: "/ims/oneroster/gradebook/v1p2/assessmentLineItems",
58025
- assessmentResults: "/ims/oneroster/gradebook/v1p2/assessmentResults",
58026
- users: "/ims/oneroster/rostering/v1p2/users"
58027
- };
58028
- QTI_ENDPOINTS = {
58029
- assessmentTests: "/assessment-tests",
58030
- assessmentItems: "/assessment-items"
58031
- };
58032
- CALIPER_ENDPOINTS = {
58033
- event: "/caliper/event",
58034
- events: "/caliper/events",
58035
- validate: "/caliper/event/validate"
58036
- };
58037
- CALIPER_CONSTANTS = {
58038
- context: "http://purl.imsglobal.org/ctx/caliper/v1p2",
58039
- profile: "TimebackProfile",
58040
- dataVersion: "http://purl.imsglobal.org/ctx/caliper/v1p2"
58041
- };
58042
- TIMEBACK_EVENT_TYPES = {
58043
- activityEvent: "ActivityEvent",
58044
- timeSpentEvent: "TimeSpentEvent"
58045
- };
58046
- TIMEBACK_ACTIONS = {
58047
- completed: "Completed",
58048
- spentTime: "SpentTime"
58049
- };
58050
- TIMEBACK_TYPES = {
58051
- user: "TimebackUser",
58052
- activityContext: "TimebackActivityContext",
58053
- activityMetricsCollection: "TimebackActivityMetricsCollection",
58054
- timeSpentMetricsCollection: "TimebackTimeSpentMetricsCollection"
58055
- };
58056
- ACTIVITY_METRIC_TYPES = {
58057
- totalQuestions: "totalQuestions",
58058
- correctQuestions: "correctQuestions",
58059
- xpEarned: "xpEarned",
58060
- masteredUnits: "masteredUnits"
58061
- };
58062
- TIME_METRIC_TYPES = {
58063
- active: "active",
58064
- inactive: "inactive",
58065
- waste: "waste",
58066
- unknown: "unknown",
58067
- antiPattern: "anti-pattern"
58068
- };
58069
- TIMEBACK_SUBJECTS2 = [
58070
- "Math",
58071
- "FastMath",
58072
- "Science",
58073
- "Social Studies",
58074
- "Language",
58075
- "Reading",
58076
- "Vocabulary",
58077
- "Writing"
58078
- ];
58079
- TIMEBACK_GRADE_LEVELS = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
58080
- TIMEBACK_GRADE_LEVEL_LABELS = {
58081
- "-1": "pre-k",
58082
- "0": "kindergarten",
58083
- "1": "1st grade",
58084
- "2": "2nd grade",
58085
- "3": "3rd grade",
58086
- "4": "4th grade",
58087
- "5": "5th grade",
58088
- "6": "6th grade",
58089
- "7": "7th grade",
58090
- "8": "8th grade",
58091
- "9": "9th grade",
58092
- "10": "10th grade",
58093
- "11": "11th grade",
58094
- "12": "12th grade",
58095
- "13": "AP"
58096
- };
58097
- CALIPER_SUBJECTS = {
58098
- Reading: "Reading",
58099
- Language: "Language",
58100
- Vocabulary: "Vocabulary",
58101
- SocialStudies: "Social Studies",
58102
- Writing: "Writing",
58103
- Science: "Science",
58104
- FastMath: "FastMath",
58105
- Math: "Math",
58106
- None: "None"
58107
- };
58108
- ONEROSTER_STATUS = {
58109
- active: "active",
58110
- toBeDeleted: "tobedeleted"
58111
- };
58112
- SCORE_STATUS = {
58113
- exempt: "exempt",
58114
- fullyGraded: "fully graded",
58115
- notSubmitted: "not submitted",
58116
- partiallyGraded: "partially graded",
58117
- submitted: "submitted"
58118
- };
58119
- ENV_VARS = {
58120
- clientId: "TIMEBACK_CLIENT_ID",
58121
- clientSecret: "TIMEBACK_CLIENT_SECRET",
58122
- baseUrl: "TIMEBACK_BASE_URL",
58123
- environment: "TIMEBACK_ENVIRONMENT",
58124
- vendorResourceId: "TIMEBACK_VENDOR_RESOURCE_ID",
58125
- launchBaseUrl: "GAME_URL",
58126
- qtiClientId: "QTI_CLIENT_ID",
58127
- qtiClientSecret: "QTI_CLIENT_SECRET",
58128
- qtiAuthUrl: "QTI_AUTH_URL"
58129
- };
58130
- HTTP_DEFAULTS = {
58131
- timeout: 30000,
58132
- retries: 3,
58133
- retryBackoffBase: 2
58134
- };
58135
- AUTH_DEFAULTS = {
58136
- tokenCacheDuration: 50000
58137
- };
58138
- CACHE_DEFAULTS = {
58139
- defaultTTL: 600000,
58140
- defaultMaxSize: 500,
58141
- defaultName: "TimebackCache",
58142
- studentTTL: 600000,
58143
- studentMaxSize: 500,
58144
- assessmentTTL: 1800000,
58145
- assessmentMaxSize: 200,
58146
- enrollmentTTL: 5000,
58147
- enrollmentMaxSize: 100
58148
- };
58149
- CONFIG_DEFAULTS = {
58150
- fileNames: ["timeback.config.js", "timeback.config.json"]
58151
- };
58152
- PLAYCADEMY_DEFAULTS = {
58153
- organization: TIMEBACK_ORG_SOURCED_ID,
58154
- launchBaseUrls: PLAYCADEMY_BASE_URLS
58155
- };
58156
- RESOURCE_DEFAULTS = {
58157
- organization: {
58158
- name: TIMEBACK_ORG_NAME,
58159
- type: TIMEBACK_ORG_TYPE
58160
- },
58161
- course: {
58162
- gradingScheme: TIMEBACK_COURSE_DEFAULTS.gradingScheme,
58163
- level: TIMEBACK_COURSE_DEFAULTS.level,
58164
- metadata: {
58165
- goals: TIMEBACK_COURSE_DEFAULTS.goals,
58166
- metrics: TIMEBACK_COURSE_DEFAULTS.metrics
58167
- }
58168
- },
58169
- component: TIMEBACK_COMPONENT_DEFAULTS,
58170
- resource: TIMEBACK_RESOURCE_DEFAULTS,
58171
- componentResource: TIMEBACK_COMPONENT_RESOURCE_DEFAULTS
58172
- };
58173
- HTTP_STATUS = {
58174
- CLIENT_ERROR_MIN: 400,
58175
- CLIENT_ERROR_MAX: 500,
58176
- SERVER_ERROR_MIN: 500
58177
- };
58178
- ERROR_NAMES = {
58179
- timebackAuth: "TimebackAuthError",
58180
- timebackApi: "TimebackApiError",
58181
- timebackConfig: "TimebackConfigError",
58182
- timebackSdk: "TimebackSDKError"
58183
- };
58184
- });
58185
- exports_verify = {};
58186
- __export2(exports_verify, {
58187
- verifyTimebackResources: () => verifyTimebackResources,
58188
- fetchTimebackConfig: () => fetchTimebackConfig
58189
- });
58190
- init_verify = __esm2(() => {
58191
- init_constants4();
58192
- });
58193
- init_constants4();
58194
- TimebackError = class TimebackError2 extends Error {
58195
- constructor(message) {
58196
- super(message);
58197
- this.name = ERROR_NAMES.timebackSdk;
58198
- }
58199
- };
58200
- TimebackApiError = class TimebackApiError2 extends Error {
58201
- status;
58202
- details;
58203
- constructor(status, message, details) {
58204
- super(`${status} ${message}`);
58205
- this.name = ERROR_NAMES.timebackApi;
58206
- this.status = status;
58207
- this.details = details;
58208
- Object.setPrototypeOf(this, TimebackApiError2.prototype);
58209
- }
58210
- };
58211
- TimebackAuthenticationError = class TimebackAuthenticationError2 extends TimebackError {
58212
- constructor(message) {
58213
- super(message || "Authentication failed. Please verify TIMEBACK_CLIENT_ID and TIMEBACK_CLIENT_SECRET are set correctly.");
58214
- this.name = "TimebackAuthenticationError";
58215
- Object.setPrototypeOf(this, TimebackAuthenticationError2.prototype);
58216
- }
58217
- };
58218
- StudentNotFoundError = class StudentNotFoundError2 extends TimebackError {
58219
- identifier;
58220
- identifierType;
58221
- constructor(identifier, identifierType = "email") {
58222
- super(`Student not found with ${identifierType}: ${identifier}. Ensure the student exists in OneRoster. If this is a new student, they must be created in OneRoster first.`);
58223
- this.name = "StudentNotFoundError";
58224
- this.identifier = identifier;
58225
- this.identifierType = identifierType;
58226
- Object.setPrototypeOf(this, StudentNotFoundError2.prototype);
58227
- }
58228
- };
58229
- ConfigurationError = class ConfigurationError2 extends TimebackError {
58230
- field;
58231
- constructor(field, message) {
58232
- super(message || `Missing required configuration: ${field}. Please check your timeback.config.js file.`);
58233
- this.name = "ConfigurationError";
58234
- this.field = field;
58235
- Object.setPrototypeOf(this, ConfigurationError2.prototype);
58236
- }
58237
- };
58238
- ResourceNotFoundError = class ResourceNotFoundError2 extends TimebackError {
58239
- resourceType;
58240
- sourcedId;
58241
- constructor(resourceType, sourcedId) {
58242
- const message = sourcedId === "setup-not-run" ? `TimeBack resources have not been created yet. Please run 'bunx @playcademy/timeback setup' to create the required resources (organization, course, components, etc.) before recording progress.` : `${resourceType} with ID '${sourcedId}' not found`;
58243
- super(message);
58244
- this.name = "ResourceNotFoundError";
58245
- this.resourceType = resourceType;
58246
- this.sourcedId = sourcedId;
58247
- Object.setPrototypeOf(this, ResourceNotFoundError2.prototype);
58248
- }
58249
- };
58250
- init_constants4();
58251
- SUBJECT_VALUES = TIMEBACK_SUBJECTS2;
58252
- GRADE_VALUES = TIMEBACK_GRADE_LEVELS;
58253
- init_verify();
58254
- init_constants4();
58255
- init_constants4();
58256
- if (process.env.DEBUG === "true") {
58257
- process.env.TERM = "dumb";
58258
- }
58259
- TimebackAuthError = class TimebackAuthError2 extends Error {
58260
- statusCode;
58261
- constructor(message, statusCode) {
58262
- super(message);
58263
- this.name = ERROR_NAMES.timebackAuth;
58264
- this.statusCode = statusCode;
58265
- }
58266
- };
58267
- init_constants4();
58268
- UUID_PATTERN = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
58269
- storage = new AsyncLocalStorage;
58270
- init_constants4();
58271
- init_constants4();
58272
- init_constants4();
58273
- init_constants4();
58274
- init_constants4();
58275
- init_constants4();
58276
- init_constants4();
58277
- init_constants4();
58278
- EmailSchema = exports_external.string().email();
58279
- StudentSourcedIdSchema = exports_external.string().min(1, {
58280
- message: "Student sourcedId must be a non-empty string"
58281
- });
58282
- StudentIdentifierSchema = exports_external.union([EmailSchema, StudentSourcedIdSchema]);
58283
- });
58284
- var __esm3 = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
58285
- var TIMEBACK_API_URLS2;
58286
- var QTI_API_URL2 = "https://qti.alpha-1edtech.ai/api";
58287
- var TIMEBACK_AUTH_URLS2;
58288
- var CALIPER_API_URLS2;
58289
- var ONEROSTER_ENDPOINTS2;
58290
- var QTI_ENDPOINTS2;
58291
- var CALIPER_ENDPOINTS2;
58292
- var CALIPER_CONSTANTS2;
58293
- var TIMEBACK_EVENT_TYPES2;
58294
- var TIMEBACK_ACTIONS2;
58295
- var TIMEBACK_TYPES2;
58296
- var ACTIVITY_METRIC_TYPES2;
58297
- var TIME_METRIC_TYPES2;
58298
- var TIMEBACK_SUBJECTS3;
58299
- var TIMEBACK_GRADE_LEVELS2;
58300
- var TIMEBACK_GRADE_LEVEL_LABELS2;
58301
- var CALIPER_SUBJECTS2;
58302
- var ONEROSTER_STATUS2;
58303
- var SCORE_STATUS2;
58304
- var ENV_VARS2;
58305
- var HTTP_DEFAULTS2;
58306
- var AUTH_DEFAULTS2;
58307
- var CACHE_DEFAULTS2;
58308
- var CONFIG_DEFAULTS2;
58309
- var PLAYCADEMY_DEFAULTS2;
58310
- var RESOURCE_DEFAULTS2;
58311
- var HTTP_STATUS2;
58312
- var ERROR_NAMES2;
58313
- var init_constants6;
58314
- var init_constants5 = __esm(() => {
58315
- init_src();
58316
- init_constants6 = __esm3(() => {
58628
+ init_constants5 = __esm3(() => {
58317
58629
  TIMEBACK_API_URLS2 = {
58318
58630
  production: "https://api.alpha-1edtech.ai",
58319
58631
  staging: "https://api.staging.alpha-1edtech.com"
@@ -58495,34 +58807,152 @@ var init_constants5 = __esm(() => {
58495
58807
  timebackSdk: "TimebackSDKError"
58496
58808
  };
58497
58809
  });
58498
- init_constants6();
58810
+ init_errors4 = __esm3(() => {
58811
+ init_constants5();
58812
+ TimebackError = class TimebackError2 extends Error {
58813
+ constructor(message) {
58814
+ super(message);
58815
+ this.name = ERROR_NAMES2.timebackSdk;
58816
+ }
58817
+ };
58818
+ TimebackApiError = class TimebackApiError2 extends Error {
58819
+ status;
58820
+ details;
58821
+ constructor(status, message, details) {
58822
+ super(`${status} ${message}`);
58823
+ this.name = ERROR_NAMES2.timebackApi;
58824
+ this.status = status;
58825
+ this.details = details;
58826
+ Object.setPrototypeOf(this, TimebackApiError2.prototype);
58827
+ }
58828
+ };
58829
+ TimebackAuthenticationError = class TimebackAuthenticationError2 extends TimebackError {
58830
+ constructor(message) {
58831
+ super(message || "Authentication failed. Please verify TIMEBACK_CLIENT_ID and TIMEBACK_CLIENT_SECRET are set correctly.");
58832
+ this.name = "TimebackAuthenticationError";
58833
+ Object.setPrototypeOf(this, TimebackAuthenticationError2.prototype);
58834
+ }
58835
+ };
58836
+ StudentNotFoundError = class StudentNotFoundError2 extends TimebackError {
58837
+ identifier;
58838
+ identifierType;
58839
+ constructor(identifier, identifierType = "email") {
58840
+ super(`Student not found with ${identifierType}: ${identifier}. Ensure the student exists in OneRoster. If this is a new student, they must be created in OneRoster first.`);
58841
+ this.name = "StudentNotFoundError";
58842
+ this.identifier = identifier;
58843
+ this.identifierType = identifierType;
58844
+ Object.setPrototypeOf(this, StudentNotFoundError2.prototype);
58845
+ }
58846
+ };
58847
+ ConfigurationError = class ConfigurationError2 extends TimebackError {
58848
+ field;
58849
+ constructor(field, message) {
58850
+ super(message || `Missing required configuration: ${field}. Please check your timeback.config.js file.`);
58851
+ this.name = "ConfigurationError";
58852
+ this.field = field;
58853
+ Object.setPrototypeOf(this, ConfigurationError2.prototype);
58854
+ }
58855
+ };
58856
+ ResourceAlreadyExistsError = class ResourceAlreadyExistsError2 extends TimebackError {
58857
+ resourceType;
58858
+ sourcedId;
58859
+ originalError;
58860
+ constructor(resourceType, sourcedId, originalError) {
58861
+ super(`${resourceType} with ID '${sourcedId}' already exists`);
58862
+ this.name = "ResourceAlreadyExistsError";
58863
+ this.resourceType = resourceType;
58864
+ this.sourcedId = sourcedId;
58865
+ this.originalError = originalError;
58866
+ Object.setPrototypeOf(this, ResourceAlreadyExistsError2.prototype);
58867
+ }
58868
+ };
58869
+ ResourceNotFoundError = class ResourceNotFoundError2 extends TimebackError {
58870
+ resourceType;
58871
+ sourcedId;
58872
+ constructor(resourceType, sourcedId) {
58873
+ const message = sourcedId === "setup-not-run" ? `TimeBack resources have not been created yet. Please run 'bunx @playcademy/timeback setup' to create the required resources (organization, course, components, etc.) before recording progress.` : `${resourceType} with ID '${sourcedId}' not found`;
58874
+ super(message);
58875
+ this.name = "ResourceNotFoundError";
58876
+ this.resourceType = resourceType;
58877
+ this.sourcedId = sourcedId;
58878
+ Object.setPrototypeOf(this, ResourceNotFoundError2.prototype);
58879
+ }
58880
+ };
58881
+ });
58882
+ init_ids = __esm3(() => {
58883
+ init_errors4();
58884
+ UUID_REGEX2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
58885
+ });
58886
+ exports_verify = {};
58887
+ __export2(exports_verify, {
58888
+ verifyTimebackResources: () => verifyTimebackResources,
58889
+ fetchTimebackConfig: () => fetchTimebackConfig
58890
+ });
58891
+ init_verify = __esm3(() => {
58892
+ init_constants5();
58893
+ init_ids();
58894
+ });
58895
+ init_constants5();
58896
+ SUBJECT_VALUES2 = TIMEBACK_SUBJECTS3;
58897
+ GRADE_VALUES2 = TIMEBACK_GRADE_LEVELS2;
58898
+ init_ids();
58899
+ init_ids();
58900
+ init_verify();
58901
+ init_ids();
58902
+ init_constants5();
58903
+ init_errors4();
58904
+ init_constants5();
58905
+ if (process.env.DEBUG === "true") {
58906
+ process.env.TERM = "dumb";
58907
+ }
58908
+ TimebackAuthError = class TimebackAuthError2 extends Error {
58909
+ statusCode;
58910
+ constructor(message, statusCode) {
58911
+ super(message);
58912
+ this.name = ERROR_NAMES2.timebackAuth;
58913
+ this.statusCode = statusCode;
58914
+ }
58915
+ };
58916
+ init_ids();
58917
+ init_constants5();
58918
+ init_errors4();
58919
+ UUID_PATTERN = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
58920
+ storage = new AsyncLocalStorage;
58921
+ init_constants5();
58922
+ init_constants5();
58923
+ init_errors4();
58924
+ init_constants5();
58925
+ init_constants5();
58926
+ init_constants5();
58927
+ init_constants5();
58928
+ init_constants5();
58929
+ init_ids();
58930
+ init_ids();
58931
+ init_errors4();
58932
+ init_ids();
58933
+ init_errors4();
58934
+ EmailSchema = exports_external.string().email();
58935
+ StudentSourcedIdSchema = exports_external.string().min(1, {
58936
+ message: "Student sourcedId must be a non-empty string"
58937
+ });
58938
+ StudentIdentifierSchema = exports_external.union([EmailSchema, StudentSourcedIdSchema]);
58939
+ init_ids();
58499
58940
  });
58500
- function isObject2(value) {
58501
- return typeof value === "object" && value !== null;
58502
- }
58503
- function isCourseMetadata(value) {
58504
- return isObject2(value);
58505
- }
58506
- function isResourceMetadata(value) {
58507
- return isObject2(value);
58508
- }
58509
- function isPlaycademyResourceMetadata2(value) {
58510
- if (!isObject2(value)) {
58511
- return false;
58941
+ function deriveTimebackCourseLevelFromGrade(grade) {
58942
+ if (grade === 13) {
58943
+ return TIMEBACK_COURSE_DEFAULTS.level.ap;
58512
58944
  }
58513
- if (!("mastery" in value) || value.mastery === undefined) {
58514
- return true;
58945
+ if (grade <= 5) {
58946
+ return TIMEBACK_COURSE_DEFAULTS.level.elementary;
58515
58947
  }
58516
- return isObject2(value.mastery);
58517
- }
58518
- function isTimebackSubject3(value) {
58519
- return typeof value === "string" && SUBJECT_VALUES2.includes(value);
58520
- }
58521
- function isTimebackGrade3(value) {
58522
- return typeof value === "number" && Number.isInteger(value) && GRADE_VALUES2.includes(value);
58948
+ if (grade <= 8) {
58949
+ return TIMEBACK_COURSE_DEFAULTS.level.middle;
58950
+ }
58951
+ return TIMEBACK_COURSE_DEFAULTS.level.high;
58523
58952
  }
58524
58953
  var __esm4 = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
58525
58954
  var TIMEBACK_API_URLS3;
58955
+ var QTI_API_URL2 = "https://qti.alpha-1edtech.ai/api";
58526
58956
  var TIMEBACK_AUTH_URLS3;
58527
58957
  var CALIPER_API_URLS3;
58528
58958
  var ONEROSTER_ENDPOINTS3;
@@ -58550,9 +58980,7 @@ var RESOURCE_DEFAULTS3;
58550
58980
  var HTTP_STATUS3;
58551
58981
  var ERROR_NAMES3;
58552
58982
  var init_constants7;
58553
- var SUBJECT_VALUES2;
58554
- var GRADE_VALUES2;
58555
- var init_types3 = __esm(() => {
58983
+ var init_constants6 = __esm(() => {
58556
58984
  init_src();
58557
58985
  init_constants7 = __esm4(() => {
58558
58986
  TIMEBACK_API_URLS3 = {
@@ -58737,8 +59165,6 @@ var init_types3 = __esm(() => {
58737
59165
  };
58738
59166
  });
58739
59167
  init_constants7();
58740
- SUBJECT_VALUES2 = TIMEBACK_SUBJECTS4;
58741
- GRADE_VALUES2 = TIMEBACK_GRADE_LEVELS3;
58742
59168
  });
58743
59169
  function deriveSourcedIds2(courseId) {
58744
59170
  return {
@@ -58787,6 +59213,16 @@ var RESOURCE_DEFAULTS4;
58787
59213
  var HTTP_STATUS4;
58788
59214
  var ERROR_NAMES4;
58789
59215
  var init_constants8;
59216
+ var TimebackError2;
59217
+ var TimebackApiError2;
59218
+ var TimebackAuthenticationError2;
59219
+ var StudentNotFoundError2;
59220
+ var ConfigurationError2;
59221
+ var ResourceAlreadyExistsError2;
59222
+ var ResourceNotFoundError2;
59223
+ var init_errors5;
59224
+ var UUID_REGEX3;
59225
+ var init_ids2;
58790
59226
  var init_utils6 = __esm(() => {
58791
59227
  init_src();
58792
59228
  init_constants8 = __esm5(() => {
@@ -58971,6 +59407,82 @@ var init_utils6 = __esm(() => {
58971
59407
  timebackSdk: "TimebackSDKError"
58972
59408
  };
58973
59409
  });
59410
+ init_errors5 = __esm5(() => {
59411
+ init_constants8();
59412
+ TimebackError2 = class TimebackError3 extends Error {
59413
+ constructor(message) {
59414
+ super(message);
59415
+ this.name = ERROR_NAMES4.timebackSdk;
59416
+ }
59417
+ };
59418
+ TimebackApiError2 = class TimebackApiError3 extends Error {
59419
+ status;
59420
+ details;
59421
+ constructor(status, message, details) {
59422
+ super(`${status} ${message}`);
59423
+ this.name = ERROR_NAMES4.timebackApi;
59424
+ this.status = status;
59425
+ this.details = details;
59426
+ Object.setPrototypeOf(this, TimebackApiError3.prototype);
59427
+ }
59428
+ };
59429
+ TimebackAuthenticationError2 = class TimebackAuthenticationError3 extends TimebackError2 {
59430
+ constructor(message) {
59431
+ super(message || "Authentication failed. Please verify TIMEBACK_CLIENT_ID and TIMEBACK_CLIENT_SECRET are set correctly.");
59432
+ this.name = "TimebackAuthenticationError";
59433
+ Object.setPrototypeOf(this, TimebackAuthenticationError3.prototype);
59434
+ }
59435
+ };
59436
+ StudentNotFoundError2 = class StudentNotFoundError3 extends TimebackError2 {
59437
+ identifier;
59438
+ identifierType;
59439
+ constructor(identifier, identifierType = "email") {
59440
+ super(`Student not found with ${identifierType}: ${identifier}. Ensure the student exists in OneRoster. If this is a new student, they must be created in OneRoster first.`);
59441
+ this.name = "StudentNotFoundError";
59442
+ this.identifier = identifier;
59443
+ this.identifierType = identifierType;
59444
+ Object.setPrototypeOf(this, StudentNotFoundError3.prototype);
59445
+ }
59446
+ };
59447
+ ConfigurationError2 = class ConfigurationError3 extends TimebackError2 {
59448
+ field;
59449
+ constructor(field, message) {
59450
+ super(message || `Missing required configuration: ${field}. Please check your timeback.config.js file.`);
59451
+ this.name = "ConfigurationError";
59452
+ this.field = field;
59453
+ Object.setPrototypeOf(this, ConfigurationError3.prototype);
59454
+ }
59455
+ };
59456
+ ResourceAlreadyExistsError2 = class ResourceAlreadyExistsError3 extends TimebackError2 {
59457
+ resourceType;
59458
+ sourcedId;
59459
+ originalError;
59460
+ constructor(resourceType, sourcedId, originalError) {
59461
+ super(`${resourceType} with ID '${sourcedId}' already exists`);
59462
+ this.name = "ResourceAlreadyExistsError";
59463
+ this.resourceType = resourceType;
59464
+ this.sourcedId = sourcedId;
59465
+ this.originalError = originalError;
59466
+ Object.setPrototypeOf(this, ResourceAlreadyExistsError3.prototype);
59467
+ }
59468
+ };
59469
+ ResourceNotFoundError2 = class ResourceNotFoundError3 extends TimebackError2 {
59470
+ resourceType;
59471
+ sourcedId;
59472
+ constructor(resourceType, sourcedId) {
59473
+ const message = sourcedId === "setup-not-run" ? `TimeBack resources have not been created yet. Please run 'bunx @playcademy/timeback setup' to create the required resources (organization, course, components, etc.) before recording progress.` : `${resourceType} with ID '${sourcedId}' not found`;
59474
+ super(message);
59475
+ this.name = "ResourceNotFoundError";
59476
+ this.resourceType = resourceType;
59477
+ this.sourcedId = sourcedId;
59478
+ Object.setPrototypeOf(this, ResourceNotFoundError3.prototype);
59479
+ }
59480
+ };
59481
+ });
59482
+ init_ids2 = __esm5(() => {
59483
+ init_errors5();
59484
+ UUID_REGEX3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
59485
+ });
58974
59486
  init_constants8();
58975
59487
  init_constants8();
58976
59488
  if (process.env.DEBUG === "true") {
@@ -59130,331 +59642,6 @@ function compareEnrollmentsByRecency(a, b) {
59130
59642
  var init_timeback_admin_util = __esm(() => {
59131
59643
  init_errors();
59132
59644
  });
59133
- function isRecord2(value) {
59134
- return typeof value === "object" && value !== null;
59135
- }
59136
- function filterEnrollmentsByGame(enrollments, gameId) {
59137
- return enrollments.filter((enrollment) => enrollment.gameId === gameId).map(({ gameId: _2, ...enrollment }) => enrollment);
59138
- }
59139
- function mapEnrollmentsToUserEnrollments(enrollments, integrations) {
59140
- const enrollmentByCourse = new Map(enrollments.map((enrollment) => [enrollment.courseId, enrollment]));
59141
- const courseToSchool = new Map(enrollments.filter((enrollment) => enrollment.school?.id).map((enrollment) => [enrollment.courseId, enrollment.school.id]));
59142
- return integrations.map((integration) => {
59143
- const enrollment = enrollmentByCourse.get(integration.courseId);
59144
- return {
59145
- gameId: integration.gameId,
59146
- grade: integration.grade,
59147
- subject: integration.subject,
59148
- courseId: integration.courseId,
59149
- orgId: courseToSchool.get(integration.courseId),
59150
- ...enrollment ? { id: enrollment.sourcedId } : {}
59151
- };
59152
- });
59153
- }
59154
- function getStringValue(value) {
59155
- return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
59156
- }
59157
- function parseSourcedIdFromUrl(url2) {
59158
- if (!url2) {
59159
- return;
59160
- }
59161
- const trimmed = url2.trim().replace(/\/$/, "");
59162
- if (!trimmed) {
59163
- return;
59164
- }
59165
- const segments = trimmed.split("/");
59166
- const lastSegment = segments.at(-1);
59167
- return lastSegment ? decodeURIComponent(lastSegment) : undefined;
59168
- }
59169
- function getGeneratedMetricValue(event, type) {
59170
- const items = event.generated?.items;
59171
- if (!Array.isArray(items)) {
59172
- return;
59173
- }
59174
- const metric = items.find((item) => item?.type === type);
59175
- if (!metric) {
59176
- return;
59177
- }
59178
- const value = typeof metric.value === "number" ? metric.value : Number(metric.value);
59179
- return Number.isFinite(value) ? value : undefined;
59180
- }
59181
- function getMergedCaliperExtensions(event) {
59182
- const objectActivityExtensions = isRecord2(event.object.activity?.extensions) ? event.object.activity.extensions : undefined;
59183
- const generatedExtensions = isRecord2(event.generated?.extensions) ? event.generated.extensions : undefined;
59184
- const eventExtensions = isRecord2(event.extensions) ? event.extensions : undefined;
59185
- return {
59186
- ...objectActivityExtensions,
59187
- ...generatedExtensions,
59188
- ...eventExtensions
59189
- };
59190
- }
59191
- function getPlaycademyMetadata(event) {
59192
- const extensions = getMergedCaliperExtensions(event);
59193
- return isRecord2(extensions.playcademy) ? extensions.playcademy : undefined;
59194
- }
59195
- function getActivityId(event, playcademy) {
59196
- const metadataActivityId = getStringValue(playcademy?.activityId);
59197
- if (metadataActivityId) {
59198
- return metadataActivityId;
59199
- }
59200
- const activityId = getStringValue(event.object.activity?.id);
59201
- if (activityId) {
59202
- return activityId;
59203
- }
59204
- const objectId = getStringValue(event.object.id);
59205
- if (!objectId) {
59206
- return;
59207
- }
59208
- const trimmed = objectId.replace(/\/$/, "");
59209
- const segments = trimmed.split("/");
59210
- const activityIndex = segments.lastIndexOf("activities");
59211
- if (activityIndex !== -1 && segments.length >= activityIndex + 3) {
59212
- const candidate = segments[activityIndex + 2];
59213
- return candidate ? decodeURIComponent(candidate) : undefined;
59214
- }
59215
- return;
59216
- }
59217
- function buildResourceMetadata({
59218
- baseMetadata,
59219
- subject,
59220
- grade,
59221
- totalXp,
59222
- masterableUnits
59223
- }) {
59224
- const normalizedBaseMetadata = isResourceMetadata(baseMetadata) ? baseMetadata : undefined;
59225
- const metadata2 = {
59226
- ...normalizedBaseMetadata
59227
- };
59228
- metadata2.subject = subject;
59229
- metadata2.grades = [grade];
59230
- metadata2.xp = totalXp;
59231
- if (masterableUnits !== undefined && masterableUnits !== null) {
59232
- const existingPlaycademy = isPlaycademyResourceMetadata2(metadata2.playcademy) ? metadata2.playcademy : undefined;
59233
- metadata2.playcademy = {
59234
- ...existingPlaycademy,
59235
- mastery: {
59236
- ...existingPlaycademy?.mastery,
59237
- masterableUnits
59238
- }
59239
- };
59240
- }
59241
- return metadata2;
59242
- }
59243
- function getDurationSecondsFromExtensions(event) {
59244
- const extensions = getMergedCaliperExtensions(event);
59245
- const playcademy = isRecord2(extensions.playcademy) ? extensions.playcademy : undefined;
59246
- const rawValue = extensions.durationSeconds ?? playcademy?.durationSeconds;
59247
- const value = typeof rawValue === "number" ? rawValue : Number(rawValue);
59248
- return Number.isFinite(value) ? value : undefined;
59249
- }
59250
- function getCanonicalRunId(session2) {
59251
- const sessionId = getStringValue(session2?.id);
59252
- if (!sessionId) {
59253
- return;
59254
- }
59255
- return sessionId.replace(/^urn:uuid:/, "");
59256
- }
59257
- function getResumeId(event) {
59258
- const playcademy = getPlaycademyMetadata(event);
59259
- return getStringValue(playcademy?.resumeId);
59260
- }
59261
- function isCaliperRemediationOrCompletionEvent(event) {
59262
- const playcademy = getPlaycademyMetadata(event);
59263
- return REMEDIATION_OR_COMPLETION_EVENT_KINDS.has(getStringValue(playcademy?.eventKind) || "");
59264
- }
59265
- function groupCaliperEventsByRun(events) {
59266
- const groups = new Map;
59267
- for (const event of events) {
59268
- const objectId = getStringValue(event.object.id) || "unknown-activity";
59269
- const groupKey = `${objectId}::${getStringValue(event.session?.id) || event.externalId}`;
59270
- const existing = groups.get(groupKey);
59271
- if (existing) {
59272
- existing.push(event);
59273
- } else {
59274
- groups.set(groupKey, [event]);
59275
- }
59276
- }
59277
- return groups;
59278
- }
59279
- function findCaliperEventGroupContainingExternalId(events, externalId) {
59280
- const targetExternalId = externalId.trim();
59281
- if (!targetExternalId) {
59282
- return;
59283
- }
59284
- return [...groupCaliperEventsByRun(events).values()].find((group) => group.some((event) => event.externalId === targetExternalId));
59285
- }
59286
- function mapCaliperEventGroupToActivity(events, relevantCourseIds) {
59287
- if (events.length === 0) {
59288
- return null;
59289
- }
59290
- const sortedEvents = events.toSorted((a, b) => a.eventTime.localeCompare(b.eventTime));
59291
- const activityEvent = [...sortedEvents].toReversed().find((event) => event.type === "ActivityEvent");
59292
- const contextSource = activityEvent || sortedEvents.at(-1);
59293
- if (!contextSource) {
59294
- return null;
59295
- }
59296
- const ctx = parseCaliperEventContext(contextSource, relevantCourseIds);
59297
- if (!ctx) {
59298
- return null;
59299
- }
59300
- const score = activityEvent !== undefined ? (() => {
59301
- const totalQuestions = getGeneratedMetricValue(activityEvent, "totalQuestions");
59302
- const correctQuestions = getGeneratedMetricValue(activityEvent, "correctQuestions");
59303
- if (totalQuestions === undefined || correctQuestions === undefined || totalQuestions <= 0) {
59304
- return;
59305
- }
59306
- return correctQuestions / totalQuestions * 100;
59307
- })() : undefined;
59308
- const xpEarned = activityEvent !== undefined ? getGeneratedMetricValue(activityEvent, "xpEarned") : undefined;
59309
- const masteredUnits = activityEvent !== undefined ? getGeneratedMetricValue(activityEvent, "masteredUnits") : undefined;
59310
- const timeSpentEvents = sortedEvents.filter((event) => event.type === "TimeSpentEvent");
59311
- let totalActiveTimeSeconds;
59312
- if (timeSpentEvents.length > 0) {
59313
- totalActiveTimeSeconds = timeSpentEvents.reduce((sum, event) => sum + (getGeneratedMetricValue(event, "active") ?? 0), 0);
59314
- } else if (activityEvent !== undefined) {
59315
- totalActiveTimeSeconds = getDurationSecondsFromExtensions(activityEvent);
59316
- }
59317
- const fallbackActivityId = getActivityId(contextSource, getPlaycademyMetadata(contextSource));
59318
- const occurredAt = getStringValue(activityEvent?.eventTime) || getStringValue(sortedEvents.at(-1)?.eventTime);
59319
- const runId = getCanonicalRunId(contextSource.session);
59320
- const resumeIds = new Set(sortedEvents.map((event) => getResumeId(event)).filter((resumeId) => resumeId !== undefined));
59321
- const sessionCount = resumeIds.size > 0 ? resumeIds.size : 1;
59322
- const kind = activityEvent !== undefined ? "activity" : "activity-in-progress";
59323
- if (!occurredAt) {
59324
- return null;
59325
- }
59326
- return {
59327
- id: activityEvent?.externalId || sortedEvents.at(-1)?.externalId || events[0].externalId,
59328
- kind,
59329
- occurredAt,
59330
- courseId: ctx.courseId,
59331
- title: getStringValue(activityEvent?.object.activity?.name) || ctx.titleFromEvent || (fallbackActivityId ? kebabToTitleCase(fallbackActivityId) : "Activity completed"),
59332
- ...ctx.activityId ? { activityId: ctx.activityId } : {},
59333
- ...ctx.appName ? { appName: ctx.appName } : {},
59334
- ...score !== undefined ? { score } : {},
59335
- ...xpEarned !== undefined ? { xpDelta: xpEarned } : {},
59336
- ...masteredUnits !== undefined ? { masteredUnitsDelta: masteredUnits } : {},
59337
- ...totalActiveTimeSeconds !== undefined ? { timeDeltaSeconds: totalActiveTimeSeconds } : {},
59338
- ...runId ? { runId } : {},
59339
- ...sessionCount > 0 ? { sessionCount } : {}
59340
- };
59341
- }
59342
- function parseCaliperEventContext(event, relevantCourseIds) {
59343
- const playcademy = getPlaycademyMetadata(event);
59344
- const courseId = getStringValue(playcademy?.courseId) || parseSourcedIdFromUrl(event.object.course?.id);
59345
- if (!courseId || !relevantCourseIds.has(courseId)) {
59346
- return null;
59347
- }
59348
- const occurredAt = getStringValue(event.eventTime);
59349
- if (!occurredAt) {
59350
- return null;
59351
- }
59352
- return {
59353
- courseId,
59354
- occurredAt,
59355
- eventKind: getStringValue(playcademy?.eventKind),
59356
- source: getStringValue(playcademy?.source),
59357
- reason: getStringValue(playcademy?.reason),
59358
- titleFromEvent: getStringValue(event.object.activity?.name),
59359
- appName: getStringValue(event.object.app?.name),
59360
- activityId: getActivityId(event, playcademy)
59361
- };
59362
- }
59363
- function mapTimeSpentRemediation(event, ctx) {
59364
- if (ctx.eventKind !== "remediation-time") {
59365
- return null;
59366
- }
59367
- return {
59368
- id: event.externalId,
59369
- kind: "remediation-time",
59370
- occurredAt: ctx.occurredAt,
59371
- courseId: ctx.courseId,
59372
- title: "Time Adjustment",
59373
- activityId: ctx.activityId,
59374
- appName: ctx.appName,
59375
- reason: ctx.reason,
59376
- timeDeltaSeconds: getGeneratedMetricValue(event, "active")
59377
- };
59378
- }
59379
- function mapActivityRemediation(event, ctx) {
59380
- if (ctx.eventKind === "remediation-xp") {
59381
- return {
59382
- id: event.externalId,
59383
- kind: "remediation-xp",
59384
- occurredAt: ctx.occurredAt,
59385
- courseId: ctx.courseId,
59386
- title: "XP Adjustment",
59387
- activityId: ctx.activityId,
59388
- appName: ctx.appName,
59389
- reason: ctx.reason,
59390
- xpDelta: getGeneratedMetricValue(event, "xpEarned"),
59391
- masteredUnitsDelta: getGeneratedMetricValue(event, "masteredUnits")
59392
- };
59393
- }
59394
- if (ctx.eventKind === "remediation-mastery") {
59395
- return {
59396
- id: event.externalId,
59397
- kind: "remediation-mastery",
59398
- occurredAt: ctx.occurredAt,
59399
- courseId: ctx.courseId,
59400
- title: "Mastery Adjustment",
59401
- activityId: ctx.activityId,
59402
- appName: ctx.appName,
59403
- reason: ctx.reason,
59404
- xpDelta: getGeneratedMetricValue(event, "xpEarned"),
59405
- masteredUnitsDelta: getGeneratedMetricValue(event, "masteredUnits")
59406
- };
59407
- }
59408
- if (ctx.eventKind === "course-completed") {
59409
- return {
59410
- id: event.externalId,
59411
- kind: "course-completed",
59412
- occurredAt: ctx.occurredAt,
59413
- courseId: ctx.courseId,
59414
- title: ctx.source === "admin" ? "Course marked complete" : "Course completed",
59415
- activityId: ctx.activityId,
59416
- appName: ctx.appName,
59417
- reason: ctx.reason
59418
- };
59419
- }
59420
- if (ctx.eventKind === "course-resumed") {
59421
- return {
59422
- id: event.externalId,
59423
- kind: "course-resumed",
59424
- occurredAt: ctx.occurredAt,
59425
- courseId: ctx.courseId,
59426
- title: "Course resumed",
59427
- activityId: ctx.activityId,
59428
- appName: ctx.appName,
59429
- reason: ctx.reason
59430
- };
59431
- }
59432
- return null;
59433
- }
59434
- function mapCaliperEventToRemediationActivity(event, relevantCourseIds) {
59435
- const ctx = parseCaliperEventContext(event, relevantCourseIds);
59436
- if (!ctx) {
59437
- return null;
59438
- }
59439
- if (event.type === "TimeSpentEvent") {
59440
- return mapTimeSpentRemediation(event, ctx);
59441
- }
59442
- if (event.type === "ActivityEvent") {
59443
- return mapActivityRemediation(event, ctx);
59444
- }
59445
- return null;
59446
- }
59447
- var REMEDIATION_OR_COMPLETION_EVENT_KINDS;
59448
- var init_timeback_util = __esm(() => {
59449
- init_types3();
59450
- REMEDIATION_OR_COMPLETION_EVENT_KINDS = new Set([
59451
- "remediation-xp",
59452
- "remediation-time",
59453
- "remediation-mastery",
59454
- "course-completed",
59455
- "course-resumed"
59456
- ]);
59457
- });
59458
59645
  function parseDateInputParts(value) {
59459
59646
  const parts2 = value.split("-");
59460
59647
  return {
@@ -59823,7 +60010,7 @@ function isAllowedGradeLevelTestType(value) {
59823
60010
  }
59824
60011
  function isQualifyingGradeLevelTestResult(result, options) {
59825
60012
  const metadata2 = metadataOf(result);
59826
- return sourceIdFromRef(result.student) === options.studentId && normalizeText(result.status) === "active" && result.scoreStatus === SCORE_STATUS2.fullyGraded && normalizeText(metadata2.resultType) === "assessment" && stringField(metadata2.subject).trim() === options.subject && isAllowedGradeLevelTestType(metadata2.testType);
60013
+ return sourceIdFromRef(result.student) === options.studentId && normalizeText(result.status) === "active" && result.scoreStatus === SCORE_STATUS3.fullyGraded && normalizeText(metadata2.resultType) === "assessment" && stringField(metadata2.subject).trim() === options.subject && isAllowedGradeLevelTestType(metadata2.testType);
59827
60014
  }
59828
60015
  function mapGradeLevelTestSummary(result, options) {
59829
60016
  if (!isQualifyingGradeLevelTestResult(result, options)) {
@@ -60083,7 +60270,7 @@ function latestAssessmentResultsByLineItem(results) {
60083
60270
  const latestResultsByLineItem = new Map;
60084
60271
  for (const result of results) {
60085
60272
  const lineItemId = sourceIdFromRef(result.assessmentLineItem);
60086
- if (lineItemId && !latestResultsByLineItem.has(lineItemId) && result.scoreStatus === SCORE_STATUS2.fullyGraded) {
60273
+ if (lineItemId && !latestResultsByLineItem.has(lineItemId) && result.scoreStatus === SCORE_STATUS3.fullyGraded) {
60087
60274
  latestResultsByLineItem.set(lineItemId, result);
60088
60275
  }
60089
60276
  }
@@ -60184,7 +60371,7 @@ function qtiIdCandidates(parentLineItem, resource) {
60184
60371
  var GRADE_LEVEL_TEST_TYPES;
60185
60372
  var naturalTitleSort;
60186
60373
  var init_timeback_grade_level_results_util = __esm(() => {
60187
- init_constants5();
60374
+ init_constants6();
60188
60375
  init_utils6();
60189
60376
  GRADE_LEVEL_TEST_TYPES = [
60190
60377
  "placement",
@@ -60207,18 +60394,18 @@ async function upsertMasteryCompletionEntry(params) {
60207
60394
  await client.oneroster.assessmentLineItems.findOrCreate(lineItemId, {
60208
60395
  sourcedId: lineItemId,
60209
60396
  title: "Mastery Completion",
60210
- status: ONEROSTER_STATUS2.active,
60397
+ status: ONEROSTER_STATUS3.active,
60211
60398
  course: { sourcedId: ids.course },
60212
60399
  ...ids.componentResource ? { componentResource: { sourcedId: ids.componentResource } } : {}
60213
60400
  });
60214
60401
  await client.oneroster.assessmentResults.upsert(resultId, {
60215
60402
  sourcedId: resultId,
60216
- status: ONEROSTER_STATUS2.active,
60403
+ status: ONEROSTER_STATUS3.active,
60217
60404
  assessmentLineItem: { sourcedId: lineItemId },
60218
60405
  student: { sourcedId: studentId },
60219
60406
  score: 100,
60220
60407
  scoreDate: new Date().toISOString(),
60221
- scoreStatus: SCORE_STATUS2.fullyGraded,
60408
+ scoreStatus: SCORE_STATUS3.fullyGraded,
60222
60409
  inProgress: "false",
60223
60410
  metadata: {
60224
60411
  isMasteryCompletion: true,
@@ -60230,12 +60417,12 @@ async function upsertMasteryCompletionEntry(params) {
60230
60417
  try {
60231
60418
  await client.oneroster.assessmentResults.upsert(resultId, {
60232
60419
  sourcedId: resultId,
60233
- status: ONEROSTER_STATUS2.active,
60420
+ status: ONEROSTER_STATUS3.active,
60234
60421
  assessmentLineItem: { sourcedId: lineItemId },
60235
60422
  student: { sourcedId: studentId },
60236
60423
  score: 0,
60237
60424
  scoreDate: new Date().toISOString(),
60238
- scoreStatus: SCORE_STATUS2.notSubmitted,
60425
+ scoreStatus: SCORE_STATUS3.notSubmitted,
60239
60426
  inProgress: "true",
60240
60427
  metadata: {
60241
60428
  isMasteryCompletion: true,
@@ -60247,7 +60434,7 @@ async function upsertMasteryCompletionEntry(params) {
60247
60434
  }
60248
60435
  }
60249
60436
  var init_timeback_mastery_completion_util = __esm(() => {
60250
- init_constants5();
60437
+ init_constants6();
60251
60438
  init_utils6();
60252
60439
  });
60253
60440
 
@@ -60275,7 +60462,7 @@ class TimebackAdminService {
60275
60462
  this.deps = deps;
60276
60463
  }
60277
60464
  getGradeLevelTestCourseScope(integration) {
60278
- if (!isTimebackSubject(integration.subject) || !isTimebackGrade(integration.grade)) {
60465
+ if (!isTimebackSubject2(integration.subject) || !isTimebackGrade2(integration.grade)) {
60279
60466
  throw new ValidationError("Timeback integration has invalid grade or subject");
60280
60467
  }
60281
60468
  return {
@@ -60354,7 +60541,7 @@ class TimebackAdminService {
60354
60541
  await this.deps.validateDeveloperAccess(user, gameId);
60355
60542
  }
60356
60543
  const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
60357
- where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
60544
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId), isActiveGameTimebackIntegrationStatus())
60358
60545
  });
60359
60546
  if (!integration) {
60360
60547
  throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
@@ -60452,7 +60639,7 @@ class TimebackAdminService {
60452
60639
  const ids = deriveSourcedIds2(courseId);
60453
60640
  const resource = await client.oneroster.resources.get(ids.resource);
60454
60641
  const playcademyMetadata = resource.metadata?.playcademy;
60455
- if (!isPlaycademyResourceMetadata2(playcademyMetadata)) {
60642
+ if (!isPlaycademyResourceMetadata(playcademyMetadata)) {
60456
60643
  return;
60457
60644
  }
60458
60645
  return playcademyMetadata?.mastery?.masterableUnits;
@@ -60856,7 +61043,7 @@ class TimebackAdminService {
60856
61043
  "app.timeback.include_inactive": options?.includeInactive ?? false
60857
61044
  });
60858
61045
  const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
60859
- where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
61046
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId), isActiveGameTimebackIntegrationStatus())
60860
61047
  });
60861
61048
  if (!integration) {
60862
61049
  throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
@@ -60934,7 +61121,7 @@ class TimebackAdminService {
60934
61121
  columns: { id: true }
60935
61122
  }),
60936
61123
  this.deps.db.query.gameTimebackIntegrations.findMany({
60937
- where: eq(gameTimebackIntegrations.gameId, gameId)
61124
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveGameTimebackIntegrationStatus())
60938
61125
  }),
60939
61126
  this.deps.db.query.games.findFirst({
60940
61127
  where: eq(games.id, gameId),
@@ -61037,7 +61224,7 @@ class TimebackAdminService {
61037
61224
  "app.timeback.course_id": courseId
61038
61225
  });
61039
61226
  const integrations = await this.deps.db.query.gameTimebackIntegrations.findMany({
61040
- where: courseId ? and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId)) : eq(gameTimebackIntegrations.gameId, gameId)
61227
+ where: courseId ? and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId), isActiveGameTimebackIntegrationStatus()) : and(eq(gameTimebackIntegrations.gameId, gameId), isActiveGameTimebackIntegrationStatus())
61041
61228
  });
61042
61229
  if (integrations.length === 0) {
61043
61230
  throw new NotFoundError("Timeback integration", gameId);
@@ -61127,7 +61314,7 @@ class TimebackAdminService {
61127
61314
  });
61128
61315
  const [integration, gameSource] = await Promise.all([
61129
61316
  this.deps.db.query.gameTimebackIntegrations.findFirst({
61130
- where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
61317
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId), isActiveGameTimebackIntegrationStatus())
61131
61318
  }),
61132
61319
  this.getGameActivitySource(gameId)
61133
61320
  ]);
@@ -61231,7 +61418,7 @@ class TimebackAdminService {
61231
61418
  "app.timeback.course_id": courseId
61232
61419
  });
61233
61420
  const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
61234
- where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
61421
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId), isActiveGameTimebackIntegrationStatus())
61235
61422
  });
61236
61423
  if (!integration) {
61237
61424
  throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
@@ -61286,7 +61473,7 @@ class TimebackAdminService {
61286
61473
  "app.timeback.assessment_result_id": resultId
61287
61474
  });
61288
61475
  const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
61289
- where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
61476
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId), isActiveGameTimebackIntegrationStatus())
61290
61477
  });
61291
61478
  if (!integration) {
61292
61479
  throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
@@ -61364,7 +61551,7 @@ class TimebackAdminService {
61364
61551
  });
61365
61552
  const [integration, gameSource, roster] = await Promise.all([
61366
61553
  this.deps.db.query.gameTimebackIntegrations.findFirst({
61367
- where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
61554
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId), isActiveGameTimebackIntegrationStatus())
61368
61555
  }),
61369
61556
  this.getGameActivitySource(gameId),
61370
61557
  client.oneroster.enrollments.listByCourse(courseId, {
@@ -61485,7 +61672,7 @@ class TimebackAdminService {
61485
61672
  });
61486
61673
  const [integration, gameSource] = await Promise.all([
61487
61674
  this.deps.db.query.gameTimebackIntegrations.findFirst({
61488
- where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
61675
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId), isActiveGameTimebackIntegrationStatus())
61489
61676
  }),
61490
61677
  this.getGameActivitySource(gameId)
61491
61678
  ]);
@@ -61569,7 +61756,7 @@ class TimebackAdminService {
61569
61756
  });
61570
61757
  const [integration, gameSource] = await Promise.all([
61571
61758
  this.deps.db.query.gameTimebackIntegrations.findFirst({
61572
- where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
61759
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId), isActiveGameTimebackIntegrationStatus())
61573
61760
  }),
61574
61761
  this.getGameActivitySource(gameId)
61575
61762
  ]);
@@ -61798,7 +61985,7 @@ class TimebackAdminService {
61798
61985
  "app.timeback.course_id": courseId
61799
61986
  });
61800
61987
  const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
61801
- where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
61988
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId), isActiveGameTimebackIntegrationStatus())
61802
61989
  });
61803
61990
  if (!integration) {
61804
61991
  throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
@@ -61869,7 +62056,7 @@ class TimebackAdminService {
61869
62056
  "app.timeback.enrollment.operation": "enroll"
61870
62057
  });
61871
62058
  const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
61872
- where: and(eq(gameTimebackIntegrations.gameId, data.gameId), eq(gameTimebackIntegrations.courseId, data.courseId))
62059
+ where: and(eq(gameTimebackIntegrations.gameId, data.gameId), eq(gameTimebackIntegrations.courseId, data.courseId), isActiveGameTimebackIntegrationStatus())
61873
62060
  });
61874
62061
  if (!integration) {
61875
62062
  throw new NotFoundError("Timeback integration", `${data.gameId}:${data.courseId}`);
@@ -61909,7 +62096,7 @@ class TimebackAdminService {
61909
62096
  "app.timeback.enrollment.operation": "unenroll"
61910
62097
  });
61911
62098
  const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
61912
- where: and(eq(gameTimebackIntegrations.gameId, data.gameId), eq(gameTimebackIntegrations.courseId, data.courseId))
62099
+ where: and(eq(gameTimebackIntegrations.gameId, data.gameId), eq(gameTimebackIntegrations.courseId, data.courseId), isActiveGameTimebackIntegrationStatus())
61913
62100
  });
61914
62101
  if (!integration) {
61915
62102
  throw new NotFoundError("Timeback integration", `${data.gameId}:${data.courseId}`);
@@ -61930,7 +62117,7 @@ class TimebackAdminService {
61930
62117
  "app.timeback.enrollment.operation": "reactivate"
61931
62118
  });
61932
62119
  const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
61933
- where: and(eq(gameTimebackIntegrations.gameId, data.gameId), eq(gameTimebackIntegrations.courseId, data.courseId))
62120
+ where: and(eq(gameTimebackIntegrations.gameId, data.gameId), eq(gameTimebackIntegrations.courseId, data.courseId), isActiveGameTimebackIntegrationStatus())
61934
62121
  });
61935
62122
  if (!integration) {
61936
62123
  throw new NotFoundError("Timeback integration", `${data.gameId}:${data.courseId}`);
@@ -61976,7 +62163,7 @@ class TimebackAdminService {
61976
62163
  const resultId = `${lineItemId}:${studentId}:completion`;
61977
62164
  try {
61978
62165
  const result = await client.oneroster.assessmentResults.get(resultId);
61979
- if (result.scoreStatus === SCORE_STATUS2.fullyGraded) {
62166
+ if (result.scoreStatus === SCORE_STATUS3.fullyGraded) {
61980
62167
  return "complete";
61981
62168
  }
61982
62169
  return "incomplete";
@@ -62012,12 +62199,13 @@ class TimebackAdminService {
62012
62199
  var init_timeback_admin_service = __esm(() => {
62013
62200
  init_drizzle_orm();
62014
62201
  init_src();
62202
+ init_helpers_index();
62015
62203
  init_schemas_index();
62016
62204
  init_tables_index();
62017
62205
  init_spans();
62018
62206
  init_dist2();
62019
- init_constants5();
62020
- init_types3();
62207
+ init_constants6();
62208
+ init_types2();
62021
62209
  init_utils6();
62022
62210
  init_src4();
62023
62211
  init_timeback3();
@@ -62059,8 +62247,15 @@ var RESOURCE_DEFAULTS5;
62059
62247
  var HTTP_STATUS5;
62060
62248
  var ERROR_NAMES5;
62061
62249
  var init_constants9;
62062
- var TimebackApiError2;
62063
- var init_errors4 = __esm(() => {
62250
+ var TimebackError3;
62251
+ var TimebackApiError3;
62252
+ var TimebackAuthenticationError3;
62253
+ var StudentNotFoundError3;
62254
+ var ConfigurationError3;
62255
+ var ResourceAlreadyExistsError3;
62256
+ var ResourceNotFoundError3;
62257
+ var init_errors7;
62258
+ var init_errors6 = __esm(() => {
62064
62259
  init_src();
62065
62260
  init_constants9 = __esm6(() => {
62066
62261
  TIMEBACK_API_URLS5 = {
@@ -62244,18 +62439,79 @@ var init_errors4 = __esm(() => {
62244
62439
  timebackSdk: "TimebackSDKError"
62245
62440
  };
62246
62441
  });
62247
- init_constants9();
62248
- TimebackApiError2 = class TimebackApiError22 extends Error {
62249
- status;
62250
- details;
62251
- constructor(status, message, details) {
62252
- super(`${status} ${message}`);
62253
- this.name = ERROR_NAMES5.timebackApi;
62254
- this.status = status;
62255
- this.details = details;
62256
- Object.setPrototypeOf(this, TimebackApiError22.prototype);
62257
- }
62258
- };
62442
+ init_errors7 = __esm6(() => {
62443
+ init_constants9();
62444
+ TimebackError3 = class TimebackError4 extends Error {
62445
+ constructor(message) {
62446
+ super(message);
62447
+ this.name = ERROR_NAMES5.timebackSdk;
62448
+ }
62449
+ };
62450
+ TimebackApiError3 = class TimebackApiError4 extends Error {
62451
+ status;
62452
+ details;
62453
+ constructor(status, message, details) {
62454
+ super(`${status} ${message}`);
62455
+ this.name = ERROR_NAMES5.timebackApi;
62456
+ this.status = status;
62457
+ this.details = details;
62458
+ Object.setPrototypeOf(this, TimebackApiError4.prototype);
62459
+ }
62460
+ };
62461
+ TimebackAuthenticationError3 = class TimebackAuthenticationError4 extends TimebackError3 {
62462
+ constructor(message) {
62463
+ super(message || "Authentication failed. Please verify TIMEBACK_CLIENT_ID and TIMEBACK_CLIENT_SECRET are set correctly.");
62464
+ this.name = "TimebackAuthenticationError";
62465
+ Object.setPrototypeOf(this, TimebackAuthenticationError4.prototype);
62466
+ }
62467
+ };
62468
+ StudentNotFoundError3 = class StudentNotFoundError4 extends TimebackError3 {
62469
+ identifier;
62470
+ identifierType;
62471
+ constructor(identifier, identifierType = "email") {
62472
+ super(`Student not found with ${identifierType}: ${identifier}. Ensure the student exists in OneRoster. If this is a new student, they must be created in OneRoster first.`);
62473
+ this.name = "StudentNotFoundError";
62474
+ this.identifier = identifier;
62475
+ this.identifierType = identifierType;
62476
+ Object.setPrototypeOf(this, StudentNotFoundError4.prototype);
62477
+ }
62478
+ };
62479
+ ConfigurationError3 = class ConfigurationError4 extends TimebackError3 {
62480
+ field;
62481
+ constructor(field, message) {
62482
+ super(message || `Missing required configuration: ${field}. Please check your timeback.config.js file.`);
62483
+ this.name = "ConfigurationError";
62484
+ this.field = field;
62485
+ Object.setPrototypeOf(this, ConfigurationError4.prototype);
62486
+ }
62487
+ };
62488
+ ResourceAlreadyExistsError3 = class ResourceAlreadyExistsError4 extends TimebackError3 {
62489
+ resourceType;
62490
+ sourcedId;
62491
+ originalError;
62492
+ constructor(resourceType, sourcedId, originalError) {
62493
+ super(`${resourceType} with ID '${sourcedId}' already exists`);
62494
+ this.name = "ResourceAlreadyExistsError";
62495
+ this.resourceType = resourceType;
62496
+ this.sourcedId = sourcedId;
62497
+ this.originalError = originalError;
62498
+ Object.setPrototypeOf(this, ResourceAlreadyExistsError4.prototype);
62499
+ }
62500
+ };
62501
+ ResourceNotFoundError3 = class ResourceNotFoundError4 extends TimebackError3 {
62502
+ resourceType;
62503
+ sourcedId;
62504
+ constructor(resourceType, sourcedId) {
62505
+ const message = sourcedId === "setup-not-run" ? `TimeBack resources have not been created yet. Please run 'bunx @playcademy/timeback setup' to create the required resources (organization, course, components, etc.) before recording progress.` : `${resourceType} with ID '${sourcedId}' not found`;
62506
+ super(message);
62507
+ this.name = "ResourceNotFoundError";
62508
+ this.resourceType = resourceType;
62509
+ this.sourcedId = sourcedId;
62510
+ Object.setPrototypeOf(this, ResourceNotFoundError4.prototype);
62511
+ }
62512
+ };
62513
+ });
62514
+ init_errors7();
62259
62515
  });
62260
62516
 
62261
62517
  class TimebackAssessmentsService {
@@ -62266,7 +62522,7 @@ class TimebackAssessmentsService {
62266
62522
  async resolveIntegrationId(gameId, courseId, user) {
62267
62523
  await this.deps.validateGameManagementAccess(user, gameId);
62268
62524
  const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
62269
- where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
62525
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId), isActiveGameTimebackIntegrationStatus())
62270
62526
  });
62271
62527
  if (!integration) {
62272
62528
  throw new NotFoundError(`No Timeback integration found for game ${gameId} course ${courseId}`);
@@ -62359,7 +62615,7 @@ class TimebackAssessmentsService {
62359
62615
  ]
62360
62616
  });
62361
62617
  } catch (error) {
62362
- if (error instanceof TimebackApiError2 && error.status === 409) {} else {
62618
+ if (error instanceof TimebackApiError3 && error.status === 409) {} else {
62363
62619
  throw error;
62364
62620
  }
62365
62621
  }
@@ -62659,7 +62915,7 @@ class TimebackAssessmentsService {
62659
62915
  }
62660
62916
  async requireIntegration(integrationId) {
62661
62917
  const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
62662
- where: eq(gameTimebackIntegrations.id, integrationId)
62918
+ where: and(eq(gameTimebackIntegrations.id, integrationId), isActiveGameTimebackIntegrationStatus())
62663
62919
  });
62664
62920
  if (!integration) {
62665
62921
  throw new NotFoundError(`Integration not found: ${integrationId}`);
@@ -62690,7 +62946,7 @@ class TimebackAssessmentsService {
62690
62946
  }
62691
62947
  });
62692
62948
  } catch (error) {
62693
- if (!(error instanceof TimebackApiError2 && error.status === 409)) {
62949
+ if (!(error instanceof TimebackApiError3 && error.status === 409)) {
62694
62950
  throw error;
62695
62951
  }
62696
62952
  }
@@ -62712,7 +62968,7 @@ class TimebackAssessmentsService {
62712
62968
  }
62713
62969
  });
62714
62970
  } catch (error) {
62715
- if (!(error instanceof TimebackApiError2 && error.status === 409)) {
62971
+ if (!(error instanceof TimebackApiError3 && error.status === 409)) {
62716
62972
  throw error;
62717
62973
  }
62718
62974
  }
@@ -62730,7 +62986,7 @@ class TimebackAssessmentsService {
62730
62986
  }
62731
62987
  });
62732
62988
  } catch (error) {
62733
- if (!(error instanceof TimebackApiError2 && error.status === 409)) {
62989
+ if (!(error instanceof TimebackApiError3 && error.status === 409)) {
62734
62990
  throw error;
62735
62991
  }
62736
62992
  }
@@ -62748,13 +63004,60 @@ class TimebackAssessmentsService {
62748
63004
  }
62749
63005
  var init_timeback_assessments_service = __esm(() => {
62750
63006
  init_drizzle_orm();
63007
+ init_helpers_index();
62751
63008
  init_tables_index();
62752
63009
  init_spans();
62753
- init_constants5();
62754
- init_errors4();
63010
+ init_constants6();
63011
+ init_errors6();
62755
63012
  init_utils6();
62756
63013
  init_errors();
62757
63014
  });
63015
+ function buildTimebackBaseConfigFromExistingConfig(config2) {
63016
+ return {
63017
+ organization: config2.organization,
63018
+ component: {
63019
+ ...config2.component,
63020
+ title: ""
63021
+ },
63022
+ resource: {
63023
+ ...config2.resource,
63024
+ title: ""
63025
+ },
63026
+ componentResource: {
63027
+ ...config2.componentResource,
63028
+ title: ""
63029
+ }
63030
+ };
63031
+ }
63032
+ function getMasterableUnitsFromTimebackConfig(config2) {
63033
+ const playcademyMetadata = config2.resource.metadata?.playcademy;
63034
+ if (!isPlaycademyResourceMetadata(playcademyMetadata)) {
63035
+ return null;
63036
+ }
63037
+ return playcademyMetadata?.mastery?.masterableUnits ?? null;
63038
+ }
63039
+ function getTotalXpFromTimebackConfig(config2) {
63040
+ const courseMetadata = isCourseMetadata(config2.course.metadata) ? config2.course.metadata : undefined;
63041
+ if (typeof courseMetadata?.metrics?.totalXp === "number") {
63042
+ return courseMetadata.metrics.totalXp;
63043
+ }
63044
+ const resourceMetadata = config2.resource.metadata;
63045
+ if (isRecord2(resourceMetadata) && typeof resourceMetadata.xp === "number") {
63046
+ return resourceMetadata.xp;
63047
+ }
63048
+ return null;
63049
+ }
63050
+ function timebackConfigMatchesCreateIntegrationRequest(config2, request2) {
63051
+ const subject = config2.course.subjects[0];
63052
+ const grade = config2.course.grades[0];
63053
+ const requestedLevel = request2.level ?? deriveTimebackCourseLevelFromGrade(request2.grade);
63054
+ return subject === request2.subject && grade === request2.grade && config2.course.title === request2.title && config2.course.courseCode === request2.courseCode && config2.course.level === requestedLevel && (getTotalXpFromTimebackConfig(config2) ?? null) === request2.totalXp && (getMasterableUnitsFromTimebackConfig(config2) ?? null) === request2.masterableUnits;
63055
+ }
63056
+ var init_timeback_create_integration_util = __esm(() => {
63057
+ init_constants6();
63058
+ init_types2();
63059
+ init_timeback_util();
63060
+ });
62758
63061
  async function promoteCompletedCourse({
62759
63062
  db: db2,
62760
63063
  client,
@@ -62763,7 +63066,7 @@ async function promoteCompletedCourse({
62763
63066
  enrollments: prefetchedEnrollments
62764
63067
  }) {
62765
63068
  const subjectIntegrations = await db2.query.gameTimebackIntegrations.findMany({
62766
- where: and(eq(gameTimebackIntegrations.gameId, currentIntegration.gameId), eq(gameTimebackIntegrations.subject, currentIntegration.subject))
63069
+ where: and(eq(gameTimebackIntegrations.gameId, currentIntegration.gameId), eq(gameTimebackIntegrations.subject, currentIntegration.subject), isActiveGameTimebackIntegrationStatus())
62767
63070
  });
62768
63071
  const nextIntegration = subjectIntegrations.filter((integration) => integration.grade > currentIntegration.grade).toSorted((left, right) => left.grade - right.grade)[0];
62769
63072
  if (!nextIntegration) {
@@ -62818,19 +63121,68 @@ async function promoteCompletedCourse({
62818
63121
  }
62819
63122
  var init_timeback_promotion_util = __esm(() => {
62820
63123
  init_drizzle_orm();
63124
+ init_helpers_index();
62821
63125
  init_tables_index();
62822
63126
  init_spans();
62823
63127
  });
63128
+ function toGameTimebackIntegration(integration) {
63129
+ return {
63130
+ id: integration.id,
63131
+ gameId: integration.gameId,
63132
+ courseId: integration.courseId,
63133
+ grade: integration.grade,
63134
+ subject: integration.subject,
63135
+ totalXp: integration.totalXp ?? null,
63136
+ status: integration.status ?? ACTIVE_GAME_TIMEBACK_INTEGRATION_STATUS,
63137
+ deactivatedAt: integration.deactivatedAt ?? null,
63138
+ reactivatedAt: integration.reactivatedAt ?? null,
63139
+ createdAt: integration.createdAt,
63140
+ updatedAt: integration.updatedAt,
63141
+ lastVerifiedAt: integration.lastVerifiedAt ?? null
63142
+ };
63143
+ }
63144
+ function buildFallbackRemovedGameTimebackIntegration(integration) {
63145
+ if (!isTimebackSubject(integration.subject)) {
63146
+ throw new ValidationError(`Invalid subject "${integration.subject}"`);
63147
+ }
63148
+ if (!isTimebackGrade(integration.grade)) {
63149
+ throw new ValidationError(`Invalid grade "${integration.grade}"`);
63150
+ }
63151
+ return {
63152
+ integration: toGameTimebackIntegration(integration),
63153
+ title: `${integration.subject} ${formatGradeLabel(integration.grade)}`,
63154
+ courseCode: integration.courseId,
63155
+ subject: integration.subject,
63156
+ grade: integration.grade,
63157
+ totalXp: integration.totalXp ?? null,
63158
+ masterableUnits: null,
63159
+ removedAt: integration.deactivatedAt ?? null,
63160
+ metadata: null
63161
+ };
63162
+ }
63163
+ var init_timeback_removed_integration_util = __esm(() => {
63164
+ init_helpers_index();
63165
+ init_types2();
63166
+ init_timeback3();
63167
+ init_errors();
63168
+ });
63169
+ async function findGameTimebackIntegrationForUpdate(db2, condition) {
63170
+ const [integration] = await db2.select().from(gameTimebackIntegrations).where(condition).limit(1).for("update");
63171
+ return integration;
63172
+ }
62824
63173
  var TimebackService;
62825
63174
  var init_timeback_service = __esm(() => {
62826
63175
  init_drizzle_orm();
62827
63176
  init_src();
63177
+ init_helpers_index();
62828
63178
  init_tables_index();
62829
63179
  init_spans();
62830
63180
  init_dist2();
62831
- init_types3();
63181
+ init_types2();
62832
63182
  init_errors();
63183
+ init_timeback_create_integration_util();
62833
63184
  init_timeback_promotion_util();
63185
+ init_timeback_removed_integration_util();
62834
63186
  init_timeback_util();
62835
63187
  TimebackService = class TimebackService2 {
62836
63188
  static HEARTBEAT_DEDUPE_TTL_MS = 300000;
@@ -63060,7 +63412,7 @@ var init_timeback_service = __esm(() => {
63060
63412
  return [];
63061
63413
  }
63062
63414
  const integrations = await db2.query.gameTimebackIntegrations.findMany({
63063
- where: inArray(gameTimebackIntegrations.courseId, courseIds)
63415
+ where: and(inArray(gameTimebackIntegrations.courseId, courseIds), isActiveGameTimebackIntegrationStatus())
63064
63416
  });
63065
63417
  return mapEnrollmentsToUserEnrollments(enrollments, integrations);
63066
63418
  } catch (error) {
@@ -63085,6 +63437,7 @@ var init_timeback_service = __esm(() => {
63085
63437
  const verboseData = [];
63086
63438
  let integrationCreatedCount = 0;
63087
63439
  let integrationUpdatedCount = 0;
63440
+ let integrationReactivatedCount = 0;
63088
63441
  for (const courseConfig of courses) {
63089
63442
  let applySuffix = function(text3) {
63090
63443
  return suffix ? `${text3} ${suffix}` : text3;
@@ -63099,16 +63452,16 @@ var init_timeback_service = __esm(() => {
63099
63452
  totalXp: derivedTotalXp,
63100
63453
  masterableUnits: derivedMasterableUnits
63101
63454
  } = courseConfig;
63102
- if (!isTimebackSubject3(subjectInput)) {
63455
+ if (!isTimebackSubject(subjectInput)) {
63103
63456
  throw new ValidationError(`Invalid subject "${subjectInput}"`);
63104
63457
  }
63105
- if (!isTimebackGrade3(grade)) {
63458
+ if (!isTimebackGrade(grade)) {
63106
63459
  throw new ValidationError(`Invalid grade "${grade}"`);
63107
63460
  }
63108
63461
  const subject = subjectInput;
63109
63462
  const courseMetadata = isCourseMetadata(metadata2) ? metadata2 : undefined;
63110
63463
  const totalXp = derivedTotalXp ?? courseMetadata?.metrics?.totalXp;
63111
- const masterableUnits = derivedMasterableUnits ?? (isPlaycademyResourceMetadata2(courseMetadata?.playcademy) ? courseMetadata?.playcademy?.mastery?.masterableUnits : undefined);
63464
+ const masterableUnits = derivedMasterableUnits ?? (isPlaycademyResourceMetadata(courseMetadata?.playcademy) ? courseMetadata?.playcademy?.mastery?.masterableUnits : undefined);
63112
63465
  if (typeof totalXp !== "number") {
63113
63466
  throw new ValidationError(`Course "${title}" is missing totalXp`);
63114
63467
  }
@@ -63154,19 +63507,28 @@ var init_timeback_service = __esm(() => {
63154
63507
  title: applySuffix(baseConfig.componentResource.title || "")
63155
63508
  }
63156
63509
  };
63157
- const existingIntegration = existing.find((i2) => i2.grade === grade && i2.subject === subject);
63510
+ const matches = existing.filter((i2) => i2.grade === grade && i2.subject === subject);
63511
+ const existingIntegration = matches.find((i2) => i2.status !== DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS) ?? matches.toSorted((a, b) => (b.deactivatedAt?.getTime() ?? 0) - (a.deactivatedAt?.getTime() ?? 0))[0];
63158
63512
  if (existingIntegration) {
63159
- await client.update(existingIntegration.courseId, fullConfig);
63160
- const [updated] = await db2.update(gameTimebackIntegrations).set({ subject, totalXp, updatedAt: new Date }).where(eq(gameTimebackIntegrations.id, existingIntegration.id)).returning();
63513
+ const { updated, revived } = await this.updateSetupIntegration({
63514
+ client,
63515
+ existingIntegration,
63516
+ fullConfig,
63517
+ subject,
63518
+ totalXp
63519
+ });
63161
63520
  if (updated) {
63162
- integrations.push(this.toGameTimebackIntegration(updated));
63521
+ integrations.push(toGameTimebackIntegration(updated));
63163
63522
  integrationUpdatedCount++;
63523
+ if (revived) {
63524
+ integrationReactivatedCount++;
63525
+ }
63164
63526
  }
63165
63527
  } else {
63166
63528
  const result = await client.setup(fullConfig, { verbose });
63167
63529
  const [integration] = await db2.insert(gameTimebackIntegrations).values({ gameId, courseId: result.courseId, grade, subject, totalXp }).returning();
63168
63530
  if (integration) {
63169
- const dto = this.toGameTimebackIntegration(integration);
63531
+ const dto = toGameTimebackIntegration(integration);
63170
63532
  integrations.push(dto);
63171
63533
  integrationCreatedCount++;
63172
63534
  if (verbose && result.verboseData) {
@@ -63178,7 +63540,8 @@ var init_timeback_service = __esm(() => {
63178
63540
  setAttributes({
63179
63541
  "app.timeback.integration_count": integrations.length,
63180
63542
  "app.timeback.integration_created_count": integrationCreatedCount,
63181
- "app.timeback.integration_updated_count": integrationUpdatedCount
63543
+ "app.timeback.integration_updated_count": integrationUpdatedCount,
63544
+ "app.timeback.integration_reactivated_count": integrationReactivatedCount
63182
63545
  });
63183
63546
  return {
63184
63547
  integrations,
@@ -63186,19 +63549,221 @@ var init_timeback_service = __esm(() => {
63186
63549
  };
63187
63550
  });
63188
63551
  }
63552
+ async updateSetupIntegration(args2) {
63553
+ const { client, existingIntegration, fullConfig, subject, totalXp } = args2;
63554
+ const revived = existingIntegration.status === DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS;
63555
+ await client.update(existingIntegration.courseId, fullConfig);
63556
+ if (revived) {
63557
+ await client.reactivateCourse(existingIntegration.courseId);
63558
+ }
63559
+ const now2 = new Date;
63560
+ const [updated] = await this.deps.db.update(gameTimebackIntegrations).set({
63561
+ subject,
63562
+ totalXp,
63563
+ updatedAt: now2,
63564
+ ...revived && {
63565
+ status: ACTIVE_GAME_TIMEBACK_INTEGRATION_STATUS,
63566
+ deactivatedAt: null,
63567
+ reactivatedAt: now2
63568
+ }
63569
+ }).where(eq(gameTimebackIntegrations.id, existingIntegration.id)).returning();
63570
+ return { updated, revived };
63571
+ }
63572
+ async createIntegration(gameId, user, request2) {
63573
+ return this.withClientTelemetry(async () => {
63574
+ const client = this.requireClient();
63575
+ const db2 = this.deps.db;
63576
+ await this.deps.validateDeveloperAccess(user, gameId);
63577
+ setAttributes({
63578
+ "app.timeback.grade": request2.grade,
63579
+ "app.timeback.subject": request2.subject
63580
+ });
63581
+ const activeConflict = await db2.query.gameTimebackIntegrations.findFirst({
63582
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, request2.grade), eq(gameTimebackIntegrations.subject, request2.subject), isActiveGameTimebackIntegrationStatus())
63583
+ });
63584
+ if (activeConflict) {
63585
+ throw new ConflictError(`A TimeBack integration already exists for ${request2.subject} Grade ${request2.grade}`);
63586
+ }
63587
+ const removedCandidates = await db2.query.gameTimebackIntegrations.findMany({
63588
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, request2.grade), eq(gameTimebackIntegrations.subject, request2.subject), eq(gameTimebackIntegrations.status, DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS))
63589
+ });
63590
+ for (const candidate of removedCandidates) {
63591
+ const config2 = await client.getConfig(candidate.courseId).catch((error) => {
63592
+ addEvent("timeback.removed_integration_config_fetch_failed", {
63593
+ "app.game.id": gameId,
63594
+ "app.timeback.course_id": candidate.courseId,
63595
+ "exception.type": errorType(error),
63596
+ "app.error.message": errorMessage(error)
63597
+ });
63598
+ return null;
63599
+ });
63600
+ if (config2 && timebackConfigMatchesCreateIntegrationRequest(config2, request2)) {
63601
+ const updated = await db2.transaction(async (tx) => {
63602
+ const database = tx;
63603
+ const lockedCandidate = await findGameTimebackIntegrationForUpdate(database, eq(gameTimebackIntegrations.id, candidate.id));
63604
+ if (lockedCandidate?.status !== DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS) {
63605
+ throw new ConflictError(`A TimeBack integration already exists for ${request2.subject} Grade ${request2.grade}`);
63606
+ }
63607
+ const now2 = new Date;
63608
+ const [integration2] = await database.update(gameTimebackIntegrations).set({
63609
+ status: ACTIVE_GAME_TIMEBACK_INTEGRATION_STATUS,
63610
+ totalXp: request2.totalXp,
63611
+ deactivatedAt: null,
63612
+ reactivatedAt: now2,
63613
+ updatedAt: now2
63614
+ }).where(and(eq(gameTimebackIntegrations.id, candidate.id), eq(gameTimebackIntegrations.status, DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS))).returning();
63615
+ if (!integration2) {
63616
+ throw new NotFoundError("Timeback integration", candidate.id);
63617
+ }
63618
+ await client.reactivateCourse(candidate.courseId);
63619
+ return integration2;
63620
+ });
63621
+ setAttribute("app.timeback.course_create_mode", "reactivated-removed");
63622
+ return toGameTimebackIntegration(updated);
63623
+ }
63624
+ }
63625
+ const reference = await db2.query.gameTimebackIntegrations.findFirst({
63626
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveGameTimebackIntegrationStatus()),
63627
+ orderBy: (table8, { asc: asc2 }) => [asc2(table8.createdAt)]
63628
+ }) ?? await db2.query.gameTimebackIntegrations.findFirst({
63629
+ where: eq(gameTimebackIntegrations.gameId, gameId),
63630
+ orderBy: (table8, { asc: asc2 }) => [asc2(table8.createdAt)]
63631
+ });
63632
+ if (!reference) {
63633
+ throw new ValidationError("Run `playcademy timeback setup` before adding courses from the dashboard.");
63634
+ }
63635
+ const referenceConfig = await client.getConfig(reference.courseId);
63636
+ const result = await this.setupIntegration(gameId, {
63637
+ gameId,
63638
+ courses: [
63639
+ {
63640
+ title: request2.title,
63641
+ courseCode: request2.courseCode,
63642
+ subject: request2.subject,
63643
+ grade: request2.grade,
63644
+ level: request2.level ?? deriveTimebackCourseLevelFromGrade(request2.grade),
63645
+ totalXp: request2.totalXp,
63646
+ masterableUnits: request2.masterableUnits
63647
+ }
63648
+ ],
63649
+ baseConfig: buildTimebackBaseConfigFromExistingConfig(referenceConfig)
63650
+ }, user);
63651
+ const integration = result.integrations[0];
63652
+ if (!integration) {
63653
+ throw new InternalError("TimeBack course creation did not return an integration");
63654
+ }
63655
+ setAttribute("app.timeback.course_create_mode", "created-new");
63656
+ return integration;
63657
+ });
63658
+ }
63189
63659
  async getIntegrations(gameId, user) {
63190
63660
  await this.deps.validateGameManagementAccess(user, gameId);
63191
63661
  const rows = await this.deps.db.query.gameTimebackIntegrations.findMany({
63192
- where: eq(gameTimebackIntegrations.gameId, gameId)
63662
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveGameTimebackIntegrationStatus())
63663
+ });
63664
+ return rows.map((row) => toGameTimebackIntegration(row));
63665
+ }
63666
+ async getRemovedIntegrations(gameId, user) {
63667
+ return this.withClientTelemetry(async () => {
63668
+ const client = this.requireClient();
63669
+ await this.deps.validateGameManagementAccess(user, gameId);
63670
+ const rows = await this.deps.db.query.gameTimebackIntegrations.findMany({
63671
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.status, DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS)),
63672
+ orderBy: (table8, { asc: asc2 }) => [asc2(table8.subject), asc2(table8.grade)]
63673
+ });
63674
+ setAttribute("app.timeback.removed_integration_count", rows.length);
63675
+ return Promise.all(rows.map(async (row) => {
63676
+ try {
63677
+ const config2 = await client.getConfig(row.courseId);
63678
+ return this.toRemovedGameTimebackIntegration(row, config2);
63679
+ } catch (error) {
63680
+ addEvent("timeback.removed_integration_config_fetch_failed", {
63681
+ "app.game.id": gameId,
63682
+ "app.timeback.course_id": row.courseId,
63683
+ "exception.type": errorType(error),
63684
+ "app.error.message": errorMessage(error)
63685
+ });
63686
+ return buildFallbackRemovedGameTimebackIntegration(row);
63687
+ }
63688
+ }));
63689
+ });
63690
+ }
63691
+ async deactivateCourse(gameId, courseId, user) {
63692
+ return this.withClientTelemetry(async () => {
63693
+ const client = this.requireClient();
63694
+ const db2 = this.deps.db;
63695
+ await this.deps.validateDeveloperAccess(user, gameId);
63696
+ const updated = await db2.transaction(async (tx) => {
63697
+ const database = tx;
63698
+ const integration = await findGameTimebackIntegrationForUpdate(database, and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId), isActiveGameTimebackIntegrationStatus()));
63699
+ if (!integration) {
63700
+ throw new NotFoundError("Timeback course", `${gameId}:${courseId}`);
63701
+ }
63702
+ const activeEnrollments = await client.oneroster.enrollments.listByCourse(courseId, {
63703
+ role: "student",
63704
+ includeUsers: false
63705
+ });
63706
+ setAttribute("app.timeback.active_enrollment_count", activeEnrollments.length);
63707
+ if (activeEnrollments.length > 0) {
63708
+ throw new ConflictError("Cannot remove course with active enrollments", {
63709
+ courseId,
63710
+ activeEnrollmentCount: activeEnrollments.length
63711
+ });
63712
+ }
63713
+ const now2 = new Date;
63714
+ const [updatedIntegration] = await database.update(gameTimebackIntegrations).set({
63715
+ status: DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS,
63716
+ deactivatedAt: now2,
63717
+ updatedAt: now2
63718
+ }).where(and(eq(gameTimebackIntegrations.id, integration.id), isActiveGameTimebackIntegrationStatus())).returning();
63719
+ if (!updatedIntegration) {
63720
+ throw new NotFoundError("Timeback course", `${gameId}:${courseId}`);
63721
+ }
63722
+ await client.deactivateCourse(courseId);
63723
+ return updatedIntegration;
63724
+ });
63725
+ return toGameTimebackIntegration(updated);
63726
+ });
63727
+ }
63728
+ async reactivateCourse(gameId, courseId, user) {
63729
+ return this.withClientTelemetry(async () => {
63730
+ const client = this.requireClient();
63731
+ const db2 = this.deps.db;
63732
+ await this.deps.validateDeveloperAccess(user, gameId);
63733
+ const updated = await db2.transaction(async (tx) => {
63734
+ const database = tx;
63735
+ const integration = await findGameTimebackIntegrationForUpdate(database, and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId), eq(gameTimebackIntegrations.status, DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS)));
63736
+ if (!integration) {
63737
+ throw new NotFoundError("Removed Timeback course", `${gameId}:${courseId}`);
63738
+ }
63739
+ const activeConflict = await database.query.gameTimebackIntegrations.findFirst({
63740
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, integration.grade), eq(gameTimebackIntegrations.subject, integration.subject), isActiveGameTimebackIntegrationStatus())
63741
+ });
63742
+ if (activeConflict) {
63743
+ throw new ConflictError(`An active TimeBack course already exists for ${integration.subject} Grade ${integration.grade}`);
63744
+ }
63745
+ const now2 = new Date;
63746
+ const [updatedIntegration] = await database.update(gameTimebackIntegrations).set({
63747
+ status: ACTIVE_GAME_TIMEBACK_INTEGRATION_STATUS,
63748
+ deactivatedAt: null,
63749
+ reactivatedAt: now2,
63750
+ updatedAt: now2
63751
+ }).where(and(eq(gameTimebackIntegrations.id, integration.id), eq(gameTimebackIntegrations.status, DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS))).returning();
63752
+ if (!updatedIntegration) {
63753
+ throw new NotFoundError("Timeback course", `${gameId}:${courseId}`);
63754
+ }
63755
+ await client.reactivateCourse(courseId);
63756
+ return updatedIntegration;
63757
+ });
63758
+ return toGameTimebackIntegration(updated);
63193
63759
  });
63194
- return rows.map((row) => this.toGameTimebackIntegration(row));
63195
63760
  }
63196
63761
  async getIntegrationConfig(gameId, courseId, user) {
63197
63762
  return this.withClientTelemetry(async () => {
63198
63763
  const client = this.requireClient();
63199
63764
  await this.deps.validateGameManagementAccess(user, gameId);
63200
63765
  const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
63201
- where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
63766
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId), isActiveGameTimebackIntegrationStatus())
63202
63767
  });
63203
63768
  if (!integration) {
63204
63769
  throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
@@ -63215,15 +63780,15 @@ var init_timeback_service = __esm(() => {
63215
63780
  "app.timeback.course_id": courseId
63216
63781
  });
63217
63782
  const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
63218
- where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
63783
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId), isActiveGameTimebackIntegrationStatus())
63219
63784
  });
63220
63785
  if (!integration) {
63221
63786
  throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
63222
63787
  }
63223
63788
  const timebackConfig = await client.getConfig(courseId);
63224
63789
  const liveSubject = timebackConfig.course.subjects[0];
63225
- const subject = patch.subject ?? (isTimebackSubject3(liveSubject) ? liveSubject : integration.subject);
63226
- if (!isTimebackSubject3(subject)) {
63790
+ const subject = patch.subject ?? (isTimebackSubject(liveSubject) ? liveSubject : integration.subject);
63791
+ if (!isTimebackSubject(subject)) {
63227
63792
  throw new ValidationError(`Invalid subject "${subject}"`);
63228
63793
  }
63229
63794
  setAttributes({
@@ -63233,14 +63798,14 @@ var init_timeback_service = __esm(() => {
63233
63798
  });
63234
63799
  if (subject !== integration.subject) {
63235
63800
  const subjectConflict = await this.deps.db.query.gameTimebackIntegrations.findFirst({
63236
- where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, integration.grade), eq(gameTimebackIntegrations.subject, subject))
63801
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, integration.grade), eq(gameTimebackIntegrations.subject, subject), isActiveGameTimebackIntegrationStatus())
63237
63802
  });
63238
63803
  if (subjectConflict && subjectConflict.id !== integration.id) {
63239
63804
  throw new ValidationError(`A TimeBack integration already exists for ${subject} Grade ${integration.grade}`);
63240
63805
  }
63241
63806
  }
63242
- const totalXp = "totalXp" in patch ? patch.totalXp ?? null : TimebackService2.getTotalXpFromConfig(timebackConfig) ?? integration.totalXp ?? null;
63243
- const masterableUnits = "masterableUnits" in patch ? patch.masterableUnits ?? null : TimebackService2.getMasterableUnitsFromConfig(timebackConfig);
63807
+ const totalXp = "totalXp" in patch ? patch.totalXp ?? null : getTotalXpFromTimebackConfig(timebackConfig) ?? integration.totalXp ?? null;
63808
+ const masterableUnits = "masterableUnits" in patch ? patch.masterableUnits ?? null : getMasterableUnitsFromTimebackConfig(timebackConfig);
63244
63809
  setAttributes({
63245
63810
  "app.timeback.total_xp": totalXp,
63246
63811
  "app.timeback.masterable_units": masterableUnits
@@ -63260,7 +63825,7 @@ var init_timeback_service = __esm(() => {
63260
63825
  if (!updated) {
63261
63826
  throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
63262
63827
  }
63263
- return this.toGameTimebackIntegration(updated);
63828
+ return toGameTimebackIntegration(updated);
63264
63829
  });
63265
63830
  }
63266
63831
  async verifyIntegration(gameId, user) {
@@ -63270,7 +63835,7 @@ var init_timeback_service = __esm(() => {
63270
63835
  await this.deps.validateDeveloperAccess(user, gameId);
63271
63836
  TimebackService2.recordIntegrationOperation("verify_integration");
63272
63837
  const integrations = await db2.query.gameTimebackIntegrations.findMany({
63273
- where: eq(gameTimebackIntegrations.gameId, gameId)
63838
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveGameTimebackIntegrationStatus())
63274
63839
  });
63275
63840
  if (integrations.length === 0) {
63276
63841
  throw new NotFoundError("Timeback integration", gameId);
@@ -63283,7 +63848,7 @@ var init_timeback_service = __esm(() => {
63283
63848
  const errors3 = Object.entries(resources).filter(([_2, r]) => !r.found).map(([name3]) => `${name3} not found`);
63284
63849
  const status = allFound ? "success" : "error";
63285
63850
  return {
63286
- integration: this.toGameTimebackIntegration({
63851
+ integration: toGameTimebackIntegration({
63287
63852
  ...integration,
63288
63853
  lastVerifiedAt: now2
63289
63854
  }),
@@ -63292,7 +63857,7 @@ var init_timeback_service = __esm(() => {
63292
63857
  ...errors3.length > 0 && { errors: errors3 }
63293
63858
  };
63294
63859
  }));
63295
- await db2.update(gameTimebackIntegrations).set({ lastVerifiedAt: now2 }).where(eq(gameTimebackIntegrations.gameId, gameId));
63860
+ await db2.update(gameTimebackIntegrations).set({ lastVerifiedAt: now2 }).where(inArray(gameTimebackIntegrations.id, integrations.map((integration) => integration.id)));
63296
63861
  const overallStatus = results.every((r) => r.status === "success") ? "success" : "error";
63297
63862
  const missingResourceCount = results.reduce((count, result) => count + (result.errors?.length ?? 0), 0);
63298
63863
  setAttributes({
@@ -63308,7 +63873,7 @@ var init_timeback_service = __esm(() => {
63308
63873
  const client = this.requireClient();
63309
63874
  await this.deps.validateDeveloperAccess(user, gameId);
63310
63875
  const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
63311
- where: eq(gameTimebackIntegrations.gameId, gameId)
63876
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), isActiveGameTimebackIntegrationStatus())
63312
63877
  });
63313
63878
  if (!integration) {
63314
63879
  throw new NotFoundError("Timeback integration", gameId);
@@ -63335,24 +63900,6 @@ var init_timeback_service = __esm(() => {
63335
63900
  await db2.delete(gameTimebackIntegrations).where(eq(gameTimebackIntegrations.gameId, gameId));
63336
63901
  });
63337
63902
  }
63338
- static getMasterableUnitsFromConfig(config2) {
63339
- const playcademyMetadata = config2.resource.metadata?.playcademy;
63340
- if (!isPlaycademyResourceMetadata2(playcademyMetadata)) {
63341
- return null;
63342
- }
63343
- return playcademyMetadata?.mastery?.masterableUnits ?? null;
63344
- }
63345
- static getTotalXpFromConfig(config2) {
63346
- const courseMetadata = isCourseMetadata(config2.course.metadata) ? config2.course.metadata : undefined;
63347
- if (typeof courseMetadata?.metrics?.totalXp === "number") {
63348
- return courseMetadata.metrics.totalXp;
63349
- }
63350
- const resourceMetadata = config2.resource.metadata;
63351
- if (isRecord2(resourceMetadata) && typeof resourceMetadata.xp === "number") {
63352
- return resourceMetadata.xp;
63353
- }
63354
- return null;
63355
- }
63356
63903
  static patchCourseMetadata(metadata2, totalXp, options) {
63357
63904
  const nextMetadata = isRecord2(metadata2) ? { ...metadata2 } : {};
63358
63905
  const currentMetrics = isRecord2(nextMetadata.metrics) ? nextMetadata.metrics : {};
@@ -63475,31 +64022,29 @@ var init_timeback_service = __esm(() => {
63475
64022
  }
63476
64023
  };
63477
64024
  }
63478
- toGameTimebackIntegration(integration) {
64025
+ toRemovedGameTimebackIntegration(integration, config2) {
64026
+ const grade = config2.course.grades[0] ?? integration.grade;
64027
+ if (!isTimebackGrade(grade)) {
64028
+ throw new ValidationError(`Invalid grade "${grade}"`);
64029
+ }
63479
64030
  return {
63480
- id: integration.id,
63481
- gameId: integration.gameId,
63482
- courseId: integration.courseId,
63483
- grade: integration.grade,
63484
- subject: integration.subject,
63485
- totalXp: integration.totalXp ?? null,
63486
- createdAt: integration.createdAt,
63487
- updatedAt: integration.updatedAt,
63488
- lastVerifiedAt: integration.lastVerifiedAt ?? null
64031
+ ...this.toGameTimebackIntegrationConfig(integration, config2),
64032
+ grade,
64033
+ removedAt: integration.deactivatedAt ?? null
63489
64034
  };
63490
64035
  }
63491
64036
  toGameTimebackIntegrationConfig(integration, config2) {
63492
64037
  const subject = config2.course.subjects[0] ?? integration.subject;
63493
- if (!isTimebackSubject3(subject)) {
64038
+ if (!isTimebackSubject(subject)) {
63494
64039
  throw new ValidationError(`Invalid subject "${subject}"`);
63495
64040
  }
63496
64041
  return {
63497
- integration: this.toGameTimebackIntegration(integration),
64042
+ integration: toGameTimebackIntegration(integration),
63498
64043
  title: config2.course.title,
63499
64044
  courseCode: config2.course.courseCode,
63500
64045
  subject,
63501
- totalXp: TimebackService2.getTotalXpFromConfig(config2) ?? integration.totalXp ?? null,
63502
- masterableUnits: TimebackService2.getMasterableUnitsFromConfig(config2),
64046
+ totalXp: getTotalXpFromTimebackConfig(config2) ?? integration.totalXp ?? null,
64047
+ masterableUnits: getMasterableUnitsFromTimebackConfig(config2),
63503
64048
  metadata: isCourseMetadata(config2.course.metadata) ? config2.course.metadata : null
63504
64049
  };
63505
64050
  }
@@ -63531,7 +64076,7 @@ var init_timeback_service = __esm(() => {
63531
64076
  });
63532
64077
  await this.deps.validateDeveloperAccess(user, gameId);
63533
64078
  const integration = await db2.query.gameTimebackIntegrations.findFirst({
63534
- where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject))
64079
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject), isActiveGameTimebackIntegrationStatus())
63535
64080
  });
63536
64081
  if (!integration) {
63537
64082
  throw new NotFoundError(`Timeback integration for game (grade ${activityData.grade}, subject ${activityData.subject})`);
@@ -63592,9 +64137,7 @@ var init_timeback_service = __esm(() => {
63592
64137
  "app.timeback.xp_awarded": result.xpAwarded,
63593
64138
  "app.timeback.mastered_units_requested": masteredUnits,
63594
64139
  "app.timeback.mastered_units_absolute_requested": masteredUnitsAbsolute,
63595
- "app.timeback.mastered_units_applied": result.masteredUnitsApplied,
63596
- "app.timeback.score_status": result.scoreStatus,
63597
- "app.timeback.in_progress": result.inProgress
64140
+ "app.timeback.mastered_units_applied": result.masteredUnitsApplied
63598
64141
  });
63599
64142
  return {
63600
64143
  status: "ok",
@@ -63602,8 +64145,6 @@ var init_timeback_service = __esm(() => {
63602
64145
  xpAwarded: result.xpAwarded,
63603
64146
  masteredUnits: result.masteredUnitsApplied,
63604
64147
  pctCompleteApp: result.pctCompleteApp,
63605
- scoreStatus: result.scoreStatus,
63606
- inProgress: result.inProgress,
63607
64148
  ...result.warnings ? { warnings: result.warnings } : {}
63608
64149
  };
63609
64150
  }
@@ -63616,7 +64157,7 @@ var init_timeback_service = __esm(() => {
63616
64157
  const client = this.requireClient();
63617
64158
  const db2 = this.deps.db;
63618
64159
  const integrations = await db2.query.gameTimebackIntegrations.findMany({
63619
- where: subject ? and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.subject, subject)) : eq(gameTimebackIntegrations.gameId, gameId)
64160
+ where: subject ? and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.subject, subject), isActiveGameTimebackIntegrationStatus()) : and(eq(gameTimebackIntegrations.gameId, gameId), isActiveGameTimebackIntegrationStatus())
63620
64161
  });
63621
64162
  if (integrations.length === 0) {
63622
64163
  throw new NotFoundError(subject ? `Timeback integrations for game (subject ${subject})` : "Timeback integrations for game");
@@ -63798,7 +64339,7 @@ var init_timeback_service = __esm(() => {
63798
64339
  }
63799
64340
  const pendingHeartbeat = (async () => {
63800
64341
  const integration = await db2.query.gameTimebackIntegrations.findFirst({
63801
- where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject))
64342
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject), isActiveGameTimebackIntegrationStatus())
63802
64343
  });
63803
64344
  if (!integration) {
63804
64345
  throw new NotFoundError(`Timeback integration for game (grade ${activityData.grade}, subject ${activityData.subject})`);
@@ -63846,7 +64387,10 @@ var init_timeback_service = __esm(() => {
63846
64387
  let courseIds = [];
63847
64388
  if (options?.gameId) {
63848
64389
  await this.deps.validateDeveloperAccess(user, options.gameId);
63849
- const conditions2 = [eq(gameTimebackIntegrations.gameId, options.gameId)];
64390
+ const conditions2 = [
64391
+ eq(gameTimebackIntegrations.gameId, options.gameId),
64392
+ isActiveGameTimebackIntegrationStatus()
64393
+ ];
63850
64394
  if (options.grade !== undefined && options.subject) {
63851
64395
  conditions2.push(eq(gameTimebackIntegrations.grade, options.grade));
63852
64396
  conditions2.push(eq(gameTimebackIntegrations.subject, options.subject));
@@ -63873,7 +64417,10 @@ var init_timeback_service = __esm(() => {
63873
64417
  const client = this.requireClient();
63874
64418
  const db2 = this.deps.db;
63875
64419
  await this.deps.validateDeveloperAccess(user, options.gameId);
63876
- const conditions2 = [eq(gameTimebackIntegrations.gameId, options.gameId)];
64420
+ const conditions2 = [
64421
+ eq(gameTimebackIntegrations.gameId, options.gameId),
64422
+ isActiveGameTimebackIntegrationStatus()
64423
+ ];
63877
64424
  if (options.grade !== undefined && options.subject) {
63878
64425
  conditions2.push(eq(gameTimebackIntegrations.grade, options.grade));
63879
64426
  conditions2.push(eq(gameTimebackIntegrations.subject, options.subject));
@@ -63900,7 +64447,7 @@ var init_timeback_service = __esm(() => {
63900
64447
  const db2 = this.deps.db;
63901
64448
  await this.deps.validateDeveloperAccess(user, options.gameId);
63902
64449
  const integration = await db2.query.gameTimebackIntegrations.findFirst({
63903
- where: and(eq(gameTimebackIntegrations.gameId, options.gameId), eq(gameTimebackIntegrations.subject, options.subject))
64450
+ where: and(eq(gameTimebackIntegrations.gameId, options.gameId), eq(gameTimebackIntegrations.subject, options.subject), isActiveGameTimebackIntegrationStatus())
63904
64451
  });
63905
64452
  if (!integration) {
63906
64453
  throw new ValidationError(`Subject "${options.subject}" is not configured for game ${options.gameId}`);
@@ -64767,7 +65314,7 @@ class UserService {
64767
65314
  return [];
64768
65315
  }
64769
65316
  const integrations = await this.deps.db.query.gameTimebackIntegrations.findMany({
64770
- where: inArray(gameTimebackIntegrations.courseId, courseIds)
65317
+ where: and(inArray(gameTimebackIntegrations.courseId, courseIds), isActiveGameTimebackIntegrationStatus())
64771
65318
  });
64772
65319
  return mapEnrollmentsToUserEnrollments(enrollments, integrations);
64773
65320
  } catch (error) {
@@ -64789,6 +65336,7 @@ class UserService {
64789
65336
  var init_user_service = __esm(() => {
64790
65337
  init_drizzle_orm();
64791
65338
  init_src();
65339
+ init_helpers_index();
64792
65340
  init_tables_index();
64793
65341
  init_spans();
64794
65342
  init_errors();
@@ -81189,7 +81737,7 @@ function extractTablesRelationalConfig2(schema5, configHelpers) {
81189
81737
  return { tables: tablesConfig, tableNamesMap };
81190
81738
  }
81191
81739
  function relations3(table62, relations22) {
81192
- return new Relations2(table62, (helpers) => Object.fromEntries(Object.entries(relations22(helpers)).map(([key, value]) => [
81740
+ return new Relations2(table62, (helpers3) => Object.fromEntries(Object.entries(relations22(helpers3)).map(([key, value]) => [
81193
81741
  key,
81194
81742
  value.withFieldName(key)
81195
81743
  ])));
@@ -82452,7 +83000,7 @@ var errorMap2;
82452
83000
  var en_default2;
82453
83001
  var init_en2;
82454
83002
  var overrideErrorMap2;
82455
- var init_errors5;
83003
+ var init_errors8;
82456
83004
  var makeIssue2;
82457
83005
  var ParseStatus2;
82458
83006
  var INVALID2;
@@ -88689,7 +89237,7 @@ See: https://github.com/isaacs/node-glob/issues/167`);
88689
89237
  en_default2 = errorMap2;
88690
89238
  }
88691
89239
  });
88692
- init_errors5 = __esm7({
89240
+ init_errors8 = __esm7({
88693
89241
  "../node_modules/.pnpm/zod@3.25.42/node_modules/zod/dist/esm/v3/errors.js"() {
88694
89242
  init_en2();
88695
89243
  overrideErrorMap2 = en_default2;
@@ -88697,7 +89245,7 @@ See: https://github.com/isaacs/node-glob/issues/167`);
88697
89245
  });
88698
89246
  init_parseUtil2 = __esm7({
88699
89247
  "../node_modules/.pnpm/zod@3.25.42/node_modules/zod/dist/esm/v3/helpers/parseUtil.js"() {
88700
- init_errors5();
89248
+ init_errors8();
88701
89249
  init_en2();
88702
89250
  makeIssue2 = (params) => {
88703
89251
  const { data, path: path22, errorMaps, issueData } = params;
@@ -88803,7 +89351,7 @@ See: https://github.com/isaacs/node-glob/issues/167`);
88803
89351
  init_types5 = __esm7({
88804
89352
  "../node_modules/.pnpm/zod@3.25.42/node_modules/zod/dist/esm/v3/types.js"() {
88805
89353
  init_ZodError2();
88806
- init_errors5();
89354
+ init_errors8();
88807
89355
  init_errorUtil2();
88808
89356
  init_parseUtil2();
88809
89357
  init_util2();
@@ -91961,7 +92509,7 @@ See: https://github.com/isaacs/node-glob/issues/167`);
91961
92509
  });
91962
92510
  init_external2 = __esm7({
91963
92511
  "../node_modules/.pnpm/zod@3.25.42/node_modules/zod/dist/esm/v3/external.js"() {
91964
- init_errors5();
92512
+ init_errors8();
91965
92513
  init_parseUtil2();
91966
92514
  init_typeAliases2();
91967
92515
  init_util2();
@@ -122615,7 +123163,7 @@ async function seedTimebackIntegrations(db2, gameId, courses) {
122615
123163
  const courseId = course.courseId || `mock-${course.subject.toLowerCase()}-g${course.grade}`;
122616
123164
  try {
122617
123165
  const existing = await db2.query.gameTimebackIntegrations.findFirst({
122618
- where: (table9, { and: and3, eq: eq3 }) => and3(eq3(table9.gameId, gameId), eq3(table9.grade, course.grade), eq3(table9.subject, course.subject))
123166
+ where: (table9, { and: and3, eq: eq3 }) => and3(eq3(table9.gameId, gameId), eq3(table9.grade, course.grade), eq3(table9.subject, course.subject), isActiveGameTimebackIntegrationStatus(table9.status))
122619
123167
  });
122620
123168
  if (!existing) {
122621
123169
  await db2.insert(gameTimebackIntegrations).values({
@@ -122635,6 +123183,7 @@ async function seedTimebackIntegrations(db2, gameId, courses) {
122635
123183
  return seededCount;
122636
123184
  }
122637
123185
  var init_timeback5 = __esm(() => {
123186
+ init_helpers_index();
122638
123187
  init_tables_index();
122639
123188
  init_config();
122640
123189
  });
@@ -123840,6 +124389,7 @@ var init_game_member_controller = __esm(() => {
123840
124389
  var list2;
123841
124390
  var listAccessible;
123842
124391
  var getSubjects;
124392
+ var getTimebackSummaries;
123843
124393
  var getById;
123844
124394
  var getBySlug;
123845
124395
  var getManifest;
@@ -123855,6 +124405,7 @@ var init_game_controller = __esm(() => {
123855
124405
  list2 = requireNonAnonymous(async (ctx) => ctx.services.game.list(ctx.user));
123856
124406
  listAccessible = requireNonAnonymous(async (ctx) => ctx.services.game.listAccessible(ctx.user));
123857
124407
  getSubjects = requireNonAnonymous(async (ctx) => ctx.services.game.getSubjects());
124408
+ getTimebackSummaries = requireGameManagementAccess(async (ctx) => ctx.services.game.getTimebackSummaries(ctx.user));
123858
124409
  getById = requireNonAnonymous(async (ctx) => {
123859
124410
  const gameId = requireGameId(ctx.params.gameId);
123860
124411
  return ctx.services.game.getById(gameId, ctx.user);
@@ -123902,6 +124453,7 @@ var init_game_controller = __esm(() => {
123902
124453
  list: list2,
123903
124454
  listAccessible,
123904
124455
  getSubjects,
124456
+ getTimebackSummaries,
123905
124457
  getById,
123906
124458
  getManifest,
123907
124459
  getBySlug,
@@ -124305,7 +124857,11 @@ var getUserEnrollments;
124305
124857
  var getUserById;
124306
124858
  var setupIntegration;
124307
124859
  var getIntegrations;
124860
+ var getRemovedIntegrations;
124861
+ var createIntegration;
124308
124862
  var updateIntegration;
124863
+ var deactivateCourse;
124864
+ var reactivateCourse;
124309
124865
  var getIntegrationConfig;
124310
124866
  var verifyIntegration;
124311
124867
  var getConfig;
@@ -124403,6 +124959,27 @@ var init_timeback_controller = __esm(() => {
124403
124959
  }
124404
124960
  return ctx.services.timeback.getIntegrations(gameId, ctx.user);
124405
124961
  });
124962
+ getRemovedIntegrations = requireGameManagementAccess(async (ctx) => {
124963
+ const gameId = ctx.params.gameId;
124964
+ if (!gameId) {
124965
+ throw ApiError.badRequest("Missing gameId");
124966
+ }
124967
+ if (!isValidUUID(gameId)) {
124968
+ throw ApiError.unprocessableEntity("Invalid gameId format");
124969
+ }
124970
+ return ctx.services.timeback.getRemovedIntegrations(gameId, ctx.user);
124971
+ });
124972
+ createIntegration = requireDeveloper(async (ctx) => {
124973
+ const gameId = ctx.params.gameId;
124974
+ if (!gameId) {
124975
+ throw ApiError.badRequest("Missing gameId");
124976
+ }
124977
+ if (!isValidUUID(gameId)) {
124978
+ throw ApiError.unprocessableEntity("Invalid gameId format");
124979
+ }
124980
+ const body2 = await parseRequestBody(ctx.request, CreateGameTimebackIntegrationRequestSchema);
124981
+ return ctx.services.timeback.createIntegration(gameId, ctx.user, body2);
124982
+ });
124406
124983
  updateIntegration = requireDeveloper(async (ctx) => {
124407
124984
  const { gameId, courseId } = ctx.params;
124408
124985
  if (!gameId || !courseId) {
@@ -124414,6 +124991,26 @@ var init_timeback_controller = __esm(() => {
124414
124991
  const body2 = await parseRequestBody(ctx.request, UpdateGameTimebackIntegrationRequestSchema);
124415
124992
  return ctx.services.timeback.updateIntegration(gameId, courseId, ctx.user, body2);
124416
124993
  });
124994
+ deactivateCourse = requireDeveloper(async (ctx) => {
124995
+ const { gameId, courseId } = ctx.params;
124996
+ if (!gameId || !courseId) {
124997
+ throw ApiError.badRequest("Missing gameId or courseId parameter");
124998
+ }
124999
+ if (!isValidUUID(gameId)) {
125000
+ throw ApiError.unprocessableEntity("Invalid gameId format");
125001
+ }
125002
+ return ctx.services.timeback.deactivateCourse(gameId, courseId, ctx.user);
125003
+ });
125004
+ reactivateCourse = requireDeveloper(async (ctx) => {
125005
+ const { gameId, courseId } = ctx.params;
125006
+ if (!gameId || !courseId) {
125007
+ throw ApiError.badRequest("Missing gameId or courseId parameter");
125008
+ }
125009
+ if (!isValidUUID(gameId)) {
125010
+ throw ApiError.unprocessableEntity("Invalid gameId format");
125011
+ }
125012
+ return ctx.services.timeback.reactivateCourse(gameId, courseId, ctx.user);
125013
+ });
124417
125014
  getIntegrationConfig = requireGameManagementAccess(async (ctx) => {
124418
125015
  const { gameId, courseId } = ctx.params;
124419
125016
  if (!gameId || !courseId) {
@@ -124563,11 +125160,11 @@ var init_timeback_controller = __esm(() => {
124563
125160
  let subject;
124564
125161
  if (gradeParam !== null && subjectParam !== null) {
124565
125162
  const parsedGrade = parseInt(gradeParam, 10);
124566
- if (!isTimebackGrade(parsedGrade)) {
125163
+ if (!isTimebackGrade2(parsedGrade)) {
124567
125164
  throw ApiError.badRequest(`Invalid grade: ${gradeParam}. Valid grades: ${TIMEBACK_GRADES.join(", ")}`);
124568
125165
  }
124569
- if (!isTimebackSubject(subjectParam)) {
124570
- throw ApiError.badRequest(`Invalid subject: ${subjectParam}. Valid subjects: ${TIMEBACK_SUBJECTS.join(", ")}`);
125166
+ if (!isTimebackSubject2(subjectParam)) {
125167
+ throw ApiError.badRequest(`Invalid subject: ${subjectParam}. Valid subjects: ${TIMEBACK_SUBJECTS2.join(", ")}`);
124571
125168
  }
124572
125169
  grade = parsedGrade;
124573
125170
  subject = subjectParam;
@@ -124603,11 +125200,11 @@ var init_timeback_controller = __esm(() => {
124603
125200
  let subject;
124604
125201
  if (gradeParam !== null && subjectParam !== null) {
124605
125202
  const parsedGrade = parseInt(gradeParam, 10);
124606
- if (!isTimebackGrade(parsedGrade)) {
125203
+ if (!isTimebackGrade2(parsedGrade)) {
124607
125204
  throw ApiError.badRequest(`Invalid grade: ${gradeParam}. Valid grades: ${TIMEBACK_GRADES.join(", ")}`);
124608
125205
  }
124609
- if (!isTimebackSubject(subjectParam)) {
124610
- throw ApiError.badRequest(`Invalid subject: ${subjectParam}. Valid subjects: ${TIMEBACK_SUBJECTS.join(", ")}`);
125206
+ if (!isTimebackSubject2(subjectParam)) {
125207
+ throw ApiError.badRequest(`Invalid subject: ${subjectParam}. Valid subjects: ${TIMEBACK_SUBJECTS2.join(", ")}`);
124611
125208
  }
124612
125209
  grade = parsedGrade;
124613
125210
  subject = subjectParam;
@@ -124637,8 +125234,8 @@ var init_timeback_controller = __esm(() => {
124637
125234
  if (!subjectParam) {
124638
125235
  throw ApiError.badRequest("Missing required subject query parameter");
124639
125236
  }
124640
- if (!isTimebackSubject(subjectParam)) {
124641
- throw ApiError.badRequest(`Invalid subject: ${subjectParam}. Valid subjects: ${TIMEBACK_SUBJECTS.join(", ")}`);
125237
+ if (!isTimebackSubject2(subjectParam)) {
125238
+ throw ApiError.badRequest(`Invalid subject: ${subjectParam}. Valid subjects: ${TIMEBACK_SUBJECTS2.join(", ")}`);
124642
125239
  }
124643
125240
  return ctx.services.timeback.getStudentHighestGradeMastered(timebackId, ctx.user, {
124644
125241
  gameId,
@@ -124949,7 +125546,11 @@ var init_timeback_controller = __esm(() => {
124949
125546
  getUserById,
124950
125547
  setupIntegration,
124951
125548
  getIntegrations,
125549
+ getRemovedIntegrations,
125550
+ createIntegration,
124952
125551
  updateIntegration,
125552
+ deactivateCourse,
125553
+ reactivateCourse,
124953
125554
  getIntegrationConfig,
124954
125555
  verifyIntegration,
124955
125556
  getConfig,
@@ -125909,12 +126510,12 @@ var init_timeback7 = __esm(() => {
125909
126510
  }
125910
126511
  if (gradeParam !== null && subjectParam !== null) {
125911
126512
  const grade = parseInt(gradeParam, 10);
125912
- if (!Number.isFinite(grade) || !isTimebackGrade(grade)) {
126513
+ if (!Number.isFinite(grade) || !isTimebackGrade2(grade)) {
125913
126514
  const error2 = ApiError.badRequest(`Invalid grade: ${gradeParam}. Valid grades: ${TIMEBACK_GRADES.join(", ")}`);
125914
126515
  return c2.json(createErrorResponse(error2), error2.status);
125915
126516
  }
125916
- if (!isTimebackSubject(subjectParam)) {
125917
- const error2 = ApiError.badRequest(`Invalid subject: ${subjectParam}. Valid subjects: ${TIMEBACK_SUBJECTS.join(", ")}`);
126517
+ if (!isTimebackSubject2(subjectParam)) {
126518
+ const error2 = ApiError.badRequest(`Invalid subject: ${subjectParam}. Valid subjects: ${TIMEBACK_SUBJECTS2.join(", ")}`);
125918
126519
  return c2.json(createErrorResponse(error2), error2.status);
125919
126520
  }
125920
126521
  enrollments = enrollments.filter((e) => e.grade === grade && e.subject === subjectParam);
@@ -125967,12 +126568,12 @@ var init_timeback7 = __esm(() => {
125967
126568
  }
125968
126569
  if (gradeParam !== null && subjectParam !== null) {
125969
126570
  const grade = parseInt(gradeParam, 10);
125970
- if (!Number.isFinite(grade) || !isTimebackGrade(grade)) {
126571
+ if (!Number.isFinite(grade) || !isTimebackGrade2(grade)) {
125971
126572
  const error2 = ApiError.badRequest(`Invalid grade: ${gradeParam}. Valid grades: ${TIMEBACK_GRADES.join(", ")}`);
125972
126573
  return c2.json(createErrorResponse(error2), error2.status);
125973
126574
  }
125974
- if (!isTimebackSubject(subjectParam)) {
125975
- const error2 = ApiError.badRequest(`Invalid subject: ${subjectParam}. Valid subjects: ${TIMEBACK_SUBJECTS.join(", ")}`);
126575
+ if (!isTimebackSubject2(subjectParam)) {
126576
+ const error2 = ApiError.badRequest(`Invalid subject: ${subjectParam}. Valid subjects: ${TIMEBACK_SUBJECTS2.join(", ")}`);
125976
126577
  return c2.json(createErrorResponse(error2), error2.status);
125977
126578
  }
125978
126579
  enrollments = enrollments.filter((e) => e.grade === grade && e.subject === subjectParam);
@@ -126021,8 +126622,8 @@ var init_timeback7 = __esm(() => {
126021
126622
  const error2 = ApiError.badRequest("Missing required gameId query parameter");
126022
126623
  return c2.json(createErrorResponse(error2), error2.status);
126023
126624
  }
126024
- if (!isTimebackSubject(subject)) {
126025
- const error2 = ApiError.badRequest(`Invalid subject: ${subject}. Valid subjects: ${TIMEBACK_SUBJECTS.join(", ")}`);
126625
+ if (!isTimebackSubject2(subject)) {
126626
+ const error2 = ApiError.badRequest(`Invalid subject: ${subject}. Valid subjects: ${TIMEBACK_SUBJECTS2.join(", ")}`);
126026
126627
  return c2.json(createErrorResponse(error2), error2.status);
126027
126628
  }
126028
126629
  const db2 = c2.get("db");
@@ -126251,12 +126852,12 @@ import path3 from "node:path";
126251
126852
  import { loadPlaycademyConfig as loadPlaycademyConfig2 } from "playcademy/utils";
126252
126853
 
126253
126854
  // ../utils/src/uuid.ts
126254
- var UUID_REGEX2 = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
126855
+ var UUID_REGEX4 = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
126255
126856
  function isValidUUID2(value) {
126256
126857
  if (!value || typeof value !== "string") {
126257
126858
  return false;
126258
126859
  }
126259
- return UUID_REGEX2.test(value);
126860
+ return UUID_REGEX4.test(value);
126260
126861
  }
126261
126862
  // ../utils/src/ansi.ts
126262
126863
  var colors3 = {