@playcademy/sandbox 0.3.17-beta.4 → 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 +478 -391
- package/dist/server.js +478 -391
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -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: {
|
|
@@ -26647,447 +26647,533 @@ var init_developer_service = __esm(() => {
|
|
|
26647
26647
|
logger4 = log.scope("DeveloperService");
|
|
26648
26648
|
});
|
|
26649
26649
|
|
|
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
|
-
}
|
|
26650
|
+
// ../utils/src/fns.ts
|
|
26651
|
+
function sleep(ms) {
|
|
26652
|
+
if (ms <= 0) {
|
|
26653
|
+
return Promise.resolve();
|
|
26664
26654
|
}
|
|
26665
|
-
|
|
26666
|
-
|
|
26667
|
-
|
|
26668
|
-
|
|
26669
|
-
|
|
26670
|
-
|
|
26671
|
-
|
|
26672
|
-
|
|
26673
|
-
|
|
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
|
+
}
|
|
26674
26685
|
}
|
|
26675
|
-
|
|
26676
|
-
|
|
26677
|
-
|
|
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);
|
|
26678
26701
|
}
|
|
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");
|
|
26702
|
+
static isRetryableStatus(status) {
|
|
26703
|
+
return status === 429 || status >= 500;
|
|
26695
26704
|
}
|
|
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;
|
|
26705
|
+
static getRetryBackoffMs(attemptIndex) {
|
|
26706
|
+
const backoff = GameService.MANIFEST_FETCH_RETRY_BACKOFF_MS;
|
|
26707
|
+
if (backoff.length === 0) {
|
|
26708
|
+
return 0;
|
|
26719
26709
|
}
|
|
26710
|
+
return backoff[Math.min(attemptIndex, backoff.length - 1)] ?? 0;
|
|
26720
26711
|
}
|
|
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);
|
|
26712
|
+
static normalizeDeploymentUrl(deploymentUrl) {
|
|
26713
|
+
return deploymentUrl.replace(/\/$/, "");
|
|
26730
26714
|
}
|
|
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);
|
|
26741
|
-
}
|
|
26742
|
-
this.enforceVisibility(game, caller, slug);
|
|
26743
|
-
return game;
|
|
26744
|
-
}
|
|
26745
|
-
async getManifest(gameId, caller) {
|
|
26746
|
-
const game = await this.getById(gameId, caller);
|
|
26747
|
-
if (game.gameType !== "hosted" || !game.deploymentUrl) {
|
|
26748
|
-
throw new BadRequestError("Game does not have a deployment manifest");
|
|
26715
|
+
static getManifestCacheKey(deploymentUrl) {
|
|
26716
|
+
return `${GameService.MANIFEST_CACHE_KEY_PREFIX}:${deploymentUrl}`;
|
|
26749
26717
|
}
|
|
26750
|
-
|
|
26751
|
-
|
|
26752
|
-
|
|
26753
|
-
|
|
26754
|
-
|
|
26755
|
-
|
|
26756
|
-
|
|
26757
|
-
|
|
26758
|
-
|
|
26759
|
-
|
|
26760
|
-
|
|
26761
|
-
|
|
26762
|
-
|
|
26763
|
-
|
|
26764
|
-
|
|
26765
|
-
|
|
26766
|
-
};
|
|
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
|
+
});
|
|
26767
26734
|
}
|
|
26768
|
-
|
|
26769
|
-
|
|
26770
|
-
|
|
26771
|
-
|
|
26772
|
-
|
|
26773
|
-
|
|
26774
|
-
},
|
|
26775
|
-
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)]
|
|
26776
26741
|
});
|
|
26777
|
-
}
|
|
26778
|
-
|
|
26779
|
-
const
|
|
26780
|
-
const
|
|
26781
|
-
|
|
26782
|
-
|
|
26783
|
-
manifestUrl,
|
|
26784
|
-
error,
|
|
26785
|
-
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)]
|
|
26786
26748
|
});
|
|
26787
|
-
|
|
26788
|
-
|
|
26749
|
+
const subjectMap = {};
|
|
26750
|
+
for (const integration of integrations) {
|
|
26751
|
+
if (!(integration.gameId in subjectMap)) {
|
|
26752
|
+
subjectMap[integration.gameId] = integration.subject;
|
|
26753
|
+
}
|
|
26789
26754
|
}
|
|
26790
|
-
|
|
26791
|
-
} finally {
|
|
26792
|
-
clearTimeout(timeout);
|
|
26755
|
+
return subjectMap;
|
|
26793
26756
|
}
|
|
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
|
|
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)
|
|
26816
26761
|
});
|
|
26817
|
-
if (
|
|
26818
|
-
throw new
|
|
26762
|
+
if (!game) {
|
|
26763
|
+
throw new NotFoundError("Game", gameId);
|
|
26819
26764
|
}
|
|
26820
|
-
|
|
26765
|
+
this.enforceVisibility(game, caller, gameId);
|
|
26766
|
+
return game;
|
|
26821
26767
|
}
|
|
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
|
|
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)
|
|
26844
26772
|
});
|
|
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");
|
|
26773
|
+
if (!game) {
|
|
26774
|
+
throw new NotFoundError("Game", slug);
|
|
26903
26775
|
}
|
|
26904
|
-
|
|
26776
|
+
this.enforceVisibility(game, caller, slug);
|
|
26777
|
+
return game;
|
|
26905
26778
|
}
|
|
26906
|
-
|
|
26907
|
-
|
|
26908
|
-
|
|
26909
|
-
|
|
26910
|
-
|
|
26911
|
-
|
|
26912
|
-
|
|
26913
|
-
|
|
26914
|
-
|
|
26915
|
-
|
|
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
|
|
26916
26852
|
});
|
|
26853
|
+
await sleep(backoffMs);
|
|
26917
26854
|
}
|
|
26855
|
+
throw new InternalError("Exhausted manifest fetch retries without result");
|
|
26918
26856
|
}
|
|
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) {
|
|
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;
|
|
26964
26874
|
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
|
|
26875
|
+
response = await fetch(manifestUrl, {
|
|
26876
|
+
method: "GET",
|
|
26877
|
+
headers: {
|
|
26878
|
+
Accept: "application/json"
|
|
26879
|
+
},
|
|
26880
|
+
signal: controller.signal
|
|
26975
26881
|
});
|
|
26976
|
-
} catch (
|
|
26977
|
-
|
|
26978
|
-
|
|
26979
|
-
|
|
26980
|
-
|
|
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
|
+
} : {}
|
|
26981
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
|
+
};
|
|
26982
26914
|
}
|
|
26983
26915
|
try {
|
|
26984
|
-
const
|
|
26985
|
-
|
|
26986
|
-
|
|
26987
|
-
|
|
26988
|
-
|
|
26989
|
-
|
|
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
|
|
26990
27010
|
});
|
|
26991
27011
|
}
|
|
26992
|
-
} catch (keyError) {
|
|
26993
|
-
logger5.warn("Failed to cleanup API key", { gameId, error: keyError });
|
|
26994
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;
|
|
26995
27020
|
}
|
|
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({
|
|
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({
|
|
27004
27025
|
where: eq(games.id, gameId),
|
|
27005
|
-
columns: { id: true }
|
|
27026
|
+
columns: { id: true, slug: true, displayName: true }
|
|
27006
27027
|
});
|
|
27007
|
-
if (!
|
|
27028
|
+
if (!gameToDelete?.slug) {
|
|
27008
27029
|
throw new NotFoundError("Game", gameId);
|
|
27009
27030
|
}
|
|
27010
|
-
|
|
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
|
+
};
|
|
27011
27094
|
}
|
|
27012
|
-
|
|
27013
|
-
|
|
27014
|
-
|
|
27015
|
-
|
|
27016
|
-
|
|
27017
|
-
|
|
27018
|
-
|
|
27019
|
-
|
|
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)),
|
|
27020
27109
|
columns: { id: true }
|
|
27021
27110
|
});
|
|
27022
|
-
if (!
|
|
27023
|
-
|
|
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");
|
|
27024
27120
|
}
|
|
27025
|
-
throw new AccessDeniedError("You do not own this game");
|
|
27026
27121
|
}
|
|
27027
|
-
|
|
27028
|
-
|
|
27029
|
-
|
|
27030
|
-
|
|
27031
|
-
|
|
27032
|
-
|
|
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)),
|
|
27033
27137
|
columns: { id: true }
|
|
27034
27138
|
});
|
|
27035
|
-
if (!
|
|
27139
|
+
if (!existingGame) {
|
|
27036
27140
|
throw new NotFoundError("Game", gameId);
|
|
27037
27141
|
}
|
|
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
27142
|
}
|
|
27048
|
-
|
|
27049
|
-
|
|
27050
|
-
|
|
27051
|
-
|
|
27052
|
-
|
|
27053
|
-
|
|
27054
|
-
|
|
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))
|
|
27055
27157
|
});
|
|
27056
|
-
if (!
|
|
27158
|
+
if (!game) {
|
|
27057
27159
|
throw new NotFoundError("Game", slug);
|
|
27058
27160
|
}
|
|
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;
|
|
27161
|
+
return game;
|
|
27072
27162
|
}
|
|
27073
|
-
|
|
27074
|
-
|
|
27075
|
-
|
|
27076
|
-
|
|
27077
|
-
|
|
27078
|
-
|
|
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
|
+
}
|
|
27079
27174
|
}
|
|
27080
27175
|
}
|
|
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");
|
|
27176
|
+
};
|
|
27091
27177
|
});
|
|
27092
27178
|
|
|
27093
27179
|
// ../api-core/src/services/factory/game.ts
|
|
@@ -27096,6 +27182,7 @@ function createGameServices(deps) {
|
|
|
27096
27182
|
const game = new GameService({
|
|
27097
27183
|
db: db2,
|
|
27098
27184
|
alerts,
|
|
27185
|
+
cache,
|
|
27099
27186
|
cloudflare,
|
|
27100
27187
|
deleteApiKeyByName: auth2.deleteApiKeyByName.bind(auth2)
|
|
27101
27188
|
});
|