@timeback/sdk 0.1.6 → 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.
Files changed (117) hide show
  1. package/dist/client/adapters/react/hooks/types.d.ts +15 -0
  2. package/dist/client/adapters/react/hooks/types.d.ts.map +1 -0
  3. package/dist/client/adapters/react/hooks/useTimebackVerification.d.ts +18 -0
  4. package/dist/client/adapters/react/hooks/useTimebackVerification.d.ts.map +1 -0
  5. package/dist/client/adapters/react/index.d.ts +2 -0
  6. package/dist/client/adapters/react/index.d.ts.map +1 -1
  7. package/dist/client/adapters/react/index.js +139 -9
  8. package/dist/client/auth/bearer.d.ts +17 -0
  9. package/dist/client/auth/bearer.d.ts.map +1 -0
  10. package/dist/client/auth/index.d.ts +3 -0
  11. package/dist/client/auth/index.d.ts.map +1 -0
  12. package/dist/client/auth/types.d.ts +39 -0
  13. package/dist/client/auth/types.d.ts.map +1 -0
  14. package/dist/client/index.d.ts +2 -0
  15. package/dist/client/index.d.ts.map +1 -1
  16. package/dist/client/lib/fetch.d.ts +19 -0
  17. package/dist/client/lib/fetch.d.ts.map +1 -0
  18. package/dist/client/namespaces/user.d.ts +25 -2
  19. package/dist/client/namespaces/user.d.ts.map +1 -1
  20. package/dist/client/timeback-client.class.d.ts +15 -0
  21. package/dist/client/timeback-client.class.d.ts.map +1 -1
  22. package/dist/client/timeback-client.d.ts +3 -0
  23. package/dist/client/timeback-client.d.ts.map +1 -1
  24. package/dist/client.d.ts +2 -1
  25. package/dist/client.d.ts.map +1 -1
  26. package/dist/client.js +69 -6
  27. package/dist/edge.js +85291 -169
  28. package/dist/identity.js +85186 -74
  29. package/dist/index.js +773 -493
  30. package/dist/server/adapters/express.d.ts.map +1 -1
  31. package/dist/server/adapters/express.js +169 -193
  32. package/dist/server/adapters/native.d.ts.map +1 -1
  33. package/dist/server/adapters/native.js +32 -1
  34. package/dist/server/adapters/nextjs.js +32 -1
  35. package/dist/server/adapters/nuxt.d.ts.map +1 -1
  36. package/dist/server/adapters/nuxt.js +173 -193
  37. package/dist/server/adapters/solid-start.d.ts.map +1 -1
  38. package/dist/server/adapters/solid-start.js +173 -193
  39. package/dist/server/adapters/svelte-kit.d.ts.map +1 -1
  40. package/dist/server/adapters/svelte-kit.js +37 -1
  41. package/dist/server/adapters/tanstack-start.d.ts.map +1 -1
  42. package/dist/server/adapters/tanstack-start.js +168 -193
  43. package/dist/server/adapters/utils.d.ts +1 -1
  44. package/dist/server/adapters/utils.d.ts.map +1 -1
  45. package/dist/server/{lib/build-activity-events.d.ts → handlers/activity/caliper.d.ts} +29 -4
  46. package/dist/server/handlers/activity/caliper.d.ts.map +1 -0
  47. package/dist/server/handlers/activity/gradebook.d.ts +56 -0
  48. package/dist/server/handlers/activity/gradebook.d.ts.map +1 -0
  49. package/dist/server/handlers/activity/handler.d.ts +15 -0
  50. package/dist/server/handlers/activity/handler.d.ts.map +1 -0
  51. package/dist/server/handlers/activity/index.d.ts +9 -0
  52. package/dist/server/handlers/activity/index.d.ts.map +1 -0
  53. package/dist/server/handlers/activity/resolve.d.ts +39 -0
  54. package/dist/server/handlers/activity/resolve.d.ts.map +1 -0
  55. package/dist/server/handlers/activity/schema.d.ts +52 -0
  56. package/dist/server/handlers/activity/schema.d.ts.map +1 -0
  57. package/dist/server/handlers/activity/types.d.ts +52 -0
  58. package/dist/server/handlers/activity/types.d.ts.map +1 -0
  59. package/dist/server/handlers/identity/handler.d.ts +14 -0
  60. package/dist/server/handlers/identity/handler.d.ts.map +1 -0
  61. package/dist/server/handlers/identity/index.d.ts +8 -0
  62. package/dist/server/handlers/identity/index.d.ts.map +1 -0
  63. package/dist/server/handlers/identity/oidc.d.ts +43 -0
  64. package/dist/server/handlers/identity/oidc.d.ts.map +1 -0
  65. package/dist/server/handlers/identity/types.d.ts +24 -0
  66. package/dist/server/handlers/identity/types.d.ts.map +1 -0
  67. package/dist/server/handlers/identity-only/handler.d.ts +15 -0
  68. package/dist/server/handlers/identity-only/handler.d.ts.map +1 -0
  69. package/dist/server/handlers/identity-only/index.d.ts +8 -0
  70. package/dist/server/handlers/identity-only/index.d.ts.map +1 -0
  71. package/dist/server/handlers/identity-only/oidc.d.ts +26 -0
  72. package/dist/server/handlers/identity-only/oidc.d.ts.map +1 -0
  73. package/dist/server/handlers/identity-only/types.d.ts +19 -0
  74. package/dist/server/handlers/identity-only/types.d.ts.map +1 -0
  75. package/dist/server/handlers/index.d.ts +5 -2
  76. package/dist/server/handlers/index.d.ts.map +1 -1
  77. package/dist/server/{lib/build-user-profile.d.ts → handlers/user/enrollments.d.ts} +7 -2
  78. package/dist/server/handlers/user/enrollments.d.ts.map +1 -0
  79. package/dist/server/handlers/user/handler.d.ts +17 -0
  80. package/dist/server/handlers/user/handler.d.ts.map +1 -0
  81. package/dist/server/handlers/user/index.d.ts +10 -0
  82. package/dist/server/handlers/user/index.d.ts.map +1 -0
  83. package/dist/server/handlers/user/profile.d.ts +22 -0
  84. package/dist/server/handlers/user/profile.d.ts.map +1 -0
  85. package/dist/server/handlers/user/types.d.ts +35 -0
  86. package/dist/server/handlers/user/types.d.ts.map +1 -0
  87. package/dist/server/handlers/user/verify.d.ts +25 -0
  88. package/dist/server/handlers/user/verify.d.ts.map +1 -0
  89. package/dist/server/index.d.ts +1 -1
  90. package/dist/server/index.d.ts.map +1 -1
  91. package/dist/server/lib/index.d.ts +4 -5
  92. package/dist/server/lib/index.d.ts.map +1 -1
  93. package/dist/server/lib/resolve.d.ts +4 -42
  94. package/dist/server/lib/resolve.d.ts.map +1 -1
  95. package/dist/server/lib/sso.d.ts +86 -0
  96. package/dist/server/lib/sso.d.ts.map +1 -0
  97. package/dist/server/lib/utils.d.ts +32 -1
  98. package/dist/server/lib/utils.d.ts.map +1 -1
  99. package/dist/server/timeback-identity.d.ts.map +1 -1
  100. package/dist/server/timeback.d.ts.map +1 -1
  101. package/dist/server/types.d.ts +16 -9
  102. package/dist/server/types.d.ts.map +1 -1
  103. package/dist/shared/constants.d.ts +1 -0
  104. package/dist/shared/constants.d.ts.map +1 -1
  105. package/dist/shared/types.d.ts +15 -0
  106. package/dist/shared/types.d.ts.map +1 -1
  107. package/package.json +6 -2
  108. package/dist/server/handlers/activity.d.ts +0 -25
  109. package/dist/server/handlers/activity.d.ts.map +0 -1
  110. package/dist/server/handlers/identity-full.d.ts +0 -28
  111. package/dist/server/handlers/identity-full.d.ts.map +0 -1
  112. package/dist/server/handlers/identity-only.d.ts +0 -22
  113. package/dist/server/handlers/identity-only.d.ts.map +0 -1
  114. package/dist/server/handlers/user.d.ts +0 -31
  115. package/dist/server/handlers/user.d.ts.map +0 -1
  116. package/dist/server/lib/build-activity-events.d.ts.map +0 -1
  117. package/dist/server/lib/build-user-profile.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -15297,14 +15297,20 @@ var TimebackConfig = exports_external.object({
15297
15297
  message: "Duplicate courseCode found; each must be unique",
15298
15298
  path: ["courses"]
15299
15299
  }).refine((config2) => {
15300
- return config2.courses.every((c) => c.sensor !== undefined || config2.sensor !== undefined);
15301
- }, {
15302
- message: "Each course must have an effective sensor; set a top-level `sensor` or per-course `sensor`",
15303
- path: ["courses"]
15304
- }).refine((config2) => {
15305
- return config2.courses.every((c) => c.launchUrl !== undefined || config2.launchUrl !== undefined);
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
+ });
15306
15312
  }, {
15307
- message: "Each course must have an effective launchUrl; set a top-level `launchUrl` or per-course `launchUrl`",
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.",
15308
15314
  path: ["courses"]
15309
15315
  });
