@playcademy/sdk 0.24.0 → 0.24.1-beta.1

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
@@ -838,12 +488,22 @@ interface PopulateStudentResponse {
838
488
  status: string;
839
489
  message?: string;
840
490
  }
491
+ /**
492
+ * `base` courses form a game's grade ladder (one active per grade and subject).
493
+ * `hole_filling` courses are remediation courses Timeback enrolls students into
494
+ * after a failed test; each sits beside a base course.
495
+ */
496
+ type GameTimebackIntegrationKind = 'base' | 'hole_filling';
841
497
  interface GameTimebackIntegration {
842
498
  id: string;
843
499
  gameId: string;
844
500
  courseId: string;
845
501
  grade: number;
846
502
  subject: string;
503
+ /** Defaults to `base` when absent (responses from older servers). */
504
+ kind?: GameTimebackIntegrationKind;
505
+ /** The base course a hole-filling course belongs to; null for base courses. */
506
+ baseIntegrationId?: string | null;
847
507
  status?: 'active' | 'deactivated';
848
508
  totalXp?: number | null;
849
509
  lastVerifiedAt?: Date | null;
@@ -1735,6 +1395,370 @@ interface AssessmentAwardRecord extends AssessmentMasteryAward {
1735
1395
  warnings?: MasteryWriteWarning[];
1736
1396
  }
1737
1397
 
1398
+ /**
1399
+ * User Types
1400
+ *
1401
+ * Enums, DTOs and API response types. Database row types are in @playcademy/data/types.
1402
+ *
1403
+ * @module types/user
1404
+ */
1405
+
1406
+ type UserRoleEnumType = UserRole;
1407
+ type DeveloperStatusEnumType = 'none' | 'pending' | 'approved';
1408
+ type DeveloperStatusValue = DeveloperStatusEnumType;
1409
+ type TimebackUserRole = 'administrator' | 'aide' | 'guardian' | 'parent' | 'proctor' | 'relative' | 'student' | 'teacher';
1410
+ type TimebackOrgType = 'department' | 'school' | 'district' | 'local' | 'state' | 'national';
1411
+ interface UserEnrollment {
1412
+ gameId?: string;
1413
+ courseId: string;
1414
+ id?: string;
1415
+ grade: number;
1416
+ subject: string;
1417
+ orgId?: string;
1418
+ /**
1419
+ * `base` for the game's grade courses, `hole_filling` for a remediation
1420
+ * course Timeback enrolled the student in. Absent means `base`.
1421
+ */
1422
+ kind?: GameTimebackIntegrationKind;
1423
+ /** For hole-filling enrollments: the course ID of the base course it belongs to. */
1424
+ baseCourseId?: string;
1425
+ /** For hole-filling enrollments: when the enrollment started (ISO 8601). */
1426
+ beginDate?: string | null;
1427
+ /**
1428
+ * For hole-filling enrollments: enrollment metadata as set by Timeback
1429
+ * (e.g. `assignmentId`, `testType`). Every field is optional.
1430
+ */
1431
+ enrollmentMetadata?: Record<string, unknown>;
1432
+ }
1433
+ interface UserOrganization {
1434
+ id: string;
1435
+ name: string | null;
1436
+ type: TimebackOrgType | string;
1437
+ isPrimary: boolean;
1438
+ }
1439
+ interface TimebackStudentProfile {
1440
+ role: TimebackUserRole;
1441
+ organizations: UserOrganization[];
1442
+ }
1443
+ /**
1444
+ * Timeback identity on `/users/me`: who the student is, without enrollments.
1445
+ * Enrollments come from `/timeback/user` (or `client.timeback.user`).
1446
+ */
1447
+ interface UserTimebackProfile extends TimebackStudentProfile {
1448
+ id: string;
1449
+ }
1450
+ interface UserTimebackData extends UserTimebackProfile {
1451
+ enrollments: UserEnrollment[];
1452
+ }
1453
+ type LocalDaySource = 'browser_last_seen' | 'platform_default';
1454
+ interface LocalDayContext {
1455
+ timeZone: string;
1456
+ source: LocalDaySource;
1457
+ /** ISO 8601 timestamp for the last persisted timezone observation. */
1458
+ observedAt: string | null;
1459
+ }
1460
+ /**
1461
+ * OpenID Connect UserInfo claims (NOT a database row).
1462
+ */
1463
+ interface UserInfo {
1464
+ sub: string;
1465
+ email: string;
1466
+ name: string | null;
1467
+ email_verified?: boolean;
1468
+ given_name?: string;
1469
+ family_name?: string;
1470
+ issuer?: string;
1471
+ lti_roles?: unknown;
1472
+ lti_context?: unknown;
1473
+ lti_resource_link?: unknown;
1474
+ timeback_id?: string;
1475
+ }
1476
+ interface DeveloperStatusResponse {
1477
+ status: DeveloperStatusEnumType;
1478
+ }
1479
+ interface DemoProfile {
1480
+ displayName: string;
1481
+ isDefault: boolean;
1482
+ }
1483
+ /**
1484
+ * Update shape for `client.demo.profile.update(...)`.
1485
+ *
1486
+ * Kept as a named type so callers typed against it pick up new fields
1487
+ * automatically, but `displayName` is the only updatable field today and
1488
+ * the server's `DemoProfileSchema` requires it — a no-field payload would
1489
+ * 400 at runtime, so we model that at the type level too. When additional
1490
+ * fields land, make them required/optional individually based on server
1491
+ * validation, rather than blanket-optional.
1492
+ */
1493
+ interface DemoProfileUpdate {
1494
+ displayName: string;
1495
+ }
1496
+ /**
1497
+ * Authenticated user for API responses.
1498
+ * Differs from UserRow: omits timebackId, adds hasTimebackAccount and timeback.
1499
+ */
1500
+ interface AuthenticatedUser {
1501
+ id: string;
1502
+ email: string;
1503
+ emailVerified: boolean;
1504
+ name: string | null;
1505
+ image: string | null;
1506
+ username: string | null;
1507
+ role: UserRoleEnumType;
1508
+ developerStatus: DeveloperStatusEnumType;
1509
+ createdAt: Date;
1510
+ updatedAt: Date;
1511
+ hasTimebackAccount: boolean;
1512
+ localDay: LocalDayContext;
1513
+ timeback?: UserTimebackProfile;
1514
+ }
1515
+ interface GameUser {
1516
+ id: string;
1517
+ name: string | null;
1518
+ role: UserRoleEnumType;
1519
+ username: string | null;
1520
+ email: string | null;
1521
+ localDay: LocalDayContext;
1522
+ timeback?: UserTimebackProfile;
1523
+ }
1524
+
1525
+ /**
1526
+ * Game Types
1527
+ *
1528
+ * Literal types and API DTOs. Database row types are in @playcademy/data/types.
1529
+ *
1530
+ * @module types/game
1531
+ */
1532
+
1533
+ type GameType = 'hosted' | 'external';
1534
+ type GamePlatform = 'web' | 'godot' | (string & {});
1535
+ /**
1536
+ * Game manifest file format (manifest.json).
1537
+ * Note: createdAt is a string here because it's parsed from JSON file.
1538
+ */
1539
+ interface ManifestV1 {
1540
+ version: '1';
1541
+ platform: string;
1542
+ createdAt: string;
1543
+ }
1544
+ interface ManifestVersions {
1545
+ vitePlugin?: string;
1546
+ sdk?: string;
1547
+ cli?: string;
1548
+ vite?: string;
1549
+ godotSdk?: string;
1550
+ godot?: string;
1551
+ }
1552
+ interface ManifestV2 {
1553
+ version: '2';
1554
+ platform: string;
1555
+ createdAt: string;
1556
+ gameVersion?: string;
1557
+ versions: ManifestVersions;
1558
+ }
1559
+ type GameManifest = ManifestV1 | ManifestV2;
1560
+ /**
1561
+ * Git provenance of the working tree a deploy was run from (PLA-294).
1562
+ *
1563
+ * Captured by the CLI at deploy time and persisted verbatim on the deploy
1564
+ * job, so "which commit is running" is answerable from the platform record.
1565
+ * Every field is nullable: a tree with no git, no remote, or a detached HEAD
1566
+ * is recorded honestly rather than refused. Recorded per deploy, never
1567
+ * pinned to the game — a fork deployed under the same slug shows the fork.
1568
+ */
1569
+ interface DeploySource {
1570
+ /** Normalized `https://github.com/<org>/<repo>`; null when no remote or unparseable */
1571
+ repository: string | null;
1572
+ /** Full 40-char SHA of HEAD; null when not in a git tree */
1573
+ commit: string | null;
1574
+ /** Current branch name; null on detached HEAD outside CI */
1575
+ branch: string | null;
1576
+ /** True when the tree had uncommitted changes — the SHA does not describe what shipped */
1577
+ dirty: boolean;
1578
+ /**
1579
+ * True when the uploaded build artifact lives outside the git tree the
1580
+ * other fields describe (`--build ~/Downloads/game.zip`). The commit is
1581
+ * still the deployer's tree, but it may not be what produced the artifact.
1582
+ */
1583
+ externalBuild: boolean;
1584
+ }
1585
+ /**
1586
+ * One successful deploy of a game's worker, as the open releases surface
1587
+ * describes it: what is running and which commit produced it.
1588
+ *
1589
+ * This shape is parsed by external automation. Every field is present on
1590
+ * every entry — nullable, never absent — and nothing from the deploy job
1591
+ * beyond its git provenance and completion time is exposed. No deployer
1592
+ * identity, no schema state, no secrets manifest.
1593
+ */
1594
+ interface Release {
1595
+ slug: string;
1596
+ /** Where the game is served; null when the platform has no URL for it */
1597
+ url: string | null;
1598
+ /**
1599
+ * Git provenance the deploying CLI captured. Null for deploys that
1600
+ * predate provenance capture, so the fleet list is complete on day one
1601
+ * and a consumer can tell "unknown" from "not in git".
1602
+ */
1603
+ source: DeploySource | null;
1604
+ /** When the deploy succeeded, ISO 8601 */
1605
+ deployedAt: string;
1606
+ }
1607
+ /** GET /api/releases — the current release of every game */
1608
+ interface ReleasesResponse {
1609
+ releases: Release[];
1610
+ }
1611
+ /** GET /api/releases/:slug — one game's current release and, on request, its recent history */
1612
+ interface GameReleaseResponse {
1613
+ /** The current release; null when the game has never deployed successfully */
1614
+ current: Release | null;
1615
+ /**
1616
+ * The last N successful deploys, newest first, current included. Only
1617
+ * present when `?history=N` was requested.
1618
+ */
1619
+ history?: Release[];
1620
+ }
1621
+ interface DomainValidationRecords {
1622
+ ownership?: {
1623
+ name?: string;
1624
+ value?: string;
1625
+ type?: string;
1626
+ };
1627
+ ssl?: {
1628
+ txt_name?: string;
1629
+ txt_value?: string;
1630
+ }[];
1631
+ }
1632
+
1633
+ /**
1634
+ * Leaderboard Types
1635
+ *
1636
+ * @module types/leaderboard
1637
+ */
1638
+ type LeaderboardTimeframe = 'all_time' | 'monthly' | 'weekly' | 'daily';
1639
+ interface LeaderboardOptions {
1640
+ timeframe?: LeaderboardTimeframe;
1641
+ limit?: number;
1642
+ offset?: number;
1643
+ gameId?: string;
1644
+ }
1645
+ interface LeaderboardEntry {
1646
+ rank: number;
1647
+ userId: string;
1648
+ username: string;
1649
+ userImage?: string | null;
1650
+ score: number;
1651
+ achievedAt: Date;
1652
+ metadata?: Record<string, unknown>;
1653
+ gameId?: string;
1654
+ gameTitle?: string;
1655
+ gameSlug?: string;
1656
+ }
1657
+ interface UserRank {
1658
+ rank: number;
1659
+ totalPlayers: number;
1660
+ score: number;
1661
+ percentile: number;
1662
+ }
1663
+ interface UserRankResponse {
1664
+ rank: number;
1665
+ score: number;
1666
+ userId: string;
1667
+ }
1668
+ interface UserScore {
1669
+ id: string;
1670
+ score: number;
1671
+ achievedAt: Date;
1672
+ metadata?: Record<string, unknown>;
1673
+ gameId: string;
1674
+ gameTitle: string;
1675
+ gameSlug: string;
1676
+ }
1677
+ /**
1678
+ * Leaderboard entry with required game context.
1679
+ * Used when fetching leaderboards for a specific game.
1680
+ */
1681
+ interface GameLeaderboardEntry {
1682
+ rank: number;
1683
+ userId: string;
1684
+ username: string;
1685
+ userImage?: string | null;
1686
+ score: number;
1687
+ achievedAt: Date;
1688
+ metadata?: Record<string, unknown>;
1689
+ gameId: string;
1690
+ gameTitle: string;
1691
+ gameSlug: string;
1692
+ }
1693
+
1694
+ /**
1695
+ * Skill Status Types
1696
+ *
1697
+ * Contract for cross-game skill status queries. An answering game exports
1698
+ * `getSkillStatus` from `server/lib/skills.ts`; the platform calls its
1699
+ * reserved `/__playcademy/skills/status` route on behalf of another game.
1700
+ *
1701
+ * @module types/skills
1702
+ */
1703
+
1704
+ /**
1705
+ * Current status of one skill for one student, as judged by the answering game.
1706
+ *
1707
+ * - `locked`: the student can't work on the skill yet in this game.
1708
+ * - `unlocked`: available to the student, but no progress yet.
1709
+ * - `in_progress`: started, not mastered.
1710
+ * - `mastered`: currently meets this game's mastery bar, including by placement.
1711
+ * - `unknown_skill`: the skill ID isn't recognized. Never report an unrecognized
1712
+ * skill as `locked`.
1713
+ */
1714
+ type SkillStatus = (typeof SKILL_STATUSES)[number];
1715
+ /** How a `mastered` status was reached. */
1716
+ type SkillMasterySource = (typeof SKILL_MASTERY_SOURCES)[number];
1717
+ interface SkillStatusResult {
1718
+ /** Skill ID, lowercase. */
1719
+ skill: string;
1720
+ /** Current status. */
1721
+ status: SkillStatus;
1722
+ /** How mastery was reached. Only allowed when `status` is `mastered`. */
1723
+ source?: SkillMasterySource;
1724
+ /** ISO 8601 time of the evidence behind this status. */
1725
+ asOf?: string;
1726
+ }
1727
+ /** Response returned by an answering game's `getSkillStatus` handler. */
1728
+ interface SkillStatusResponse {
1729
+ /** Student identifier (Playcademy user ID). Must match the requested student. */
1730
+ studentId: string;
1731
+ /** One result per skill the handler was asked about. */
1732
+ results: SkillStatusResult[];
1733
+ }
1734
+ /** The game asking for skill status, as identified by the platform. */
1735
+ interface SkillStatusRequestingGame {
1736
+ id: string;
1737
+ slug: string;
1738
+ }
1739
+ /** Body of `POST /api/skills/status`, sent by the asking game's worker. */
1740
+ interface SkillStatusRequest {
1741
+ /** Slug of the game to ask. */
1742
+ game: string;
1743
+ /** Playcademy user ID of the student. */
1744
+ studentId: string;
1745
+ /** Skill IDs to ask about. Normalized (trimmed, lowercased) and de-duplicated. */
1746
+ skills: string[];
1747
+ }
1748
+ /** Why the answering game couldn't answer. */
1749
+ type SkillStatusUnsupportedReason = 'not_implemented' | 'no_active_deployment' | 'timeout' | 'fetch_failed' | 'invalid_response';
1750
+ /**
1751
+ * What the asking game gets back. When `supported` is false, fall back to
1752
+ * the game's own default (usually: treat the skills as not mastered).
1753
+ */
1754
+ type SkillStatusLookupResult = {
1755
+ supported: true;
1756
+ results: SkillStatusResult[];
1757
+ } | {
1758
+ supported: false;
1759
+ reason: SkillStatusUnsupportedReason;
1760
+ };
1761
+
1738
1762
  /**
1739
1763
  * Base error class for Cademy SDK specific errors.
1740
1764
  */