@timeback/sdk 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -22
- package/dist/client/adapters/react/hooks/types.d.ts +15 -0
- package/dist/client/adapters/react/hooks/types.d.ts.map +1 -0
- package/dist/client/adapters/react/hooks/useTimebackVerification.d.ts +18 -0
- package/dist/client/adapters/react/hooks/useTimebackVerification.d.ts.map +1 -0
- package/dist/client/adapters/react/index.d.ts +2 -0
- package/dist/client/adapters/react/index.d.ts.map +1 -1
- package/dist/client/adapters/react/index.js +139 -9
- package/dist/client/auth/bearer.d.ts +17 -0
- package/dist/client/auth/bearer.d.ts.map +1 -0
- package/dist/client/auth/index.d.ts +3 -0
- package/dist/client/auth/index.d.ts.map +1 -0
- package/dist/client/auth/types.d.ts +39 -0
- package/dist/client/auth/types.d.ts.map +1 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/lib/fetch.d.ts +19 -0
- package/dist/client/lib/fetch.d.ts.map +1 -0
- package/dist/client/namespaces/user.d.ts +25 -2
- package/dist/client/namespaces/user.d.ts.map +1 -1
- package/dist/client/timeback-client.class.d.ts +15 -0
- package/dist/client/timeback-client.class.d.ts.map +1 -1
- package/dist/client/timeback-client.d.ts +3 -0
- package/dist/client/timeback-client.d.ts.map +1 -1
- package/dist/client.d.ts +2 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +69 -6
- package/dist/edge.d.ts +1 -1
- package/dist/edge.js +85291 -169
- package/dist/identity.js +85186 -74
- package/dist/index.js +1289 -840
- package/dist/server/adapters/express.d.ts.map +1 -1
- package/dist/server/adapters/express.js +489 -388
- package/dist/server/adapters/native.d.ts.map +1 -1
- package/dist/server/adapters/native.js +32 -1
- package/dist/server/adapters/nextjs.js +32 -1
- package/dist/server/adapters/nuxt.d.ts.map +1 -1
- package/dist/server/adapters/nuxt.js +493 -388
- package/dist/server/adapters/solid-start.d.ts.map +1 -1
- package/dist/server/adapters/solid-start.js +493 -388
- package/dist/server/adapters/svelte-kit.d.ts.map +1 -1
- package/dist/server/adapters/svelte-kit.js +37 -1
- package/dist/server/adapters/tanstack-start.d.ts.map +1 -1
- package/dist/server/adapters/tanstack-start.js +488 -388
- package/dist/server/adapters/utils.d.ts +1 -1
- package/dist/server/adapters/utils.d.ts.map +1 -1
- package/dist/server/{lib/build-activity-events.d.ts → handlers/activity/caliper.d.ts} +29 -4
- package/dist/server/handlers/activity/caliper.d.ts.map +1 -0
- package/dist/server/handlers/activity/gradebook.d.ts +56 -0
- package/dist/server/handlers/activity/gradebook.d.ts.map +1 -0
- package/dist/server/handlers/activity/handler.d.ts +15 -0
- package/dist/server/handlers/activity/handler.d.ts.map +1 -0
- package/dist/server/handlers/activity/index.d.ts +9 -0
- package/dist/server/handlers/activity/index.d.ts.map +1 -0
- package/dist/server/handlers/activity/resolve.d.ts +39 -0
- package/dist/server/handlers/activity/resolve.d.ts.map +1 -0
- package/dist/server/handlers/activity/schema.d.ts +52 -0
- package/dist/server/handlers/activity/schema.d.ts.map +1 -0
- package/dist/server/handlers/activity/types.d.ts +52 -0
- package/dist/server/handlers/activity/types.d.ts.map +1 -0
- package/dist/server/handlers/identity/handler.d.ts +14 -0
- package/dist/server/handlers/identity/handler.d.ts.map +1 -0
- package/dist/server/handlers/identity/index.d.ts +8 -0
- package/dist/server/handlers/identity/index.d.ts.map +1 -0
- package/dist/server/handlers/identity/oidc.d.ts +43 -0
- package/dist/server/handlers/identity/oidc.d.ts.map +1 -0
- package/dist/server/handlers/identity/types.d.ts +24 -0
- package/dist/server/handlers/identity/types.d.ts.map +1 -0
- package/dist/server/handlers/identity-only/handler.d.ts +15 -0
- package/dist/server/handlers/identity-only/handler.d.ts.map +1 -0
- package/dist/server/handlers/identity-only/index.d.ts +8 -0
- package/dist/server/handlers/identity-only/index.d.ts.map +1 -0
- package/dist/server/handlers/identity-only/oidc.d.ts +26 -0
- package/dist/server/handlers/identity-only/oidc.d.ts.map +1 -0
- package/dist/server/handlers/identity-only/types.d.ts +19 -0
- package/dist/server/handlers/identity-only/types.d.ts.map +1 -0
- package/dist/server/handlers/index.d.ts +5 -2
- package/dist/server/handlers/index.d.ts.map +1 -1
- package/dist/server/{lib/build-user-profile.d.ts → handlers/user/enrollments.d.ts} +7 -2
- package/dist/server/handlers/user/enrollments.d.ts.map +1 -0
- package/dist/server/handlers/user/handler.d.ts +17 -0
- package/dist/server/handlers/user/handler.d.ts.map +1 -0
- package/dist/server/handlers/user/index.d.ts +10 -0
- package/dist/server/handlers/user/index.d.ts.map +1 -0
- package/dist/server/handlers/user/profile.d.ts +22 -0
- package/dist/server/handlers/user/profile.d.ts.map +1 -0
- package/dist/server/handlers/user/types.d.ts +35 -0
- package/dist/server/handlers/user/types.d.ts.map +1 -0
- package/dist/server/handlers/user/verify.d.ts +25 -0
- package/dist/server/handlers/user/verify.d.ts.map +1 -0
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/lib/index.d.ts +4 -5
- package/dist/server/lib/index.d.ts.map +1 -1
- package/dist/server/lib/resolve.d.ts +4 -42
- package/dist/server/lib/resolve.d.ts.map +1 -1
- package/dist/server/lib/sso.d.ts +86 -0
- package/dist/server/lib/sso.d.ts.map +1 -0
- package/dist/server/lib/utils.d.ts +32 -1
- package/dist/server/lib/utils.d.ts.map +1 -1
- package/dist/server/timeback-identity.d.ts +2 -2
- package/dist/server/timeback-identity.d.ts.map +1 -1
- package/dist/server/timeback.d.ts.map +1 -1
- package/dist/server/types.d.ts +19 -12
- package/dist/server/types.d.ts.map +1 -1
- package/dist/shared/constants.d.ts +1 -0
- package/dist/shared/constants.d.ts.map +1 -1
- package/dist/shared/types.d.ts +18 -3
- package/dist/shared/types.d.ts.map +1 -1
- package/package.json +7 -7
- package/dist/config.d.ts +0 -20
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -0
- package/dist/server/handlers/activity.d.ts +0 -25
- package/dist/server/handlers/activity.d.ts.map +0 -1
- package/dist/server/handlers/identity-full.d.ts +0 -28
- package/dist/server/handlers/identity-full.d.ts.map +0 -1
- package/dist/server/handlers/identity-only.d.ts +0 -22
- package/dist/server/handlers/identity-only.d.ts.map +0 -1
- package/dist/server/handlers/user.d.ts +0 -31
- package/dist/server/handlers/user.d.ts.map +0 -1
- package/dist/server/lib/build-activity-events.d.ts.map +0 -1
- package/dist/server/lib/build-user-profile.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -14949,7 +14949,7 @@ var TimebackSubject = exports_external.enum([
|
|
|
14949
14949
|
"Math",
|
|
14950
14950
|
"None",
|
|
14951
14951
|
"Other"
|
|
14952
|
-
]);
|
|
14952
|
+
]).meta({ id: "TimebackSubject", description: "Subject area" });
|
|
14953
14953
|
var TimebackGrade = exports_external.union([
|
|
14954
14954
|
exports_external.literal(-1),
|
|
14955
14955
|
exports_external.literal(0),
|
|
@@ -14966,7 +14966,10 @@ var TimebackGrade = exports_external.union([
|
|
|
14966
14966
|
exports_external.literal(11),
|
|
14967
14967
|
exports_external.literal(12),
|
|
14968
14968
|
exports_external.literal(13)
|
|
14969
|
-
])
|
|
14969
|
+
]).meta({
|
|
14970
|
+
id: "TimebackGrade",
|
|
14971
|
+
description: "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)"
|
|
14972
|
+
});
|
|
14970
14973
|
var ScoreStatus = exports_external.enum([
|
|
14971
14974
|
"exempt",
|
|
14972
14975
|
"fully graded",
|
|
@@ -15196,62 +15199,84 @@ var CaliperListEventsParams = exports_external.object({
|
|
|
15196
15199
|
actorEmail: exports_external.email().optional()
|
|
15197
15200
|
}).strict();
|
|
15198
15201
|
var CourseIds = exports_external.object({
|
|
15199
|
-
staging: exports_external.string().optional(),
|
|
15200
|
-
production: exports_external.string().optional()
|
|
15201
|
-
});
|
|
15202
|
-
var CourseType = exports_external.enum(["base", "hole-filling", "optional"]);
|
|
15203
|
-
var PublishStatus = exports_external.enum(["draft", "testing", "published", "deactivated"]);
|
|
15202
|
+
staging: exports_external.string().meta({ description: "Course ID in staging environment" }).optional(),
|
|
15203
|
+
production: exports_external.string().meta({ description: "Course ID in production environment" }).optional()
|
|
15204
|
+
}).meta({ id: "CourseIds", description: "Environment-specific course IDs (populated by sync)" });
|
|
15205
|
+
var CourseType = exports_external.enum(["base", "hole-filling", "optional"]).meta({ id: "CourseType", description: "Course classification type" });
|
|
15206
|
+
var PublishStatus = exports_external.enum(["draft", "testing", "published", "deactivated"]).meta({ id: "PublishStatus", description: "Course publication status" });
|
|
15204
15207
|
var CourseGoals = exports_external.object({
|
|
15205
|
-
dailyXp: exports_external.number().int().positive().optional(),
|
|
15206
|
-
dailyLessons: exports_external.number().int().positive().optional(),
|
|
15207
|
-
dailyActiveMinutes: exports_external.number().int().positive().optional(),
|
|
15208
|
-
dailyAccuracy: exports_external.number().int().min(0).max(100).optional(),
|
|
15209
|
-
dailyMasteredUnits: exports_external.number().int().positive().optional()
|
|
15210
|
-
});
|
|
15208
|
+
dailyXp: exports_external.number().int().positive().meta({ description: "Target XP to earn per day" }).optional(),
|
|
15209
|
+
dailyLessons: exports_external.number().int().positive().meta({ description: "Target lessons to complete per day" }).optional(),
|
|
15210
|
+
dailyActiveMinutes: exports_external.number().int().positive().meta({ description: "Target active learning minutes per day" }).optional(),
|
|
15211
|
+
dailyAccuracy: exports_external.number().int().min(0).max(100).meta({ description: "Target accuracy percentage (0-100)" }).optional(),
|
|
15212
|
+
dailyMasteredUnits: exports_external.number().int().positive().meta({ description: "Target units to master per day" }).optional()
|
|
15213
|
+
}).meta({ id: "CourseGoals", description: "Daily learning goals for a course" });
|
|
15211
15214
|
var CourseMetrics = exports_external.object({
|
|
15212
|
-
totalXp: exports_external.number().int().positive().optional(),
|
|
15213
|
-
totalLessons: exports_external.number().int().positive().optional(),
|
|
15214
|
-
totalGrades: exports_external.number().int().positive().optional()
|
|
15215
|
-
});
|
|
15215
|
+
totalXp: exports_external.number().int().positive().meta({ description: "Total XP available in the course" }).optional(),
|
|
15216
|
+
totalLessons: exports_external.number().int().positive().meta({ description: "Total number of lessons/activities" }).optional(),
|
|
15217
|
+
totalGrades: exports_external.number().int().positive().meta({ description: "Total grade levels covered" }).optional()
|
|
15218
|
+
}).meta({ id: "CourseMetrics", description: "Aggregate metrics for a course" });
|
|
15216
15219
|
var CourseMetadata = exports_external.object({
|
|
15217
15220
|
courseType: CourseType.optional(),
|
|
15218
|
-
isSupplemental: exports_external.boolean().optional(),
|
|
15219
|
-
isCustom: exports_external.boolean().optional(),
|
|
15221
|
+
isSupplemental: exports_external.boolean().meta({ description: "Whether this is supplemental to a base course" }).optional(),
|
|
15222
|
+
isCustom: exports_external.boolean().meta({ description: "Whether this is a custom course for an individual student" }).optional(),
|
|
15220
15223
|
publishStatus: PublishStatus.optional(),
|
|
15221
|
-
contactEmail: exports_external.email().optional(),
|
|
15222
|
-
primaryApp: exports_external.string().optional(),
|
|
15224
|
+
contactEmail: exports_external.email().meta({ description: "Contact email for course issues" }).optional(),
|
|
15225
|
+
primaryApp: exports_external.string().meta({ description: "Primary application identifier" }).optional(),
|
|
15223
15226
|
goals: CourseGoals.optional(),
|
|
15224
15227
|
metrics: CourseMetrics.optional()
|
|
15225
|
-
});
|
|
15228
|
+
}).meta({ id: "CourseMetadata", description: "Course metadata (matches API metadata object)" });
|
|
15226
15229
|
var CourseDefaults = exports_external.object({
|
|
15227
|
-
courseCode: exports_external.string().optional(),
|
|
15228
|
-
level: exports_external.string().optional(),
|
|
15230
|
+
courseCode: exports_external.string().meta({ description: "Course code (e.g., 'MATH101')" }).optional(),
|
|
15231
|
+
level: exports_external.string().meta({ description: "Course level (e.g., 'AP', 'Honors')" }).optional(),
|
|
15229
15232
|
metadata: CourseMetadata.optional()
|
|
15233
|
+
}).meta({
|
|
15234
|
+
id: "CourseDefaults",
|
|
15235
|
+
description: "Default properties that apply to all courses unless overridden"
|
|
15230
15236
|
});
|
|
15231
15237
|
var CourseEnvOverrides = exports_external.object({
|
|
15232
|
-
level: exports_external.string().optional(),
|
|
15233
|
-
sensor: exports_external.
|
|
15234
|
-
launchUrl: exports_external.
|
|
15238
|
+
level: exports_external.string().meta({ description: "Course level for this environment" }).optional(),
|
|
15239
|
+
sensor: exports_external.url().meta({ description: "Caliper sensor endpoint URL for this environment" }).optional(),
|
|
15240
|
+
launchUrl: exports_external.url().meta({ description: "LTI launch URL for this environment" }).optional(),
|
|
15235
15241
|
metadata: CourseMetadata.optional()
|
|
15242
|
+
}).meta({
|
|
15243
|
+
id: "CourseEnvOverrides",
|
|
15244
|
+
description: "Environment-specific course overrides (non-identity fields)"
|
|
15236
15245
|
});
|
|
15237
15246
|
var CourseOverrides = exports_external.object({
|
|
15238
|
-
staging: CourseEnvOverrides.
|
|
15239
|
-
|
|
15240
|
-
})
|
|
15247
|
+
staging: CourseEnvOverrides.meta({
|
|
15248
|
+
description: "Overrides for staging environment"
|
|
15249
|
+
}).optional(),
|
|
15250
|
+
production: CourseEnvOverrides.meta({
|
|
15251
|
+
description: "Overrides for production environment"
|
|
15252
|
+
}).optional()
|
|
15253
|
+
}).meta({ id: "CourseOverrides", description: "Per-environment course overrides" });
|
|
15241
15254
|
var CourseConfig = CourseDefaults.extend({
|
|
15242
|
-
subject: TimebackSubject,
|
|
15243
|
-
grade: TimebackGrade.
|
|
15255
|
+
subject: TimebackSubject.meta({ description: "Subject area for this course" }),
|
|
15256
|
+
grade: TimebackGrade.meta({
|
|
15257
|
+
description: "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)"
|
|
15258
|
+
}).optional(),
|
|
15244
15259
|
ids: CourseIds.nullable().optional(),
|
|
15245
|
-
sensor: exports_external.
|
|
15246
|
-
launchUrl: exports_external.
|
|
15260
|
+
sensor: exports_external.url().meta({ description: "Caliper sensor endpoint URL for this course" }).optional(),
|
|
15261
|
+
launchUrl: exports_external.url().meta({ description: "LTI launch URL for this course" }).optional(),
|
|
15247
15262
|
overrides: CourseOverrides.optional()
|
|
15263
|
+
}).meta({
|
|
15264
|
+
id: "CourseConfig",
|
|
15265
|
+
description: "Configuration for a single course. Must have either grade or courseCode (or both)."
|
|
15248
15266
|
});
|
|
15249
15267
|
var TimebackConfig = exports_external.object({
|
|
15250
|
-
|
|
15251
|
-
|
|
15252
|
-
|
|
15253
|
-
|
|
15254
|
-
|
|
15268
|
+
$schema: exports_external.string().meta({ description: "JSON Schema reference for editor support" }).optional(),
|
|
15269
|
+
name: exports_external.string().min(1, "App name is required").meta({ description: "Display name for your app" }),
|
|
15270
|
+
defaults: CourseDefaults.meta({
|
|
15271
|
+
description: "Default properties applied to all courses"
|
|
15272
|
+
}).optional(),
|
|
15273
|
+
courses: exports_external.array(CourseConfig).min(1, "At least one course is required").meta({ description: "Courses available in this app" }),
|
|
15274
|
+
sensor: exports_external.url().meta({ description: "Default Caliper sensor endpoint URL for all courses" }).optional(),
|
|
15275
|
+
launchUrl: exports_external.url().meta({ description: "Default LTI launch URL for all courses" }).optional()
|
|
15276
|
+
}).meta({
|
|
15277
|
+
id: "TimebackConfig",
|
|
15278
|
+
title: "Timeback Config",
|
|
15279
|
+
description: "Configuration schema for timeback.config.json files"
|
|
15255
15280
|
}).refine((config2) => {
|
|
15256
15281
|
return config2.courses.every((c) => c.grade !== undefined || c.courseCode !== undefined);
|
|
15257
15282
|
}, {
|
|
@@ -15272,14 +15297,20 @@ var TimebackConfig = exports_external.object({
|
|
|
15272
15297
|
message: "Duplicate courseCode found; each must be unique",
|
|
15273
15298
|
path: ["courses"]
|
|
15274
15299
|
}).refine((config2) => {
|
|
15275
|
-
return config2.courses.every((c) =>
|
|
15276
|
-
|
|
15277
|
-
|
|
15278
|
-
|
|
15279
|
-
|
|
15280
|
-
|
|
15300
|
+
return config2.courses.every((c) => {
|
|
15301
|
+
if (c.sensor !== undefined || config2.sensor !== undefined) {
|
|
15302
|
+
return true;
|
|
15303
|
+
}
|
|
15304
|
+
const launchUrls = [
|
|
15305
|
+
c.launchUrl,
|
|
15306
|
+
config2.launchUrl,
|
|
15307
|
+
c.overrides?.staging?.launchUrl,
|
|
15308
|
+
c.overrides?.production?.launchUrl
|
|
15309
|
+
].filter(Boolean);
|
|
15310
|
+
return launchUrls.length > 0;
|
|
15311
|
+
});
|
|
15281
15312
|
}, {
|
|
15282
|
-
message: "Each course must have an effective
|
|
15313
|
+
message: "Each course must have an effective sensor. Either set `sensor` explicitly (top-level or per-course), or provide a `launchUrl` so sensor can be derived from its origin.",
|
|
15283
15314
|
path: ["courses"]
|
|
15284
15315
|
});
|
|
15285
15316
|
var EdubridgeDateString = exports_external.union([IsoDateString, IsoDateTimeString]);
|
|
@@ -31327,7 +31358,7 @@ var TimebackSubject2 = exports_external2.enum([
|
|
|
31327
31358
|
"Math",
|
|
31328
31359
|
"None",
|
|
31329
31360
|
"Other"
|
|
31330
|
-
]);
|
|
31361
|
+
]).meta({ id: "TimebackSubject", description: "Subject area" });
|
|
31331
31362
|
var TimebackGrade2 = exports_external2.union([
|
|
31332
31363
|
exports_external2.literal(-1),
|
|
31333
31364
|
exports_external2.literal(0),
|
|
@@ -31344,7 +31375,10 @@ var TimebackGrade2 = exports_external2.union([
|
|
|
31344
31375
|
exports_external2.literal(11),
|
|
31345
31376
|
exports_external2.literal(12),
|
|
31346
31377
|
exports_external2.literal(13)
|
|
31347
|
-
])
|
|
31378
|
+
]).meta({
|
|
31379
|
+
id: "TimebackGrade",
|
|
31380
|
+
description: "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)"
|
|
31381
|
+
});
|
|
31348
31382
|
var ScoreStatus2 = exports_external2.enum([
|
|
31349
31383
|
"exempt",
|
|
31350
31384
|
"fully graded",
|
|
@@ -31574,62 +31608,84 @@ var CaliperListEventsParams2 = exports_external2.object({
|
|
|
31574
31608
|
actorEmail: exports_external2.email().optional()
|
|
31575
31609
|
}).strict();
|
|
31576
31610
|
var CourseIds2 = exports_external2.object({
|
|
31577
|
-
staging: exports_external2.string().optional(),
|
|
31578
|
-
production: exports_external2.string().optional()
|
|
31579
|
-
});
|
|
31580
|
-
var CourseType2 = exports_external2.enum(["base", "hole-filling", "optional"]);
|
|
31581
|
-
var PublishStatus2 = exports_external2.enum(["draft", "testing", "published", "deactivated"]);
|
|
31611
|
+
staging: exports_external2.string().meta({ description: "Course ID in staging environment" }).optional(),
|
|
31612
|
+
production: exports_external2.string().meta({ description: "Course ID in production environment" }).optional()
|
|
31613
|
+
}).meta({ id: "CourseIds", description: "Environment-specific course IDs (populated by sync)" });
|
|
31614
|
+
var CourseType2 = exports_external2.enum(["base", "hole-filling", "optional"]).meta({ id: "CourseType", description: "Course classification type" });
|
|
31615
|
+
var PublishStatus2 = exports_external2.enum(["draft", "testing", "published", "deactivated"]).meta({ id: "PublishStatus", description: "Course publication status" });
|
|
31582
31616
|
var CourseGoals2 = exports_external2.object({
|
|
31583
|
-
dailyXp: exports_external2.number().int().positive().optional(),
|
|
31584
|
-
dailyLessons: exports_external2.number().int().positive().optional(),
|
|
31585
|
-
dailyActiveMinutes: exports_external2.number().int().positive().optional(),
|
|
31586
|
-
dailyAccuracy: exports_external2.number().int().min(0).max(100).optional(),
|
|
31587
|
-
dailyMasteredUnits: exports_external2.number().int().positive().optional()
|
|
31588
|
-
});
|
|
31617
|
+
dailyXp: exports_external2.number().int().positive().meta({ description: "Target XP to earn per day" }).optional(),
|
|
31618
|
+
dailyLessons: exports_external2.number().int().positive().meta({ description: "Target lessons to complete per day" }).optional(),
|
|
31619
|
+
dailyActiveMinutes: exports_external2.number().int().positive().meta({ description: "Target active learning minutes per day" }).optional(),
|
|
31620
|
+
dailyAccuracy: exports_external2.number().int().min(0).max(100).meta({ description: "Target accuracy percentage (0-100)" }).optional(),
|
|
31621
|
+
dailyMasteredUnits: exports_external2.number().int().positive().meta({ description: "Target units to master per day" }).optional()
|
|
31622
|
+
}).meta({ id: "CourseGoals", description: "Daily learning goals for a course" });
|
|
31589
31623
|
var CourseMetrics2 = exports_external2.object({
|
|
31590
|
-
totalXp: exports_external2.number().int().positive().optional(),
|
|
31591
|
-
totalLessons: exports_external2.number().int().positive().optional(),
|
|
31592
|
-
totalGrades: exports_external2.number().int().positive().optional()
|
|
31593
|
-
});
|
|
31624
|
+
totalXp: exports_external2.number().int().positive().meta({ description: "Total XP available in the course" }).optional(),
|
|
31625
|
+
totalLessons: exports_external2.number().int().positive().meta({ description: "Total number of lessons/activities" }).optional(),
|
|
31626
|
+
totalGrades: exports_external2.number().int().positive().meta({ description: "Total grade levels covered" }).optional()
|
|
31627
|
+
}).meta({ id: "CourseMetrics", description: "Aggregate metrics for a course" });
|
|
31594
31628
|
var CourseMetadata2 = exports_external2.object({
|
|
31595
31629
|
courseType: CourseType2.optional(),
|
|
31596
|
-
isSupplemental: exports_external2.boolean().optional(),
|
|
31597
|
-
isCustom: exports_external2.boolean().optional(),
|
|
31630
|
+
isSupplemental: exports_external2.boolean().meta({ description: "Whether this is supplemental to a base course" }).optional(),
|
|
31631
|
+
isCustom: exports_external2.boolean().meta({ description: "Whether this is a custom course for an individual student" }).optional(),
|
|
31598
31632
|
publishStatus: PublishStatus2.optional(),
|
|
31599
|
-
contactEmail: exports_external2.email().optional(),
|
|
31600
|
-
primaryApp: exports_external2.string().optional(),
|
|
31633
|
+
contactEmail: exports_external2.email().meta({ description: "Contact email for course issues" }).optional(),
|
|
31634
|
+
primaryApp: exports_external2.string().meta({ description: "Primary application identifier" }).optional(),
|
|
31601
31635
|
goals: CourseGoals2.optional(),
|
|
31602
31636
|
metrics: CourseMetrics2.optional()
|
|
31603
|
-
});
|
|
31637
|
+
}).meta({ id: "CourseMetadata", description: "Course metadata (matches API metadata object)" });
|
|
31604
31638
|
var CourseDefaults2 = exports_external2.object({
|
|
31605
|
-
courseCode: exports_external2.string().optional(),
|
|
31606
|
-
level: exports_external2.string().optional(),
|
|
31639
|
+
courseCode: exports_external2.string().meta({ description: "Course code (e.g., 'MATH101')" }).optional(),
|
|
31640
|
+
level: exports_external2.string().meta({ description: "Course level (e.g., 'AP', 'Honors')" }).optional(),
|
|
31607
31641
|
metadata: CourseMetadata2.optional()
|
|
31642
|
+
}).meta({
|
|
31643
|
+
id: "CourseDefaults",
|
|
31644
|
+
description: "Default properties that apply to all courses unless overridden"
|
|
31608
31645
|
});
|
|
31609
31646
|
var CourseEnvOverrides2 = exports_external2.object({
|
|
31610
|
-
level: exports_external2.string().optional(),
|
|
31611
|
-
sensor: exports_external2.
|
|
31612
|
-
launchUrl: exports_external2.
|
|
31647
|
+
level: exports_external2.string().meta({ description: "Course level for this environment" }).optional(),
|
|
31648
|
+
sensor: exports_external2.url().meta({ description: "Caliper sensor endpoint URL for this environment" }).optional(),
|
|
31649
|
+
launchUrl: exports_external2.url().meta({ description: "LTI launch URL for this environment" }).optional(),
|
|
31613
31650
|
metadata: CourseMetadata2.optional()
|
|
31651
|
+
}).meta({
|
|
31652
|
+
id: "CourseEnvOverrides",
|
|
31653
|
+
description: "Environment-specific course overrides (non-identity fields)"
|
|
31614
31654
|
});
|
|
31615
31655
|
var CourseOverrides2 = exports_external2.object({
|
|
31616
|
-
staging: CourseEnvOverrides2.
|
|
31617
|
-
|
|
31618
|
-
})
|
|
31656
|
+
staging: CourseEnvOverrides2.meta({
|
|
31657
|
+
description: "Overrides for staging environment"
|
|
31658
|
+
}).optional(),
|
|
31659
|
+
production: CourseEnvOverrides2.meta({
|
|
31660
|
+
description: "Overrides for production environment"
|
|
31661
|
+
}).optional()
|
|
31662
|
+
}).meta({ id: "CourseOverrides", description: "Per-environment course overrides" });
|
|
31619
31663
|
var CourseConfig2 = CourseDefaults2.extend({
|
|
31620
|
-
subject: TimebackSubject2,
|
|
31621
|
-
grade: TimebackGrade2.
|
|
31664
|
+
subject: TimebackSubject2.meta({ description: "Subject area for this course" }),
|
|
31665
|
+
grade: TimebackGrade2.meta({
|
|
31666
|
+
description: "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)"
|
|
31667
|
+
}).optional(),
|
|
31622
31668
|
ids: CourseIds2.nullable().optional(),
|
|
31623
|
-
sensor: exports_external2.
|
|
31624
|
-
launchUrl: exports_external2.
|
|
31669
|
+
sensor: exports_external2.url().meta({ description: "Caliper sensor endpoint URL for this course" }).optional(),
|
|
31670
|
+
launchUrl: exports_external2.url().meta({ description: "LTI launch URL for this course" }).optional(),
|
|
31625
31671
|
overrides: CourseOverrides2.optional()
|
|
31672
|
+
}).meta({
|
|
31673
|
+
id: "CourseConfig",
|
|
31674
|
+
description: "Configuration for a single course. Must have either grade or courseCode (or both)."
|
|
31626
31675
|
});
|
|
31627
31676
|
var TimebackConfig2 = exports_external2.object({
|
|
31628
|
-
|
|
31629
|
-
|
|
31630
|
-
|
|
31631
|
-
|
|
31632
|
-
|
|
31677
|
+
$schema: exports_external2.string().meta({ description: "JSON Schema reference for editor support" }).optional(),
|
|
31678
|
+
name: exports_external2.string().min(1, "App name is required").meta({ description: "Display name for your app" }),
|
|
31679
|
+
defaults: CourseDefaults2.meta({
|
|
31680
|
+
description: "Default properties applied to all courses"
|
|
31681
|
+
}).optional(),
|
|
31682
|
+
courses: exports_external2.array(CourseConfig2).min(1, "At least one course is required").meta({ description: "Courses available in this app" }),
|
|
31683
|
+
sensor: exports_external2.url().meta({ description: "Default Caliper sensor endpoint URL for all courses" }).optional(),
|
|
31684
|
+
launchUrl: exports_external2.url().meta({ description: "Default LTI launch URL for all courses" }).optional()
|
|
31685
|
+
}).meta({
|
|
31686
|
+
id: "TimebackConfig",
|
|
31687
|
+
title: "Timeback Config",
|
|
31688
|
+
description: "Configuration schema for timeback.config.json files"
|
|
31633
31689
|
}).refine((config22) => {
|
|
31634
31690
|
return config22.courses.every((c) => c.grade !== undefined || c.courseCode !== undefined);
|
|
31635
31691
|
}, {
|
|
@@ -31650,14 +31706,20 @@ var TimebackConfig2 = exports_external2.object({
|
|
|
31650
31706
|
message: "Duplicate courseCode found; each must be unique",
|
|
31651
31707
|
path: ["courses"]
|
|
31652
31708
|
}).refine((config22) => {
|
|
31653
|
-
return config22.courses.every((c) =>
|
|
31654
|
-
|
|
31655
|
-
|
|
31656
|
-
|
|
31657
|
-
|
|
31658
|
-
|
|
31709
|
+
return config22.courses.every((c) => {
|
|
31710
|
+
if (c.sensor !== undefined || config22.sensor !== undefined) {
|
|
31711
|
+
return true;
|
|
31712
|
+
}
|
|
31713
|
+
const launchUrls = [
|
|
31714
|
+
c.launchUrl,
|
|
31715
|
+
config22.launchUrl,
|
|
31716
|
+
c.overrides?.staging?.launchUrl,
|
|
31717
|
+
c.overrides?.production?.launchUrl
|
|
31718
|
+
].filter(Boolean);
|
|
31719
|
+
return launchUrls.length > 0;
|
|
31720
|
+
});
|
|
31659
31721
|
}, {
|
|
31660
|
-
message: "Each course must have an effective
|
|
31722
|
+
message: "Each course must have an effective sensor. Either set `sensor` explicitly (top-level or per-course), or provide a `launchUrl` so sensor can be derived from its origin.",
|
|
31661
31723
|
path: ["courses"]
|
|
31662
31724
|
});
|
|
31663
31725
|
var EdubridgeDateString2 = exports_external2.union([IsoDateString2, IsoDateTimeString2]);
|
|
@@ -48735,7 +48797,7 @@ var TimebackSubject3 = exports_external3.enum([
|
|
|
48735
48797
|
"Math",
|
|
48736
48798
|
"None",
|
|
48737
48799
|
"Other"
|
|
48738
|
-
]);
|
|
48800
|
+
]).meta({ id: "TimebackSubject", description: "Subject area" });
|
|
48739
48801
|
var TimebackGrade3 = exports_external3.union([
|
|
48740
48802
|
exports_external3.literal(-1),
|
|
48741
48803
|
exports_external3.literal(0),
|
|
@@ -48752,7 +48814,10 @@ var TimebackGrade3 = exports_external3.union([
|
|
|
48752
48814
|
exports_external3.literal(11),
|
|
48753
48815
|
exports_external3.literal(12),
|
|
48754
48816
|
exports_external3.literal(13)
|
|
48755
|
-
])
|
|
48817
|
+
]).meta({
|
|
48818
|
+
id: "TimebackGrade",
|
|
48819
|
+
description: "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)"
|
|
48820
|
+
});
|
|
48756
48821
|
var ScoreStatus3 = exports_external3.enum([
|
|
48757
48822
|
"exempt",
|
|
48758
48823
|
"fully graded",
|
|
@@ -48982,62 +49047,84 @@ var CaliperListEventsParams3 = exports_external3.object({
|
|
|
48982
49047
|
actorEmail: exports_external3.email().optional()
|
|
48983
49048
|
}).strict();
|
|
48984
49049
|
var CourseIds3 = exports_external3.object({
|
|
48985
|
-
staging: exports_external3.string().optional(),
|
|
48986
|
-
production: exports_external3.string().optional()
|
|
48987
|
-
});
|
|
48988
|
-
var CourseType3 = exports_external3.enum(["base", "hole-filling", "optional"]);
|
|
48989
|
-
var PublishStatus3 = exports_external3.enum(["draft", "testing", "published", "deactivated"]);
|
|
49050
|
+
staging: exports_external3.string().meta({ description: "Course ID in staging environment" }).optional(),
|
|
49051
|
+
production: exports_external3.string().meta({ description: "Course ID in production environment" }).optional()
|
|
49052
|
+
}).meta({ id: "CourseIds", description: "Environment-specific course IDs (populated by sync)" });
|
|
49053
|
+
var CourseType3 = exports_external3.enum(["base", "hole-filling", "optional"]).meta({ id: "CourseType", description: "Course classification type" });
|
|
49054
|
+
var PublishStatus3 = exports_external3.enum(["draft", "testing", "published", "deactivated"]).meta({ id: "PublishStatus", description: "Course publication status" });
|
|
48990
49055
|
var CourseGoals3 = exports_external3.object({
|
|
48991
|
-
dailyXp: exports_external3.number().int().positive().optional(),
|
|
48992
|
-
dailyLessons: exports_external3.number().int().positive().optional(),
|
|
48993
|
-
dailyActiveMinutes: exports_external3.number().int().positive().optional(),
|
|
48994
|
-
dailyAccuracy: exports_external3.number().int().min(0).max(100).optional(),
|
|
48995
|
-
dailyMasteredUnits: exports_external3.number().int().positive().optional()
|
|
48996
|
-
});
|
|
49056
|
+
dailyXp: exports_external3.number().int().positive().meta({ description: "Target XP to earn per day" }).optional(),
|
|
49057
|
+
dailyLessons: exports_external3.number().int().positive().meta({ description: "Target lessons to complete per day" }).optional(),
|
|
49058
|
+
dailyActiveMinutes: exports_external3.number().int().positive().meta({ description: "Target active learning minutes per day" }).optional(),
|
|
49059
|
+
dailyAccuracy: exports_external3.number().int().min(0).max(100).meta({ description: "Target accuracy percentage (0-100)" }).optional(),
|
|
49060
|
+
dailyMasteredUnits: exports_external3.number().int().positive().meta({ description: "Target units to master per day" }).optional()
|
|
49061
|
+
}).meta({ id: "CourseGoals", description: "Daily learning goals for a course" });
|
|
48997
49062
|
var CourseMetrics3 = exports_external3.object({
|
|
48998
|
-
totalXp: exports_external3.number().int().positive().optional(),
|
|
48999
|
-
totalLessons: exports_external3.number().int().positive().optional(),
|
|
49000
|
-
totalGrades: exports_external3.number().int().positive().optional()
|
|
49001
|
-
});
|
|
49063
|
+
totalXp: exports_external3.number().int().positive().meta({ description: "Total XP available in the course" }).optional(),
|
|
49064
|
+
totalLessons: exports_external3.number().int().positive().meta({ description: "Total number of lessons/activities" }).optional(),
|
|
49065
|
+
totalGrades: exports_external3.number().int().positive().meta({ description: "Total grade levels covered" }).optional()
|
|
49066
|
+
}).meta({ id: "CourseMetrics", description: "Aggregate metrics for a course" });
|
|
49002
49067
|
var CourseMetadata3 = exports_external3.object({
|
|
49003
49068
|
courseType: CourseType3.optional(),
|
|
49004
|
-
isSupplemental: exports_external3.boolean().optional(),
|
|
49005
|
-
isCustom: exports_external3.boolean().optional(),
|
|
49069
|
+
isSupplemental: exports_external3.boolean().meta({ description: "Whether this is supplemental to a base course" }).optional(),
|
|
49070
|
+
isCustom: exports_external3.boolean().meta({ description: "Whether this is a custom course for an individual student" }).optional(),
|
|
49006
49071
|
publishStatus: PublishStatus3.optional(),
|
|
49007
|
-
contactEmail: exports_external3.email().optional(),
|
|
49008
|
-
primaryApp: exports_external3.string().optional(),
|
|
49072
|
+
contactEmail: exports_external3.email().meta({ description: "Contact email for course issues" }).optional(),
|
|
49073
|
+
primaryApp: exports_external3.string().meta({ description: "Primary application identifier" }).optional(),
|
|
49009
49074
|
goals: CourseGoals3.optional(),
|
|
49010
49075
|
metrics: CourseMetrics3.optional()
|
|
49011
|
-
});
|
|
49076
|
+
}).meta({ id: "CourseMetadata", description: "Course metadata (matches API metadata object)" });
|
|
49012
49077
|
var CourseDefaults3 = exports_external3.object({
|
|
49013
|
-
courseCode: exports_external3.string().optional(),
|
|
49014
|
-
level: exports_external3.string().optional(),
|
|
49078
|
+
courseCode: exports_external3.string().meta({ description: "Course code (e.g., 'MATH101')" }).optional(),
|
|
49079
|
+
level: exports_external3.string().meta({ description: "Course level (e.g., 'AP', 'Honors')" }).optional(),
|
|
49015
49080
|
metadata: CourseMetadata3.optional()
|
|
49081
|
+
}).meta({
|
|
49082
|
+
id: "CourseDefaults",
|
|
49083
|
+
description: "Default properties that apply to all courses unless overridden"
|
|
49016
49084
|
});
|
|
49017
49085
|
var CourseEnvOverrides3 = exports_external3.object({
|
|
49018
|
-
level: exports_external3.string().optional(),
|
|
49019
|
-
sensor: exports_external3.
|
|
49020
|
-
launchUrl: exports_external3.
|
|
49086
|
+
level: exports_external3.string().meta({ description: "Course level for this environment" }).optional(),
|
|
49087
|
+
sensor: exports_external3.url().meta({ description: "Caliper sensor endpoint URL for this environment" }).optional(),
|
|
49088
|
+
launchUrl: exports_external3.url().meta({ description: "LTI launch URL for this environment" }).optional(),
|
|
49021
49089
|
metadata: CourseMetadata3.optional()
|
|
49090
|
+
}).meta({
|
|
49091
|
+
id: "CourseEnvOverrides",
|
|
49092
|
+
description: "Environment-specific course overrides (non-identity fields)"
|
|
49022
49093
|
});
|
|
49023
49094
|
var CourseOverrides3 = exports_external3.object({
|
|
49024
|
-
staging: CourseEnvOverrides3.
|
|
49025
|
-
|
|
49026
|
-
})
|
|
49095
|
+
staging: CourseEnvOverrides3.meta({
|
|
49096
|
+
description: "Overrides for staging environment"
|
|
49097
|
+
}).optional(),
|
|
49098
|
+
production: CourseEnvOverrides3.meta({
|
|
49099
|
+
description: "Overrides for production environment"
|
|
49100
|
+
}).optional()
|
|
49101
|
+
}).meta({ id: "CourseOverrides", description: "Per-environment course overrides" });
|
|
49027
49102
|
var CourseConfig3 = CourseDefaults3.extend({
|
|
49028
|
-
subject: TimebackSubject3,
|
|
49029
|
-
grade: TimebackGrade3.
|
|
49103
|
+
subject: TimebackSubject3.meta({ description: "Subject area for this course" }),
|
|
49104
|
+
grade: TimebackGrade3.meta({
|
|
49105
|
+
description: "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)"
|
|
49106
|
+
}).optional(),
|
|
49030
49107
|
ids: CourseIds3.nullable().optional(),
|
|
49031
|
-
sensor: exports_external3.
|
|
49032
|
-
launchUrl: exports_external3.
|
|
49108
|
+
sensor: exports_external3.url().meta({ description: "Caliper sensor endpoint URL for this course" }).optional(),
|
|
49109
|
+
launchUrl: exports_external3.url().meta({ description: "LTI launch URL for this course" }).optional(),
|
|
49033
49110
|
overrides: CourseOverrides3.optional()
|
|
49111
|
+
}).meta({
|
|
49112
|
+
id: "CourseConfig",
|
|
49113
|
+
description: "Configuration for a single course. Must have either grade or courseCode (or both)."
|
|
49034
49114
|
});
|
|
49035
49115
|
var TimebackConfig3 = exports_external3.object({
|
|
49036
|
-
|
|
49037
|
-
|
|
49038
|
-
|
|
49039
|
-
|
|
49040
|
-
|
|
49116
|
+
$schema: exports_external3.string().meta({ description: "JSON Schema reference for editor support" }).optional(),
|
|
49117
|
+
name: exports_external3.string().min(1, "App name is required").meta({ description: "Display name for your app" }),
|
|
49118
|
+
defaults: CourseDefaults3.meta({
|
|
49119
|
+
description: "Default properties applied to all courses"
|
|
49120
|
+
}).optional(),
|
|
49121
|
+
courses: exports_external3.array(CourseConfig3).min(1, "At least one course is required").meta({ description: "Courses available in this app" }),
|
|
49122
|
+
sensor: exports_external3.url().meta({ description: "Default Caliper sensor endpoint URL for all courses" }).optional(),
|
|
49123
|
+
launchUrl: exports_external3.url().meta({ description: "Default LTI launch URL for all courses" }).optional()
|
|
49124
|
+
}).meta({
|
|
49125
|
+
id: "TimebackConfig",
|
|
49126
|
+
title: "Timeback Config",
|
|
49127
|
+
description: "Configuration schema for timeback.config.json files"
|
|
49041
49128
|
}).refine((config22) => {
|
|
49042
49129
|
return config22.courses.every((c) => c.grade !== undefined || c.courseCode !== undefined);
|
|
49043
49130
|
}, {
|
|
@@ -49058,14 +49145,20 @@ var TimebackConfig3 = exports_external3.object({
|
|
|
49058
49145
|
message: "Duplicate courseCode found; each must be unique",
|
|
49059
49146
|
path: ["courses"]
|
|
49060
49147
|
}).refine((config22) => {
|
|
49061
|
-
return config22.courses.every((c) =>
|
|
49062
|
-
|
|
49063
|
-
|
|
49064
|
-
|
|
49065
|
-
|
|
49066
|
-
|
|
49148
|
+
return config22.courses.every((c) => {
|
|
49149
|
+
if (c.sensor !== undefined || config22.sensor !== undefined) {
|
|
49150
|
+
return true;
|
|
49151
|
+
}
|
|
49152
|
+
const launchUrls = [
|
|
49153
|
+
c.launchUrl,
|
|
49154
|
+
config22.launchUrl,
|
|
49155
|
+
c.overrides?.staging?.launchUrl,
|
|
49156
|
+
c.overrides?.production?.launchUrl
|
|
49157
|
+
].filter(Boolean);
|
|
49158
|
+
return launchUrls.length > 0;
|
|
49159
|
+
});
|
|
49067
49160
|
}, {
|
|
49068
|
-
message: "Each course must have an effective
|
|
49161
|
+
message: "Each course must have an effective sensor. Either set `sensor` explicitly (top-level or per-course), or provide a `launchUrl` so sensor can be derived from its origin.",
|
|
49069
49162
|
path: ["courses"]
|
|
49070
49163
|
});
|
|
49071
49164
|
var EdubridgeDateString3 = exports_external3.union([IsoDateString3, IsoDateTimeString3]);
|
|
@@ -66342,7 +66435,7 @@ var TimebackSubject4 = exports_external4.enum([
|
|
|
66342
66435
|
"Math",
|
|
66343
66436
|
"None",
|
|
66344
66437
|
"Other"
|
|
66345
|
-
]);
|
|
66438
|
+
]).meta({ id: "TimebackSubject", description: "Subject area" });
|
|
66346
66439
|
var TimebackGrade4 = exports_external4.union([
|
|
66347
66440
|
exports_external4.literal(-1),
|
|
66348
66441
|
exports_external4.literal(0),
|
|
@@ -66359,7 +66452,10 @@ var TimebackGrade4 = exports_external4.union([
|
|
|
66359
66452
|
exports_external4.literal(11),
|
|
66360
66453
|
exports_external4.literal(12),
|
|
66361
66454
|
exports_external4.literal(13)
|
|
66362
|
-
])
|
|
66455
|
+
]).meta({
|
|
66456
|
+
id: "TimebackGrade",
|
|
66457
|
+
description: "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)"
|
|
66458
|
+
});
|
|
66363
66459
|
var ScoreStatus4 = exports_external4.enum([
|
|
66364
66460
|
"exempt",
|
|
66365
66461
|
"fully graded",
|
|
@@ -66589,62 +66685,84 @@ var CaliperListEventsParams4 = exports_external4.object({
|
|
|
66589
66685
|
actorEmail: exports_external4.email().optional()
|
|
66590
66686
|
}).strict();
|
|
66591
66687
|
var CourseIds4 = exports_external4.object({
|
|
66592
|
-
staging: exports_external4.string().optional(),
|
|
66593
|
-
production: exports_external4.string().optional()
|
|
66594
|
-
});
|
|
66595
|
-
var CourseType4 = exports_external4.enum(["base", "hole-filling", "optional"]);
|
|
66596
|
-
var PublishStatus4 = exports_external4.enum(["draft", "testing", "published", "deactivated"]);
|
|
66688
|
+
staging: exports_external4.string().meta({ description: "Course ID in staging environment" }).optional(),
|
|
66689
|
+
production: exports_external4.string().meta({ description: "Course ID in production environment" }).optional()
|
|
66690
|
+
}).meta({ id: "CourseIds", description: "Environment-specific course IDs (populated by sync)" });
|
|
66691
|
+
var CourseType4 = exports_external4.enum(["base", "hole-filling", "optional"]).meta({ id: "CourseType", description: "Course classification type" });
|
|
66692
|
+
var PublishStatus4 = exports_external4.enum(["draft", "testing", "published", "deactivated"]).meta({ id: "PublishStatus", description: "Course publication status" });
|
|
66597
66693
|
var CourseGoals4 = exports_external4.object({
|
|
66598
|
-
dailyXp: exports_external4.number().int().positive().optional(),
|
|
66599
|
-
dailyLessons: exports_external4.number().int().positive().optional(),
|
|
66600
|
-
dailyActiveMinutes: exports_external4.number().int().positive().optional(),
|
|
66601
|
-
dailyAccuracy: exports_external4.number().int().min(0).max(100).optional(),
|
|
66602
|
-
dailyMasteredUnits: exports_external4.number().int().positive().optional()
|
|
66603
|
-
});
|
|
66694
|
+
dailyXp: exports_external4.number().int().positive().meta({ description: "Target XP to earn per day" }).optional(),
|
|
66695
|
+
dailyLessons: exports_external4.number().int().positive().meta({ description: "Target lessons to complete per day" }).optional(),
|
|
66696
|
+
dailyActiveMinutes: exports_external4.number().int().positive().meta({ description: "Target active learning minutes per day" }).optional(),
|
|
66697
|
+
dailyAccuracy: exports_external4.number().int().min(0).max(100).meta({ description: "Target accuracy percentage (0-100)" }).optional(),
|
|
66698
|
+
dailyMasteredUnits: exports_external4.number().int().positive().meta({ description: "Target units to master per day" }).optional()
|
|
66699
|
+
}).meta({ id: "CourseGoals", description: "Daily learning goals for a course" });
|
|
66604
66700
|
var CourseMetrics4 = exports_external4.object({
|
|
66605
|
-
totalXp: exports_external4.number().int().positive().optional(),
|
|
66606
|
-
totalLessons: exports_external4.number().int().positive().optional(),
|
|
66607
|
-
totalGrades: exports_external4.number().int().positive().optional()
|
|
66608
|
-
});
|
|
66701
|
+
totalXp: exports_external4.number().int().positive().meta({ description: "Total XP available in the course" }).optional(),
|
|
66702
|
+
totalLessons: exports_external4.number().int().positive().meta({ description: "Total number of lessons/activities" }).optional(),
|
|
66703
|
+
totalGrades: exports_external4.number().int().positive().meta({ description: "Total grade levels covered" }).optional()
|
|
66704
|
+
}).meta({ id: "CourseMetrics", description: "Aggregate metrics for a course" });
|
|
66609
66705
|
var CourseMetadata4 = exports_external4.object({
|
|
66610
66706
|
courseType: CourseType4.optional(),
|
|
66611
|
-
isSupplemental: exports_external4.boolean().optional(),
|
|
66612
|
-
isCustom: exports_external4.boolean().optional(),
|
|
66707
|
+
isSupplemental: exports_external4.boolean().meta({ description: "Whether this is supplemental to a base course" }).optional(),
|
|
66708
|
+
isCustom: exports_external4.boolean().meta({ description: "Whether this is a custom course for an individual student" }).optional(),
|
|
66613
66709
|
publishStatus: PublishStatus4.optional(),
|
|
66614
|
-
contactEmail: exports_external4.email().optional(),
|
|
66615
|
-
primaryApp: exports_external4.string().optional(),
|
|
66710
|
+
contactEmail: exports_external4.email().meta({ description: "Contact email for course issues" }).optional(),
|
|
66711
|
+
primaryApp: exports_external4.string().meta({ description: "Primary application identifier" }).optional(),
|
|
66616
66712
|
goals: CourseGoals4.optional(),
|
|
66617
66713
|
metrics: CourseMetrics4.optional()
|
|
66618
|
-
});
|
|
66714
|
+
}).meta({ id: "CourseMetadata", description: "Course metadata (matches API metadata object)" });
|
|
66619
66715
|
var CourseDefaults4 = exports_external4.object({
|
|
66620
|
-
courseCode: exports_external4.string().optional(),
|
|
66621
|
-
level: exports_external4.string().optional(),
|
|
66716
|
+
courseCode: exports_external4.string().meta({ description: "Course code (e.g., 'MATH101')" }).optional(),
|
|
66717
|
+
level: exports_external4.string().meta({ description: "Course level (e.g., 'AP', 'Honors')" }).optional(),
|
|
66622
66718
|
metadata: CourseMetadata4.optional()
|
|
66719
|
+
}).meta({
|
|
66720
|
+
id: "CourseDefaults",
|
|
66721
|
+
description: "Default properties that apply to all courses unless overridden"
|
|
66623
66722
|
});
|
|
66624
66723
|
var CourseEnvOverrides4 = exports_external4.object({
|
|
66625
|
-
level: exports_external4.string().optional(),
|
|
66626
|
-
sensor: exports_external4.
|
|
66627
|
-
launchUrl: exports_external4.
|
|
66724
|
+
level: exports_external4.string().meta({ description: "Course level for this environment" }).optional(),
|
|
66725
|
+
sensor: exports_external4.url().meta({ description: "Caliper sensor endpoint URL for this environment" }).optional(),
|
|
66726
|
+
launchUrl: exports_external4.url().meta({ description: "LTI launch URL for this environment" }).optional(),
|
|
66628
66727
|
metadata: CourseMetadata4.optional()
|
|
66728
|
+
}).meta({
|
|
66729
|
+
id: "CourseEnvOverrides",
|
|
66730
|
+
description: "Environment-specific course overrides (non-identity fields)"
|
|
66629
66731
|
});
|
|
66630
66732
|
var CourseOverrides4 = exports_external4.object({
|
|
66631
|
-
staging: CourseEnvOverrides4.
|
|
66632
|
-
|
|
66633
|
-
})
|
|
66733
|
+
staging: CourseEnvOverrides4.meta({
|
|
66734
|
+
description: "Overrides for staging environment"
|
|
66735
|
+
}).optional(),
|
|
66736
|
+
production: CourseEnvOverrides4.meta({
|
|
66737
|
+
description: "Overrides for production environment"
|
|
66738
|
+
}).optional()
|
|
66739
|
+
}).meta({ id: "CourseOverrides", description: "Per-environment course overrides" });
|
|
66634
66740
|
var CourseConfig4 = CourseDefaults4.extend({
|
|
66635
|
-
subject: TimebackSubject4,
|
|
66636
|
-
grade: TimebackGrade4.
|
|
66741
|
+
subject: TimebackSubject4.meta({ description: "Subject area for this course" }),
|
|
66742
|
+
grade: TimebackGrade4.meta({
|
|
66743
|
+
description: "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)"
|
|
66744
|
+
}).optional(),
|
|
66637
66745
|
ids: CourseIds4.nullable().optional(),
|
|
66638
|
-
sensor: exports_external4.
|
|
66639
|
-
launchUrl: exports_external4.
|
|
66746
|
+
sensor: exports_external4.url().meta({ description: "Caliper sensor endpoint URL for this course" }).optional(),
|
|
66747
|
+
launchUrl: exports_external4.url().meta({ description: "LTI launch URL for this course" }).optional(),
|
|
66640
66748
|
overrides: CourseOverrides4.optional()
|
|
66749
|
+
}).meta({
|
|
66750
|
+
id: "CourseConfig",
|
|
66751
|
+
description: "Configuration for a single course. Must have either grade or courseCode (or both)."
|
|
66641
66752
|
});
|
|
66642
66753
|
var TimebackConfig4 = exports_external4.object({
|
|
66643
|
-
|
|
66644
|
-
|
|
66645
|
-
|
|
66646
|
-
|
|
66647
|
-
|
|
66754
|
+
$schema: exports_external4.string().meta({ description: "JSON Schema reference for editor support" }).optional(),
|
|
66755
|
+
name: exports_external4.string().min(1, "App name is required").meta({ description: "Display name for your app" }),
|
|
66756
|
+
defaults: CourseDefaults4.meta({
|
|
66757
|
+
description: "Default properties applied to all courses"
|
|
66758
|
+
}).optional(),
|
|
66759
|
+
courses: exports_external4.array(CourseConfig4).min(1, "At least one course is required").meta({ description: "Courses available in this app" }),
|
|
66760
|
+
sensor: exports_external4.url().meta({ description: "Default Caliper sensor endpoint URL for all courses" }).optional(),
|
|
66761
|
+
launchUrl: exports_external4.url().meta({ description: "Default LTI launch URL for all courses" }).optional()
|
|
66762
|
+
}).meta({
|
|
66763
|
+
id: "TimebackConfig",
|
|
66764
|
+
title: "Timeback Config",
|
|
66765
|
+
description: "Configuration schema for timeback.config.json files"
|
|
66648
66766
|
}).refine((config22) => {
|
|
66649
66767
|
return config22.courses.every((c) => c.grade !== undefined || c.courseCode !== undefined);
|
|
66650
66768
|
}, {
|
|
@@ -66665,14 +66783,20 @@ var TimebackConfig4 = exports_external4.object({
|
|
|
66665
66783
|
message: "Duplicate courseCode found; each must be unique",
|
|
66666
66784
|
path: ["courses"]
|
|
66667
66785
|
}).refine((config22) => {
|
|
66668
|
-
return config22.courses.every((c) =>
|
|
66669
|
-
|
|
66670
|
-
|
|
66671
|
-
|
|
66672
|
-
|
|
66673
|
-
|
|
66786
|
+
return config22.courses.every((c) => {
|
|
66787
|
+
if (c.sensor !== undefined || config22.sensor !== undefined) {
|
|
66788
|
+
return true;
|
|
66789
|
+
}
|
|
66790
|
+
const launchUrls = [
|
|
66791
|
+
c.launchUrl,
|
|
66792
|
+
config22.launchUrl,
|
|
66793
|
+
c.overrides?.staging?.launchUrl,
|
|
66794
|
+
c.overrides?.production?.launchUrl
|
|
66795
|
+
].filter(Boolean);
|
|
66796
|
+
return launchUrls.length > 0;
|
|
66797
|
+
});
|
|
66674
66798
|
}, {
|
|
66675
|
-
message: "Each course must have an effective
|
|
66799
|
+
message: "Each course must have an effective sensor. Either set `sensor` explicitly (top-level or per-course), or provide a `launchUrl` so sensor can be derived from its origin.",
|
|
66676
66800
|
path: ["courses"]
|
|
66677
66801
|
});
|
|
66678
66802
|
var EdubridgeDateString4 = exports_external4.union([IsoDateString4, IsoDateTimeString4]);
|
|
@@ -82979,7 +83103,7 @@ var TimebackSubject5 = exports_external5.enum([
|
|
|
82979
83103
|
"Math",
|
|
82980
83104
|
"None",
|
|
82981
83105
|
"Other"
|
|
82982
|
-
]);
|
|
83106
|
+
]).meta({ id: "TimebackSubject", description: "Subject area" });
|
|
82983
83107
|
var TimebackGrade5 = exports_external5.union([
|
|
82984
83108
|
exports_external5.literal(-1),
|
|
82985
83109
|
exports_external5.literal(0),
|
|
@@ -82996,7 +83120,10 @@ var TimebackGrade5 = exports_external5.union([
|
|
|
82996
83120
|
exports_external5.literal(11),
|
|
82997
83121
|
exports_external5.literal(12),
|
|
82998
83122
|
exports_external5.literal(13)
|
|
82999
|
-
])
|
|
83123
|
+
]).meta({
|
|
83124
|
+
id: "TimebackGrade",
|
|
83125
|
+
description: "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)"
|
|
83126
|
+
});
|
|
83000
83127
|
var ScoreStatus5 = exports_external5.enum([
|
|
83001
83128
|
"exempt",
|
|
83002
83129
|
"fully graded",
|
|
@@ -83226,62 +83353,84 @@ var CaliperListEventsParams5 = exports_external5.object({
|
|
|
83226
83353
|
actorEmail: exports_external5.email().optional()
|
|
83227
83354
|
}).strict();
|
|
83228
83355
|
var CourseIds5 = exports_external5.object({
|
|
83229
|
-
staging: exports_external5.string().optional(),
|
|
83230
|
-
production: exports_external5.string().optional()
|
|
83231
|
-
});
|
|
83232
|
-
var CourseType5 = exports_external5.enum(["base", "hole-filling", "optional"]);
|
|
83233
|
-
var PublishStatus5 = exports_external5.enum(["draft", "testing", "published", "deactivated"]);
|
|
83356
|
+
staging: exports_external5.string().meta({ description: "Course ID in staging environment" }).optional(),
|
|
83357
|
+
production: exports_external5.string().meta({ description: "Course ID in production environment" }).optional()
|
|
83358
|
+
}).meta({ id: "CourseIds", description: "Environment-specific course IDs (populated by sync)" });
|
|
83359
|
+
var CourseType5 = exports_external5.enum(["base", "hole-filling", "optional"]).meta({ id: "CourseType", description: "Course classification type" });
|
|
83360
|
+
var PublishStatus5 = exports_external5.enum(["draft", "testing", "published", "deactivated"]).meta({ id: "PublishStatus", description: "Course publication status" });
|
|
83234
83361
|
var CourseGoals5 = exports_external5.object({
|
|
83235
|
-
dailyXp: exports_external5.number().int().positive().optional(),
|
|
83236
|
-
dailyLessons: exports_external5.number().int().positive().optional(),
|
|
83237
|
-
dailyActiveMinutes: exports_external5.number().int().positive().optional(),
|
|
83238
|
-
dailyAccuracy: exports_external5.number().int().min(0).max(100).optional(),
|
|
83239
|
-
dailyMasteredUnits: exports_external5.number().int().positive().optional()
|
|
83240
|
-
});
|
|
83362
|
+
dailyXp: exports_external5.number().int().positive().meta({ description: "Target XP to earn per day" }).optional(),
|
|
83363
|
+
dailyLessons: exports_external5.number().int().positive().meta({ description: "Target lessons to complete per day" }).optional(),
|
|
83364
|
+
dailyActiveMinutes: exports_external5.number().int().positive().meta({ description: "Target active learning minutes per day" }).optional(),
|
|
83365
|
+
dailyAccuracy: exports_external5.number().int().min(0).max(100).meta({ description: "Target accuracy percentage (0-100)" }).optional(),
|
|
83366
|
+
dailyMasteredUnits: exports_external5.number().int().positive().meta({ description: "Target units to master per day" }).optional()
|
|
83367
|
+
}).meta({ id: "CourseGoals", description: "Daily learning goals for a course" });
|
|
83241
83368
|
var CourseMetrics5 = exports_external5.object({
|
|
83242
|
-
totalXp: exports_external5.number().int().positive().optional(),
|
|
83243
|
-
totalLessons: exports_external5.number().int().positive().optional(),
|
|
83244
|
-
totalGrades: exports_external5.number().int().positive().optional()
|
|
83245
|
-
});
|
|
83369
|
+
totalXp: exports_external5.number().int().positive().meta({ description: "Total XP available in the course" }).optional(),
|
|
83370
|
+
totalLessons: exports_external5.number().int().positive().meta({ description: "Total number of lessons/activities" }).optional(),
|
|
83371
|
+
totalGrades: exports_external5.number().int().positive().meta({ description: "Total grade levels covered" }).optional()
|
|
83372
|
+
}).meta({ id: "CourseMetrics", description: "Aggregate metrics for a course" });
|
|
83246
83373
|
var CourseMetadata5 = exports_external5.object({
|
|
83247
83374
|
courseType: CourseType5.optional(),
|
|
83248
|
-
isSupplemental: exports_external5.boolean().optional(),
|
|
83249
|
-
isCustom: exports_external5.boolean().optional(),
|
|
83375
|
+
isSupplemental: exports_external5.boolean().meta({ description: "Whether this is supplemental to a base course" }).optional(),
|
|
83376
|
+
isCustom: exports_external5.boolean().meta({ description: "Whether this is a custom course for an individual student" }).optional(),
|
|
83250
83377
|
publishStatus: PublishStatus5.optional(),
|
|
83251
|
-
contactEmail: exports_external5.email().optional(),
|
|
83252
|
-
primaryApp: exports_external5.string().optional(),
|
|
83378
|
+
contactEmail: exports_external5.email().meta({ description: "Contact email for course issues" }).optional(),
|
|
83379
|
+
primaryApp: exports_external5.string().meta({ description: "Primary application identifier" }).optional(),
|
|
83253
83380
|
goals: CourseGoals5.optional(),
|
|
83254
83381
|
metrics: CourseMetrics5.optional()
|
|
83255
|
-
});
|
|
83382
|
+
}).meta({ id: "CourseMetadata", description: "Course metadata (matches API metadata object)" });
|
|
83256
83383
|
var CourseDefaults5 = exports_external5.object({
|
|
83257
|
-
courseCode: exports_external5.string().optional(),
|
|
83258
|
-
level: exports_external5.string().optional(),
|
|
83384
|
+
courseCode: exports_external5.string().meta({ description: "Course code (e.g., 'MATH101')" }).optional(),
|
|
83385
|
+
level: exports_external5.string().meta({ description: "Course level (e.g., 'AP', 'Honors')" }).optional(),
|
|
83259
83386
|
metadata: CourseMetadata5.optional()
|
|
83387
|
+
}).meta({
|
|
83388
|
+
id: "CourseDefaults",
|
|
83389
|
+
description: "Default properties that apply to all courses unless overridden"
|
|
83260
83390
|
});
|
|
83261
83391
|
var CourseEnvOverrides5 = exports_external5.object({
|
|
83262
|
-
level: exports_external5.string().optional(),
|
|
83263
|
-
sensor: exports_external5.
|
|
83264
|
-
launchUrl: exports_external5.
|
|
83392
|
+
level: exports_external5.string().meta({ description: "Course level for this environment" }).optional(),
|
|
83393
|
+
sensor: exports_external5.url().meta({ description: "Caliper sensor endpoint URL for this environment" }).optional(),
|
|
83394
|
+
launchUrl: exports_external5.url().meta({ description: "LTI launch URL for this environment" }).optional(),
|
|
83265
83395
|
metadata: CourseMetadata5.optional()
|
|
83396
|
+
}).meta({
|
|
83397
|
+
id: "CourseEnvOverrides",
|
|
83398
|
+
description: "Environment-specific course overrides (non-identity fields)"
|
|
83266
83399
|
});
|
|
83267
83400
|
var CourseOverrides5 = exports_external5.object({
|
|
83268
|
-
staging: CourseEnvOverrides5.
|
|
83269
|
-
|
|
83270
|
-
})
|
|
83401
|
+
staging: CourseEnvOverrides5.meta({
|
|
83402
|
+
description: "Overrides for staging environment"
|
|
83403
|
+
}).optional(),
|
|
83404
|
+
production: CourseEnvOverrides5.meta({
|
|
83405
|
+
description: "Overrides for production environment"
|
|
83406
|
+
}).optional()
|
|
83407
|
+
}).meta({ id: "CourseOverrides", description: "Per-environment course overrides" });
|
|
83271
83408
|
var CourseConfig5 = CourseDefaults5.extend({
|
|
83272
|
-
subject: TimebackSubject5,
|
|
83273
|
-
grade: TimebackGrade5.
|
|
83409
|
+
subject: TimebackSubject5.meta({ description: "Subject area for this course" }),
|
|
83410
|
+
grade: TimebackGrade5.meta({
|
|
83411
|
+
description: "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)"
|
|
83412
|
+
}).optional(),
|
|
83274
83413
|
ids: CourseIds5.nullable().optional(),
|
|
83275
|
-
sensor: exports_external5.
|
|
83276
|
-
launchUrl: exports_external5.
|
|
83414
|
+
sensor: exports_external5.url().meta({ description: "Caliper sensor endpoint URL for this course" }).optional(),
|
|
83415
|
+
launchUrl: exports_external5.url().meta({ description: "LTI launch URL for this course" }).optional(),
|
|
83277
83416
|
overrides: CourseOverrides5.optional()
|
|
83417
|
+
}).meta({
|
|
83418
|
+
id: "CourseConfig",
|
|
83419
|
+
description: "Configuration for a single course. Must have either grade or courseCode (or both)."
|
|
83278
83420
|
});
|
|
83279
83421
|
var TimebackConfig5 = exports_external5.object({
|
|
83280
|
-
|
|
83281
|
-
|
|
83282
|
-
|
|
83283
|
-
|
|
83284
|
-
|
|
83422
|
+
$schema: exports_external5.string().meta({ description: "JSON Schema reference for editor support" }).optional(),
|
|
83423
|
+
name: exports_external5.string().min(1, "App name is required").meta({ description: "Display name for your app" }),
|
|
83424
|
+
defaults: CourseDefaults5.meta({
|
|
83425
|
+
description: "Default properties applied to all courses"
|
|
83426
|
+
}).optional(),
|
|
83427
|
+
courses: exports_external5.array(CourseConfig5).min(1, "At least one course is required").meta({ description: "Courses available in this app" }),
|
|
83428
|
+
sensor: exports_external5.url().meta({ description: "Default Caliper sensor endpoint URL for all courses" }).optional(),
|
|
83429
|
+
launchUrl: exports_external5.url().meta({ description: "Default LTI launch URL for all courses" }).optional()
|
|
83430
|
+
}).meta({
|
|
83431
|
+
id: "TimebackConfig",
|
|
83432
|
+
title: "Timeback Config",
|
|
83433
|
+
description: "Configuration schema for timeback.config.json files"
|
|
83285
83434
|
}).refine((config22) => {
|
|
83286
83435
|
return config22.courses.every((c) => c.grade !== undefined || c.courseCode !== undefined);
|
|
83287
83436
|
}, {
|
|
@@ -83302,14 +83451,20 @@ var TimebackConfig5 = exports_external5.object({
|
|
|
83302
83451
|
message: "Duplicate courseCode found; each must be unique",
|
|
83303
83452
|
path: ["courses"]
|
|
83304
83453
|
}).refine((config22) => {
|
|
83305
|
-
return config22.courses.every((c) =>
|
|
83306
|
-
|
|
83307
|
-
|
|
83308
|
-
|
|
83309
|
-
|
|
83310
|
-
|
|
83454
|
+
return config22.courses.every((c) => {
|
|
83455
|
+
if (c.sensor !== undefined || config22.sensor !== undefined) {
|
|
83456
|
+
return true;
|
|
83457
|
+
}
|
|
83458
|
+
const launchUrls = [
|
|
83459
|
+
c.launchUrl,
|
|
83460
|
+
config22.launchUrl,
|
|
83461
|
+
c.overrides?.staging?.launchUrl,
|
|
83462
|
+
c.overrides?.production?.launchUrl
|
|
83463
|
+
].filter(Boolean);
|
|
83464
|
+
return launchUrls.length > 0;
|
|
83465
|
+
});
|
|
83311
83466
|
}, {
|
|
83312
|
-
message: "Each course must have an effective
|
|
83467
|
+
message: "Each course must have an effective sensor. Either set `sensor` explicitly (top-level or per-course), or provide a `launchUrl` so sensor can be derived from its origin.",
|
|
83313
83468
|
path: ["courses"]
|
|
83314
83469
|
});
|
|
83315
83470
|
var EdubridgeDateString5 = exports_external5.union([IsoDateString5, IsoDateTimeString5]);
|
|
@@ -85060,8 +85215,8 @@ var {
|
|
|
85060
85215
|
|
|
85061
85216
|
// ../internal/cli-infra/src/config/timeback.ts
|
|
85062
85217
|
import { existsSync } from "node:fs";
|
|
85063
|
-
import { basename, relative, resolve } from "node:path";
|
|
85064
|
-
import {
|
|
85218
|
+
import { basename, extname, relative, resolve } from "node:path";
|
|
85219
|
+
import { loadConfig as c12LoadConfig } from "c12";
|
|
85065
85220
|
|
|
85066
85221
|
// ../types/src/zod/primitives.ts
|
|
85067
85222
|
import { z as z6 } from "zod/v4";
|
|
@@ -85079,7 +85234,7 @@ var TimebackSubject6 = z6.enum([
|
|
|
85079
85234
|
"Math",
|
|
85080
85235
|
"None",
|
|
85081
85236
|
"Other"
|
|
85082
|
-
]);
|
|
85237
|
+
]).meta({ id: "TimebackSubject", description: "Subject area" });
|
|
85083
85238
|
var TimebackGrade6 = z6.union([
|
|
85084
85239
|
z6.literal(-1),
|
|
85085
85240
|
z6.literal(0),
|
|
@@ -85096,7 +85251,10 @@ var TimebackGrade6 = z6.union([
|
|
|
85096
85251
|
z6.literal(11),
|
|
85097
85252
|
z6.literal(12),
|
|
85098
85253
|
z6.literal(13)
|
|
85099
|
-
])
|
|
85254
|
+
]).meta({
|
|
85255
|
+
id: "TimebackGrade",
|
|
85256
|
+
description: "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)"
|
|
85257
|
+
});
|
|
85100
85258
|
var ScoreStatus6 = z6.enum([
|
|
85101
85259
|
"exempt",
|
|
85102
85260
|
"fully graded",
|
|
@@ -85330,62 +85488,84 @@ var CaliperListEventsParams6 = z8.object({
|
|
|
85330
85488
|
// ../types/src/zod/config.ts
|
|
85331
85489
|
import { z as z9 } from "zod/v4";
|
|
85332
85490
|
var CourseIds6 = z9.object({
|
|
85333
|
-
staging: z9.string().optional(),
|
|
85334
|
-
production: z9.string().optional()
|
|
85335
|
-
});
|
|
85336
|
-
var CourseType6 = z9.enum(["base", "hole-filling", "optional"]);
|
|
85337
|
-
var PublishStatus6 = z9.enum(["draft", "testing", "published", "deactivated"]);
|
|
85491
|
+
staging: z9.string().meta({ description: "Course ID in staging environment" }).optional(),
|
|
85492
|
+
production: z9.string().meta({ description: "Course ID in production environment" }).optional()
|
|
85493
|
+
}).meta({ id: "CourseIds", description: "Environment-specific course IDs (populated by sync)" });
|
|
85494
|
+
var CourseType6 = z9.enum(["base", "hole-filling", "optional"]).meta({ id: "CourseType", description: "Course classification type" });
|
|
85495
|
+
var PublishStatus6 = z9.enum(["draft", "testing", "published", "deactivated"]).meta({ id: "PublishStatus", description: "Course publication status" });
|
|
85338
85496
|
var CourseGoals6 = z9.object({
|
|
85339
|
-
dailyXp: z9.number().int().positive().optional(),
|
|
85340
|
-
dailyLessons: z9.number().int().positive().optional(),
|
|
85341
|
-
dailyActiveMinutes: z9.number().int().positive().optional(),
|
|
85342
|
-
dailyAccuracy: z9.number().int().min(0).max(100).optional(),
|
|
85343
|
-
dailyMasteredUnits: z9.number().int().positive().optional()
|
|
85344
|
-
});
|
|
85497
|
+
dailyXp: z9.number().int().positive().meta({ description: "Target XP to earn per day" }).optional(),
|
|
85498
|
+
dailyLessons: z9.number().int().positive().meta({ description: "Target lessons to complete per day" }).optional(),
|
|
85499
|
+
dailyActiveMinutes: z9.number().int().positive().meta({ description: "Target active learning minutes per day" }).optional(),
|
|
85500
|
+
dailyAccuracy: z9.number().int().min(0).max(100).meta({ description: "Target accuracy percentage (0-100)" }).optional(),
|
|
85501
|
+
dailyMasteredUnits: z9.number().int().positive().meta({ description: "Target units to master per day" }).optional()
|
|
85502
|
+
}).meta({ id: "CourseGoals", description: "Daily learning goals for a course" });
|
|
85345
85503
|
var CourseMetrics6 = z9.object({
|
|
85346
|
-
totalXp: z9.number().int().positive().optional(),
|
|
85347
|
-
totalLessons: z9.number().int().positive().optional(),
|
|
85348
|
-
totalGrades: z9.number().int().positive().optional()
|
|
85349
|
-
});
|
|
85504
|
+
totalXp: z9.number().int().positive().meta({ description: "Total XP available in the course" }).optional(),
|
|
85505
|
+
totalLessons: z9.number().int().positive().meta({ description: "Total number of lessons/activities" }).optional(),
|
|
85506
|
+
totalGrades: z9.number().int().positive().meta({ description: "Total grade levels covered" }).optional()
|
|
85507
|
+
}).meta({ id: "CourseMetrics", description: "Aggregate metrics for a course" });
|
|
85350
85508
|
var CourseMetadata6 = z9.object({
|
|
85351
85509
|
courseType: CourseType6.optional(),
|
|
85352
|
-
isSupplemental: z9.boolean().optional(),
|
|
85353
|
-
isCustom: z9.boolean().optional(),
|
|
85510
|
+
isSupplemental: z9.boolean().meta({ description: "Whether this is supplemental to a base course" }).optional(),
|
|
85511
|
+
isCustom: z9.boolean().meta({ description: "Whether this is a custom course for an individual student" }).optional(),
|
|
85354
85512
|
publishStatus: PublishStatus6.optional(),
|
|
85355
|
-
contactEmail: z9.email().optional(),
|
|
85356
|
-
primaryApp: z9.string().optional(),
|
|
85513
|
+
contactEmail: z9.email().meta({ description: "Contact email for course issues" }).optional(),
|
|
85514
|
+
primaryApp: z9.string().meta({ description: "Primary application identifier" }).optional(),
|
|
85357
85515
|
goals: CourseGoals6.optional(),
|
|
85358
85516
|
metrics: CourseMetrics6.optional()
|
|
85359
|
-
});
|
|
85517
|
+
}).meta({ id: "CourseMetadata", description: "Course metadata (matches API metadata object)" });
|
|
85360
85518
|
var CourseDefaults6 = z9.object({
|
|
85361
|
-
courseCode: z9.string().optional(),
|
|
85362
|
-
level: z9.string().optional(),
|
|
85519
|
+
courseCode: z9.string().meta({ description: "Course code (e.g., 'MATH101')" }).optional(),
|
|
85520
|
+
level: z9.string().meta({ description: "Course level (e.g., 'AP', 'Honors')" }).optional(),
|
|
85363
85521
|
metadata: CourseMetadata6.optional()
|
|
85522
|
+
}).meta({
|
|
85523
|
+
id: "CourseDefaults",
|
|
85524
|
+
description: "Default properties that apply to all courses unless overridden"
|
|
85364
85525
|
});
|
|
85365
85526
|
var CourseEnvOverrides6 = z9.object({
|
|
85366
|
-
level: z9.string().optional(),
|
|
85367
|
-
sensor: z9.
|
|
85368
|
-
launchUrl: z9.
|
|
85527
|
+
level: z9.string().meta({ description: "Course level for this environment" }).optional(),
|
|
85528
|
+
sensor: z9.url().meta({ description: "Caliper sensor endpoint URL for this environment" }).optional(),
|
|
85529
|
+
launchUrl: z9.url().meta({ description: "LTI launch URL for this environment" }).optional(),
|
|
85369
85530
|
metadata: CourseMetadata6.optional()
|
|
85531
|
+
}).meta({
|
|
85532
|
+
id: "CourseEnvOverrides",
|
|
85533
|
+
description: "Environment-specific course overrides (non-identity fields)"
|
|
85370
85534
|
});
|
|
85371
85535
|
var CourseOverrides6 = z9.object({
|
|
85372
|
-
staging: CourseEnvOverrides6.
|
|
85373
|
-
|
|
85374
|
-
})
|
|
85536
|
+
staging: CourseEnvOverrides6.meta({
|
|
85537
|
+
description: "Overrides for staging environment"
|
|
85538
|
+
}).optional(),
|
|
85539
|
+
production: CourseEnvOverrides6.meta({
|
|
85540
|
+
description: "Overrides for production environment"
|
|
85541
|
+
}).optional()
|
|
85542
|
+
}).meta({ id: "CourseOverrides", description: "Per-environment course overrides" });
|
|
85375
85543
|
var CourseConfig6 = CourseDefaults6.extend({
|
|
85376
|
-
subject: TimebackSubject6,
|
|
85377
|
-
grade: TimebackGrade6.
|
|
85544
|
+
subject: TimebackSubject6.meta({ description: "Subject area for this course" }),
|
|
85545
|
+
grade: TimebackGrade6.meta({
|
|
85546
|
+
description: "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)"
|
|
85547
|
+
}).optional(),
|
|
85378
85548
|
ids: CourseIds6.nullable().optional(),
|
|
85379
|
-
sensor: z9.
|
|
85380
|
-
launchUrl: z9.
|
|
85549
|
+
sensor: z9.url().meta({ description: "Caliper sensor endpoint URL for this course" }).optional(),
|
|
85550
|
+
launchUrl: z9.url().meta({ description: "LTI launch URL for this course" }).optional(),
|
|
85381
85551
|
overrides: CourseOverrides6.optional()
|
|
85552
|
+
}).meta({
|
|
85553
|
+
id: "CourseConfig",
|
|
85554
|
+
description: "Configuration for a single course. Must have either grade or courseCode (or both)."
|
|
85382
85555
|
});
|
|
85383
85556
|
var TimebackConfig6 = z9.object({
|
|
85384
|
-
|
|
85385
|
-
|
|
85386
|
-
|
|
85387
|
-
|
|
85388
|
-
|
|
85557
|
+
$schema: z9.string().meta({ description: "JSON Schema reference for editor support" }).optional(),
|
|
85558
|
+
name: z9.string().min(1, "App name is required").meta({ description: "Display name for your app" }),
|
|
85559
|
+
defaults: CourseDefaults6.meta({
|
|
85560
|
+
description: "Default properties applied to all courses"
|
|
85561
|
+
}).optional(),
|
|
85562
|
+
courses: z9.array(CourseConfig6).min(1, "At least one course is required").meta({ description: "Courses available in this app" }),
|
|
85563
|
+
sensor: z9.url().meta({ description: "Default Caliper sensor endpoint URL for all courses" }).optional(),
|
|
85564
|
+
launchUrl: z9.url().meta({ description: "Default LTI launch URL for all courses" }).optional()
|
|
85565
|
+
}).meta({
|
|
85566
|
+
id: "TimebackConfig",
|
|
85567
|
+
title: "Timeback Config",
|
|
85568
|
+
description: "Configuration schema for timeback.config.json files"
|
|
85389
85569
|
}).refine((config6) => {
|
|
85390
85570
|
return config6.courses.every((c) => c.grade !== undefined || c.courseCode !== undefined);
|
|
85391
85571
|
}, {
|
|
@@ -85406,14 +85586,20 @@ var TimebackConfig6 = z9.object({
|
|
|
85406
85586
|
message: "Duplicate courseCode found; each must be unique",
|
|
85407
85587
|
path: ["courses"]
|
|
85408
85588
|
}).refine((config6) => {
|
|
85409
|
-
return config6.courses.every((c) =>
|
|
85410
|
-
|
|
85411
|
-
|
|
85412
|
-
|
|
85413
|
-
|
|
85414
|
-
|
|
85589
|
+
return config6.courses.every((c) => {
|
|
85590
|
+
if (c.sensor !== undefined || config6.sensor !== undefined) {
|
|
85591
|
+
return true;
|
|
85592
|
+
}
|
|
85593
|
+
const launchUrls = [
|
|
85594
|
+
c.launchUrl,
|
|
85595
|
+
config6.launchUrl,
|
|
85596
|
+
c.overrides?.staging?.launchUrl,
|
|
85597
|
+
c.overrides?.production?.launchUrl
|
|
85598
|
+
].filter(Boolean);
|
|
85599
|
+
return launchUrls.length > 0;
|
|
85600
|
+
});
|
|
85415
85601
|
}, {
|
|
85416
|
-
message: "Each course must have an effective
|
|
85602
|
+
message: "Each course must have an effective sensor. Either set `sensor` explicitly (top-level or per-course), or provide a `launchUrl` so sensor can be derived from its origin.",
|
|
85417
85603
|
path: ["courses"]
|
|
85418
85604
|
});
|
|
85419
85605
|
// ../types/src/zod/edubridge.ts
|
|
@@ -86433,108 +86619,89 @@ var CREDENTIALS_DIR = getConfigDir();
|
|
|
86433
86619
|
var CREDENTIALS_FILE = join(CREDENTIALS_DIR, "credentials.json");
|
|
86434
86620
|
|
|
86435
86621
|
// ../internal/cli-infra/src/config/timeback.ts
|
|
86436
|
-
var
|
|
86437
|
-
|
|
86438
|
-
|
|
86439
|
-
|
|
86440
|
-
|
|
86441
|
-
|
|
86442
|
-
|
|
86443
|
-
|
|
86444
|
-
|
|
86445
|
-
|
|
86446
|
-
|
|
86447
|
-
|
|
86448
|
-
|
|
86449
|
-
|
|
86450
|
-
|
|
86451
|
-
|
|
86452
|
-
|
|
86453
|
-
|
|
86454
|
-
|
|
86455
|
-
|
|
86622
|
+
var CONFIG_FILENAME = "timeback.config.json";
|
|
86623
|
+
function isJsonConfigPath(configPath) {
|
|
86624
|
+
return extname(configPath).toLowerCase() === ".json";
|
|
86625
|
+
}
|
|
86626
|
+
async function loadWithC12(cwd, configPath) {
|
|
86627
|
+
if (configPath && !isJsonConfigPath(configPath)) {
|
|
86628
|
+
throw new Error(`Config file must be JSON (.json): ${configPath}`);
|
|
86629
|
+
}
|
|
86630
|
+
const result = await c12LoadConfig({
|
|
86631
|
+
cwd,
|
|
86632
|
+
name: "timeback",
|
|
86633
|
+
configFile: configPath ?? CONFIG_FILENAME,
|
|
86634
|
+
rcFile: false,
|
|
86635
|
+
packageJson: false,
|
|
86636
|
+
dotenv: false,
|
|
86637
|
+
envName: false,
|
|
86638
|
+
extend: false,
|
|
86639
|
+
omit$Keys: true,
|
|
86640
|
+
defaults: {},
|
|
86641
|
+
overrides: {}
|
|
86642
|
+
});
|
|
86643
|
+
if (!result.config || Object.keys(result.config).length === 0) {
|
|
86644
|
+
return null;
|
|
86645
|
+
}
|
|
86646
|
+
const rawConfig = result.config;
|
|
86647
|
+
if ("extends" in rawConfig) {
|
|
86648
|
+
throw new Error("The 'extends' feature is not supported in timeback.config.json. " + "Please inline all configuration.");
|
|
86649
|
+
}
|
|
86650
|
+
const { $schema: _schema, ...configWithoutSchema } = rawConfig;
|
|
86651
|
+
return {
|
|
86652
|
+
config: configWithoutSchema,
|
|
86653
|
+
configFile: result.configFile ?? resolve(cwd, configPath ?? CONFIG_FILENAME)
|
|
86654
|
+
};
|
|
86456
86655
|
}
|
|
86457
86656
|
async function loadConfig(opts = {}) {
|
|
86458
86657
|
const cwd = process.cwd();
|
|
86459
|
-
|
|
86460
|
-
|
|
86461
|
-
let loadError = null;
|
|
86462
|
-
if (opts.configPath) {
|
|
86463
|
-
const fullPath = resolve(cwd, opts.configPath);
|
|
86464
|
-
if (!existsSync(fullPath)) {
|
|
86658
|
+
try {
|
|
86659
|
+
if (opts.configPath && !isJsonConfigPath(opts.configPath)) {
|
|
86465
86660
|
return {
|
|
86466
86661
|
success: false,
|
|
86467
|
-
error: `Config file
|
|
86662
|
+
error: `Config file must be JSON (.json): ${opts.configPath}`
|
|
86468
86663
|
};
|
|
86469
86664
|
}
|
|
86470
|
-
|
|
86471
|
-
|
|
86472
|
-
|
|
86473
|
-
|
|
86665
|
+
if (opts.configPath) {
|
|
86666
|
+
const fullPath = resolve(cwd, opts.configPath);
|
|
86667
|
+
if (!existsSync(fullPath)) {
|
|
86668
|
+
return {
|
|
86669
|
+
success: false,
|
|
86670
|
+
error: `Config file not found: ${opts.configPath}`
|
|
86671
|
+
};
|
|
86672
|
+
}
|
|
86673
|
+
}
|
|
86674
|
+
const loaded = await loadWithC12(cwd, opts.configPath);
|
|
86675
|
+
if (!loaded) {
|
|
86474
86676
|
return {
|
|
86475
86677
|
success: false,
|
|
86476
|
-
error: `
|
|
86477
|
-
${err instanceof Error ? err.message : String(err)}`
|
|
86678
|
+
error: `No timeback config found. Run ${greenBright("timeback init")} to create one.`
|
|
86478
86679
|
};
|
|
86479
86680
|
}
|
|
86480
|
-
|
|
86481
|
-
|
|
86482
|
-
const
|
|
86483
|
-
|
|
86484
|
-
|
|
86485
|
-
|
|
86486
|
-
|
|
86487
|
-
|
|
86488
|
-
|
|
86489
|
-
} catch (err) {
|
|
86490
|
-
loadError = err instanceof Error ? err : new Error(String(err));
|
|
86491
|
-
foundPath = fullPath;
|
|
86492
|
-
break;
|
|
86493
|
-
}
|
|
86681
|
+
const result = TimebackConfig6.safeParse(loaded.config);
|
|
86682
|
+
if (!result.success) {
|
|
86683
|
+
const issues = result.error.issues.map((issue4) => ` - ${issue4.path.join(".")}: ${issue4.message}`).join(`
|
|
86684
|
+
`);
|
|
86685
|
+
return {
|
|
86686
|
+
success: false,
|
|
86687
|
+
error: `Invalid config in ${basename(loaded.configFile)}:
|
|
86688
|
+
${issues}`
|
|
86689
|
+
};
|
|
86494
86690
|
}
|
|
86495
|
-
}
|
|
86496
|
-
if (loadError && foundPath) {
|
|
86497
86691
|
return {
|
|
86498
|
-
success:
|
|
86499
|
-
|
|
86500
|
-
|
|
86692
|
+
success: true,
|
|
86693
|
+
config: result.data,
|
|
86694
|
+
configPath: loaded.configFile
|
|
86501
86695
|
};
|
|
86502
|
-
}
|
|
86503
|
-
|
|
86504
|
-
return {
|
|
86505
|
-
success: false,
|
|
86506
|
-
error: `No timeback config found. Run ${greenBright("timeback init")} to create one.`
|
|
86507
|
-
};
|
|
86508
|
-
}
|
|
86509
|
-
const result = TimebackConfig6.safeParse(rawConfig);
|
|
86510
|
-
if (!result.success) {
|
|
86511
|
-
const issues = result.error.issues.map((issue4) => ` - ${issue4.path.join(".")}: ${issue4.message}`).join(`
|
|
86512
|
-
`);
|
|
86696
|
+
} catch (err) {
|
|
86697
|
+
const filename = opts.configPath ? basename(opts.configPath) : CONFIG_FILENAME;
|
|
86513
86698
|
return {
|
|
86514
86699
|
success: false,
|
|
86515
|
-
error: `
|
|
86516
|
-
${
|
|
86700
|
+
error: `Failed to load ${filename}:
|
|
86701
|
+
${err instanceof Error ? err.message : String(err)}`
|
|
86517
86702
|
};
|
|
86518
86703
|
}
|
|
86519
|
-
return {
|
|
86520
|
-
success: true,
|
|
86521
|
-
config: result.data,
|
|
86522
|
-
configPath: foundPath
|
|
86523
|
-
};
|
|
86524
86704
|
}
|
|
86525
|
-
// src/shared/constants.ts
|
|
86526
|
-
var ROUTES = {
|
|
86527
|
-
ACTIVITY: "/activity",
|
|
86528
|
-
IDENTITY: {
|
|
86529
|
-
SIGNIN: "/identity/signin",
|
|
86530
|
-
SIGNOUT: "/identity/signout",
|
|
86531
|
-
CALLBACK: "/identity/callback"
|
|
86532
|
-
},
|
|
86533
|
-
USER: {
|
|
86534
|
-
ME: "/user/me"
|
|
86535
|
-
}
|
|
86536
|
-
};
|
|
86537
|
-
|
|
86538
86705
|
// ../internal/logger/src/debug.ts
|
|
86539
86706
|
var patterns7 = null;
|
|
86540
86707
|
var debugAll7 = false;
|
|
@@ -86952,12 +87119,32 @@ async function getUserInfo(params) {
|
|
|
86952
87119
|
return response.json();
|
|
86953
87120
|
}
|
|
86954
87121
|
// src/server/lib/utils.ts
|
|
87122
|
+
function safeIdSegment(value) {
|
|
87123
|
+
return encodeURIComponent(value).replace(/%/g, "_");
|
|
87124
|
+
}
|
|
87125
|
+
function hashSuffix64Base36(value) {
|
|
87126
|
+
let hash6 = 0xcbf29ce484222325n;
|
|
87127
|
+
const prime = 0x100000001b3n;
|
|
87128
|
+
const mod64 = 0xffffffffffffffffn;
|
|
87129
|
+
for (let i = 0;i < value.length; i++) {
|
|
87130
|
+
hash6 ^= BigInt(value.charCodeAt(i));
|
|
87131
|
+
hash6 = hash6 * prime & mod64;
|
|
87132
|
+
}
|
|
87133
|
+
const base36 = hash6.toString(36);
|
|
87134
|
+
return base36.length > 12 ? base36.slice(-12) : base36;
|
|
87135
|
+
}
|
|
86955
87136
|
function mapEnvForApi(env2) {
|
|
86956
87137
|
if (env2 === "local" || env2 === "staging") {
|
|
86957
87138
|
return "staging";
|
|
86958
87139
|
}
|
|
86959
87140
|
return "production";
|
|
86960
87141
|
}
|
|
87142
|
+
function normalizeEnv(env2) {
|
|
87143
|
+
if (env2 === "production" || env2 === "local" || env2 === "staging") {
|
|
87144
|
+
return env2;
|
|
87145
|
+
}
|
|
87146
|
+
return "staging";
|
|
87147
|
+
}
|
|
86961
87148
|
function jsonResponse(data, status = 200, headers) {
|
|
86962
87149
|
const responseHeaders = new Headers(headers);
|
|
86963
87150
|
responseHeaders.set("Content-Type", "application/json");
|
|
@@ -87092,164 +87279,21 @@ async function lookupTimebackIdByEmail(params) {
|
|
|
87092
87279
|
throw new TimebackUserResolutionError(`Failed to lookup Timeback user: ${message}`, "timeback_user_lookup_failed");
|
|
87093
87280
|
}
|
|
87094
87281
|
}
|
|
87095
|
-
|
|
87096
|
-
|
|
87097
|
-
|
|
87098
|
-
|
|
87099
|
-
|
|
87100
|
-
|
|
87101
|
-
|
|
87102
|
-
|
|
87103
|
-
|
|
87104
|
-
|
|
87105
|
-
|
|
87106
|
-
get selectorDescription() {
|
|
87107
|
-
if ("grade" in this.selector) {
|
|
87108
|
-
return `${this.selector.subject} grade ${this.selector.grade}`;
|
|
87109
|
-
}
|
|
87110
|
-
return `code "${this.selector.code}"`;
|
|
87111
|
-
}
|
|
87112
|
-
}
|
|
87113
|
-
function resolveActivityCourse(courses, courseRef) {
|
|
87114
|
-
let matches;
|
|
87115
|
-
if ("grade" in courseRef) {
|
|
87116
|
-
matches = courses.filter((c) => c.subject === courseRef.subject && c.grade === courseRef.grade);
|
|
87117
|
-
} else {
|
|
87118
|
-
matches = courses.filter((c) => c.courseCode === courseRef.code);
|
|
87119
|
-
}
|
|
87120
|
-
if (matches.length === 0) {
|
|
87121
|
-
throw new ActivityCourseResolutionError("unknown_course", courseRef);
|
|
87122
|
-
}
|
|
87123
|
-
if (matches.length > 1) {
|
|
87124
|
-
throw new ActivityCourseResolutionError("ambiguous_course", courseRef, matches.length);
|
|
87125
|
-
}
|
|
87126
|
-
return matches[0];
|
|
87127
|
-
}
|
|
87128
|
-
// src/server/lib/build-activity-events.ts
|
|
87129
|
-
class MissingSyncedCourseIdError extends Error {
|
|
87130
|
-
course;
|
|
87131
|
-
env;
|
|
87132
|
-
constructor(course, env2) {
|
|
87133
|
-
const identifier = course.grade === undefined ? course.courseCode ?? course.subject : `${course.subject} grade ${course.grade}`;
|
|
87134
|
-
super(`Course "${identifier}" is missing a synced ID for ${env2}. Run \`timeback sync\` first.`);
|
|
87135
|
-
this.name = "MissingSyncedCourseIdError";
|
|
87136
|
-
this.course = course;
|
|
87137
|
-
this.env = env2;
|
|
87138
|
-
}
|
|
87139
|
-
}
|
|
87140
|
-
function buildCourseId(course, apiEnv) {
|
|
87141
|
-
const courseId = course.ids?.[apiEnv];
|
|
87142
|
-
if (!courseId) {
|
|
87143
|
-
throw new MissingSyncedCourseIdError(course, apiEnv);
|
|
87144
|
-
}
|
|
87145
|
-
return courseId;
|
|
87146
|
-
}
|
|
87147
|
-
function buildCourseName(course) {
|
|
87148
|
-
if (course.courseCode) {
|
|
87149
|
-
return course.courseCode;
|
|
87150
|
-
}
|
|
87151
|
-
if (course.grade !== undefined) {
|
|
87152
|
-
return `${course.subject} G${String(course.grade)}`;
|
|
87282
|
+
// src/shared/constants.ts
|
|
87283
|
+
var ROUTES = {
|
|
87284
|
+
ACTIVITY: "/activity",
|
|
87285
|
+
IDENTITY: {
|
|
87286
|
+
SIGNIN: "/identity/signin",
|
|
87287
|
+
SIGNOUT: "/identity/signout",
|
|
87288
|
+
CALLBACK: "/identity/callback"
|
|
87289
|
+
},
|
|
87290
|
+
USER: {
|
|
87291
|
+
ME: "/user/me",
|
|
87292
|
+
VERIFY: "/user/verify"
|
|
87153
87293
|
}
|
|
87154
|
-
|
|
87155
|
-
}
|
|
87294
|
+
};
|
|
87156
87295
|
|
|
87157
|
-
|
|
87158
|
-
sensor;
|
|
87159
|
-
constructor(sensor) {
|
|
87160
|
-
super(`Invalid sensor URL "${sensor}". Sensor must be a valid absolute URL (e.g., "https://sensor.example.com") to support slug-based activity IDs.`);
|
|
87161
|
-
this.name = "InvalidSensorUrlError";
|
|
87162
|
-
this.sensor = sensor;
|
|
87163
|
-
}
|
|
87164
|
-
}
|
|
87165
|
-
function buildCanonicalActivityUrl(sensor, selector, slug) {
|
|
87166
|
-
let base;
|
|
87167
|
-
try {
|
|
87168
|
-
base = new URL(sensor);
|
|
87169
|
-
} catch {
|
|
87170
|
-
throw new InvalidSensorUrlError(sensor);
|
|
87171
|
-
}
|
|
87172
|
-
const pathSegment = "grade" in selector ? `${selector.subject}/g${String(selector.grade)}` : selector.code;
|
|
87173
|
-
const basePath = base.pathname.replace(/\/+$/, "");
|
|
87174
|
-
base.pathname = `${basePath}/activities/${pathSegment}/${encodeURIComponent(slug)}`;
|
|
87175
|
-
return base.toString();
|
|
87176
|
-
}
|
|
87177
|
-
function buildActivityContext(payload, course, appName, apiEnv, sensor) {
|
|
87178
|
-
return {
|
|
87179
|
-
id: buildCanonicalActivityUrl(sensor, payload.course, payload.id),
|
|
87180
|
-
type: "TimebackActivityContext",
|
|
87181
|
-
subject: course.subject,
|
|
87182
|
-
app: { name: appName },
|
|
87183
|
-
activity: { name: payload.name },
|
|
87184
|
-
course: {
|
|
87185
|
-
id: buildCourseId(course, apiEnv),
|
|
87186
|
-
name: buildCourseName(course)
|
|
87187
|
-
}
|
|
87188
|
-
};
|
|
87189
|
-
}
|
|
87190
|
-
function buildActivityMetrics(metrics) {
|
|
87191
|
-
const result = [];
|
|
87192
|
-
if (metrics.totalQuestions !== undefined) {
|
|
87193
|
-
result.push({ type: "totalQuestions", value: metrics.totalQuestions });
|
|
87194
|
-
}
|
|
87195
|
-
if (metrics.correctQuestions !== undefined) {
|
|
87196
|
-
result.push({ type: "correctQuestions", value: metrics.correctQuestions });
|
|
87197
|
-
}
|
|
87198
|
-
if (metrics.xpEarned !== undefined) {
|
|
87199
|
-
result.push({ type: "xpEarned", value: metrics.xpEarned });
|
|
87200
|
-
}
|
|
87201
|
-
if (metrics.masteredUnits !== undefined) {
|
|
87202
|
-
result.push({ type: "masteredUnits", value: metrics.masteredUnits });
|
|
87203
|
-
}
|
|
87204
|
-
return result;
|
|
87205
|
-
}
|
|
87206
|
-
function buildTimeSpentMetrics(elapsedMs, pausedMs) {
|
|
87207
|
-
const result = [{ type: "active", value: Math.max(0, elapsedMs) / 1000 }];
|
|
87208
|
-
if (pausedMs > 0) {
|
|
87209
|
-
result.push({ type: "inactive", value: Math.max(0, pausedMs) / 1000 });
|
|
87210
|
-
}
|
|
87211
|
-
return result;
|
|
87212
|
-
}
|
|
87213
|
-
async function sendCaliperEnvelope(client, sensor, activityEvent, timeSpentEvent) {
|
|
87214
|
-
await client.caliper.events.send(sensor, [activityEvent, timeSpentEvent]);
|
|
87215
|
-
}
|
|
87216
|
-
// src/server/lib/build-user-profile.ts
|
|
87217
|
-
function buildCourseLookup(courses, apiEnv) {
|
|
87218
|
-
const courseById = new Map;
|
|
87219
|
-
for (const course of courses) {
|
|
87220
|
-
const courseId = course.ids?.[apiEnv];
|
|
87221
|
-
if (courseId) {
|
|
87222
|
-
courseById.set(courseId, course);
|
|
87223
|
-
}
|
|
87224
|
-
}
|
|
87225
|
-
return courseById;
|
|
87226
|
-
}
|
|
87227
|
-
function mapEnrollmentsToCourses(enrollments, courseById) {
|
|
87228
|
-
return enrollments.map((enrollment) => {
|
|
87229
|
-
const configuredCourse = courseById.get(enrollment.course.id);
|
|
87230
|
-
return {
|
|
87231
|
-
id: enrollment.course.id,
|
|
87232
|
-
code: configuredCourse?.courseCode ?? enrollment.course.id,
|
|
87233
|
-
name: enrollment.course.title
|
|
87234
|
-
};
|
|
87235
|
-
});
|
|
87236
|
-
}
|
|
87237
|
-
function pickGoalsFromEnrollments(enrollments) {
|
|
87238
|
-
return enrollments.map((enrollment) => enrollment.metadata?.goals).find(Boolean);
|
|
87239
|
-
}
|
|
87240
|
-
function getUtcDayRange(date6) {
|
|
87241
|
-
const start = new Date(Date.UTC(date6.getUTCFullYear(), date6.getUTCMonth(), date6.getUTCDate()));
|
|
87242
|
-
const end = new Date(Date.UTC(date6.getUTCFullYear(), date6.getUTCMonth(), date6.getUTCDate(), 23, 59, 59, 999));
|
|
87243
|
-
return { start, end };
|
|
87244
|
-
}
|
|
87245
|
-
function sumXp(facts) {
|
|
87246
|
-
return Object.values(facts).reduce((dateTotal, subjects) => {
|
|
87247
|
-
return dateTotal + Object.values(subjects).reduce((subjectTotal, metrics) => {
|
|
87248
|
-
return subjectTotal + (metrics.activityMetrics?.xpEarned ?? 0);
|
|
87249
|
-
}, 0);
|
|
87250
|
-
}, 0);
|
|
87251
|
-
}
|
|
87252
|
-
// src/server/handlers/identity-full.ts
|
|
87296
|
+
// src/server/lib/sso.ts
|
|
87253
87297
|
function buildErrorContext(error57, errorCode, state, req) {
|
|
87254
87298
|
return {
|
|
87255
87299
|
error: error57,
|
|
@@ -87268,123 +87312,59 @@ function tryDecodeState(stateParam) {
|
|
|
87268
87312
|
return;
|
|
87269
87313
|
}
|
|
87270
87314
|
}
|
|
87271
|
-
function
|
|
87315
|
+
function handleIdpError(errorParam, url6, state, req, onCallbackError) {
|
|
87272
87316
|
const errorDesc = url6.searchParams.get("error_description");
|
|
87273
87317
|
ssoLog.error("IdP returned error", { error: errorParam, description: errorDesc });
|
|
87274
87318
|
const error57 = new Error(errorDesc ?? errorParam);
|
|
87275
|
-
if (
|
|
87276
|
-
return
|
|
87319
|
+
if (onCallbackError) {
|
|
87320
|
+
return onCallbackError(buildErrorContext(error57, errorParam, state, req));
|
|
87277
87321
|
}
|
|
87278
87322
|
return jsonResponse({ error: errorParam }, 400);
|
|
87279
87323
|
}
|
|
87280
|
-
function handleMissingCode(state, req,
|
|
87324
|
+
function handleMissingCode(state, req, onCallbackError) {
|
|
87281
87325
|
ssoLog.error("Missing authorization code in callback");
|
|
87282
87326
|
const error57 = new Error("Missing authorization code");
|
|
87283
|
-
if (
|
|
87284
|
-
return
|
|
87327
|
+
if (onCallbackError) {
|
|
87328
|
+
return onCallbackError(buildErrorContext(error57, "missing_code", state, req));
|
|
87285
87329
|
}
|
|
87286
87330
|
return jsonResponse({ error: "Missing authorization code" }, 400);
|
|
87287
87331
|
}
|
|
87288
|
-
async function
|
|
87289
|
-
|
|
87290
|
-
|
|
87291
|
-
return jsonResponse({ error: "SSO not configured" }, 400);
|
|
87292
|
-
}
|
|
87293
|
-
const issuer = identity.issuer ?? getIssuer(env2);
|
|
87332
|
+
async function initiateSignIn(params) {
|
|
87333
|
+
const { req, env: env2, clientId, buildState } = params;
|
|
87334
|
+
const issuer = params.issuer ?? getIssuer(env2);
|
|
87294
87335
|
const url6 = new URL(req.url);
|
|
87295
|
-
let redirectUri =
|
|
87336
|
+
let redirectUri = params.redirectUri;
|
|
87296
87337
|
if (!redirectUri) {
|
|
87297
87338
|
const basePath = url6.pathname.replace(ROUTES.IDENTITY.SIGNIN, "");
|
|
87298
87339
|
redirectUri = `${url6.origin}${basePath}${ROUTES.IDENTITY.CALLBACK}`;
|
|
87299
87340
|
}
|
|
87300
|
-
ssoLog.debug("SSO sign-in initiated", { env: env2, issuer, clientId
|
|
87301
|
-
const stateData =
|
|
87341
|
+
ssoLog.debug("SSO sign-in initiated", { env: env2, issuer, clientId, redirectUri });
|
|
87342
|
+
const stateData = buildState ? buildState({ req, url: url6 }) : {};
|
|
87302
87343
|
const state = encodeBase64Url(stateData);
|
|
87303
87344
|
const authUrl = await buildAuthorizationUrl({
|
|
87304
87345
|
issuer,
|
|
87305
|
-
clientId
|
|
87346
|
+
clientId,
|
|
87306
87347
|
redirectUri,
|
|
87307
87348
|
state
|
|
87308
87349
|
});
|
|
87309
87350
|
return redirectResponse(authUrl);
|
|
87310
87351
|
}
|
|
87311
|
-
|
|
87312
|
-
try {
|
|
87313
|
-
const issuer = identity.issuer ?? getIssuer(env2);
|
|
87314
|
-
let redirectUri = identity.redirectUri;
|
|
87315
|
-
if (!redirectUri) {
|
|
87316
|
-
const basePath = url6.pathname.replace(ROUTES.IDENTITY.CALLBACK, "");
|
|
87317
|
-
redirectUri = `${url6.origin}${basePath}${ROUTES.IDENTITY.CALLBACK}`;
|
|
87318
|
-
}
|
|
87319
|
-
ssoLog.debug("Exchanging auth code for tokens", { issuer, clientId: identity.clientId });
|
|
87320
|
-
const tokens = await exchangeCodeForTokens({
|
|
87321
|
-
issuer,
|
|
87322
|
-
clientId: identity.clientId,
|
|
87323
|
-
clientSecret: identity.clientSecret,
|
|
87324
|
-
code,
|
|
87325
|
-
redirectUri
|
|
87326
|
-
});
|
|
87327
|
-
const userInfo = await getUserInfo({ issuer, accessToken: tokens.access_token });
|
|
87328
|
-
const identities = typeof userInfo.identities === "string" ? JSON.parse(userInfo.identities) : userInfo.identities;
|
|
87329
|
-
ssoLog.debug("SSO completed, resolving Timeback user", {
|
|
87330
|
-
user: { ...userInfo, identities }
|
|
87331
|
-
});
|
|
87332
|
-
const authUser = await resolveTimebackUserByEmail({
|
|
87333
|
-
env: env2,
|
|
87334
|
-
apiCredentials: api.credentials,
|
|
87335
|
-
userInfo,
|
|
87336
|
-
client: api.getClient()
|
|
87337
|
-
});
|
|
87338
|
-
ssoLog.debug("Timeback user resolved", { timebackId: authUser.id });
|
|
87339
|
-
const ctx = {
|
|
87340
|
-
user: authUser,
|
|
87341
|
-
idp: { tokens, userInfo },
|
|
87342
|
-
state,
|
|
87343
|
-
req,
|
|
87344
|
-
redirect: redirectResponse,
|
|
87345
|
-
json: jsonResponse
|
|
87346
|
-
};
|
|
87347
|
-
return identity.onCallbackSuccess(ctx);
|
|
87348
|
-
} catch (err) {
|
|
87349
|
-
const error57 = err instanceof Error ? err : new Error("Unknown error");
|
|
87350
|
-
const errorCode = err instanceof TimebackUserResolutionError ? err.code : "token_exchange_failed";
|
|
87351
|
-
ssoLog.error("SSO callback failed", { error: error57.message, errorCode });
|
|
87352
|
-
if (identity.onCallbackError) {
|
|
87353
|
-
return identity.onCallbackError(buildErrorContext(error57, errorCode, state, req));
|
|
87354
|
-
}
|
|
87355
|
-
return jsonResponse({ error: error57.message }, 500);
|
|
87356
|
-
}
|
|
87357
|
-
}
|
|
87358
|
-
async function handleCallback(req, env2, identity, api) {
|
|
87359
|
-
if (identity.mode !== "sso") {
|
|
87360
|
-
ssoLog.warn("SSO not configured");
|
|
87361
|
-
return jsonResponse({ error: "SSO not configured" }, 400);
|
|
87362
|
-
}
|
|
87352
|
+
function parseCallback(req) {
|
|
87363
87353
|
const url6 = new URL(req.url);
|
|
87364
87354
|
const code = url6.searchParams.get("code");
|
|
87365
87355
|
const errorParam = url6.searchParams.get("error");
|
|
87366
87356
|
const stateParam = url6.searchParams.get("state");
|
|
87367
87357
|
ssoLog.debug("Received callback from IdP", { hasCode: !!code, error: errorParam });
|
|
87368
87358
|
const state = stateParam ? tryDecodeState(stateParam) : undefined;
|
|
87369
|
-
|
|
87370
|
-
return await handleCallbackError(errorParam, url6, state, req, identity);
|
|
87371
|
-
}
|
|
87372
|
-
if (!code) {
|
|
87373
|
-
return await handleMissingCode(state, req, identity);
|
|
87374
|
-
}
|
|
87375
|
-
return await completeCallback(code, url6, state, req, env2, identity, api);
|
|
87359
|
+
return { url: url6, code, errorParam, state };
|
|
87376
87360
|
}
|
|
87377
|
-
function
|
|
87378
|
-
|
|
87379
|
-
|
|
87380
|
-
|
|
87381
|
-
|
|
87382
|
-
|
|
87383
|
-
};
|
|
87361
|
+
function computeRedirectUri(url6, configuredRedirectUri) {
|
|
87362
|
+
if (configuredRedirectUri) {
|
|
87363
|
+
return configuredRedirectUri;
|
|
87364
|
+
}
|
|
87365
|
+
const basePath = url6.pathname.replace(ROUTES.IDENTITY.CALLBACK, "");
|
|
87366
|
+
return `${url6.origin}${basePath}${ROUTES.IDENTITY.CALLBACK}`;
|
|
87384
87367
|
}
|
|
87385
|
-
// src/server/handlers/activity.ts
|
|
87386
|
-
import * as z16 from "zod";
|
|
87387
|
-
|
|
87388
87368
|
// ../clients/caliper/dist/index.js
|
|
87389
87369
|
var __defProp7 = Object.defineProperty;
|
|
87390
87370
|
var __export = (target, all) => {
|
|
@@ -102317,7 +102297,7 @@ var TimebackSubject8 = exports_external6.enum([
|
|
|
102317
102297
|
"Math",
|
|
102318
102298
|
"None",
|
|
102319
102299
|
"Other"
|
|
102320
|
-
]);
|
|
102300
|
+
]).meta({ id: "TimebackSubject", description: "Subject area" });
|
|
102321
102301
|
var TimebackGrade8 = exports_external6.union([
|
|
102322
102302
|
exports_external6.literal(-1),
|
|
102323
102303
|
exports_external6.literal(0),
|
|
@@ -102334,7 +102314,10 @@ var TimebackGrade8 = exports_external6.union([
|
|
|
102334
102314
|
exports_external6.literal(11),
|
|
102335
102315
|
exports_external6.literal(12),
|
|
102336
102316
|
exports_external6.literal(13)
|
|
102337
|
-
])
|
|
102317
|
+
]).meta({
|
|
102318
|
+
id: "TimebackGrade",
|
|
102319
|
+
description: "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)"
|
|
102320
|
+
});
|
|
102338
102321
|
var ScoreStatus8 = exports_external6.enum([
|
|
102339
102322
|
"exempt",
|
|
102340
102323
|
"fully graded",
|
|
@@ -102564,62 +102547,84 @@ var CaliperListEventsParams8 = exports_external6.object({
|
|
|
102564
102547
|
actorEmail: exports_external6.email().optional()
|
|
102565
102548
|
}).strict();
|
|
102566
102549
|
var CourseIds8 = exports_external6.object({
|
|
102567
|
-
staging: exports_external6.string().optional(),
|
|
102568
|
-
production: exports_external6.string().optional()
|
|
102569
|
-
});
|
|
102570
|
-
var CourseType8 = exports_external6.enum(["base", "hole-filling", "optional"]);
|
|
102571
|
-
var PublishStatus8 = exports_external6.enum(["draft", "testing", "published", "deactivated"]);
|
|
102550
|
+
staging: exports_external6.string().meta({ description: "Course ID in staging environment" }).optional(),
|
|
102551
|
+
production: exports_external6.string().meta({ description: "Course ID in production environment" }).optional()
|
|
102552
|
+
}).meta({ id: "CourseIds", description: "Environment-specific course IDs (populated by sync)" });
|
|
102553
|
+
var CourseType8 = exports_external6.enum(["base", "hole-filling", "optional"]).meta({ id: "CourseType", description: "Course classification type" });
|
|
102554
|
+
var PublishStatus8 = exports_external6.enum(["draft", "testing", "published", "deactivated"]).meta({ id: "PublishStatus", description: "Course publication status" });
|
|
102572
102555
|
var CourseGoals8 = exports_external6.object({
|
|
102573
|
-
dailyXp: exports_external6.number().int().positive().optional(),
|
|
102574
|
-
dailyLessons: exports_external6.number().int().positive().optional(),
|
|
102575
|
-
dailyActiveMinutes: exports_external6.number().int().positive().optional(),
|
|
102576
|
-
dailyAccuracy: exports_external6.number().int().min(0).max(100).optional(),
|
|
102577
|
-
dailyMasteredUnits: exports_external6.number().int().positive().optional()
|
|
102578
|
-
});
|
|
102556
|
+
dailyXp: exports_external6.number().int().positive().meta({ description: "Target XP to earn per day" }).optional(),
|
|
102557
|
+
dailyLessons: exports_external6.number().int().positive().meta({ description: "Target lessons to complete per day" }).optional(),
|
|
102558
|
+
dailyActiveMinutes: exports_external6.number().int().positive().meta({ description: "Target active learning minutes per day" }).optional(),
|
|
102559
|
+
dailyAccuracy: exports_external6.number().int().min(0).max(100).meta({ description: "Target accuracy percentage (0-100)" }).optional(),
|
|
102560
|
+
dailyMasteredUnits: exports_external6.number().int().positive().meta({ description: "Target units to master per day" }).optional()
|
|
102561
|
+
}).meta({ id: "CourseGoals", description: "Daily learning goals for a course" });
|
|
102579
102562
|
var CourseMetrics8 = exports_external6.object({
|
|
102580
|
-
totalXp: exports_external6.number().int().positive().optional(),
|
|
102581
|
-
totalLessons: exports_external6.number().int().positive().optional(),
|
|
102582
|
-
totalGrades: exports_external6.number().int().positive().optional()
|
|
102583
|
-
});
|
|
102563
|
+
totalXp: exports_external6.number().int().positive().meta({ description: "Total XP available in the course" }).optional(),
|
|
102564
|
+
totalLessons: exports_external6.number().int().positive().meta({ description: "Total number of lessons/activities" }).optional(),
|
|
102565
|
+
totalGrades: exports_external6.number().int().positive().meta({ description: "Total grade levels covered" }).optional()
|
|
102566
|
+
}).meta({ id: "CourseMetrics", description: "Aggregate metrics for a course" });
|
|
102584
102567
|
var CourseMetadata8 = exports_external6.object({
|
|
102585
102568
|
courseType: CourseType8.optional(),
|
|
102586
|
-
isSupplemental: exports_external6.boolean().optional(),
|
|
102587
|
-
isCustom: exports_external6.boolean().optional(),
|
|
102569
|
+
isSupplemental: exports_external6.boolean().meta({ description: "Whether this is supplemental to a base course" }).optional(),
|
|
102570
|
+
isCustom: exports_external6.boolean().meta({ description: "Whether this is a custom course for an individual student" }).optional(),
|
|
102588
102571
|
publishStatus: PublishStatus8.optional(),
|
|
102589
|
-
contactEmail: exports_external6.email().optional(),
|
|
102590
|
-
primaryApp: exports_external6.string().optional(),
|
|
102572
|
+
contactEmail: exports_external6.email().meta({ description: "Contact email for course issues" }).optional(),
|
|
102573
|
+
primaryApp: exports_external6.string().meta({ description: "Primary application identifier" }).optional(),
|
|
102591
102574
|
goals: CourseGoals8.optional(),
|
|
102592
102575
|
metrics: CourseMetrics8.optional()
|
|
102593
|
-
});
|
|
102576
|
+
}).meta({ id: "CourseMetadata", description: "Course metadata (matches API metadata object)" });
|
|
102594
102577
|
var CourseDefaults8 = exports_external6.object({
|
|
102595
|
-
courseCode: exports_external6.string().optional(),
|
|
102596
|
-
level: exports_external6.string().optional(),
|
|
102578
|
+
courseCode: exports_external6.string().meta({ description: "Course code (e.g., 'MATH101')" }).optional(),
|
|
102579
|
+
level: exports_external6.string().meta({ description: "Course level (e.g., 'AP', 'Honors')" }).optional(),
|
|
102597
102580
|
metadata: CourseMetadata8.optional()
|
|
102581
|
+
}).meta({
|
|
102582
|
+
id: "CourseDefaults",
|
|
102583
|
+
description: "Default properties that apply to all courses unless overridden"
|
|
102598
102584
|
});
|
|
102599
102585
|
var CourseEnvOverrides8 = exports_external6.object({
|
|
102600
|
-
level: exports_external6.string().optional(),
|
|
102601
|
-
sensor: exports_external6.
|
|
102602
|
-
launchUrl: exports_external6.
|
|
102586
|
+
level: exports_external6.string().meta({ description: "Course level for this environment" }).optional(),
|
|
102587
|
+
sensor: exports_external6.url().meta({ description: "Caliper sensor endpoint URL for this environment" }).optional(),
|
|
102588
|
+
launchUrl: exports_external6.url().meta({ description: "LTI launch URL for this environment" }).optional(),
|
|
102603
102589
|
metadata: CourseMetadata8.optional()
|
|
102590
|
+
}).meta({
|
|
102591
|
+
id: "CourseEnvOverrides",
|
|
102592
|
+
description: "Environment-specific course overrides (non-identity fields)"
|
|
102604
102593
|
});
|
|
102605
102594
|
var CourseOverrides8 = exports_external6.object({
|
|
102606
|
-
staging: CourseEnvOverrides8.
|
|
102607
|
-
|
|
102608
|
-
})
|
|
102595
|
+
staging: CourseEnvOverrides8.meta({
|
|
102596
|
+
description: "Overrides for staging environment"
|
|
102597
|
+
}).optional(),
|
|
102598
|
+
production: CourseEnvOverrides8.meta({
|
|
102599
|
+
description: "Overrides for production environment"
|
|
102600
|
+
}).optional()
|
|
102601
|
+
}).meta({ id: "CourseOverrides", description: "Per-environment course overrides" });
|
|
102609
102602
|
var CourseConfig8 = CourseDefaults8.extend({
|
|
102610
|
-
subject: TimebackSubject8,
|
|
102611
|
-
grade: TimebackGrade8.
|
|
102603
|
+
subject: TimebackSubject8.meta({ description: "Subject area for this course" }),
|
|
102604
|
+
grade: TimebackGrade8.meta({
|
|
102605
|
+
description: "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)"
|
|
102606
|
+
}).optional(),
|
|
102612
102607
|
ids: CourseIds8.nullable().optional(),
|
|
102613
|
-
sensor: exports_external6.
|
|
102614
|
-
launchUrl: exports_external6.
|
|
102608
|
+
sensor: exports_external6.url().meta({ description: "Caliper sensor endpoint URL for this course" }).optional(),
|
|
102609
|
+
launchUrl: exports_external6.url().meta({ description: "LTI launch URL for this course" }).optional(),
|
|
102615
102610
|
overrides: CourseOverrides8.optional()
|
|
102611
|
+
}).meta({
|
|
102612
|
+
id: "CourseConfig",
|
|
102613
|
+
description: "Configuration for a single course. Must have either grade or courseCode (or both)."
|
|
102616
102614
|
});
|
|
102617
102615
|
var TimebackConfig8 = exports_external6.object({
|
|
102618
|
-
|
|
102619
|
-
|
|
102620
|
-
|
|
102621
|
-
|
|
102622
|
-
|
|
102616
|
+
$schema: exports_external6.string().meta({ description: "JSON Schema reference for editor support" }).optional(),
|
|
102617
|
+
name: exports_external6.string().min(1, "App name is required").meta({ description: "Display name for your app" }),
|
|
102618
|
+
defaults: CourseDefaults8.meta({
|
|
102619
|
+
description: "Default properties applied to all courses"
|
|
102620
|
+
}).optional(),
|
|
102621
|
+
courses: exports_external6.array(CourseConfig8).min(1, "At least one course is required").meta({ description: "Courses available in this app" }),
|
|
102622
|
+
sensor: exports_external6.url().meta({ description: "Default Caliper sensor endpoint URL for all courses" }).optional(),
|
|
102623
|
+
launchUrl: exports_external6.url().meta({ description: "Default LTI launch URL for all courses" }).optional()
|
|
102624
|
+
}).meta({
|
|
102625
|
+
id: "TimebackConfig",
|
|
102626
|
+
title: "Timeback Config",
|
|
102627
|
+
description: "Configuration schema for timeback.config.json files"
|
|
102623
102628
|
}).refine((config22) => {
|
|
102624
102629
|
return config22.courses.every((c) => c.grade !== undefined || c.courseCode !== undefined);
|
|
102625
102630
|
}, {
|
|
@@ -102640,14 +102645,20 @@ var TimebackConfig8 = exports_external6.object({
|
|
|
102640
102645
|
message: "Duplicate courseCode found; each must be unique",
|
|
102641
102646
|
path: ["courses"]
|
|
102642
102647
|
}).refine((config22) => {
|
|
102643
|
-
return config22.courses.every((c) =>
|
|
102644
|
-
|
|
102645
|
-
|
|
102646
|
-
|
|
102647
|
-
|
|
102648
|
-
|
|
102648
|
+
return config22.courses.every((c) => {
|
|
102649
|
+
if (c.sensor !== undefined || config22.sensor !== undefined) {
|
|
102650
|
+
return true;
|
|
102651
|
+
}
|
|
102652
|
+
const launchUrls = [
|
|
102653
|
+
c.launchUrl,
|
|
102654
|
+
config22.launchUrl,
|
|
102655
|
+
c.overrides?.staging?.launchUrl,
|
|
102656
|
+
c.overrides?.production?.launchUrl
|
|
102657
|
+
].filter(Boolean);
|
|
102658
|
+
return launchUrls.length > 0;
|
|
102659
|
+
});
|
|
102649
102660
|
}, {
|
|
102650
|
-
message: "Each course must have an effective
|
|
102661
|
+
message: "Each course must have an effective sensor. Either set `sensor` explicitly (top-level or per-course), or provide a `launchUrl` so sensor can be derived from its origin.",
|
|
102651
102662
|
path: ["courses"]
|
|
102652
102663
|
});
|
|
102653
102664
|
var EdubridgeDateString8 = exports_external6.union([IsoDateString8, IsoDateTimeString8]);
|
|
@@ -103930,8 +103941,337 @@ function createCaliperClient2(registry22 = DEFAULT_PROVIDER_REGISTRY7) {
|
|
|
103930
103941
|
}
|
|
103931
103942
|
var CaliperClient2 = createCaliperClient2();
|
|
103932
103943
|
|
|
103933
|
-
// src/server/handlers/activity.ts
|
|
103934
|
-
|
|
103944
|
+
// src/server/handlers/activity/caliper.ts
|
|
103945
|
+
class MissingSyncedCourseIdError extends Error {
|
|
103946
|
+
course;
|
|
103947
|
+
env;
|
|
103948
|
+
constructor(course, env2) {
|
|
103949
|
+
const identifier = course.grade === undefined ? course.courseCode ?? course.subject : `${course.subject} grade ${course.grade}`;
|
|
103950
|
+
super(`Course "${identifier}" is missing a synced ID for ${env2}. Run \`timeback resources push\` first.`);
|
|
103951
|
+
this.name = "MissingSyncedCourseIdError";
|
|
103952
|
+
this.course = course;
|
|
103953
|
+
this.env = env2;
|
|
103954
|
+
}
|
|
103955
|
+
}
|
|
103956
|
+
|
|
103957
|
+
class InvalidSensorUrlError extends Error {
|
|
103958
|
+
sensor;
|
|
103959
|
+
constructor(sensor) {
|
|
103960
|
+
super(`Invalid sensor URL "${sensor}". Sensor must be a valid absolute URL (e.g., "https://sensor.example.com") to support slug-based activity IDs.`);
|
|
103961
|
+
this.name = "InvalidSensorUrlError";
|
|
103962
|
+
this.sensor = sensor;
|
|
103963
|
+
}
|
|
103964
|
+
}
|
|
103965
|
+
function buildCourseId(course, apiEnv) {
|
|
103966
|
+
const courseId = course.ids?.[apiEnv];
|
|
103967
|
+
if (!courseId) {
|
|
103968
|
+
throw new MissingSyncedCourseIdError(course, apiEnv);
|
|
103969
|
+
}
|
|
103970
|
+
return courseId;
|
|
103971
|
+
}
|
|
103972
|
+
function buildCourseName(course) {
|
|
103973
|
+
if (course.courseCode) {
|
|
103974
|
+
return course.courseCode;
|
|
103975
|
+
}
|
|
103976
|
+
if (course.grade !== undefined) {
|
|
103977
|
+
return `${course.subject} G${String(course.grade)}`;
|
|
103978
|
+
}
|
|
103979
|
+
return course.subject;
|
|
103980
|
+
}
|
|
103981
|
+
function buildCanonicalActivityUrl(sensor, selector, slug) {
|
|
103982
|
+
let base;
|
|
103983
|
+
try {
|
|
103984
|
+
base = new URL(sensor);
|
|
103985
|
+
} catch {
|
|
103986
|
+
throw new InvalidSensorUrlError(sensor);
|
|
103987
|
+
}
|
|
103988
|
+
const pathSegment = "grade" in selector ? `${selector.subject}/g${String(selector.grade)}` : selector.code;
|
|
103989
|
+
const basePath = base.pathname.replace(/\/+$/, "");
|
|
103990
|
+
base.pathname = `${basePath}/activities/${pathSegment}/${encodeURIComponent(slug)}`;
|
|
103991
|
+
return base.toString();
|
|
103992
|
+
}
|
|
103993
|
+
function buildActivityContext(payload, course, appName, apiEnv, sensor) {
|
|
103994
|
+
return {
|
|
103995
|
+
id: buildCanonicalActivityUrl(sensor, payload.course, payload.id),
|
|
103996
|
+
type: "TimebackActivityContext",
|
|
103997
|
+
subject: course.subject,
|
|
103998
|
+
app: { name: appName },
|
|
103999
|
+
activity: { name: payload.name },
|
|
104000
|
+
course: {
|
|
104001
|
+
id: buildCourseId(course, apiEnv),
|
|
104002
|
+
name: buildCourseName(course)
|
|
104003
|
+
},
|
|
104004
|
+
process: false
|
|
104005
|
+
};
|
|
104006
|
+
}
|
|
104007
|
+
function buildActivityMetrics(metrics) {
|
|
104008
|
+
const result = [];
|
|
104009
|
+
if (metrics.totalQuestions !== undefined) {
|
|
104010
|
+
result.push({ type: "totalQuestions", value: metrics.totalQuestions });
|
|
104011
|
+
}
|
|
104012
|
+
if (metrics.correctQuestions !== undefined) {
|
|
104013
|
+
result.push({ type: "correctQuestions", value: metrics.correctQuestions });
|
|
104014
|
+
}
|
|
104015
|
+
if (metrics.xpEarned !== undefined) {
|
|
104016
|
+
result.push({ type: "xpEarned", value: metrics.xpEarned });
|
|
104017
|
+
}
|
|
104018
|
+
if (metrics.masteredUnits !== undefined) {
|
|
104019
|
+
result.push({ type: "masteredUnits", value: metrics.masteredUnits });
|
|
104020
|
+
}
|
|
104021
|
+
return result;
|
|
104022
|
+
}
|
|
104023
|
+
function buildTimeSpentMetrics(elapsedMs, pausedMs) {
|
|
104024
|
+
const result = [{ type: "active", value: Math.max(0, elapsedMs) / 1000 }];
|
|
104025
|
+
if (pausedMs > 0) {
|
|
104026
|
+
result.push({ type: "inactive", value: Math.max(0, pausedMs) / 1000 });
|
|
104027
|
+
}
|
|
104028
|
+
return result;
|
|
104029
|
+
}
|
|
104030
|
+
function buildActivityEvents(params) {
|
|
104031
|
+
const { sensor, timebackId, email: email8, payload, course, appName, apiEnv } = params;
|
|
104032
|
+
const actor = {
|
|
104033
|
+
id: `urn:timeback:user:${timebackId}`,
|
|
104034
|
+
type: "TimebackUser",
|
|
104035
|
+
email: email8
|
|
104036
|
+
};
|
|
104037
|
+
const object7 = buildActivityContext(payload, course, appName, apiEnv, sensor);
|
|
104038
|
+
const metrics = buildActivityMetrics(payload.metrics);
|
|
104039
|
+
const timeSpentMetrics = buildTimeSpentMetrics(payload.elapsedMs, payload.pausedMs);
|
|
104040
|
+
const activityEvent = createActivityEvent2({
|
|
104041
|
+
actor,
|
|
104042
|
+
object: object7,
|
|
104043
|
+
metrics,
|
|
104044
|
+
eventTime: payload.endedAt,
|
|
104045
|
+
attempt: payload.attemptNumber,
|
|
104046
|
+
generatedExtensions: payload.pctCompleteApp === undefined ? undefined : { pctCompleteApp: payload.pctCompleteApp }
|
|
104047
|
+
});
|
|
104048
|
+
const timeSpentEvent = createTimeSpentEvent2({
|
|
104049
|
+
actor,
|
|
104050
|
+
object: object7,
|
|
104051
|
+
metrics: timeSpentMetrics,
|
|
104052
|
+
eventTime: payload.endedAt
|
|
104053
|
+
});
|
|
104054
|
+
return {
|
|
104055
|
+
sensor,
|
|
104056
|
+
actor,
|
|
104057
|
+
object: object7,
|
|
104058
|
+
events: [activityEvent, timeSpentEvent],
|
|
104059
|
+
payload,
|
|
104060
|
+
course,
|
|
104061
|
+
appName,
|
|
104062
|
+
apiEnv,
|
|
104063
|
+
email: email8,
|
|
104064
|
+
timebackId
|
|
104065
|
+
};
|
|
104066
|
+
}
|
|
104067
|
+
async function sendCaliperEnvelope(client, sensor, activityEvent, timeSpentEvent) {
|
|
104068
|
+
await client.caliper.events.send(sensor, [activityEvent, timeSpentEvent]);
|
|
104069
|
+
}
|
|
104070
|
+
|
|
104071
|
+
// src/server/handlers/activity/gradebook.ts
|
|
104072
|
+
var log11 = createScopedLogger3("handlers:activity:gradebook");
|
|
104073
|
+
function buildAssessmentResultPayload(params) {
|
|
104074
|
+
const { resultId, lineItemId, timebackId, attempt, write } = params;
|
|
104075
|
+
return {
|
|
104076
|
+
sourcedId: resultId,
|
|
104077
|
+
status: "active",
|
|
104078
|
+
assessmentLineItem: { sourcedId: lineItemId },
|
|
104079
|
+
student: { sourcedId: timebackId },
|
|
104080
|
+
score: write.score,
|
|
104081
|
+
scoreDate: write.endedAt,
|
|
104082
|
+
scoreStatus: "fully graded",
|
|
104083
|
+
inProgress: "false",
|
|
104084
|
+
metadata: {
|
|
104085
|
+
totalQuestions: write.metrics.totalQuestions,
|
|
104086
|
+
correctQuestions: write.metrics.correctQuestions,
|
|
104087
|
+
accuracy: write.score,
|
|
104088
|
+
xpEarned: write.metrics.xpEarned,
|
|
104089
|
+
masteredUnits: write.metrics.masteredUnits,
|
|
104090
|
+
attempt,
|
|
104091
|
+
endedAt: write.endedAt,
|
|
104092
|
+
pctCompleteApp: write.pctCompleteApp,
|
|
104093
|
+
appName: write.appName,
|
|
104094
|
+
lastUpdated: new Date().toISOString()
|
|
104095
|
+
}
|
|
104096
|
+
};
|
|
104097
|
+
}
|
|
104098
|
+
function buildAttemptResultId(lineItemId, timebackId, attempt, endedAt) {
|
|
104099
|
+
return `${lineItemId}:${safeIdSegment(timebackId)}:attempt-${attempt}:e-${hashSuffix64Base36(endedAt)}`;
|
|
104100
|
+
}
|
|
104101
|
+
async function ensureAssessmentLineItemExists(params) {
|
|
104102
|
+
const { client, lineItemId, activityName, courseId, appName } = params;
|
|
104103
|
+
try {
|
|
104104
|
+
await client.oneroster.assessmentLineItems(lineItemId).get();
|
|
104105
|
+
return;
|
|
104106
|
+
} catch {}
|
|
104107
|
+
try {
|
|
104108
|
+
await client.oneroster.assessmentLineItems.create({
|
|
104109
|
+
sourcedId: lineItemId,
|
|
104110
|
+
title: activityName,
|
|
104111
|
+
status: "active",
|
|
104112
|
+
course: { sourcedId: courseId },
|
|
104113
|
+
resultValueMin: 0,
|
|
104114
|
+
resultValueMax: 100,
|
|
104115
|
+
metadata: {
|
|
104116
|
+
createdBy: "timeback-sdk",
|
|
104117
|
+
appName
|
|
104118
|
+
}
|
|
104119
|
+
});
|
|
104120
|
+
} catch (err) {
|
|
104121
|
+
try {
|
|
104122
|
+
await client.oneroster.assessmentLineItems(lineItemId).get();
|
|
104123
|
+
} catch {
|
|
104124
|
+
throw err;
|
|
104125
|
+
}
|
|
104126
|
+
}
|
|
104127
|
+
log11.debug("Created assessment line item", { lineItemId });
|
|
104128
|
+
}
|
|
104129
|
+
function findRetryResult(existingResults, endedAt) {
|
|
104130
|
+
return existingResults.find((result) => {
|
|
104131
|
+
const metadata = result.metadata;
|
|
104132
|
+
return result.scoreDate === endedAt || metadata?.endedAt === endedAt;
|
|
104133
|
+
});
|
|
104134
|
+
}
|
|
104135
|
+
function computeMaxAttempt(existingResults) {
|
|
104136
|
+
let maxAttempt = 0;
|
|
104137
|
+
for (const result of existingResults) {
|
|
104138
|
+
const metadata = result.metadata;
|
|
104139
|
+
const attempt = metadata?.attempt;
|
|
104140
|
+
if (typeof attempt === "number" && attempt > maxAttempt) {
|
|
104141
|
+
maxAttempt = attempt;
|
|
104142
|
+
}
|
|
104143
|
+
}
|
|
104144
|
+
return maxAttempt;
|
|
104145
|
+
}
|
|
104146
|
+
function resolveAttemptFromResult(result) {
|
|
104147
|
+
const metadata = result.metadata;
|
|
104148
|
+
return typeof metadata?.attempt === "number" && metadata.attempt >= 1 ? metadata.attempt : 1;
|
|
104149
|
+
}
|
|
104150
|
+
async function writeAssessmentResultWithAttemptReservation(params) {
|
|
104151
|
+
const { client, lineItemId, timebackId, write } = params;
|
|
104152
|
+
const existingResults = await client.oneroster.assessmentResults.listAll({
|
|
104153
|
+
where: {
|
|
104154
|
+
status: "active",
|
|
104155
|
+
"assessmentLineItem.sourcedId": lineItemId,
|
|
104156
|
+
"student.sourcedId": timebackId
|
|
104157
|
+
}
|
|
104158
|
+
});
|
|
104159
|
+
const retryResult = findRetryResult(existingResults, write.endedAt);
|
|
104160
|
+
if (retryResult) {
|
|
104161
|
+
const attempt2 = resolveAttemptFromResult(retryResult);
|
|
104162
|
+
const resultId2 = retryResult.sourcedId || buildAttemptResultId(lineItemId, timebackId, attempt2, write.endedAt);
|
|
104163
|
+
await client.oneroster.assessmentResults.update(resultId2, buildAssessmentResultPayload({
|
|
104164
|
+
resultId: resultId2,
|
|
104165
|
+
lineItemId,
|
|
104166
|
+
timebackId,
|
|
104167
|
+
attempt: attempt2,
|
|
104168
|
+
write
|
|
104169
|
+
}));
|
|
104170
|
+
log11.debug("Wrote gradebook entry (retry)", {
|
|
104171
|
+
lineItemId,
|
|
104172
|
+
resultId: resultId2,
|
|
104173
|
+
score: write.score,
|
|
104174
|
+
attempt: attempt2
|
|
104175
|
+
});
|
|
104176
|
+
return;
|
|
104177
|
+
}
|
|
104178
|
+
const attempt = computeMaxAttempt(existingResults) + 1;
|
|
104179
|
+
const resultId = buildAttemptResultId(lineItemId, timebackId, attempt, write.endedAt);
|
|
104180
|
+
await client.oneroster.assessmentResults.update(resultId, buildAssessmentResultPayload({
|
|
104181
|
+
resultId,
|
|
104182
|
+
lineItemId,
|
|
104183
|
+
timebackId,
|
|
104184
|
+
attempt,
|
|
104185
|
+
write
|
|
104186
|
+
}));
|
|
104187
|
+
log11.debug("Wrote gradebook entry (new attempt)", {
|
|
104188
|
+
lineItemId,
|
|
104189
|
+
resultId,
|
|
104190
|
+
score: write.score,
|
|
104191
|
+
attempt
|
|
104192
|
+
});
|
|
104193
|
+
}
|
|
104194
|
+
async function writeGradebookEntry(params) {
|
|
104195
|
+
const { client, courseId, activityId, activityName, timebackId, payload, appName } = params;
|
|
104196
|
+
const { metrics, endedAt, pctCompleteApp } = payload;
|
|
104197
|
+
if (metrics.totalQuestions === undefined || metrics.correctQuestions === undefined) {
|
|
104198
|
+
log11.debug("Skipping gradebook write: missing totalQuestions or correctQuestions", {
|
|
104199
|
+
activityId
|
|
104200
|
+
});
|
|
104201
|
+
return;
|
|
104202
|
+
}
|
|
104203
|
+
if (metrics.totalQuestions <= 0) {
|
|
104204
|
+
log11.debug("Skipping gradebook write: totalQuestions must be positive", {
|
|
104205
|
+
activityId,
|
|
104206
|
+
totalQuestions: metrics.totalQuestions
|
|
104207
|
+
});
|
|
104208
|
+
return;
|
|
104209
|
+
}
|
|
104210
|
+
const rawScore = metrics.correctQuestions / metrics.totalQuestions * 100;
|
|
104211
|
+
const score = Math.round(Math.min(100, Math.max(0, rawScore)));
|
|
104212
|
+
const lineItemId = `${safeIdSegment(courseId)}-${safeIdSegment(activityId)}-assessment`;
|
|
104213
|
+
try {
|
|
104214
|
+
await ensureAssessmentLineItemExists({
|
|
104215
|
+
client,
|
|
104216
|
+
lineItemId,
|
|
104217
|
+
activityName,
|
|
104218
|
+
courseId,
|
|
104219
|
+
appName
|
|
104220
|
+
});
|
|
104221
|
+
await writeAssessmentResultWithAttemptReservation({
|
|
104222
|
+
client,
|
|
104223
|
+
lineItemId,
|
|
104224
|
+
timebackId,
|
|
104225
|
+
write: { score, endedAt, metrics, pctCompleteApp, appName }
|
|
104226
|
+
});
|
|
104227
|
+
} catch (err) {
|
|
104228
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
104229
|
+
log11.warn("Failed to write gradebook entry", {
|
|
104230
|
+
lineItemId,
|
|
104231
|
+
error: message
|
|
104232
|
+
});
|
|
104233
|
+
}
|
|
104234
|
+
}
|
|
104235
|
+
|
|
104236
|
+
// src/server/handlers/activity/schema.ts
|
|
104237
|
+
import * as z16 from "zod";
|
|
104238
|
+
|
|
104239
|
+
// src/server/handlers/activity/resolve.ts
|
|
104240
|
+
class ActivityCourseResolutionError extends Error {
|
|
104241
|
+
code;
|
|
104242
|
+
selector;
|
|
104243
|
+
count;
|
|
104244
|
+
constructor(code, selector, count) {
|
|
104245
|
+
super(code);
|
|
104246
|
+
this.code = code;
|
|
104247
|
+
this.selector = selector;
|
|
104248
|
+
this.count = count;
|
|
104249
|
+
}
|
|
104250
|
+
get selectorDescription() {
|
|
104251
|
+
if ("grade" in this.selector) {
|
|
104252
|
+
return `${this.selector.subject} grade ${this.selector.grade}`;
|
|
104253
|
+
}
|
|
104254
|
+
return `code "${this.selector.code}"`;
|
|
104255
|
+
}
|
|
104256
|
+
}
|
|
104257
|
+
function resolveActivityCourse(courses, courseRef) {
|
|
104258
|
+
let matches;
|
|
104259
|
+
if ("grade" in courseRef) {
|
|
104260
|
+
matches = courses.filter((c) => c.subject === courseRef.subject && c.grade === courseRef.grade);
|
|
104261
|
+
} else {
|
|
104262
|
+
matches = courses.filter((c) => c.courseCode === courseRef.code);
|
|
104263
|
+
}
|
|
104264
|
+
if (matches.length === 0) {
|
|
104265
|
+
throw new ActivityCourseResolutionError("unknown_course", courseRef);
|
|
104266
|
+
}
|
|
104267
|
+
if (matches.length > 1) {
|
|
104268
|
+
throw new ActivityCourseResolutionError("ambiguous_course", courseRef, matches.length);
|
|
104269
|
+
}
|
|
104270
|
+
return matches[0];
|
|
104271
|
+
}
|
|
104272
|
+
|
|
104273
|
+
// src/server/handlers/activity/schema.ts
|
|
104274
|
+
var log12 = createScopedLogger3("handlers:activity:schema");
|
|
103935
104275
|
var activityMetricsSchema = z16.object({
|
|
103936
104276
|
totalQuestions: z16.number().int().nonnegative().optional(),
|
|
103937
104277
|
correctQuestions: z16.number().int().nonnegative().optional(),
|
|
@@ -103983,16 +104323,16 @@ function validateActivityRequest(body, appConfig, env2) {
|
|
|
103983
104323
|
if (err instanceof ActivityCourseResolutionError) {
|
|
103984
104324
|
const selectorDesc = formatCourseSelector(payload.course);
|
|
103985
104325
|
if (err.code === "unknown_course") {
|
|
103986
|
-
|
|
104326
|
+
log12.warn("Unknown course selector", { selector: payload.course });
|
|
103987
104327
|
return {
|
|
103988
104328
|
ok: false,
|
|
103989
104329
|
response: jsonResponse({ success: false, error: `Unknown course: ${selectorDesc}` }, 400)
|
|
103990
104330
|
};
|
|
103991
104331
|
}
|
|
103992
|
-
|
|
104332
|
+
log12.error("Ambiguous course selector", { selector: payload.course });
|
|
103993
104333
|
return {
|
|
103994
104334
|
ok: false,
|
|
103995
|
-
response: jsonResponse({ success: false, error: "Ambiguous course selector in timeback.config.
|
|
104335
|
+
response: jsonResponse({ success: false, error: "Ambiguous course selector in timeback.config.json" }, 500)
|
|
103996
104336
|
};
|
|
103997
104337
|
}
|
|
103998
104338
|
throw err;
|
|
@@ -104001,17 +104341,20 @@ function validateActivityRequest(body, appConfig, env2) {
|
|
|
104001
104341
|
const sensor = course.overrides?.[envForOverrides]?.sensor ?? course.sensor ?? appConfig.sensor;
|
|
104002
104342
|
if (!sensor) {
|
|
104003
104343
|
const selectorDesc = formatCourseSelector(payload.course);
|
|
104004
|
-
|
|
104344
|
+
log12.error("Missing sensor for course", { selector: payload.course });
|
|
104005
104345
|
return {
|
|
104006
104346
|
ok: false,
|
|
104007
104347
|
response: jsonResponse({
|
|
104008
104348
|
success: false,
|
|
104009
|
-
error: `Course "${selectorDesc}" has no sensor configured. Set 'courses[].overrides.${envForOverrides}.sensor', 'courses[].sensor', or top-level 'sensor' in timeback.config.
|
|
104349
|
+
error: `Course "${selectorDesc}" has no sensor configured. Set 'courses[].overrides.${envForOverrides}.sensor', 'courses[].sensor', or top-level 'sensor' in timeback.config.json.`
|
|
104010
104350
|
}, 500)
|
|
104011
104351
|
};
|
|
104012
104352
|
}
|
|
104013
104353
|
return { ok: true, payload, course, sensor };
|
|
104014
104354
|
}
|
|
104355
|
+
|
|
104356
|
+
// src/server/handlers/activity/handler.ts
|
|
104357
|
+
var log13 = createScopedLogger3("handlers:activity");
|
|
104015
104358
|
async function getActivityUserInfo(identity, req) {
|
|
104016
104359
|
if (identity.mode === "custom") {
|
|
104017
104360
|
const email8 = await identity.getEmail(req);
|
|
@@ -104023,55 +104366,18 @@ async function getActivityUserInfo(identity, req) {
|
|
|
104023
104366
|
function resolveTimebackId(userInfo, client) {
|
|
104024
104367
|
return userInfo.timebackId ?? lookupTimebackIdByEmail({ email: userInfo.email, client });
|
|
104025
104368
|
}
|
|
104026
|
-
function buildActivityEvents(params) {
|
|
104027
|
-
const { sensor, timebackId, email: email8, payload, course, appName, apiEnv } = params;
|
|
104028
|
-
const actor = {
|
|
104029
|
-
id: `urn:timeback:user:${timebackId}`,
|
|
104030
|
-
type: "TimebackUser",
|
|
104031
|
-
email: email8
|
|
104032
|
-
};
|
|
104033
|
-
const object7 = buildActivityContext(payload, course, appName, apiEnv, sensor);
|
|
104034
|
-
const metrics = buildActivityMetrics(payload.metrics);
|
|
104035
|
-
const timeSpentMetrics = buildTimeSpentMetrics(payload.elapsedMs, payload.pausedMs);
|
|
104036
|
-
const activityEvent = createActivityEvent2({
|
|
104037
|
-
actor,
|
|
104038
|
-
object: object7,
|
|
104039
|
-
metrics,
|
|
104040
|
-
eventTime: payload.endedAt,
|
|
104041
|
-
attempt: payload.attemptNumber,
|
|
104042
|
-
generatedExtensions: payload.pctCompleteApp === undefined ? undefined : { pctCompleteApp: payload.pctCompleteApp }
|
|
104043
|
-
});
|
|
104044
|
-
const timeSpentEvent = createTimeSpentEvent2({
|
|
104045
|
-
actor,
|
|
104046
|
-
object: object7,
|
|
104047
|
-
metrics: timeSpentMetrics,
|
|
104048
|
-
eventTime: payload.endedAt
|
|
104049
|
-
});
|
|
104050
|
-
return {
|
|
104051
|
-
sensor,
|
|
104052
|
-
actor,
|
|
104053
|
-
object: object7,
|
|
104054
|
-
events: [activityEvent, timeSpentEvent],
|
|
104055
|
-
payload,
|
|
104056
|
-
course,
|
|
104057
|
-
appName,
|
|
104058
|
-
apiEnv,
|
|
104059
|
-
email: email8,
|
|
104060
|
-
timebackId
|
|
104061
|
-
};
|
|
104062
|
-
}
|
|
104063
104369
|
function mapErrorToResponse(err, context) {
|
|
104064
104370
|
if (err instanceof TimebackUserResolutionError) {
|
|
104065
|
-
|
|
104371
|
+
log13.warn("Failed to resolve Timeback user", { code: err.code });
|
|
104066
104372
|
return jsonResponse({ success: false, error: "Unable to resolve Timeback identity" }, resolveStatusForUserResolutionError(err));
|
|
104067
104373
|
}
|
|
104068
104374
|
if (err instanceof MissingSyncedCourseIdError) {
|
|
104069
104375
|
const syncErr = err;
|
|
104070
|
-
|
|
104376
|
+
log13.error("Course not synced", { course: syncErr.course, env: syncErr.env });
|
|
104071
104377
|
return jsonResponse({ success: false, error: syncErr.message }, 500);
|
|
104072
104378
|
}
|
|
104073
104379
|
const message = err instanceof Error ? err.message : "Unknown error";
|
|
104074
|
-
|
|
104380
|
+
log13.error("Failed to submit activity", { ...context, error: message });
|
|
104075
104381
|
return jsonResponse({ success: false, error: message }, 502);
|
|
104076
104382
|
}
|
|
104077
104383
|
function createActivityHandler(config7) {
|
|
@@ -104117,8 +104423,20 @@ function createActivityHandler(config7) {
|
|
|
104117
104423
|
events: effective.events
|
|
104118
104424
|
});
|
|
104119
104425
|
}
|
|
104426
|
+
const courseId = effective.object.course?.id;
|
|
104427
|
+
if (courseId) {
|
|
104428
|
+
await writeGradebookEntry({
|
|
104429
|
+
client,
|
|
104430
|
+
courseId,
|
|
104431
|
+
activityId: effective.payload.id,
|
|
104432
|
+
activityName: effective.payload.name,
|
|
104433
|
+
timebackId: effective.timebackId,
|
|
104434
|
+
payload: effective.payload,
|
|
104435
|
+
appName: effective.appName
|
|
104436
|
+
});
|
|
104437
|
+
}
|
|
104120
104438
|
await sendCaliperEnvelope(client, effective.sensor, effective.events[0], effective.events[1]);
|
|
104121
|
-
|
|
104439
|
+
log13.debug("Submitted activity", {
|
|
104122
104440
|
courseSelector: payload.course,
|
|
104123
104441
|
activityId: payload.id
|
|
104124
104442
|
});
|
|
@@ -104133,27 +104451,191 @@ function createActivityHandler(config7) {
|
|
|
104133
104451
|
}
|
|
104134
104452
|
};
|
|
104135
104453
|
}
|
|
104136
|
-
// src/server/handlers/
|
|
104137
|
-
|
|
104454
|
+
// src/server/handlers/identity/oidc.ts
|
|
104455
|
+
async function handleSignIn(req, env2, identity) {
|
|
104456
|
+
if (identity.mode !== "sso") {
|
|
104457
|
+
ssoLog.warn("SSO not configured");
|
|
104458
|
+
return jsonResponse({ error: "SSO not configured" }, 400);
|
|
104459
|
+
}
|
|
104460
|
+
return await initiateSignIn({
|
|
104461
|
+
req,
|
|
104462
|
+
env: env2,
|
|
104463
|
+
clientId: identity.clientId,
|
|
104464
|
+
issuer: identity.issuer,
|
|
104465
|
+
redirectUri: identity.redirectUri,
|
|
104466
|
+
buildState: identity.buildState
|
|
104467
|
+
});
|
|
104468
|
+
}
|
|
104469
|
+
async function completeCallback(code, url7, state, req, env2, identity, api) {
|
|
104470
|
+
try {
|
|
104471
|
+
const issuer = identity.issuer ?? getIssuer(env2);
|
|
104472
|
+
const redirectUri = computeRedirectUri(url7, identity.redirectUri);
|
|
104473
|
+
ssoLog.debug("Exchanging auth code for tokens", { issuer, clientId: identity.clientId });
|
|
104474
|
+
const tokens = await exchangeCodeForTokens({
|
|
104475
|
+
issuer,
|
|
104476
|
+
clientId: identity.clientId,
|
|
104477
|
+
clientSecret: identity.clientSecret,
|
|
104478
|
+
code,
|
|
104479
|
+
redirectUri
|
|
104480
|
+
});
|
|
104481
|
+
const userInfo = await getUserInfo({ issuer, accessToken: tokens.access_token });
|
|
104482
|
+
const identities = typeof userInfo.identities === "string" ? JSON.parse(userInfo.identities) : userInfo.identities;
|
|
104483
|
+
ssoLog.debug("SSO completed, resolving Timeback user", {
|
|
104484
|
+
user: { ...userInfo, identities }
|
|
104485
|
+
});
|
|
104486
|
+
const authUser = await resolveTimebackUserByEmail({
|
|
104487
|
+
env: env2,
|
|
104488
|
+
apiCredentials: api.credentials,
|
|
104489
|
+
userInfo,
|
|
104490
|
+
client: api.getClient()
|
|
104491
|
+
});
|
|
104492
|
+
ssoLog.debug("Timeback user resolved", { timebackId: authUser.id });
|
|
104493
|
+
const ctx = {
|
|
104494
|
+
user: authUser,
|
|
104495
|
+
idp: { tokens, userInfo },
|
|
104496
|
+
state,
|
|
104497
|
+
req,
|
|
104498
|
+
redirect: redirectResponse,
|
|
104499
|
+
json: jsonResponse
|
|
104500
|
+
};
|
|
104501
|
+
return identity.onCallbackSuccess(ctx);
|
|
104502
|
+
} catch (err) {
|
|
104503
|
+
const error59 = err instanceof Error ? err : new Error("Unknown error");
|
|
104504
|
+
const errorCode = err instanceof TimebackUserResolutionError ? err.code : "token_exchange_failed";
|
|
104505
|
+
ssoLog.error("SSO callback failed", { error: error59.message, errorCode });
|
|
104506
|
+
if (identity.onCallbackError) {
|
|
104507
|
+
return identity.onCallbackError(buildErrorContext(error59, errorCode, state, req));
|
|
104508
|
+
}
|
|
104509
|
+
return jsonResponse({ error: error59.message }, 500);
|
|
104510
|
+
}
|
|
104511
|
+
}
|
|
104512
|
+
async function handleCallback(req, env2, identity, api) {
|
|
104513
|
+
if (identity.mode !== "sso") {
|
|
104514
|
+
ssoLog.warn("SSO not configured");
|
|
104515
|
+
return jsonResponse({ error: "SSO not configured" }, 400);
|
|
104516
|
+
}
|
|
104517
|
+
const { url: url7, code, errorParam, state } = parseCallback(req);
|
|
104518
|
+
if (errorParam) {
|
|
104519
|
+
return handleIdpError(errorParam, url7, state, req, identity.onCallbackError);
|
|
104520
|
+
}
|
|
104521
|
+
if (!code) {
|
|
104522
|
+
return handleMissingCode(state, req, identity.onCallbackError);
|
|
104523
|
+
}
|
|
104524
|
+
return await completeCallback(code, url7, state, req, env2, identity, api);
|
|
104525
|
+
}
|
|
104526
|
+
|
|
104527
|
+
// src/server/handlers/identity/handler.ts
|
|
104528
|
+
function createIdentityHandlers(params) {
|
|
104529
|
+
const { env: env2, identity, api } = params;
|
|
104530
|
+
return {
|
|
104531
|
+
signIn: (req) => handleSignIn(req, env2, identity),
|
|
104532
|
+
callback: (req) => handleCallback(req, env2, identity, api),
|
|
104533
|
+
signOut: () => redirectResponse("/")
|
|
104534
|
+
};
|
|
104535
|
+
}
|
|
104536
|
+
// src/server/handlers/user/enrollments.ts
|
|
104537
|
+
function buildCourseLookup(courses, apiEnv) {
|
|
104538
|
+
const courseById = new Map;
|
|
104539
|
+
for (const course of courses) {
|
|
104540
|
+
const courseId = course.ids?.[apiEnv];
|
|
104541
|
+
if (courseId) {
|
|
104542
|
+
courseById.set(courseId, course);
|
|
104543
|
+
}
|
|
104544
|
+
}
|
|
104545
|
+
return courseById;
|
|
104546
|
+
}
|
|
104547
|
+
function mapEnrollmentsToCourses(enrollments, courseById) {
|
|
104548
|
+
return enrollments.map((enrollment) => {
|
|
104549
|
+
const configuredCourse = courseById.get(enrollment.course.id);
|
|
104550
|
+
return {
|
|
104551
|
+
id: enrollment.course.id,
|
|
104552
|
+
code: configuredCourse?.courseCode ?? enrollment.course.id,
|
|
104553
|
+
name: enrollment.course.title
|
|
104554
|
+
};
|
|
104555
|
+
});
|
|
104556
|
+
}
|
|
104557
|
+
function pickGoalsFromEnrollments(enrollments) {
|
|
104558
|
+
return enrollments.map((enrollment) => enrollment.metadata?.goals).find(Boolean);
|
|
104559
|
+
}
|
|
104560
|
+
function getUtcDayRange(date10) {
|
|
104561
|
+
const start = new Date(Date.UTC(date10.getUTCFullYear(), date10.getUTCMonth(), date10.getUTCDate()));
|
|
104562
|
+
const end = new Date(Date.UTC(date10.getUTCFullYear(), date10.getUTCMonth(), date10.getUTCDate(), 23, 59, 59, 999));
|
|
104563
|
+
return { start, end };
|
|
104564
|
+
}
|
|
104565
|
+
function sumXp(facts) {
|
|
104566
|
+
return Object.values(facts).reduce((dateTotal, subjects) => {
|
|
104567
|
+
return dateTotal + Object.values(subjects).reduce((subjectTotal, metrics) => {
|
|
104568
|
+
return subjectTotal + (metrics.activityMetrics?.xpEarned ?? 0);
|
|
104569
|
+
}, 0);
|
|
104570
|
+
}, 0);
|
|
104571
|
+
}
|
|
104572
|
+
|
|
104573
|
+
// src/server/handlers/user/profile.ts
|
|
104574
|
+
async function buildUserProfile(client, user, appConfig, apiEnv) {
|
|
104575
|
+
const enrollments = await client.edubridge.enrollments.list({
|
|
104576
|
+
userId: user.id
|
|
104577
|
+
});
|
|
104578
|
+
const courseById = buildCourseLookup(appConfig.courses, apiEnv);
|
|
104579
|
+
const courses = mapEnrollmentsToCourses(enrollments, courseById);
|
|
104580
|
+
const goals = pickGoalsFromEnrollments(enrollments);
|
|
104581
|
+
const { start: todayStart, end: todayEnd } = getUtcDayRange(new Date);
|
|
104582
|
+
const [todayFacts, allFacts] = await Promise.all([
|
|
104583
|
+
client.edubridge.analytics.getActivity({
|
|
104584
|
+
studentId: user.id,
|
|
104585
|
+
startDate: todayStart.toISOString(),
|
|
104586
|
+
endDate: todayEnd.toISOString()
|
|
104587
|
+
}),
|
|
104588
|
+
client.edubridge.analytics.getActivity({
|
|
104589
|
+
studentId: user.id,
|
|
104590
|
+
startDate: "2000-01-01",
|
|
104591
|
+
endDate: todayEnd.toISOString()
|
|
104592
|
+
})
|
|
104593
|
+
]);
|
|
104594
|
+
return {
|
|
104595
|
+
id: user.id,
|
|
104596
|
+
email: user.email,
|
|
104597
|
+
name: user.name,
|
|
104598
|
+
school: user.school,
|
|
104599
|
+
grade: user.grade,
|
|
104600
|
+
courses: courses.length ? courses : undefined,
|
|
104601
|
+
goals,
|
|
104602
|
+
xp: {
|
|
104603
|
+
today: sumXp(todayFacts),
|
|
104604
|
+
all: sumXp(allFacts)
|
|
104605
|
+
}
|
|
104606
|
+
};
|
|
104607
|
+
}
|
|
104608
|
+
|
|
104609
|
+
// src/server/handlers/user/handler.ts
|
|
104610
|
+
var log14 = createScopedLogger3("handlers:user");
|
|
104611
|
+
async function getUserIdentity(identity, req) {
|
|
104612
|
+
if (identity.mode === "custom") {
|
|
104613
|
+
const email8 = await identity.getEmail(req);
|
|
104614
|
+
if (!email8)
|
|
104615
|
+
return;
|
|
104616
|
+
return { sub: email8, email: email8 };
|
|
104617
|
+
}
|
|
104618
|
+
const user = await identity.getUser(req);
|
|
104619
|
+
if (!user)
|
|
104620
|
+
return;
|
|
104621
|
+
return { sub: user.id, email: user.email };
|
|
104622
|
+
}
|
|
104623
|
+
function mapResolutionErrorToResponse(error59) {
|
|
104624
|
+
log14.warn("Timeback user resolution failed", { code: error59.code });
|
|
104625
|
+
if (error59.code === "timeback_user_ambiguous") {
|
|
104626
|
+
return jsonResponse({ error: "Timeback user resolution ambiguous" }, 409);
|
|
104627
|
+
}
|
|
104628
|
+
if (error59.code === "timeback_user_not_found") {
|
|
104629
|
+
return jsonResponse({ error: "Timeback user not found" }, 404);
|
|
104630
|
+
}
|
|
104631
|
+
return jsonResponse({ error: "User resolution failed" }, 500);
|
|
104632
|
+
}
|
|
104138
104633
|
function createUserHandler(config7) {
|
|
104139
104634
|
return async (req) => {
|
|
104140
104635
|
try {
|
|
104141
|
-
|
|
104142
|
-
|
|
104143
|
-
|
|
104144
|
-
const email8 = await config7.identity.getEmail(req);
|
|
104145
|
-
if (!email8) {
|
|
104146
|
-
return jsonResponse({ error: "Unauthorized" }, 401);
|
|
104147
|
-
}
|
|
104148
|
-
userSub = email8;
|
|
104149
|
-
userEmail = email8;
|
|
104150
|
-
} else {
|
|
104151
|
-
const user = await config7.identity.getUser(req);
|
|
104152
|
-
if (!user) {
|
|
104153
|
-
return jsonResponse({ error: "Unauthorized" }, 401);
|
|
104154
|
-
}
|
|
104155
|
-
userSub = user.id;
|
|
104156
|
-
userEmail = user.email;
|
|
104636
|
+
const userIdentity = await getUserIdentity(config7.identity, req);
|
|
104637
|
+
if (!userIdentity) {
|
|
104638
|
+
return jsonResponse({ error: "Unauthorized" }, 401);
|
|
104157
104639
|
}
|
|
104158
104640
|
const apiEnv = mapEnvForApi(config7.env);
|
|
104159
104641
|
const client = new TimebackClient({
|
|
@@ -104167,65 +104649,85 @@ function createUserHandler(config7) {
|
|
|
104167
104649
|
const resolved = await resolveTimebackUserByEmail({
|
|
104168
104650
|
env: config7.env,
|
|
104169
104651
|
apiCredentials: config7.api,
|
|
104170
|
-
userInfo:
|
|
104171
|
-
sub: userSub,
|
|
104172
|
-
email: userEmail
|
|
104173
|
-
},
|
|
104652
|
+
userInfo: userIdentity,
|
|
104174
104653
|
client
|
|
104175
104654
|
});
|
|
104176
|
-
const
|
|
104177
|
-
userId: resolved.id
|
|
104178
|
-
});
|
|
104179
|
-
const courseById = buildCourseLookup(config7.appConfig.courses, apiEnv);
|
|
104180
|
-
const courses = mapEnrollmentsToCourses(enrollments, courseById);
|
|
104181
|
-
const goals = pickGoalsFromEnrollments(enrollments);
|
|
104182
|
-
const { start: todayStart, end: todayEnd } = getUtcDayRange(new Date);
|
|
104183
|
-
const [todayFacts, allFacts] = await Promise.all([
|
|
104184
|
-
client.edubridge.analytics.getActivity({
|
|
104185
|
-
studentId: resolved.id,
|
|
104186
|
-
startDate: todayStart.toISOString(),
|
|
104187
|
-
endDate: todayEnd.toISOString()
|
|
104188
|
-
}),
|
|
104189
|
-
client.edubridge.analytics.getActivity({
|
|
104190
|
-
studentId: resolved.id,
|
|
104191
|
-
startDate: "2000-01-01",
|
|
104192
|
-
endDate: todayEnd.toISOString()
|
|
104193
|
-
})
|
|
104194
|
-
]);
|
|
104195
|
-
const profile = {
|
|
104196
|
-
id: resolved.id,
|
|
104197
|
-
email: resolved.email,
|
|
104198
|
-
name: resolved.name,
|
|
104199
|
-
school: resolved.school,
|
|
104200
|
-
grade: resolved.grade,
|
|
104201
|
-
courses: courses.length ? courses : undefined,
|
|
104202
|
-
goals,
|
|
104203
|
-
xp: {
|
|
104204
|
-
today: sumXp(todayFacts),
|
|
104205
|
-
all: sumXp(allFacts)
|
|
104206
|
-
}
|
|
104207
|
-
};
|
|
104655
|
+
const profile = await buildUserProfile(client, resolved, config7.appConfig, apiEnv);
|
|
104208
104656
|
return jsonResponse(profile);
|
|
104209
104657
|
} catch (error59) {
|
|
104210
104658
|
if (error59 instanceof TimebackUserResolutionError) {
|
|
104211
|
-
|
|
104659
|
+
return mapResolutionErrorToResponse(error59);
|
|
104660
|
+
}
|
|
104661
|
+
const message = error59 instanceof Error ? error59.message : "Unknown error";
|
|
104662
|
+
log14.error("Failed to build user profile", { error: message });
|
|
104663
|
+
return jsonResponse({ error: message }, 502);
|
|
104664
|
+
} finally {
|
|
104665
|
+
client.close();
|
|
104666
|
+
}
|
|
104667
|
+
} catch (error59) {
|
|
104668
|
+
const message = error59 instanceof Error ? error59.message : "Unknown error";
|
|
104669
|
+
log14.error("Unhandled error in user handler", { error: message });
|
|
104670
|
+
return jsonResponse({ error: message }, 500);
|
|
104671
|
+
}
|
|
104672
|
+
};
|
|
104673
|
+
}
|
|
104674
|
+
// src/server/handlers/user/verify.ts
|
|
104675
|
+
var log15 = createScopedLogger3("handlers:user:verify");
|
|
104676
|
+
async function getUserIdentity2(identity, req) {
|
|
104677
|
+
if (identity.mode === "custom") {
|
|
104678
|
+
const email8 = await identity.getEmail(req);
|
|
104679
|
+
if (!email8)
|
|
104680
|
+
return;
|
|
104681
|
+
return { sub: email8, email: email8 };
|
|
104682
|
+
}
|
|
104683
|
+
const user = await identity.getUser(req);
|
|
104684
|
+
if (!user)
|
|
104685
|
+
return;
|
|
104686
|
+
return { sub: user.id, email: user.email };
|
|
104687
|
+
}
|
|
104688
|
+
function createUserVerifyHandler(config7) {
|
|
104689
|
+
return async (req) => {
|
|
104690
|
+
try {
|
|
104691
|
+
const userIdentity = await getUserIdentity2(config7.identity, req);
|
|
104692
|
+
if (!userIdentity) {
|
|
104693
|
+
return jsonResponse({ verified: false, error: "Unauthorized" }, 401);
|
|
104694
|
+
}
|
|
104695
|
+
const client = new TimebackClient({
|
|
104696
|
+
env: mapEnvForApi(config7.env),
|
|
104697
|
+
auth: {
|
|
104698
|
+
clientId: config7.api.clientId,
|
|
104699
|
+
clientSecret: config7.api.clientSecret
|
|
104700
|
+
}
|
|
104701
|
+
});
|
|
104702
|
+
try {
|
|
104703
|
+
const resolved = await resolveTimebackUserByEmail({
|
|
104704
|
+
env: config7.env,
|
|
104705
|
+
apiCredentials: config7.api,
|
|
104706
|
+
userInfo: userIdentity,
|
|
104707
|
+
client
|
|
104708
|
+
});
|
|
104709
|
+
return jsonResponse({ verified: true, timebackId: resolved.id });
|
|
104710
|
+
} catch (error59) {
|
|
104711
|
+
if (error59 instanceof TimebackUserResolutionError) {
|
|
104712
|
+
log15.warn("Timeback user resolution failed", { code: error59.code });
|
|
104212
104713
|
if (error59.code === "timeback_user_ambiguous") {
|
|
104213
|
-
return jsonResponse({ error: "Timeback user resolution ambiguous" }, 409);
|
|
104714
|
+
return jsonResponse({ verified: false, error: "Timeback user resolution ambiguous" }, 409);
|
|
104214
104715
|
}
|
|
104215
104716
|
if (error59.code === "timeback_user_not_found") {
|
|
104216
|
-
return jsonResponse({
|
|
104717
|
+
return jsonResponse({ verified: false });
|
|
104217
104718
|
}
|
|
104719
|
+
return jsonResponse({ verified: false, error: error59.message }, 502);
|
|
104218
104720
|
}
|
|
104219
104721
|
const message = error59 instanceof Error ? error59.message : "Unknown error";
|
|
104220
|
-
|
|
104221
|
-
return jsonResponse({ error: message }, 502);
|
|
104722
|
+
log15.error("Failed to verify user", { error: message });
|
|
104723
|
+
return jsonResponse({ verified: false, error: message }, 502);
|
|
104222
104724
|
} finally {
|
|
104223
104725
|
client.close();
|
|
104224
104726
|
}
|
|
104225
104727
|
} catch (error59) {
|
|
104226
104728
|
const message = error59 instanceof Error ? error59.message : "Unknown error";
|
|
104227
|
-
|
|
104228
|
-
return jsonResponse({ error: message }, 500);
|
|
104729
|
+
log15.error("Unhandled error in verify handler", { error: message });
|
|
104730
|
+
return jsonResponse({ verified: false, error: message }, 500);
|
|
104229
104731
|
}
|
|
104230
104732
|
};
|
|
104231
104733
|
}
|
|
@@ -104240,6 +104742,7 @@ function toAppCourses(courses) {
|
|
|
104240
104742
|
});
|
|
104241
104743
|
}
|
|
104242
104744
|
async function createTimeback(config7) {
|
|
104745
|
+
const env2 = normalizeEnv(config7.env);
|
|
104243
104746
|
const configResult = await loadConfig({ configPath: config7.configPath });
|
|
104244
104747
|
if (!configResult.success) {
|
|
104245
104748
|
throw new Error(`Failed to load timeback config: ${configResult.error}`);
|
|
@@ -104249,7 +104752,7 @@ async function createTimeback(config7) {
|
|
|
104249
104752
|
const getApiClient = () => {
|
|
104250
104753
|
if (!apiClient) {
|
|
104251
104754
|
apiClient = new TimebackClient({
|
|
104252
|
-
env: mapEnvForApi(
|
|
104755
|
+
env: mapEnvForApi(env2),
|
|
104253
104756
|
auth: {
|
|
104254
104757
|
clientId: config7.api.clientId,
|
|
104255
104758
|
clientSecret: config7.api.clientSecret
|
|
@@ -104259,7 +104762,7 @@ async function createTimeback(config7) {
|
|
|
104259
104762
|
return apiClient;
|
|
104260
104763
|
};
|
|
104261
104764
|
const activity = createActivityHandler({
|
|
104262
|
-
env:
|
|
104765
|
+
env: env2,
|
|
104263
104766
|
identity: config7.identity,
|
|
104264
104767
|
appConfig: {
|
|
104265
104768
|
name: appConfig.name,
|
|
@@ -104270,7 +104773,7 @@ async function createTimeback(config7) {
|
|
|
104270
104773
|
hooks: config7.hooks
|
|
104271
104774
|
});
|
|
104272
104775
|
const identity = createIdentityHandlers({
|
|
104273
|
-
env:
|
|
104776
|
+
env: env2,
|
|
104274
104777
|
identity: config7.identity,
|
|
104275
104778
|
api: {
|
|
104276
104779
|
credentials: config7.api,
|
|
@@ -104278,7 +104781,7 @@ async function createTimeback(config7) {
|
|
|
104278
104781
|
}
|
|
104279
104782
|
});
|
|
104280
104783
|
const user = createUserHandler({
|
|
104281
|
-
env:
|
|
104784
|
+
env: env2,
|
|
104282
104785
|
identity: config7.identity,
|
|
104283
104786
|
api: config7.api,
|
|
104284
104787
|
appConfig: {
|
|
@@ -104287,13 +104790,19 @@ async function createTimeback(config7) {
|
|
|
104287
104790
|
courses: toAppCourses(appConfig.courses)
|
|
104288
104791
|
}
|
|
104289
104792
|
});
|
|
104793
|
+
const userVerify = createUserVerifyHandler({
|
|
104794
|
+
env: env2,
|
|
104795
|
+
identity: config7.identity,
|
|
104796
|
+
api: config7.api
|
|
104797
|
+
});
|
|
104290
104798
|
const instance = {
|
|
104291
104799
|
config: config7,
|
|
104292
104800
|
handle: {
|
|
104293
104801
|
activity,
|
|
104294
104802
|
identity,
|
|
104295
104803
|
user: {
|
|
104296
|
-
me: user
|
|
104804
|
+
me: user,
|
|
104805
|
+
verify: userVerify
|
|
104297
104806
|
}
|
|
104298
104807
|
},
|
|
104299
104808
|
get api() {
|
|
@@ -104302,74 +104811,21 @@ async function createTimeback(config7) {
|
|
|
104302
104811
|
};
|
|
104303
104812
|
return instance;
|
|
104304
104813
|
}
|
|
104305
|
-
// src/server/handlers/identity-only.ts
|
|
104306
|
-
function buildErrorContext2(error59, errorCode, state, req) {
|
|
104307
|
-
return {
|
|
104308
|
-
error: error59,
|
|
104309
|
-
errorCode,
|
|
104310
|
-
state,
|
|
104311
|
-
req,
|
|
104312
|
-
redirect: redirectResponse,
|
|
104313
|
-
json: jsonResponse
|
|
104314
|
-
};
|
|
104315
|
-
}
|
|
104316
|
-
function tryDecodeState2(stateParam) {
|
|
104317
|
-
try {
|
|
104318
|
-
return decodeBase64Url(stateParam);
|
|
104319
|
-
} catch {
|
|
104320
|
-
ssoLog.warn("Failed to decode state");
|
|
104321
|
-
return;
|
|
104322
|
-
}
|
|
104323
|
-
}
|
|
104324
|
-
function handleCallbackError2(errorParam, url7, state, req, identity) {
|
|
104325
|
-
const errorDesc = url7.searchParams.get("error_description");
|
|
104326
|
-
ssoLog.error("IdP returned error", { error: errorParam, description: errorDesc });
|
|
104327
|
-
const error59 = new Error(errorDesc ?? errorParam);
|
|
104328
|
-
if (identity.onCallbackError) {
|
|
104329
|
-
return identity.onCallbackError(buildErrorContext2(error59, errorParam, state, req));
|
|
104330
|
-
}
|
|
104331
|
-
return jsonResponse({ error: errorParam }, 400);
|
|
104332
|
-
}
|
|
104333
|
-
function handleMissingCode2(state, req, identity) {
|
|
104334
|
-
ssoLog.error("Missing authorization code in callback");
|
|
104335
|
-
const error59 = new Error("Missing authorization code");
|
|
104336
|
-
if (identity.onCallbackError) {
|
|
104337
|
-
return identity.onCallbackError(buildErrorContext2(error59, "missing_code", state, req));
|
|
104338
|
-
}
|
|
104339
|
-
return jsonResponse({ error: "Missing authorization code" }, 400);
|
|
104340
|
-
}
|
|
104814
|
+
// src/server/handlers/identity-only/oidc.ts
|
|
104341
104815
|
async function handleSignIn2(req, env2, identity) {
|
|
104342
|
-
|
|
104343
|
-
|
|
104344
|
-
let redirectUri = identity.redirectUri;
|
|
104345
|
-
if (!redirectUri) {
|
|
104346
|
-
const basePath = url7.pathname.replace(ROUTES.IDENTITY.SIGNIN, "");
|
|
104347
|
-
redirectUri = `${url7.origin}${basePath}${ROUTES.IDENTITY.CALLBACK}`;
|
|
104348
|
-
}
|
|
104349
|
-
ssoLog.debug("SSO sign-in initiated (identity-only)", {
|
|
104816
|
+
return await initiateSignIn({
|
|
104817
|
+
req,
|
|
104350
104818
|
env: env2,
|
|
104351
|
-
issuer,
|
|
104352
|
-
clientId: identity.clientId,
|
|
104353
|
-
redirectUri
|
|
104354
|
-
});
|
|
104355
|
-
const stateData = identity.buildState ? identity.buildState({ req, url: url7 }) : {};
|
|
104356
|
-
const state = encodeBase64Url(stateData);
|
|
104357
|
-
const authUrl = await buildAuthorizationUrl({
|
|
104358
|
-
issuer,
|
|
104359
104819
|
clientId: identity.clientId,
|
|
104360
|
-
|
|
104361
|
-
|
|
104820
|
+
issuer: identity.issuer,
|
|
104821
|
+
redirectUri: identity.redirectUri,
|
|
104822
|
+
buildState: identity.buildState
|
|
104362
104823
|
});
|
|
104363
|
-
return redirectResponse(authUrl);
|
|
104364
104824
|
}
|
|
104365
|
-
async function
|
|
104825
|
+
async function completeCallback2(code, url7, state, req, env2, identity) {
|
|
104366
104826
|
try {
|
|
104367
104827
|
const issuer = identity.issuer ?? getIssuer(env2);
|
|
104368
|
-
|
|
104369
|
-
if (!redirectUri) {
|
|
104370
|
-
const basePath = url7.pathname.replace(ROUTES.IDENTITY.CALLBACK, "");
|
|
104371
|
-
redirectUri = `${url7.origin}${basePath}${ROUTES.IDENTITY.CALLBACK}`;
|
|
104372
|
-
}
|
|
104828
|
+
const redirectUri = computeRedirectUri(url7, identity.redirectUri);
|
|
104373
104829
|
ssoLog.debug("Exchanging auth code for tokens (identity-only)", {
|
|
104374
104830
|
issuer,
|
|
104375
104831
|
clientId: identity.clientId
|
|
@@ -104397,29 +104853,23 @@ async function completeCallbackIdentityOnly(code, url7, state, req, env2, identi
|
|
|
104397
104853
|
const error59 = err instanceof Error ? err : new Error("Unknown error");
|
|
104398
104854
|
ssoLog.error("Token exchange failed (identity-only)", { error: error59.message });
|
|
104399
104855
|
if (identity.onCallbackError) {
|
|
104400
|
-
return identity.onCallbackError(
|
|
104856
|
+
return identity.onCallbackError(buildErrorContext(error59, undefined, state, req));
|
|
104401
104857
|
}
|
|
104402
104858
|
return jsonResponse({ error: error59.message }, 500);
|
|
104403
104859
|
}
|
|
104404
104860
|
}
|
|
104405
|
-
async function
|
|
104406
|
-
const url7 =
|
|
104407
|
-
const code = url7.searchParams.get("code");
|
|
104408
|
-
const errorParam = url7.searchParams.get("error");
|
|
104409
|
-
const stateParam = url7.searchParams.get("state");
|
|
104410
|
-
ssoLog.debug("Received callback from IdP (identity-only)", {
|
|
104411
|
-
hasCode: !!code,
|
|
104412
|
-
error: errorParam
|
|
104413
|
-
});
|
|
104414
|
-
const state = stateParam ? tryDecodeState2(stateParam) : undefined;
|
|
104861
|
+
async function handleCallback2(req, env2, identity) {
|
|
104862
|
+
const { url: url7, code, errorParam, state } = parseCallback(req);
|
|
104415
104863
|
if (errorParam) {
|
|
104416
|
-
return
|
|
104864
|
+
return handleIdpError(errorParam, url7, state, req, identity.onCallbackError);
|
|
104417
104865
|
}
|
|
104418
104866
|
if (!code) {
|
|
104419
|
-
return
|
|
104867
|
+
return handleMissingCode(state, req, identity.onCallbackError);
|
|
104420
104868
|
}
|
|
104421
|
-
return await
|
|
104869
|
+
return await completeCallback2(code, url7, state, req, env2, identity);
|
|
104422
104870
|
}
|
|
104871
|
+
|
|
104872
|
+
// src/server/handlers/identity-only/handler.ts
|
|
104423
104873
|
function createIdentityOnlyHandlers(params) {
|
|
104424
104874
|
const { env: env2, identity } = params;
|
|
104425
104875
|
if (identity.mode !== "sso") {
|
|
@@ -104427,15 +104877,14 @@ function createIdentityOnlyHandlers(params) {
|
|
|
104427
104877
|
}
|
|
104428
104878
|
return {
|
|
104429
104879
|
signIn: (req) => handleSignIn2(req, env2, identity),
|
|
104430
|
-
callback: (req) =>
|
|
104880
|
+
callback: (req) => handleCallback2(req, env2, identity),
|
|
104431
104881
|
signOut: () => redirectResponse("/")
|
|
104432
104882
|
};
|
|
104433
104883
|
}
|
|
104434
|
-
|
|
104435
104884
|
// src/server/timeback-identity.ts
|
|
104436
104885
|
function createTimebackIdentity(config7) {
|
|
104437
104886
|
const identity = createIdentityOnlyHandlers({
|
|
104438
|
-
env: config7.env,
|
|
104887
|
+
env: normalizeEnv(config7.env),
|
|
104439
104888
|
identity: config7.identity
|
|
104440
104889
|
});
|
|
104441
104890
|
return {
|