@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.
@@ -1,5 +1,7 @@
1
1
  import { SchemaInfo } from '@playcademy/cloudflare';
2
- import { TimebackGrade, TimebackSubject, HeartbeatRequest, TimebackCourseConfig, CourseConfig, OrganizationConfig, ComponentConfig, ResourceConfig, ComponentResourceConfig } from '@playcademy/types/timeback';
2
+ import { GamePermission, AUTH_PROVIDER_IDS } from '@playcademy/constants';
3
+ import { TimebackGrade, TimebackSubject, ELevel, HeartbeatRequest, EndActivityRequest, EndActivityScoreData, EndActivityResponse, TimebackCourseConfig, CourseConfig, OrganizationConfig, ComponentConfig, ResourceConfig, ComponentResourceConfig } from '@playcademy/types/timeback';
4
+ 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';
3
5
  import * as _playcademy_types from '@playcademy/types';
4
6
  import { GameManifest, LocalDayContext } from '@playcademy/types';
5
7
  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';
@@ -7,7 +9,6 @@ import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
7
9
  import { DomainValidationRecords } from '@playcademy/types/game';
8
10
  import { z } from 'zod';
9
11
  import { TimebackUserRole, UserEnrollment, UserOrganization, UserInfo } from '@playcademy/types/user';
10
- import { AUTH_PROVIDER_IDS } from '@playcademy/constants';
11
12
 
12
13
  /** Permitted HTTP verbs */
13
14
  type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
@@ -292,11 +293,32 @@ declare enum MessageEvents {
292
293
  */
293
294
  DEMO_END = "PLAYCADEMY_DEMO_END",
294
295
  /**
295
- * Game shares its latest TimeBack heartbeat window with the parent shell.
296
- * The shell can relay this payload during top-level page teardown, which is
297
- * more reliable than relying only on cross-origin iframe unload events.
296
+ * Game shares its latest TimeBack heartbeat window with the embedding
297
+ * window. The hub relays this payload during top-level page teardown
298
+ * (more reliable than cross-origin iframe unload events); in child mode
299
+ * this stream is the heartbeat's only delivery path.
298
300
  */
299
301
  TIMEBACK_HEARTBEAT_RELAY = "PLAYCADEMY_TIMEBACK_HEARTBEAT_RELAY",
302
+ /**
303
+ * Game announces its tracker opened a run (startActivity).
304
+ * Today only child-mode games emit this: the parent opens its own
305
+ * Timeback run on receipt, so tracked time starts when the lesson
306
+ * does, not when the iframe boots.
307
+ */
308
+ TIMEBACK_ACTIVITY_START = "PLAYCADEMY_TIMEBACK_ACTIVITY_START",
309
+ /**
310
+ * Game relays its full end-activity report instead of POSTing it to
311
+ * its own backend. Today only child-mode games emit this: the parent
312
+ * reports the result to Timeback under its own identity.
313
+ */
314
+ TIMEBACK_ACTIVITY_END = "PLAYCADEMY_TIMEBACK_ACTIVITY_END",
315
+ /**
316
+ * Child game → parent game. An opaque checkpoint of the child's
317
+ * internal state, streamed during play so the parent always holds a
318
+ * near-current copy (the death-time postMessage hop is unreliable;
319
+ * continuous checkpointing is the design). Latest wins.
320
+ */
321
+ CHECKPOINT = "PLAYCADEMY_CHECKPOINT",
300
322
  /**
301
323
  * Notifies about authentication state changes.
302
324
  * Can be sent in both directions depending on auth flow.
@@ -366,11 +388,36 @@ interface MessageEventMap {
366
388
  [MessageEvents.DEMO_END]: DemoEndPayload;
367
389
  /** Latest TimeBack heartbeat window for parent-shell unload relay */
368
390
  [MessageEvents.TIMEBACK_HEARTBEAT_RELAY]: TimebackHeartbeatRelayRequest;
391
+ /** Child-mode run-opened announcement */
392
+ [MessageEvents.TIMEBACK_ACTIVITY_START]: TimebackActivityStartRelay;
393
+ /** Child-mode end-activity report relay */
394
+ [MessageEvents.TIMEBACK_ACTIVITY_END]: TimebackActivityEndRelay;
395
+ [MessageEvents.CHECKPOINT]: ChildCheckpointRelay;
369
396
  /** Authentication state change notification */
370
397
  [MessageEvents.AUTH_STATE_CHANGE]: AuthStateChangePayload;
371
398
  /** OAuth callback data from popup/new-tab windows */
372
399
  [MessageEvents.AUTH_CALLBACK]: AuthCallbackPayload;
373
400
  }
401
+ /**
402
+ * Options for `messaging.listen()`.
403
+ */
404
+ interface ListenOptions {
405
+ /**
406
+ * Accept postMessages only from the embedding window (`window.parent`).
407
+ *
408
+ * Defaults by direction: on for launcher-directed control messages
409
+ * (see the direction table), off otherwise. A child iframe posts to
410
+ * this window too (that is how the embed relay works), but it must
411
+ * never be able to impersonate the platform above; pass `false` to
412
+ * loosen a launcher-directed listener deliberately (for example a
413
+ * test rig that posts control messages from a non-parent window).
414
+ * Local CustomEvent delivery is unaffected, since a separate
415
+ * document cannot dispatch into this one. At the top window nothing
416
+ * can match (`window.parent === window`), which is correct: a
417
+ * standalone run has no launcher.
418
+ */
419
+ fromParent?: boolean;
420
+ }
374
421
  /**
375
422
  * **PlaycademyMessaging Class**
376
423
  *
@@ -506,7 +553,7 @@ declare class PlaycademyMessaging {
506
553
  * })
507
554
  * ```
508
555
  */