15310
15316
  var EdubridgeDateString = exports_external.union([IsoDateString, IsoDateTimeString]);
@@ -31700,14 +31706,20 @@ var TimebackConfig2 = exports_external2.object({
31700
31706
  message: "Duplicate courseCode found; each must be unique",
31701
31707
  path: ["courses"]
31702
31708
  }).refine((config22) => {
31703
- return config22.courses.every((c) => c.sensor !== undefined || config22.sensor !== undefined);
31704
- }, {
31705
- message: "Each course must have an effective sensor; set a top-level `sensor` or per-course `sensor`",
31706
- path: ["courses"]
31707
- }).refine((config22) => {
31708
- return config22.courses.every((c) => c.launchUrl !== undefined || config22.launchUrl !== undefined);
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
+ });
31709
31721
  }, {
31710
- message: "Each course must have an effective launchUrl; set a top-level `launchUrl` or per-course `launchUrl`",
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.",
31711
31723
  path: ["courses"]
31712
31724
  });
31713
31725
  var EdubridgeDateString2 = exports_external2.union([IsoDateString2, IsoDateTimeString2]);
@@ -49133,14 +49145,20 @@ var TimebackConfig3 = exports_external3.object({
49133
49145
  message: "Duplicate courseCode found; each must be unique",
49134
49146
  path: ["courses"]
49135
49147
  }).refine((config22) => {
49136
- return config22.courses.every((c) => c.sensor !== undefined || config22.sensor !== undefined);
49137
- }, {
49138
- message: "Each course must have an effective sensor; set a top-level `sensor` or per-course `sensor`",
49139
- path: ["courses"]
49140
- }).refine((config22) => {
49141
- return config22.courses.every((c) => c.launchUrl !== undefined || config22.launchUrl !== undefined);
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
+ });
49142
49160
  }, {
49143
- message: "Each course must have an effective launchUrl; set a top-level `launchUrl` or per-course `launchUrl`",
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.",
49144
49162
  path: ["courses"]
49145
49163
  });
49146
49164
  var EdubridgeDateString3 = exports_external3.union([IsoDateString3, IsoDateTimeString3]);
@@ -66765,14 +66783,20 @@ var TimebackConfig4 = exports_external4.object({
66765
66783
  message: "Duplicate courseCode found; each must be unique",
66766
66784
  path: ["courses"]
66767
66785
  }).refine((config22) => {
66768
- return config22.courses.every((c) => c.sensor !== undefined || config22.sensor !== undefined);
66769
- }, {
66770
- message: "Each course must have an effective sensor; set a top-level `sensor` or per-course `sensor`",
66771
- path: ["courses"]
66772
- }).refine((config22) => {
66773
- return config22.courses.every((c) => c.launchUrl !== undefined || config22.launchUrl !== undefined);
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
+ });
66774
66798
  }, {
66775
- message: "Each course must have an effective launchUrl; set a top-level `launchUrl` or per-course `launchUrl`",
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.",
66776
66800
  path: ["courses"]
66777
66801
  });
66778
66802
  var EdubridgeDateString4 = exports_external4.union([IsoDateString4, IsoDateTimeString4]);
@@ -83427,14 +83451,20 @@ var TimebackConfig5 = exports_external5.object({
83427
83451
  message: "Duplicate courseCode found; each must be unique",
83428
83452
  path: ["courses"]
83429
83453
  }).refine((config22) => {
83430
- return config22.courses.every((c) => c.sensor !== undefined || config22.sensor !== undefined);
83431
- }, {
83432
- message: "Each course must have an effective sensor; set a top-level `sensor` or per-course `sensor`",
83433
- path: ["courses"]
83434
- }).refine((config22) => {
83435
- return config22.courses.every((c) => c.launchUrl !== undefined || config22.launchUrl !== undefined);
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
+ });
83436
83466
  }, {
83437
- message: "Each course must have an effective launchUrl; set a top-level `launchUrl` or per-course `launchUrl`",
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.",
83438
83468
  path: ["courses"]
83439
83469
  });
