@playcademy/sdk 0.16.1-beta.2 → 0.16.1-beta.21

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/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as _playcademy_types from '@playcademy/types';
2
2
  import { LocalDayContext } from '@playcademy/types';
3
- import { TimebackGrade, TimebackSubject, HeartbeatRequest } from '@playcademy/types/timeback';
3
+ import { TimebackGrade, TimebackSubject, ELevel, ChildEndActivityScoreData, EndActivityRequest, HeartbeatRequest, EndActivityScoreData, EndActivityResponse } from '@playcademy/types/timeback';
4
4
  import { TimebackUserRole, UserEnrollment, UserOrganization, UserInfo } from '@playcademy/types/user';
5
5
  import { AUTH_PROVIDER_IDS } from '@playcademy/constants';
6
6
 
@@ -227,6 +227,8 @@ declare abstract class PlaycademyBaseClient {
227
227
  isInIframe: boolean;
228
228
  };
229
229
  protected initPayload?: InitPayload;
230
+ /** Memoized `client.parent` handle; built on first access in child mode. */
231
+ private parentHandle?;
230
232
  protected launchId?: string;
231
233
  protected gameOrigin?: string;
232
234
  private browserTimeZone?;
@@ -247,6 +249,13 @@ declare abstract class PlaycademyBaseClient {
247
249
  * Local day context supplied by the platform during iframe initialization.
248
250
  */
249
251
  get localDay(): LocalDayContext | undefined;
252
+ /**
253
+ * Null outside `mode: 'child'` even if a payload smuggled in a `parent`
254
+ * block, so `if (client.parent)` is a reliable child-mode test. The
255
+ * returned handle is the wire block plus `checkpoint()`, memoized so
256
+ * `client.parent === client.parent`.
257
+ */
258
+ get parent(): ParentGameHandle | null;
250
259
  /**
251
260
  * Sets the authentication token for API requests.
252
261
  */
@@ -520,11 +529,32 @@ declare enum MessageEvents {
520
529
  */
521
530
  DEMO_END = "PLAYCADEMY_DEMO_END",
522
531
  /**
523
- * Game shares its latest TimeBack heartbeat window with the parent shell.
524
- * The shell can relay this payload during top-level page teardown, which is
525
- * more reliable than relying only on cross-origin iframe unload events.
532
+ * Game shares its latest TimeBack heartbeat window with the embedding
533
+ * window. The hub relays this payload during top-level page teardown
534
+ * (more reliable than cross-origin iframe unload events); in child mode
535
+ * this stream is the heartbeat's only delivery path.
526
536
  */
527
537
  TIMEBACK_HEARTBEAT_RELAY = "PLAYCADEMY_TIMEBACK_HEARTBEAT_RELAY",
538
+ /**
539
+ * Game announces its tracker opened a run (startActivity).
540
+ * Today only child-mode games emit this: the parent opens its own
541
+ * Timeback run on receipt, so tracked time starts when the lesson
542
+ * does, not when the iframe boots.
543
+ */
544
+ TIMEBACK_ACTIVITY_START = "PLAYCADEMY_TIMEBACK_ACTIVITY_START",
545
+ /**
546
+ * Game relays its full end-activity report instead of POSTing it to
547
+ * its own backend. Today only child-mode games emit this: the parent
548
+ * reports the result to Timeback under its own identity.
549
+ */
550
+ TIMEBACK_ACTIVITY_END = "PLAYCADEMY_TIMEBACK_ACTIVITY_END",
551
+ /**
552
+ * Child game → parent game. An opaque checkpoint of the child's
553
+ * internal state, streamed during play so the parent always holds a
554
+ * near-current copy (the death-time postMessage hop is unreliable;
555
+ * continuous checkpointing is the design). Latest wins.
556
+ */
557
+ CHECKPOINT = "PLAYCADEMY_CHECKPOINT",
528
558
  /**
529
559
  * Notifies about authentication state changes.
530
560
  * Can be sent in both directions depending on auth flow.
@@ -594,11 +624,36 @@ interface MessageEventMap {
594
624
  [MessageEvents.DEMO_END]: DemoEndPayload;
595
625
  /** Latest TimeBack heartbeat window for parent-shell unload relay */
596
626
  [MessageEvents.TIMEBACK_HEARTBEAT_RELAY]: TimebackHeartbeatRelayRequest;
627
+ /** Child-mode run-opened announcement */
628
+ [MessageEvents.TIMEBACK_ACTIVITY_START]: TimebackActivityStartRelay;
629
+ /** Child-mode end-activity report relay */
630
+ [MessageEvents.TIMEBACK_ACTIVITY_END]: TimebackActivityEndRelay;
631
+ [MessageEvents.CHECKPOINT]: ChildCheckpointRelay;
597
632
  /** Authentication state change notification */
598
633
  [MessageEvents.AUTH_STATE_CHANGE]: AuthStateChangePayload;
599
634
  /** OAuth callback data from popup/new-tab windows */
600
635
  [MessageEvents.AUTH_CALLBACK]: AuthCallbackPayload;
601
636
  }
637
+ /**
638
+ * Options for `messaging.listen()`.
639
+ */
640
+ interface ListenOptions {
641
+ /**
642
+ * Accept postMessages only from the embedding window (`window.parent`).
643
+ *
644
+ * Defaults by direction: on for launcher-directed control messages
645
+ * (see the direction table), off otherwise. A child iframe posts to
646
+ * this window too (that is how the embed relay works), but it must
647
+ * never be able to impersonate the platform above; pass `false` to
648
+ * loosen a launcher-directed listener deliberately (for example a
649
+ * test rig that posts control messages from a non-parent window).
650
+ * Local CustomEvent delivery is unaffected, since a separate
651
+ * document cannot dispatch into this one. At the top window nothing
652
+ * can match (`window.parent === window`), which is correct: a
653
+ * standalone run has no launcher.
654
+ */
655
+ fromParent?: boolean;
656
+ }
602
657
  /**
603
658
  * **PlaycademyMessaging Class**
604
659
  *
@@ -734,7 +789,7 @@ declare class PlaycademyMessaging {
734
789
  * })
735
790
  * ```
736
791
  */
737
- listen<K extends MessageEvents>(type: K, handler: MessageHandler<MessageEventMap[K]>): void;
792
+ listen<K extends MessageEvents>(type: K, handler: MessageHandler<MessageEventMap[K]>, options?: ListenOptions): void;
738
793
  /**
739
794
  * **Remove Message Listener Method**
740
795
  *
@@ -966,153 +1021,14 @@ declare class PlaycademyMessaging {
966
1021
  declare const messaging: PlaycademyMessaging;
967
1022
 
968
1023
  /**
969
- * Playcademy SDK client for game developers.
970
- * Provides namespaced access to platform features for games running inside Cademy.
1024
+ * `endActivity()`'s resolution in child mode: the report was relayed to the
1025
+ * parent game rather than POSTed to the platform, so no platform award data
1026
+ * (courseId, xpAwarded) exists. The parent reports under its own identity.
971
1027
  */
972
- declare class PlaycademyClient extends PlaycademyBaseClient {
973
- /**
974
- * Connect external identity providers to the user's Playcademy account.
975
- * - `connect(provider)` - Link Discord, Google, etc. via OAuth popup
976
- */
977
- identity: {
978
- connect: (options: AuthOptions) => Promise<AuthResult>;
979
- _getContext: () => {
980
- isInIframe: boolean;
981
- };
982
- };
983
- /**
984
- * Game runtime lifecycle and asset loading.
985
- * - `exit()` - Return to Cademy hub
986
- * - `getGameToken()` - Get short-lived auth token
987
- * - `assets.url()`, `assets.json()`, `assets.fetch()` - Load game assets
988
- * - `on('pause')`, `on('resume')` - Handle visibility changes
989
- */
990
- runtime: {
991
- exit: () => void;
992
- onInit: (handler: (context: GameContextPayload) => void) => void;
993
- onTokenRefresh: (handler: (data: {
994
- token: string;
995
- exp: number;
996
- }) => void) => void;
997
- onPause: (handler: () => void) => void;
998
- onResume: (handler: () => void) => void;
999
- onForceExit: (handler: () => void) => void;
1000
- onOverlay: (handler: (isVisible: boolean) => void) => void;
1001
- ready: () => void;
1002
- sendTelemetry: (data: {
1003
- fps: number;
1004
- mem: number;
1005
- }) => void;
1006
- removeListener: (eventType: MessageEvents, handler: ((context: GameContextPayload) => void) | ((data: {
1007
- token: string;
1008
- exp: number;
1009
- }) => void) | (() => void) | ((isVisible: boolean) => void)) => void;
1010
- removeAllListeners: () => void;
1011
- getListenerCounts: () => Record<string, number>;
1012
- assets: {
1013
- url(pathOrStrings: string | TemplateStringsArray, ...values: unknown[]): string;
1014
- fetch: (path: string, options?: RequestInit) => Promise<Response>;
1015
- json: <T = unknown>(path: string) => Promise<T>;
1016
- blob: (path: string) => Promise<Blob>;
1017
- text: (path: string) => Promise<string>;
1018
- arrayBuffer: (path: string) => Promise<ArrayBuffer>;
1019
- };
1020
- };
1021
- /**
1022
- * TimeBack integration for activity tracking and user context.
1023
- *
1024
- * User context (cached from init, refreshable):
1025
- * - `user.role` - User's role (student, parent, teacher, etc.)
1026
- * - `user.enrollments` - Courses the player is enrolled in for this game
1027
- * - `user.refresh({ only: ['enrollments'] })` - Refresh enrollments from server
1028
- * - `user.organizations` - Schools/districts the player belongs to
1029
- * - `user.fetch()` - Refresh user context from server
1030
- *
1031
- * Activity tracking:
1032
- * - `currentRunId` - Current activity run ID, or undefined when inactive
1033
- * - `startActivity(metadata)` - Begin tracking an activity, return its run
1034
- * ID, and automatically handle hidden-tab and visible-tab inactivity
1035
- * with configurable paused-heartbeat timeout behavior
1036
- * - `pauseActivity()` / `resumeActivity()` - Pause/resume timer
1037
- * - `endActivity(scoreData)` - Submit activity results to TimeBack
1038
- */
1039
- timeback: {
1040
- assessments: {
1041
- start: (input: _playcademy_types.StartAssessmentInput) => Promise<_playcademy_types.AssessmentAttemptSnapshot>;
1042
- latest: (options: _playcademy_types.GetLatestAssessmentOptions) => Promise<_playcademy_types.LatestAssessmentResult | null>;
1043
- get: (attemptId: string) => Promise<_playcademy_types.AssessmentAttemptSnapshot>;
1044
- save: (attemptId: string, input: _playcademy_types.SaveAssessmentInput) => Promise<_playcademy_types.AssessmentSaveResult>;
1045
- submit: (attemptId: string, input: _playcademy_types.SubmitAssessmentInput) => Promise<_playcademy_types.AssessmentSubmitResult>;
1046
- };
1047
- readonly user: TimebackUser;
1048
- readonly currentRunId: string | undefined;
1049
- startActivity: (metadata: _playcademy_types.ActivityData, options?: StartActivityOptions) => StartActivityResult;
1050
- pauseActivity: () => void;
1051
- resumeActivity: () => void;
1052
- endActivity: (data: _playcademy_types.EndActivityScoreData) => Promise<_playcademy_types.EndActivityResponse>;
1053
- course: {
1054
- advance: (options?: {
1055
- subject?: _playcademy_types.TimebackSubject;
1056
- }) => Promise<_playcademy_types.AdvanceCourseResponse>;
1057
- unenroll: (options?: {
1058
- subject?: _playcademy_types.TimebackSubject;
1059
- force?: boolean;
1060
- }) => Promise<_playcademy_types.UnenrollCourseResponse>;
1061
- };
1062
- };
1063
- /**
1064
- * Game score submission and leaderboards.
1065
- * - `submit(score, metadata?)` - Record a game score
1066
- */
1067
- scores: {
1068
- submit: (score: number, metadata?: Record<string, unknown>) => Promise<ScoreSubmission>;
1069
- };
1070
- /**
1071
- * Read-only leaderboard access for the current game scope.
1072
- * - `fetch(options?)` - Fetch leaderboard entries
1073
- */
1074
- leaderboard: {
1075
- fetch: (options?: _playcademy_types.LeaderboardOptions) => Promise<_playcademy_types.GameLeaderboardEntry[]>;
1076
- };
1077
- /**
1078
- * Demo-mode helpers. Methods throw when called outside `client.mode === 'demo'`,
1079
- * so callers should gate on the mode before reaching in.
1080
- * - `profile.get()` - Read the anonymous demo player's profile
1081
- * - `profile.update(updates)` - Update the demo player's profile (today: the required `displayName`)
1082
- * - `end(score, options?)` - Signal to the parent shell that the demo has ended
1083
- */
1084
- demo: {
1085
- profile: {
1086
- get: () => Promise<_playcademy_types.DemoProfile>;
1087
- update: (updates: _playcademy_types.DemoProfileUpdate) => Promise<_playcademy_types.DemoProfile>;
1088
- };
1089
- end: (score: number, options?: DemoEndOptions) => void;
1090
- };
1091
- /**
1092
- * Make requests to your game's custom backend API routes.
1093
- * - `get(path)`, `post(path, body)`, `put()`, `delete()` - HTTP methods
1094
- * - Routes are relative to your game's deployment (e.g., '/hello' → your-game.playcademy.gg/api/hello)
1095
- */
1096
- backend: {
1097
- get<T = unknown>(path: string, headers?: Record<string, string>): Promise<T>;
1098
- post<T = unknown>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T>;
1099
- put<T = unknown>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T>;
1100
- patch<T = unknown>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T>;
1101
- delete<T = unknown>(path: string, headers?: Record<string, string>): Promise<T>;
1102
- request<T = unknown>(path: string, method: Method, body?: unknown, headers?: Record<string, string>): Promise<T>;
1103
- download(path: string, method?: Method, body?: unknown, headers?: Record<string, string>): Promise<Response>;
1104
- url(pathOrStrings: string | TemplateStringsArray, ...values: unknown[]): string;
1105
- };
1106
- /** Auto-initializes a PlaycademyClient with context from the environment */
1107
- static init: typeof init;
1108
- /** Authenticates a user with email and password */
1109
- static login: typeof login;
1110
- /** Static identity utilities for OAuth operations */
1111
- static identity: {
1112
- parseOAuthState: typeof parseOAuthState;
1113
- };
1028
+ interface RelayedEndActivityResult {
1029
+ status: 'relayed';
1030
+ runId: string;
1114
1031
  }
1115
-
1116
1032
  /**
1117
1033
  * Options for configuring activity tracking behavior.
1118
1034
  */
@@ -1450,8 +1366,79 @@ type TokenType = 'session' | 'apiKey' | 'gameJwt';
1450
1366
  * - `'standalone'` — game is running outside any iframe (e.g. `bun run dev`
1451
1367
  * or direct-deploy preview) with a mock token and no real platform
1452
1368
  * context. API calls will not succeed; use this to branch UX locally.
1369
+ * - `'child'` — game is embedded by another game (the parent), which ran
1370
+ * the INIT handshake itself and included a `parent` block in the payload.
1371
+ * The user and token are real (the parent's own). Direct Timeback
1372
+ * reporting is suppressed and relayed to the parent instead; read
1373
+ * `client.parent` for the parent's identity and launch instructions.
1374
+ */
1375
+ type PlaycademyMode = 'platform' | 'demo' | 'standalone' | 'child';
1376
+ /**
1377
+ * What a parent game asks a child game to deliver.
1378
+ *
1379
+ * This contract is platform-defined so any parent can launch any child
1380
+ * without pair-specific vocabularies. `lessonId` addresses the child's own
1381
+ * catalog; the child maps it to internal content. Pair-specific extras
1382
+ * belong in `extensions`, which the platform never interprets (the same
1383
+ * split LTI makes between its resource link and custom claims).
1384
+ *
1385
+ * Parent and child ship independently, so children should still validate
1386
+ * the intent at runtime; SDK compatibility floors manage version skew.
1453
1387
  */
1454
- type PlaycademyMode = 'platform' | 'demo' | 'standalone';
1388
+ interface LaunchIntent {
1389
+ /** The activity to deliver, addressed in the child's own catalog. */
1390
+ lessonId: string;
1391
+ /** Pedagogy stage of the lesson (the platform's E1-E4 taxonomy). */
1392
+ eLevel: ELevel;
1393
+ /** Pair-specific extras. Never interpreted by the platform. */
1394
+ extensions?: Record<string, unknown>;
1395
+ }
1396
+ /**
1397
+ * What `client.parent` actually returns: the parent's wire context plus
1398
+ * the child's one capability toward it. The wire block
1399
+ * (`ParentGameContext`) stays a pure serializable payload; this handle
1400
+ * wraps it. Null outside child mode, so `if (client.parent)` remains the
1401
+ * child-launch test.
1402
+ */
1403
+ type ParentGameHandle = ParentGameContext & {
1404
+ /**
1405
+ * Streams an opaque checkpoint of this game's state to the parent,
1406
+ * so an interrupted launch can resume later (the parent hands it
1407
+ * back as `client.parent.resume`). Call it whenever your state
1408
+ * meaningfully changes — never wait for teardown, the closing-tab
1409
+ * message hop is unreliable. State must be JSON-serializable and
1410
+ * under 64KB; anything else is dropped with a warning. This is not
1411
+ * your save system: the parent holds it for resume only.
1412
+ */
1413
+ checkpoint(state: unknown): void;
1414
+ };
1415
+ /**
1416
+ * The parent game's block in a `mode: 'child'` INIT payload. Present only
1417
+ * when a parent game launched this client (see the parent-child game
1418
+ * embedding proposal, `docs/dev/timeback/`).
1419
+ */
1420
+ interface ParentGameContext {
1421
+ /** The parent game's own platform game ID (NOT this game's ID). */
1422
+ gameId: string;
1423
+ /** What the parent wants this child to deliver. */
1424
+ intent: LaunchIntent;
1425
+ /**
1426
+ * An earlier launch's checkpoint state, when the parent is resuming
1427
+ * an interrupted lesson. Opaque: this game wrote it via
1428
+ * `client.parent.checkpoint()`, and only this game can interpret it.
1429
+ * Validate it like the intent — a blob from an older build of this
1430
+ * game should be ignored, not trusted.
1431
+ */
1432
+ resume?: unknown;
1433
+ /**
1434
+ * The interrupted run id, present when the parent is resuming a
1435
+ * lesson. Consumed by the SDK: the launch's first `startActivity()`
1436
+ * adopts it automatically, which is what continues the platform run.
1437
+ * Games never read this; pass an explicit `runId` to
1438
+ * `startActivity()` to start a deliberate fresh attempt instead.
1439
+ */
1440
+ resumeRunId?: string;
1441
+ }
1455
1442
  interface ClientConfig {
1456
1443
  baseUrl: string;
1457
1444
  gameUrl?: string;
@@ -1480,6 +1467,8 @@ interface InitPayload {
1480
1467
  launchId?: string;
1481
1468
  /** When `true`, the parent shell provides a heartbeat relay via postMessage, so the SDK can skip its own `fetch({ keepalive })` beacon on pagehide. Defaults to `false`. */
1482
1469
  hasHeartbeatRelay?: boolean;
1470
+ /** Parent game context. Present only when `mode` is `'child'`. */
1471
+ parent?: ParentGameContext;
1483
1472
  }
1484
1473
  interface GameContextPayload extends InitPayload {
1485
1474
  forwardKeys?: string[];
@@ -1493,6 +1482,202 @@ interface ClientEvents {
1493
1482
  };
1494
1483
  }
1495
1484
 
1485
+ /**
1486
+ * Playcademy SDK client for game developers.
1487
+ * Provides namespaced access to platform features for games running inside Cademy.
1488
+ */
1489
+ declare class PlaycademyClient extends PlaycademyBaseClient {
1490
+ /**
1491
+ * Connect external identity providers to the user's Playcademy account.
1492
+ * - `connect(provider)` - Link Discord, Google, etc. via OAuth popup
1493
+ */
1494
+ identity: {
1495
+ connect: (options: AuthOptions) => Promise<AuthResult>;
1496
+ _getContext: () => {
1497
+ isInIframe: boolean;
1498
+ };
1499
+ };
1500
+ /**
1501
+ * Game runtime lifecycle and asset loading.
1502
+ * - `exit()` - Return to Cademy hub
1503
+ * - `getGameToken()` - Get short-lived auth token
1504
+ * - `assets.url()`, `assets.json()`, `assets.fetch()` - Load game assets
1505
+ * - `on('pause')`, `on('resume')` - Handle visibility changes
1506
+ */
1507
+ runtime: {
1508
+ exit: () => void;
1509
+ onInit: (handler: (context: GameContextPayload) => void) => void;
1510
+ onTokenRefresh: (handler: (data: {
1511
+ token: string;
1512
+ exp: number;
1513
+ }) => void) => void;
1514
+ onPause: (handler: () => void) => void;
1515
+ onResume: (handler: () => void) => void;
1516
+ onForceExit: (handler: () => void) => void;
1517
+ onOverlay: (handler: (isVisible: boolean) => void) => void;
1518
+ ready: () => void;
1519
+ sendTelemetry: (data: {
1520
+ fps: number;
1521
+ mem: number;
1522
+ }) => void;
1523
+ removeListener: (eventType: MessageEvents, handler: ((context: GameContextPayload) => void) | ((data: {
1524
+ token: string;
1525
+ exp: number;
1526
+ }) => void) | (() => void) | ((isVisible: boolean) => void)) => void;
1527
+ removeAllListeners: () => void;
1528
+ getListenerCounts: () => Record<string, number>;
1529
+ assets: {
1530
+ url(pathOrStrings: string | TemplateStringsArray, ...values: unknown[]): string;
1531
+ fetch: (path: string, options?: RequestInit) => Promise<Response>;
1532
+ json: <T = unknown>(path: string) => Promise<T>;
1533
+ blob: (path: string) => Promise<Blob>;
1534
+ text: (path: string) => Promise<string>;
1535
+ arrayBuffer: (path: string) => Promise<ArrayBuffer>;
1536
+ };
1537
+ };
1538
+ /**
1539
+ * TimeBack integration for activity tracking and user context.
1540
+ *
1541
+ * User context (cached from init, refreshable):
1542
+ * - `user.role` - User's role (student, parent, teacher, etc.)
1543
+ * - `user.enrollments` - Courses the player is enrolled in for this game
1544
+ * - `user.refresh({ only: ['enrollments'] })` - Refresh enrollments from server
1545
+ * - `user.organizations` - Schools/districts the player belongs to
1546
+ * - `user.fetch()` - Refresh user context from server
1547
+ *
1548
+ * Activity tracking:
1549
+ * - `currentRunId` - Current activity run ID, or undefined when inactive
1550
+ * - `startActivity(metadata)` - Begin tracking an activity, return its run
1551
+ * ID, and automatically handle hidden-tab and visible-tab inactivity
1552
+ * with configurable paused-heartbeat timeout behavior
1553
+ * - `pauseActivity()` / `resumeActivity()` - Pause/resume timer
1554
+ * - `endActivity(scoreData)` - Submit activity results to TimeBack
1555
+ */
1556
+ timeback: {
1557
+ assessments: {
1558
+ start: (input: _playcademy_types.StartAssessmentInput) => Promise<_playcademy_types.AssessmentAttemptSnapshot>;
1559
+ latest: (options: _playcademy_types.GetLatestAssessmentOptions) => Promise<_playcademy_types.LatestAssessmentResult | null>;
1560
+ get: (attemptId: string) => Promise<_playcademy_types.AssessmentAttemptSnapshot>;
1561
+ stop: (attemptId: string) => Promise<{
1562
+ attemptId: string;
1563
+ }>;
1564
+ save: (attemptId: string, input: _playcademy_types.SaveAssessmentInput) => Promise<_playcademy_types.AssessmentSaveResult>;
1565
+ submitItem: (attemptId: string, input: _playcademy_types.SubmitAssessmentItemInput) => Promise<_playcademy_types.SubmitAssessmentItemResult>;
1566
+ submit: (attemptId: string, input: _playcademy_types.SubmitAssessmentInput) => Promise<_playcademy_types.AssessmentSubmitResult>;
1567
+ };
1568
+ readonly user: TimebackUser;
1569
+ readonly currentRunId: string | undefined;
1570
+ startActivity: (metadata: _playcademy_types.ActivityData, options?: StartActivityOptions) => StartActivityResult;
1571
+ pauseActivity: () => void;
1572
+ resumeActivity: () => void;
1573
+ endActivity: (data: _playcademy_types.EndActivityScoreData) => Promise<_playcademy_types.EndActivityResponse | RelayedEndActivityResult>;
1574
+ course: {
1575
+ advance: (options?: {
1576
+ subject?: _playcademy_types.TimebackSubject;
1577
+ }) => Promise<_playcademy_types.AdvanceCourseResponse>;
1578
+ unenroll: (options?: {
1579
+ subject?: _playcademy_types.TimebackSubject;
1580
+ force?: boolean;
1581
+ }) => Promise<_playcademy_types.UnenrollCourseResponse>;
1582
+ };
1583
+ };
1584
+ /**
1585
+ * Game score submission and leaderboards.
1586
+ * - `submit(score, metadata?)` - Record a game score
1587
+ */
1588
+ scores: {
1589
+ submit: (score: number, metadata?: Record<string, unknown>) => Promise<ScoreSubmission>;
1590
+ };
1591
+ /**
1592
+ * Read-only leaderboard access for the current game scope.
1593
+ * - `fetch(options?)` - Fetch leaderboard entries
1594
+ */
1595
+ leaderboard: {
1596
+ fetch: (options?: _playcademy_types.LeaderboardOptions) => Promise<_playcademy_types.GameLeaderboardEntry[]>;
1597
+ };
1598
+ /**
1599
+ * Demo-mode helpers. Methods throw when called outside `client.mode === 'demo'`,
1600
+ * so callers should gate on the mode before reaching in.
1601
+ * - `profile.get()` - Read the anonymous demo player's profile
1602
+ * - `profile.update(updates)` - Update the demo player's profile (today: the required `displayName`)
1603
+ * - `end(score, options?)` - Signal to the parent shell that the demo has ended
1604
+ */
1605
+ demo: {
1606
+ profile: {
1607
+ get: () => Promise<_playcademy_types.DemoProfile>;
1608
+ update: (updates: _playcademy_types.DemoProfileUpdate) => Promise<_playcademy_types.DemoProfile>;
1609
+ };
1610
+ end: (score: number, options?: DemoEndOptions) => void;
1611
+ };
1612
+ /**
1613
+ * Make requests to your game's custom backend API routes.
1614
+ * - `get(path)`, `post(path, body)`, `put()`, `delete()` - HTTP methods
1615
+ * - Routes are relative to your game's deployment (e.g., '/hello' → your-game.playcademy.gg/api/hello)
1616
+ */
1617
+ backend: {
1618
+ get<T = unknown>(path: string, headers?: Record<string, string>): Promise<T>;
1619
+ post<T = unknown>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T>;
1620
+ put<T = unknown>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T>;
1621
+ patch<T = unknown>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T>;
1622
+ delete<T = unknown>(path: string, headers?: Record<string, string>): Promise<T>;
1623
+ request<T = unknown>(path: string, method: Method, body?: unknown, headers?: Record<string, string>): Promise<T>;
1624
+ download(path: string, method?: Method, body?: unknown, headers?: Record<string, string>): Promise<Response>;
1625
+ url(pathOrStrings: string | TemplateStringsArray, ...values: unknown[]): string;
1626
+ };
1627
+ /**
1628
+ * Launch other Playcademy games as embedded children (platform mode only).
1629
+ * - `launch({ slug, container, intent })` - Mount a child game in a nested
1630
+ * iframe with `mode: 'child'`; returns a session handle with `finished`
1631
+ * and `closed` promises and `close()`
1632
+ */
1633
+ embed: {
1634
+ launch(options: EmbedLaunchOptions): EmbedSession;
1635
+ };
1636
+ /** Auto-initializes a PlaycademyClient with context from the environment */
1637
+ static init: typeof init;
1638
+ /** Authenticates a user with email and password */
1639
+ static login: typeof login;
1640
+ /** Static identity utilities for OAuth operations */
1641
+ static identity: {
1642
+ parseOAuthState: typeof parseOAuthState;
1643
+ };
1644
+ }
1645
+ /**
1646
+ * A game client narrowed to a child launch (`mode: 'child'`).
1647
+ *
1648
+ * Produced by `isChildLaunched()`. Three members sharpen: `mode` becomes
1649
+ * the literal `'child'`, `parent` is non-null, and
1650
+ * `timeback.endActivity()` gains a relaxed call signature where
1651
+ * `xpAwarded` is optional and the result is always the relayed shape.
1652
+ * Built as an intersection so the class's private state stays assignable;
1653
+ * the relaxed signature joins the base one as an overload, which is why
1654
+ * an un-narrowed client still requires `xpAwarded`.
1655
+ */
1656
+ type ChildLaunchedClient = PlaycademyClient & {
1657
+ mode: 'child';
1658
+ parent: ParentGameHandle;
1659
+ timeback: {
1660
+ endActivity(data: ChildEndActivityScoreData): Promise<RelayedEndActivityResult>;
1661
+ };
1662
+ };
1663
+ /**
1664
+ * Narrows a client to `ChildLaunchedClient` when a parent game launched
1665
+ * it. TypeScript cannot condition a signature on a runtime mode, but it
1666
+ * can flow this narrowing: inside the guarded branch, `client.parent` is
1667
+ * non-null and `endActivity()` may omit `xpAwarded` (the parent game
1668
+ * decides the award).
1669
+ *
1670
+ * @example
1671
+ * ```typescript
1672
+ * if (isChildLaunched(client)) {
1673
+ * const { lessonId, eLevel } = client.parent.intent
1674
+ * // ... play the lesson ...
1675
+ * await client.timeback.endActivity({ correctQuestions, totalQuestions })
1676
+ * }
1677
+ * ```
1678
+ */
1679
+ declare function isChildLaunched(client: PlaycademyClient): client is ChildLaunchedClient;
1680
+
1496
1681
  /**
1497
1682
  * Event and message payload types for SDK messaging system
1498
1683
  */
@@ -1583,6 +1768,42 @@ interface DemoEndPayload extends DemoEndOptions {
1583
1768
  }
1584
1769
  type TimebackHeartbeatRelayRequest = Omit<HeartbeatRequest, 'gameId' | 'studentId' | 'windowStartedAtMs' | 'windowSequence'> & {
1585
1770
  windowStartedAtMs: number;
1771
+ /**
1772
+ * Marks a closed heartbeat window from the child's 15s accounting
1773
+ * cadence: the window's totals are final, its key never recurs, and
1774
+ * the parent forwards it exactly once (retries are safe against the
1775
+ * server's first-write-wins window dedupe). Absent on the 1s display
1776
+ * snapshots of the still-open window.
1777
+ */
1778
+ windowClosed?: boolean;
1779
+ };
1780
+ /**
1781
+ * Wire payload for `PLAYCADEMY_CHECKPOINT`. An opaque snapshot of the
1782
+ * child game's internal state; the SDK and the parent never interpret
1783
+ * `state`. The parent stamps the resume envelope's `childRunId` from the
1784
+ * activity-start announcement, so the checkpoint itself carries no ids.
1785
+ */
1786
+ interface ChildCheckpointRelay {
1787
+ state: unknown;
1788
+ }
1789
+ /**
1790
+ * Wire payload for `PLAYCADEMY_TIMEBACK_ACTIVITY_START`. A child-mode game
1791
+ * announces that its tracker opened a run, so the parent can open its own
1792
+ * Timeback run at the moment the lesson actually begins.
1793
+ */
1794
+ type TimebackActivityStartRelay = Pick<TimebackHeartbeatRelayRequest, 'runId' | 'resumeId' | 'activityData'>;
1795
+ /**
1796
+ * Wire payload for `PLAYCADEMY_TIMEBACK_ACTIVITY_END`. The same end-activity
1797
+ * body a platform-mode game would POST to its backend, relayed to the parent
1798
+ * instead. `timingData.durationSeconds` is the full active sitting and
1799
+ * `sessionTimingData` carries the FULL session totals: a relayed window is
1800
+ * never marked persisted (a postMessage hand-off proves nothing about the
1801
+ * parent's POST), so the parent reconciles these totals against the windows
1802
+ * the server confirmed before reporting the completion's remainder.
1803
+ */
1804
+ type TimebackActivityEndRelay = Omit<EndActivityRequest, 'gameId' | 'studentId' | 'xpEarned'> & {
1805
+ /** The child's XP suggestion; the parent decides the actual award. */
1806
+ xpEarned?: number;
1586
1807
  };
1587
1808
 
1588
1809
  /**
@@ -1592,6 +1813,265 @@ interface LoginResponse {
1592
1813
  token: string;
1593
1814
  }
1594
1815
 
1816
+ /**
1817
+ * Public types for the launch protocol's parent side: the embedded
1818
+ * child-game session behind `client.embed.launch()`. Only what `launch()`
1819
+ * callers touch lives here; the implementation and its constructor-side
1820
+ * plumbing contracts live in `core/launch/session.ts`.
1821
+ */
1822
+
1823
+ /**
1824
+ * How a child launch is recorded on this game's Timeback course.
1825
+ * The same metadata you would give `startActivity()` if your own document
1826
+ * were running the lesson: the embed session runs the whole
1827
+ * start-through-end lifecycle for you, stamped with this.
1828
+ */
1829
+ interface EmbedTimebackRecording {
1830
+ /** The activity on this game's own course the launch is recorded as. */
1831
+ activityId: string;
1832
+ /** Display name for dashboards; prettified from `activityId` when omitted. */
1833
+ activityName?: string;
1834
+ /** With `subject`, routes the recording to one of this game's courses. */
1835
+ grade: TimebackGrade;
1836
+ /** With `grade`, routes the recording to one of this game's courses. */
1837
+ subject: TimebackSubject;
1838
+ /** Course id hint, same semantics as `startActivity()`. */
1839
+ courseId?: string;
1840
+ }
1841
+ /**
1842
+ * Everything a parent needs to resume an interrupted launch later. The
1843
+ * parent persists this wherever the interruption demands (memory for
1844
+ * exit-and-return, localStorage for tab close, its backend KV for
1845
+ * cross-device) and hands it back via `embed.launch({ resume })`.
1846
+ */
1847
+ interface EmbedResumeEnvelope {
1848
+ /**
1849
+ * The child's opaque checkpoint state, exactly as it last reported
1850
+ * it. Never introspect it: only the child can interpret its own
1851
+ * state, and it validates the blob on the way back in.
1852
+ */
1853
+ state: unknown;
1854
+ /**
1855
+ * The child's own run id from the interrupted launch, when a run was
1856
+ * active. On resume it crosses to the child, whose SDK re-announces
1857
+ * it automatically at the next launch's first start; the announced match
1858
+ * is what makes reusing `parentRunId` safe.
1859
+ */
1860
+ childRunId?: string;
1861
+ /**
1862
+ * The interrupted platform run, when Timeback reporting was active.
1863
+ * Reused (with a fresh sitting id) only when the child accepts the
1864
+ * resume; otherwise a fresh run is minted.
1865
+ */
1866
+ parentRunId?: string;
1867
+ }
1868
+ /**
1869
+ * Custom persistence for resume envelopes, passed as `launch()`'s
1870
+ * `resume` option. The default (when `resume` is omitted) is a built-in
1871
+ * localStorage store keyed by user, parent game, child game, and the
1872
+ * intent's lesson identity; supply your own store to keep envelopes
1873
+ * elsewhere (for example your backend, for cross-device resume).
1874
+ */
1875
+ interface EmbedResumeStore {
1876
+ /**
1877
+ * Returns the stored envelope for this lesson identity, or
1878
+ * null/undefined when there is nothing to resume. May be async; the
1879
+ * boot waits for it before the child's INIT is sent.
1880
+ */
1881
+ load(): EmbedResumeEnvelope | null | undefined | Promise<EmbedResumeEnvelope | null | undefined>;
1882
+ /**
1883
+ * Persists the latest envelope. Called on every checkpoint the child
1884
+ * relays (envelopes are capped at 64KB) and again when run identity
1885
+ * is minted. Writes should be synchronous or fire-and-forget: the
1886
+ * SDK never blocks on them, and a throw costs that envelope's
1887
+ * persistence, never the launch. Returned promises are used only for
1888
+ * ordering: `save` and `clear` run strictly in call order, so a slow
1889
+ * async save cannot land after the completion's clear.
1890
+ */
1891
+ save(envelope: EmbedResumeEnvelope): void;
1892
+ /** Deletes the stored envelope. Called once when the launch completes. */
1893
+ clear(): void;
1894
+ }
1895
+ /**
1896
+ * Play-time totals for a child launch, measured by the child's own
1897
+ * tracker (the parent's document is idle while the student plays).
1898
+ */
1899
+ interface EmbedSessionTiming {
1900
+ /** Seconds of active play. */
1901
+ activeSeconds: number;
1902
+ /** Seconds the child's tracker classified as paused or inactive, when known. */
1903
+ inactiveSeconds?: number;
1904
+ }
1905
+ /**
1906
+ * The launch's activity record, resolved by `session.finished`.
1907
+ *
1908
+ * `'completed'` carries the child's report and the `end()` capability;
1909
+ * `'abandoned'` means the session ended first (child exit or `close()`);
1910
+ * `'failed'` means the launch never happened. Failure is a state to
1911
+ * render, not an exception to catch — `finished` never rejects.
1912
+ */
1913
+ type EmbedActivity = EmbedActivityCompleted | EmbedActivityAbandoned | EmbedActivityFailed;
1914
+ /** The child called `endActivity()` and its report was relayed. */
1915
+ interface EmbedActivityCompleted {
1916
+ status: 'completed';
1917
+ /**
1918
+ * The platform run this completion records under. One run ends in at
1919
+ * most one completion (the server dedupes on it), and a resumed launch
1920
+ * keeps the interrupted run's id — so this doubles as the completion's
1921
+ * attempt identity: feed it to whatever consumes the result and drop
1922
+ * anything you have seen before. Absent only for pure UX embeds
1923
+ * (launched without `timeback`), which record nothing.
1924
+ */
1925
+ runId?: string;
1926
+ /** Correct answers, from the child's report. */
1927
+ correct: number;
1928
+ /** Total questions, from the child's report. */
1929
+ total: number;
1930
+ timing: EmbedSessionTiming;
1931
+ /**
1932
+ * The child's full relayed end-activity body: its own activity ids,
1933
+ * suggested XP, and extensions. Audit data — the parent decides what
1934
+ * actually reaches the platform, via `end()`.
1935
+ */
1936
+ childReport: TimebackActivityEndRelay;
1937
+ /**
1938
+ * Posts the launch's completion to the parent's own backend: the
1939
+ * parent-minted run id, the `timeback` recording, the caller's score
1940
+ * and XP decision, and the child's ids as audit extensions. Requires
1941
+ * the `timeback` option at launch. Calling twice returns the same
1942
+ * promise, so a launch can never double-report from the client.
1943
+ */
1944
+ end(scores: EndActivityScoreData): Promise<EndActivityResponse>;
1945
+ }
1946
+ /**
1947
+ * The session ended (child exit or `close()`) before a report arrived.
1948
+ * Played time has already reached the platform through forwarded
1949
+ * heartbeats; an abandoned launch leaves no completion, exactly like a
1950
+ * student wandering away from any other game.
1951
+ */
1952
+ interface EmbedActivityAbandoned {
1953
+ status: 'abandoned';
1954
+ /**
1955
+ * The platform run the launch was recording under, when one had
1956
+ * opened. Absent when the child never started an activity or the
1957
+ * launch was a pure UX embed.
1958
+ */
1959
+ runId?: string;
1960
+ timing: EmbedSessionTiming;
1961
+ /**
1962
+ * The final resume envelope, when the child checkpointed during the
1963
+ * launch. Persist it (see `EmbedResumeEnvelope`) and pass it back to
1964
+ * `embed.launch({ resume })` to pick the lesson up later. Absent
1965
+ * when the child never checkpointed.
1966
+ */
1967
+ resume?: EmbedResumeEnvelope;
1968
+ }
1969
+ /** The launch never happened: the child could not be resolved or booted. */
1970
+ interface EmbedActivityFailed {
1971
+ status: 'failed';
1972
+ /** Why — unknown slug, missing deployment URL, INIT error or timeout. */
1973
+ error: PlaycademyError;
1974
+ }
1975
+ /**
1976
+ * Handle for one embedded child-game session.
1977
+ */
1978
+ interface EmbedSession {
1979
+ /** The mounted child iframe. Useful for focus management. */
1980
+ readonly iframe: HTMLIFrameElement;
1981
+ /**
1982
+ * The platform run this launch records under, or null before the run
1983
+ * opens (the child's first activity) and for pure UX embeds. Stable
1984
+ * once set; also echoed on the finished record, which is where most
1985
+ * callers should read it.
1986
+ */
1987
+ readonly runId: string | null;
1988
+ /**
1989
+ * The latest resume envelope, live during play; null until the child
1990
+ * first checkpoints. Read it on your own cadence to persist
1991
+ * mid-lesson (for example a debounced upload to your backend), so a
1992
+ * closed tab can resume on another device.
1993
+ */
1994
+ readonly checkpoint: EmbedResumeEnvelope | null;
1995
+ /**
1996
+ * Resolves the launch's activity record when it ends — the
1997
+ * `animation.finished` idiom. Never rejects: operational failures
1998
+ * resolve as `{ status: 'failed', error }`.
1999
+ */
2000
+ readonly finished: Promise<EmbedActivity>;
2001
+ /**
2002
+ * Resolves when the session is torn down and the iframe is unmounted:
2003
+ * on child exit, `close()`, boot failure, or when the SDK detects the
2004
+ * iframe was removed from the DOM. `finished` can resolve earlier than
2005
+ * this (a completed child usually shows a results screen before
2006
+ * exiting), so use `closed` to dismiss surrounding UI.
2007
+ */
2008
+ readonly closed: Promise<void>;
2009
+ /**
2010
+ * Tears the session down: unmounts the iframe and stops all listeners.
2011
+ * Resolves a still-pending `finished` as `'abandoned'`. Safe to call
2012
+ * more than once. Calling it is the deterministic path; the SDK also
2013
+ * tears down when the child exits, and detects an iframe removed
2014
+ * without `close()` within ~5 seconds, so nothing leaks either way.
2015
+ */
2016
+ close(): void;
2017
+ }
2018
+
2019
+ /**
2020
+ * Options for `client.embed.launch()`.
2021
+ */
2022
+ interface EmbedLaunchOptions {
2023
+ /** Slug of the child game to launch. Resolved to a game id at launch time. */
2024
+ slug: string;
2025
+ /** Element the child iframe is mounted into. The iframe fills it. */
2026
+ container: HTMLElement;
2027
+ /**
2028
+ * What the child should deliver. This is the platform-defined
2029
+ * {@link LaunchIntent} contract; it crosses the iframe in the INIT
2030
+ * payload's `parent` block and surfaces in the child as `client.parent.intent`.
2031
+ */
2032
+ intent: LaunchIntent;
2033
+ /**
2034
+ * Whether and how an interrupted launch can resume.
2035
+ *
2036
+ * Omitted (the default): the SDK persists the child's latest
2037
+ * checkpoint in localStorage, keyed by user, this game, the child,
2038
+ * and the intent's lesson identity. The next launch with the same
2039
+ * identity resumes automatically; completion clears the entry. Inert
2040
+ * for children that never call `client.parent.checkpoint()`.
2041
+ *
2042
+ * `false`: no persistence and no automatic resume. The manual surface
2043
+ * (`session.checkpoint`, the abandoned outcome's `resume`) still works.
2044
+ *
2045
+ * An {@link EmbedResumeEnvelope}: fully manual, one-shot. The launch
2046
+ * resumes from exactly this envelope and nothing is persisted.
2047
+ *
2048
+ * An {@link EmbedResumeStore}: delegate persistence (for example to
2049
+ * your backend, for cross-device resume).
2050
+ *
2051
+ * Whatever the policy, the child receives only the opaque `state`
2052
+ * (as `client.parent.resume`) and decides whether to use it; run ids
2053
+ * stay parent-side and drive run continuity when the child accepts.
2054
+ */
2055
+ resume?: false | EmbedResumeEnvelope | EmbedResumeStore;
2056
+ /**
2057
+ * Records the launch on this game's Timeback course: the metadata
2058
+ * you would have given `startActivity()` if your own document were
2059
+ * running the lesson. Stays in the parent SDK, stamping every
2060
+ * forwarded heartbeat and the final completion; it never crosses the
2061
+ * iframe. Omit for a pure UX embed.
2062
+ */
2063
+ timeback?: EmbedTimebackRecording;
2064
+ /**
2065
+ * Overrides the child's resolved deployment URL. Intended for local
2066
+ * development, where the child runs on a dev server the platform
2067
+ * doesn't know about. With `gameUrl` set, a slug that fails to
2068
+ * resolve degrades to a warning (the slug stands in as the child's
2069
+ * game id) instead of failing the launch, so an unregistered child
2070
+ * still launches locally.
2071
+ */
2072
+ gameUrl?: string;
2073
+ }
2074
+
1595
2075
  /**
1596
2076
  * Scores namespace types
1597
2077
  */
@@ -1672,5 +2152,5 @@ interface DevUploadHooks {
1672
2152
  onClose?: () => void;
1673
2153
  }
1674
2154
 
1675
- export { ApiError, MessageEvents, PlaycademyClient, PlaycademyError, extractApiErrorInfo, messaging };
1676
- export type { ApiErrorCode, ApiErrorInfo, DevUploadEvent, DevUploadHooks, ErrorResponseBody, PlaycademyMode };
2155
+ export { ApiError, MessageEvents, PlaycademyClient, PlaycademyError, extractApiErrorInfo, isChildLaunched, messaging };
2156
+ export type { ApiErrorCode, ApiErrorInfo, ChildLaunchedClient, DevUploadEvent, DevUploadHooks, ErrorResponseBody, PlaycademyMode };