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