@playcademy/sdk 0.16.1-beta.1 → 0.16.1-beta.11

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
@@ -1,9 +1,10 @@
1
1
  import * as _playcademy_types from '@playcademy/types';
2
2
  import { GameManifest, LocalDayContext } from '@playcademy/types';
3
3
  export { AuthenticatedUser, DeveloperStatusEnumType, DeveloperStatusResponse, DeveloperStatusValue, GameCourseMetrics, GameLeaderboardEntry, GameManifest, GameMetricComparisonKind, GameMetricComparisonMetric, GameMetricComparisonRow, GameMetricComparisonRowStatus, GameMetricsProxyResponse, GameMetricsResponse, GameMetricsUnsupportedReason, GamePlatform, GameRunMetrics, GameRunMetricsComparison, GameRunMetricsComparisonStatus, GameRunMetricsComparisonSummary, GameTimebackIntegration, GameType, GameUser, LeaderboardEntry, LeaderboardOptions, LeaderboardTimeframe, LocalDayContext, LocalDaySource, ManifestV1, ManifestV2, ManifestVersions, PopulateStudentResponse, UserEnrollment, UserInfo, UserOrganization, UserRank, UserRankResponse, UserRoleEnumType, UserScore, UserTimebackData } from '@playcademy/types';
4
- import { TimebackCourseConfig, CourseConfig, OrganizationConfig, ComponentConfig, ResourceConfig, ComponentResourceConfig, TimebackGrade, TimebackSubject, HeartbeatRequest } from '@playcademy/types/timeback';
4
+ import { TimebackCourseConfig, CourseConfig, OrganizationConfig, ComponentConfig, ResourceConfig, ComponentResourceConfig, TimebackGrade, TimebackSubject, ELevel, EndActivityRequest, HeartbeatRequest, EndActivityScoreData, EndActivityResponse } from '@playcademy/types/timeback';
5
+ export { AssessmentAttemptSnapshot, AssessmentFlow, AssessmentItemSubmission, AssessmentResponseUpdate, AssessmentResponseValue, AssessmentResponses, AssessmentSaveResult, AssessmentScore, AssessmentStandardRef, AssessmentSubmitResult, ELevel, PlayableAssessment, PlayableAssessmentChoice, PlayableAssessmentGraphic, PlayableAssessmentHotspot, PlayableAssessmentInteraction, PlayableAssessmentItem, PlayableContentNode, SaveAssessmentInput, StartAssessmentInput, SubmitAssessmentInput, SubmitAssessmentItemInput, SubmitAssessmentItemResult } from '@playcademy/types/timeback';
5
6
  import { TimebackUserRole, UserEnrollment, UserOrganization, UserInfo } from '@playcademy/types/user';
6
- import { AUTH_PROVIDER_IDS } from '@playcademy/constants';
7
+ import { GamePermission, AUTH_PROVIDER_IDS } from '@playcademy/constants';
7
8
  import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
8
9
  import { DomainValidationRecords } from '@playcademy/types/game';
9
10
  import { z } from 'zod';
@@ -30,6 +31,13 @@ interface RetryPolicy {
30
31
  retryDelaysMs?: readonly number[];
31
32
  }
32
33
 
34
+ /**
35
+ * Base error class for Cademy SDK specific errors.
36
+ */
37
+ declare class PlaycademyError extends Error {
38
+ constructor(message: string);
39
+ }
40
+
33
41
  /**
34
42
  * @fileoverview Playcademy Messaging System
35
43
  *
@@ -158,11 +166,32 @@ declare enum MessageEvents {
158
166
  */
159
167
  DEMO_END = "PLAYCADEMY_DEMO_END",
160
168
  /**
161
- * Game shares its latest TimeBack heartbeat window with the parent shell.
162
- * The shell can relay this payload during top-level page teardown, which is
163
- * more reliable than relying only on cross-origin iframe unload events.
169
+ * Game shares its latest TimeBack heartbeat window with the embedding
170
+ * window. The hub relays this payload during top-level page teardown
171
+ * (more reliable than cross-origin iframe unload events); in child mode
172
+ * this stream is the heartbeat's only delivery path.
164
173
  */
165
174
  TIMEBACK_HEARTBEAT_RELAY = "PLAYCADEMY_TIMEBACK_HEARTBEAT_RELAY",
175
+ /**
176
+ * Game announces its tracker opened a run (startActivity).
177
+ * Today only child-mode games emit this: the parent opens its own
178
+ * Timeback run on receipt, so tracked time starts when the lesson
179
+ * does, not when the iframe boots.
180
+ */
181
+ TIMEBACK_ACTIVITY_START = "PLAYCADEMY_TIMEBACK_ACTIVITY_START",
182
+ /**
183
+ * Game relays its full end-activity report instead of POSTing it to
184
+ * its own backend. Today only child-mode games emit this: the parent
185
+ * reports the result to Timeback under its own identity.
186
+ */
187
+ TIMEBACK_ACTIVITY_END = "PLAYCADEMY_TIMEBACK_ACTIVITY_END",
188
+ /**
189
+ * Child game → parent game. An opaque checkpoint of the child's
190
+ * internal state, streamed during play so the parent always holds a
191
+ * near-current copy (the death-time postMessage hop is unreliable;
192
+ * continuous checkpointing is the design). Latest wins.
193
+ */
194
+ CHECKPOINT = "PLAYCADEMY_CHECKPOINT",
166
195
  /**
167
196
  * Notifies about authentication state changes.
168
197
  * Can be sent in both directions depending on auth flow.
@@ -345,6 +374,13 @@ interface PlaycademyConfig {
345
374
  description?: string;
346
375
  /** Game emoji icon */
347
376
  emoji?: string;
377
+ /**
378
+ * Browser permissions to request for the game's iframe (e.g.
379
+ * `['microphone']`). Opt-in per game; only `microphone` and `camera` are
380
+ * delegated this way. Fullscreen, autoplay, and gamepad are granted to
381
+ * every game automatically.
382
+ */
383
+ permissions?: GamePermission[];
348
384
  /** Build command to run before deployment */
349
385
  buildCommand?: string[];
350
386
  /** Path to build output */
