@playcademy/sandbox 0.3.17-beta.3 → 0.3.17-beta.5
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/cli.js +486 -397
- package/dist/constants.js +1 -1
- package/dist/server.js +486 -397
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -439,7 +439,7 @@ var init_timeback2 = __esm(() => {
|
|
|
439
439
|
});
|
|
440
440
|
|
|
441
441
|
// ../constants/src/workers.ts
|
|
442
|
-
var WORKER_NAMING, SECRETS_PREFIX = "secrets_";
|
|
442
|
+
var WORKER_NAMING, SECRETS_PREFIX = "secrets_", CLOUDFLARE_COMPATIBILITY_DATE = "2025-10-11";
|
|
443
443
|
var init_workers = __esm(() => {
|
|
444
444
|
WORKER_NAMING = {
|
|
445
445
|
STAGING_PREFIX: "staging-",
|
|
@@ -1309,7 +1309,7 @@ var package_default;
|
|
|
1309
1309
|
var init_package = __esm(() => {
|
|
1310
1310
|
package_default = {
|
|
1311
1311
|
name: "@playcademy/sandbox",
|
|
1312
|
-
version: "0.3.17-beta.
|
|
1312
|
+
version: "0.3.17-beta.5",
|
|
1313
1313
|
description: "Local development server for Playcademy game development",
|
|
1314
1314
|
type: "module",
|
|
1315
1315
|
exports: {
|
|
@@ -23743,13 +23743,11 @@ var init_dedent = __esm(() => {
|
|
|
23743
23743
|
});
|
|
23744
23744
|
|
|
23745
23745
|
// ../cloudflare/src/core/namespaces/workers.ts
|
|
23746
|
-
var DEFAULT_COMPATIBILITY_DATE;
|
|
23747
23746
|
var init_workers2 = __esm(() => {
|
|
23748
23747
|
init_dedent();
|
|
23749
23748
|
init_src2();
|
|
23750
23749
|
init_assets();
|
|
23751
23750
|
init_multipart();
|
|
23752
|
-
DEFAULT_COMPATIBILITY_DATE = new Date().toISOString().slice(0, 10);
|
|
23753
23751
|
});
|
|
23754
23752
|
|
|
23755
23753
|
// ../cloudflare/src/core/namespaces/index.ts
|
|
@@ -23771,10 +23769,9 @@ var init_core = __esm(() => {
|
|
|
23771
23769
|
});
|
|
23772
23770
|
|
|
23773
23771
|
// ../cloudflare/src/playcademy/constants.ts
|
|
23774
|
-
var CUSTOM_DOMAINS_KV_NAME = "cademy-custom-domains", QUEUE_NAME_PREFIX = "playcademy",
|
|
23772
|
+
var CUSTOM_DOMAINS_KV_NAME = "cademy-custom-domains", QUEUE_NAME_PREFIX = "playcademy", GAME_WORKER_DOMAIN_PRODUCTION, GAME_WORKER_DOMAIN_STAGING;
|
|
23775
23773
|
var init_constants2 = __esm(() => {
|
|
23776
23774
|
init_src();
|
|
23777
|
-
DEFAULT_COMPATIBILITY_DATE2 = new Date().toISOString().slice(0, 10);
|
|
23778
23775
|
GAME_WORKER_DOMAIN_PRODUCTION = GAME_WORKER_DOMAINS.production;
|
|
23779
23776
|
GAME_WORKER_DOMAIN_STAGING = GAME_WORKER_DOMAINS.staging;
|
|
23780
23777
|
});
|
|
@@ -26478,6 +26475,7 @@ class DeployService {
|
|
|
26478
26475
|
try {
|
|
26479
26476
|
result = await this.timeStep("Cloudflare deploy", () => cf.deploy(deploymentId, request.code, env, {
|
|
26480
26477
|
...deploymentOptions,
|
|
26478
|
+
compatibilityDate: request.compatibilityDate ?? CLOUDFLARE_COMPATIBILITY_DATE,
|
|
26481
26479
|
compatibilityFlags: request.compatibilityFlags,
|
|
26482
26480
|
existingResources: activeDeployment?.resources ?? undefined,
|
|
26483
26481
|
assetsPath: frontendAssetsPath,
|
|
@@ -26579,6 +26577,7 @@ var logger3;
|
|
|
26579
26577
|
var init_deploy_service = __esm(() => {
|
|
26580
26578
|
init_drizzle_orm();
|
|
26581
26579
|
init_playcademy();
|
|
26580
|
+
init_src();
|
|
26582
26581
|
init_tables_index();
|
|
26583
26582
|
init_src2();
|
|
26584
26583
|
init_config2();
|
|
@@ -26647,447 +26646,533 @@ var init_developer_service = __esm(() => {
|
|
|
26647
26646
|
logger4 = log.scope("DeveloperService");
|
|
26648
26647
|
});
|
|
26649
26648
|
|
|
26650
|
-
// ../
|
|
26651
|
-
|
|
26652
|
-
|
|
26653
|
-
|
|
26654
|
-
static MAX_FETCH_ERROR_MESSAGE_LENGTH = 512;
|
|
26655
|
-
constructor(deps) {
|
|
26656
|
-
this.deps = deps;
|
|
26657
|
-
}
|
|
26658
|
-
static getManifestHost(manifestUrl) {
|
|
26659
|
-
try {
|
|
26660
|
-
return new URL(manifestUrl).host;
|
|
26661
|
-
} catch {
|
|
26662
|
-
return manifestUrl;
|
|
26663
|
-
}
|
|
26649
|
+
// ../utils/src/fns.ts
|
|
26650
|
+
function sleep(ms) {
|
|
26651
|
+
if (ms <= 0) {
|
|
26652
|
+
return Promise.resolve();
|
|
26664
26653
|
}
|
|
26665
|
-
|
|
26666
|
-
|
|
26667
|
-
|
|
26668
|
-
|
|
26669
|
-
|
|
26670
|
-
|
|
26671
|
-
|
|
26672
|
-
|
|
26673
|
-
|
|
26654
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
26655
|
+
}
|
|
26656
|
+
|
|
26657
|
+
// ../api-core/src/services/game.service.ts
|
|
26658
|
+
var logger5, inFlightManifestFetches, GameService;
|
|
26659
|
+
var init_game_service = __esm(() => {
|
|
26660
|
+
init_drizzle_orm();
|
|
26661
|
+
init_tables_index();
|
|
26662
|
+
init_src2();
|
|
26663
|
+
init_errors();
|
|
26664
|
+
init_deployment_util();
|
|
26665
|
+
logger5 = log.scope("GameService");
|
|
26666
|
+
inFlightManifestFetches = new Map;
|
|
26667
|
+
GameService = class GameService {
|
|
26668
|
+
deps;
|
|
26669
|
+
static MANIFEST_FETCH_ATTEMPT_TIMEOUT_MS = 1e4;
|
|
26670
|
+
static MANIFEST_FETCH_MAX_RETRIES = 2;
|
|
26671
|
+
static MANIFEST_FETCH_RETRY_BACKOFF_MS = [250, 750];
|
|
26672
|
+
static MANIFEST_CACHE_TTL_SECONDS = 60;
|
|
26673
|
+
static MANIFEST_CACHE_KEY_PREFIX = "game:manifest";
|
|
26674
|
+
static MAX_FETCH_ERROR_MESSAGE_LENGTH = 512;
|
|
26675
|
+
constructor(deps) {
|
|
26676
|
+
this.deps = deps;
|
|
26677
|
+
}
|
|
26678
|
+
static getManifestHost(manifestUrl) {
|
|
26679
|
+
try {
|
|
26680
|
+
return new URL(manifestUrl).host;
|
|
26681
|
+
} catch {
|
|
26682
|
+
return manifestUrl;
|
|
26683
|
+
}
|
|
26674
26684
|
}
|
|
26675
|
-
|
|
26676
|
-
|
|
26677
|
-
|
|
26685
|
+
static getFetchErrorMessage(error) {
|
|
26686
|
+
let raw;
|
|
26687
|
+
if (error instanceof Error) {
|
|
26688
|
+
raw = error.message;
|
|
26689
|
+
} else if (typeof error === "string") {
|
|
26690
|
+
raw = error;
|
|
26691
|
+
}
|
|
26692
|
+
if (!raw) {
|
|
26693
|
+
return;
|
|
26694
|
+
}
|
|
26695
|
+
const normalized = raw.replace(/\s+/g, " ").trim();
|
|
26696
|
+
if (!normalized) {
|
|
26697
|
+
return;
|
|
26698
|
+
}
|
|
26699
|
+
return normalized.slice(0, GameService.MAX_FETCH_ERROR_MESSAGE_LENGTH);
|
|
26678
26700
|
}
|
|
26679
|
-
|
|
26680
|
-
|
|
26681
|
-
static isRetryableStatus(status) {
|
|
26682
|
-
return status === 429 || status >= 500;
|
|
26683
|
-
}
|
|
26684
|
-
async list(caller) {
|
|
26685
|
-
const db2 = this.deps.db;
|
|
26686
|
-
const isAdmin = caller?.role === "admin";
|
|
26687
|
-
const isDeveloper = caller?.role === "developer";
|
|
26688
|
-
let whereClause;
|
|
26689
|
-
if (isAdmin) {
|
|
26690
|
-
whereClause = undefined;
|
|
26691
|
-
} else if (isDeveloper && caller?.id) {
|
|
26692
|
-
whereClause = or(ne(games.visibility, "internal"), eq(games.developerId, caller.id));
|
|
26693
|
-
} else {
|
|
26694
|
-
whereClause = ne(games.visibility, "internal");
|
|
26701
|
+
static isRetryableStatus(status) {
|
|
26702
|
+
return status === 429 || status >= 500;
|
|
26695
26703
|
}
|
|
26696
|
-
|
|
26697
|
-
|
|
26698
|
-
|
|
26699
|
-
|
|
26700
|
-
}
|
|
26701
|
-
async listManageable(user) {
|
|
26702
|
-
this.validateDeveloperStatus(user);
|
|
26703
|
-
const db2 = this.deps.db;
|
|
26704
|
-
return db2.query.games.findMany({
|
|
26705
|
-
where: user.role === "admin" ? undefined : eq(games.developerId, user.id),
|
|
26706
|
-
orderBy: [desc(games.createdAt)]
|
|
26707
|
-
});
|
|
26708
|
-
}
|
|
26709
|
-
async getSubjects() {
|
|
26710
|
-
const db2 = this.deps.db;
|
|
26711
|
-
const integrations = await db2.query.gameTimebackIntegrations.findMany({
|
|
26712
|
-
columns: { gameId: true, subject: true },
|
|
26713
|
-
orderBy: [asc(gameTimebackIntegrations.createdAt)]
|
|
26714
|
-
});
|
|
26715
|
-
const subjectMap = {};
|
|
26716
|
-
for (const integration of integrations) {
|
|
26717
|
-
if (!(integration.gameId in subjectMap)) {
|
|
26718
|
-
subjectMap[integration.gameId] = integration.subject;
|
|
26704
|
+
static getRetryBackoffMs(attemptIndex) {
|
|
26705
|
+
const backoff = GameService.MANIFEST_FETCH_RETRY_BACKOFF_MS;
|
|
26706
|
+
if (backoff.length === 0) {
|
|
26707
|
+
return 0;
|
|
26719
26708
|
}
|
|
26709
|
+
return backoff[Math.min(attemptIndex, backoff.length - 1)] ?? 0;
|
|
26720
26710
|
}
|
|
26721
|
-
|
|
26722
|
-
|
|
26723
|
-
async getById(gameId, caller) {
|
|
26724
|
-
const db2 = this.deps.db;
|
|
26725
|
-
const game = await db2.query.games.findFirst({
|
|
26726
|
-
where: eq(games.id, gameId)
|
|
26727
|
-
});
|
|
26728
|
-
if (!game) {
|
|
26729
|
-
throw new NotFoundError("Game", gameId);
|
|
26711
|
+
static normalizeDeploymentUrl(deploymentUrl) {
|
|
26712
|
+
return deploymentUrl.replace(/\/$/, "");
|
|
26730
26713
|
}
|
|
26731
|
-
|
|
26732
|
-
|
|
26733
|
-
}
|
|
26734
|
-
async getBySlug(slug, caller) {
|
|
26735
|
-
const db2 = this.deps.db;
|
|
26736
|
-
const game = await db2.query.games.findFirst({
|
|
26737
|
-
where: eq(games.slug, slug)
|
|
26738
|
-
});
|
|
26739
|
-
if (!game) {
|
|
26740
|
-
throw new NotFoundError("Game", slug);
|
|
26714
|
+
static getManifestCacheKey(deploymentUrl) {
|
|
26715
|
+
return `${GameService.MANIFEST_CACHE_KEY_PREFIX}:${deploymentUrl}`;
|
|
26741
26716
|
}
|
|
26742
|
-
|
|
26743
|
-
|
|
26744
|
-
|
|
26745
|
-
|
|
26746
|
-
|
|
26747
|
-
|
|
26748
|
-
|
|
26749
|
-
|
|
26750
|
-
|
|
26751
|
-
|
|
26752
|
-
|
|
26753
|
-
|
|
26754
|
-
|
|
26755
|
-
|
|
26756
|
-
|
|
26757
|
-
|
|
26758
|
-
manifestUrl,
|
|
26759
|
-
manifestHost,
|
|
26760
|
-
deploymentUrl,
|
|
26761
|
-
fetchOutcome,
|
|
26762
|
-
retryCount: 0,
|
|
26763
|
-
durationMs: Date.now() - startedAt,
|
|
26764
|
-
manifestErrorKind,
|
|
26765
|
-
...extra
|
|
26766
|
-
};
|
|
26717
|
+
async list(caller) {
|
|
26718
|
+
const db2 = this.deps.db;
|
|
26719
|
+
const isAdmin = caller?.role === "admin";
|
|
26720
|
+
const isDeveloper = caller?.role === "developer";
|
|
26721
|
+
let whereClause;
|
|
26722
|
+
if (isAdmin) {
|
|
26723
|
+
whereClause = undefined;
|
|
26724
|
+
} else if (isDeveloper && caller?.id) {
|
|
26725
|
+
whereClause = or(ne(games.visibility, "internal"), eq(games.developerId, caller.id));
|
|
26726
|
+
} else {
|
|
26727
|
+
whereClause = ne(games.visibility, "internal");
|
|
26728
|
+
}
|
|
26729
|
+
return db2.query.games.findMany({
|
|
26730
|
+
where: whereClause,
|
|
26731
|
+
orderBy: [desc(games.createdAt)]
|
|
26732
|
+
});
|
|
26767
26733
|
}
|
|
26768
|
-
|
|
26769
|
-
|
|
26770
|
-
|
|
26771
|
-
|
|
26772
|
-
|
|
26773
|
-
|
|
26774
|
-
},
|
|
26775
|
-
signal: controller.signal
|
|
26734
|
+
async listManageable(user) {
|
|
26735
|
+
this.validateDeveloperStatus(user);
|
|
26736
|
+
const db2 = this.deps.db;
|
|
26737
|
+
return db2.query.games.findMany({
|
|
26738
|
+
where: user.role === "admin" ? undefined : eq(games.developerId, user.id),
|
|
26739
|
+
orderBy: [desc(games.createdAt)]
|
|
26776
26740
|
});
|
|
26777
|
-
}
|
|
26778
|
-
|
|
26779
|
-
const
|
|
26780
|
-
const
|
|
26781
|
-
|
|
26782
|
-
|
|
26783
|
-
manifestUrl,
|
|
26784
|
-
error,
|
|
26785
|
-
details
|
|
26741
|
+
}
|
|
26742
|
+
async getSubjects() {
|
|
26743
|
+
const db2 = this.deps.db;
|
|
26744
|
+
const integrations = await db2.query.gameTimebackIntegrations.findMany({
|
|
26745
|
+
columns: { gameId: true, subject: true },
|
|
26746
|
+
orderBy: [asc(gameTimebackIntegrations.createdAt)]
|
|
26786
26747
|
});
|
|
26787
|
-
|
|
26788
|
-
|
|
26748
|
+
const subjectMap = {};
|
|
26749
|
+
for (const integration of integrations) {
|
|
26750
|
+
if (!(integration.gameId in subjectMap)) {
|
|
26751
|
+
subjectMap[integration.gameId] = integration.subject;
|
|
26752
|
+
}
|
|
26789
26753
|
}
|
|
26790
|
-
|
|
26791
|
-
} finally {
|
|
26792
|
-
clearTimeout(timeout);
|
|
26754
|
+
return subjectMap;
|
|
26793
26755
|
}
|
|
26794
|
-
|
|
26795
|
-
const
|
|
26796
|
-
const
|
|
26797
|
-
|
|
26798
|
-
const details = buildDetails("bad_status", manifestErrorKind, {
|
|
26799
|
-
manifestUrl: resolvedManifestUrl,
|
|
26800
|
-
manifestHost: resolvedManifestHost,
|
|
26801
|
-
status: response.status,
|
|
26802
|
-
contentType: response.headers.get("content-type") ?? undefined,
|
|
26803
|
-
cfRay: response.headers.get("cf-ray") ?? undefined,
|
|
26804
|
-
redirected: response.redirected,
|
|
26805
|
-
...response.redirected ? {
|
|
26806
|
-
originalManifestUrl: manifestUrl,
|
|
26807
|
-
originalManifestHost: manifestHost
|
|
26808
|
-
} : {}
|
|
26809
|
-
});
|
|
26810
|
-
const message = `Failed to fetch manifest: ${response.status} ${response.statusText}`;
|
|
26811
|
-
logger5.error("Game manifest returned non-ok response", {
|
|
26812
|
-
gameId,
|
|
26813
|
-
manifestUrl,
|
|
26814
|
-
status: response.status,
|
|
26815
|
-
details
|
|
26756
|
+
async getById(gameId, caller) {
|
|
26757
|
+
const db2 = this.deps.db;
|
|
26758
|
+
const game = await db2.query.games.findFirst({
|
|
26759
|
+
where: eq(games.id, gameId)
|
|
26816
26760
|
});
|
|
26817
|
-
if (
|
|
26818
|
-
throw new
|
|
26761
|
+
if (!game) {
|
|
26762
|
+
throw new NotFoundError("Game", gameId);
|
|
26819
26763
|
}
|
|
26820
|
-
|
|
26764
|
+
this.enforceVisibility(game, caller, gameId);
|
|
26765
|
+
return game;
|
|
26821
26766
|
}
|
|
26822
|
-
|
|
26823
|
-
|
|
26824
|
-
|
|
26825
|
-
|
|
26826
|
-
const resolvedManifestHost = GameService.getManifestHost(resolvedManifestUrl);
|
|
26827
|
-
const details = buildDetails("invalid_body", "permanent", {
|
|
26828
|
-
manifestUrl: resolvedManifestUrl,
|
|
26829
|
-
manifestHost: resolvedManifestHost,
|
|
26830
|
-
status: response.status,
|
|
26831
|
-
contentType: response.headers.get("content-type") ?? undefined,
|
|
26832
|
-
cfRay: response.headers.get("cf-ray") ?? undefined,
|
|
26833
|
-
redirected: response.redirected,
|
|
26834
|
-
...response.redirected ? {
|
|
26835
|
-
originalManifestUrl: manifestUrl,
|
|
26836
|
-
originalManifestHost: manifestHost
|
|
26837
|
-
} : {}
|
|
26838
|
-
});
|
|
26839
|
-
logger5.error("Failed to parse game manifest", {
|
|
26840
|
-
gameId,
|
|
26841
|
-
manifestUrl,
|
|
26842
|
-
error,
|
|
26843
|
-
details
|
|
26767
|
+
async getBySlug(slug, caller) {
|
|
26768
|
+
const db2 = this.deps.db;
|
|
26769
|
+
const game = await db2.query.games.findFirst({
|
|
26770
|
+
where: eq(games.slug, slug)
|
|
26844
26771
|
});
|
|
26845
|
-
|
|
26846
|
-
|
|
26847
|
-
}
|
|
26848
|
-
enforceVisibility(game, caller, lookupIdentifier) {
|
|
26849
|
-
if (game.visibility !== "internal") {
|
|
26850
|
-
return;
|
|
26851
|
-
}
|
|
26852
|
-
const isAdmin = caller?.role === "admin";
|
|
26853
|
-
const isOwner = caller?.id != null && caller.id === game.developerId;
|
|
26854
|
-
if (!isAdmin && !isOwner) {
|
|
26855
|
-
throw new NotFoundError("Game", lookupIdentifier);
|
|
26856
|
-
}
|
|
26857
|
-
}
|
|
26858
|
-
async upsertBySlug(slug, data, user) {
|
|
26859
|
-
const db2 = this.deps.db;
|
|
26860
|
-
const existingGame = await db2.query.games.findFirst({
|
|
26861
|
-
where: eq(games.slug, slug)
|
|
26862
|
-
});
|
|
26863
|
-
const isUpdate = Boolean(existingGame);
|
|
26864
|
-
const gameId = existingGame?.id ?? crypto.randomUUID();
|
|
26865
|
-
if (isUpdate) {
|
|
26866
|
-
await this.validateDeveloperAccess(user, gameId);
|
|
26867
|
-
} else {
|
|
26868
|
-
this.validateDeveloperStatus(user);
|
|
26869
|
-
}
|
|
26870
|
-
const gameDataForDb = {
|
|
26871
|
-
displayName: data.displayName,
|
|
26872
|
-
platform: data.platform,
|
|
26873
|
-
metadata: data.metadata,
|
|
26874
|
-
mapElementId: data.mapElementId,
|
|
26875
|
-
gameType: data.gameType,
|
|
26876
|
-
...data.visibility && { visibility: data.visibility },
|
|
26877
|
-
externalUrl: data.externalUrl || null,
|
|
26878
|
-
updatedAt: new Date
|
|
26879
|
-
};
|
|
26880
|
-
let gameResponse;
|
|
26881
|
-
if (isUpdate) {
|
|
26882
|
-
const [updatedGame] = await db2.update(games).set(gameDataForDb).where(eq(games.id, gameId)).returning();
|
|
26883
|
-
if (!updatedGame) {
|
|
26884
|
-
logger5.error("Game update returned no rows", { gameId, slug });
|
|
26885
|
-
throw new InternalError("DB update failed to return result for existing game");
|
|
26886
|
-
}
|
|
26887
|
-
gameResponse = updatedGame;
|
|
26888
|
-
} else {
|
|
26889
|
-
const insertData = {
|
|
26890
|
-
...gameDataForDb,
|
|
26891
|
-
id: gameId,
|
|
26892
|
-
slug,
|
|
26893
|
-
developerId: user.id,
|
|
26894
|
-
metadata: data.metadata || {},
|
|
26895
|
-
version: data.gameType === "external" ? "external" : "",
|
|
26896
|
-
deploymentUrl: null,
|
|
26897
|
-
createdAt: new Date
|
|
26898
|
-
};
|
|
26899
|
-
const [createdGame] = await db2.insert(games).values(insertData).returning();
|
|
26900
|
-
if (!createdGame) {
|
|
26901
|
-
logger5.error("Game insert returned no rows", { slug, developerId: user.id });
|
|
26902
|
-
throw new InternalError("DB insert failed to return result for new game");
|
|
26772
|
+
if (!game) {
|
|
26773
|
+
throw new NotFoundError("Game", slug);
|
|
26903
26774
|
}
|
|
26904
|
-
|
|
26775
|
+
this.enforceVisibility(game, caller, slug);
|
|
26776
|
+
return game;
|
|
26905
26777
|
}
|
|
26906
|
-
|
|
26907
|
-
|
|
26908
|
-
|
|
26909
|
-
|
|
26910
|
-
|
|
26911
|
-
|
|
26912
|
-
|
|
26913
|
-
|
|
26914
|
-
|
|
26915
|
-
|
|
26778
|
+
async getManifest(gameId, caller) {
|
|
26779
|
+
const game = await this.getById(gameId, caller);
|
|
26780
|
+
if (game.gameType !== "hosted" || !game.deploymentUrl) {
|
|
26781
|
+
throw new BadRequestError("Game does not have a deployment manifest");
|
|
26782
|
+
}
|
|
26783
|
+
const deploymentUrl = GameService.normalizeDeploymentUrl(game.deploymentUrl);
|
|
26784
|
+
const cacheKey2 = GameService.getManifestCacheKey(deploymentUrl);
|
|
26785
|
+
const cached = await this.deps.cache.get(cacheKey2);
|
|
26786
|
+
if (cached) {
|
|
26787
|
+
return cached;
|
|
26788
|
+
}
|
|
26789
|
+
const inFlight = inFlightManifestFetches.get(deploymentUrl);
|
|
26790
|
+
if (inFlight) {
|
|
26791
|
+
return inFlight;
|
|
26792
|
+
}
|
|
26793
|
+
const promise = this.fetchManifestFromOrigin({ gameId, deploymentUrl }).then(async (manifest) => {
|
|
26794
|
+
try {
|
|
26795
|
+
await this.deps.cache.set(cacheKey2, manifest, GameService.MANIFEST_CACHE_TTL_SECONDS);
|
|
26796
|
+
} catch (cacheError) {
|
|
26797
|
+
logger5.warn("Failed to cache game manifest", {
|
|
26798
|
+
gameId,
|
|
26799
|
+
deploymentUrl,
|
|
26800
|
+
cacheKey: cacheKey2,
|
|
26801
|
+
error: cacheError
|
|
26802
|
+
});
|
|
26803
|
+
}
|
|
26804
|
+
return manifest;
|
|
26805
|
+
}).finally(() => {
|
|
26806
|
+
inFlightManifestFetches.delete(deploymentUrl);
|
|
26807
|
+
});
|
|
26808
|
+
inFlightManifestFetches.set(deploymentUrl, promise);
|
|
26809
|
+
return promise;
|
|
26810
|
+
}
|
|
26811
|
+
async fetchManifestFromOrigin(args2) {
|
|
26812
|
+
const { gameId, deploymentUrl } = args2;
|
|
26813
|
+
const manifestUrl = `${deploymentUrl}/playcademy.manifest.json`;
|
|
26814
|
+
const manifestHost = GameService.getManifestHost(manifestUrl);
|
|
26815
|
+
const startedAt = Date.now();
|
|
26816
|
+
const maxAttempts = GameService.MANIFEST_FETCH_MAX_RETRIES + 1;
|
|
26817
|
+
for (let attempt = 0;attempt < maxAttempts; attempt++) {
|
|
26818
|
+
const isLastAttempt = attempt === maxAttempts - 1;
|
|
26819
|
+
const outcome = await this.attemptManifestFetch({
|
|
26820
|
+
manifestUrl,
|
|
26821
|
+
manifestHost,
|
|
26822
|
+
deploymentUrl,
|
|
26823
|
+
startedAt,
|
|
26824
|
+
retryCount: attempt
|
|
26825
|
+
});
|
|
26826
|
+
if (outcome.kind === "success") {
|
|
26827
|
+
return outcome.manifest;
|
|
26828
|
+
}
|
|
26829
|
+
if (!outcome.retryable || isLastAttempt) {
|
|
26830
|
+
logger5.error("Failed to fetch game manifest", {
|
|
26831
|
+
gameId,
|
|
26832
|
+
manifestUrl,
|
|
26833
|
+
attempt: attempt + 1,
|
|
26834
|
+
maxAttempts,
|
|
26835
|
+
retryable: outcome.retryable,
|
|
26836
|
+
details: outcome.details,
|
|
26837
|
+
throwable: outcome.throwable,
|
|
26838
|
+
cause: outcome.cause
|
|
26839
|
+
});
|
|
26840
|
+
throw outcome.throwable;
|
|
26841
|
+
}
|
|
26842
|
+
const backoffMs = GameService.getRetryBackoffMs(attempt);
|
|
26843
|
+
logger5.warn("Retrying game manifest fetch after transient failure", {
|
|
26844
|
+
gameId,
|
|
26845
|
+
manifestUrl,
|
|
26846
|
+
attempt: attempt + 1,
|
|
26847
|
+
maxAttempts,
|
|
26848
|
+
backoffMs,
|
|
26849
|
+
details: outcome.details,
|
|
26850
|
+
cause: outcome.cause
|
|
26916
26851
|
});
|
|
26852
|
+
await sleep(backoffMs);
|
|
26917
26853
|
}
|
|
26854
|
+
throw new InternalError("Exhausted manifest fetch retries without result");
|
|
26918
26855
|
}
|
|
26919
|
-
|
|
26920
|
-
|
|
26921
|
-
|
|
26922
|
-
|
|
26923
|
-
|
|
26924
|
-
|
|
26925
|
-
|
|
26926
|
-
|
|
26927
|
-
|
|
26928
|
-
|
|
26929
|
-
|
|
26930
|
-
|
|
26931
|
-
|
|
26932
|
-
|
|
26933
|
-
|
|
26934
|
-
|
|
26935
|
-
|
|
26936
|
-
}
|
|
26937
|
-
const activeDeployment = await db2.query.gameDeployments.findFirst({
|
|
26938
|
-
where: and(eq(gameDeployments.gameId, gameId), eq(gameDeployments.isActive, true)),
|
|
26939
|
-
columns: { deploymentId: true, provider: true, resources: true }
|
|
26940
|
-
});
|
|
26941
|
-
const customHostnames = await db2.select({
|
|
26942
|
-
hostname: gameCustomHostnames.hostname,
|
|
26943
|
-
cloudflareId: gameCustomHostnames.cloudflareId,
|
|
26944
|
-
environment: gameCustomHostnames.environment
|
|
26945
|
-
}).from(gameCustomHostnames).where(eq(gameCustomHostnames.gameId, gameId));
|
|
26946
|
-
const result = await db2.delete(games).where(eq(games.id, gameId)).returning({ id: games.id });
|
|
26947
|
-
if (result.length === 0) {
|
|
26948
|
-
throw new NotFoundError("Game", gameId);
|
|
26949
|
-
}
|
|
26950
|
-
logger5.info("Deleted game", {
|
|
26951
|
-
gameId: result[0].id,
|
|
26952
|
-
slug: gameToDelete.slug,
|
|
26953
|
-
hadActiveDeployment: Boolean(activeDeployment),
|
|
26954
|
-
customDomainsCount: customHostnames.length
|
|
26955
|
-
});
|
|
26956
|
-
this.deps.alerts.notifyGameDeletion({
|
|
26957
|
-
slug: gameToDelete.slug,
|
|
26958
|
-
displayName: gameToDelete.displayName,
|
|
26959
|
-
developer: { id: user.id, email: user.email }
|
|
26960
|
-
}).catch((error) => {
|
|
26961
|
-
logger5.warn("Failed to send deletion alert", { error });
|
|
26962
|
-
});
|
|
26963
|
-
if (activeDeployment?.provider === "cloudflare" && this.deps.cloudflare) {
|
|
26856
|
+
async attemptManifestFetch(args2) {
|
|
26857
|
+
const { manifestUrl, manifestHost, deploymentUrl, startedAt, retryCount } = args2;
|
|
26858
|
+
function buildDetails(fetchOutcome, manifestErrorKind, extra = {}) {
|
|
26859
|
+
return {
|
|
26860
|
+
manifestUrl,
|
|
26861
|
+
manifestHost,
|
|
26862
|
+
deploymentUrl,
|
|
26863
|
+
fetchOutcome,
|
|
26864
|
+
retryCount,
|
|
26865
|
+
durationMs: Date.now() - startedAt,
|
|
26866
|
+
manifestErrorKind,
|
|
26867
|
+
...extra
|
|
26868
|
+
};
|
|
26869
|
+
}
|
|
26870
|
+
const controller = new AbortController;
|
|
26871
|
+
const timeout = setTimeout(() => controller.abort(), GameService.MANIFEST_FETCH_ATTEMPT_TIMEOUT_MS);
|
|
26872
|
+
let response;
|
|
26964
26873
|
try {
|
|
26965
|
-
await
|
|
26966
|
-
|
|
26967
|
-
|
|
26968
|
-
|
|
26969
|
-
|
|
26970
|
-
|
|
26971
|
-
logger5.info("Cleaned up Cloudflare resources", {
|
|
26972
|
-
gameId,
|
|
26973
|
-
deploymentId: activeDeployment.deploymentId,
|
|
26974
|
-
customDomainsDeleted: customHostnames.length
|
|
26874
|
+
response = await fetch(manifestUrl, {
|
|
26875
|
+
method: "GET",
|
|
26876
|
+
headers: {
|
|
26877
|
+
Accept: "application/json"
|
|
26878
|
+
},
|
|
26879
|
+
signal: controller.signal
|
|
26975
26880
|
});
|
|
26976
|
-
} catch (
|
|
26977
|
-
|
|
26978
|
-
|
|
26979
|
-
|
|
26980
|
-
|
|
26881
|
+
} catch (error) {
|
|
26882
|
+
const fetchErrorMessage = GameService.getFetchErrorMessage(error);
|
|
26883
|
+
const details = buildDetails("network_error", "temporary", fetchErrorMessage ? { fetchErrorMessage } : {});
|
|
26884
|
+
const throwable = error instanceof Error && error.name === "AbortError" ? new TimeoutError("Timed out loading game manifest", details) : new ServiceUnavailableError("Failed to load game manifest", details);
|
|
26885
|
+
return { kind: "failure", retryable: true, throwable, details, cause: error };
|
|
26886
|
+
} finally {
|
|
26887
|
+
clearTimeout(timeout);
|
|
26888
|
+
}
|
|
26889
|
+
if (!response.ok) {
|
|
26890
|
+
const resolvedManifestUrl = response.url || manifestUrl;
|
|
26891
|
+
const resolvedManifestHost = GameService.getManifestHost(resolvedManifestUrl);
|
|
26892
|
+
const manifestErrorKind = GameService.isRetryableStatus(response.status) ? "temporary" : "permanent";
|
|
26893
|
+
const details = buildDetails("bad_status", manifestErrorKind, {
|
|
26894
|
+
manifestUrl: resolvedManifestUrl,
|
|
26895
|
+
manifestHost: resolvedManifestHost,
|
|
26896
|
+
status: response.status,
|
|
26897
|
+
contentType: response.headers.get("content-type") ?? undefined,
|
|
26898
|
+
cfRay: response.headers.get("cf-ray") ?? undefined,
|
|
26899
|
+
redirected: response.redirected,
|
|
26900
|
+
...response.redirected ? {
|
|
26901
|
+
originalManifestUrl: manifestUrl,
|
|
26902
|
+
originalManifestHost: manifestHost
|
|
26903
|
+
} : {}
|
|
26981
26904
|
});
|
|
26905
|
+
const message = `Failed to fetch manifest: ${response.status} ${response.statusText}`;
|
|
26906
|
+
const throwable = manifestErrorKind === "temporary" ? new ServiceUnavailableError(message, details) : new BadRequestError(message, details);
|
|
26907
|
+
return {
|
|
26908
|
+
kind: "failure",
|
|
26909
|
+
retryable: manifestErrorKind === "temporary",
|
|
26910
|
+
throwable,
|
|
26911
|
+
details
|
|
26912
|
+
};
|
|
26982
26913
|
}
|
|
26983
26914
|
try {
|
|
26984
|
-
const
|
|
26985
|
-
|
|
26986
|
-
|
|
26987
|
-
|
|
26988
|
-
|
|
26989
|
-
|
|
26915
|
+
const manifest = await response.json();
|
|
26916
|
+
return { kind: "success", manifest };
|
|
26917
|
+
} catch (error) {
|
|
26918
|
+
const resolvedManifestUrl = response.url || manifestUrl;
|
|
26919
|
+
const resolvedManifestHost = GameService.getManifestHost(resolvedManifestUrl);
|
|
26920
|
+
const details = buildDetails("invalid_body", "permanent", {
|
|
26921
|
+
manifestUrl: resolvedManifestUrl,
|
|
26922
|
+
manifestHost: resolvedManifestHost,
|
|
26923
|
+
status: response.status,
|
|
26924
|
+
contentType: response.headers.get("content-type") ?? undefined,
|
|
26925
|
+
cfRay: response.headers.get("cf-ray") ?? undefined,
|
|
26926
|
+
redirected: response.redirected,
|
|
26927
|
+
...response.redirected ? {
|
|
26928
|
+
originalManifestUrl: manifestUrl,
|
|
26929
|
+
originalManifestHost: manifestHost
|
|
26930
|
+
} : {}
|
|
26931
|
+
});
|
|
26932
|
+
return {
|
|
26933
|
+
kind: "failure",
|
|
26934
|
+
retryable: false,
|
|
26935
|
+
throwable: new BadRequestError("Failed to parse game manifest", details),
|
|
26936
|
+
details,
|
|
26937
|
+
cause: error
|
|
26938
|
+
};
|
|
26939
|
+
}
|
|
26940
|
+
}
|
|
26941
|
+
enforceVisibility(game, caller, lookupIdentifier) {
|
|
26942
|
+
if (game.visibility !== "internal") {
|
|
26943
|
+
return;
|
|
26944
|
+
}
|
|
26945
|
+
const isAdmin = caller?.role === "admin";
|
|
26946
|
+
const isOwner = caller?.id != null && caller.id === game.developerId;
|
|
26947
|
+
if (!isAdmin && !isOwner) {
|
|
26948
|
+
throw new NotFoundError("Game", lookupIdentifier);
|
|
26949
|
+
}
|
|
26950
|
+
}
|
|
26951
|
+
async upsertBySlug(slug, data, user) {
|
|
26952
|
+
const db2 = this.deps.db;
|
|
26953
|
+
const existingGame = await db2.query.games.findFirst({
|
|
26954
|
+
where: eq(games.slug, slug)
|
|
26955
|
+
});
|
|
26956
|
+
const isUpdate = Boolean(existingGame);
|
|
26957
|
+
const gameId = existingGame?.id ?? crypto.randomUUID();
|
|
26958
|
+
if (isUpdate) {
|
|
26959
|
+
await this.validateDeveloperAccess(user, gameId);
|
|
26960
|
+
} else {
|
|
26961
|
+
this.validateDeveloperStatus(user);
|
|
26962
|
+
}
|
|
26963
|
+
const gameDataForDb = {
|
|
26964
|
+
displayName: data.displayName,
|
|
26965
|
+
platform: data.platform,
|
|
26966
|
+
metadata: data.metadata,
|
|
26967
|
+
mapElementId: data.mapElementId,
|
|
26968
|
+
gameType: data.gameType,
|
|
26969
|
+
...data.visibility && { visibility: data.visibility },
|
|
26970
|
+
externalUrl: data.externalUrl || null,
|
|
26971
|
+
updatedAt: new Date
|
|
26972
|
+
};
|
|
26973
|
+
let gameResponse;
|
|
26974
|
+
if (isUpdate) {
|
|
26975
|
+
const [updatedGame] = await db2.update(games).set(gameDataForDb).where(eq(games.id, gameId)).returning();
|
|
26976
|
+
if (!updatedGame) {
|
|
26977
|
+
logger5.error("Game update returned no rows", { gameId, slug });
|
|
26978
|
+
throw new InternalError("DB update failed to return result for existing game");
|
|
26979
|
+
}
|
|
26980
|
+
gameResponse = updatedGame;
|
|
26981
|
+
} else {
|
|
26982
|
+
const insertData = {
|
|
26983
|
+
...gameDataForDb,
|
|
26984
|
+
id: gameId,
|
|
26985
|
+
slug,
|
|
26986
|
+
developerId: user.id,
|
|
26987
|
+
metadata: data.metadata || {},
|
|
26988
|
+
version: data.gameType === "external" ? "external" : "",
|
|
26989
|
+
deploymentUrl: null,
|
|
26990
|
+
createdAt: new Date
|
|
26991
|
+
};
|
|
26992
|
+
const [createdGame] = await db2.insert(games).values(insertData).returning();
|
|
26993
|
+
if (!createdGame) {
|
|
26994
|
+
logger5.error("Game insert returned no rows", { slug, developerId: user.id });
|
|
26995
|
+
throw new InternalError("DB insert failed to return result for new game");
|
|
26996
|
+
}
|
|
26997
|
+
gameResponse = createdGame;
|
|
26998
|
+
}
|
|
26999
|
+
if (data.mapElementId) {
|
|
27000
|
+
try {
|
|
27001
|
+
await db2.update(mapElements).set({
|
|
27002
|
+
interactionType: "game_entry",
|
|
27003
|
+
gameId: gameResponse.id
|
|
27004
|
+
}).where(eq(mapElements.id, data.mapElementId));
|
|
27005
|
+
} catch (mapError) {
|
|
27006
|
+
logger5.warn("Failed to update map element", {
|
|
27007
|
+
mapElementId: data.mapElementId,
|
|
27008
|
+
error: mapError
|
|
26990
27009
|
});
|
|
26991
27010
|
}
|
|
26992
|
-
} catch (keyError) {
|
|
26993
|
-
logger5.warn("Failed to cleanup API key", { gameId, error: keyError });
|
|
26994
27011
|
}
|
|
27012
|
+
logger5.info("Upserted game", {
|
|
27013
|
+
gameId: gameResponse.id,
|
|
27014
|
+
slug: gameResponse.slug,
|
|
27015
|
+
operation: isUpdate ? "update" : "create",
|
|
27016
|
+
displayName: gameResponse.displayName
|
|
27017
|
+
});
|
|
27018
|
+
return gameResponse;
|
|
26995
27019
|
}
|
|
26996
|
-
|
|
26997
|
-
|
|
26998
|
-
|
|
26999
|
-
|
|
27000
|
-
}
|
|
27001
|
-
async validateOwnership(user, gameId) {
|
|
27002
|
-
if (user.role === "admin") {
|
|
27003
|
-
const gameExists = await this.deps.db.query.games.findFirst({
|
|
27020
|
+
async delete(gameId, user) {
|
|
27021
|
+
await this.validateDeveloperAccess(user, gameId);
|
|
27022
|
+
const db2 = this.deps.db;
|
|
27023
|
+
const gameToDelete = await db2.query.games.findFirst({
|
|
27004
27024
|
where: eq(games.id, gameId),
|
|
27005
|
-
columns: { id: true }
|
|
27025
|
+
columns: { id: true, slug: true, displayName: true }
|
|
27006
27026
|
});
|
|
27007
|
-
if (!
|
|
27027
|
+
if (!gameToDelete?.slug) {
|
|
27008
27028
|
throw new NotFoundError("Game", gameId);
|
|
27009
27029
|
}
|
|
27010
|
-
|
|
27030
|
+
const activeDeployment = await db2.query.gameDeployments.findFirst({
|
|
27031
|
+
where: and(eq(gameDeployments.gameId, gameId), eq(gameDeployments.isActive, true)),
|
|
27032
|
+
columns: { deploymentId: true, provider: true, resources: true }
|
|
27033
|
+
});
|
|
27034
|
+
const customHostnames = await db2.select({
|
|
27035
|
+
hostname: gameCustomHostnames.hostname,
|
|
27036
|
+
cloudflareId: gameCustomHostnames.cloudflareId,
|
|
27037
|
+
environment: gameCustomHostnames.environment
|
|
27038
|
+
}).from(gameCustomHostnames).where(eq(gameCustomHostnames.gameId, gameId));
|
|
27039
|
+
const result = await db2.delete(games).where(eq(games.id, gameId)).returning({ id: games.id });
|
|
27040
|
+
if (result.length === 0) {
|
|
27041
|
+
throw new NotFoundError("Game", gameId);
|
|
27042
|
+
}
|
|
27043
|
+
logger5.info("Deleted game", {
|
|
27044
|
+
gameId: result[0].id,
|
|
27045
|
+
slug: gameToDelete.slug,
|
|
27046
|
+
hadActiveDeployment: Boolean(activeDeployment),
|
|
27047
|
+
customDomainsCount: customHostnames.length
|
|
27048
|
+
});
|
|
27049
|
+
this.deps.alerts.notifyGameDeletion({
|
|
27050
|
+
slug: gameToDelete.slug,
|
|
27051
|
+
displayName: gameToDelete.displayName,
|
|
27052
|
+
developer: { id: user.id, email: user.email }
|
|
27053
|
+
}).catch((error) => {
|
|
27054
|
+
logger5.warn("Failed to send deletion alert", { error });
|
|
27055
|
+
});
|
|
27056
|
+
if (activeDeployment?.provider === "cloudflare" && this.deps.cloudflare) {
|
|
27057
|
+
try {
|
|
27058
|
+
await this.deps.cloudflare.delete(activeDeployment.deploymentId, {
|
|
27059
|
+
deleteBindings: true,
|
|
27060
|
+
resources: activeDeployment.resources ?? undefined,
|
|
27061
|
+
customDomains: customHostnames.length > 0 ? customHostnames : undefined,
|
|
27062
|
+
gameSlug: gameToDelete.slug
|
|
27063
|
+
});
|
|
27064
|
+
logger5.info("Cleaned up Cloudflare resources", {
|
|
27065
|
+
gameId,
|
|
27066
|
+
deploymentId: activeDeployment.deploymentId,
|
|
27067
|
+
customDomainsDeleted: customHostnames.length
|
|
27068
|
+
});
|
|
27069
|
+
} catch (cfError) {
|
|
27070
|
+
logger5.warn("Failed to cleanup Cloudflare resources", {
|
|
27071
|
+
gameId,
|
|
27072
|
+
deploymentId: activeDeployment.deploymentId,
|
|
27073
|
+
error: cfError
|
|
27074
|
+
});
|
|
27075
|
+
}
|
|
27076
|
+
try {
|
|
27077
|
+
const deletedKeyId = await this.deps.deleteApiKeyByName(getGameWorkerApiKeyName(gameToDelete.slug), user.id);
|
|
27078
|
+
if (deletedKeyId) {
|
|
27079
|
+
logger5.info("Cleaned up API key for deleted game", {
|
|
27080
|
+
gameId,
|
|
27081
|
+
slug: gameToDelete.slug,
|
|
27082
|
+
keyId: deletedKeyId
|
|
27083
|
+
});
|
|
27084
|
+
}
|
|
27085
|
+
} catch (keyError) {
|
|
27086
|
+
logger5.warn("Failed to cleanup API key", { gameId, error: keyError });
|
|
27087
|
+
}
|
|
27088
|
+
}
|
|
27089
|
+
return {
|
|
27090
|
+
slug: gameToDelete.slug,
|
|
27091
|
+
displayName: gameToDelete.displayName
|
|
27092
|
+
};
|
|
27011
27093
|
}
|
|
27012
|
-
|
|
27013
|
-
|
|
27014
|
-
|
|
27015
|
-
|
|
27016
|
-
|
|
27017
|
-
|
|
27018
|
-
|
|
27019
|
-
|
|
27094
|
+
async validateOwnership(user, gameId) {
|
|
27095
|
+
if (user.role === "admin") {
|
|
27096
|
+
const gameExists = await this.deps.db.query.games.findFirst({
|
|
27097
|
+
where: eq(games.id, gameId),
|
|
27098
|
+
columns: { id: true }
|
|
27099
|
+
});
|
|
27100
|
+
if (!gameExists) {
|
|
27101
|
+
throw new NotFoundError("Game", gameId);
|
|
27102
|
+
}
|
|
27103
|
+
return;
|
|
27104
|
+
}
|
|
27105
|
+
const db2 = this.deps.db;
|
|
27106
|
+
const gameOwnership = await db2.query.games.findFirst({
|
|
27107
|
+
where: and(eq(games.id, gameId), eq(games.developerId, user.id)),
|
|
27020
27108
|
columns: { id: true }
|
|
27021
27109
|
});
|
|
27022
|
-
if (!
|
|
27023
|
-
|
|
27110
|
+
if (!gameOwnership) {
|
|
27111
|
+
const gameExists = await db2.query.games.findFirst({
|
|
27112
|
+
where: eq(games.id, gameId),
|
|
27113
|
+
columns: { id: true }
|
|
27114
|
+
});
|
|
27115
|
+
if (!gameExists) {
|
|
27116
|
+
throw new NotFoundError("Game", gameId);
|
|
27117
|
+
}
|
|
27118
|
+
throw new AccessDeniedError("You do not own this game");
|
|
27024
27119
|
}
|
|
27025
|
-
throw new AccessDeniedError("You do not own this game");
|
|
27026
27120
|
}
|
|
27027
|
-
|
|
27028
|
-
|
|
27029
|
-
|
|
27030
|
-
|
|
27031
|
-
|
|
27032
|
-
|
|
27121
|
+
async validateDeveloperAccess(user, gameId) {
|
|
27122
|
+
this.validateDeveloperStatus(user);
|
|
27123
|
+
if (user.role === "admin") {
|
|
27124
|
+
const gameExists = await this.deps.db.query.games.findFirst({
|
|
27125
|
+
where: eq(games.id, gameId),
|
|
27126
|
+
columns: { id: true }
|
|
27127
|
+
});
|
|
27128
|
+
if (!gameExists) {
|
|
27129
|
+
throw new NotFoundError("Game", gameId);
|
|
27130
|
+
}
|
|
27131
|
+
return;
|
|
27132
|
+
}
|
|
27133
|
+
const db2 = this.deps.db;
|
|
27134
|
+
const existingGame = await db2.query.games.findFirst({
|
|
27135
|
+
where: and(eq(games.id, gameId), eq(games.developerId, user.id)),
|
|
27033
27136
|
columns: { id: true }
|
|
27034
27137
|
});
|
|
27035
|
-
if (!
|
|
27138
|
+
if (!existingGame) {
|
|
27036
27139
|
throw new NotFoundError("Game", gameId);
|
|
27037
27140
|
}
|
|
27038
|
-
return;
|
|
27039
|
-
}
|
|
27040
|
-
const db2 = this.deps.db;
|
|
27041
|
-
const existingGame = await db2.query.games.findFirst({
|
|
27042
|
-
where: and(eq(games.id, gameId), eq(games.developerId, user.id)),
|
|
27043
|
-
columns: { id: true }
|
|
27044
|
-
});
|
|
27045
|
-
if (!existingGame) {
|
|
27046
|
-
throw new NotFoundError("Game", gameId);
|
|
27047
27141
|
}
|
|
27048
|
-
|
|
27049
|
-
|
|
27050
|
-
|
|
27051
|
-
|
|
27052
|
-
|
|
27053
|
-
|
|
27054
|
-
|
|
27142
|
+
async validateDeveloperAccessBySlug(user, slug) {
|
|
27143
|
+
this.validateDeveloperStatus(user);
|
|
27144
|
+
const db2 = this.deps.db;
|
|
27145
|
+
if (user.role === "admin") {
|
|
27146
|
+
const game2 = await db2.query.games.findFirst({
|
|
27147
|
+
where: eq(games.slug, slug)
|
|
27148
|
+
});
|
|
27149
|
+
if (!game2) {
|
|
27150
|
+
throw new NotFoundError("Game", slug);
|
|
27151
|
+
}
|
|
27152
|
+
return game2;
|
|
27153
|
+
}
|
|
27154
|
+
const game = await db2.query.games.findFirst({
|
|
27155
|
+
where: and(eq(games.slug, slug), eq(games.developerId, user.id))
|
|
27055
27156
|
});
|
|
27056
|
-
if (!
|
|
27157
|
+
if (!game) {
|
|
27057
27158
|
throw new NotFoundError("Game", slug);
|
|
27058
27159
|
}
|
|
27059
|
-
return
|
|
27060
|
-
}
|
|
27061
|
-
const game = await db2.query.games.findFirst({
|
|
27062
|
-
where: and(eq(games.slug, slug), eq(games.developerId, user.id))
|
|
27063
|
-
});
|
|
27064
|
-
if (!game) {
|
|
27065
|
-
throw new NotFoundError("Game", slug);
|
|
27066
|
-
}
|
|
27067
|
-
return game;
|
|
27068
|
-
}
|
|
27069
|
-
validateDeveloperStatus(user) {
|
|
27070
|
-
if (user.role === "admin") {
|
|
27071
|
-
return;
|
|
27160
|
+
return game;
|
|
27072
27161
|
}
|
|
27073
|
-
|
|
27074
|
-
|
|
27075
|
-
|
|
27076
|
-
|
|
27077
|
-
|
|
27078
|
-
|
|
27162
|
+
validateDeveloperStatus(user) {
|
|
27163
|
+
if (user.role === "admin") {
|
|
27164
|
+
return;
|
|
27165
|
+
}
|
|
27166
|
+
if (user.developerStatus !== "approved") {
|
|
27167
|
+
const status = user.developerStatus || "none";
|
|
27168
|
+
if (status === "pending") {
|
|
27169
|
+
throw new AccessDeniedError("Developer application is pending approval. You will be notified when approved.");
|
|
27170
|
+
} else {
|
|
27171
|
+
throw new AccessDeniedError("Developer status required. Apply for developer access to create and manage games.");
|
|
27172
|
+
}
|
|
27079
27173
|
}
|
|
27080
27174
|
}
|
|
27081
|
-
}
|
|
27082
|
-
}
|
|
27083
|
-
var logger5;
|
|
27084
|
-
var init_game_service = __esm(() => {
|
|
27085
|
-
init_drizzle_orm();
|
|
27086
|
-
init_tables_index();
|
|
27087
|
-
init_src2();
|
|
27088
|
-
init_errors();
|
|
27089
|
-
init_deployment_util();
|
|
27090
|
-
logger5 = log.scope("GameService");
|
|
27175
|
+
};
|
|
27091
27176
|
});
|
|
27092
27177
|
|
|
27093
27178
|
// ../api-core/src/services/factory/game.ts
|
|
@@ -27096,6 +27181,7 @@ function createGameServices(deps) {
|
|
|
27096
27181
|
const game = new GameService({
|
|
27097
27182
|
db: db2,
|
|
27098
27183
|
alerts,
|
|
27184
|
+
cache,
|
|
27099
27185
|
cloudflare,
|
|
27100
27186
|
deleteApiKeyByName: auth2.deleteApiKeyByName.bind(auth2)
|
|
27101
27187
|
});
|
|
@@ -28913,7 +28999,8 @@ class SeedService {
|
|
|
28913
28999
|
PLAYCADEMY_BASE_URL: ""
|
|
28914
29000
|
}, {
|
|
28915
29001
|
bindings: { d1: [deploymentId], r2: [], kv: [] },
|
|
28916
|
-
keepAssets: false
|
|
29002
|
+
keepAssets: false,
|
|
29003
|
+
compatibilityDate: CLOUDFLARE_COMPATIBILITY_DATE
|
|
28917
29004
|
});
|
|
28918
29005
|
logger14.info("Worker deployed", { seedDeploymentId, url: result.url });
|
|
28919
29006
|
if (secrets && Object.keys(secrets).length > 0) {
|
|
@@ -29043,6 +29130,7 @@ class SeedService {
|
|
|
29043
29130
|
}
|
|
29044
29131
|
var logger14;
|
|
29045
29132
|
var init_seed_service = __esm(() => {
|
|
29133
|
+
init_src();
|
|
29046
29134
|
init_setup2();
|
|
29047
29135
|
init_src2();
|
|
29048
29136
|
init_config2();
|
|
@@ -93151,6 +93239,7 @@ var init_schemas2 = __esm(() => {
|
|
|
93151
93239
|
code: exports_external.string().optional(),
|
|
93152
93240
|
codeUploadToken: exports_external.string().optional(),
|
|
93153
93241
|
config: exports_external.unknown().optional(),
|
|
93242
|
+
compatibilityDate: exports_external.string().optional(),
|
|
93154
93243
|
compatibilityFlags: exports_external.array(exports_external.string()).optional(),
|
|
93155
93244
|
bindings: exports_external.object({
|
|
93156
93245
|
database: exports_external.array(exports_external.string()).optional(),
|