@playcademy/sdk 0.15.1-beta.1 → 0.15.1-beta.3

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.js CHANGED
@@ -580,7 +580,7 @@ async function login(baseUrl, email, password) {
580
580
  async function generateSecureRandomString(length) {
581
581
  const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
582
582
  const randomValues = new Uint8Array(length);
583
- globalThis.crypto.getRandomValues(randomValues);
583
+ crypto.getRandomValues(randomValues);
584
584
  return [...randomValues].map((byte) => charset[byte % charset.length]).join("");
585
585
  }
586
586
 
@@ -2391,7 +2391,7 @@ async function request({
2391
2391
  return rawText && rawText.length > 0 ? rawText : undefined;
2392
2392
  }
2393
2393
  // src/version.ts
2394
- var SDK_VERSION = "0.15.1-beta.1";
2394
+ var SDK_VERSION = "0.15.1-beta.3";
2395
2395
 
2396
2396
  // src/clients/base.ts
2397
2397
  class PlaycademyBaseClient {
@@ -5,8 +5,8 @@ import * as _playcademy_types from '@playcademy/types';
5
5
  import { GameManifest, LocalDayContext } from '@playcademy/types';
6
6
  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
  import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
8
- import { z } from 'zod';
9
8
  import { DomainValidationRecords } from '@playcademy/types/game';
9
+ import { z } from 'zod';
10
10
  import { TimebackUserRole, UserEnrollment, UserOrganization, UserInfo } from '@playcademy/types/user';
11
11
  import { AUTH_PROVIDER_IDS } from '@playcademy/constants';
12
12
 
@@ -1336,6 +1336,277 @@ declare const gameMembers: drizzle_orm_pg_core.PgTableWithColumns<{
1336
1336
  };
1337
1337
  dialect: 'pg';
1338
1338
  }>;
1339
+ /**
1340
+ * Standalone login accounts for a game's developer dashboard.
1341
+ *
1342
+ * These are NOT platform accounts — dashboard users authenticate directly
1343
+ * against their game's dashboard worker with credentials stored here. The
1344
+ * platform owns this table entirely; game devs manage rows only through the
1345
+ * dashboard endpoints (gated by game membership). `platformUserId` is
1346
+ * reserved for a future "sign in with Playcademy" grant path.
1347
+ */
1348
+ declare const gameDashboardUsers: drizzle_orm_pg_core.PgTableWithColumns<{
1349
+ name: "game_dashboard_users";
1350
+ schema: undefined;
1351
+ columns: {
1352
+ id: drizzle_orm_pg_core.PgColumn<{
1353
+ name: "id";
1354
+ tableName: "game_dashboard_users";
1355
+ dataType: "string";
1356
+ columnType: "PgUUID";
1357
+ data: string;
1358
+ driverParam: string;
1359
+ notNull: true;
1360
+ hasDefault: true;
1361
+ isPrimaryKey: true;
1362
+ isAutoincrement: false;
1363
+ hasRuntimeDefault: false;
1364
+ enumValues: undefined;
1365
+ baseColumn: never;
1366
+ identity: undefined;
1367
+ generated: undefined;
1368
+ }, {}, {}>;
1369
+ gameId: drizzle_orm_pg_core.PgColumn<{
1370
+ name: "game_id";
1371
+ tableName: "game_dashboard_users";
1372
+ dataType: "string";
1373
+ columnType: "PgUUID";
1374
+ data: string;
1375
+ driverParam: string;
1376
+ notNull: true;
1377
+ hasDefault: false;
1378
+ isPrimaryKey: false;
1379
+ isAutoincrement: false;
1380
+ hasRuntimeDefault: false;
1381
+ enumValues: undefined;
1382
+ baseColumn: never;
1383
+ identity: undefined;
1384
+ generated: undefined;
1385
+ }, {}, {}>;
1386
+ email: drizzle_orm_pg_core.PgColumn<{
1387
+ name: "email";
1388
+ tableName: "game_dashboard_users";
1389
+ dataType: "string";
1390
+ columnType: "PgText";
1391
+ data: string;
1392
+ driverParam: string;
1393
+ notNull: true;
1394
+ hasDefault: false;
1395
+ isPrimaryKey: false;
1396
+ isAutoincrement: false;
1397
+ hasRuntimeDefault: false;
1398
+ enumValues: [string, ...string[]];
1399
+ baseColumn: never;
1400
+ identity: undefined;
1401
+ generated: undefined;
1402
+ }, {}, {}>;
1403
+ displayName: drizzle_orm_pg_core.PgColumn<{
1404
+ name: "display_name";
1405
+ tableName: "game_dashboard_users";
1406
+ dataType: "string";
1407
+ columnType: "PgText";
1408
+ data: string;
1409
+ driverParam: string;
1410
+ notNull: false;
1411
+ hasDefault: false;
1412
+ isPrimaryKey: false;
1413
+ isAutoincrement: false;
1414
+ hasRuntimeDefault: false;
1415
+ enumValues: [string, ...string[]];
1416
+ baseColumn: never;
1417
+ identity: undefined;
1418
+ generated: undefined;
1419
+ }, {}, {}>;
1420
+ role: drizzle_orm_pg_core.PgColumn<{
1421
+ name: "role";
1422
+ tableName: "game_dashboard_users";
1423
+ dataType: "string";
1424
+ columnType: "PgEnumColumn";
1425
+ data: "admin" | "viewer";
1426
+ driverParam: string;
1427
+ notNull: true;
1428
+ hasDefault: true;
1429
+ isPrimaryKey: false;
1430
+ isAutoincrement: false;
1431
+ hasRuntimeDefault: false;
1432
+ enumValues: ["admin", "viewer"];
1433
+ baseColumn: never;
1434
+ identity: undefined;
1435
+ generated: undefined;
1436
+ }, {}, {}>;
1437
+ passwordHash: drizzle_orm_pg_core.PgColumn<{
1438
+ name: "password_hash";
1439
+ tableName: "game_dashboard_users";
1440
+ dataType: "string";
1441
+ columnType: "PgText";
1442
+ data: string;
1443
+ driverParam: string;
1444
+ notNull: false;
1445
+ hasDefault: false;
1446
+ isPrimaryKey: false;
1447
+ isAutoincrement: false;
1448
+ hasRuntimeDefault: false;
1449
+ enumValues: [string, ...string[]];
1450
+ baseColumn: never;
1451
+ identity: undefined;
1452
+ generated: undefined;
1453
+ }, {}, {}>;
1454
+ inviteTokenHash: drizzle_orm_pg_core.PgColumn<{
1455
+ name: "invite_token_hash";
1456
+ tableName: "game_dashboard_users";
1457
+ dataType: "string";
1458
+ columnType: "PgText";
1459
+ data: string;
1460
+ driverParam: string;
1461
+ notNull: false;
1462
+ hasDefault: false;
1463
+ isPrimaryKey: false;
1464
+ isAutoincrement: false;
1465
+ hasRuntimeDefault: false;
1466
+ enumValues: [string, ...string[]];
1467
+ baseColumn: never;
1468
+ identity: undefined;
1469
+ generated: undefined;
1470
+ }, {}, {}>;
1471
+ inviteExpiresAt: drizzle_orm_pg_core.PgColumn<{
1472
+ name: "invite_expires_at";
1473
+ tableName: "game_dashboard_users";
1474
+ dataType: "date";
1475
+ columnType: "PgTimestamp";
1476
+ data: Date;
1477
+ driverParam: string;
1478
+ notNull: false;
1479
+ hasDefault: false;
1480
+ isPrimaryKey: false;
1481
+ isAutoincrement: false;
1482
+ hasRuntimeDefault: false;
1483
+ enumValues: undefined;
1484
+ baseColumn: never;
1485
+ identity: undefined;
1486
+ generated: undefined;
1487
+ }, {}, {}>;
1488
+ resetTokenHash: drizzle_orm_pg_core.PgColumn<{
1489
+ name: "reset_token_hash";
1490
+ tableName: "game_dashboard_users";
1491
+ dataType: "string";
1492
+ columnType: "PgText";
1493
+ data: string;
1494
+ driverParam: string;
1495
+ notNull: false;
1496
+ hasDefault: false;
1497
+ isPrimaryKey: false;
1498
+ isAutoincrement: false;
1499
+ hasRuntimeDefault: false;
1500
+ enumValues: [string, ...string[]];
1501
+ baseColumn: never;
1502
+ identity: undefined;
1503
+ generated: undefined;
1504
+ }, {}, {}>;
1505
+ resetExpiresAt: drizzle_orm_pg_core.PgColumn<{
1506
+ name: "reset_expires_at";
1507
+ tableName: "game_dashboard_users";
1508
+ dataType: "date";
1509
+ columnType: "PgTimestamp";
1510
+ data: Date;
1511
+ driverParam: string;
1512
+ notNull: false;
1513
+ hasDefault: false;
1514
+ isPrimaryKey: false;
1515
+ isAutoincrement: false;
1516
+ hasRuntimeDefault: false;
1517
+ enumValues: undefined;
1518
+ baseColumn: never;
1519
+ identity: undefined;
1520
+ generated: undefined;
1521
+ }, {}, {}>;
1522
+ failedLoginAttempts: drizzle_orm_pg_core.PgColumn<{
1523
+ name: "failed_login_attempts";
1524
+ tableName: "game_dashboard_users";
1525
+ dataType: "number";
1526
+ columnType: "PgInteger";
1527
+ data: number;
1528
+ driverParam: string | number;
1529
+ notNull: true;
1530
+ hasDefault: true;
1531
+ isPrimaryKey: false;
1532
+ isAutoincrement: false;
1533
+ hasRuntimeDefault: false;
1534
+ enumValues: undefined;
1535
+ baseColumn: never;
1536
+ identity: undefined;
1537
+ generated: undefined;
1538
+ }, {}, {}>;
1539
+ lastFailedLoginAt: drizzle_orm_pg_core.PgColumn<{
1540
+ name: "last_failed_login_at";
1541
+ tableName: "game_dashboard_users";
1542
+ dataType: "date";
1543
+ columnType: "PgTimestamp";
1544
+ data: Date;
1545
+ driverParam: string;
1546
+ notNull: false;
1547
+ hasDefault: false;
1548
+ isPrimaryKey: false;
1549
+ isAutoincrement: false;
1550
+ hasRuntimeDefault: false;
1551
+ enumValues: undefined;
1552
+ baseColumn: never;
1553
+ identity: undefined;
1554
+ generated: undefined;
1555
+ }, {}, {}>;
1556
+ platformUserId: drizzle_orm_pg_core.PgColumn<{
1557
+ name: "platform_user_id";
1558
+ tableName: "game_dashboard_users";
1559
+ dataType: "string";
1560
+ columnType: "PgText";
1561
+ data: string;
1562
+ driverParam: string;
1563
+ notNull: false;
1564
+ hasDefault: false;
1565
+ isPrimaryKey: false;
1566
+ isAutoincrement: false;
1567
+ hasRuntimeDefault: false;
1568
+ enumValues: [string, ...string[]];
1569
+ baseColumn: never;
1570
+ identity: undefined;
1571
+ generated: undefined;
1572
+ }, {}, {}>;
1573
+ createdAt: drizzle_orm_pg_core.PgColumn<{
1574
+ name: "created_at";
1575
+ tableName: "game_dashboard_users";
1576
+ dataType: "date";
1577
+ columnType: "PgTimestamp";
1578
+ data: Date;
1579
+ driverParam: string;
1580
+ notNull: true;
1581
+ hasDefault: true;
1582
+ isPrimaryKey: false;
1583
+ isAutoincrement: false;
1584
+ hasRuntimeDefault: false;
1585
+ enumValues: undefined;
1586
+ baseColumn: never;
1587
+ identity: undefined;
1588
+ generated: undefined;
1589
+ }, {}, {}>;
1590
+ updatedAt: drizzle_orm_pg_core.PgColumn<{
1591
+ name: "updated_at";
1592
+ tableName: "game_dashboard_users";
1593
+ dataType: "date";
1594
+ columnType: "PgTimestamp";
1595
+ data: Date;
1596
+ driverParam: string;
1597
+ notNull: true;
1598
+ hasDefault: true;
1599
+ isPrimaryKey: false;
1600
+ isAutoincrement: false;
1601
+ hasRuntimeDefault: false;
1602
+ enumValues: undefined;
1603
+ baseColumn: never;
1604
+ identity: undefined;
1605
+ generated: undefined;
1606
+ }, {}, {}>;
1607
+ };
1608
+ dialect: 'pg';
1609
+ }>;
1339
1610
  /**
1340
1611
  * Custom hostnames table
1341
1612
  *
@@ -1621,6 +1892,19 @@ declare const UpdateGameMemberRoleSchema: z.ZodObject<{
1621
1892
  }, {
1622
1893
  role: "collaborator" | "owner";
1623
1894
  }>;
1895
+ declare const AddGameDashboardUserSchema: z.ZodObject<{
1896
+ email: z.ZodString;
1897
+ displayName: z.ZodOptional<z.ZodString>;
1898
+ role: z.ZodDefault<z.ZodOptional<z.ZodEnum<["admin", "viewer"]>>>;
1899
+ }, "strict", z.ZodTypeAny, {
1900
+ email: string;
1901
+ displayName?: string | undefined;
1902
+ role: "admin" | "viewer";
1903
+ }, {
1904
+ email: string;
1905
+ displayName?: string | undefined;
1906
+ role?: "admin" | "viewer" | undefined;
1907
+ }>;
1624
1908
 
1625
1909
  type GameRow = typeof games.$inferSelect;
1626
1910
  type BaseGame = Omit<GameRow, 'gameType' | 'deploymentUrl' | 'externalUrl'>;
@@ -1650,12 +1934,25 @@ interface GameMemberSearchResult {
1650
1934
  email: string;
1651
1935
  image: string | null;
1652
1936
  }
1937
+ type GameDashboardUserRow = typeof gameDashboardUsers.$inferSelect;
1938
+ /** Dashboard user shape safe to return from the API (no credential material) */
1939
+ type GameDashboardUserSummary = Pick<GameDashboardUserRow, 'id' | 'gameId' | 'email' | 'displayName' | 'role' | 'platformUserId' | 'createdAt' | 'updatedAt'> & {
1940
+ /** Whether the invite is still pending (password not yet set) */
1941
+ pending: boolean;
1942
+ };
1943
+ /** A freshly-minted credential link: an invite for pending users, a password reset for active ones */
1944
+ interface GameDashboardCredentialLink {
1945
+ user: GameDashboardUserSummary;
1946
+ token: string;
1947
+ kind: 'invite' | 'reset';
1948
+ }
1653
1949
  type GameCustomHostnameRow = typeof gameCustomHostnames.$inferSelect;
1654
1950
 
1655
1951
  type UpsertGameMetadataInput = z.infer<typeof UpsertGameMetadataSchema>;
1656
1952
  type PatchGameMetadataInput = z.infer<typeof PatchGameMetadataSchema>;
1657
1953
  type AddGameMemberInput = z.infer<typeof AddGameMemberSchema>;
1658
1954
  type UpdateGameMemberRoleInput = z.infer<typeof UpdateGameMemberRoleSchema>;
1955
+ type AddGameDashboardUserInput = z.input<typeof AddGameDashboardUserSchema>;
1659
1956
 
1660
1957
  type UserRow = typeof users.$inferSelect;
1661
1958
 
@@ -2730,6 +3027,34 @@ interface IntegrationsConfig {
2730
3027
  /** Queues (optional) */
2731
3028
  queues?: Record<string, QueueConfig | boolean>;
2732
3029
  }
3030
+ /**
3031
+ * Developer dashboard configuration.
3032
+ *
3033
+ * The dashboard is a separate app in the game's repo, deployed as a sibling
3034
+ * worker with `playcademy dashboard deploy`. Auth and session plumbing are
3035
+ * platform-managed; the dashboard app itself is just a static SPA build.
3036
+ */
3037
+ interface DashboardConfig {
3038
+ /** Path to the dashboard app, relative to the config file (default: './dashboard') */
3039
+ directory?: string;
3040
+ /** Build command run inside the dashboard directory (defaults to the workspace package manager's build script) */
3041
+ buildCommand?: string[];
3042
+ /** Path to the dashboard build output, relative to the dashboard directory (default: 'dist') */
3043
+ buildPath?: string;
3044
+ /** Colors for the platform-managed login/invite page (hex); unset uses the built-in palette */
3045
+ theme?: DashboardThemeConfig;
3046
+ }
3047
+ /**
3048
+ * Login-page theming. Two seeds — a primary (button, focus, background
3049
+ * wash) and a secondary accent — the platform derives the full palette
3050
+ * from. Either may be omitted to keep that half of the built-in palette.
3051
+ */
3052
+ interface DashboardThemeConfig {
3053
+ /** Primary color as a hex string, e.g. '#e0245e' */
3054
+ primary?: string;
3055
+ /** Secondary/accent color as a hex string, e.g. '#ffd166' */
3056
+ secondary?: string;
3057
+ }
2733
3058
  /**
2734
3059
  * Unified Playcademy configuration
2735
3060
  * Used for playcademy.config.{js,json}
@@ -2751,6 +3076,8 @@ interface PlaycademyConfig {
2751
3076
  externalUrl?: string;
2752
3077
  /** Game platform */
2753
3078
  platform?: 'web' | 'godot';
3079
+ /** Developer dashboard app (optional) — `true` uses defaults */
3080
+ dashboard?: DashboardConfig | boolean;
2754
3081
  /** Integrations (database, custom routes, external services) */
2755
3082
  integrations?: IntegrationsConfig;
2756
3083
  }
@@ -2824,10 +3151,10 @@ interface PlaycademyServerClientState {
2824
3151
  courseId?: string;
2825
3152
  }
2826
3153
  /**
2827
- * Resource bindings for backend deployment
3154
+ * Resource bindings for worker deployment
2828
3155
  * Provider-agnostic abstraction for cloud resources
2829
3156
  */
2830
- interface BackendResourceBindings {
3157
+ interface WorkerResourceBindings {
2831
3158
  /** SQL database — `true` to request, server names it (maps to D1 on Cloudflare) */
2832
3159
  database?: boolean | string[];
2833
3160
  /** Key-value store — `true` to request, server names it (maps to KV on Cloudflare) */
@@ -2838,15 +3165,15 @@ interface BackendResourceBindings {
2838
3165
  queues?: Record<string, QueueConfig | boolean>;
2839
3166
  }
2840
3167
  /**
2841
- * Backend deployment bundle for uploading to Playcademy platform
3168
+ * Worker deployment bundle for uploading to Playcademy platform
2842
3169
  */
2843
- interface BackendDeploymentBundle {
3170
+ interface WorkerDeploymentBundle {
2844
3171
  /** Bundled JavaScript code ready for deployment */
2845
3172
  code: string;
2846
3173
  /** Game configuration */
2847
3174
  config: PlaycademyConfig;
2848
3175
  /** Optional resource bindings (database, storage, etc.) */
2849
- bindings?: BackendResourceBindings;
3176
+ bindings?: WorkerResourceBindings;
2850
3177
  /** Optional schema information for database setup */
2851
3178
  schema?: SchemaInfo;
2852
3179
  /** Cloudflare Worker compatibility date (YYYY-MM-DD) */
@@ -2996,7 +3323,7 @@ declare class PlaycademyInternalClient extends PlaycademyBaseClient {
2996
3323
  deploy: (slug: string, options: {
2997
3324
  metadata?: UpsertGameMetadataInput;
2998
3325
  file?: File | Blob | null;
2999
- backend?: BackendDeploymentBundle;
3326
+ worker?: WorkerDeploymentBundle;
3000
3327
  hooks?: DevUploadHooks;
3001
3328
  }) => Promise<Game>;
3002
3329
  seed: (slug: string, code: string, environment?: 'local' | 'staging' | 'production', secrets?: Record<string, string>) => Promise<_playcademy_types.SeedResponse>;
@@ -3007,6 +3334,26 @@ declare class PlaycademyInternalClient extends PlaycademyBaseClient {
3007
3334
  list: (slug: string) => Promise<string[]>;
3008
3335
  delete: (slug: string, key: string) => Promise<void>;
3009
3336
  };
3337
+ dashboard: {
3338
+ deploy: (slug: string, options: {
3339
+ file: File | Blob;
3340
+ worker: WorkerDeploymentBundle;
3341
+ hooks?: DevUploadHooks;
3342
+ }) => Promise<{
3343
+ url: string;
3344
+ deploymentId: string;
3345
+ }>;
3346
+ remove: (slug: string) => Promise<void>;
3347
+ users: {
3348
+ list: (slug: string) => Promise<GameDashboardUserSummary[]>;
3349
+ add: (slug: string, input: AddGameDashboardUserInput) => Promise<GameDashboardCredentialLink>;
3350
+ recover: (slug: string, userId: string) => Promise<GameDashboardCredentialLink>;
3351
+ remove: (slug: string, userId: string) => Promise<void>;
3352
+ };
3353
+ sessions: {
3354
+ revoke: (slug: string) => Promise<void>;
3355
+ };
3356
+ };
3010
3357
  database: {
3011
3358
  reset: (slug: string, schema?: {
3012
3359
  sql: string;
package/dist/internal.js CHANGED
@@ -580,7 +580,7 @@ async function login(baseUrl, email, password) {
580
580
  async function generateSecureRandomString(length) {
581
581
  const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
582
582
  const randomValues = new Uint8Array(length);
583
- globalThis.crypto.getRandomValues(randomValues);
583
+ crypto.getRandomValues(randomValues);
584
584
  return [...randomValues].map((byte) => charset[byte % charset.length]).join("");
585
585
  }
586
586
 
@@ -2262,7 +2262,18 @@ class DeployPipeline {
2262
2262
  }
2263
2263
  return uploadToken;
2264
2264
  }
2265
- async submit(args) {
2265
+ async submitGame(args) {
2266
+ await this.submitAndPoll(args);
2267
+ return this.fetchGameWithRetry(args.slug);
2268
+ }
2269
+ async submitDashboard(args) {
2270
+ const result = await this.submitAndPoll({ ...args, target: "dashboard" });
2271
+ if (!result.deploymentId) {
2272
+ throw new Error("Deployment completed but no deployment ID was recorded");
2273
+ }
2274
+ return { url: result.url, deploymentId: result.deploymentId };
2275
+ }
2276
+ async submitAndPoll(args) {
2266
2277
  const { requestBody } = await this.buildRequestBody(args);
2267
2278
  const { slug, hooks } = args;
2268
2279
  const job = await this.client["request"](`/games/${slug}/deploy`, "POST", { body: requestBody });
@@ -2270,44 +2281,50 @@ class DeployPipeline {
2270
2281
  if (!completedJob.result?.url) {
2271
2282
  throw new Error("Deployment completed but no deployment URL was recorded");
2272
2283
  }
2273
- return this.fetchGameWithRetry(slug);
2284
+ return {
2285
+ url: completedJob.result.url,
2286
+ deploymentId: completedJob.result.deploymentId ?? undefined
2287
+ };
2274
2288
  }
2275
2289
  async buildRequestBody(args) {
2276
2290
  const game2 = await this.resolveGame(args.slug, args.game);
2277
2291
  const requestBody = {};
2292
+ if (args.target) {
2293
+ requestBody.target = args.target;
2294
+ }
2278
2295
  if (args.uploadToken) {
2279
2296
  requestBody.uploadToken = args.uploadToken;
2280
2297
  }
2281
2298
  if (args.metadata) {
2282
2299
  requestBody.metadata = args.metadata;
2283
2300
  }
2284
- if (!args.backend) {
2301
+ if (!args.worker) {
2285
2302
  return { game: game2, requestBody };
2286
2303
  }
2287
- const backendFields = {
2288
- config: args.backend.config,
2289
- ...args.backend.bindings ? { bindings: args.backend.bindings } : {},
2290
- ...args.backend.schema ? { schema: args.backend.schema } : {},
2291
- ...args.backend.compatibilityDate ? { compatibilityDate: args.backend.compatibilityDate } : {},
2292
- ...args.backend.compatibilityFlags?.length ? { compatibilityFlags: args.backend.compatibilityFlags } : {}
2304
+ const workerFields = {
2305
+ config: args.worker.config,
2306
+ ...args.worker.bindings ? { bindings: args.worker.bindings } : {},
2307
+ ...args.worker.schema ? { schema: args.worker.schema } : {},
2308
+ ...args.worker.compatibilityDate ? { compatibilityDate: args.worker.compatibilityDate } : {},
2309
+ ...args.worker.compatibilityFlags?.length ? { compatibilityFlags: args.worker.compatibilityFlags } : {}
2293
2310
  };
2294
2311
  const inlineBody = {
2295
2312
  ...requestBody,
2296
- ...backendFields,
2297
- code: args.backend.code
2313
+ ...workerFields,
2314
+ code: args.worker.code
2298
2315
  };
2299
2316
  if (this.serializedSize(inlineBody) <= DeployPipeline.MAX_INLINE_REQUEST_BYTES) {
2300
2317
  return { game: game2, requestBody: inlineBody };
2301
2318
  }
2302
2319
  const skeletonBody = {
2303
2320
  ...requestBody,
2304
- ...backendFields,
2321
+ ...workerFields,
2305
2322
  codeUploadToken: "________placeholder________"
2306
2323
  };
2307
2324
  if (this.serializedSize(skeletonBody) > DeployPipeline.MAX_INLINE_REQUEST_BYTES) {
2308
- throw new Error("Deploy request is too large even after uploading backend code");
2325
+ throw new Error("Deploy request is too large even after uploading worker code");
2309
2326
  }
2310
- skeletonBody.codeUploadToken = await this.uploadCode(game2.id, args.backend.code);
2327
+ skeletonBody.codeUploadToken = await this.uploadCode(game2.id, args.worker.code);
2311
2328
  return { game: game2, requestBody: skeletonBody };
2312
2329
  }
2313
2330
  serializedSize(body) {
@@ -2325,7 +2342,7 @@ class DeployPipeline {
2325
2342
  });
2326
2343
  if (!res.ok) {
2327
2344
  const detail = await parseS3Error(res);
2328
- throw new Error(`Backend code upload failed: ${detail}`);
2345
+ throw new Error(`Worker code upload failed: ${detail}`);
2329
2346
  }
2330
2347
  return uploadToken;
2331
2348
  }
@@ -2431,25 +2448,25 @@ function createDevNamespace(client) {
2431
2448
  games: {
2432
2449
  list: () => client["request"]("/games/accessible", "GET"),
2433
2450
  deploy: async (slug, options) => {
2434
- const { metadata, file, backend, hooks } = options;
2451
+ const { metadata, file, worker, hooks } = options;
2435
2452
  hooks?.onEvent?.({ type: "init" });
2436
2453
  let game2;
2437
2454
  if (metadata) {
2438
2455
  game2 = await client["request"](`/games/${slug}`, "PUT", {
2439
2456
  body: metadata
2440
2457
  });
2441
- if (metadata.gameType === "external" && !file && !backend) {
2458
+ if (metadata.gameType === "external" && !file && !worker) {
2442
2459
  return game2;
2443
2460
  }
2444
2461
  }
2445
2462
  const uploadToken = file ? await deploy.uploadFile(file, game2?.id || slug, hooks) : undefined;
2446
- if (uploadToken || backend) {
2447
- return deploy.submit({ slug, game: game2, uploadToken, metadata, backend, hooks });
2463
+ if (uploadToken || worker) {
2464
+ return deploy.submitGame({ slug, game: game2, uploadToken, metadata, worker, hooks });
2448
2465
  }
2449
2466
  if (game2) {
2450
2467
  return game2;
2451
2468
  }
2452
- throw new Error("No deployment actions specified (need metadata, file, or backend)");
2469
+ throw new Error("No deployment actions specified (need metadata, file, or worker)");
2453
2470
  },
2454
2471
  seed: async (slug, code, environment, secrets) => client["request"](`/games/${slug}/seed`, "POST", {
2455
2472
  body: { code, environment, secrets }
@@ -2469,6 +2486,34 @@ function createDevNamespace(client) {
2469
2486
  await client["request"](`/games/${slug}/secrets/${key}`, "DELETE");
2470
2487
  }
2471
2488
  },
2489
+ dashboard: {
2490
+ deploy: async (slug, options) => {
2491
+ const { file, worker, hooks } = options;
2492
+ hooks?.onEvent?.({ type: "init" });
2493
+ const game2 = await client["request"](`/games/${slug}`, "GET");
2494
+ const uploadToken = await deploy.uploadFile(file, game2.id, hooks);
2495
+ return deploy.submitDashboard({ slug, game: game2, uploadToken, worker, hooks });
2496
+ },
2497
+ remove: async (slug) => {
2498
+ await client["request"](`/games/${slug}/dashboard`, "DELETE");
2499
+ },
2500
+ users: {
2501
+ list: async (slug) => {
2502
+ const result = await client["request"](`/games/${slug}/dashboard/users`, "GET");
2503
+ return result.users;
2504
+ },
2505
+ add: async (slug, input) => client["request"](`/games/${slug}/dashboard/users`, "POST", { body: input }),
2506
+ recover: async (slug, userId) => client["request"](`/games/${slug}/dashboard/users/${userId}/recovery`, "POST"),
2507
+ remove: async (slug, userId) => {
2508
+ await client["request"](`/games/${slug}/dashboard/users/${userId}`, "DELETE");
2509
+ }
2510
+ },
2511
+ sessions: {
2512
+ revoke: async (slug) => {
2513
+ await client["request"](`/games/${slug}/dashboard/sessions/revoke`, "POST");
2514
+ }
2515
+ }
2516
+ },
2472
2517
  database: {
2473
2518
  reset: async (slug, schema) => client["request"](`/games/${slug}/database/reset`, "POST", {
2474
2519
  body: { schema }
@@ -3094,7 +3139,7 @@ async function request({
3094
3139
  return rawText && rawText.length > 0 ? rawText : undefined;
3095
3140
  }
3096
3141
  // src/version.ts
3097
- var SDK_VERSION = "0.15.1-beta.1";
3142
+ var SDK_VERSION = "0.15.1-beta.3";
3098
3143
 
3099
3144
  // src/clients/base.ts
3100
3145
  class PlaycademyBaseClient {
@@ -113,6 +113,34 @@ interface IntegrationsConfig {
113
113
  /** Queues (optional) */
114
114
  queues?: Record<string, QueueConfig | boolean>;
115
115
  }
116
+ /**
117
+ * Developer dashboard configuration.
118
+ *
119
+ * The dashboard is a separate app in the game's repo, deployed as a sibling
120
+ * worker with `playcademy dashboard deploy`. Auth and session plumbing are
121
+ * platform-managed; the dashboard app itself is just a static SPA build.
122
+ */
123
+ interface DashboardConfig {
124
+ /** Path to the dashboard app, relative to the config file (default: './dashboard') */
125
+ directory?: string;
126
+ /** Build command run inside the dashboard directory (defaults to the workspace package manager's build script) */
127
+ buildCommand?: string[];
128
+ /** Path to the dashboard build output, relative to the dashboard directory (default: 'dist') */
129
+ buildPath?: string;
130
+ /** Colors for the platform-managed login/invite page (hex); unset uses the built-in palette */
131
+ theme?: DashboardThemeConfig;
132
+ }
133
+ /**
134
+ * Login-page theming. Two seeds — a primary (button, focus, background
135
+ * wash) and a secondary accent — the platform derives the full palette
136
+ * from. Either may be omitted to keep that half of the built-in palette.
137
+ */
138
+ interface DashboardThemeConfig {
139
+ /** Primary color as a hex string, e.g. '#e0245e' */
140
+ primary?: string;
141
+ /** Secondary/accent color as a hex string, e.g. '#ffd166' */
142
+ secondary?: string;
143
+ }
116
144
  /**
117
145
  * Unified Playcademy configuration
118
146
  * Used for playcademy.config.{js,json}
@@ -134,6 +162,8 @@ interface PlaycademyConfig {
134
162
  externalUrl?: string;
135
163
  /** Game platform */
136
164
  platform?: 'web' | 'godot';
165
+ /** Developer dashboard app (optional) — `true` uses defaults */
166
+ dashboard?: DashboardConfig | boolean;
137
167
  /** Integrations (database, custom routes, external services) */
138
168
  integrations?: IntegrationsConfig;
139
169
  }
@@ -214,10 +244,10 @@ interface RequestContext {
214
244
  }
215
245
 
216
246
  /**
217
- * Resource bindings for backend deployment
247
+ * Resource bindings for worker deployment
218
248
  * Provider-agnostic abstraction for cloud resources
219
249
  */
220
- interface BackendResourceBindings {
250
+ interface WorkerResourceBindings {
221
251
  /** SQL database — `true` to request, server names it (maps to D1 on Cloudflare) */
222
252
  database?: boolean | string[];
223
253
  /** Key-value store — `true` to request, server names it (maps to KV on Cloudflare) */
@@ -228,15 +258,15 @@ interface BackendResourceBindings {
228
258
  queues?: Record<string, QueueConfig | boolean>;
229
259
  }
230
260
  /**
231
- * Backend deployment bundle for uploading to Playcademy platform
261
+ * Worker deployment bundle for uploading to Playcademy platform
232
262
  */
233
- interface BackendDeploymentBundle {
263
+ interface WorkerDeploymentBundle {
234
264
  /** Bundled JavaScript code ready for deployment */
235
265
  code: string;
236
266
  /** Game configuration */
237
267
  config: PlaycademyConfig;
238
268
  /** Optional resource bindings (database, storage, etc.) */
239
- bindings?: BackendResourceBindings;
269
+ bindings?: WorkerResourceBindings;
240
270
  /** Optional schema information for database setup */
241
271
  schema?: SchemaInfo;
242
272
  /** Cloudflare Worker compatibility date (YYYY-MM-DD) */
@@ -395,4 +425,4 @@ declare function verifyGameToken(gameToken: string, options?: {
395
425
  }): Promise<VerifyGameTokenResponse>;
396
426
 
397
427
  export { PlaycademyClient, verifyGameToken };
398
- export type { BackendDeploymentBundle, BackendResourceBindings, IntegrationsConfig, PlaycademyConfig, PlaycademyServerClientConfig, PlaycademyServerClientState, QueueConfig, RequestContext, TimebackBaseConfig, TimebackCourseConfigWithOverrides, TimebackIntegrationConfig };
428
+ export type { IntegrationsConfig, PlaycademyConfig, PlaycademyServerClientConfig, PlaycademyServerClientState, QueueConfig, RequestContext, TimebackBaseConfig, TimebackCourseConfigWithOverrides, TimebackIntegrationConfig, WorkerDeploymentBundle, WorkerResourceBindings };
@@ -234,7 +234,7 @@ function extractApiErrorInfo(error) {
234
234
  }
235
235
 
236
236
  // src/version.ts
237
- var SDK_VERSION = "0.15.1-beta.1";
237
+ var SDK_VERSION = "0.15.1-beta.3";
238
238
 
239
239
  // src/server/request.ts
240
240
  async function makeApiRequest(opts) {
package/dist/server.d.ts CHANGED
@@ -113,6 +113,34 @@ interface IntegrationsConfig {
113
113
  /** Queues (optional) */
114
114
  queues?: Record<string, QueueConfig | boolean>;
115
115
  }
116
+ /**
117
+ * Developer dashboard configuration.
118
+ *
119
+ * The dashboard is a separate app in the game's repo, deployed as a sibling
120
+ * worker with `playcademy dashboard deploy`. Auth and session plumbing are
121
+ * platform-managed; the dashboard app itself is just a static SPA build.
122
+ */
123
+ interface DashboardConfig {
124
+ /** Path to the dashboard app, relative to the config file (default: './dashboard') */
125
+ directory?: string;
126
+ /** Build command run inside the dashboard directory (defaults to the workspace package manager's build script) */
127
+ buildCommand?: string[];
128
+ /** Path to the dashboard build output, relative to the dashboard directory (default: 'dist') */
129
+ buildPath?: string;
130
+ /** Colors for the platform-managed login/invite page (hex); unset uses the built-in palette */
131
+ theme?: DashboardThemeConfig;
132
+ }
133
+ /**
134
+ * Login-page theming. Two seeds — a primary (button, focus, background
135
+ * wash) and a secondary accent — the platform derives the full palette
136
+ * from. Either may be omitted to keep that half of the built-in palette.
137
+ */
138
+ interface DashboardThemeConfig {
139
+ /** Primary color as a hex string, e.g. '#e0245e' */
140
+ primary?: string;
141
+ /** Secondary/accent color as a hex string, e.g. '#ffd166' */
142
+ secondary?: string;
143
+ }
116
144
  /**
117
145
  * Unified Playcademy configuration
118
146
  * Used for playcademy.config.{js,json}
@@ -134,6 +162,8 @@ interface PlaycademyConfig {
134
162
  externalUrl?: string;
135
163
  /** Game platform */
136
164
  platform?: 'web' | 'godot';
165
+ /** Developer dashboard app (optional) — `true` uses defaults */
166
+ dashboard?: DashboardConfig | boolean;
137
167
  /** Integrations (database, custom routes, external services) */
138
168
  integrations?: IntegrationsConfig;
139
169
  }
@@ -214,10 +244,10 @@ interface RequestContext {
214
244
  }
215
245
 
216
246
  /**
217
- * Resource bindings for backend deployment
247
+ * Resource bindings for worker deployment
218
248
  * Provider-agnostic abstraction for cloud resources
219
249
  */
220
- interface BackendResourceBindings {
250
+ interface WorkerResourceBindings {
221
251
  /** SQL database — `true` to request, server names it (maps to D1 on Cloudflare) */
222
252
  database?: boolean | string[];
223
253
  /** Key-value store — `true` to request, server names it (maps to KV on Cloudflare) */
@@ -228,15 +258,15 @@ interface BackendResourceBindings {
228
258
  queues?: Record<string, QueueConfig | boolean>;
229
259
  }
230
260
  /**
231
- * Backend deployment bundle for uploading to Playcademy platform
261
+ * Worker deployment bundle for uploading to Playcademy platform
232
262
  */
233
- interface BackendDeploymentBundle {
263
+ interface WorkerDeploymentBundle {
234
264
  /** Bundled JavaScript code ready for deployment */
235
265
  code: string;
236
266
  /** Game configuration */
237
267
  config: PlaycademyConfig;
238
268
  /** Optional resource bindings (database, storage, etc.) */
239
- bindings?: BackendResourceBindings;
269
+ bindings?: WorkerResourceBindings;
240
270
  /** Optional schema information for database setup */
241
271
  schema?: SchemaInfo;
242
272
  /** Cloudflare Worker compatibility date (YYYY-MM-DD) */
@@ -441,4 +471,4 @@ declare function verifyGameToken(gameToken: string, options?: {
441
471
  }): Promise<VerifyGameTokenResponse>;
442
472
 
443
473
  export { PlaycademyClient, verifyGameToken };
444
- export type { BackendDeploymentBundle, BackendResourceBindings, IntegrationsConfig, PlaycademyConfig, PlaycademyServerClientConfig, PlaycademyServerClientState, QueueConfig, TimebackBaseConfig, TimebackCourseConfigWithOverrides, TimebackIntegrationConfig };
474
+ export type { DashboardConfig, IntegrationsConfig, PlaycademyConfig, PlaycademyServerClientConfig, PlaycademyServerClientState, QueueConfig, TimebackBaseConfig, TimebackCourseConfigWithOverrides, TimebackIntegrationConfig, WorkerDeploymentBundle, WorkerResourceBindings };
package/dist/server.js CHANGED
@@ -423,7 +423,7 @@ function extractApiErrorInfo(error) {
423
423
  }
424
424
 
425
425
  // src/version.ts
426
- var SDK_VERSION = "0.15.1-beta.1";
426
+ var SDK_VERSION = "0.15.1-beta.3";
427
427
 
428
428
  // src/server/request.ts
429
429
  async function makeApiRequest(opts) {
package/dist/types.d.ts CHANGED
@@ -6,8 +6,8 @@ export { QtiTestQuestionRef, QtiTestQuestionsResponse } from '@playcademy/types/
6
6
  import { TimebackUserRole, UserEnrollment, UserOrganization, UserInfo } from '@playcademy/types/user';
7
7
  import { AUTH_PROVIDER_IDS } from '@playcademy/constants';
8
8
  import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
9
- import { z } from 'zod';
10
9
  import { DomainValidationRecords } from '@playcademy/types/game';
10
+ import { z } from 'zod';
11
11
 
12
12
  /**
13
13
  * OAuth 2.0 implementation for the Playcademy SDK
@@ -307,6 +307,34 @@ interface IntegrationsConfig {
307
307
  /** Queues (optional) */
308
308
  queues?: Record<string, QueueConfig | boolean>;
309
309
  }
310
+ /**
311
+ * Developer dashboard configuration.
312
+ *
313
+ * The dashboard is a separate app in the game's repo, deployed as a sibling
314
+ * worker with `playcademy dashboard deploy`. Auth and session plumbing are
315
+ * platform-managed; the dashboard app itself is just a static SPA build.
316
+ */
317
+ interface DashboardConfig {
318
+ /** Path to the dashboard app, relative to the config file (default: './dashboard') */
319
+ directory?: string;
320
+ /** Build command run inside the dashboard directory (defaults to the workspace package manager's build script) */
321
+ buildCommand?: string[];
322
+ /** Path to the dashboard build output, relative to the dashboard directory (default: 'dist') */
323
+ buildPath?: string;
324
+ /** Colors for the platform-managed login/invite page (hex); unset uses the built-in palette */
325
+ theme?: DashboardThemeConfig;
326
+ }
327
+ /**
328
+ * Login-page theming. Two seeds — a primary (button, focus, background
329
+ * wash) and a secondary accent — the platform derives the full palette
330
+ * from. Either may be omitted to keep that half of the built-in palette.
331
+ */
332
+ interface DashboardThemeConfig {
333
+ /** Primary color as a hex string, e.g. '#e0245e' */
334
+ primary?: string;
335
+ /** Secondary/accent color as a hex string, e.g. '#ffd166' */
336
+ secondary?: string;
337
+ }
310
338
  /**
311
339
  * Unified Playcademy configuration
312
340
  * Used for playcademy.config.{js,json}
@@ -328,6 +356,8 @@ interface PlaycademyConfig {
328
356
  externalUrl?: string;
329
357
  /** Game platform */
330
358
  platform?: 'web' | 'godot';
359
+ /** Developer dashboard app (optional) — `true` uses defaults */
360
+ dashboard?: DashboardConfig | boolean;
331
361
  /** Integrations (database, custom routes, external services) */
332
362
  integrations?: IntegrationsConfig;
333
363
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcademy/sdk",
3
- "version": "0.15.1-beta.1",
3
+ "version": "0.15.1-beta.3",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {