@playcademy/sdk 0.24.0 → 0.24.1-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/types.d.ts CHANGED
@@ -96,442 +96,92 @@ declare const ASSESSMENT_PURPOSES: readonly ['end_of_course', 'diagnostic', 'rev
96
96
  declare const PLAYCADEMY_ASSESSMENT_CONTRACT_VERSION: 2;
97
97
 
98
98
  /**
99
- * User Types
99
+ * TimeBack Enums & Literal Types
100
100
  *
101
- * Enums, DTOs and API response types. Database row types are in @playcademy/data/types.
101
+ * Basic type definitions used throughout the TimeBack integration. Unions
102
+ * derive from the canonical value lists in @playcademy/constants (type-only
103
+ * imports, so this package ships no runtime code).
102
104
  *
103
- * @module types/user
105
+ * @module types/timeback/types
104
106
  */
105
107
 
106
- type UserRoleEnumType = UserRole;
107
- type DeveloperStatusEnumType = 'none' | 'pending' | 'approved';
108
- type DeveloperStatusValue = DeveloperStatusEnumType;
109
- type TimebackUserRole = 'administrator' | 'aide' | 'guardian' | 'parent' | 'proctor' | 'relative' | 'student' | 'teacher';
110
- type TimebackOrgType = 'department' | 'school' | 'district' | 'local' | 'state' | 'national';
111
- interface UserEnrollment {
112
- gameId?: string;
113
- courseId: string;
114
- id?: string;
115
- grade: number;
116
- subject: string;
117
- orgId?: string;
118
- }
119
- interface UserOrganization {
120
- id: string;
121
- name: string | null;
122
- type: TimebackOrgType | string;
123
- isPrimary: boolean;
124
- }
125
- interface TimebackStudentProfile {
126
- role: TimebackUserRole;
127
- organizations: UserOrganization[];
128
- }
129
108
  /**
130
- * Timeback identity on `/users/me`: who the student is, without enrollments.
131
- * Enrollments come from `/timeback/user` (or `client.timeback.user`).
109
+ * The TimeBack subject vocabulary. 'None' is canonical: TimeBack accepts it
110
+ * on courses and uses it as the fallback for unknown or missing subjects.
132
111
  */
133
- interface UserTimebackProfile extends TimebackStudentProfile {
134
- id: string;
135
- }
136
- interface UserTimebackData extends UserTimebackProfile {
137
- enrollments: UserEnrollment[];
138
- }
139
- type LocalDaySource = 'browser_last_seen' | 'platform_default';
140
- interface LocalDayContext {
141
- timeZone: string;
142
- source: LocalDaySource;
143
- /** ISO 8601 timestamp for the last persisted timezone observation. */
144
- observedAt: string | null;
145
- }
112
+ type TimebackSubject = (typeof TIMEBACK_SUBJECTS)[number];
146
113
  /**
147
- * OpenID Connect UserInfo claims (NOT a database row).
114
+ * Grade levels per AE OneRoster GradeEnum.
115
+ * -1 = Pre-K, 0 = Kindergarten, 1-12 = Grades 1-12, 13 = AP
148
116
  */
149
- interface UserInfo {
150
- sub: string;
151
- email: string;
152
- name: string | null;
153
- email_verified?: boolean;
154
- given_name?: string;
155
- family_name?: string;
156
- issuer?: string;
157
- lti_roles?: unknown;
158
- lti_context?: unknown;
159
- lti_resource_link?: unknown;
160
- timeback_id?: string;
161
- }
162
- interface DeveloperStatusResponse {
163
- status: DeveloperStatusEnumType;
164
- }
165
- interface DemoProfile {
166
- displayName: string;
167
- isDefault: boolean;
168
- }
117
+ type TimebackGrade = (typeof TIMEBACK_GRADES)[number];
169
118
  /**
170
- * Update shape for `client.demo.profile.update(...)`.
171
- *
172
- * Kept as a named type so callers typed against it pick up new fields
173
- * automatically, but `displayName` is the only updatable field today and
174
- * the server's `DemoProfileSchema` requires it — a no-field payload would
175
- * 400 at runtime, so we model that at the type level too. When additional
176
- * fields land, make them required/optional individually based on server
177
- * validation, rather than blanket-optional.
119
+ * Platform pedagogy levels for a single lesson of instruction.
120
+ * E1 teaches, E2/E3 practice and remediate, E4 is the mastery check.
178
121
  */
179
- interface DemoProfileUpdate {
180
- displayName: string;
181
- }
122
+ type ELevel = 'E1' | 'E2' | 'E3' | 'E4';
182
123
  /**
183
- * Authenticated user for API responses.
184
- * Differs from UserRow: omits timebackId, adds hasTimebackAccount and timeback.
124
+ * Valid Caliper subject values. The same vocabulary as OneRoster subjects.
185
125
  */
186
- interface AuthenticatedUser {
187
- id: string;
188
- email: string;
189
- emailVerified: boolean;
190
- name: string | null;
191
- image: string | null;
192
- username: string | null;
193
- role: UserRoleEnumType;
194
- developerStatus: DeveloperStatusEnumType;
195
- createdAt: Date;
196
- updatedAt: Date;
197
- hasTimebackAccount: boolean;
198
- localDay: LocalDayContext;
199
- timeback?: UserTimebackProfile;
200
- }
201
- interface GameUser {
202
- id: string;
203
- name: string | null;
204
- role: UserRoleEnumType;
205
- username: string | null;
206
- email: string | null;
207
- localDay: LocalDayContext;
208
- timeback?: UserTimebackProfile;
209
- }
126
+ type CaliperSubject = (typeof TIMEBACK_SUBJECTS)[number];
127
+ /**
128
+ * OneRoster organization types.
129
+ */
130
+ type OrganizationType = 'department' | 'school' | 'district' | 'local' | 'state' | 'national';
131
+ /**
132
+ * Lesson types for PowerPath integration.
133
+ */
134
+ type LessonType = 'powerpath-100' | 'quiz' | 'test-out' | 'placement' | 'unit-test' | 'alpha-read-article' | null;
210
135
 
211
136
  /**
212
- * Game Types
137
+ * TimeBack Configuration Types
213
138
  *
214
- * Literal types and API DTOs. Database row types are in @playcademy/data/types.
139
+ * Configuration interfaces for Organization, Course, Component,
140
+ * Resource, and complete TimeBack setup.
215
141
  *
216
- * @module types/game
142
+ * @module types/timeback/config
217
143
  */
218
144
 
219
- type GameType = 'hosted' | 'external';
220
- type GamePlatform = 'web' | 'godot' | (string & {});
221
145
  /**
222
- * Game manifest file format (manifest.json).
223
- * Note: createdAt is a string here because it's parsed from JSON file.
146
+ * Organization configuration for TimeBack (user input - optionals allowed)
224
147
  */
225
- interface ManifestV1 {
226
- version: '1';
227
- platform: string;
228
- createdAt: string;
229
- }
230
- interface ManifestVersions {
231
- vitePlugin?: string;
232
- sdk?: string;
233
- cli?: string;
234
- vite?: string;
235
- godotSdk?: string;
236
- godot?: string;
237
- }
238
- interface ManifestV2 {
239
- version: '2';
240
- platform: string;
241
- createdAt: string;
242
- gameVersion?: string;
243
- versions: ManifestVersions;
148
+ interface OrganizationConfig {
149
+ /** Display name for your organization */
150
+ name?: string;
151
+ /** Organization type */
152
+ type?: OrganizationType;
153
+ /** Unique identifier (defaults to Playcademy's org) */
154
+ identifier?: string;
244
155
  }
245
- type GameManifest = ManifestV1 | ManifestV2;
246
156
  /**
247
- * Git provenance of the working tree a deploy was run from (PLA-294).
248
- *
249
- * Captured by the CLI at deploy time and persisted verbatim on the deploy
250
- * job, so "which commit is running" is answerable from the platform record.
251
- * Every field is nullable: a tree with no git, no remote, or a detached HEAD
252
- * is recorded honestly rather than refused. Recorded per deploy, never
253
- * pinned to the game — a fork deployed under the same slug shows the fork.
157
+ * Course goals for daily student targets
254
158
  */
255
- interface DeploySource {
256
- /** Normalized `https://github.com/<org>/<repo>`; null when no remote or unparseable */
257
- repository: string | null;
258
- /** Full 40-char SHA of HEAD; null when not in a git tree */
259
- commit: string | null;
260
- /** Current branch name; null on detached HEAD outside CI */
261
- branch: string | null;
262
- /** True when the tree had uncommitted changes — the SHA does not describe what shipped */
263
- dirty: boolean;
264
- /**
265
- * True when the uploaded build artifact lives outside the git tree the
266
- * other fields describe (`--build ~/Downloads/game.zip`). The commit is
267
- * still the deployer's tree, but it may not be what produced the artifact.
268
- */
269
- externalBuild: boolean;
159
+ interface CourseGoals {
160
+ /** Target XP students should earn per day */
161
+ dailyXp?: number;
162
+ /** Target lessons per day */
163
+ dailyLessons?: number;
164
+ /** Target active minutes per day */
165
+ dailyActiveMinutes?: number;
166
+ /** Target accuracy percentage */
167
+ dailyAccuracy?: number;
168
+ /** Target mastered units per day */
169
+ dailyMasteredUnits?: number;
270
170
  }
271
171
  /**
272
- * One successful deploy of a game's worker, as the open releases surface
273
- * describes it: what is running and which commit produced it.
274
- *
275
- * This shape is parsed by external automation. Every field is present on
276
- * every entry — nullable, never absent — and nothing from the deploy job
277
- * beyond its git provenance and completion time is exposed. No deployer
278
- * identity, no schema state, no secrets manifest.
172
+ * Course metrics and totals
279
173
  */
280
- interface Release {
281
- slug: string;
282
- /** Where the game is served; null when the platform has no URL for it */
283
- url: string | null;
284
- /**
285
- * Git provenance the deploying CLI captured. Null for deploys that
286
- * predate provenance capture, so the fleet list is complete on day one
287
- * and a consumer can tell "unknown" from "not in git".
288
- */
289
- source: DeploySource | null;
290
- /** When the deploy succeeded, ISO 8601 */
291
- deployedAt: string;
292
- }
293
- /** GET /api/releases — the current release of every game */
294
- interface ReleasesResponse {
295
- releases: Release[];
296
- }
297
- /** GET /api/releases/:slug — one game's current release and, on request, its recent history */
298
- interface GameReleaseResponse {
299
- /** The current release; null when the game has never deployed successfully */
300
- current: Release | null;
301
- /**
302
- * The last N successful deploys, newest first, current included. Only
303
- * present when `?history=N` was requested.
304
- */
305
- history?: Release[];
306
- }
307
- interface DomainValidationRecords {
308
- ownership?: {
309
- name?: string;
310
- value?: string;
311
- type?: string;
312
- };
313
- ssl?: {
314
- txt_name?: string;
315
- txt_value?: string;
316
- }[];
317
- }
318
-
319
- /**
320
- * Leaderboard Types
321
- *
322
- * @module types/leaderboard
323
- */
324
- type LeaderboardTimeframe = 'all_time' | 'monthly' | 'weekly' | 'daily';
325
- interface LeaderboardOptions {
326
- timeframe?: LeaderboardTimeframe;
327
- limit?: number;
328
- offset?: number;
329
- gameId?: string;
330
- }
331
- interface LeaderboardEntry {
332
- rank: number;
333
- userId: string;
334
- username: string;
335
- userImage?: string | null;
336
- score: number;
337
- achievedAt: Date;
338
- metadata?: Record<string, unknown>;
339
- gameId?: string;
340
- gameTitle?: string;
341
- gameSlug?: string;
342
- }
343
- interface UserRank {
344
- rank: number;
345
- totalPlayers: number;
346
- score: number;
347
- percentile: number;
348
- }
349
- interface UserRankResponse {
350
- rank: number;
351
- score: number;
352
- userId: string;
353
- }
354
- interface UserScore {
355
- id: string;
356
- score: number;
357
- achievedAt: Date;
358
- metadata?: Record<string, unknown>;
359
- gameId: string;
360
- gameTitle: string;
361
- gameSlug: string;
362
- }
363
- /**
364
- * Leaderboard entry with required game context.
365
- * Used when fetching leaderboards for a specific game.
366
- */
367
- interface GameLeaderboardEntry {
368
- rank: number;
369
- userId: string;
370
- username: string;
371
- userImage?: string | null;
372
- score: number;
373
- achievedAt: Date;
374
- metadata?: Record<string, unknown>;
375
- gameId: string;
376
- gameTitle: string;
377
- gameSlug: string;
378
- }
379
-
380
- /**
381
- * Skill Status Types
382
- *
383
- * Contract for cross-game skill status queries. An answering game exports
384
- * `getSkillStatus` from `server/lib/skills.ts`; the platform calls its
385
- * reserved `/__playcademy/skills/status` route on behalf of another game.
386
- *
387
- * @module types/skills
388
- */
389
-
390
- /**
391
- * Current status of one skill for one student, as judged by the answering game.
392
- *
393
- * - `locked`: the student can't work on the skill yet in this game.
394
- * - `unlocked`: available to the student, but no progress yet.
395
- * - `in_progress`: started, not mastered.
396
- * - `mastered`: currently meets this game's mastery bar, including by placement.
397
- * - `unknown_skill`: the skill ID isn't recognized. Never report an unrecognized
398
- * skill as `locked`.
399
- */
400
- type SkillStatus = (typeof SKILL_STATUSES)[number];
401
- /** How a `mastered` status was reached. */
402
- type SkillMasterySource = (typeof SKILL_MASTERY_SOURCES)[number];
403
- interface SkillStatusResult {
404
- /** Skill ID, lowercase. */
405
- skill: string;
406
- /** Current status. */
407
- status: SkillStatus;
408
- /** How mastery was reached. Only allowed when `status` is `mastered`. */
409
- source?: SkillMasterySource;
410
- /** ISO 8601 time of the evidence behind this status. */
411
- asOf?: string;
412
- }
413
- /** Response returned by an answering game's `getSkillStatus` handler. */
414
- interface SkillStatusResponse {
415
- /** Student identifier (Playcademy user ID). Must match the requested student. */
416
- studentId: string;
417
- /** One result per skill the handler was asked about. */
418
- results: SkillStatusResult[];
419
- }
420
- /** The game asking for skill status, as identified by the platform. */
421
- interface SkillStatusRequestingGame {
422
- id: string;
423
- slug: string;
424
- }
425
- /** Body of `POST /api/skills/status`, sent by the asking game's worker. */
426
- interface SkillStatusRequest {
427
- /** Slug of the game to ask. */
428
- game: string;
429
- /** Playcademy user ID of the student. */
430
- studentId: string;
431
- /** Skill IDs to ask about. Normalized (trimmed, lowercased) and de-duplicated. */
432
- skills: string[];
433
- }
434
- /** Why the answering game couldn't answer. */
435
- type SkillStatusUnsupportedReason = 'not_implemented' | 'no_active_deployment' | 'timeout' | 'fetch_failed' | 'invalid_response';
436
- /**
437
- * What the asking game gets back. When `supported` is false, fall back to
438
- * the game's own default (usually: treat the skills as not mastered).
439
- */
440
- type SkillStatusLookupResult = {
441
- supported: true;
442
- results: SkillStatusResult[];
443
- } | {
444
- supported: false;
445
- reason: SkillStatusUnsupportedReason;
446
- };
447
-
448
- /**
449
- * TimeBack Enums & Literal Types
450
- *
451
- * Basic type definitions used throughout the TimeBack integration. Unions
452
- * derive from the canonical value lists in @playcademy/constants (type-only
453
- * imports, so this package ships no runtime code).
454
- *
455
- * @module types/timeback/types
456
- */
457
-
458
- /**
459
- * The TimeBack subject vocabulary. 'None' is canonical: TimeBack accepts it
460
- * on courses and uses it as the fallback for unknown or missing subjects.
461
- */
462
- type TimebackSubject = (typeof TIMEBACK_SUBJECTS)[number];
463
- /**
464
- * Grade levels per AE OneRoster GradeEnum.
465
- * -1 = Pre-K, 0 = Kindergarten, 1-12 = Grades 1-12, 13 = AP
466
- */
467
- type TimebackGrade = (typeof TIMEBACK_GRADES)[number];
468
- /**
469
- * Platform pedagogy levels for a single lesson of instruction.
470
- * E1 teaches, E2/E3 practice and remediate, E4 is the mastery check.
471
- */
472
- type ELevel = 'E1' | 'E2' | 'E3' | 'E4';
473
- /**
474
- * Valid Caliper subject values. The same vocabulary as OneRoster subjects.
475
- */
476
- type CaliperSubject = (typeof TIMEBACK_SUBJECTS)[number];
477
- /**
478
- * OneRoster organization types.
479
- */
480
- type OrganizationType = 'department' | 'school' | 'district' | 'local' | 'state' | 'national';
481
- /**
482
- * Lesson types for PowerPath integration.
483
- */
484
- type LessonType = 'powerpath-100' | 'quiz' | 'test-out' | 'placement' | 'unit-test' | 'alpha-read-article' | null;
485
-
486
- /**
487
- * TimeBack Configuration Types
488
- *
489
- * Configuration interfaces for Organization, Course, Component,
490
- * Resource, and complete TimeBack setup.
491
- *
492
- * @module types/timeback/config
493
- */
494
-
495
- /**
496
- * Organization configuration for TimeBack (user input - optionals allowed)
497
- */
498
- interface OrganizationConfig {
499
- /** Display name for your organization */
500
- name?: string;
501
- /** Organization type */
502
- type?: OrganizationType;
503
- /** Unique identifier (defaults to Playcademy's org) */
504
- identifier?: string;
505
- }
506
- /**
507
- * Course goals for daily student targets
508
- */
509
- interface CourseGoals {
510
- /** Target XP students should earn per day */
511
- dailyXp?: number;
512
- /** Target lessons per day */
513
- dailyLessons?: number;
514
- /** Target active minutes per day */
515
- dailyActiveMinutes?: number;
516
- /** Target accuracy percentage */
517
- dailyAccuracy?: number;
518
- /** Target mastered units per day */
519
- dailyMasteredUnits?: number;
520
- }
521
- /**
522
- * Course metrics and totals
523
- */
524
- interface CourseMetrics {
525
- /** Total XP available in the course */
526
- totalXp?: number;
527
- /** Total lessons/activities in the course */
528
- totalLessons?: number;
529
- /** Total number of grade levels covered by this course */
530
- totalGrades?: number;
531
- /** The type of course (e.g. 'optional', 'hole-filling', 'base') */
532
- courseType?: 'base' | 'hole-filling' | 'optional' | 'Base' | 'Hole-Filling' | 'Optional';
533
- /** Indicates whether the course is supplemental content */
534
- isSupplemental?: boolean;
174
+ interface CourseMetrics {
175
+ /** Total XP available in the course */
176
+ totalXp?: number;
177
+ /** Total lessons/activities in the course */
178
+ totalLessons?: number;
179
+ /** Total number of grade levels covered by this course */
180
+ totalGrades?: number;
181
+ /** The type of course (e.g. 'optional', 'hole-filling', 'base') */
182
+ courseType?: 'base' | 'hole-filling' | 'optional' | 'Base' | 'Hole-Filling' | 'Optional';
183
+ /** Indicates whether the course is supplemental content */
184
+ isSupplemental?: boolean;
535
185
  }
536
186
  /**
537
187
  * Complete course metadata structure
@@ -706,8 +356,18 @@ interface ActivityData {
706
356
  subject: CaliperSubject;
707
357
  /** Activity display name (optional) */
708
358
  activityName?: string;
709
- /** Course identifier (auto-filled from config if not provided) */
359
+ /**
360
+ * Optional course to report against. When omitted, the SDK fills it from
361
+ * the student's enrollment for this grade/subject (preferring a
362
+ * hole-filling course). The platform uses it if it's the base course or one
363
+ * of that grade/subject's hole-filling courses, otherwise the base course.
364
+ */
710
365
  courseId?: string;
366
+ /**
367
+ * The student's enrollment in `courseId`. Filled by the SDK alongside an
368
+ * automatic `courseId`; recorded on hole-filling Caliper events.
369
+ */
370
+ enrollmentId?: string;
711
371
  /** Course display name (auto-filled from config if not provided) */
712
372
  courseName?: string;
713
373
  /** Student email address (optional) */
@@ -838,12 +498,22 @@ interface PopulateStudentResponse {
838
498
  status: string;
839
499
  message?: string;
840
500
  }
501
+ /**
502
+ * `base` courses form a game's grade ladder (one active per grade and subject).
503
+ * `hole_filling` courses are remediation courses Timeback enrolls students into
504
+ * after a failed test; each sits beside a base course.
505
+ */
506
+ type GameTimebackIntegrationKind = 'base' | 'hole_filling';
841
507
  interface GameTimebackIntegration {
842
508
  id: string;
843
509
  gameId: string;
844
510
  courseId: string;
845
511
  grade: number;
846
512
  subject: string;
513
+ /** Defaults to `base` when absent (responses from older servers). */
514
+ kind?: GameTimebackIntegrationKind;
515
+ /** The base course a hole-filling course belongs to; null for base courses. */
516
+ baseIntegrationId?: string | null;
847
517
  status?: 'active' | 'deactivated';
848
518
  totalXp?: number | null;
849
519
  lastVerifiedAt?: Date | null;
@@ -873,6 +543,8 @@ interface HeartbeatRequestBase {
873
543
  appName?: string;
874
544
  sensorUrl?: string;
875
545
  courseId?: string;
546
+ /** The student's enrollment in `courseId`, sent by the SDK with the course hint. */
547
+ enrollmentId?: string;
876
548
  courseName?: string;
877
549
  studentEmail?: string;
878
550
  };
@@ -926,6 +598,8 @@ interface EndActivityRequest {
926
598
  appName?: string;
927
599
  sensorUrl?: string;
928
600
  courseId?: string;
601
+ /** The student's enrollment in `courseId`, sent by the SDK with the course hint. */
602
+ enrollmentId?: string;
929
603
  courseName?: string;
930
604
  studentEmail?: string;
931
605
  };
@@ -955,7 +629,14 @@ interface TimebackPromotionResult {
955
629
  }
956
630
  interface EndActivityResponse {
957
631
  status: 'ok';
632
+ /** The course the activity was recorded against. */
958
633
  courseId: string;
634
+ /**
635
+ * `'hole_filling'` when the activity was recorded against a hole-filling
636
+ * course (the student is enrolled there, or the game passed its
637
+ * `courseId`); `'base'` otherwise.
638
+ */
639
+ courseKind?: GameTimebackIntegrationKind;
959
640
  xpAwarded: number;
960
641
  masteredUnits?: number;
961
642
  pctCompleteApp?: number;
@@ -1735,6 +1416,370 @@ interface AssessmentAwardRecord extends AssessmentMasteryAward {
1735
1416
  warnings?: MasteryWriteWarning[];
1736
1417
  }
1737
1418
 
1419
+ /**
1420
+ * User Types
1421
+ *
1422
+ * Enums, DTOs and API response types. Database row types are in @playcademy/data/types.
1423
+ *
1424
+ * @module types/user
1425
+ */
1426
+
1427
+ type UserRoleEnumType = UserRole;
1428
+ type DeveloperStatusEnumType = 'none' | 'pending' | 'approved';
1429
+ type DeveloperStatusValue = DeveloperStatusEnumType;
1430
+ type TimebackUserRole = 'administrator' | 'aide' | 'guardian' | 'parent' | 'proctor' | 'relative' | 'student' | 'teacher';
1431
+ type TimebackOrgType = 'department' | 'school' | 'district' | 'local' | 'state' | 'national';
1432
+ interface UserEnrollment {
1433
+ gameId?: string;
1434
+ courseId: string;
1435
+ id?: string;
1436
+ grade: number;
1437
+ subject: string;
1438
+ orgId?: string;
1439
+ /**
1440
+ * `base` for the game's grade courses, `hole_filling` for a remediation
1441
+ * course Timeback enrolled the student in. Absent means `base`.
1442
+ */
1443
+ kind?: GameTimebackIntegrationKind;
1444
+ /** For hole-filling enrollments: the course ID of the base course it belongs to. */
1445
+ baseCourseId?: string;
1446
+ /** For hole-filling enrollments: when the enrollment started (ISO 8601). */
1447
+ beginDate?: string | null;
1448
+ /**
1449
+ * For hole-filling enrollments: enrollment metadata as set by Timeback
1450
+ * (e.g. `assignmentId`, `testType`). Every field is optional.
1451
+ */
1452
+ enrollmentMetadata?: Record<string, unknown>;
1453
+ }
1454
+ interface UserOrganization {
1455
+ id: string;
1456
+ name: string | null;
1457
+ type: TimebackOrgType | string;
1458
+ isPrimary: boolean;
1459
+ }
1460
+ interface TimebackStudentProfile {
1461
+ role: TimebackUserRole;
1462
+ organizations: UserOrganization[];
1463
+ }
1464
+ /**
1465
+ * Timeback identity on `/users/me`: who the student is, without enrollments.
1466
+ * Enrollments come from `/timeback/user` (or `client.timeback.user`).
1467
+ */
1468
+ interface UserTimebackProfile extends TimebackStudentProfile {
1469
+ id: string;
1470
+ }
1471
+ interface UserTimebackData extends UserTimebackProfile {
1472
+ enrollments: UserEnrollment[];
1473
+ }
1474
+ type LocalDaySource = 'browser_last_seen' | 'platform_default';
1475
+ interface LocalDayContext {
1476
+ timeZone: string;
1477
+ source: LocalDaySource;
1478
+ /** ISO 8601 timestamp for the last persisted timezone observation. */
1479
+ observedAt: string | null;
1480
+ }
1481
+ /**
1482
+ * OpenID Connect UserInfo claims (NOT a database row).
1483
+ */
1484
+ interface UserInfo {
1485
+ sub: string;
1486
+ email: string;
1487
+ name: string | null;
1488
+ email_verified?: boolean;
1489
+ given_name?: string;
1490
+ family_name?: string;
1491
+ issuer?: string;
1492
+ lti_roles?: unknown;
1493
+ lti_context?: unknown;
1494
+ lti_resource_link?: unknown;
1495
+ timeback_id?: string;
1496
+ }
1497
+ interface DeveloperStatusResponse {
1498
+ status: DeveloperStatusEnumType;
1499
+ }
1500
+ interface DemoProfile {
1501
+ displayName: string;
1502
+ isDefault: boolean;
1503
+ }
1504
+ /**
1505
+ * Update shape for `client.demo.profile.update(...)`.
1506
+ *
1507
+ * Kept as a named type so callers typed against it pick up new fields
1508
+ * automatically, but `displayName` is the only updatable field today and
1509
+ * the server's `DemoProfileSchema` requires it — a no-field payload would
1510
+ * 400 at runtime, so we model that at the type level too. When additional
1511
+ * fields land, make them required/optional individually based on server
1512
+ * validation, rather than blanket-optional.
1513
+ */
1514
+ interface DemoProfileUpdate {
1515
+ displayName: string;
1516
+ }
1517
+ /**
1518
+ * Authenticated user for API responses.
1519
+ * Differs from UserRow: omits timebackId, adds hasTimebackAccount and timeback.
1520
+ */
1521
+ interface AuthenticatedUser {
1522
+ id: string;
1523
+ email: string;
1524
+ emailVerified: boolean;
1525
+ name: string | null;
1526
+ image: string | null;
1527
+ username: string | null;
1528
+ role: UserRoleEnumType;
1529
+ developerStatus: DeveloperStatusEnumType;
1530
+ createdAt: Date;
1531
+ updatedAt: Date;
1532
+ hasTimebackAccount: boolean;
1533
+ localDay: LocalDayContext;
1534
+ timeback?: UserTimebackProfile;
1535
+ }
1536
+ interface GameUser {
1537
+ id: string;
1538
+ name: string | null;
1539
+ role: UserRoleEnumType;
1540
+ username: string | null;
1541
+ email: string | null;
1542
+ localDay: LocalDayContext;
1543
+ timeback?: UserTimebackProfile;
1544
+ }
1545
+
1546
+ /**
1547
+ * Game Types
1548
+ *
1549
+ * Literal types and API DTOs. Database row types are in @playcademy/data/types.
1550
+ *
1551
+ * @module types/game
1552
+ */
1553
+
1554
+ type GameType = 'hosted' | 'external';
1555
+ type GamePlatform = 'web' | 'godot' | (string & {});
1556
+ /**
1557
+ * Game manifest file format (manifest.json).
1558
+ * Note: createdAt is a string here because it's parsed from JSON file.
1559
+ */
1560
+ interface ManifestV1 {
1561
+ version: '1';
1562
+ platform: string;
1563
+ createdAt: string;
1564
+ }
1565
+ interface ManifestVersions {
1566
+ vitePlugin?: string;
1567
+ sdk?: string;
1568
+ cli?: string;
1569
+ vite?: string;
1570
+ godotSdk?: string;
1571
+ godot?: string;
1572
+ }
1573
+ interface ManifestV2 {
1574
+ version: '2';
1575
+ platform: string;
1576
+ createdAt: string;
1577
+ gameVersion?: string;
1578
+ versions: ManifestVersions;
1579
+ }
1580
+ type GameManifest = ManifestV1 | ManifestV2;
1581
+ /**
1582
+ * Git provenance of the working tree a deploy was run from (PLA-294).
1583
+ *
1584
+ * Captured by the CLI at deploy time and persisted verbatim on the deploy
1585
+ * job, so "which commit is running" is answerable from the platform record.
1586
+ * Every field is nullable: a tree with no git, no remote, or a detached HEAD
1587
+ * is recorded honestly rather than refused. Recorded per deploy, never
1588
+ * pinned to the game — a fork deployed under the same slug shows the fork.
1589
+ */
1590
+ interface DeploySource {
1591
+ /** Normalized `https://github.com/<org>/<repo>`; null when no remote or unparseable */
1592
+ repository: string | null;
1593
+ /** Full 40-char SHA of HEAD; null when not in a git tree */
1594
+ commit: string | null;
1595
+ /** Current branch name; null on detached HEAD outside CI */
1596
+ branch: string | null;
1597
+ /** True when the tree had uncommitted changes — the SHA does not describe what shipped */
1598
+ dirty: boolean;
1599
+ /**
1600
+ * True when the uploaded build artifact lives outside the git tree the
1601
+ * other fields describe (`--build ~/Downloads/game.zip`). The commit is
1602
+ * still the deployer's tree, but it may not be what produced the artifact.
1603
+ */
1604
+ externalBuild: boolean;
1605
+ }
1606
+ /**
1607
+ * One successful deploy of a game's worker, as the open releases surface
1608
+ * describes it: what is running and which commit produced it.
1609
+ *
1610
+ * This shape is parsed by external automation. Every field is present on
1611
+ * every entry — nullable, never absent — and nothing from the deploy job
1612
+ * beyond its git provenance and completion time is exposed. No deployer
1613
+ * identity, no schema state, no secrets manifest.
1614
+ */
1615
+ interface Release {
1616
+ slug: string;
1617
+ /** Where the game is served; null when the platform has no URL for it */
1618
+ url: string | null;
1619
+ /**
1620
+ * Git provenance the deploying CLI captured. Null for deploys that
1621
+ * predate provenance capture, so the fleet list is complete on day one
1622
+ * and a consumer can tell "unknown" from "not in git".
1623
+ */
1624
+ source: DeploySource | null;
1625
+ /** When the deploy succeeded, ISO 8601 */
1626
+ deployedAt: string;
1627
+ }
1628
+ /** GET /api/releases — the current release of every game */
1629
+ interface ReleasesResponse {
1630
+ releases: Release[];
1631
+ }
1632
+ /** GET /api/releases/:slug — one game's current release and, on request, its recent history */
1633
+ interface GameReleaseResponse {
1634
+ /** The current release; null when the game has never deployed successfully */
1635
+ current: Release | null;
1636
+ /**
1637
+ * The last N successful deploys, newest first, current included. Only
1638
+ * present when `?history=N` was requested.
1639
+ */
1640
+ history?: Release[];
1641
+ }
1642
+ interface DomainValidationRecords {
1643
+ ownership?: {
1644
+ name?: string;
1645
+ value?: string;
1646
+ type?: string;
1647
+ };
1648
+ ssl?: {
1649
+ txt_name?: string;
1650
+ txt_value?: string;
1651
+ }[];
1652
+ }
1653
+
1654
+ /**
1655
+ * Leaderboard Types
1656
+ *
1657
+ * @module types/leaderboard
1658
+ */
1659
+ type LeaderboardTimeframe = 'all_time' | 'monthly' | 'weekly' | 'daily';
1660
+ interface LeaderboardOptions {
1661
+ timeframe?: LeaderboardTimeframe;
1662
+ limit?: number;
1663
+ offset?: number;
1664
+ gameId?: string;
1665
+ }
1666
+ interface LeaderboardEntry {
1667
+ rank: number;
1668
+ userId: string;
1669
+ username: string;
1670
+ userImage?: string | null;
1671
+ score: number;
1672
+ achievedAt: Date;
1673
+ metadata?: Record<string, unknown>;
1674
+ gameId?: string;
1675
+ gameTitle?: string;
1676
+ gameSlug?: string;
1677
+ }
1678
+ interface UserRank {
1679
+ rank: number;
1680
+ totalPlayers: number;
1681
+ score: number;
1682
+ percentile: number;
1683
+ }
1684
+ interface UserRankResponse {
1685
+ rank: number;
1686
+ score: number;
1687
+ userId: string;
1688
+ }
1689
+ interface UserScore {
1690
+ id: string;
1691
+ score: number;
1692
+ achievedAt: Date;
1693
+ metadata?: Record<string, unknown>;
1694
+ gameId: string;
1695
+ gameTitle: string;
1696
+ gameSlug: string;
1697
+ }
1698
+ /**
1699
+ * Leaderboard entry with required game context.
1700
+ * Used when fetching leaderboards for a specific game.
1701
+ */
1702
+ interface GameLeaderboardEntry {
1703
+ rank: number;
1704
+ userId: string;
1705
+ username: string;
1706
+ userImage?: string | null;
1707
+ score: number;
1708
+ achievedAt: Date;
1709
+ metadata?: Record<string, unknown>;
1710
+ gameId: string;
1711
+ gameTitle: string;
1712
+ gameSlug: string;
1713
+ }
1714
+
1715
+ /**
1716
+ * Skill Status Types
1717
+ *
1718
+ * Contract for cross-game skill status queries. An answering game exports
1719
+ * `getSkillStatus` from `server/lib/skills.ts`; the platform calls its
1720
+ * reserved `/__playcademy/skills/status` route on behalf of another game.
1721
+ *
1722
+ * @module types/skills
1723
+ */
1724
+
1725
+ /**
1726
+ * Current status of one skill for one student, as judged by the answering game.
1727
+ *
1728
+ * - `locked`: the student can't work on the skill yet in this game.
1729
+ * - `unlocked`: available to the student, but no progress yet.
1730
+ * - `in_progress`: started, not mastered.
1731
+ * - `mastered`: currently meets this game's mastery bar, including by placement.
1732
+ * - `unknown_skill`: the skill ID isn't recognized. Never report an unrecognized
1733
+ * skill as `locked`.
1734
+ */
1735
+ type SkillStatus = (typeof SKILL_STATUSES)[number];
1736
+ /** How a `mastered` status was reached. */
1737
+ type SkillMasterySource = (typeof SKILL_MASTERY_SOURCES)[number];
1738
+ interface SkillStatusResult {
1739
+ /** Skill ID, lowercase. */
1740
+ skill: string;
1741
+ /** Current status. */
1742
+ status: SkillStatus;
1743
+ /** How mastery was reached. Only allowed when `status` is `mastered`. */
1744
+ source?: SkillMasterySource;
1745
+ /** ISO 8601 time of the evidence behind this status. */
1746
+ asOf?: string;
1747
+ }
1748
+ /** Response returned by an answering game's `getSkillStatus` handler. */
1749
+ interface SkillStatusResponse {
1750
+ /** Student identifier (Playcademy user ID). Must match the requested student. */
1751
+ studentId: string;
1752
+ /** One result per skill the handler was asked about. */
1753
+ results: SkillStatusResult[];
1754
+ }
1755
+ /** The game asking for skill status, as identified by the platform. */
1756
+ interface SkillStatusRequestingGame {
1757
+ id: string;
1758
+ slug: string;
1759
+ }
1760
+ /** Body of `POST /api/skills/status`, sent by the asking game's worker. */
1761
+ interface SkillStatusRequest {
1762
+ /** Slug of the game to ask. */
1763
+ game: string;
1764
+ /** Playcademy user ID of the student. */
1765
+ studentId: string;
1766
+ /** Skill IDs to ask about. Normalized (trimmed, lowercased) and de-duplicated. */
1767
+ skills: string[];
1768
+ }
1769
+ /** Why the answering game couldn't answer. */
1770
+ type SkillStatusUnsupportedReason = 'not_implemented' | 'no_active_deployment' | 'timeout' | 'fetch_failed' | 'invalid_response';
1771
+ /**
1772
+ * What the asking game gets back. When `supported` is false, fall back to
1773
+ * the game's own default (usually: treat the skills as not mastered).
1774
+ */
1775
+ type SkillStatusLookupResult = {
1776
+ supported: true;
1777
+ results: SkillStatusResult[];
1778
+ } | {
1779
+ supported: false;
1780
+ reason: SkillStatusUnsupportedReason;
1781
+ };
1782
+
1738
1783
  /**
1739
1784
  * Base error class for Cademy SDK specific errors.
1740
1785
  */
@@ -3989,6 +4034,8 @@ interface EmbedTimebackRecording {
3989
4034
  subject: TimebackSubject;
3990
4035
  /** Course id hint, same semantics as `startActivity()`. */
3991
4036
  courseId?: string;
4037
+ /** The student's enrollment in `courseId`; filled with it automatically. */
4038
+ enrollmentId?: string;
3992
4039
  }
3993
4040
  /**
3994
4041
  * Everything a parent needs to resume an interrupted launch later. The