@@ -679,6 +715,8 @@ declare const users: drizzle_orm_pg_core.PgTableWithColumns<{
679
715
  interface GameMetadata {
680
716
  description?: string;
681
717
  emoji?: string;
718
+ /** Browser permissions delegated to the game's iframe (opt-in per game). */
719
+ permissions?: GamePermission[];
682
720
  [key: string]: unknown;
683
721
  }
684
722
  /**
@@ -1228,6 +1266,8 @@ declare abstract class PlaycademyBaseClient {
1228
1266
  isInIframe: boolean;
1229
1267
  };
1230
1268
  protected initPayload?: InitPayload;
1269
+ /** Memoized `client.parent` handle; built on first access in child mode. */
1270
+ private parentHandle?;
1231
1271
  protected launchId?: string;
1232
1272
  protected gameOrigin?: string;
1233
1273
  private browserTimeZone?;
@@ -1248,6 +1288,13 @@ declare abstract class PlaycademyBaseClient {
1248
1288
  * Local day context supplied by the platform during iframe initialization.
1249
1289
  */
1250
1290
  get localDay(): LocalDayContext | undefined;
1291
+ /**
1292
+ * Null outside `mode: 'child'` even if a payload smuggled in a `parent`
1293
+ * block, so `if (client.parent)` is a reliable child-mode test. The
1294
+ * returned handle is the wire block plus `checkpoint()`, memoized so
1295
+ * `client.parent === client.parent`.
1296
+ */
1297
+ get parent(): ParentGameHandle | null;
1251
1298
  /**
1252
1299
  * Sets the authentication token for API requests.
1253
1300
  */
@@ -1394,153 +1441,14 @@ declare function init<T extends PlaycademyBaseClient = PlaycademyBaseClient>(thi
1394
1441
  declare function login(baseUrl: string, email: string, password: string): Promise<LoginResponse>;
1395
1442
 
1396
1443
  /**
1397
- * Playcademy SDK client for game developers.
1398
- * Provides namespaced access to platform features for games running inside Cademy.
1444
+ * `endActivity()`'s resolution in child mode: the report was relayed to the
1445
+ * parent game rather than POSTed to the platform, so no platform award data
1446
+ * (courseId, xpAwarded) exists. The parent reports under its own identity.
1399
1447
  */
1400
- declare class PlaycademyClient extends PlaycademyBaseClient {
1401
- /**
1402
- * Connect external identity providers to the user's Playcademy account.
1403
- * - `connect(provider)` - Link Discord, Google, etc. via OAuth popup
1404
- */
1405
- identity: {
1406
- connect: (options: AuthOptions) => Promise<AuthResult>;
1407
- _getContext: () => {
1408
- isInIframe: boolean;
1409
- };
1410
- };
1411
- /**
1412
- * Game runtime lifecycle and asset loading.
1413
- * - `exit()` - Return to Cademy hub
1414
- * - `getGameToken()` - Get short-lived auth token
1415
- * - `assets.url()`, `assets.json()`, `assets.fetch()` - Load game assets
1416
- * - `on('pause')`, `on('resume')` - Handle visibility changes
1417
- */
1418
- runtime: {
1419
- exit: () => void;
1420
- onInit: (handler: (context: GameContextPayload) => void) => void;
1421
- onTokenRefresh: (handler: (data: {
1422
- token: string;
1423
- exp: number;
1424
- }) => void) => void;
1425
- onPause: (handler: () => void) => void;
1426
- onResume: (handler: () => void) => void;
1427
- onForceExit: (handler: () => void) => void;
1428
- onOverlay: (handler: (isVisible: boolean) => void) => void;
1429
- ready: () => void;
1430
- sendTelemetry: (data: {
1431
- fps: number;
1432
- mem: number;
1433
- }) => void;
1434
- removeListener: (eventType: MessageEvents, handler: ((context: GameContextPayload) => void) | ((data: {
1435
- token: string;
1436
- exp: number;
1437
- }) => void) | (() => void) | ((isVisible: boolean) => void)) => void;
1438
- removeAllListeners: () => void;
1439
- getListenerCounts: () => Record<string, number>;
1440
- assets: {
1441
- url(pathOrStrings: string | TemplateStringsArray, ...values: unknown[]): string;
1442
- fetch: (path: string, options?: RequestInit) => Promise<Response>;
1443
- json: <T = unknown>(path: string) => Promise<T>;
1444
- blob: (path: string) => Promise<Blob>;
1445
- text: (path: string) => Promise<string>;
1446
- arrayBuffer: (path: string) => Promise<ArrayBuffer>;
1447
- };
1448
- };
1449
- /**
1450
- * TimeBack integration for activity tracking and user context.
1451
- *
1452
- * User context (cached from init, refreshable):
1453
- * - `user.role` - User's role (student, parent, teacher, etc.)
1454
- * - `user.enrollments` - Courses the player is enrolled in for this game
1455
- * - `user.refresh({ only: ['enrollments'] })` - Refresh enrollments from server
1456
- * - `user.organizations` - Schools/districts the player belongs to
1457
- * - `user.fetch()` - Refresh user context from server
1458
- *
1459
- * Activity tracking:
1460
- * - `currentRunId` - Current activity run ID, or undefined when inactive
1461
- * - `startActivity(metadata)` - Begin tracking an activity, return its run
1462
- * ID, and automatically handle hidden-tab and visible-tab inactivity
1463
- * with configurable paused-heartbeat timeout behavior
1464
- * - `pauseActivity()` / `resumeActivity()` - Pause/resume timer
1465
- * - `endActivity(scoreData)` - Submit activity results to TimeBack
1466
- */
1467
- timeback: {
1468
- assessments: {
1469
- start: (input: _playcademy_types.StartAssessmentInput) => Promise<_playcademy_types.AssessmentAttemptSnapshot>;
1470
- latest: (options: _playcademy_types.GetLatestAssessmentOptions) => Promise<_playcademy_types.LatestAssessmentResult | null>;
1471
- get: (attemptId: string) => Promise<_playcademy_types.AssessmentAttemptSnapshot>;
1472
- save: (attemptId: string, input: _playcademy_types.SaveAssessmentInput) => Promise<_playcademy_types.AssessmentSaveResult>;
1473
- submit: (attemptId: string, input: _playcademy_types.SubmitAssessmentInput) => Promise<_playcademy_types.AssessmentSubmitResult>;
1474
- };
1475
- readonly user: TimebackUser;
1476
- readonly currentRunId: string | undefined;
1477
- startActivity: (metadata: _playcademy_types.ActivityData, options?: StartActivityOptions) => StartActivityResult;
1478
- pauseActivity: () => void;
1479
- resumeActivity: () => void;
1480
- endActivity: (data: _playcademy_types.EndActivityScoreData) => Promise<_playcademy_types.EndActivityResponse>;
1481
- course: {
1482
- advance: (options?: {
1483
- subject?: _playcademy_types.TimebackSubject;
1484
- }) => Promise<_playcademy_types.AdvanceCourseResponse>;
1485
- unenroll: (options?: {
1486
- subject?: _playcademy_types.TimebackSubject;
1487
- force?: boolean;
1488
- }) => Promise<_playcademy_types.UnenrollCourseResponse>;
1489
- };
1490
- };
1491
- /**
1492
- * Game score submission and leaderboards.
1493
- * - `submit(score, metadata?)` - Record a game score
1494
- */
1495
- scores: {
1496
- submit: (score: number, metadata?: Record<string, unknown>) => Promise<ScoreSubmission>;
1497
- };
1498
- /**
1499
- * Read-only leaderboard access for the current game scope.
1500
- * - `fetch(options?)` - Fetch leaderboard entries
1501
- */
1502
- leaderboard: {
1503
- fetch: (options?: _playcademy_types.LeaderboardOptions) => Promise<_playcademy_types.GameLeaderboardEntry[]>;
1504
- };
1505
- /**
1506
- * Demo-mode helpers. Methods throw when called outside `client.mode === 'demo'`,
1507
- * so callers should gate on the mode before reaching in.
1508
- * - `profile.get()` - Read the anonymous demo player's profile
1509
- * - `profile.update(updates)` - Update the demo player's profile (today: the required `displayName`)
1510
- * - `end(score, options?)` - Signal to the parent shell that the demo has ended
1511
- */
1512
- demo: {
1513
- profile: {
1514
- get: () => Promise<_playcademy_types.DemoProfile>;
1515
- update: (updates: _playcademy_types.DemoProfileUpdate) => Promise<_playcademy_types.DemoProfile>;
1516
- };
1517
- end: (score: number, options?: DemoEndOptions) => void;
1518
- };
1519
- /**
1520
- * Make requests to your game's custom backend API routes.
1521
- * - `get(path)`, `post(path, body)`, `put()`, `delete()` - HTTP methods
1522
- * - Routes are relative to your game's deployment (e.g., '/hello' → your-game.playcademy.gg/api/hello)
1523
- */
1524
- backend: {
1525
- get<T = unknown>(path: string, headers?: Record<string, string>): Promise<T>;
1526
- post<T = unknown>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T>;
1527
- put<T = unknown>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T>;
1528
- patch<T = unknown>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T>;
1529
- delete<T = unknown>(path: string, headers?: Record<string, string>): Promise<T>;
1530
- request<T = unknown>(path: string, method: Method, body?: unknown, headers?: Record<string, string>): Promise<T>;
1531
- download(path: string, method?: Method, body?: unknown, headers?: Record<string, string>): Promise<Response>;
1532
- url(pathOrStrings: string | TemplateStringsArray, ...values: unknown[]): string;
1533
- };
1534
- /** Auto-initializes a PlaycademyClient with context from the environment */
1535
- static init: typeof init;
1536
- /** Authenticates a user with email and password */
1537
- static login: typeof login;
1538
- /** Static identity utilities for OAuth operations */
1539
- static identity: {
1540
- parseOAuthState: typeof parseOAuthState;
1541
- };
1448
+ interface RelayedEndActivityResult {
1449
+ status: 'relayed';
1450
+ runId: string;
1542
1451
  }
1543
-
1544
1452
  /**
1545
1453
  * Options for configuring activity tracking behavior.
1546
1454
  */
@@ -1878,8 +1786,79 @@ type TokenType = 'session' | 'apiKey' | 'gameJwt';
1878
1786
  * - `'standalone'` — game is running outside any iframe (e.g. `bun run dev`
1879
1787
  * or direct-deploy preview) with a mock token and no real platform
1880
1788
  * context. API calls will not succeed; use this to branch UX locally.
1789
+ * - `'child'` — game is embedded by another game (the parent), which ran
1790
+ * the INIT handshake itself and included a `parent` block in the payload.
1791
+ * The user and token are real (the parent's own). Direct Timeback
1792
+ * reporting is suppressed and relayed to the parent instead; read
1793
+ * `client.parent` for the parent's identity and launch instructions.
1794
+ */
1795
+ type PlaycademyMode = 'platform' | 'demo' | 'standalone' | 'child';
1796
+ /**
1797
+ * What a parent game asks a child game to deliver.
1798
+ *
1799
+ * This contract is platform-defined so any parent can launch any child
1800
+ * without pair-specific vocabularies. `lessonId` addresses the child's own
1801
+ * catalog; the child maps it to internal content. Pair-specific extras
1802
+ * belong in `extensions`, which the platform never interprets (the same
1803
+ * split LTI makes between its resource link and custom claims).
1804
+ *
1805
+ * Parent and child ship independently, so children should still validate
1806
+ * the intent at runtime; SDK compatibility floors manage version skew.
1807
+ */
1808
+ interface LaunchIntent {
1809
+ /** The activity to deliver, addressed in the child's own catalog. */
1810
+ lessonId: string;
1811
+ /** Pedagogy stage of the lesson (the platform's E1-E4 taxonomy). */
1812
+ eLevel: ELevel;
1813
+ /** Pair-specific extras. Never interpreted by the platform. */
1814
+ extensions?: Record<string, unknown>;
1815
+ }
1816
+ /**
1817
+ * What `client.parent` actually returns: the parent's wire context plus
1818
+ * the child's one capability toward it. The wire block
1819
+ * (`ParentGameContext`) stays a pure serializable payload; this handle
1820
+ * wraps it. Null outside child mode, so `if (client.parent)` remains the
1821
+ * child-launch test.
1822
+ */
1823
+ type ParentGameHandle = ParentGameContext & {
1824
+ /**
1825
+ * Streams an opaque checkpoint of this game's state to the parent,
1826
+ * so an interrupted launch can resume later (the parent hands it
1827
+ * back as `client.parent.resume`). Call it whenever your state
1828
+ * meaningfully changes — never wait for teardown, the closing-tab
1829
+ * message hop is unreliable. State must be JSON-serializable and
1830
+ * under 64KB; anything else is dropped with a warning. This is not
1831
+ * your save system: the parent holds it for resume only.
1832
+ */
1833
+ checkpoint(state: unknown): void;
1834
+ };
1835
+ /**
1836
+ * The parent game's block in a `mode: 'child'` INIT payload. Present only
1837
+ * when a parent game launched this client (see the parent-child game
1838
+ * embedding proposal, `docs/dev/timeback/`).
1881
1839
  */
1882
- type PlaycademyMode = 'platform' | 'demo' | 'standalone';
1840
+ interface ParentGameContext {
1841
+ /** The parent game's own platform game ID (NOT this game's ID). */
1842
+ gameId: string;
1843
+ /** What the parent wants this child to deliver. */
1844
+ intent: LaunchIntent;
1845
+ /**
1846
+ * An earlier launch's checkpoint state, when the parent is resuming
1847
+ * an interrupted lesson. Opaque: this game wrote it via
1848
+ * `client.parent.checkpoint()`, and only this game can interpret it.
1849
+ * Validate it like the intent — a blob from an older build of this
1850
+ * game should be ignored, not trusted.
1851
+ */
1852
+ resume?: unknown;
1853
+ /**
1854
+ * The interrupted run id, present when the parent is resuming a
1855
+ * lesson. Consumed by the SDK: the launch's first `startActivity()`
1856
+ * adopts it automatically, which is what continues the platform run.
1857
+ * Games never read this; pass an explicit `runId` to
1858
+ * `startActivity()` to start a deliberate fresh attempt instead.
1859
+ */
1860
+ resumeRunId?: string;
1861
+ }
1883
1862
  interface ClientConfig {
1884
1863
  baseUrl: string;
1885
1864
  gameUrl?: string;
@@ -1908,6 +1887,8 @@ interface InitPayload {
1908
1887
  launchId?: string;
1909
1888
  /** 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`. */
1910
1889
  hasHeartbeatRelay?: boolean;
1890
+ /** Parent game context. Present only when `mode` is `'child'`. */
1891
+ parent?: ParentGameContext;
1911
1892
  }
1912
1893
  /**
1913
1894
  * Simplified user data passed to games via InitPayload
@@ -1942,6 +1923,164 @@ interface ClientEvents {
1942
1923
  };
1943
1924
  }
1944
1925
 
1926
+ /**
1927
+ * Playcademy SDK client for game developers.
1928
+ * Provides namespaced access to platform features for games running inside Cademy.
1929
+ */
1930
+ declare class PlaycademyClient extends PlaycademyBaseClient {
1931
+ /**
1932
+ * Connect external identity providers to the user's Playcademy account.
1933
+ * - `connect(provider)` - Link Discord, Google, etc. via OAuth popup
1934
+ */
1935
+ identity: {
1936
+ connect: (options: AuthOptions) => Promise<AuthResult>;
1937
+ _getContext: () => {
1938
+ isInIframe: boolean;
1939
+ };
1940
+ };
1941
+ /**
1942
+ * Game runtime lifecycle and asset loading.
1943
+ * - `exit()` - Return to Cademy hub
1944
+ * - `getGameToken()` - Get short-lived auth token
1945
+ * - `assets.url()`, `assets.json()`, `assets.fetch()` - Load game assets
1946
+ * - `on('pause')`, `on('resume')` - Handle visibility changes
1947
+ */
1948
+ runtime: {
1949
+ exit: () => void;
1950
+ onInit: (handler: (context: GameContextPayload) => void) => void;
1951
+ onTokenRefresh: (handler: (data: {
1952
+ token: string;
1953
+ exp: number;
1954
+ }) => void) => void;
1955
+ onPause: (handler: () => void) => void;
1956
+ onResume: (handler: () => void) => void;
1957
+ onForceExit: (handler: () => void) => void;
1958
+ onOverlay: (handler: (isVisible: boolean) => void) => void;
1959
+ ready: () => void;
1960
+ sendTelemetry: (data: {
1961
+ fps: number;
1962
+ mem: number;
1963
+ }) => void;
1964
+ removeListener: (eventType: MessageEvents, handler: ((context: GameContextPayload) => void) | ((data: {
1965
+ token: string;
1966
+ exp: number;
1967
+ }) => void) | (() => void) | ((isVisible: boolean) => void)) => void;
1968
+ removeAllListeners: () => void;
1969
+ getListenerCounts: () => Record<string, number>;
1970
+ assets: {
1971
+ url(pathOrStrings: string | TemplateStringsArray, ...values: unknown[]): string;
1972
+ fetch: (path: string, options?: RequestInit) => Promise<Response>;
1973
+ json: <T = unknown>(path: string) => Promise<T>;
1974
+ blob: (path: string) => Promise<Blob>;
1975
+ text: (path: string) => Promise<string>;
1976
+ arrayBuffer: (path: string) => Promise<ArrayBuffer>;
1977
+ };
1978
+ };
1979
+ /**
1980
+ * TimeBack integration for activity tracking and user context.
1981
+ *
1982
+ * User context (cached from init, refreshable):
1983
+ * - `user.role` - User's role (student, parent, teacher, etc.)
1984
+ * - `user.enrollments` - Courses the player is enrolled in for this game
1985
+ * - `user.refresh({ only: ['enrollments'] })` - Refresh enrollments from server
1986
+ * - `user.organizations` - Schools/districts the player belongs to
1987
+ * - `user.fetch()` - Refresh user context from server
1988
+ *
1989
+ * Activity tracking:
1990
+ * - `currentRunId` - Current activity run ID, or undefined when inactive
1991
+ * - `startActivity(metadata)` - Begin tracking an activity, return its run
1992
+ * ID, and automatically handle hidden-tab and visible-tab inactivity
1993
+ * with configurable paused-heartbeat timeout behavior
1994
+ * - `pauseActivity()` / `resumeActivity()` - Pause/resume timer
1995
+ * - `endActivity(scoreData)` - Submit activity results to TimeBack
1996
+ */
1997
+ timeback: {
1998
+ assessments: {
1999
+ start: (input: _playcademy_types.StartAssessmentInput) => Promise<_playcademy_types.AssessmentAttemptSnapshot>;
2000
+ latest: (options: _playcademy_types.GetLatestAssessmentOptions) => Promise<_playcademy_types.LatestAssessmentResult | null>;
2001
+ get: (attemptId: string) => Promise<_playcademy_types.AssessmentAttemptSnapshot>;
2002
+ save: (attemptId: string, input: _playcademy_types.SaveAssessmentInput) => Promise<_playcademy_types.AssessmentSaveResult>;
2003
+ submitItem: (attemptId: string, input: _playcademy_types.SubmitAssessmentItemInput) => Promise<_playcademy_types.SubmitAssessmentItemResult>;
2004
+ submit: (attemptId: string, input: _playcademy_types.SubmitAssessmentInput) => Promise<_playcademy_types.AssessmentSubmitResult>;
2005
+ };
2006
+ readonly user: TimebackUser;
2007
+ readonly currentRunId: string | undefined;
2008
+ startActivity: (metadata: _playcademy_types.ActivityData, options?: StartActivityOptions) => StartActivityResult;
2009
+ pauseActivity: () => void;
2010
+ resumeActivity: () => void;
2011
+ endActivity: (data: _playcademy_types.EndActivityScoreData) => Promise<_playcademy_types.EndActivityResponse | RelayedEndActivityResult>;
2012
+ course: {
2013
+ advance: (options?: {
2014
+ subject?: _playcademy_types.TimebackSubject;
2015
+ }) => Promise<_playcademy_types.AdvanceCourseResponse>;
2016
+ unenroll: (options?: {
2017
+ subject?: _playcademy_types.TimebackSubject;
2018
+ force?: boolean;
2019
+ }) => Promise<_playcademy_types.UnenrollCourseResponse>;
2020
+ };
2021
+ };
2022
+ /**
2023
+ * Game score submission and leaderboards.
2024
+ * - `submit(score, metadata?)` - Record a game score
2025
+ */
2026
+ scores: {
2027
+ submit: (score: number, metadata?: Record<string, unknown>) => Promise<ScoreSubmission>;
2028
+ };
2029
+ /**
2030
+ * Read-only leaderboard access for the current game scope.
2031
+ * - `fetch(options?)` - Fetch leaderboard entries
2032
+ */
2033
+ leaderboard: {
2034
+ fetch: (options?: _playcademy_types.LeaderboardOptions) => Promise<_playcademy_types.GameLeaderboardEntry[]>;
2035
+ };
2036
+ /**
2037
+ * Demo-mode helpers. Methods throw when called outside `client.mode === 'demo'`,
2038
+ * so callers should gate on the mode before reaching in.
2039
+ * - `profile.get()` - Read the anonymous demo player's profile
2040
+ * - `profile.update(updates)` - Update the demo player's profile (today: the required `displayName`)
2041
+ * - `end(score, options?)` - Signal to the parent shell that the demo has ended
2042
+ */
2043
+ demo: {
2044
+ profile: {
2045
+ get: () => Promise<_playcademy_types.DemoProfile>;
2046
+ update: (updates: _playcademy_types.DemoProfileUpdate) => Promise<_playcademy_types.DemoProfile>;
2047
+ };
2048
+ end: (score: number, options?: DemoEndOptions) => void;
2049
+ };
2050
+ /**
2051
+ * Make requests to your game's custom backend API routes.
2052
+ * - `get(path)`, `post(path, body)`, `put()`, `delete()` - HTTP methods
2053
+ * - Routes are relative to your game's deployment (e.g., '/hello' → your-game.playcademy.gg/api/hello)
2054
+ */
2055
+ backend: {
2056
+ get<T = unknown>(path: string, headers?: Record<string, string>): Promise<T>;
2057
+ post<T = unknown>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T>;
2058
+ put<T = unknown>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T>;
2059
+ patch<T = unknown>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T>;
2060
+ delete<T = unknown>(path: string, headers?: Record<string, string>): Promise<T>;
2061
+ request<T = unknown>(path: string, method: Method, body?: unknown, headers?: Record<string, string>): Promise<T>;
2062
+ download(path: string, method?: Method, body?: unknown, headers?: Record<string, string>): Promise<Response>;
2063
+ url(pathOrStrings: string | TemplateStringsArray, ...values: unknown[]): string;
2064
+ };
2065
+ /**
2066
+ * Launch other Playcademy games as embedded children (platform mode only).
2067
+ * - `launch({ slug, container, intent })` - Mount a child game in a nested
2068
+ * iframe with `mode: 'child'`; returns a session handle with `finished`
2069
+ * and `closed` promises and `close()`
2070
+ */
2071
+ embed: {
2072
+ launch(options: EmbedLaunchOptions): EmbedSession;
2073
+ };
2074
+ /** Auto-initializes a PlaycademyClient with context from the environment */
2075
+ static init: typeof init;
2076
+ /** Authenticates a user with email and password */
2077
+ static login: typeof login;
2078
+ /** Static identity utilities for OAuth operations */
2079
+ static identity: {
2080
+ parseOAuthState: typeof parseOAuthState;
2081
+ };
2082
+ }
2083
+
1945
2084
  /**
1946
2085
  * Event and message payload types for SDK messaging system
1947
2086
  */
@@ -2050,6 +2189,42 @@ interface DemoEndPayload extends DemoEndOptions {
2050
2189
  }
2051
2190
  type TimebackHeartbeatRelayRequest = Omit<HeartbeatRequest, 'gameId' | 'studentId' | 'windowStartedAtMs' | 'windowSequence'> & {
2052
2191
  windowStartedAtMs: number;
2192
+ /**
2193
+ * Marks a closed heartbeat window from the child's 15s accounting
2194
+ * cadence: the window's totals are final, its key never recurs, and
2195
+ * the parent forwards it exactly once (retries are safe against the
2196
+ * server's first-write-wins window dedupe). Absent on the 1s display
2197
+ * snapshots of the still-open window.
2198
+ */
2199
+ windowClosed?: boolean;
2200
+ };
2201
+ /**
2202
+ * Wire payload for `PLAYCADEMY_CHECKPOINT`. An opaque snapshot of the
2203
+ * child game's internal state; the SDK and the parent never interpret
2204
+ * `state`. The parent stamps the resume envelope's `childRunId` from the
2205
+ * activity-start announcement, so the checkpoint itself carries no ids.
2206
+ */
2207
+ interface ChildCheckpointRelay {
2208
+ state: unknown;
2209
+ }
2210
+ /**
2211
+ * Wire payload for `PLAYCADEMY_TIMEBACK_ACTIVITY_START`. A child-mode game
2212
+ * announces that its tracker opened a run, so the parent can open its own
2213
+ * Timeback run at the moment the lesson actually begins.
2214
+ */
2215
+ type TimebackActivityStartRelay = Pick<TimebackHeartbeatRelayRequest, 'runId' | 'resumeId' | 'activityData'>;
2216
+ /**
2217
+ * Wire payload for `PLAYCADEMY_TIMEBACK_ACTIVITY_END`. The same end-activity
2218
+ * body a platform-mode game would POST to its backend, relayed to the parent
2219
+ * instead. `timingData.durationSeconds` is the full active sitting and
2220
+ * `sessionTimingData` carries the FULL session totals: a relayed window is
2221
+ * never marked persisted (a postMessage hand-off proves nothing about the
2222
+ * parent's POST), so the parent reconciles these totals against the windows
2223
+ * the server confirmed before reporting the completion's remainder.
2224
+ */
2225
+ type TimebackActivityEndRelay = Omit<EndActivityRequest, 'gameId' | 'studentId' | 'xpEarned'> & {
2226
+ /** The child's XP suggestion; the parent decides the actual award. */
2227
+ xpEarned?: number;
2053
2228
  };
2054
2229
 
2055
2230
  /**
@@ -2064,6 +2239,243 @@ interface GameTokenResponse {
2064
2239
  baseUrl?: string;
2065
2240
  }
2066
2241
 
2242
+ /**
2243
+ * Public types for the launch protocol's parent side: the embedded
2244
+ * child-game session behind `client.embed.launch()`. Only what `launch()`
2245
+ * callers touch lives here; the implementation and its constructor-side
2246
+ * plumbing contracts live in `core/launch/session.ts`.
2247
+ */
2248
+
2249
+ /**
2250
+ * How a child launch is recorded on this game's Timeback course.
2251
+ * The same metadata you would give `startActivity()` if your own document
2252
+ * were running the lesson: the embed session runs the whole
2253
+ * start-through-end lifecycle for you, stamped with this.
2254
+ */
2255
+ interface EmbedTimebackRecording {
2256
+ /** The activity on this game's own course the launch is recorded as. */
2257
+ activityId: string;
2258
+ /** Display name for dashboards; prettified from `activityId` when omitted. */
2259
+ activityName?: string;
2260
+ /** With `subject`, routes the recording to one of this game's courses. */
2261
+ grade: TimebackGrade;
2262
+ /** With `grade`, routes the recording to one of this game's courses. */
2263
+ subject: TimebackSubject;
2264
+ /** Course id hint, same semantics as `startActivity()`. */
2265
+ courseId?: string;
2266
+ }
2267
+ /**
2268
+ * Everything a parent needs to resume an interrupted launch later. The
2269
+ * parent persists this wherever the interruption demands (memory for
2270
+ * exit-and-return, localStorage for tab close, its backend KV for
2271
+ * cross-device) and hands it back via `embed.launch({ resume })`.
2272
+ */
2273
+ interface EmbedResumeEnvelope {
2274
+ /**
2275
+ * The child's opaque checkpoint state, exactly as it last reported
2276
+ * it. Never introspect it: only the child can interpret its own
2277
+ * state, and it validates the blob on the way back in.
2278
+ */
2279
+ state: unknown;
2280
+ /**
2281
+ * The child's own run id from the interrupted launch, when a run was
2282
+ * active. On resume it crosses to the child, whose SDK re-announces
2283
+ * it automatically at the next launch's first start; the announced match
2284
+ * is what makes reusing `parentRunId` safe.
2285
+ */
2286
+ childRunId?: string;
2287
+ /**
2288
+ * The interrupted platform run, when Timeback reporting was active.
2289
+ * Reused (with a fresh sitting id) only when the child accepts the
2290
+ * resume; otherwise a fresh run is minted.
2291
+ */
2292
+ parentRunId?: string;
2293
+ }
2294
+ /**
2295
+ * Custom persistence for resume envelopes, passed as `launch()`'s
2296
+ * `resume` option. The default (when `resume` is omitted) is a built-in
2297
+ * localStorage store keyed by user, parent game, child game, and the
2298
+ * intent's lesson identity; supply your own store to keep envelopes
2299
+ * elsewhere (for example your backend, for cross-device resume).
2300
+ */
2301
+ interface EmbedResumeStore {
2302
+ /**
2303
+ * Returns the stored envelope for this lesson identity, or
2304
+ * null/undefined when there is nothing to resume. May be async; the
2305
+ * boot waits for it before the child's INIT is sent.
2306
+ */
2307
+ load(): EmbedResumeEnvelope | null | undefined | Promise<EmbedResumeEnvelope | null | undefined>;
2308
+ /**
2309
+ * Persists the latest envelope. Called on every checkpoint the child
2310
+ * relays (envelopes are capped at 64KB) and again when run identity
2311
+ * is minted. Writes should be synchronous or fire-and-forget: the
2312
+ * SDK never blocks on them, and a throw costs that envelope's
2313
+ * persistence, never the launch. Returned promises are used only for
2314
+ * ordering: `save` and `clear` run strictly in call order, so a slow
2315
+ * async save cannot land after the completion's clear.
2316
+ */
2317
+ save(envelope: EmbedResumeEnvelope): void;
2318
+ /** Deletes the stored envelope. Called once when the launch completes. */
2319
+ clear(): void;
2320
+ }
2321
+ /**
2322
+ * Play-time totals for a child launch, measured by the child's own
2323
+ * tracker (the parent's document is idle while the student plays).
2324
+ */
2325
+ interface EmbedSessionTiming {
2326
+ /** Seconds of active play. */
2327
+ activeSeconds: number;
2328
+ /** Seconds the child's tracker classified as paused or inactive, when known. */
2329
+ inactiveSeconds?: number;
2330
+ }
2331
+ /**
2332
+ * The launch's activity record, resolved by `session.finished`.
2333
+ *
2334
+ * `'completed'` carries the child's report and the `end()` capability;
2335
+ * `'abandoned'` means the session ended first (child exit or `close()`);
2336
+ * `'failed'` means the launch never happened. Failure is a state to
2337
+ * render, not an exception to catch — `finished` never rejects.
2338
+ */
2339
+ type EmbedActivity = EmbedActivityCompleted | EmbedActivityAbandoned | EmbedActivityFailed;
2340
+ /** The child called `endActivity()` and its report was relayed. */
2341
+ interface EmbedActivityCompleted {
2342
+ status: 'completed';
2343
+ /** Correct answers, from the child's report. */
2344
+ correct: number;
2345
+ /** Total questions, from the child's report. */
2346
+ total: number;
2347
+ timing: EmbedSessionTiming;
2348
+ /**
2349
+ * The child's full relayed end-activity body: its own activity ids,
2350
+ * suggested XP, and extensions. Audit data — the parent decides what
2351
+ * actually reaches the platform, via `end()`.
2352
+ */
2353
+ childReport: TimebackActivityEndRelay;
2354
+ /**
2355
+ * Posts the launch's completion to the parent's own backend: the
2356
+ * parent-minted run id, the `timeback` recording, the caller's score
2357
+ * and XP decision, and the child's ids as audit extensions. Requires
2358
+ * the `timeback` option at launch. Calling twice returns the same
2359
+ * promise, so a launch can never double-report from the client.
2360
+ */
2361
+ end(scores: EndActivityScoreData): Promise<EndActivityResponse>;
2362
+ }
2363
+ /**
2364
+ * The session ended (child exit or `close()`) before a report arrived.
2365
+ * Played time has already reached the platform through forwarded
2366
+ * heartbeats; an abandoned launch leaves no completion, exactly like a
2367
+ * student wandering away from any other game.
2368
+ */
2369
+ interface EmbedActivityAbandoned {
2370
+ status: 'abandoned';
2371
+ timing: EmbedSessionTiming;
2372
+ /**
2373
+ * The final resume envelope, when the child checkpointed during the
2374
+ * launch. Persist it (see `EmbedResumeEnvelope`) and pass it back to
2375
+ * `embed.launch({ resume })` to pick the lesson up later. Absent
2376
+ * when the child never checkpointed.
2377
+ */
2378
+ resume?: EmbedResumeEnvelope;
2379
+ }
2380
+ /** The launch never happened: the child could not be resolved or booted. */
2381
+ interface EmbedActivityFailed {
2382
+ status: 'failed';
2383
+ /** Why — unknown slug, missing deployment URL, INIT error or timeout. */
2384
+ error: PlaycademyError;
2385
+ }
2386
+ /**
2387
+ * Handle for one embedded child-game session.
2388
+ */
2389
+ interface EmbedSession {
2390
+ /** The mounted child iframe. Useful for focus management. */
2391
+ readonly iframe: HTMLIFrameElement;
2392
+ /**
2393
+ * The latest resume envelope, live during play; null until the child
2394
+ * first checkpoints. Read it on your own cadence to persist
2395
+ * mid-lesson (for example a debounced upload to your backend), so a
2396
+ * closed tab can resume on another device.
2397
+ */
2398
+ readonly checkpoint: EmbedResumeEnvelope | null;
2399
+ /**
2400
+ * Resolves the launch's activity record when it ends — the
2401
+ * `animation.finished` idiom. Never rejects: operational failures
2402
+ * resolve as `{ status: 'failed', error }`.
2403
+ */
2404
+ readonly finished: Promise<EmbedActivity>;
2405
+ /**
2406
+ * Resolves when the session is torn down and the iframe is unmounted:
2407
+ * on child exit, `close()`, boot failure, or when the SDK detects the
2408
+ * iframe was removed from the DOM. `finished` can resolve earlier than
2409
+ * this (a completed child usually shows a results screen before
2410
+ * exiting), so use `closed` to dismiss surrounding UI.
2411
+ */
2412
+ readonly closed: Promise<void>;
2413
+ /**
2414
+ * Tears the session down: unmounts the iframe and stops all listeners.
2415
+ * Resolves a still-pending `finished` as `'abandoned'`. Safe to call
2416
+ * more than once. Calling it is the deterministic path; the SDK also
2417
+ * tears down when the child exits, and detects an iframe removed
2418
+ * without `close()` within ~5 seconds, so nothing leaks either way.
2419
+ */
2420
+ close(): void;
2421
+ }
2422
+
2423
+ /**
2424
+ * Options for `client.embed.launch()`.
2425
+ */
2426
+ interface EmbedLaunchOptions {
2427
+ /** Slug of the child game to launch. Resolved to a game id at launch time. */
2428
+ slug: string;
2429
+ /** Element the child iframe is mounted into. The iframe fills it. */
2430
+ container: HTMLElement;
2431
+ /**
2432
+ * What the child should deliver. This is the platform-defined
2433
+ * {@link LaunchIntent} contract; it crosses the iframe in the INIT
2434
+ * payload's `parent` block and surfaces in the child as `client.parent.intent`.
2435
+ */
2436
+ intent: LaunchIntent;
2437
+ /**
2438
+ * Whether and how an interrupted launch can resume.
2439
+ *
2440
+ * Omitted (the default): the SDK persists the child's latest
2441
+ * checkpoint in localStorage, keyed by user, this game, the child,
2442
+ * and the intent's lesson identity. The next launch with the same
2443
+ * identity resumes automatically; completion clears the entry. Inert
2444
+ * for children that never call `client.parent.checkpoint()`.
2445
+ *
2446
+ * `false`: no persistence and no automatic resume. The manual surface
2447
+ * (`session.checkpoint`, the abandoned outcome's `resume`) still works.
2448
+ *
2449
+ * An {@link EmbedResumeEnvelope}: fully manual, one-shot. The launch
2450
+ * resumes from exactly this envelope and nothing is persisted.
2451
+ *
2452
+ * An {@link EmbedResumeStore}: delegate persistence (for example to
2453
+ * your backend, for cross-device resume).
2454
+ *
2455
+ * Whatever the policy, the child receives only the opaque `state`
2456
+ * (as `client.parent.resume`) and decides whether to use it; run ids
2457
+ * stay parent-side and drive run continuity when the child accepts.
2458
+ */
2459
+ resume?: false | EmbedResumeEnvelope | EmbedResumeStore;
2460
+ /**
2461
+ * Records the launch on this game's Timeback course: the metadata
2462
+ * you would have given `startActivity()` if your own document were
2463
+ * running the lesson. Stays in the parent SDK, stamping every
2464
+ * forwarded heartbeat and the final completion; it never crosses the
2465
+ * iframe. Omit for a pure UX embed.
2466
+ */
2467
+ timeback?: EmbedTimebackRecording;
2468
+ /**
2469
+ * Overrides the child's resolved deployment URL. Intended for local
2470
+ * development, where the child runs on a dev server the platform
2471
+ * doesn't know about. With `gameUrl` set, a slug that fails to
2472
+ * resolve degrades to a warning (the slug stands in as the child's
2473
+ * game id) instead of failing the launch, so an unregistered child
2474
+ * still launches locally.
2475
+ */
2476
+ gameUrl?: string;
2477
+ }
2478
+
2067
2479
  /**
2068
2480
  * Scores namespace types
2069
2481
  */
@@ -2167,6 +2579,29 @@ interface BucketFile {
2167
2579
  lastModified: string;
2168
2580
  contentType?: string;
2169
2581
  }
2582
+ /**
2583
+ * Options for a single-page bucket listing
2584
+ */
2585
+ interface BucketListPageOptions {
2586
+ /** Restrict results to keys starting with this prefix */
2587
+ prefix?: string;
2588
+ /** Opaque continuation cursor from the previous page's result */
2589
+ cursor?: string;
2590
+ /** Page size (1-1000); the server may return fewer */
2591
+ limit?: number;
2592
+ /** Roll deeper keys into `prefixes` entries, S3 delimiter style */
2593
+ delimiter?: string;
2594
+ }
2595
+ /**
2596
+ * One page of a bucket listing
2597
+ */
2598
+ interface BucketFilePage {
2599
+ files: BucketFile[];
2600
+ /** Rolled-up common prefixes; present for delimiter listings that found any */
2601
+ prefixes?: string[];
2602
+ /** Present only when more pages remain */
2603
+ cursor?: string;
2604
+ }
2170
2605
  /**
2171
2606
  * KV key entry
2172
2607
  */
@@ -2260,4 +2695,4 @@ interface PlatformTimebackUser extends PlatformTimebackUserContext {
2260
2695
  }
2261
2696
 
2262
2697
  export { PlaycademyClient };
2263
- export type { AuthCallbackPayload, AuthOptions, AuthProviderType, AuthResult, AuthServerMessage, AuthStateChangePayload, AuthStateUpdate, BetterAuthApiKey, BetterAuthApiKeyResponse, BetterAuthSignInResponse, BucketFile, ClientConfig, ClientEvents, CourseMastery, CourseXp, DemoEndOptions, DemoEndPayload, DevUploadEvent, DevUploadHooks, EventListeners, ExternalGame, FetchedGame, Game, GameContextPayload, GameCustomHostname, GameInitUser, GameRow as GameRecord, GameTokenResponse, GetHighestGradeMasteredOptions, GetMasteryOptions, GetXpOptions, HighestGradeMasteredResponse, HostedGame, InitErrorPayload, InitPayload, KVKeyEntry, KVKeyMetadata, KVSeedEntry, KVStatsResponse, KeyEventPayload, LoginResponse, MasteryResponse, PlatformTimebackUser, PlatformTimebackUserContext, PlaycademyMode, PlaycademyServerClientConfig, PlaycademyServerClientState, ScoreSubmission, StartActivityOptions, StartActivityResult, TelemetryPayload, TimebackEnrollment, TimebackHeartbeatRelayRequest, TimebackInitContext, TimebackOrganization, TimebackUser, TimebackUserContext, TimebackUserHighestGradeMastered, TimebackUserMastery, TimebackUserRefreshField, TimebackUserRefreshOptions, TimebackUserXp, TokenRefreshPayload, TokenType, UpsertGameMetadataInput, UserRow as User, XpResponse };
2698
+ export type { AuthCallbackPayload, AuthOptions, AuthProviderType, AuthResult, AuthServerMessage, AuthStateChangePayload, AuthStateUpdate, BetterAuthApiKey, BetterAuthApiKeyResponse, BetterAuthSignInResponse, BucketFile, BucketFilePage, BucketListPageOptions, ChildCheckpointRelay, ClientConfig, ClientEvents, CourseMastery, CourseXp, DemoEndOptions, DemoEndPayload, DevUploadEvent, DevUploadHooks, EmbedActivity, EmbedActivityAbandoned, EmbedActivityCompleted, EmbedActivityFailed, EmbedLaunchOptions, EmbedResumeEnvelope, EmbedResumeStore, EmbedSession, EmbedSessionTiming, EmbedTimebackRecording, EventListeners, ExternalGame, FetchedGame, Game, GameContextPayload, GameCustomHostname, GameInitUser, GameRow as GameRecord, GameTokenResponse, GetHighestGradeMasteredOptions, GetMasteryOptions, GetXpOptions, HighestGradeMasteredResponse, HostedGame, InitErrorPayload, InitPayload, KVKeyEntry, KVKeyMetadata, KVSeedEntry, KVStatsResponse, KeyEventPayload, LaunchIntent, LoginResponse, MasteryResponse, ParentGameContext, ParentGameHandle, PlatformTimebackUser, PlatformTimebackUserContext, PlaycademyMode, PlaycademyServerClientConfig, PlaycademyServerClientState, ScoreSubmission, StartActivityOptions, StartActivityResult, TelemetryPayload, TimebackActivityEndRelay, TimebackActivityStartRelay, TimebackEnrollment, TimebackHeartbeatRelayRequest, TimebackInitContext, TimebackOrganization, TimebackUser, TimebackUserContext, TimebackUserHighestGradeMastered, TimebackUserMastery, TimebackUserRefreshField, TimebackUserRefreshOptions, TimebackUserXp, TokenRefreshPayload, TokenType, UpsertGameMetadataInput, UserRow as User, XpResponse };