509
- listen<K extends MessageEvents>(type: K, handler: MessageHandler<MessageEventMap[K]>): void;
556
+ listen<K extends MessageEvents>(type: K, handler: MessageHandler<MessageEventMap[K]>, options?: ListenOptions): void;
510
557
  /**
511
558
  * **Remove Message Listener Method**
512
559
  *
@@ -736,6 +783,22 @@ declare class PlaycademyMessaging {
736
783
  * All message sending and receiving is fully type-safe with TypeScript.
737
784
  */
738
785
  declare const messaging: PlaycademyMessaging;
786
+ /**
787
+ * Validates that a `message` event came from an embedded game iframe this
788
+ * window controls. This is a security boundary — every receiver that
789
+ * accepts game→embedder messages should gate on it:
790
+ *
791
+ * - `source` must be the iframe's own `contentWindow` (no other window may
792
+ * drive the session)
793
+ * - `'null'` (sandboxed, opaque) origins are rejected
794
+ * - unless `origin` is the `'*'` wildcard, the event origin must match it
795
+ * exactly
796
+ *
797
+ * `messaging.listen()` is the wrong tool for that direction: it accepts
798
+ * messages arriving at this window without source filtering, which is
799
+ * correct only for a game receiving from its own embedder.
800
+ */
801
+ declare function isTrustedIframeMessage(event: MessageEvent, iframeWindow: Window | null, origin: string): boolean;
739
802
 
740
803
  /**
741
804
  * @fileoverview Authentication Strategy Pattern
@@ -1005,6 +1068,8 @@ declare const users: drizzle_orm_pg_core.PgTableWithColumns<{
1005
1068
  interface GameMetadata {
1006
1069
  description?: string;
1007
1070
  emoji?: string;
1071
+ /** Browser permissions delegated to the game's iframe (opt-in per game). */
1072
+ permissions?: GamePermission[];
1008
1073
  [key: string]: unknown;
1009
1074
  }
1010
1075
  /**
@@ -1990,6 +2055,8 @@ declare abstract class PlaycademyBaseClient {
1990
2055
  isInIframe: boolean;
1991
2056
  };
1992
2057
  protected initPayload?: InitPayload;
2058
+ /** Memoized `client.parent` handle; built on first access in child mode. */
2059
+ private parentHandle?;
1993
2060
  protected launchId?: string;
1994
2061
  protected gameOrigin?: string;
1995
2062
  private browserTimeZone?;