83440
83470
  var EdubridgeDateString5 = exports_external5.union([IsoDateString5, IsoDateTimeString5]);
@@ -85556,14 +85586,20 @@ var TimebackConfig6 = z9.object({
85556
85586
  message: "Duplicate courseCode found; each must be unique",
85557
85587
  path: ["courses"]
85558
85588
  }).refine((config6) => {
85559
- return config6.courses.every((c) => c.sensor !== undefined || config6.sensor !== undefined);
85560
- }, {
85561
- message: "Each course must have an effective sensor; set a top-level `sensor` or per-course `sensor`",
85562
- path: ["courses"]
85563
- }).refine((config6) => {
85564
- return config6.courses.every((c) => c.launchUrl !== undefined || config6.launchUrl !== undefined);
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
+ });
85565
85601
  }, {
85566
- message: "Each course must have an effective launchUrl; set a top-level `launchUrl` or per-course `launchUrl`",
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.",
85567
85603
  path: ["courses"]
85568
85604
  });
85569
85605
  // ../types/src/zod/edubridge.ts
@@ -86666,19 +86702,6 @@ ${err instanceof Error ? err.message : String(err)}`
86666
86702
  };
86667
86703
  }
86668
86704
  }
86669
- // src/shared/constants.ts
86670
- var ROUTES = {
86671
- ACTIVITY: "/activity",
86672
- IDENTITY: {
86673
- SIGNIN: "/identity/signin",
86674
- SIGNOUT: "/identity/signout",
86675
- CALLBACK: "/identity/callback"
86676
- },
86677
- USER: {
86678
- ME: "/user/me"
86679
- }
86680
- };
86681
-
86682
86705
  // ../internal/logger/src/debug.ts
86683
86706
  var patterns7 = null;
86684
86707
  var debugAll7 = false;
@@ -87096,12 +87119,32 @@ async function getUserInfo(params) {
87096
87119
  return response.json();
87097
87120
  }
87098
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
+ }
87099
87136
  function mapEnvForApi(env2) {
87100
87137
  if (env2 === "local" || env2 === "staging") {
87101
87138
  return "staging";
87102
87139
  }
87103
87140
  return "production";
87104
87141
  }
87142
+ function normalizeEnv(env2) {
87143
+ if (env2 === "production" || env2 === "local" || env2 === "staging") {
87144
+ return env2;
87145
+ }
87146
+ return "staging";
87147
+ }
87105
87148
  function jsonResponse(data, status = 200, headers) {
87106
87149
  const responseHeaders = new Headers(headers);
87107
87150
  responseHeaders.set("Content-Type", "application/json");
@@ -87236,164 +87279,21 @@ async function lookupTimebackIdByEmail(params) {
87236
87279
  throw new TimebackUserResolutionError(`Failed to lookup Timeback user: ${message}`, "timeback_user_lookup_failed");
87237
87280
  }
87238
87281
  }
87239
-
87240
- class ActivityCourseResolutionError extends Error {
87241
- code;
87242
- selector;
87243
- count;
87244
- constructor(code, selector, count) {
87245
- super(code);
87246
- this.code = code;
87247
- this.selector = selector;
87248
- this.count = count;
87249
- }
87250
- get selectorDescription() {
87251
- if ("grade" in this.selector) {
87252
- return `${this.selector.subject} grade ${this.selector.grade}`;
87253
- }
87254
- return `code "${this.selector.code}"`;
87255
- }
87256
- }
87257
- function resolveActivityCourse(courses, courseRef) {
87258
- let matches;
87259
- if ("grade" in courseRef) {
87260
- matches = courses.filter((c) => c.subject === courseRef.subject && c.grade === courseRef.grade);
87261
- } else {
87262
- matches = courses.filter((c) => c.courseCode === courseRef.code);
87263
- }
87264
- if (matches.length === 0) {
87265
- throw new ActivityCourseResolutionError("unknown_course", courseRef);
87266
- }
87267
- if (matches.length > 1) {
87268
- throw new ActivityCourseResolutionError("ambiguous_course", courseRef, matches.length);
87269
- }
87270
- return matches[0];
87271
- }
87272
- // src/server/lib/build-activity-events.ts
87273
- class MissingSyncedCourseIdError extends Error {
87274
- course;
87275
- env;
87276
- constructor(course, env2) {
87277
- const identifier = course.grade === undefined ? course.courseCode ?? course.subject : `${course.subject} grade ${course.grade}`;
87278
- super(`Course "${identifier}" is missing a synced ID for ${env2}. Run \`timeback sync\` first.`);
87279
- this.name = "MissingSyncedCourseIdError";
87280
- this.course = course;
87281
- this.env = env2;
87282
- }
87283
- }
87284
- function buildCourseId(course, apiEnv) {
87285
- const courseId = course.ids?.[apiEnv];
87286
- if (!courseId) {
87287
- throw new MissingSyncedCourseIdError(course, apiEnv);
87288
- }
87289
- return courseId;
87290
- }
87291
- function buildCourseName(course) {
87292
- if (course.courseCode) {
87293
- return course.courseCode;
87294
- }
87295
- if (course.grade !== undefined) {
87296
- 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"
87297
87293
  }
87298
- return course.subject;
87299
- }
87294
+ };
87300
87295
 