@@ -2010,6 +2077,13 @@ declare abstract class PlaycademyBaseClient {
2010
2077
  * Local day context supplied by the platform during iframe initialization.
2011
2078
  */
2012
2079
  get localDay(): LocalDayContext | undefined;
2080
+ /**
2081
+ * Null outside `mode: 'child'` even if a payload smuggled in a `parent`
2082
+ * block, so `if (client.parent)` is a reliable child-mode test. The
2083
+ * returned handle is the wire block plus `checkpoint()`, memoized so
2084
+ * `client.parent === client.parent`.
2085
+ */
2086
+ get parent(): ParentGameHandle | null;
2013
2087
  /**
2014
2088
  * Sets the authentication token for API requests.
2015
2089
  */
@@ -2155,21 +2229,6 @@ declare function init<T extends PlaycademyBaseClient = PlaycademyBaseClient>(thi
2155
2229
  */
2156
2230
  declare function login(baseUrl: string, email: string, password: string): Promise<LoginResponse>;
2157
2231
 
2158
- /**
2159
- * Cache configuration types for runtime customization
2160
- */
2161
- /**
2162
- * Runtime configuration for TTL cache behavior
2163
- */
2164
- interface TTLCacheConfig {
2165
- /** Time-to-live in milliseconds. Set to 0 to disable caching for this call. */
2166
- ttl?: number;
2167
- /** Force refresh, bypassing cache */
2168
- force?: boolean;
2169
- /** Skip cache and fetch fresh data (alias for force) */
2170
- skipCache?: boolean;
2171
- }
2172
-
2173
2232
  /**
2174
2233
  * Options for configuring activity tracking behavior.
2175
2234
  */
@@ -2227,6 +2286,21 @@ interface StartActivityResult {
2227
2286
  runId: string;
2228
2287
  }
2229
2288
 
2289
+ /**
2290
+ * Cache configuration types for runtime customization
2291
+ */
2292
+ /**
2293
+ * Runtime configuration for TTL cache behavior
2294
+ */
2295
+ interface TTLCacheConfig {
2296
+ /** Time-to-live in milliseconds. Set to 0 to disable caching for this call. */
2297
+ ttl?: number;
2298
+ /** Force refresh, bypassing cache */
2299
+ force?: boolean;
2300
+ /** Skip cache and fetch fresh data (alias for force) */
2301
+ skipCache?: boolean;
2302
+ }
2303
+
2230
2304
  /**
2231
2305
  * Type definitions for the game timeback namespace.
2232
2306
  *
@@ -2507,8 +2581,79 @@ type TokenType = 'session' | 'apiKey' | 'gameJwt';
2507
2581
  * - `'standalone'` — game is running outside any iframe (e.g. `bun run dev`
2508
2582
  * or direct-deploy preview) with a mock token and no real platform
2509
2583
  * context. API calls will not succeed; use this to branch UX locally.
2584
+ * - `'child'` — game is embedded by another game (the parent), which ran
2585
+ * the INIT handshake itself and included a `parent` block in the payload.
2586
+ * The user and token are real (the parent's own). Direct Timeback
2587
+ * reporting is suppressed and relayed to the parent instead; read
2588
+ * `client.parent` for the parent's identity and launch instructions.
2589
+ */
2590
+ type PlaycademyMode = 'platform' | 'demo' | 'standalone' | 'child';
2591
+ /**
2592
+ * What a parent game asks a child game to deliver.
2593
+ *
2594
+ * This contract is platform-defined so any parent can launch any child
2595
+ * without pair-specific vocabularies. `lessonId` addresses the child's own
2596
+ * catalog; the child maps it to internal content. Pair-specific extras
2597
+ * belong in `extensions`, which the platform never interprets (the same
2598
+ * split LTI makes between its resource link and custom claims).
2599
+ *
2600
+ * Parent and child ship independently, so children should still validate
2601
+ * the intent at runtime; SDK compatibility floors manage version skew.
2510
2602
  */
2511
- type PlaycademyMode = 'platform' | 'demo' | 'standalone';
2603
+ interface LaunchIntent {
2604
+ /** The activity to deliver, addressed in the child's own catalog. */
2605
+ lessonId: string;
2606
+ /** Pedagogy stage of the lesson (the platform's E1-E4 taxonomy). */
2607
+ eLevel: ELevel;
2608
+ /** Pair-specific extras. Never interpreted by the platform. */
2609
+ extensions?: Record<string, unknown>;
2610
+ }
2611
+ /**
2612
+ * What `client.parent` actually returns: the parent's wire context plus
2613
+ * the child's one capability toward it. The wire block
2614
+ * (`ParentGameContext`) stays a pure serializable payload; this handle
2615
+ * wraps it. Null outside child mode, so `if (client.parent)` remains the
2616
+ * child-launch test.
2617
+ */
2618
+ type ParentGameHandle = ParentGameContext & {
2619
+ /**
2620
+ * Streams an opaque checkpoint of this game's state to the parent,
2621
+ * so an interrupted launch can resume later (the parent hands it
2622
+ * back as `client.parent.resume`). Call it whenever your state
2623
+ * meaningfully changes — never wait for teardown, the closing-tab
2624
+ * message hop is unreliable. State must be JSON-serializable and
2625
+ * under 64KB; anything else is dropped with a warning. This is not
2626
+ * your save system: the parent holds it for resume only.
2627
+ */
2628
+ checkpoint(state: unknown): void;
2629
+ };
2630
+ /**
2631
+ * The parent game's block in a `mode: 'child'` INIT payload. Present only
2632
+ * when a parent game launched this client (see the parent-child game
2633
+ * embedding proposal, `docs/dev/timeback/`).
2634
+ */
2635
+ interface ParentGameContext {
2636
+ /** The parent game's own platform game ID (NOT this game's ID). */
2637
+ gameId: string;
2638
+ /** What the parent wants this child to deliver. */
2639
+ intent: LaunchIntent;
2640
+ /**
2641
+ * An earlier launch's checkpoint state, when the parent is resuming
2642
+ * an interrupted lesson. Opaque: this game wrote it via
2643
+ * `client.parent.checkpoint()`, and only this game can interpret it.
2644
+ * Validate it like the intent — a blob from an older build of this
2645
+ * game should be ignored, not trusted.
2646
+ */
2647
+ resume?: unknown;
2648
+ /**
2649
+ * The interrupted run id, present when the parent is resuming a
2650
+ * lesson. Consumed by the SDK: the launch's first `startActivity()`
2651
+ * adopts it automatically, which is what continues the platform run.
2652
+ * Games never read this; pass an explicit `runId` to
2653
+ * `startActivity()` to start a deliberate fresh attempt instead.
2654
+ */
2655
+ resumeRunId?: string;
2656
+ }
2512
2657
  interface ClientConfig {
2513
2658
  baseUrl: string;
2514
2659
  gameUrl?: string;
@@ -2537,6 +2682,8 @@ interface InitPayload {
2537
2682
  launchId?: string;
2538
2683
  /** 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`. */
2539
2684
  hasHeartbeatRelay?: boolean;
2685
+ /** Parent game context. Present only when `mode` is `'child'`. */
2686
+ parent?: ParentGameContext;
2540
2687
  }
2541
2688
  /**
2542
2689
  * Simplified user data passed to games via InitPayload
@@ -2679,6 +2826,42 @@ interface DemoEndPayload extends DemoEndOptions {
2679
2826
  }
2680
2827
  type TimebackHeartbeatRelayRequest = Omit<HeartbeatRequest, 'gameId' | 'studentId' | 'windowStartedAtMs' | 'windowSequence'> & {
2681
2828
  windowStartedAtMs: number;
2829
+ /**
2830
+ * Marks a closed heartbeat window from the child's 15s accounting
2831
+ * cadence: the window's totals are final, its key never recurs, and
2832
+ * the parent forwards it exactly once (retries are safe against the
2833
+ * server's first-write-wins window dedupe). Absent on the 1s display
2834
+ * snapshots of the still-open window.
2835
+ */
2836
+ windowClosed?: boolean;
2837
+ };
2838
+ /**
2839
+ * Wire payload for `PLAYCADEMY_CHECKPOINT`. An opaque snapshot of the
2840
+ * child game's internal state; the SDK and the parent never interpret
2841
+ * `state`. The parent stamps the resume envelope's `childRunId` from the
2842
+ * activity-start announcement, so the checkpoint itself carries no ids.
2843
+ */
2844
+ interface ChildCheckpointRelay {
2845
+ state: unknown;
2846
+ }
2847
+ /**
2848
+ * Wire payload for `PLAYCADEMY_TIMEBACK_ACTIVITY_START`. A child-mode game
2849
+ * announces that its tracker opened a run, so the parent can open its own
2850
+ * Timeback run at the moment the lesson actually begins.
2851
+ */
2852
+ type TimebackActivityStartRelay = Pick<TimebackHeartbeatRelayRequest, 'runId' | 'resumeId' | 'activityData'>;
2853
+ /**
2854
+ * Wire payload for `PLAYCADEMY_TIMEBACK_ACTIVITY_END`. The same end-activity
2855
+ * body a platform-mode game would POST to its backend, relayed to the parent
2856
+ * instead. `timingData.durationSeconds` is the full active sitting and
2857
+ * `sessionTimingData` carries the FULL session totals: a relayed window is
2858
+ * never marked persisted (a postMessage hand-off proves nothing about the
2859
+ * parent's POST), so the parent reconciles these totals against the windows
2860
+ * the server confirmed before reporting the completion's remainder.
2861
+ */
2862
+ type TimebackActivityEndRelay = Omit<EndActivityRequest, 'gameId' | 'studentId' | 'xpEarned'> & {
2863
+ /** The child's XP suggestion; the parent decides the actual award. */
2864
+ xpEarned?: number;
2682
2865
  };
2683
2866
 
2684
2867
  /**
@@ -2693,6 +2876,265 @@ interface GameTokenResponse {
2693
2876
  baseUrl?: string;
2694
2877
  }
2695
2878
 
2879
+ /**
2880
+ * Public types for the launch protocol's parent side: the embedded
2881
+ * child-game session behind `client.embed.launch()`. Only what `launch()`
2882
+ * callers touch lives here; the implementation and its constructor-side
2883
+ * plumbing contracts live in `core/launch/session.ts`.
2884
+ */
2885
+
2886
+ /**
2887
+ * How a child launch is recorded on this game's Timeback course.
2888
+ * The same metadata you would give `startActivity()` if your own document
2889
+ * were running the lesson: the embed session runs the whole
2890
+ * start-through-end lifecycle for you, stamped with this.
2891
+ */
2892
+ interface EmbedTimebackRecording {
2893
+ /** The activity on this game's own course the launch is recorded as. */
2894
+ activityId: string;
2895
+ /** Display name for dashboards; prettified from `activityId` when omitted. */
2896
+ activityName?: string;
2897
+ /** With `subject`, routes the recording to one of this game's courses. */
2898
+ grade: TimebackGrade;
2899
+ /** With `grade`, routes the recording to one of this game's courses. */
2900
+ subject: TimebackSubject;
2901
+ /** Course id hint, same semantics as `startActivity()`. */
2902
+ courseId?: string;
2903
+ }
2904
+ /**
2905
+ * Everything a parent needs to resume an interrupted launch later. The
2906
+ * parent persists this wherever the interruption demands (memory for
2907
+ * exit-and-return, localStorage for tab close, its backend KV for
2908
+ * cross-device) and hands it back via `embed.launch({ resume })`.
2909
+ */
2910
+ interface EmbedResumeEnvelope {
2911
+ /**
2912
+ * The child's opaque checkpoint state, exactly as it last reported
2913
+ * it. Never introspect it: only the child can interpret its own
2914
+ * state, and it validates the blob on the way back in.
2915
+ */
2916
+ state: unknown;
2917
+ /**
2918
+ * The child's own run id from the interrupted launch, when a run was
2919
+ * active. On resume it crosses to the child, whose SDK re-announces
2920
+ * it automatically at the next launch's first start; the announced match
2921
+ * is what makes reusing `parentRunId` safe.
2922
+ */
2923
+ childRunId?: string;
2924
+ /**
2925
+ * The interrupted platform run, when Timeback reporting was active.
2926
+ * Reused (with a fresh sitting id) only when the child accepts the
2927
+ * resume; otherwise a fresh run is minted.
2928
+ */
2929
+ parentRunId?: string;
2930
+ }
2931
+ /**
2932
+ * Custom persistence for resume envelopes, passed as `launch()`'s
2933
+ * `resume` option. The default (when `resume` is omitted) is a built-in
2934
+ * localStorage store keyed by user, parent game, child game, and the
2935
+ * intent's lesson identity; supply your own store to keep envelopes
2936
+ * elsewhere (for example your backend, for cross-device resume).
2937
+ */
2938
+ interface EmbedResumeStore {
2939
+ /**
2940
+ * Returns the stored envelope for this lesson identity, or
2941
+ * null/undefined when there is nothing to resume. May be async; the
2942
+ * boot waits for it before the child's INIT is sent.
2943
+ */
2944
+ load(): EmbedResumeEnvelope | null | undefined | Promise<EmbedResumeEnvelope | null | undefined>;
2945
+ /**
2946
+ * Persists the latest envelope. Called on every checkpoint the child
2947
+ * relays (envelopes are capped at 64KB) and again when run identity
2948
+ * is minted. Writes should be synchronous or fire-and-forget: the
2949
+ * SDK never blocks on them, and a throw costs that envelope's
2950
+ * persistence, never the launch. Returned promises are used only for
2951
+ * ordering: `save` and `clear` run strictly in call order, so a slow
2952
+ * async save cannot land after the completion's clear.
2953
+ */
2954
+ save(envelope: EmbedResumeEnvelope): void;
2955
+ /** Deletes the stored envelope. Called once when the launch completes. */
2956
+ clear(): void;
2957
+ }
2958
+ /**
2959
+ * Play-time totals for a child launch, measured by the child's own
2960
+ * tracker (the parent's document is idle while the student plays).
2961
+ */
2962
+ interface EmbedSessionTiming {
2963
+ /** Seconds of active play. */
2964
+ activeSeconds: number;
2965
+ /** Seconds the child's tracker classified as paused or inactive, when known. */
2966
+ inactiveSeconds?: number;
2967
+ }
2968
+ /**
2969
+ * The launch's activity record, resolved by `session.finished`.
2970
+ *
2971
+ * `'completed'` carries the child's report and the `end()` capability;
2972
+ * `'abandoned'` means the session ended first (child exit or `close()`);
2973
+ * `'failed'` means the launch never happened. Failure is a state to
2974
+ * render, not an exception to catch — `finished` never rejects.
2975
+ */
2976
+ type EmbedActivity = EmbedActivityCompleted | EmbedActivityAbandoned | EmbedActivityFailed;
2977
+ /** The child called `endActivity()` and its report was relayed. */
2978
+ interface EmbedActivityCompleted {
2979
+ status: 'completed';
2980
+ /**
2981
+ * The platform run this completion records under. One run ends in at
2982
+ * most one completion (the server dedupes on it), and a resumed launch
2983
+ * keeps the interrupted run's id — so this doubles as the completion's
2984
+ * attempt identity: feed it to whatever consumes the result and drop
2985
+ * anything you have seen before. Absent only for pure UX embeds
2986
+ * (launched without `timeback`), which record nothing.
2987
+ */
2988
+ runId?: string;
2989
+ /** Correct answers, from the child's report. */
2990
+ correct: number;
2991
+ /** Total questions, from the child's report. */
2992
+ total: number;
2993
+ timing: EmbedSessionTiming;
2994
+ /**
2995
+ * The child's full relayed end-activity body: its own activity ids,
2996
+ * suggested XP, and extensions. Audit data — the parent decides what
2997
+ * actually reaches the platform, via `end()`.
2998
+ */
2999
+ childReport: TimebackActivityEndRelay;
3000
+ /**
3001
+ * Posts the launch's completion to the parent's own backend: the
3002
+ * parent-minted run id, the `timeback` recording, the caller's score
3003
+ * and XP decision, and the child's ids as audit extensions. Requires
3004
+ * the `timeback` option at launch. Calling twice returns the same
3005
+ * promise, so a launch can never double-report from the client.
3006
+ */
3007
+ end(scores: EndActivityScoreData): Promise<EndActivityResponse>;
3008
+ }
3009
+ /**
3010
+ * The session ended (child exit or `close()`) before a report arrived.
3011
+ * Played time has already reached the platform through forwarded
3012
+ * heartbeats; an abandoned launch leaves no completion, exactly like a
3013
+ * student wandering away from any other game.
3014
+ */
3015
+ interface EmbedActivityAbandoned {
3016
+ status: 'abandoned';
3017
+ /**
3018
+ * The platform run the launch was recording under, when one had
3019
+ * opened. Absent when the child never started an activity or the
3020
+ * launch was a pure UX embed.
3021
+ */
3022
+ runId?: string;
3023
+ timing: EmbedSessionTiming;
3024
+ /**
3025
+ * The final resume envelope, when the child checkpointed during the
3026
+ * launch. Persist it (see `EmbedResumeEnvelope`) and pass it back to
3027
+ * `embed.launch({ resume })` to pick the lesson up later. Absent
3028
+ * when the child never checkpointed.
3029
+ */
3030
+ resume?: EmbedResumeEnvelope;
3031
+ }
3032
+ /** The launch never happened: the child could not be resolved or booted. */
3033
+ interface EmbedActivityFailed {
3034
+ status: 'failed';
3035
+ /** Why — unknown slug, missing deployment URL, INIT error or timeout. */
3036
+ error: PlaycademyError;
3037
+ }
3038
+ /**
3039
+ * Handle for one embedded child-game session.
3040
+ */
3041
+ interface EmbedSession {
3042
+ /** The mounted child iframe. Useful for focus management. */
3043
+ readonly iframe: HTMLIFrameElement;
3044
+ /**
3045
+ * The platform run this launch records under, or null before the run
3046
+ * opens (the child's first activity) and for pure UX embeds. Stable
3047
+ * once set; also echoed on the finished record, which is where most
3048
+ * callers should read it.
3049
+ */
3050
+ readonly runId: string | null;
3051
+ /**
3052
+ * The latest resume envelope, live during play; null until the child
3053
+ * first checkpoints. Read it on your own cadence to persist
3054
+ * mid-lesson (for example a debounced upload to your backend), so a
3055
+ * closed tab can resume on another device.
3056
+ */
3057
+ readonly checkpoint: EmbedResumeEnvelope | null;
3058
+ /**
3059
+ * Resolves the launch's activity record when it ends — the
3060
+ * `animation.finished` idiom. Never rejects: operational failures
3061
+ * resolve as `{ status: 'failed', error }`.
3062
+ */
3063
+ readonly finished: Promise<EmbedActivity>;
3064
+ /**
3065
+ * Resolves when the session is torn down and the iframe is unmounted:
3066
+ * on child exit, `close()`, boot failure, or when the SDK detects the
3067
+ * iframe was removed from the DOM. `finished` can resolve earlier than
3068
+ * this (a completed child usually shows a results screen before
3069
+ * exiting), so use `closed` to dismiss surrounding UI.
3070
+ */
3071
+ readonly closed: Promise<void>;
3072
+ /**
3073
+ * Tears the session down: unmounts the iframe and stops all listeners.
3074
+ * Resolves a still-pending `finished` as `'abandoned'`. Safe to call
3075
+ * more than once. Calling it is the deterministic path; the SDK also
3076
+ * tears down when the child exits, and detects an iframe removed
3077
+ * without `close()` within ~5 seconds, so nothing leaks either way.
3078
+ */
3079
+ close(): void;
3080
+ }
3081
+
3082
+ /**
3083
+ * Options for `client.embed.launch()`.
3084
+ */
3085
+ interface EmbedLaunchOptions {
3086
+ /** Slug of the child game to launch. Resolved to a game id at launch time. */
3087
+ slug: string;
3088
+ /** Element the child iframe is mounted into. The iframe fills it. */
3089
+ container: HTMLElement;
3090
+ /**
3091
+ * What the child should deliver. This is the platform-defined
3092
+ * {@link LaunchIntent} contract; it crosses the iframe in the INIT
3093
+ * payload's `parent` block and surfaces in the child as `client.parent.intent`.
3094
+ */
3095
+ intent: LaunchIntent;
3096
+ /**
3097
+ * Whether and how an interrupted launch can resume.
3098
+ *
3099
+ * Omitted (the default): the SDK persists the child's latest
3100
+ * checkpoint in localStorage, keyed by user, this game, the child,
3101
+ * and the intent's lesson identity. The next launch with the same
3102
+ * identity resumes automatically; completion clears the entry. Inert
3103
+ * for children that never call `client.parent.checkpoint()`.
3104
+ *
3105
+ * `false`: no persistence and no automatic resume. The manual surface
3106
+ * (`session.checkpoint`, the abandoned outcome's `resume`) still works.
3107
+ *
3108
+ * An {@link EmbedResumeEnvelope}: fully manual, one-shot. The launch
3109
+ * resumes from exactly this envelope and nothing is persisted.
3110
+ *
3111
+ * An {@link EmbedResumeStore}: delegate persistence (for example to
3112
+ * your backend, for cross-device resume).
3113
+ *
3114
+ * Whatever the policy, the child receives only the opaque `state`
3115
+ * (as `client.parent.resume`) and decides whether to use it; run ids
3116
+ * stay parent-side and drive run continuity when the child accepts.
3117
+ */
3118
+ resume?: false | EmbedResumeEnvelope | EmbedResumeStore;
3119
+ /**
3120
+ * Records the launch on this game's Timeback course: the metadata
3121
+ * you would have given `startActivity()` if your own document were
3122
+ * running the lesson. Stays in the parent SDK, stamping every
3123
+ * forwarded heartbeat and the final completion; it never crosses the
3124
+ * iframe. Omit for a pure UX embed.
3125
+ */
3126
+ timeback?: EmbedTimebackRecording;
3127
+ /**
3128
+ * Overrides the child's resolved deployment URL. Intended for local
3129
+ * development, where the child runs on a dev server the platform
3130
+ * doesn't know about. With `gameUrl` set, a slug that fails to
3131
+ * resolve degrades to a warning (the slug stands in as the child's
3132
+ * game id) instead of failing the launch, so an unregistered child
3133
+ * still launches locally.
3134
+ */
3135
+ gameUrl?: string;
3136
+ }
3137
+
2696
3138
  /**
2697
3139
  * Scores namespace types
2698
3140
  */
@@ -2796,6 +3238,29 @@ interface BucketFile {
2796
3238
  lastModified: string;
2797
3239
  contentType?: string;
2798
3240
  }
3241
+ /**
3242
+ * Options for a single-page bucket listing
3243
+ */
3244
+ interface BucketListPageOptions {
3245
+ /** Restrict results to keys starting with this prefix */
3246
+ prefix?: string;
3247
+ /** Opaque continuation cursor from the previous page's result */
3248
+ cursor?: string;
3249
+ /** Page size (1-1000); the server may return fewer */
3250
+ limit?: number;
3251
+ /** Roll deeper keys into `prefixes` entries, S3 delimiter style */
3252
+ delimiter?: string;
3253
+ }
3254
+ /**
3255
+ * One page of a bucket listing
3256
+ */
3257
+ interface BucketFilePage {
3258
+ files: BucketFile[];
3259
+ /** Rolled-up common prefixes; present for delimiter listings that found any */
3260
+ prefixes?: string[];
3261
+ /** Present only when more pages remain */
3262
+ cursor?: string;
3263
+ }
2799
3264
  /**
2800
3265
  * KV key entry
2801
3266
  */
@@ -3024,6 +3489,19 @@ interface DashboardThemeConfig {
3024
3489
  /** Secondary/accent color as a hex string, e.g. '#ffd166' */
3025
3490
  secondary?: string;
3026
3491
  }
3492
+ /**
3493
+ * Child-catalog contract generation (see @playcademy/sdk/contracts).
3494
+ * The extractor is a repo-local script (run with the CLI's runtime) that
3495
+ * prints `{ generatedFrom, deliveries }` as JSON on stdout; the CLI
3496
+ * validates, stamps the contract fields, and writes
3497
+ * `.playcademy/catalog.json` deterministically. The document carries no
3498
+ * app identity — consuming repos assign the namespace key from sync
3499
+ * provenance.
3500
+ */
3501
+ interface CatalogConfig {
3502
+ /** Path to the extractor script, relative to the config file. */
3503
+ extractor: string;
3504
+ }
3027
3505
  /**
3028
3506
  * Unified Playcademy configuration
3029
3507
  * Used for playcademy.config.{js,json}
@@ -3035,6 +3513,13 @@ interface PlaycademyConfig {
3035
3513
  description?: string;
3036
3514
  /** Game emoji icon */
3037
3515
  emoji?: string;
3516
+ /**
3517
+ * Browser permissions to request for the game's iframe (e.g.
3518
+ * `['microphone']`). Opt-in per game; only `microphone` and `camera` are
3519
+ * delegated this way. Fullscreen, autoplay, and gamepad are granted to
3520
+ * every game automatically.
3521
+ */
3522
+ permissions?: GamePermission[];
3038
3523
  /** Build command to run before deployment */
3039
3524
  buildCommand?: string[];
3040
3525
  /** Path to build output */
@@ -3049,6 +3534,8 @@ interface PlaycademyConfig {
3049
3534
  dashboard?: DashboardConfig | boolean;
3050
3535
  /** Integrations (database, custom routes, external services) */
3051
3536
  integrations?: IntegrationsConfig;
3537
+ /** Child-catalog contract generation (`playcademy catalog`) */
3538
+ catalog?: CatalogConfig;
3052
3539
  }
3053
3540
 
3054
3541
  /**
@@ -3363,6 +3850,7 @@ declare class PlaycademyInternalClient extends PlaycademyBaseClient {
3363
3850
  };
3364
3851
  bucket: {
3365
3852
  list: (slug: string, prefix?: string) => Promise<BucketFile[]>;
3853
+ listPage: (slug: string, options?: BucketListPageOptions) => Promise<BucketFilePage>;
3366
3854
  get: (slug: string, key: string) => Promise<ArrayBuffer>;
3367
3855
  put: (slug: string, key: string, content: Blob | ArrayBuffer | Uint8Array, contentType?: string) => Promise<void>;
3368
3856
  initiateUpload: (slug: string, key: string, contentType: string) => Promise<{
@@ -3534,10 +4022,13 @@ declare class PlaycademyInternalClient extends PlaycademyBaseClient {
3534
4022
  create: (gameId: string, courseId: string, data: {
3535
4023
  title: string;
3536
4024
  purpose: _playcademy_types.AssessmentPurpose;
4025
+ standard?: _playcademy_types.AssessmentStandardRef;
3537
4026
  }) => Promise<_playcademy_types.AssessmentRow>;
4027
+ attachExisting: (gameId: string, courseId: string, manifest: _playcademy_types.AssessmentAssociationImportManifest) => Promise<_playcademy_types.AssessmentAssociationImportResponse>;
3538
4028
  update: (gameId: string, courseId: string, testIdentifier: string, data: {
3539
4029
  title?: string;
3540
4030
  purpose?: _playcademy_types.AssessmentPurpose;
4031
+ standard?: _playcademy_types.AssessmentStandardRef;
3541
4032
  status?: _playcademy_types.AssessmentStatus;
3542
4033
  }) => Promise<_playcademy_types.AssessmentRow>;
3543
4034
  reorder: (gameId: string, courseId: string, purpose: _playcademy_types.AssessmentPurpose, testIdentifiers: string[]) => Promise<{
@@ -3547,7 +4038,7 @@ declare class PlaycademyInternalClient extends PlaycademyBaseClient {
3547
4038
  action: 'discarded' | 'archived';
3548
4039
  }>;
3549
4040
  listTestLibrary: (gameId: string, courseId: string, options?: QtiLibraryQueryOptions) => Promise<_playcademy_types.QtiAssessmentTestListResponse>;
3550
- copy: (gameId: string, courseId: string, testIdentifier: string, purpose: _playcademy_types.AssessmentPurpose) => Promise<_playcademy_types.AssessmentRow>;
4041
+ copy: (gameId: string, courseId: string, testIdentifier: string, purpose: _playcademy_types.AssessmentPurpose, standard?: _playcademy_types.AssessmentStandardRef) => Promise<_playcademy_types.AssessmentRow>;
3551
4042
  listQuestions: (gameId: string, courseId: string, testIdentifier: string) => Promise<_playcademy_types.QtiTestQuestionsResponse>;
3552
4043
  listQuestionLibrary: (gameId: string, courseId: string, options?: QtiLibraryQueryOptions) => Promise<_playcademy_types.QtiAssessmentItemListResponse>;
3553
4044
  createQuestion: (gameId: string, courseId: string, testIdentifier: string, data: _playcademy_types.QtiQuestionCreateInput) => Promise<_playcademy_types.QtiTestQuestionRef>;
@@ -3574,5 +4065,54 @@ declare class PlaycademyInternalClient extends PlaycademyBaseClient {
3574
4065
  };
3575
4066
  }
3576
4067
 
3577
- export { ApiError, MessageEvents, PlaycademyInternalClient as PlaycademyClient, PlaycademyError, PlaycademyInternalClient, extractApiErrorInfo, messaging };
3578
- export type { ApiErrorCode, ApiErrorInfo, AuthCallbackPayload, AuthOptions, AuthProviderType, AuthResult, AuthServerMessage, AuthStateChangePayload, AuthStateUpdate, BetterAuthApiKey, BetterAuthApiKeyResponse, BetterAuthSignInResponse, BucketFile, ClientConfig, ClientEvents, CourseMastery, CourseXp, DemoEndOptions, DemoEndPayload, DevUploadEvent, DevUploadHooks, ErrorResponseBody, 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, MessageEventMap, 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 };
4068
+ /**
4069
+ * @fileoverview Platform INIT Handshake
4070
+ *
4071
+ * The sender half of the INIT handshake between an embedding window (the
4072
+ * hub, or a parent game via `client.embed`) and a game iframe. INIT is
4073
+ * resent on an interval until the game answers READY, which absorbs the
4074
+ * race where the iframe isn't listening yet when the first INIT lands.
4075
+ *
4076
+ * The receiver half lives in `core/static/init.ts` (`PlaycademyClient.init()`).
4077
+ * The three timing constants below describe both halves, so their invariant
4078
+ * is checkable in one place.
4079
+ */
4080
+
4081
+ /** How long a game's `init()` waits for INIT before giving up. */
4082
+ declare const INIT_WAIT_TIMEOUT_MS = 25000;
4083
+ /** How often the embedding window resends INIT until READY arrives. */
4084
+ declare const HANDSHAKE_RESEND_INTERVAL_MS = 300;
4085
+ /**
4086
+ * When the embedding window stops resending and declares the load failed.
4087
+ * Must exceed {@link INIT_WAIT_TIMEOUT_MS} so a struggling game gets to
4088
+ * send INIT_ERROR before the embedder gives up.
4089
+ */
4090
+ declare const HANDSHAKE_MAX_DURATION_MS = 30000;
4091
+ interface BeginInitHandshakeOptions {
4092
+ /** The game iframe (needs `contentWindow` for postMessage). */
4093
+ iframe: HTMLIFrameElement;
4094
+ /** Target origin for postMessage security. */
4095
+ origin: string;
4096
+ /** The INIT payload to send. */
4097
+ payload: GameContextPayload;
4098
+ /** Called once if the game never answers READY within the max duration. */
4099
+ onTimeout: () => void;
4100
+ /**
4101
+ * Called once if INIT cannot be posted at all (an unclonable payload
4102
+ * throws identically on every resend, so retrying cannot succeed).
4103
+ * The handshake is already stopped when this fires.
4104
+ */
4105
+ onSendError?: (error: unknown) => void;
4106
+ }
4107
+ /**
4108
+ * Sends INIT immediately, then resends every
4109
+ * {@link HANDSHAKE_RESEND_INTERVAL_MS} until stopped. The caller stops the
4110
+ * handshake when READY arrives; after {@link HANDSHAKE_MAX_DURATION_MS}
4111
+ * without a stop, `onTimeout` fires once (already stopped).
4112
+ *
4113
+ * @returns A `stop` function that clears the resend interval and the timeout.
4114
+ */
4115
+ declare function beginInitHandshake({ iframe, origin, payload, onTimeout, onSendError }: BeginInitHandshakeOptions): () => void;
4116
+
4117
+ export { ApiError, HANDSHAKE_MAX_DURATION_MS, HANDSHAKE_RESEND_INTERVAL_MS, INIT_WAIT_TIMEOUT_MS, MessageEvents, PlaycademyInternalClient as PlaycademyClient, PlaycademyError, PlaycademyInternalClient, beginInitHandshake, extractApiErrorInfo, isTrustedIframeMessage, messaging };
4118
+ export type { ApiErrorCode, ApiErrorInfo, 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, ErrorResponseBody, 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, MessageEventMap, 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 };