87301
- class InvalidSensorUrlError extends Error {
87302
- sensor;
87303
- constructor(sensor) {
87304
- super(`Invalid sensor URL "${sensor}". Sensor must be a valid absolute URL (e.g., "https://sensor.example.com") to support slug-based activity IDs.`);
87305
- this.name = "InvalidSensorUrlError";
87306
- this.sensor = sensor;
87307
- }
87308
- }
87309
- function buildCanonicalActivityUrl(sensor, selector, slug) {
87310
- let base;
87311
- try {
87312
- base = new URL(sensor);
87313
- } catch {
87314
- throw new InvalidSensorUrlError(sensor);
87315
- }
87316
- const pathSegment = "grade" in selector ? `${selector.subject}/g${String(selector.grade)}` : selector.code;
87317
- const basePath = base.pathname.replace(/\/+$/, "");
87318
- base.pathname = `${basePath}/activities/${pathSegment}/${encodeURIComponent(slug)}`;
87319
- return base.toString();
87320
- }
87321
- function buildActivityContext(payload, course, appName, apiEnv, sensor) {
87322
- return {
87323
- id: buildCanonicalActivityUrl(sensor, payload.course, payload.id),
87324
- type: "TimebackActivityContext",
87325
- subject: course.subject,
87326
- app: { name: appName },
87327
- activity: { name: payload.name },
87328
- course: {
87329
- id: buildCourseId(course, apiEnv),
87330
- name: buildCourseName(course)
87331
- }
87332
- };
87333
- }
87334
- function buildActivityMetrics(metrics) {
87335
- const result = [];
87336
- if (metrics.totalQuestions !== undefined) {
87337
- result.push({ type: "totalQuestions", value: metrics.totalQuestions });
87338
- }
87339
- if (metrics.correctQuestions !== undefined) {
87340
- result.push({ type: "correctQuestions", value: metrics.correctQuestions });
87341
- }
87342
- if (metrics.xpEarned !== undefined) {
87343
- result.push({ type: "xpEarned", value: metrics.xpEarned });
87344
- }
87345
- if (metrics.masteredUnits !== undefined) {
87346
- result.push({ type: "masteredUnits", value: metrics.masteredUnits });
87347
- }
87348
- return result;
87349
- }
87350
- function buildTimeSpentMetrics(elapsedMs, pausedMs) {
87351
- const result = [{ type: "active", value: Math.max(0, elapsedMs) / 1000 }];
87352
- if (pausedMs > 0) {
87353
- result.push({ type: "inactive", value: Math.max(0, pausedMs) / 1000 });
87354
- }
87355
- return result;
87356
- }
87357
- async function sendCaliperEnvelope(client, sensor, activityEvent, timeSpentEvent) {
87358
- await client.caliper.events.send(sensor, [activityEvent, timeSpentEvent]);
87359
- }
87360
- // src/server/lib/build-user-profile.ts
87361
- function buildCourseLookup(courses, apiEnv) {
87362
- const courseById = new Map;
87363
- for (const course of courses) {
87364
- const courseId = course.ids?.[apiEnv];
87365
- if (courseId) {
87366
- courseById.set(courseId, course);
87367
- }
87368
- }
87369
- return courseById;
87370
- }
87371
- function mapEnrollmentsToCourses(enrollments, courseById) {
87372
- return enrollments.map((enrollment) => {
87373
- const configuredCourse = courseById.get(enrollment.course.id);
87374
- return {
87375
- id: enrollment.course.id,
87376
- code: configuredCourse?.courseCode ?? enrollment.course.id,
87377
- name: enrollment.course.title
87378
- };
87379
- });
87380
- }
87381
- function pickGoalsFromEnrollments(enrollments) {
87382
- return enrollments.map((enrollment) => enrollment.metadata?.goals).find(Boolean);
87383
- }
87384
- function getUtcDayRange(date6) {
87385
- const start = new Date(Date.UTC(date6.getUTCFullYear(), date6.getUTCMonth(), date6.getUTCDate()));
87386
- const end = new Date(Date.UTC(date6.getUTCFullYear(), date6.getUTCMonth(), date6.getUTCDate(), 23, 59, 59, 999));
87387
- return { start, end };
87388
- }
87389
- function sumXp(facts) {
87390
- return Object.values(facts).reduce((dateTotal, subjects) => {
87391
- return dateTotal + Object.values(subjects).reduce((subjectTotal, metrics) => {
87392
- return subjectTotal + (metrics.activityMetrics?.xpEarned ?? 0);
87393
- }, 0);
87394
- }, 0);
87395
- }
87396
- // src/server/handlers/identity-full.ts
87296
+ // src/server/lib/sso.ts
87397
87297
  function buildErrorContext(error57, errorCode, state, req) {
87398
87298
  return {
87399
87299
  error: error57,
@@ -87412,123 +87312,59 @@ function tryDecodeState(stateParam) {
87412
87312
  return;
87413
87313
  }
87414
87314
  }
87415
- function handleCallbackError(errorParam, url6, state, req, identity) {
87315
+ function handleIdpError(errorParam, url6, state, req, onCallbackError) {
87416
87316
  const errorDesc = url6.searchParams.get("error_description");
87417
87317
  ssoLog.error("IdP returned error", { error: errorParam, description: errorDesc });
87418
87318
  const error57 = new Error(errorDesc ?? errorParam);
87419
- if (identity.onCallbackError) {
87420
- return identity.onCallbackError(buildErrorContext(error57, errorParam, state, req));
87319
+ if (onCallbackError) {
87320
+ return onCallbackError(buildErrorContext(error57, errorParam, state, req));
87421
87321
  }
87422
87322
  return jsonResponse({ error: errorParam }, 400);
87423
87323
  }
87424
- function handleMissingCode(state, req, identity) {
87324
+ function handleMissingCode(state, req, onCallbackError) {
87425
87325
  ssoLog.error("Missing authorization code in callback");
87426
87326
  const error57 = new Error("Missing authorization code");
87427
- if (identity.onCallbackError) {
87428
- return identity.onCallbackError(buildErrorContext(error57, "missing_code", state, req));
87327
+ if (onCallbackError) {
87328
+ return onCallbackError(buildErrorContext(error57, "missing_code", state, req));
87429
87329
  }
87430
87330
  return jsonResponse({ error: "Missing authorization code" }, 400);
87431
87331
  }
87432
- async function handleSignIn(req, env2, identity) {
87433
- if (identity.mode !== "sso") {
87434
- ssoLog.warn("SSO not configured");
87435
- return jsonResponse({ error: "SSO not configured" }, 400);
87436
- }
87437
- 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);
87438
87335
  const url6 = new URL(req.url);
87439
- let redirectUri = identity.redirectUri;
87336
+ let redirectUri = params.redirectUri;
87440
87337
  if (!redirectUri) {
87441
87338
  const basePath = url6.pathname.replace(ROUTES.IDENTITY.SIGNIN, "");
87442
87339
  redirectUri = `${url6.origin}${basePath}${ROUTES.IDENTITY.CALLBACK}`;
87443
87340
  }
87444
- ssoLog.debug("SSO sign-in initiated", { env: env2, issuer, clientId: identity.clientId, redirectUri });
87445
- const stateData = identity.buildState ? identity.buildState({ req, url: url6 }) : {};
87341
+ ssoLog.debug("SSO sign-in initiated", { env: env2, issuer, clientId, redirectUri });
87342
+ const stateData = buildState ? buildState({ req, url: url6 }) : {};
87446
87343
  const state = encodeBase64Url(stateData);
87447
87344
  const authUrl = await buildAuthorizationUrl({
87448
87345
  issuer,
87449
- clientId: identity.clientId,
87346
+ clientId,
87450
87347
  redirectUri,
87451
87348
  state
87452
87349
  });
87453
87350
  return redirectResponse(authUrl);
87454
87351
  }
87455
- async function completeCallback(code, url6, state, req, env2, identity, api) {
87456
- try {
87457
- const issuer = identity.issuer ?? getIssuer(env2);
87458
- let redirectUri = identity.redirectUri;
87459
- if (!redirectUri) {
87460
- const basePath = url6.pathname.replace(ROUTES.IDENTITY.CALLBACK, "");
87461
- redirectUri = `${url6.origin}${basePath}${ROUTES.IDENTITY.CALLBACK}`;
87462
- }
87463
- ssoLog.debug("Exchanging auth code for tokens", { issuer, clientId: identity.clientId });
87464
- const tokens = await exchangeCodeForTokens({
87465
- issuer,
87466
- clientId: identity.clientId,
87467
- clientSecret: identity.clientSecret,
87468
- code,
87469
- redirectUri
87470
- });
87471
- const userInfo = await getUserInfo({ issuer, accessToken: tokens.access_token });
87472
- const identities = typeof userInfo.identities === "string" ? JSON.parse(userInfo.identities) : userInfo.identities;
87473
- ssoLog.debug("SSO completed, resolving Timeback user", {
87474
- user: { ...userInfo, identities }
87475
- });
87476
- const authUser = await resolveTimebackUserByEmail({
87477
- env: env2,
87478
- apiCredentials: api.credentials,
87479
- userInfo,
87480
- client: api.getClient()
87481
- });
87482
- ssoLog.debug("Timeback user resolved", { timebackId: authUser.id });
87483
- const ctx = {
87484
- user: authUser,
87485
- idp: { tokens, userInfo },
87486
- state,
87487
- req,
87488
- redirect: redirectResponse,
87489
- json: jsonResponse
87490
- };
87491
- return identity.onCallbackSuccess(ctx);
87492
- } catch (err) {
87493
- const error57 = err instanceof Error ? err : new Error("Unknown error");
87494
- const errorCode = err instanceof TimebackUserResolutionError ? err.code : "token_exchange_failed";
87495
- ssoLog.error("SSO callback failed", { error: error57.message, errorCode });
87496
- if (identity.onCallbackError) {
87497
- return identity.onCallbackError(buildErrorContext(error57, errorCode, state, req));
87498
- }
87499
- return jsonResponse({ error: error57.message }, 500);
87500
- }
87501
- }
87502
- async function handleCallback(req, env2, identity, api) {
87503
- if (identity.mode !== "sso") {
87504
- ssoLog.warn("SSO not configured");
87505
- return jsonResponse({ error: "SSO not configured" }, 400);
87506
- }
87352
+ function parseCallback(req) {
87507
87353
  const url6 = new URL(req.url);
87508
87354
  const code = url6.searchParams.get("code");
87509
87355
  const errorParam = url6.searchParams.get("error");
87510
87356
  const stateParam = url6.searchParams.get("state");
87511
87357
  ssoLog.debug("Received callback from IdP", { hasCode: !!code, error: errorParam });
87512
87358
  const state = stateParam ? tryDecodeState(stateParam) : undefined;
87513
- if (errorParam) {
87514
- return await handleCallbackError(errorParam, url6, state, req, identity);
87515
- }
87516
- if (!code) {
87517
- return await handleMissingCode(state, req, identity);
87518
- }
87519
- return await completeCallback(code, url6, state, req, env2, identity, api);
87359
+ return { url: url6, code, errorParam, state };
87520
87360
  }
87521
- function createIdentityHandlers(params) {
87522
- const { env: env2, identity, api } = params;
87523
- return {
87524
- signIn: (req) => handleSignIn(req, env2, identity),
87525
- callback: (req) => handleCallback(req, env2, identity, api),
87526
- signOut: () => redirectResponse("/")
87527
- };
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}`;
87528
87367
  }
87529
- // src/server/handlers/activity.ts
87530
- import * as z16 from "zod";
87531
-
87532
87368
  // ../clients/caliper/dist/index.js
87533
87369
  var __defProp7 = Object.defineProperty;
87534
87370
  var __export = (target, all) => {
@@ -102809,14 +102645,20 @@ var TimebackConfig8 = exports_external6.object({
102809
102645
  message: "Duplicate courseCode found; each must be unique",
102810
102646
  path: ["courses"]
102811
102647
  }).refine((config22) => {
102812
- return config22.courses.every((c) => c.sensor !== undefined || config22.sensor !== undefined);
102813
- }, {
102814
- message: "Each course must have an effective sensor; set a top-level `sensor` or per-course `sensor`",
102815
- path: ["courses"]
102816
- }).refine((config22) => {
102817
- return config22.courses.every((c) => c.launchUrl !== undefined || config22.launchUrl !== undefined);
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
+ });
102818
102660
  }, {
102819
- message: "Each course must have an effective launchUrl; set a top-level `launchUrl` or per-course `launchUrl`",
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.",
102820
102662
  path: ["courses"]
102821
102663
  });
102822
102664
  var EdubridgeDateString8 = exports_external6.union([IsoDateString8, IsoDateTimeString8]);
@@ -104099,8 +103941,337 @@ function createCaliperClient2(registry22 = DEFAULT_PROVIDER_REGISTRY7) {
104099
103941
  }
104100
103942
  var CaliperClient2 = createCaliperClient2();
104101
103943
 
104102
- // src/server/handlers/activity.ts
104103
- var log11 = createScopedLogger3("handlers:activity");
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");
104104
104275
  var activityMetricsSchema = z16.object({
104105
104276
  totalQuestions: z16.number().int().nonnegative().optional(),
104106
104277
  correctQuestions: z16.number().int().nonnegative().optional(),
@@ -104152,13 +104323,13 @@ function validateActivityRequest(body, appConfig, env2) {
104152
104323
  if (err instanceof ActivityCourseResolutionError) {
104153
104324
  const selectorDesc = formatCourseSelector(payload.course);
104154
104325
  if (err.code === "unknown_course") {
104155
- log11.warn("Unknown course selector", { selector: payload.course });
104326
+ log12.warn("Unknown course selector", { selector: payload.course });
104156
104327
  return {
104157
104328
  ok: false,
104158
104329
  response: jsonResponse({ success: false, error: `Unknown course: ${selectorDesc}` }, 400)
104159
104330
  };
104160
104331
  }
104161
- log11.error("Ambiguous course selector", { selector: payload.course });
104332
+ log12.error("Ambiguous course selector", { selector: payload.course });
104162
104333
  return {
104163
104334
  ok: false,
104164
104335
  response: jsonResponse({ success: false, error: "Ambiguous course selector in timeback.config.json" }, 500)
@@ -104170,7 +104341,7 @@ function validateActivityRequest(body, appConfig, env2) {
104170
104341
  const sensor = course.overrides?.[envForOverrides]?.sensor ?? course.sensor ?? appConfig.sensor;
104171
104342
  if (!sensor) {
104172
104343
  const selectorDesc = formatCourseSelector(payload.course);
104173
- log11.error("Missing sensor for course", { selector: payload.course });
104344
+ log12.error("Missing sensor for course", { selector: payload.course });
104174
104345
  return {
104175
104346
  ok: false,
104176
104347
  response: jsonResponse({
@@ -104181,6 +104352,9 @@ function validateActivityRequest(body, appConfig, env2) {
104181
104352
  }
104182
104353
  return { ok: true, payload, course, sensor };
104183
104354
  }
104355
+
104356
+ // src/server/handlers/activity/handler.ts
104357
+ var log13 = createScopedLogger3("handlers:activity");
104184
104358
  async function getActivityUserInfo(identity, req) {
104185
104359
  if (identity.mode === "custom") {
104186
104360
  const email8 = await identity.getEmail(req);
@@ -104192,55 +104366,18 @@ async function getActivityUserInfo(identity, req) {
104192
104366
  function resolveTimebackId(userInfo, client) {
104193
104367
  return userInfo.timebackId ?? lookupTimebackIdByEmail({ email: userInfo.email, client });
104194
104368
  }
104195
- function buildActivityEvents(params) {
104196
- const { sensor, timebackId, email: email8, payload, course, appName, apiEnv } = params;
104197
- const actor = {
104198
- id: `urn:timeback:user:${timebackId}`,
104199
- type: "TimebackUser",
104200
- email: email8
104201
- };
104202
- const object7 = buildActivityContext(payload, course, appName, apiEnv, sensor);
104203
- const metrics = buildActivityMetrics(payload.metrics);
104204
- const timeSpentMetrics = buildTimeSpentMetrics(payload.elapsedMs, payload.pausedMs);
104205
- const activityEvent = createActivityEvent2({
104206
- actor,
104207
- object: object7,
104208
- metrics,
104209
- eventTime: payload.endedAt,
104210
- attempt: payload.attemptNumber,
104211
- generatedExtensions: payload.pctCompleteApp === undefined ? undefined : { pctCompleteApp: payload.pctCompleteApp }
104212
- });
104213
- const timeSpentEvent = createTimeSpentEvent2({
104214
- actor,
104215
- object: object7,
104216
- metrics: timeSpentMetrics,
104217
- eventTime: payload.endedAt
104218
- });
104219
- return {
104220
- sensor,
104221
- actor,
104222
- object: object7,
104223
- events: [activityEvent, timeSpentEvent],
104224
- payload,
104225
- course,
104226
- appName,
104227
- apiEnv,
104228
- email: email8,
104229
- timebackId
104230
- };
104231
- }
104232
104369
  function mapErrorToResponse(err, context) {
104233
104370
  if (err instanceof TimebackUserResolutionError) {
104234
- log11.warn("Failed to resolve Timeback user", { code: err.code });
104371
+ log13.warn("Failed to resolve Timeback user", { code: err.code });
104235
104372
  return jsonResponse({ success: false, error: "Unable to resolve Timeback identity" }, resolveStatusForUserResolutionError(err));
104236
104373
  }
104237
104374
  if (err instanceof MissingSyncedCourseIdError) {
104238
104375
  const syncErr = err;
104239
- log11.error("Course not synced", { course: syncErr.course, env: syncErr.env });
104376
+ log13.error("Course not synced", { course: syncErr.course, env: syncErr.env });
104240
104377
  return jsonResponse({ success: false, error: syncErr.message }, 500);
104241
104378
  }
104242
104379
  const message = err instanceof Error ? err.message : "Unknown error";
104243
- log11.error("Failed to submit activity", { ...context, error: message });
104380
+ log13.error("Failed to submit activity", { ...context, error: message });
104244
104381
  return jsonResponse({ success: false, error: message }, 502);
104245
104382
  }
104246
104383
  function createActivityHandler(config7) {
@@ -104286,8 +104423,20 @@ function createActivityHandler(config7) {
104286
104423
  events: effective.events
104287
104424
  });
104288
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
+ }
104289
104438
  await sendCaliperEnvelope(client, effective.sensor, effective.events[0], effective.events[1]);
104290
- log11.debug("Submitted activity", {
104439
+ log13.debug("Submitted activity", {
104291
104440
  courseSelector: payload.course,
104292
104441
  activityId: payload.id
104293
104442
  });
@@ -104302,27 +104451,191 @@ function createActivityHandler(config7) {
104302
104451
  }
104303
104452
  };
104304
104453
  }
104305
- // src/server/handlers/user.ts
104306
- var log12 = createScopedLogger3("handlers:user");
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
+ }
104307
104633
  function createUserHandler(config7) {
104308
104634
  return async (req) => {
104309
104635
  try {
104310
- let userSub;
104311
- let userEmail;
104312
- if (config7.identity.mode === "custom") {
104313
- const email8 = await config7.identity.getEmail(req);
104314
- if (!email8) {
104315
- return jsonResponse({ error: "Unauthorized" }, 401);
104316
- }
104317
- userSub = email8;
104318
- userEmail = email8;
104319
- } else {
104320
- const user = await config7.identity.getUser(req);
104321
- if (!user) {
104322
- return jsonResponse({ error: "Unauthorized" }, 401);
104323
- }
104324
- userSub = user.id;
104325
- userEmail = user.email;
104636
+ const userIdentity = await getUserIdentity(config7.identity, req);
104637
+ if (!userIdentity) {
104638
+ return jsonResponse({ error: "Unauthorized" }, 401);
104326
104639
  }
104327
104640
  const apiEnv = mapEnvForApi(config7.env);
104328
104641
  const client = new TimebackClient({
@@ -104336,65 +104649,85 @@ function createUserHandler(config7) {
104336
104649
  const resolved = await resolveTimebackUserByEmail({
104337
104650
  env: config7.env,
104338
104651
  apiCredentials: config7.api,
104339
- userInfo: {
104340
- sub: userSub,
104341
- email: userEmail
104342
- },
104652
+ userInfo: userIdentity,
104343
104653
  client
104344
104654
  });
104345
- const enrollments = await client.edubridge.enrollments.list({
104346
- userId: resolved.id
104347
- });
104348
- const courseById = buildCourseLookup(config7.appConfig.courses, apiEnv);
104349
- const courses = mapEnrollmentsToCourses(enrollments, courseById);
104350
- const goals = pickGoalsFromEnrollments(enrollments);
104351
- const { start: todayStart, end: todayEnd } = getUtcDayRange(new Date);
104352
- const [todayFacts, allFacts] = await Promise.all([
104353
- client.edubridge.analytics.getActivity({
104354
- studentId: resolved.id,
104355
- startDate: todayStart.toISOString(),
104356
- endDate: todayEnd.toISOString()
104357
- }),
104358
- client.edubridge.analytics.getActivity({
104359
- studentId: resolved.id,
104360
- startDate: "2000-01-01",
104361
- endDate: todayEnd.toISOString()
104362
- })
104363
- ]);
104364
- const profile = {
104365
- id: resolved.id,
104366
- email: resolved.email,
104367
- name: resolved.name,
104368
- school: resolved.school,
104369
- grade: resolved.grade,
104370
- courses: courses.length ? courses : undefined,
104371
- goals,
104372
- xp: {
104373
- today: sumXp(todayFacts),
104374
- all: sumXp(allFacts)
104375
- }
104376
- };
104655
+ const profile = await buildUserProfile(client, resolved, config7.appConfig, apiEnv);
104377
104656
  return jsonResponse(profile);
104378
104657
  } catch (error59) {
104379
104658
  if (error59 instanceof TimebackUserResolutionError) {
104380
- log12.warn("Timeback user resolution failed", { code: error59.code });
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 });
104381
104713
  if (error59.code === "timeback_user_ambiguous") {
104382
- return jsonResponse({ error: "Timeback user resolution ambiguous" }, 409);
104714
+ return jsonResponse({ verified: false, error: "Timeback user resolution ambiguous" }, 409);
104383
104715
  }
104384
104716
  if (error59.code === "timeback_user_not_found") {
104385
- return jsonResponse({ error: "Timeback user not found" }, 404);
104717
+ return jsonResponse({ verified: false });
104386
104718
  }
104719
+ return jsonResponse({ verified: false, error: error59.message }, 502);
104387
104720
  }
104388
104721
  const message = error59 instanceof Error ? error59.message : "Unknown error";
104389
- log12.error("Failed to build user profile", { error: message });
104390
- return jsonResponse({ error: message }, 502);
104722
+ log15.error("Failed to verify user", { error: message });
104723
+ return jsonResponse({ verified: false, error: message }, 502);
104391
104724
  } finally {
104392
104725
  client.close();
104393
104726
  }
104394
104727
  } catch (error59) {
104395
104728
  const message = error59 instanceof Error ? error59.message : "Unknown error";
104396
- log12.error("Unhandled error in user handler", { error: message });
104397
- return jsonResponse({ error: message }, 500);
104729
+ log15.error("Unhandled error in verify handler", { error: message });
104730
+ return jsonResponse({ verified: false, error: message }, 500);
104398
104731
  }
104399
104732
  };
104400
104733
  }
@@ -104409,6 +104742,7 @@ function toAppCourses(courses) {
104409
104742
  });
104410
104743
  }
104411
104744
  async function createTimeback(config7) {
104745
+ const env2 = normalizeEnv(config7.env);
104412
104746
  const configResult = await loadConfig({ configPath: config7.configPath });
104413
104747
  if (!configResult.success) {
104414
104748
  throw new Error(`Failed to load timeback config: ${configResult.error}`);
@@ -104418,7 +104752,7 @@ async function createTimeback(config7) {
104418
104752
  const getApiClient = () => {
104419
104753
  if (!apiClient) {
104420
104754
  apiClient = new TimebackClient({
104421
- env: mapEnvForApi(config7.env),
104755
+ env: mapEnvForApi(env2),
104422
104756
  auth: {
104423
104757
  clientId: config7.api.clientId,
104424
104758
  clientSecret: config7.api.clientSecret
@@ -104428,7 +104762,7 @@ async function createTimeback(config7) {
104428
104762
  return apiClient;
104429
104763
  };
104430
104764
  const activity = createActivityHandler({
104431
- env: config7.env,
104765
+ env: env2,
104432
104766
  identity: config7.identity,
104433
104767
  appConfig: {
104434
104768
  name: appConfig.name,
@@ -104439,7 +104773,7 @@ async function createTimeback(config7) {
104439
104773
  hooks: config7.hooks
104440
104774
  });
104441
104775
  const identity = createIdentityHandlers({
104442
- env: config7.env,
104776
+ env: env2,
104443
104777
  identity: config7.identity,
104444
104778
  api: {
104445
104779
  credentials: config7.api,
@@ -104447,7 +104781,7 @@ async function createTimeback(config7) {
104447
104781
  }
104448
104782
  });
104449
104783
  const user = createUserHandler({
104450
- env: config7.env,
104784
+ env: env2,
104451
104785
  identity: config7.identity,
104452
104786
  api: config7.api,
104453
104787
  appConfig: {
@@ -104456,13 +104790,19 @@ async function createTimeback(config7) {
104456
104790
  courses: toAppCourses(appConfig.courses)
104457
104791
  }
104458
104792
  });
104793
+ const userVerify = createUserVerifyHandler({
104794
+ env: env2,
104795
+ identity: config7.identity,
104796
+ api: config7.api
104797
+ });
104459
104798
  const instance = {
104460
104799
  config: config7,
104461
104800
  handle: {
104462
104801
  activity,
104463
104802
  identity,
104464
104803
  user: {
104465
- me: user
104804
+ me: user,
104805
+ verify: userVerify
104466
104806
  }
104467
104807
  },
104468
104808
  get api() {
@@ -104471,74 +104811,21 @@ async function createTimeback(config7) {
104471
104811
  };
104472
104812
  return instance;
104473
104813
  }
104474
- // src/server/handlers/identity-only.ts
104475
- function buildErrorContext2(error59, errorCode, state, req) {
104476
- return {
104477
- error: error59,
104478
- errorCode,
104479
- state,
104480
- req,
104481
- redirect: redirectResponse,
104482
- json: jsonResponse
104483
- };
104484
- }
104485
- function tryDecodeState2(stateParam) {
104486
- try {
104487
- return decodeBase64Url(stateParam);
104488
- } catch {
104489
- ssoLog.warn("Failed to decode state");
104490
- return;
104491
- }
104492
- }
104493
- function handleCallbackError2(errorParam, url7, state, req, identity) {
104494
- const errorDesc = url7.searchParams.get("error_description");
104495
- ssoLog.error("IdP returned error", { error: errorParam, description: errorDesc });
104496
- const error59 = new Error(errorDesc ?? errorParam);
104497
- if (identity.onCallbackError) {
104498
- return identity.onCallbackError(buildErrorContext2(error59, errorParam, state, req));
104499
- }
104500
- return jsonResponse({ error: errorParam }, 400);
104501
- }
104502
- function handleMissingCode2(state, req, identity) {
104503
- ssoLog.error("Missing authorization code in callback");
104504
- const error59 = new Error("Missing authorization code");
104505
- if (identity.onCallbackError) {
104506
- return identity.onCallbackError(buildErrorContext2(error59, "missing_code", state, req));
104507
- }
104508
- return jsonResponse({ error: "Missing authorization code" }, 400);
104509
- }
104814
+ // src/server/handlers/identity-only/oidc.ts
104510
104815
  async function handleSignIn2(req, env2, identity) {
104511
- const issuer = identity.issuer ?? getIssuer(env2);
104512
- const url7 = new URL(req.url);
104513
- let redirectUri = identity.redirectUri;
104514
- if (!redirectUri) {
104515
- const basePath = url7.pathname.replace(ROUTES.IDENTITY.SIGNIN, "");
104516
- redirectUri = `${url7.origin}${basePath}${ROUTES.IDENTITY.CALLBACK}`;
104517
- }
104518
- ssoLog.debug("SSO sign-in initiated (identity-only)", {
104816
+ return await initiateSignIn({
104817
+ req,
104519
104818
  env: env2,
104520
- issuer,
104521
- clientId: identity.clientId,
104522
- redirectUri
104523
- });
104524
- const stateData = identity.buildState ? identity.buildState({ req, url: url7 }) : {};
104525
- const state = encodeBase64Url(stateData);
104526
- const authUrl = await buildAuthorizationUrl({
104527
- issuer,
104528
104819
  clientId: identity.clientId,
104529
- redirectUri,
104530
- state
104820
+ issuer: identity.issuer,
104821
+ redirectUri: identity.redirectUri,
104822
+ buildState: identity.buildState
104531
104823
  });
104532
- return redirectResponse(authUrl);
104533
104824
  }
104534
- async function completeCallbackIdentityOnly(code, url7, state, req, env2, identity) {
104825
+ async function completeCallback2(code, url7, state, req, env2, identity) {
104535
104826
  try {
104536
104827
  const issuer = identity.issuer ?? getIssuer(env2);
104537
- let redirectUri = identity.redirectUri;
104538
- if (!redirectUri) {
104539
- const basePath = url7.pathname.replace(ROUTES.IDENTITY.CALLBACK, "");
104540
- redirectUri = `${url7.origin}${basePath}${ROUTES.IDENTITY.CALLBACK}`;
104541
- }
104828
+ const redirectUri = computeRedirectUri(url7, identity.redirectUri);
104542
104829
  ssoLog.debug("Exchanging auth code for tokens (identity-only)", {
104543
104830
  issuer,
104544
104831
  clientId: identity.clientId
@@ -104566,29 +104853,23 @@ async function completeCallbackIdentityOnly(code, url7, state, req, env2, identi
104566
104853
  const error59 = err instanceof Error ? err : new Error("Unknown error");
104567
104854
  ssoLog.error("Token exchange failed (identity-only)", { error: error59.message });
104568
104855
  if (identity.onCallbackError) {
104569
- return identity.onCallbackError(buildErrorContext2(error59, undefined, state, req));
104856
+ return identity.onCallbackError(buildErrorContext(error59, undefined, state, req));
104570
104857
  }
104571
104858
  return jsonResponse({ error: error59.message }, 500);
104572
104859
  }
104573
104860
  }
104574
- async function handleCallbackIdentityOnly(req, env2, identity) {
104575
- const url7 = new URL(req.url);
104576
- const code = url7.searchParams.get("code");
104577
- const errorParam = url7.searchParams.get("error");
104578
- const stateParam = url7.searchParams.get("state");
104579
- ssoLog.debug("Received callback from IdP (identity-only)", {
104580
- hasCode: !!code,
104581
- error: errorParam
104582
- });
104583
- const state = stateParam ? tryDecodeState2(stateParam) : undefined;
104861
+ async function handleCallback2(req, env2, identity) {
104862
+ const { url: url7, code, errorParam, state } = parseCallback(req);
104584
104863
  if (errorParam) {
104585
- return await handleCallbackError2(errorParam, url7, state, req, identity);
104864
+ return handleIdpError(errorParam, url7, state, req, identity.onCallbackError);
104586
104865
  }
104587
104866
  if (!code) {
104588
- return await handleMissingCode2(state, req, identity);
104867
+ return handleMissingCode(state, req, identity.onCallbackError);
104589
104868
  }
104590
- return await completeCallbackIdentityOnly(code, url7, state, req, env2, identity);
104869
+ return await completeCallback2(code, url7, state, req, env2, identity);
104591
104870
  }
104871
+
104872
+ // src/server/handlers/identity-only/handler.ts
104592
104873
  function createIdentityOnlyHandlers(params) {
104593
104874
  const { env: env2, identity } = params;
104594
104875
  if (identity.mode !== "sso") {
@@ -104596,15 +104877,14 @@ function createIdentityOnlyHandlers(params) {
104596
104877
  }
104597
104878
  return {
104598
104879
  signIn: (req) => handleSignIn2(req, env2, identity),
104599
- callback: (req) => handleCallbackIdentityOnly(req, env2, identity),
104880
+ callback: (req) => handleCallback2(req, env2, identity),
104600
104881
  signOut: () => redirectResponse("/")
104601
104882
  };
104602
104883
  }
104603
-
104604
104884
  // src/server/timeback-identity.ts
104605
104885
  function createTimebackIdentity(config7) {
104606
104886
  const identity = createIdentityOnlyHandlers({
104607
- env: config7.env,
104887
+ env: normalizeEnv(config7.env),
104608
104888
  identity: config7.identity
104609
104889
  });
104610
104890
  return {