@dreamboard-games/cli 0.1.30-alpha.4 → 0.1.30-alpha.40
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/README.md +32 -113
- package/dist/agent-verifier/agent-workspace-verifier.mjs +2084 -57
- package/dist/agent-verifier/agent-workspace-verifier.mjs.map +1 -1
- package/dist/agent-verifier/{chunk-XQXDOBYB.mjs → chunk-4I2WWAPK.mjs} +27 -10
- package/dist/agent-verifier/chunk-4I2WWAPK.mjs.map +1 -0
- package/dist/agent-verifier/{chunk-C3VW3DTA.mjs → chunk-BWBN2TDJ.mjs} +535 -633
- package/dist/agent-verifier/chunk-BWBN2TDJ.mjs.map +1 -0
- package/dist/agent-verifier/{chunk-TAEQKBJB.mjs → chunk-GWRZRWCF.mjs} +1 -1
- package/dist/agent-verifier/chunk-GWRZRWCF.mjs.map +1 -0
- package/dist/agent-verifier/chunk-HUBV22JQ.mjs +89 -0
- package/dist/agent-verifier/chunk-HUBV22JQ.mjs.map +1 -0
- package/dist/agent-verifier/{chunk-MW2QIWWA.mjs → chunk-KAA3B4DI.mjs} +215 -223
- package/dist/agent-verifier/chunk-KAA3B4DI.mjs.map +1 -0
- package/dist/agent-verifier/{chunk-27EEIZCI.mjs → chunk-KDAQ4CZY.mjs} +34 -27
- package/dist/agent-verifier/chunk-KDAQ4CZY.mjs.map +1 -0
- package/dist/agent-verifier/{chunk-IAYRNVUC.mjs → chunk-LMW66VBH.mjs} +2 -13
- package/dist/agent-verifier/{chunk-IAYRNVUC.mjs.map → chunk-LMW66VBH.mjs.map} +1 -1
- package/dist/agent-verifier/{chunk-776W3UGV.mjs → chunk-LROY5SN2.mjs} +7 -45
- package/dist/agent-verifier/chunk-LROY5SN2.mjs.map +1 -0
- package/dist/agent-verifier/{chunk-H76MT5UR.mjs → chunk-M7UVBANQ.mjs} +2 -1
- package/dist/agent-verifier/chunk-M7UVBANQ.mjs.map +1 -0
- package/dist/agent-verifier/{chunk-5NYBTZB4.mjs → chunk-MIRGCMUC.mjs} +112 -26
- package/dist/agent-verifier/chunk-MIRGCMUC.mjs.map +1 -0
- package/dist/agent-verifier/{chunk-NAK77WXW.mjs → chunk-MYMVXTZT.mjs} +4 -5
- package/dist/agent-verifier/chunk-MYMVXTZT.mjs.map +1 -0
- package/dist/agent-verifier/chunk-OJFZVGEL.mjs +492 -0
- package/dist/agent-verifier/chunk-OJFZVGEL.mjs.map +1 -0
- package/dist/agent-verifier/{chunk-XKCJBIRY.mjs → chunk-QD4SQNUP.mjs} +2 -2
- package/dist/agent-verifier/{chunk-QBAF7EYR.mjs → chunk-TTB7AIHZ.mjs} +4 -4
- package/dist/agent-verifier/{chunk-QBAF7EYR.mjs.map → chunk-TTB7AIHZ.mjs.map} +1 -1
- package/dist/agent-verifier/{chunk-F2DIOJJZ.mjs → chunk-XCQQIPCO.mjs} +5 -46
- package/dist/agent-verifier/chunk-XCQQIPCO.mjs.map +1 -0
- package/dist/agent-verifier/{global-config-NYCSCAUI.mjs → global-config-2NUESNEQ.mjs} +5 -5
- package/dist/agent-verifier/{keychain-backend-A3MRWLPF.mjs → keychain-backend-FF4I6ODB.mjs} +11 -6
- package/dist/agent-verifier/keychain-backend-FF4I6ODB.mjs.map +1 -0
- package/dist/agent-verifier/{local-files-QVJ2H3MH.mjs → local-files-OF4QFISU.mjs} +8 -8
- package/dist/agent-verifier/{chunk-UIOLGH4A.mjs → local-typecheck-DHVLM37Z.mjs} +4 -4
- package/dist/agent-verifier/local-typecheck-DHVLM37Z.mjs.map +1 -0
- package/dist/agent-verifier/{materialize-workspace-OZKOQCSQ.mjs → materialize-workspace-JBDL6LF4.mjs} +22 -22
- package/dist/agent-verifier/materialize-workspace-JBDL6LF4.mjs.map +1 -0
- package/dist/agent-verifier/{chunk-Z6OZWUIZ.mjs → reducer-bundle-preflight-GLUJKTWU.mjs} +75 -24
- package/dist/agent-verifier/reducer-bundle-preflight-GLUJKTWU.mjs.map +1 -0
- package/dist/agent-verifier/{chunk-YDIOW2BO.mjs → reducer-contract-preflight-WVQQPW5F.mjs} +7 -6
- package/dist/agent-verifier/reducer-contract-preflight-WVQQPW5F.mjs.map +1 -0
- package/dist/agent-verifier/{chunk-ON62IGWK.mjs → reducer-native-test-harness-XQUPIT5D.mjs} +480 -703
- package/dist/agent-verifier/reducer-native-test-harness-XQUPIT5D.mjs.map +1 -0
- package/dist/agent-verifier/static-scaffold-U5DXE23S.mjs +24 -0
- package/dist/agent-verifier/{workspace-codegen-WPZHMATU.mjs → workspace-codegen-SPPVHURX.mjs} +3 -3
- package/dist/agent-verifier/{workspace-dependencies-B6A2ZX55.mjs → workspace-dependencies-5HEEKZFP.mjs} +5 -3
- package/dist/authoring-compatibility-internal.js +12 -0
- package/dist/chunk-5IYJOVUA.js +3902 -0
- package/dist/chunk-5IYJOVUA.js.map +1 -0
- package/dist/chunk-6NYVJYN4.js +313 -0
- package/dist/chunk-6NYVJYN4.js.map +1 -0
- package/dist/chunk-EQNBQVIW.js +204 -0
- package/dist/chunk-EQNBQVIW.js.map +1 -0
- package/dist/{chunk-M4SCKH5M.js → chunk-USZAPMQ4.js} +2488 -4993
- package/dist/chunk-USZAPMQ4.js.map +1 -0
- package/dist/{global-config-YBFEGJQG.js → global-config-RBMW7IVA.js} +3 -2
- package/dist/index.js +3099 -6188
- package/dist/index.js.map +1 -1
- package/dist/internal.js +35 -9
- package/dist/internal.js.map +1 -1
- package/dist/{keychain-backend-JHTXAKWC.js → keychain-backend-FSNTNTZE.js} +11 -6
- package/dist/keychain-backend-FSNTNTZE.js.map +1 -0
- package/package.json +9 -19
- package/release/authoring-release-set.json +38 -0
- package/skills/dreamboard/SKILL.md +32 -30
- package/skills/dreamboard/references/building-your-first-game.md +16 -16
- package/skills/dreamboard/references/cli.md +54 -54
- package/skills/dreamboard/references/manifest-authoring.md +11 -3
- package/skills/dreamboard/references/quickstart.md +19 -16
- package/skills/dreamboard/references/testing.md +6 -13
- package/dist/agent-verifier/chunk-27EEIZCI.mjs.map +0 -1
- package/dist/agent-verifier/chunk-5NYBTZB4.mjs.map +0 -1
- package/dist/agent-verifier/chunk-776W3UGV.mjs.map +0 -1
- package/dist/agent-verifier/chunk-C3VW3DTA.mjs.map +0 -1
- package/dist/agent-verifier/chunk-F2DIOJJZ.mjs.map +0 -1
- package/dist/agent-verifier/chunk-G42BGGG2.mjs +0 -70
- package/dist/agent-verifier/chunk-G42BGGG2.mjs.map +0 -1
- package/dist/agent-verifier/chunk-H76MT5UR.mjs.map +0 -1
- package/dist/agent-verifier/chunk-IDVQXGAO.mjs +0 -222
- package/dist/agent-verifier/chunk-IDVQXGAO.mjs.map +0 -1
- package/dist/agent-verifier/chunk-JO5AMVZU.mjs +0 -1744
- package/dist/agent-verifier/chunk-JO5AMVZU.mjs.map +0 -1
- package/dist/agent-verifier/chunk-KDBSVLCF.mjs +0 -624
- package/dist/agent-verifier/chunk-KDBSVLCF.mjs.map +0 -1
- package/dist/agent-verifier/chunk-MW2QIWWA.mjs.map +0 -1
- package/dist/agent-verifier/chunk-NAK77WXW.mjs.map +0 -1
- package/dist/agent-verifier/chunk-ON62IGWK.mjs.map +0 -1
- package/dist/agent-verifier/chunk-QZH6IEZS.mjs +0 -39
- package/dist/agent-verifier/chunk-QZH6IEZS.mjs.map +0 -1
- package/dist/agent-verifier/chunk-TAEQKBJB.mjs.map +0 -1
- package/dist/agent-verifier/chunk-UIOLGH4A.mjs.map +0 -1
- package/dist/agent-verifier/chunk-XQXDOBYB.mjs.map +0 -1
- package/dist/agent-verifier/chunk-YDIOW2BO.mjs.map +0 -1
- package/dist/agent-verifier/chunk-Z6OZWUIZ.mjs.map +0 -1
- package/dist/agent-verifier/compile-576O7TYP.mjs +0 -312
- package/dist/agent-verifier/compile-576O7TYP.mjs.map +0 -1
- package/dist/agent-verifier/keychain-backend-A3MRWLPF.mjs.map +0 -1
- package/dist/agent-verifier/local-typecheck-2JWG5IGL.mjs +0 -10
- package/dist/agent-verifier/materialize-workspace-OZKOQCSQ.mjs.map +0 -1
- package/dist/agent-verifier/reducer-bundle-preflight-7NYZF5ZT.mjs +0 -20
- package/dist/agent-verifier/reducer-contract-preflight-COD2CO22.mjs +0 -11
- package/dist/agent-verifier/reducer-native-test-harness-QC7HZUK4.mjs +0 -50
- package/dist/agent-verifier/static-scaffold-JBUE3ROP.mjs +0 -27
- package/dist/agent-verifier/sync-C6S3OGCD.mjs +0 -588
- package/dist/agent-verifier/sync-C6S3OGCD.mjs.map +0 -1
- package/dist/agent-verifier/test-Y5UGQV7J.mjs +0 -353
- package/dist/agent-verifier/test-Y5UGQV7J.mjs.map +0 -1
- package/dist/agent-verifier/workspace-codegen-WPZHMATU.mjs.map +0 -1
- package/dist/agent-verifier/workspace-dependencies-B6A2ZX55.mjs.map +0 -1
- package/dist/chunk-3NRROR4P.js +0 -432
- package/dist/chunk-3NRROR4P.js.map +0 -1
- package/dist/chunk-M4SCKH5M.js.map +0 -1
- package/dist/dev-host/components/drawer.tsx +0 -132
- package/dist/dev-host/components/input.tsx +0 -21
- package/dist/dev-host/dev-api-proxy-plugin.ts +0 -328
- package/dist/dev-host/dev-author-dom-warnings.ts +0 -100
- package/dist/dev-host/dev-diagnostics.ts +0 -62
- package/dist/dev-host/dev-fallback-stylesheet.ts +0 -53
- package/dist/dev-host/dev-hmr-guard-plugin.ts +0 -47
- package/dist/dev-host/dev-host-controller.ts +0 -674
- package/dist/dev-host/dev-host-player-query.ts +0 -17
- package/dist/dev-host/dev-host-session-transport.ts +0 -52
- package/dist/dev-host/dev-host-storage.ts +0 -56
- package/dist/dev-host/dev-log-relay-plugin.ts +0 -510
- package/dist/dev-host/dev-runtime-config.ts +0 -14
- package/dist/dev-host/dev-runtime-platform.ts +0 -335
- package/dist/dev-host/dev-virtual-modules-plugin.ts +0 -64
- package/dist/dev-host/host-main.css +0 -224
- package/dist/dev-host/host-main.tsx +0 -948
- package/dist/dev-host/index.html +0 -56
- package/dist/dev-host/lib/utils.ts +0 -6
- package/dist/dev-host/plugin-main.ts +0 -61
- package/dist/dev-host/plugin.html +0 -24
- package/dist/dev-host/shared-styles.css +0 -144
- package/dist/dev-host/start-dev-server.ts +0 -140
- package/dist/dev-host/virtual-modules.d.ts +0 -27
- package/dist/global-config-YBFEGJQG.js.map +0 -1
- package/dist/keychain-backend-JHTXAKWC.js.map +0 -1
- package/skills/dreamboard/scripts/events-extract.mjs +0 -218
- /package/dist/agent-verifier/{chunk-XKCJBIRY.mjs.map → chunk-QD4SQNUP.mjs.map} +0 -0
- /package/dist/agent-verifier/{global-config-NYCSCAUI.mjs.map → global-config-2NUESNEQ.mjs.map} +0 -0
- /package/dist/agent-verifier/{local-files-QVJ2H3MH.mjs.map → local-files-OF4QFISU.mjs.map} +0 -0
- /package/dist/agent-verifier/{local-typecheck-2JWG5IGL.mjs.map → static-scaffold-U5DXE23S.mjs.map} +0 -0
- /package/dist/agent-verifier/{reducer-bundle-preflight-7NYZF5ZT.mjs.map → workspace-codegen-SPPVHURX.mjs.map} +0 -0
- /package/dist/agent-verifier/{reducer-contract-preflight-COD2CO22.mjs.map → workspace-dependencies-5HEEKZFP.mjs.map} +0 -0
- /package/dist/{agent-verifier/reducer-native-test-harness-QC7HZUK4.mjs.map → authoring-compatibility-internal.js.map} +0 -0
- /package/dist/{agent-verifier/static-scaffold-JBUE3ROP.mjs.map → global-config-RBMW7IVA.js.map} +0 -0
- /package/{dist/scaffold → scaffold}/assets/static/app/tsconfig.framework.json +0 -0
- /package/{dist/scaffold → scaffold}/assets/static/app/tsconfig.json +0 -0
- /package/{dist/scaffold → scaffold}/assets/static/ui/index.tsx +0 -0
- /package/{dist/scaffold → scaffold}/assets/static/ui/style.css +0 -0
- /package/{dist/scaffold → scaffold}/assets/static/ui/tsconfig.framework.json +0 -0
- /package/{dist/scaffold → scaffold}/assets/static/ui/tsconfig.json +0 -0
|
@@ -0,0 +1,3902 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
CLIENT_PROBLEM_TYPES,
|
|
4
|
+
REDUCER_TESTING_TYPES_WRAPPER_CONTENT,
|
|
5
|
+
SERVER_PROBLEM_TYPES,
|
|
6
|
+
buildReducerTestingContractContent,
|
|
7
|
+
client,
|
|
8
|
+
createGameplayCapability,
|
|
9
|
+
createProjectSessionFromReducerSnapshot,
|
|
10
|
+
createRepoLocalPackageResolutionPlugin,
|
|
11
|
+
external_exports,
|
|
12
|
+
findProjectRoot,
|
|
13
|
+
getJob,
|
|
14
|
+
getProjectCompiledResult,
|
|
15
|
+
getSessionSnapshot,
|
|
16
|
+
hashContent,
|
|
17
|
+
listProjectCompiledResults,
|
|
18
|
+
loadManifest,
|
|
19
|
+
loadProjectConfig,
|
|
20
|
+
readWorkspaceTextFileIfExists,
|
|
21
|
+
resolveCliRepoRoot,
|
|
22
|
+
resolveWorkspacePath,
|
|
23
|
+
workspacePathExists,
|
|
24
|
+
writeWorkspaceJsonFile,
|
|
25
|
+
writeWorkspaceTextFile,
|
|
26
|
+
zProblemDetails
|
|
27
|
+
} from "./chunk-USZAPMQ4.js";
|
|
28
|
+
import {
|
|
29
|
+
getStoredSession,
|
|
30
|
+
loadGlobalConfig,
|
|
31
|
+
withCredentialLock
|
|
32
|
+
} from "./chunk-6NYVJYN4.js";
|
|
33
|
+
import {
|
|
34
|
+
DEFAULT_API_BASE_URL,
|
|
35
|
+
DEFAULT_WEB_BASE_URL,
|
|
36
|
+
ENVIRONMENT_CONFIGS,
|
|
37
|
+
PROJECT_DIR_NAME,
|
|
38
|
+
ensureDir,
|
|
39
|
+
exists
|
|
40
|
+
} from "./chunk-EQNBQVIW.js";
|
|
41
|
+
|
|
42
|
+
// src/build-target.ts
|
|
43
|
+
var injectedBuildChannel = true ? "published" : void 0;
|
|
44
|
+
var injectedPackageVersion = true ? "0.1.30-alpha.40" : void 0;
|
|
45
|
+
var BUILD_CHANNEL = injectedBuildChannel === "published" ? "published" : "development";
|
|
46
|
+
var PACKAGE_VERSION = injectedPackageVersion ?? "0.0.0-development";
|
|
47
|
+
function isAlphaReleaseVersion(version) {
|
|
48
|
+
return /(?:^|-|\.)alpha(?:$|-|\.)/.test(version);
|
|
49
|
+
}
|
|
50
|
+
var IS_PUBLISHED_BUILD = BUILD_CHANNEL === "published";
|
|
51
|
+
var IS_ALPHA_RELEASE = isAlphaReleaseVersion(PACKAGE_VERSION);
|
|
52
|
+
var CAN_SELECT_ENVIRONMENT = !IS_PUBLISHED_BUILD || IS_ALPHA_RELEASE;
|
|
53
|
+
var PUBLISHED_ENVIRONMENT = "prod";
|
|
54
|
+
|
|
55
|
+
// src/auth/refresh-error.ts
|
|
56
|
+
function classifyRefreshError(error) {
|
|
57
|
+
const message = error.message?.toLowerCase() ?? "";
|
|
58
|
+
if (error.status === 400 || error.status === 401 || message.includes("invalid_grant") || message.includes("refresh token") || message.includes("expired") || message.includes("revoked")) {
|
|
59
|
+
return { kind: "permanent_invalid", reason: error.message };
|
|
60
|
+
}
|
|
61
|
+
if (error.status === 408 || error.status === 429 || typeof error.status === "number" && error.status >= 500 || message.includes("timeout") || message.includes("network") || message.includes("fetch failed")) {
|
|
62
|
+
return { kind: "transient", reason: error.message };
|
|
63
|
+
}
|
|
64
|
+
return { kind: "unknown", reason: error.message };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/auth/clerk-oauth.ts
|
|
68
|
+
import crypto from "crypto";
|
|
69
|
+
function createPkcePair() {
|
|
70
|
+
const verifier = base64Url(crypto.randomBytes(32));
|
|
71
|
+
const challenge = base64Url(
|
|
72
|
+
crypto.createHash("sha256").update(verifier).digest()
|
|
73
|
+
);
|
|
74
|
+
return { verifier, challenge };
|
|
75
|
+
}
|
|
76
|
+
function buildClerkAuthorizationUrl(input) {
|
|
77
|
+
const { issuer, clientId, scope } = assertConfigured(input.config);
|
|
78
|
+
const url = new URL("/oauth/authorize", issuer);
|
|
79
|
+
url.searchParams.set("response_type", "code");
|
|
80
|
+
url.searchParams.set("client_id", clientId);
|
|
81
|
+
url.searchParams.set("redirect_uri", input.redirectUri);
|
|
82
|
+
url.searchParams.set("state", input.state);
|
|
83
|
+
url.searchParams.set("code_challenge", input.codeChallenge);
|
|
84
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
85
|
+
url.searchParams.set("scope", scope ?? "openid profile email offline_access");
|
|
86
|
+
return url;
|
|
87
|
+
}
|
|
88
|
+
async function exchangeClerkOAuthCode(input) {
|
|
89
|
+
const { clientId, tokenUrl } = assertConfigured(input.config);
|
|
90
|
+
const body = new URLSearchParams({
|
|
91
|
+
grant_type: "authorization_code",
|
|
92
|
+
client_id: clientId,
|
|
93
|
+
code: input.code,
|
|
94
|
+
redirect_uri: input.redirectUri,
|
|
95
|
+
code_verifier: input.codeVerifier
|
|
96
|
+
});
|
|
97
|
+
return requestClerkToken(tokenUrl, body);
|
|
98
|
+
}
|
|
99
|
+
async function refreshClerkOAuthToken(input) {
|
|
100
|
+
const { clientId, tokenUrl } = assertConfigured(input.config);
|
|
101
|
+
const body = new URLSearchParams({
|
|
102
|
+
grant_type: "refresh_token",
|
|
103
|
+
client_id: clientId,
|
|
104
|
+
refresh_token: input.refreshToken
|
|
105
|
+
});
|
|
106
|
+
return requestClerkToken(tokenUrl, body);
|
|
107
|
+
}
|
|
108
|
+
function assertConfigured(config) {
|
|
109
|
+
const issuer = config.issuer?.trim().replace(/\/$/, "");
|
|
110
|
+
const clientId = config.clientId?.trim();
|
|
111
|
+
if (!issuer || !clientId) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
[
|
|
114
|
+
"Clerk OAuth CLI is not configured for this environment.",
|
|
115
|
+
"The CLI expects first-party environments to be configured in its built-in registry.",
|
|
116
|
+
"If this environment has no registered public Clerk OAuth client, create one and release a CLI with its client id.",
|
|
117
|
+
"For emergency overrides, set the environment-specific DREAMBOARD_<ENV>_CLERK_OAUTH_* variables or DREAMBOARD_CLERK_OAUTH_*.",
|
|
118
|
+
"For local harness auth, use `pnpm auth:local` or the auto-bootstrapped local harness flows instead."
|
|
119
|
+
].join(" ")
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
issuer,
|
|
124
|
+
clientId,
|
|
125
|
+
tokenUrl: config.tokenUrl?.trim() || new URL("/oauth/token", issuer).toString(),
|
|
126
|
+
scope: config.scope?.trim() || void 0
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
async function requestClerkToken(tokenUrl, body) {
|
|
130
|
+
const response = await fetch(tokenUrl, {
|
|
131
|
+
method: "POST",
|
|
132
|
+
headers: {
|
|
133
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
134
|
+
Accept: "application/json"
|
|
135
|
+
},
|
|
136
|
+
body
|
|
137
|
+
});
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
const detail = await response.text();
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Clerk OAuth token request failed (${response.status}): ${detail}`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
const payload = await response.json();
|
|
145
|
+
if (typeof payload.access_token !== "string") {
|
|
146
|
+
throw new Error("Clerk OAuth token response did not include access_token.");
|
|
147
|
+
}
|
|
148
|
+
if (typeof payload.refresh_token !== "string") {
|
|
149
|
+
throw new Error(
|
|
150
|
+
"Clerk OAuth token response did not include refresh_token."
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
const expiresAt = typeof payload.expires_in === "number" ? new Date(Date.now() + payload.expires_in * 1e3).toISOString() : void 0;
|
|
154
|
+
return {
|
|
155
|
+
accessToken: payload.access_token,
|
|
156
|
+
refreshToken: payload.refresh_token,
|
|
157
|
+
expiresAt,
|
|
158
|
+
tokenUrl
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function base64Url(bytes) {
|
|
162
|
+
return bytes.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/auth/token-exchange.ts
|
|
166
|
+
async function exchangeDreamboardUserToken(input) {
|
|
167
|
+
const fetchImpl = input.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
168
|
+
const response = await fetchImpl(
|
|
169
|
+
new URL("/api/auth/token-exchange", input.apiBaseUrl),
|
|
170
|
+
{
|
|
171
|
+
method: "POST",
|
|
172
|
+
headers: {
|
|
173
|
+
Authorization: `Bearer ${input.clerkAccessToken}`,
|
|
174
|
+
"Content-Type": "application/json",
|
|
175
|
+
Accept: "application/json"
|
|
176
|
+
},
|
|
177
|
+
body: JSON.stringify({ audience: input.audience })
|
|
178
|
+
}
|
|
179
|
+
);
|
|
180
|
+
if (!response.ok) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`Dreamboard token exchange failed (${response.status}). Run \`dreamboard auth login\` to authenticate again.`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
const payload = await response.json();
|
|
186
|
+
if (typeof payload.accessToken !== "string" || payload.accessToken === "") {
|
|
187
|
+
throw new Error("Dreamboard token exchange response omitted accessToken.");
|
|
188
|
+
}
|
|
189
|
+
if (payload.tokenType !== "Bearer") {
|
|
190
|
+
throw new Error(
|
|
191
|
+
"Dreamboard token exchange response had invalid tokenType."
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
if (payload.audience !== input.audience) {
|
|
195
|
+
throw new Error("Dreamboard token exchange response had wrong audience.");
|
|
196
|
+
}
|
|
197
|
+
const expiresIn = typeof payload.expiresIn === "number" && Number.isFinite(payload.expiresIn) ? payload.expiresIn : void 0;
|
|
198
|
+
return {
|
|
199
|
+
accessToken: payload.accessToken,
|
|
200
|
+
tokenType: "Bearer",
|
|
201
|
+
audience: input.audience,
|
|
202
|
+
expiresIn,
|
|
203
|
+
expiresAt: expiresIn === void 0 ? void 0 : new Date(Date.now() + expiresIn * 1e3).toISOString()
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/auth/user-session-manager.ts
|
|
208
|
+
var DEFAULT_TOKEN_MIN_VALIDITY_MS = 60 * 1e3;
|
|
209
|
+
function createUserSessionManager(config) {
|
|
210
|
+
return {
|
|
211
|
+
async establishRefreshableSession(session) {
|
|
212
|
+
return withCredentialLock(async (ops) => {
|
|
213
|
+
const credentials = credentialsFromRefreshableSession(session);
|
|
214
|
+
await ops.writeFull(credentials);
|
|
215
|
+
const exchanged = await exchangeDreamboardUserToken({
|
|
216
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
217
|
+
clerkAccessToken: credentials.accessToken,
|
|
218
|
+
audience: "dreamboard-api"
|
|
219
|
+
});
|
|
220
|
+
const apiToken = toAccessToken(exchanged);
|
|
221
|
+
await ops.writeFull(withApiToken(credentials, apiToken));
|
|
222
|
+
return apiToken;
|
|
223
|
+
});
|
|
224
|
+
},
|
|
225
|
+
async establishAccessOnlySession(accessToken) {
|
|
226
|
+
await withCredentialLock((ops) => ops.writeAccessOnly(accessToken));
|
|
227
|
+
},
|
|
228
|
+
async resolveApiToken(options) {
|
|
229
|
+
const localOrInjected = resolveNonStoredToken(config, "dreamboard-api");
|
|
230
|
+
if (localOrInjected) return localOrInjected;
|
|
231
|
+
if (!usesStoredSession(config)) return null;
|
|
232
|
+
const minValidityMs = options?.minValiditySeconds === void 0 ? DEFAULT_TOKEN_MIN_VALIDITY_MS : Math.max(0, options.minValiditySeconds * 1e3);
|
|
233
|
+
return withCredentialLock(async (ops) => {
|
|
234
|
+
const stored = await ops.read();
|
|
235
|
+
return resolveStoredApiToken(ops, config, stored, minValidityMs);
|
|
236
|
+
});
|
|
237
|
+
},
|
|
238
|
+
async resolveGitToken() {
|
|
239
|
+
const localOrInjected = resolveNonStoredToken(config, "dreamboard-git");
|
|
240
|
+
if (localOrInjected) return localOrInjected;
|
|
241
|
+
if (!usesStoredSession(config)) {
|
|
242
|
+
throw missingSessionError();
|
|
243
|
+
}
|
|
244
|
+
return withCredentialLock(async (ops) => {
|
|
245
|
+
const stored = await ops.read();
|
|
246
|
+
const clerk = await resolveFreshClerkSession(ops, config, stored);
|
|
247
|
+
return toAccessToken(
|
|
248
|
+
await exchangeDreamboardUserToken({
|
|
249
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
250
|
+
clerkAccessToken: clerk.accessToken,
|
|
251
|
+
audience: "dreamboard-git"
|
|
252
|
+
})
|
|
253
|
+
);
|
|
254
|
+
});
|
|
255
|
+
},
|
|
256
|
+
async inspectSession() {
|
|
257
|
+
const localOrInjected = resolveNonStoredToken(config, "dreamboard-api");
|
|
258
|
+
if (localOrInjected) {
|
|
259
|
+
return inspectAccessOnlyToken(localOrInjected);
|
|
260
|
+
}
|
|
261
|
+
if (!usesStoredSession(config)) {
|
|
262
|
+
return { kind: "none" };
|
|
263
|
+
}
|
|
264
|
+
return withCredentialLock(async (ops) => {
|
|
265
|
+
const stored = await ops.read();
|
|
266
|
+
if (!stored) return { kind: "none" };
|
|
267
|
+
const cached = freshStoredApiToken(
|
|
268
|
+
stored,
|
|
269
|
+
DEFAULT_TOKEN_MIN_VALIDITY_MS
|
|
270
|
+
);
|
|
271
|
+
if (cached) {
|
|
272
|
+
return activeStatus("refreshable", cached, false);
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
const token = await resolveStoredApiToken(
|
|
276
|
+
ops,
|
|
277
|
+
config,
|
|
278
|
+
stored,
|
|
279
|
+
DEFAULT_TOKEN_MIN_VALIDITY_MS
|
|
280
|
+
);
|
|
281
|
+
return activeStatus("refreshable", token, true);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
return failedRefreshableStatus(error);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
},
|
|
287
|
+
async logout() {
|
|
288
|
+
await withCredentialLock((ops) => ops.clear("logout_command"));
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
async function resolveStoredApiToken(ops, config, stored, minValidityMs) {
|
|
293
|
+
const cached = freshStoredApiToken(stored, minValidityMs);
|
|
294
|
+
if (cached) return cached;
|
|
295
|
+
const clerk = await resolveFreshClerkSession(ops, config, stored);
|
|
296
|
+
const exchanged = await exchangeDreamboardUserToken({
|
|
297
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
298
|
+
clerkAccessToken: clerk.accessToken,
|
|
299
|
+
audience: "dreamboard-api"
|
|
300
|
+
});
|
|
301
|
+
const apiToken = toAccessToken(exchanged);
|
|
302
|
+
await ops.writeFull(withApiToken(clerk, apiToken));
|
|
303
|
+
return apiToken;
|
|
304
|
+
}
|
|
305
|
+
async function resolveFreshClerkSession(ops, config, stored) {
|
|
306
|
+
if (!stored) {
|
|
307
|
+
throw missingSessionError();
|
|
308
|
+
}
|
|
309
|
+
if (!stored.refreshToken) {
|
|
310
|
+
throw new Error(
|
|
311
|
+
"Stored Dreamboard session is missing its refresh token. Run `dreamboard auth login` to authenticate again."
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
if (stored.accessToken && isFresh(
|
|
315
|
+
stored.tokenExpiresAt,
|
|
316
|
+
stored.accessToken,
|
|
317
|
+
DEFAULT_TOKEN_MIN_VALIDITY_MS
|
|
318
|
+
)) {
|
|
319
|
+
return credentialsFromStored(config, stored);
|
|
320
|
+
}
|
|
321
|
+
const payload = await refreshClerkOAuthToken({
|
|
322
|
+
config: {
|
|
323
|
+
issuer: stored.clerkOAuthIssuer ?? config.clerkOAuthIssuer,
|
|
324
|
+
clientId: stored.clerkOAuthClientId ?? config.clerkOAuthClientId,
|
|
325
|
+
tokenUrl: stored.clerkOAuthTokenUrl ?? config.clerkOAuthTokenUrl
|
|
326
|
+
},
|
|
327
|
+
refreshToken: stored.refreshToken
|
|
328
|
+
});
|
|
329
|
+
const refreshed = {
|
|
330
|
+
accessToken: payload.accessToken,
|
|
331
|
+
refreshToken: payload.refreshToken,
|
|
332
|
+
tokenExpiresAt: payload.expiresAt,
|
|
333
|
+
dreamboardApiToken: stored.dreamboardApiToken,
|
|
334
|
+
dreamboardApiExpiresAt: stored.dreamboardApiExpiresAt,
|
|
335
|
+
clerkOAuthIssuer: stored.clerkOAuthIssuer ?? config.clerkOAuthIssuer,
|
|
336
|
+
clerkOAuthClientId: stored.clerkOAuthClientId ?? config.clerkOAuthClientId,
|
|
337
|
+
clerkOAuthTokenUrl: payload.tokenUrl,
|
|
338
|
+
environment: stored.environment ?? config.environment
|
|
339
|
+
};
|
|
340
|
+
await ops.writeFull(refreshed);
|
|
341
|
+
return refreshed;
|
|
342
|
+
}
|
|
343
|
+
function credentialsFromStored(config, stored) {
|
|
344
|
+
if (!stored.accessToken || !stored.refreshToken) {
|
|
345
|
+
throw missingSessionError();
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
accessToken: stored.accessToken,
|
|
349
|
+
refreshToken: stored.refreshToken,
|
|
350
|
+
tokenExpiresAt: stored.tokenExpiresAt,
|
|
351
|
+
dreamboardApiToken: stored.dreamboardApiToken,
|
|
352
|
+
dreamboardApiExpiresAt: stored.dreamboardApiExpiresAt,
|
|
353
|
+
clerkOAuthIssuer: stored.clerkOAuthIssuer ?? config.clerkOAuthIssuer,
|
|
354
|
+
clerkOAuthClientId: stored.clerkOAuthClientId ?? config.clerkOAuthClientId,
|
|
355
|
+
clerkOAuthTokenUrl: stored.clerkOAuthTokenUrl ?? config.clerkOAuthTokenUrl,
|
|
356
|
+
environment: stored.environment ?? config.environment
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function credentialsFromRefreshableSession(session) {
|
|
360
|
+
return {
|
|
361
|
+
accessToken: session.clerkAccessToken,
|
|
362
|
+
refreshToken: session.refreshToken,
|
|
363
|
+
tokenExpiresAt: session.clerkAccessExpiresAt,
|
|
364
|
+
clerkOAuthIssuer: session.clerkOAuthIssuer,
|
|
365
|
+
clerkOAuthClientId: session.clerkOAuthClientId,
|
|
366
|
+
clerkOAuthTokenUrl: session.clerkOAuthTokenUrl,
|
|
367
|
+
environment: session.environment
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
function withApiToken(credentials, token) {
|
|
371
|
+
return {
|
|
372
|
+
...credentials,
|
|
373
|
+
dreamboardApiToken: token.token,
|
|
374
|
+
dreamboardApiExpiresAt: token.expiresAt
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
function toAccessToken(token) {
|
|
378
|
+
return {
|
|
379
|
+
token: token.accessToken,
|
|
380
|
+
expiresAt: token.expiresAt,
|
|
381
|
+
audience: token.audience
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
function resolveNonStoredToken(config, audience) {
|
|
385
|
+
if (usesStoredSession(config)) return null;
|
|
386
|
+
if (!config.authToken) return null;
|
|
387
|
+
return {
|
|
388
|
+
token: config.authToken,
|
|
389
|
+
expiresAt: config.tokenExpiresAt,
|
|
390
|
+
audience
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function freshStoredApiToken(stored, minValidityMs) {
|
|
394
|
+
if (!stored?.dreamboardApiToken) return null;
|
|
395
|
+
if (isFresh(
|
|
396
|
+
stored.dreamboardApiExpiresAt,
|
|
397
|
+
stored.dreamboardApiToken,
|
|
398
|
+
minValidityMs
|
|
399
|
+
)) {
|
|
400
|
+
return {
|
|
401
|
+
token: stored.dreamboardApiToken,
|
|
402
|
+
expiresAt: stored.dreamboardApiExpiresAt,
|
|
403
|
+
audience: "dreamboard-api"
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
function inspectAccessOnlyToken(token) {
|
|
409
|
+
const expiry = resolveExpiry(token.expiresAt, token.token);
|
|
410
|
+
if (expiry && expiry.getTime() <= Date.now()) {
|
|
411
|
+
return {
|
|
412
|
+
kind: "invalid",
|
|
413
|
+
sessionKind: "access-only",
|
|
414
|
+
message: "Stored Dreamboard access token is expired. Run `dreamboard auth login` to authenticate again."
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
return activeStatus("access-only", token, false);
|
|
418
|
+
}
|
|
419
|
+
function failedRefreshableStatus(error) {
|
|
420
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
421
|
+
const errorStatus = typeof error === "object" && error !== null && "status" in error ? error.status : void 0;
|
|
422
|
+
const classification = classifyRefreshError({
|
|
423
|
+
message,
|
|
424
|
+
status: typeof errorStatus === "number" ? errorStatus : void 0
|
|
425
|
+
});
|
|
426
|
+
if (classification.kind === "permanent_invalid") {
|
|
427
|
+
return {
|
|
428
|
+
kind: "invalid",
|
|
429
|
+
sessionKind: "refreshable",
|
|
430
|
+
message
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
kind: "degraded",
|
|
435
|
+
sessionKind: "refreshable",
|
|
436
|
+
message
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
function activeStatus(sessionKind, apiToken, repaired) {
|
|
440
|
+
return {
|
|
441
|
+
kind: "active",
|
|
442
|
+
sessionKind,
|
|
443
|
+
apiToken,
|
|
444
|
+
repaired
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
function missingSessionError() {
|
|
448
|
+
return new Error(
|
|
449
|
+
"Missing Dreamboard session. Run `dreamboard auth login` to authenticate."
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
function isFresh(expiresAt, token, minValidityMs) {
|
|
453
|
+
const expiry = resolveExpiry(expiresAt, token);
|
|
454
|
+
return expiry !== null && Number.isFinite(expiry.getTime()) && expiry.getTime() > Date.now() + minValidityMs;
|
|
455
|
+
}
|
|
456
|
+
function resolveExpiry(expiresAt, token) {
|
|
457
|
+
if (expiresAt) {
|
|
458
|
+
const parsed = new Date(expiresAt);
|
|
459
|
+
return Number.isFinite(parsed.getTime()) ? parsed : null;
|
|
460
|
+
}
|
|
461
|
+
return getJwtExpiry(token);
|
|
462
|
+
}
|
|
463
|
+
function getJwtExpiry(accessToken) {
|
|
464
|
+
if (!accessToken) return null;
|
|
465
|
+
const parts = accessToken.split(".");
|
|
466
|
+
if (parts.length !== 3) return null;
|
|
467
|
+
try {
|
|
468
|
+
const payload = JSON.parse(
|
|
469
|
+
Buffer.from(parts[1], "base64url").toString("utf8")
|
|
470
|
+
);
|
|
471
|
+
if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) {
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
return new Date(payload.exp * 1e3);
|
|
475
|
+
} catch {
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
function usesStoredSession(config) {
|
|
480
|
+
return config.refreshTokenSource === "global";
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// src/config/local-harness-auth.ts
|
|
484
|
+
import { createHmac, randomUUID } from "crypto";
|
|
485
|
+
var DEFAULT_SUBJECT = "harness-smoke-local@dreamboard.local";
|
|
486
|
+
var DEFAULT_ISSUER = "dreamboard-local-harness";
|
|
487
|
+
var DEFAULT_SECRET = "dreamboard-local-harness-token-secret";
|
|
488
|
+
var LOCAL_AWS_ISSUER = "dreamboard-local-aws-harness";
|
|
489
|
+
var LOCAL_AWS_SECRET = "dreamboard-local-aws-harness-token-secret";
|
|
490
|
+
var DEFAULT_TTL_SECONDS = 8 * 60 * 60;
|
|
491
|
+
var mintedTokens = /* @__PURE__ */ new Map();
|
|
492
|
+
function resolveLocalHarnessAccessToken(config) {
|
|
493
|
+
if (IS_PUBLISHED_BUILD || config.environment !== "local") {
|
|
494
|
+
return void 0;
|
|
495
|
+
}
|
|
496
|
+
const profile = inferLocalHarnessProfile(config);
|
|
497
|
+
if (config.authToken && (profile !== "local-aws" || isExplicitTokenSource(config.authTokenSource))) {
|
|
498
|
+
return void 0;
|
|
499
|
+
}
|
|
500
|
+
const cacheKey = [
|
|
501
|
+
profile,
|
|
502
|
+
process.env.LOCAL_HARNESS_TOKEN_ISSUER ?? "",
|
|
503
|
+
process.env.LOCAL_HARNESS_TOKEN_SECRET ?? "",
|
|
504
|
+
process.env.LOCAL_HARNESS_SUBJECT ?? "",
|
|
505
|
+
process.env.HARNESS_USER_EMAIL ?? "",
|
|
506
|
+
process.env.LOCAL_HARNESS_EMAIL ?? "",
|
|
507
|
+
process.env.LOCAL_HARNESS_TOKEN_TTL_SECONDS ?? ""
|
|
508
|
+
].join("\0");
|
|
509
|
+
const cached = mintedTokens.get(cacheKey);
|
|
510
|
+
if (cached) return cached;
|
|
511
|
+
const token = mintLocalHarnessToken(profile);
|
|
512
|
+
mintedTokens.set(cacheKey, token);
|
|
513
|
+
return token;
|
|
514
|
+
}
|
|
515
|
+
function isExplicitTokenSource(source) {
|
|
516
|
+
return source === "flag" || source === "env" || source === "agent-env";
|
|
517
|
+
}
|
|
518
|
+
function inferLocalHarnessProfile(config) {
|
|
519
|
+
return isLocalAwsUrl(config.apiBaseUrl) || isLocalAwsUrl(config.webBaseUrl) ? "local-aws" : "local";
|
|
520
|
+
}
|
|
521
|
+
function mintLocalHarnessToken(profile) {
|
|
522
|
+
const subject = envValue(process.env.LOCAL_HARNESS_SUBJECT) ?? envValue(process.env.HARNESS_USER_EMAIL) ?? DEFAULT_SUBJECT;
|
|
523
|
+
const email = envValue(process.env.LOCAL_HARNESS_EMAIL) ?? (subject.includes("@") ? subject : void 0);
|
|
524
|
+
const issuer = envValue(process.env.LOCAL_HARNESS_TOKEN_ISSUER) ?? (profile === "local-aws" ? LOCAL_AWS_ISSUER : DEFAULT_ISSUER);
|
|
525
|
+
const secret = envValue(process.env.LOCAL_HARNESS_TOKEN_SECRET) ?? (profile === "local-aws" ? LOCAL_AWS_SECRET : DEFAULT_SECRET);
|
|
526
|
+
const ttlSeconds = Number(
|
|
527
|
+
envValue(process.env.LOCAL_HARNESS_TOKEN_TTL_SECONDS) ?? String(DEFAULT_TTL_SECONDS)
|
|
528
|
+
);
|
|
529
|
+
if (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0) {
|
|
530
|
+
throw new Error(
|
|
531
|
+
"LOCAL_HARNESS_TOKEN_TTL_SECONDS must be a positive number."
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
535
|
+
const payload = {
|
|
536
|
+
typ: "local_harness_access",
|
|
537
|
+
dreamboard_provider: "local-harness",
|
|
538
|
+
dreamboard_provider_subject: subject,
|
|
539
|
+
...email ? { email } : {},
|
|
540
|
+
iss: issuer,
|
|
541
|
+
sub: subject,
|
|
542
|
+
iat: now,
|
|
543
|
+
exp: now + Math.floor(ttlSeconds),
|
|
544
|
+
jti: randomUUID()
|
|
545
|
+
};
|
|
546
|
+
const headerPart = base64UrlJson({ alg: "HS256", typ: "JWT" });
|
|
547
|
+
const payloadPart = base64UrlJson(payload);
|
|
548
|
+
const signature = createHmac("sha256", secret).update(`${headerPart}.${payloadPart}`).digest("base64url");
|
|
549
|
+
return `${headerPart}.${payloadPart}.${signature}`;
|
|
550
|
+
}
|
|
551
|
+
function base64UrlJson(value) {
|
|
552
|
+
return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
|
|
553
|
+
}
|
|
554
|
+
function envValue(raw) {
|
|
555
|
+
return typeof raw === "string" && raw.trim().length > 0 ? raw.trim() : void 0;
|
|
556
|
+
}
|
|
557
|
+
function isLocalAwsUrl(rawUrl) {
|
|
558
|
+
if (!rawUrl) return false;
|
|
559
|
+
try {
|
|
560
|
+
const url = new URL(rawUrl);
|
|
561
|
+
return (url.hostname === "localhost" || url.hostname === "127.0.0.1") && (url.port === "18080" || url.port === "8088");
|
|
562
|
+
} catch {
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// src/config/resolve.ts
|
|
568
|
+
var TRANSIENT_READ_RETRY_DELAYS_MS = [100, 300];
|
|
569
|
+
function resolveConfig(globalConfig, flags, project, credentials) {
|
|
570
|
+
if (IS_PUBLISHED_BUILD) {
|
|
571
|
+
assertPublicRuntimeFlags(flags);
|
|
572
|
+
}
|
|
573
|
+
const envEnvironment = CAN_SELECT_ENVIRONMENT ? environmentFromProcess() : void 0;
|
|
574
|
+
const projectEnvironment = CAN_SELECT_ENVIRONMENT ? project?.environment : void 0;
|
|
575
|
+
const environment = CAN_SELECT_ENVIRONMENT ? flags.env || envEnvironment || projectEnvironment || globalConfig.environment || (IS_PUBLISHED_BUILD ? PUBLISHED_ENVIRONMENT : "staging") : PUBLISHED_ENVIRONMENT;
|
|
576
|
+
const envConfig = ENVIRONMENT_CONFIGS[environment];
|
|
577
|
+
const publishedEnvConfig = ENVIRONMENT_CONFIGS[PUBLISHED_ENVIRONMENT];
|
|
578
|
+
const hasExplicitEnvironmentOverride = CAN_SELECT_ENVIRONMENT && Boolean(flags.env || envEnvironment || projectEnvironment);
|
|
579
|
+
const resolvedApiBaseUrl = IS_PUBLISHED_BUILD && !CAN_SELECT_ENVIRONMENT ? publishedEnvConfig?.apiBaseUrl ?? DEFAULT_API_BASE_URL : hasExplicitEnvironmentOverride ? projectLocalBaseUrl(project?.apiBaseUrl, environment) || envConfig?.apiBaseUrl || DEFAULT_API_BASE_URL : project?.apiBaseUrl || envConfig?.apiBaseUrl || DEFAULT_API_BASE_URL;
|
|
580
|
+
const apiBaseUrl = valueOrUndefined(process.env.DREAMBOARD_API_BASE_URL) ?? resolvedApiBaseUrl;
|
|
581
|
+
const resolvedWebBaseUrl = IS_PUBLISHED_BUILD && !CAN_SELECT_ENVIRONMENT ? publishedEnvConfig?.webBaseUrl ?? DEFAULT_WEB_BASE_URL : hasExplicitEnvironmentOverride ? projectLocalBaseUrl(project?.webBaseUrl, environment) || envConfig?.webBaseUrl || DEFAULT_WEB_BASE_URL : project?.webBaseUrl || envConfig?.webBaseUrl || DEFAULT_WEB_BASE_URL;
|
|
582
|
+
const webBaseUrl = valueOrUndefined(process.env.DREAMBOARD_WEB_BASE_URL) ?? resolvedWebBaseUrl;
|
|
583
|
+
const snapshot = buildCredentialSnapshot(flags, credentials, environment);
|
|
584
|
+
const oauthConfig = resolveEnvironmentOAuthConfig(environment, envConfig);
|
|
585
|
+
return {
|
|
586
|
+
environment,
|
|
587
|
+
apiBaseUrl,
|
|
588
|
+
webBaseUrl,
|
|
589
|
+
authToken: snapshot.dreamboardApiToken ?? (snapshot.refreshToken ? void 0 : snapshot.accessToken),
|
|
590
|
+
refreshToken: snapshot.refreshToken,
|
|
591
|
+
tokenExpiresAt: snapshot.dreamboardApiExpiresAt ?? (snapshot.refreshToken ? void 0 : snapshot.tokenExpiresAt),
|
|
592
|
+
clerkAccessToken: snapshot.accessToken,
|
|
593
|
+
clerkAccessExpiresAt: snapshot.tokenExpiresAt,
|
|
594
|
+
dreamboardApiToken: snapshot.dreamboardApiToken,
|
|
595
|
+
dreamboardApiExpiresAt: snapshot.dreamboardApiExpiresAt,
|
|
596
|
+
clerkOAuthIssuer: snapshot.clerkOAuthIssuer ?? oauthConfig.issuer,
|
|
597
|
+
clerkOAuthClientId: snapshot.clerkOAuthClientId ?? oauthConfig.clientId,
|
|
598
|
+
clerkOAuthTokenUrl: snapshot.clerkOAuthTokenUrl ?? oauthConfig.tokenUrl,
|
|
599
|
+
clerkOAuthScope: oauthConfig.scope,
|
|
600
|
+
authTokenSource: snapshot.authTokenSource,
|
|
601
|
+
refreshTokenSource: snapshot.refreshTokenSource
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
function resolveEnvironmentOAuthConfig(environment, envConfig) {
|
|
605
|
+
const prefix = environment.toUpperCase();
|
|
606
|
+
return {
|
|
607
|
+
issuer: valueOrUndefined(
|
|
608
|
+
process.env[`DREAMBOARD_${prefix}_CLERK_OAUTH_ISSUER`]
|
|
609
|
+
) ?? valueOrUndefined(process.env.DREAMBOARD_CLERK_OAUTH_ISSUER) ?? envConfig?.clerkOAuthIssuer,
|
|
610
|
+
clientId: valueOrUndefined(
|
|
611
|
+
process.env[`DREAMBOARD_${prefix}_CLERK_OAUTH_CLIENT_ID`]
|
|
612
|
+
) ?? valueOrUndefined(process.env.DREAMBOARD_CLERK_OAUTH_CLIENT_ID) ?? envConfig?.clerkOAuthClientId,
|
|
613
|
+
tokenUrl: valueOrUndefined(
|
|
614
|
+
process.env[`DREAMBOARD_${prefix}_CLERK_OAUTH_TOKEN_URL`]
|
|
615
|
+
) ?? valueOrUndefined(process.env.DREAMBOARD_CLERK_OAUTH_TOKEN_URL) ?? envConfig?.clerkOAuthTokenUrl,
|
|
616
|
+
scope: valueOrUndefined(process.env[`DREAMBOARD_${prefix}_CLERK_OAUTH_SCOPE`]) ?? valueOrUndefined(process.env.DREAMBOARD_CLERK_OAUTH_SCOPE) ?? envConfig?.clerkOAuthScope
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
function buildCredentialSnapshot(flags, storedCredentials, environment) {
|
|
620
|
+
const flagToken = valueOrUndefined(flags.token);
|
|
621
|
+
const agentEnvToken = valueOrUndefined(process.env.DREAMBOARD_AGENT_TOKEN);
|
|
622
|
+
const envToken = valueOrUndefined(process.env.DREAMBOARD_TOKEN);
|
|
623
|
+
const environmentScopedStoredCredentials = storedCredentials?.environment && environment && storedCredentials.environment !== environment ? null : storedCredentials ?? null;
|
|
624
|
+
if (IS_PUBLISHED_BUILD) {
|
|
625
|
+
const stored = environmentScopedStoredCredentials;
|
|
626
|
+
if (agentEnvToken) {
|
|
627
|
+
return {
|
|
628
|
+
accessToken: agentEnvToken,
|
|
629
|
+
refreshToken: void 0,
|
|
630
|
+
tokenExpiresAt: void 0,
|
|
631
|
+
authTokenSource: "agent-env",
|
|
632
|
+
refreshTokenSource: "none"
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
return {
|
|
636
|
+
accessToken: stored?.accessToken,
|
|
637
|
+
refreshToken: stored?.refreshToken,
|
|
638
|
+
tokenExpiresAt: stored?.tokenExpiresAt,
|
|
639
|
+
dreamboardApiToken: stored?.dreamboardApiToken,
|
|
640
|
+
dreamboardApiExpiresAt: stored?.dreamboardApiExpiresAt,
|
|
641
|
+
clerkOAuthIssuer: stored?.clerkOAuthIssuer,
|
|
642
|
+
clerkOAuthClientId: stored?.clerkOAuthClientId,
|
|
643
|
+
clerkOAuthTokenUrl: stored?.clerkOAuthTokenUrl,
|
|
644
|
+
environment: stored?.environment,
|
|
645
|
+
authTokenSource: stored?.dreamboardApiToken || stored?.accessToken && !stored.refreshToken ? "global" : "none",
|
|
646
|
+
refreshTokenSource: stored?.refreshToken ? "global" : "none"
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
const accessToken = flagToken || agentEnvToken || envToken || environmentScopedStoredCredentials?.accessToken;
|
|
650
|
+
const refreshToken = environmentScopedStoredCredentials?.refreshToken;
|
|
651
|
+
const authTokenSource = flagToken ? "flag" : agentEnvToken ? "agent-env" : envToken ? "env" : environmentScopedStoredCredentials?.accessToken ? "global" : "none";
|
|
652
|
+
const refreshTokenSource = environmentScopedStoredCredentials?.refreshToken ? "global" : "none";
|
|
653
|
+
return {
|
|
654
|
+
accessToken,
|
|
655
|
+
refreshToken,
|
|
656
|
+
tokenExpiresAt: environmentScopedStoredCredentials?.tokenExpiresAt,
|
|
657
|
+
dreamboardApiToken: environmentScopedStoredCredentials?.dreamboardApiToken,
|
|
658
|
+
dreamboardApiExpiresAt: environmentScopedStoredCredentials?.dreamboardApiExpiresAt,
|
|
659
|
+
clerkOAuthIssuer: environmentScopedStoredCredentials?.clerkOAuthIssuer,
|
|
660
|
+
clerkOAuthClientId: environmentScopedStoredCredentials?.clerkOAuthClientId,
|
|
661
|
+
clerkOAuthTokenUrl: environmentScopedStoredCredentials?.clerkOAuthTokenUrl,
|
|
662
|
+
environment: environmentScopedStoredCredentials?.environment,
|
|
663
|
+
authTokenSource,
|
|
664
|
+
refreshTokenSource
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
function environmentFromProcess() {
|
|
668
|
+
const value = valueOrUndefined(process.env.DREAMBOARD_ENV);
|
|
669
|
+
if (!value) return void 0;
|
|
670
|
+
if (value === "local" || value === "staging" || value === "prod") {
|
|
671
|
+
return value;
|
|
672
|
+
}
|
|
673
|
+
throw new Error(
|
|
674
|
+
`Invalid DREAMBOARD_ENV '${value}'. Valid options: local, staging, prod`
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
function projectLocalBaseUrl(rawUrl, environment) {
|
|
678
|
+
if (environment !== "local" || !rawUrl) return void 0;
|
|
679
|
+
try {
|
|
680
|
+
const url = new URL(rawUrl);
|
|
681
|
+
return url.hostname === "localhost" || url.hostname === "127.0.0.1" ? rawUrl : void 0;
|
|
682
|
+
} catch {
|
|
683
|
+
return void 0;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
function assertPublicRuntimeFlags(flags, options = {}) {
|
|
687
|
+
const canSelectEnvironment = options.canSelectEnvironment ?? CAN_SELECT_ENVIRONMENT;
|
|
688
|
+
const argv = options.argv ?? process.argv.slice(2);
|
|
689
|
+
if (!canSelectEnvironment && (flags.env || argv.includes("--env"))) {
|
|
690
|
+
throw new Error(
|
|
691
|
+
"The published Dreamboard CLI is production-only and does not accept `--env`."
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
if (valueOrUndefined(flags.token) || argv.includes("--token")) {
|
|
695
|
+
throw new Error(
|
|
696
|
+
"Direct JWT injection is not supported in the published Dreamboard CLI. Use `dreamboard auth login` so the CLI can store and refresh your session."
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
if (process.env.DREAMBOARD_TOKEN) {
|
|
700
|
+
throw new Error(
|
|
701
|
+
"The published Dreamboard CLI ignores direct token environment variables. Use `dreamboard auth login` so the CLI can manage refreshable credentials."
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
async function configureClient(config) {
|
|
706
|
+
const localHarnessToken = resolveLocalHarnessAccessToken(config);
|
|
707
|
+
const resolvedToken = localHarnessToken ? { token: localHarnessToken } : await createUserSessionManager(config).resolveApiToken();
|
|
708
|
+
const effectiveAccessToken = resolvedToken?.token;
|
|
709
|
+
client.setConfig({
|
|
710
|
+
baseUrl: config.apiBaseUrl,
|
|
711
|
+
fetch: createRetryingReadFetch(globalThis.fetch.bind(globalThis)),
|
|
712
|
+
headers: effectiveAccessToken ? { Authorization: `Bearer ${effectiveAccessToken}` } : {}
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
function createRetryingReadFetch(fetchImpl) {
|
|
716
|
+
return (async (input, init) => {
|
|
717
|
+
const method = resolveFetchMethod(input, init);
|
|
718
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
719
|
+
return fetchWithOptionalTrace(fetchImpl, input, init, {
|
|
720
|
+
attempt: 0,
|
|
721
|
+
willRetry: false
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
let lastError;
|
|
725
|
+
for (let attempt = 0; attempt <= TRANSIENT_READ_RETRY_DELAYS_MS.length; attempt += 1) {
|
|
726
|
+
try {
|
|
727
|
+
return await fetchWithOptionalTrace(fetchImpl, input, init, {
|
|
728
|
+
attempt,
|
|
729
|
+
willRetry: attempt < TRANSIENT_READ_RETRY_DELAYS_MS.length
|
|
730
|
+
});
|
|
731
|
+
} catch (error) {
|
|
732
|
+
lastError = error;
|
|
733
|
+
if (attempt >= TRANSIENT_READ_RETRY_DELAYS_MS.length || !isTransientFetchError(error)) {
|
|
734
|
+
throw error;
|
|
735
|
+
}
|
|
736
|
+
await sleep(TRANSIENT_READ_RETRY_DELAYS_MS[attempt]);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
throw lastError;
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
async function fetchWithOptionalTrace(fetchImpl, input, init, options) {
|
|
743
|
+
if (!isHttpTraceEnabled()) {
|
|
744
|
+
return fetchImpl(input, init);
|
|
745
|
+
}
|
|
746
|
+
const request = describeRequest(input, init);
|
|
747
|
+
const startedAt = Date.now();
|
|
748
|
+
try {
|
|
749
|
+
const response = await fetchImpl(input, init);
|
|
750
|
+
writeHttpTrace({
|
|
751
|
+
...request,
|
|
752
|
+
attempt: options.attempt,
|
|
753
|
+
status: response.status,
|
|
754
|
+
durationMs: Date.now() - startedAt
|
|
755
|
+
});
|
|
756
|
+
return response;
|
|
757
|
+
} catch (error) {
|
|
758
|
+
writeHttpTrace({
|
|
759
|
+
...request,
|
|
760
|
+
attempt: options.attempt,
|
|
761
|
+
durationMs: Date.now() - startedAt,
|
|
762
|
+
error: error instanceof Error ? error.name : "UnknownError",
|
|
763
|
+
willRetry: options.willRetry
|
|
764
|
+
});
|
|
765
|
+
throw error;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
function isHttpTraceEnabled() {
|
|
769
|
+
return process.env.DREAMBOARD_CLI_HTTP_TRACE === "1";
|
|
770
|
+
}
|
|
771
|
+
function describeRequest(input, init) {
|
|
772
|
+
return {
|
|
773
|
+
method: resolveFetchMethod(input, init),
|
|
774
|
+
url: redactUrl(input),
|
|
775
|
+
hasAuthorization: hasAuthorizationHeader(input, init)
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
function redactUrl(input) {
|
|
779
|
+
const rawUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
780
|
+
try {
|
|
781
|
+
const url = new URL(rawUrl);
|
|
782
|
+
return `${url.origin}${url.pathname}`;
|
|
783
|
+
} catch {
|
|
784
|
+
return "<unparseable-url>";
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
function hasAuthorizationHeader(input, init) {
|
|
788
|
+
return headersContainAuthorization(init?.headers) || typeof Request !== "undefined" && input instanceof Request && input.headers.has("Authorization");
|
|
789
|
+
}
|
|
790
|
+
function headersContainAuthorization(headers) {
|
|
791
|
+
if (!headers) {
|
|
792
|
+
return false;
|
|
793
|
+
}
|
|
794
|
+
if (headers instanceof Headers) {
|
|
795
|
+
return headers.has("Authorization");
|
|
796
|
+
}
|
|
797
|
+
if (Array.isArray(headers)) {
|
|
798
|
+
return headers.some(([name]) => name.toLowerCase() === "authorization");
|
|
799
|
+
}
|
|
800
|
+
return Object.keys(headers).some(
|
|
801
|
+
(name) => name.toLowerCase() === "authorization"
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
function writeHttpTrace(event) {
|
|
805
|
+
const parts = [
|
|
806
|
+
"[dreamboard-cli:http]",
|
|
807
|
+
`method=${event.method}`,
|
|
808
|
+
`url=${event.url}`,
|
|
809
|
+
`auth=${event.hasAuthorization ? "present" : "missing"}`,
|
|
810
|
+
`attempt=${event.attempt + 1}`,
|
|
811
|
+
`durationMs=${event.durationMs}`
|
|
812
|
+
];
|
|
813
|
+
if (typeof event.status === "number") {
|
|
814
|
+
parts.push(`status=${event.status}`);
|
|
815
|
+
}
|
|
816
|
+
if (event.error) {
|
|
817
|
+
parts.push(`error=${event.error}`);
|
|
818
|
+
}
|
|
819
|
+
if (event.willRetry) {
|
|
820
|
+
parts.push("willRetry=true");
|
|
821
|
+
}
|
|
822
|
+
console.error(parts.join(" "));
|
|
823
|
+
}
|
|
824
|
+
function resolveFetchMethod(input, init) {
|
|
825
|
+
const method = init?.method ?? (typeof Request !== "undefined" && input instanceof Request ? input.method : void 0);
|
|
826
|
+
return (method ?? "GET").toUpperCase();
|
|
827
|
+
}
|
|
828
|
+
function isTransientFetchError(error) {
|
|
829
|
+
const message = error instanceof Error ? error.message : typeof error === "object" && error !== null && "message" in error ? String(error.message) : String(error);
|
|
830
|
+
const normalized = message.toLowerCase();
|
|
831
|
+
return normalized.includes("fetch failed") || normalized.includes("network") || normalized.includes("timeout") || normalized.includes("econnreset") || normalized.includes("econnrefused") || normalized.includes("socket");
|
|
832
|
+
}
|
|
833
|
+
function sleep(ms) {
|
|
834
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
835
|
+
}
|
|
836
|
+
function requireAuth(config) {
|
|
837
|
+
if (!config.authToken && !config.refreshToken && !resolveLocalHarnessAccessToken(config)) {
|
|
838
|
+
throw new Error(
|
|
839
|
+
"Missing Dreamboard session. Run `dreamboard auth login` to authenticate."
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
function valueOrUndefined(value) {
|
|
844
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
845
|
+
}
|
|
846
|
+
function getAuthTokenExpiry(accessToken) {
|
|
847
|
+
if (!accessToken) return null;
|
|
848
|
+
const parts = accessToken.split(".");
|
|
849
|
+
if (parts.length !== 3) return null;
|
|
850
|
+
try {
|
|
851
|
+
const payload = JSON.parse(
|
|
852
|
+
Buffer.from(parts[1], "base64url").toString("utf8")
|
|
853
|
+
);
|
|
854
|
+
if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) {
|
|
855
|
+
return null;
|
|
856
|
+
}
|
|
857
|
+
return new Date(payload.exp * 1e3);
|
|
858
|
+
} catch {
|
|
859
|
+
return null;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
async function loadProjectContextCredentials(requireAuth2, loadCredentials = getStoredSession) {
|
|
863
|
+
return requireAuth2 ? loadCredentials() : void 0;
|
|
864
|
+
}
|
|
865
|
+
async function resolveProjectContext(flags, opts) {
|
|
866
|
+
const projectRoot = await findProjectRoot(process.cwd());
|
|
867
|
+
if (!projectRoot) {
|
|
868
|
+
throw new Error(
|
|
869
|
+
"Not inside a dreamboard project (missing .dreamboard/project.json)."
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
const projectConfig = await loadProjectConfig(projectRoot);
|
|
873
|
+
const requireAuthForContext = opts?.requireAuth !== false;
|
|
874
|
+
const [globalConfig, credentials] = await Promise.all([
|
|
875
|
+
loadGlobalConfig(),
|
|
876
|
+
loadProjectContextCredentials(requireAuthForContext)
|
|
877
|
+
]);
|
|
878
|
+
const config = resolveConfig(globalConfig, flags, projectConfig, credentials);
|
|
879
|
+
if (requireAuthForContext) {
|
|
880
|
+
requireAuth(config);
|
|
881
|
+
await configureClient(config);
|
|
882
|
+
}
|
|
883
|
+
return { projectRoot, projectConfig, config };
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// src/flags.ts
|
|
887
|
+
var configFlagsSchema = external_exports.object({
|
|
888
|
+
env: external_exports.enum(["local", "staging", "prod"]).optional(),
|
|
889
|
+
token: external_exports.string().optional()
|
|
890
|
+
});
|
|
891
|
+
var ruleInputFlagsSchema = external_exports.object({
|
|
892
|
+
"rule-file": external_exports.string().optional(),
|
|
893
|
+
rule: external_exports.string().optional()
|
|
894
|
+
});
|
|
895
|
+
var playerCountFlagsSchema = external_exports.object({
|
|
896
|
+
players: external_exports.string().optional(),
|
|
897
|
+
"player-count": external_exports.string().optional()
|
|
898
|
+
});
|
|
899
|
+
var newCommandArgsSchema = configFlagsSchema.extend({
|
|
900
|
+
slug: external_exports.string().min(1),
|
|
901
|
+
description: external_exports.string().min(1),
|
|
902
|
+
force: external_exports.boolean().default(false),
|
|
903
|
+
"wait-timeout-ms": external_exports.string().optional(),
|
|
904
|
+
"repository-poll-interval-ms": external_exports.string().optional()
|
|
905
|
+
});
|
|
906
|
+
var cloneCommandArgsSchema = configFlagsSchema.extend({
|
|
907
|
+
slug: external_exports.string().min(1),
|
|
908
|
+
"wait-timeout-ms": external_exports.string().optional(),
|
|
909
|
+
"repository-poll-interval-ms": external_exports.string().optional()
|
|
910
|
+
});
|
|
911
|
+
var queryCommandArgsSchema = configFlagsSchema.extend({
|
|
912
|
+
title: external_exports.string().min(1)
|
|
913
|
+
});
|
|
914
|
+
var pullCommandArgsSchema = configFlagsSchema.extend({
|
|
915
|
+
force: external_exports.boolean().default(false)
|
|
916
|
+
});
|
|
917
|
+
var syncCommandArgsSchema = configFlagsSchema.extend({
|
|
918
|
+
force: external_exports.boolean().default(false),
|
|
919
|
+
yes: external_exports.boolean().default(false)
|
|
920
|
+
});
|
|
921
|
+
var compileCommandArgsSchema = configFlagsSchema.extend({
|
|
922
|
+
debug: external_exports.boolean().default(false),
|
|
923
|
+
"skip-local-check": external_exports.boolean().default(false)
|
|
924
|
+
});
|
|
925
|
+
var statusCommandArgsSchema = configFlagsSchema.extend({
|
|
926
|
+
json: external_exports.boolean().default(false)
|
|
927
|
+
});
|
|
928
|
+
var commitScopedCommandArgsSchema = configFlagsSchema.extend({
|
|
929
|
+
commit: external_exports.string().min(1),
|
|
930
|
+
json: external_exports.boolean().default(false)
|
|
931
|
+
});
|
|
932
|
+
var buildCommandArgsSchema = commitScopedCommandArgsSchema.extend({
|
|
933
|
+
profile: external_exports.enum(["preview", "release"]).optional().default("preview")
|
|
934
|
+
});
|
|
935
|
+
var releasePublishCommandArgsSchema = commitScopedCommandArgsSchema.extend({
|
|
936
|
+
yes: external_exports.boolean().default(false)
|
|
937
|
+
});
|
|
938
|
+
var devCommandArgsSchema = configFlagsSchema.extend({
|
|
939
|
+
seed: external_exports.string().optional(),
|
|
940
|
+
"setup-profile": external_exports.string().optional(),
|
|
941
|
+
players: external_exports.string().optional(),
|
|
942
|
+
"player-count": external_exports.string().optional(),
|
|
943
|
+
debug: external_exports.boolean().default(false),
|
|
944
|
+
resume: external_exports.string().optional(),
|
|
945
|
+
"from-scenario": external_exports.string().optional(),
|
|
946
|
+
"new-session": external_exports.boolean().default(false),
|
|
947
|
+
open: external_exports.boolean().default(false),
|
|
948
|
+
port: external_exports.string().optional(),
|
|
949
|
+
host: external_exports.union([external_exports.string(), external_exports.boolean()]).optional(),
|
|
950
|
+
"allowed-host": external_exports.string().optional()
|
|
951
|
+
});
|
|
952
|
+
var joinCommandArgsSchema = configFlagsSchema.extend({
|
|
953
|
+
session: external_exports.string().min(1).optional(),
|
|
954
|
+
player: external_exports.string().min(1),
|
|
955
|
+
"raw-events": external_exports.boolean().default(false)
|
|
956
|
+
});
|
|
957
|
+
var configCommandArgsSchema = configFlagsSchema.extend({
|
|
958
|
+
action: external_exports.string().optional().default("show"),
|
|
959
|
+
scope: external_exports.enum(["global", "workspace"]).optional().default("global")
|
|
960
|
+
});
|
|
961
|
+
var authCommandArgsSchema = external_exports.object({
|
|
962
|
+
action: external_exports.enum(["set", "login", "logout", "env", "status", "git-credential"]),
|
|
963
|
+
tokenValue: external_exports.string().optional(),
|
|
964
|
+
token: external_exports.string().optional(),
|
|
965
|
+
jwt: external_exports.boolean().optional(),
|
|
966
|
+
env: external_exports.enum(["local", "staging", "prod"]).optional()
|
|
967
|
+
});
|
|
968
|
+
function parseArgs(commandName, schema, args) {
|
|
969
|
+
const parsed = schema.safeParse(args);
|
|
970
|
+
if (parsed.success) {
|
|
971
|
+
return parsed.data;
|
|
972
|
+
}
|
|
973
|
+
const details = parsed.error.issues.map((issue) => {
|
|
974
|
+
const field = issue.path.length > 0 ? issue.path.join(".") : "args";
|
|
975
|
+
return `${field}: ${issue.message}`;
|
|
976
|
+
}).join("; ");
|
|
977
|
+
throw new Error(`Invalid arguments for '${commandName}': ${details}`);
|
|
978
|
+
}
|
|
979
|
+
function parseConfigFlags(args) {
|
|
980
|
+
return parseArgs("config-flags", configFlagsSchema, args);
|
|
981
|
+
}
|
|
982
|
+
function parsePlayerCountFlags(args) {
|
|
983
|
+
return parseArgs("player-count", playerCountFlagsSchema, args);
|
|
984
|
+
}
|
|
985
|
+
function parseNewCommandArgs(args) {
|
|
986
|
+
return parseArgs("new", newCommandArgsSchema, args);
|
|
987
|
+
}
|
|
988
|
+
function parseCloneCommandArgs(args) {
|
|
989
|
+
return parseArgs("clone", cloneCommandArgsSchema, args);
|
|
990
|
+
}
|
|
991
|
+
function parseCommitScopedCommandArgs(commandName, args) {
|
|
992
|
+
return parseArgs(commandName, commitScopedCommandArgsSchema, args);
|
|
993
|
+
}
|
|
994
|
+
function parseBuildCommandArgs(args) {
|
|
995
|
+
return parseArgs("build", buildCommandArgsSchema, args);
|
|
996
|
+
}
|
|
997
|
+
function parseReleasePublishCommandArgs(args) {
|
|
998
|
+
return parseArgs("release publish", releasePublishCommandArgsSchema, args);
|
|
999
|
+
}
|
|
1000
|
+
function parseDevCommandArgs(args) {
|
|
1001
|
+
return parseArgs("dev", devCommandArgsSchema, args);
|
|
1002
|
+
}
|
|
1003
|
+
function parseAuthCommandArgs(args) {
|
|
1004
|
+
return parseArgs("auth", authCommandArgsSchema, args);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// src/command-args.ts
|
|
1008
|
+
var CONFIG_FLAG_ARGS = {
|
|
1009
|
+
env: {
|
|
1010
|
+
type: "string",
|
|
1011
|
+
description: "Environment: local | staging | prod"
|
|
1012
|
+
},
|
|
1013
|
+
token: {
|
|
1014
|
+
type: "string",
|
|
1015
|
+
description: "Auth token (Dreamboard bearer JWT)"
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
// src/utils/problem-types.ts
|
|
1020
|
+
var CLI_PROBLEM_TYPES = {
|
|
1021
|
+
...SERVER_PROBLEM_TYPES,
|
|
1022
|
+
...CLIENT_PROBLEM_TYPES
|
|
1023
|
+
};
|
|
1024
|
+
|
|
1025
|
+
// src/utils/errors.ts
|
|
1026
|
+
var STALE_CONTRACT_ARTIFACT_CODE = "STALE_CONTRACT_ARTIFACT";
|
|
1027
|
+
var STALE_CONTRACT_ARTIFACT_EXIT_CODE = 42;
|
|
1028
|
+
function isProblemViolationArray(value) {
|
|
1029
|
+
return Array.isArray(value) && value.every(
|
|
1030
|
+
(entry) => typeof entry === "object" && entry !== null && typeof entry.message === "string"
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
function isProblemDetails(value) {
|
|
1034
|
+
return zProblemDetails.safeParse(value).success;
|
|
1035
|
+
}
|
|
1036
|
+
function coerceViolations(value) {
|
|
1037
|
+
if (isProblemViolationArray(value)) {
|
|
1038
|
+
return value;
|
|
1039
|
+
}
|
|
1040
|
+
if (Array.isArray(value) && value.every((entry) => typeof entry === "string")) {
|
|
1041
|
+
return value.map((message) => ({ message }));
|
|
1042
|
+
}
|
|
1043
|
+
return void 0;
|
|
1044
|
+
}
|
|
1045
|
+
function getRequestId(response) {
|
|
1046
|
+
return response?.headers?.get?.("X-Correlation-ID") ?? response?.headers?.get?.("x-correlation-id") ?? void 0;
|
|
1047
|
+
}
|
|
1048
|
+
function toApiProblem(error, response, fallback) {
|
|
1049
|
+
if (isProblemDetails(error)) {
|
|
1050
|
+
return {
|
|
1051
|
+
...error,
|
|
1052
|
+
status: error.status || response?.status || 0,
|
|
1053
|
+
requestId: error.requestId ?? getRequestId(response)
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
if (error instanceof Error) {
|
|
1057
|
+
return {
|
|
1058
|
+
type: CLI_PROBLEM_TYPES.TRANSPORT_ERROR,
|
|
1059
|
+
title: response?.statusText || "API error",
|
|
1060
|
+
status: response?.status ?? 0,
|
|
1061
|
+
detail: error.message || fallback,
|
|
1062
|
+
requestId: getRequestId(response)
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
if (error && typeof error === "object") {
|
|
1066
|
+
const obj = error;
|
|
1067
|
+
const detail2 = typeof obj.detail === "string" ? obj.detail : typeof obj.message === "string" ? obj.message : void 0;
|
|
1068
|
+
const title = typeof obj.title === "string" ? obj.title : response?.statusText || "API error";
|
|
1069
|
+
const violations = coerceViolations(obj.violations) ?? coerceViolations(obj.errors);
|
|
1070
|
+
if (detail2) {
|
|
1071
|
+
return {
|
|
1072
|
+
type: typeof obj.type === "string" ? obj.type : CLI_PROBLEM_TYPES.UNKNOWN_API_ERROR,
|
|
1073
|
+
title,
|
|
1074
|
+
status: typeof obj.status === "number" ? obj.status : response?.status ?? 0,
|
|
1075
|
+
detail: detail2,
|
|
1076
|
+
requestId: typeof obj.requestId === "string" ? obj.requestId : getRequestId(response),
|
|
1077
|
+
retryable: typeof obj.retryable === "boolean" ? obj.retryable : void 0,
|
|
1078
|
+
context: typeof obj.context === "object" && obj.context !== null ? obj.context : void 0,
|
|
1079
|
+
violations,
|
|
1080
|
+
timestamp: typeof obj.timestamp === "string" ? obj.timestamp : void 0,
|
|
1081
|
+
instance: typeof obj.instance === "string" ? obj.instance : void 0
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
const detail = typeof error === "string" ? error.trim() || fallback : fallback;
|
|
1086
|
+
return {
|
|
1087
|
+
type: CLI_PROBLEM_TYPES.UNKNOWN_API_ERROR,
|
|
1088
|
+
title: response?.statusText || "API error",
|
|
1089
|
+
status: response?.status ?? 0,
|
|
1090
|
+
detail,
|
|
1091
|
+
requestId: getRequestId(response)
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
function formatProblem(problem) {
|
|
1095
|
+
const base = problem.detail || problem.title;
|
|
1096
|
+
const violations = problem.violations && problem.violations.length > 0 ? ` (${problem.violations.map((entry) => entry.message).join("; ")})` : "";
|
|
1097
|
+
const statusSuffix = problem.status && problem.status > 0 ? ` (HTTP ${problem.status})` : "";
|
|
1098
|
+
return `${base}${violations}${statusSuffix}`;
|
|
1099
|
+
}
|
|
1100
|
+
var DreamboardApiError = class extends Error {
|
|
1101
|
+
problem;
|
|
1102
|
+
status;
|
|
1103
|
+
requestId;
|
|
1104
|
+
retryable;
|
|
1105
|
+
constructor(problem, cause) {
|
|
1106
|
+
super(formatProblem(problem), { cause });
|
|
1107
|
+
this.name = "DreamboardApiError";
|
|
1108
|
+
this.problem = problem;
|
|
1109
|
+
this.status = problem.status;
|
|
1110
|
+
this.requestId = problem.requestId;
|
|
1111
|
+
this.retryable = problem.retryable;
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
function toDreamboardApiError(error, response, fallback) {
|
|
1115
|
+
return new DreamboardApiError(toApiProblem(error, response, fallback), error);
|
|
1116
|
+
}
|
|
1117
|
+
function isDreamboardApiError(error) {
|
|
1118
|
+
return error instanceof DreamboardApiError;
|
|
1119
|
+
}
|
|
1120
|
+
function getObjectStringProperty(value, property) {
|
|
1121
|
+
return value && typeof value === "object" && typeof value[property] === "string" ? value[property] : void 0;
|
|
1122
|
+
}
|
|
1123
|
+
function isStaleContractArtifactMessage(message) {
|
|
1124
|
+
return message.includes(STALE_CONTRACT_ARTIFACT_CODE) || message.includes("StaleContractArtifactError") || message.toLowerCase().includes("stale contract artifact");
|
|
1125
|
+
}
|
|
1126
|
+
function isStaleContractArtifactError(error) {
|
|
1127
|
+
if (getObjectStringProperty(error, "code") === STALE_CONTRACT_ARTIFACT_CODE) {
|
|
1128
|
+
return true;
|
|
1129
|
+
}
|
|
1130
|
+
if (getObjectStringProperty(error, "name") === "StaleContractArtifactError") {
|
|
1131
|
+
return true;
|
|
1132
|
+
}
|
|
1133
|
+
const message = getObjectStringProperty(error, "message");
|
|
1134
|
+
return message ? isStaleContractArtifactMessage(message) : false;
|
|
1135
|
+
}
|
|
1136
|
+
function getCliErrorExitCode(error) {
|
|
1137
|
+
return isStaleContractArtifactError(error) ? STALE_CONTRACT_ARTIFACT_EXIT_CODE : 1;
|
|
1138
|
+
}
|
|
1139
|
+
function isGameNotFoundProblem(problem) {
|
|
1140
|
+
return problem.status === 404 && (problem.detail?.startsWith("Game not found: ") === true || problem.instance?.includes("/source-blobs/upload-sessions") === true);
|
|
1141
|
+
}
|
|
1142
|
+
function getProblemResolution(problem) {
|
|
1143
|
+
if (isGameNotFoundProblem(problem)) {
|
|
1144
|
+
return [
|
|
1145
|
+
"Verify the project binding with `dreamboard project status --commit <rev>` after pushing the exact commit.",
|
|
1146
|
+
"If you meant to use an existing remote project, check that your selected account and environment point at the backend that has that project."
|
|
1147
|
+
].join(" ");
|
|
1148
|
+
}
|
|
1149
|
+
switch (problem.type) {
|
|
1150
|
+
case CLI_PROBLEM_TYPES.UNAUTHORIZED:
|
|
1151
|
+
return "Run `dreamboard auth login` to authenticate again.";
|
|
1152
|
+
case CLI_PROBLEM_TYPES.FORBIDDEN:
|
|
1153
|
+
return "Check that the signed-in account has access to this game, or run `dreamboard auth login` with the correct account.";
|
|
1154
|
+
case CLI_PROBLEM_TYPES.TOO_MANY_REQUESTS:
|
|
1155
|
+
return "Wait a moment, then retry the command.";
|
|
1156
|
+
case CLI_PROBLEM_TYPES.TRANSPORT_ERROR:
|
|
1157
|
+
return "Check that the selected Dreamboard server is reachable and try again later.";
|
|
1158
|
+
case CLI_PROBLEM_TYPES.VALIDATION_FAILED:
|
|
1159
|
+
return "Fix the validation issue above, then retry the command.";
|
|
1160
|
+
case CLI_PROBLEM_TYPES.ACTIVE_JOB_CONFLICT:
|
|
1161
|
+
return "Wait for the active job to finish, then retry the command.";
|
|
1162
|
+
case CLI_PROBLEM_TYPES.GAME_SLUG_CONFLICT:
|
|
1163
|
+
return "Choose a different game slug, or use the existing workspace for that slug.";
|
|
1164
|
+
case CLI_PROBLEM_TYPES.SOURCE_REVISION_NOT_FOUND:
|
|
1165
|
+
return "Push the exact Git commit, then run `dreamboard project status --commit <rev> --wait` before retrying.";
|
|
1166
|
+
case CLI_PROBLEM_TYPES.SOURCE_REVISION_DRIFT:
|
|
1167
|
+
case CLI_PROBLEM_TYPES.AUTHORING_STATE_DRIFT:
|
|
1168
|
+
case CLI_PROBLEM_TYPES.STATE_CONFLICT:
|
|
1169
|
+
return "Resolve the Git/source conflict in your worktree, push the intended exact commit, and retry the command against that commit.";
|
|
1170
|
+
case CLI_PROBLEM_TYPES.SOURCE_REVISION_BASE_MISSING:
|
|
1171
|
+
case CLI_PROBLEM_TYPES.AUTHORING_STATE_BASE_MISSING:
|
|
1172
|
+
return "Clone the project repository into a clean workspace or push the intended exact commit before retrying.";
|
|
1173
|
+
case CLI_PROBLEM_TYPES.INTERNAL_ERROR:
|
|
1174
|
+
return "Retry the command. If it still fails, include the request id when asking for help.";
|
|
1175
|
+
default:
|
|
1176
|
+
return void 0;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
function getProblemDetails(problem) {
|
|
1180
|
+
return [
|
|
1181
|
+
`Problem: ${problem.type}`,
|
|
1182
|
+
problem.instance ? `Endpoint: ${problem.instance}` : void 0,
|
|
1183
|
+
problem.requestId ? `Request ID: ${problem.requestId}` : void 0,
|
|
1184
|
+
problem.timestamp ? `Timestamp: ${problem.timestamp}` : void 0
|
|
1185
|
+
].filter((detail) => Boolean(detail));
|
|
1186
|
+
}
|
|
1187
|
+
function presentCliError(error) {
|
|
1188
|
+
if (isDreamboardApiError(error)) {
|
|
1189
|
+
return {
|
|
1190
|
+
message: formatProblem(error.problem),
|
|
1191
|
+
resolution: getProblemResolution(error.problem),
|
|
1192
|
+
details: getProblemDetails(error.problem)
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
if (error instanceof Error) {
|
|
1196
|
+
return {
|
|
1197
|
+
message: error.message,
|
|
1198
|
+
details: []
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
if (typeof error === "object" && error !== null) {
|
|
1202
|
+
let serialized;
|
|
1203
|
+
try {
|
|
1204
|
+
serialized = JSON.stringify(error);
|
|
1205
|
+
} catch {
|
|
1206
|
+
serialized = String(error);
|
|
1207
|
+
}
|
|
1208
|
+
return {
|
|
1209
|
+
message: serialized,
|
|
1210
|
+
details: []
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
return {
|
|
1214
|
+
message: String(error),
|
|
1215
|
+
details: []
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
function formatCliError(error) {
|
|
1219
|
+
const presentation = presentCliError(error);
|
|
1220
|
+
return [
|
|
1221
|
+
presentation.message,
|
|
1222
|
+
presentation.resolution ? `Resolution: ${presentation.resolution}` : void 0,
|
|
1223
|
+
...presentation.details
|
|
1224
|
+
].filter((line) => Boolean(line)).join("\n");
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// src/utils/strings.ts
|
|
1228
|
+
import path from "path";
|
|
1229
|
+
function normalizeSlug(input) {
|
|
1230
|
+
const lowered = input.trim().toLowerCase();
|
|
1231
|
+
const replaced = lowered.replace(/[^a-z0-9]+/g, "-");
|
|
1232
|
+
return replaced.replace(/^-+/, "").replace(/-+$/, "");
|
|
1233
|
+
}
|
|
1234
|
+
function titleFromSlug(slug) {
|
|
1235
|
+
return slug.split("-").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
1236
|
+
}
|
|
1237
|
+
function parsePositiveInt(value, label) {
|
|
1238
|
+
const parsed = Number.parseInt(value, 10);
|
|
1239
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
1240
|
+
throw new Error(`${label} must be a positive integer.`);
|
|
1241
|
+
}
|
|
1242
|
+
return parsed;
|
|
1243
|
+
}
|
|
1244
|
+
function sleep2(ms) {
|
|
1245
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// src/services/api/compiled-results-api.ts
|
|
1249
|
+
var COMPILE_JOB_POLL_INTERVAL_MS = 1e3;
|
|
1250
|
+
var DEFAULT_COMPILE_JOB_WAIT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
1251
|
+
function firstNonEmpty(...values) {
|
|
1252
|
+
for (const value of values) {
|
|
1253
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
1254
|
+
return value.trim();
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
return null;
|
|
1258
|
+
}
|
|
1259
|
+
function formatTerminalCompileJobMessage(job) {
|
|
1260
|
+
const detail = firstNonEmpty(job.errorMessage, job.message);
|
|
1261
|
+
const phase = firstNonEmpty(job.phase);
|
|
1262
|
+
const prefix = `Compile ${job.status.toLowerCase()}${phase ? ` [${phase}]` : ""}`;
|
|
1263
|
+
return detail ? `${prefix}: ${detail}` : `${prefix}: job ${job.jobId} ended before a compiled result was created.`;
|
|
1264
|
+
}
|
|
1265
|
+
function compareCreatedAtDesc(left, right) {
|
|
1266
|
+
const leftTime = Date.parse(left.createdAt);
|
|
1267
|
+
const rightTime = Date.parse(right.createdAt);
|
|
1268
|
+
if (Number.isFinite(leftTime) && Number.isFinite(rightTime)) {
|
|
1269
|
+
return rightTime - leftTime;
|
|
1270
|
+
}
|
|
1271
|
+
if (Number.isFinite(rightTime)) {
|
|
1272
|
+
return 1;
|
|
1273
|
+
}
|
|
1274
|
+
if (Number.isFinite(leftTime)) {
|
|
1275
|
+
return -1;
|
|
1276
|
+
}
|
|
1277
|
+
return 0;
|
|
1278
|
+
}
|
|
1279
|
+
async function findFallbackCompiledResultForJob(options) {
|
|
1280
|
+
const { projectId, job } = options;
|
|
1281
|
+
if (!projectId) {
|
|
1282
|
+
return null;
|
|
1283
|
+
}
|
|
1284
|
+
const results = await listProjectCompiledResults({
|
|
1285
|
+
path: { projectId },
|
|
1286
|
+
query: { limit: 100 }
|
|
1287
|
+
});
|
|
1288
|
+
if (results.error || !results.data) {
|
|
1289
|
+
return null;
|
|
1290
|
+
}
|
|
1291
|
+
if (results.data.results.length === 0) {
|
|
1292
|
+
return null;
|
|
1293
|
+
}
|
|
1294
|
+
const jobCreatedAtMs = Date.parse(job.createdAt);
|
|
1295
|
+
const resultsCreatedAfterJob = Number.isFinite(jobCreatedAtMs) ? results.data.results.filter((result) => {
|
|
1296
|
+
const resultCreatedAtMs = Date.parse(result.createdAt);
|
|
1297
|
+
return !Number.isFinite(resultCreatedAtMs) || resultCreatedAtMs >= jobCreatedAtMs;
|
|
1298
|
+
}) : results.data.results;
|
|
1299
|
+
const candidateResults = resultsCreatedAfterJob.length > 0 ? resultsCreatedAfterJob : results.data.results;
|
|
1300
|
+
return [...candidateResults].sort(compareCreatedAtDesc)[0] ?? null;
|
|
1301
|
+
}
|
|
1302
|
+
async function findCompiledResultsForAuthoringState(options) {
|
|
1303
|
+
void options;
|
|
1304
|
+
return [];
|
|
1305
|
+
}
|
|
1306
|
+
async function getProjectCompiledResultSdk(projectId, compiledResultId) {
|
|
1307
|
+
const { data, error, response } = await getProjectCompiledResult({
|
|
1308
|
+
path: { projectId, compiledResultId }
|
|
1309
|
+
});
|
|
1310
|
+
if (error || !data) {
|
|
1311
|
+
throw toDreamboardApiError(
|
|
1312
|
+
error,
|
|
1313
|
+
response,
|
|
1314
|
+
"Failed to fetch compiled result"
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
return data;
|
|
1318
|
+
}
|
|
1319
|
+
async function waitForCompiledResultJobSdk(options) {
|
|
1320
|
+
const { projectId, jobId, onProgress } = options;
|
|
1321
|
+
if (!projectId) {
|
|
1322
|
+
throw new Error("projectId is required when waiting for a compile job.");
|
|
1323
|
+
}
|
|
1324
|
+
let previousTransitionKey = null;
|
|
1325
|
+
const startedAt = Date.now();
|
|
1326
|
+
const timeoutMs = readCompileJobWaitTimeoutMs();
|
|
1327
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
1328
|
+
const {
|
|
1329
|
+
data: job,
|
|
1330
|
+
error,
|
|
1331
|
+
response
|
|
1332
|
+
} = await getJob({
|
|
1333
|
+
path: { jobId }
|
|
1334
|
+
});
|
|
1335
|
+
if (error || !job) {
|
|
1336
|
+
if (isTransientJobPollError(error, response)) {
|
|
1337
|
+
await sleep2(COMPILE_JOB_POLL_INTERVAL_MS);
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
1340
|
+
throw toDreamboardApiError(error, response, "Failed to get job");
|
|
1341
|
+
}
|
|
1342
|
+
const transitionKey = `${job.status}:${job.phase ?? ""}`;
|
|
1343
|
+
if (transitionKey !== previousTransitionKey) {
|
|
1344
|
+
previousTransitionKey = transitionKey;
|
|
1345
|
+
onProgress?.(job);
|
|
1346
|
+
}
|
|
1347
|
+
if (job.status === "COMPLETED" || job.status === "FAILED") {
|
|
1348
|
+
const compiledResultId = job.createdCompiledResultId ?? job.createdAppScriptId;
|
|
1349
|
+
if (compiledResultId) {
|
|
1350
|
+
const compiledResult = await getProjectCompiledResultSdk(
|
|
1351
|
+
projectId,
|
|
1352
|
+
compiledResultId
|
|
1353
|
+
);
|
|
1354
|
+
return { job, compiledResult };
|
|
1355
|
+
}
|
|
1356
|
+
const fallbackCompiledResult = await findFallbackCompiledResultForJob({
|
|
1357
|
+
projectId,
|
|
1358
|
+
job
|
|
1359
|
+
});
|
|
1360
|
+
if (fallbackCompiledResult) {
|
|
1361
|
+
return { job, compiledResult: fallbackCompiledResult };
|
|
1362
|
+
}
|
|
1363
|
+
throw new Error(formatTerminalCompileJobMessage(job));
|
|
1364
|
+
}
|
|
1365
|
+
if (job.status === "CANCELLED" || job.status === "INTERRUPTED") {
|
|
1366
|
+
throw new Error(formatTerminalCompileJobMessage(job));
|
|
1367
|
+
}
|
|
1368
|
+
await sleep2(COMPILE_JOB_POLL_INTERVAL_MS);
|
|
1369
|
+
}
|
|
1370
|
+
throw new Error(`Compile job ${jobId} did not complete in time.`);
|
|
1371
|
+
}
|
|
1372
|
+
function readCompileJobWaitTimeoutMs() {
|
|
1373
|
+
const raw = process.env.DREAMBOARD_COMPILE_WAIT_TIMEOUT_MS;
|
|
1374
|
+
if (!raw) return DEFAULT_COMPILE_JOB_WAIT_TIMEOUT_MS;
|
|
1375
|
+
const parsed = Number(raw);
|
|
1376
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
1377
|
+
return DEFAULT_COMPILE_JOB_WAIT_TIMEOUT_MS;
|
|
1378
|
+
}
|
|
1379
|
+
return parsed;
|
|
1380
|
+
}
|
|
1381
|
+
function isTransientJobPollError(error, response) {
|
|
1382
|
+
if (response) return false;
|
|
1383
|
+
if (!error) return false;
|
|
1384
|
+
if (error instanceof Error) {
|
|
1385
|
+
return isTransientJobPollMessage(error.message);
|
|
1386
|
+
}
|
|
1387
|
+
if (typeof error === "object" && error !== null && "message" in error) {
|
|
1388
|
+
return isTransientJobPollMessage(String(error.message));
|
|
1389
|
+
}
|
|
1390
|
+
return isTransientJobPollMessage(String(error));
|
|
1391
|
+
}
|
|
1392
|
+
function isTransientJobPollMessage(message) {
|
|
1393
|
+
const normalized = message.toLowerCase();
|
|
1394
|
+
return normalized.includes("fetch failed") || normalized.includes("network") || normalized.includes("timeout") || normalized.includes("econnreset") || normalized.includes("econnrefused") || normalized.includes("socket");
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// src/services/testing/reducer-native-test-harness.ts
|
|
1398
|
+
import path3 from "path";
|
|
1399
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1400
|
+
import { createRequire } from "module";
|
|
1401
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
1402
|
+
import {
|
|
1403
|
+
existsSync,
|
|
1404
|
+
mkdirSync,
|
|
1405
|
+
readFileSync,
|
|
1406
|
+
rmSync,
|
|
1407
|
+
writeFileSync
|
|
1408
|
+
} from "fs";
|
|
1409
|
+
import { readdir } from "fs/promises";
|
|
1410
|
+
import { isDeepStrictEqual } from "util";
|
|
1411
|
+
|
|
1412
|
+
// src/utils/ts-module-loader.ts
|
|
1413
|
+
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
1414
|
+
import { tmpdir } from "os";
|
|
1415
|
+
import path2 from "path";
|
|
1416
|
+
import { pathToFileURL } from "url";
|
|
1417
|
+
import { build } from "esbuild";
|
|
1418
|
+
var ESBUILD_EXTERNALS = [
|
|
1419
|
+
"playwright",
|
|
1420
|
+
"playwright-core",
|
|
1421
|
+
"chromium-bidi",
|
|
1422
|
+
"electron"
|
|
1423
|
+
];
|
|
1424
|
+
function resolveSourceCheckoutBuildContext() {
|
|
1425
|
+
try {
|
|
1426
|
+
const repoRoot = resolveCliRepoRoot(import.meta.url);
|
|
1427
|
+
return {
|
|
1428
|
+
nodePaths: [
|
|
1429
|
+
path2.join(repoRoot, "apps", "dreamboard-cli", "node_modules"),
|
|
1430
|
+
path2.join(repoRoot, "node_modules")
|
|
1431
|
+
],
|
|
1432
|
+
plugins: [createRepoLocalPackageResolutionPlugin({ repoRoot })]
|
|
1433
|
+
};
|
|
1434
|
+
} catch {
|
|
1435
|
+
return {
|
|
1436
|
+
nodePaths: [path2.join(process.cwd(), "node_modules")],
|
|
1437
|
+
plugins: []
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
async function bundleTypeScriptModuleText(entryPath, options = {}) {
|
|
1442
|
+
const sourceCheckoutContext = resolveSourceCheckoutBuildContext();
|
|
1443
|
+
const result = await build({
|
|
1444
|
+
entryPoints: [entryPath],
|
|
1445
|
+
bundle: true,
|
|
1446
|
+
format: "esm",
|
|
1447
|
+
platform: "node",
|
|
1448
|
+
target: "node24",
|
|
1449
|
+
sourcemap: "inline",
|
|
1450
|
+
external: [...ESBUILD_EXTERNALS, ...options.external ?? []],
|
|
1451
|
+
nodePaths: sourceCheckoutContext.nodePaths,
|
|
1452
|
+
plugins: sourceCheckoutContext.plugins,
|
|
1453
|
+
write: false
|
|
1454
|
+
});
|
|
1455
|
+
const output = result.outputFiles?.[0];
|
|
1456
|
+
if (!output) {
|
|
1457
|
+
throw new Error(`Failed to bundle TypeScript module '${entryPath}'.`);
|
|
1458
|
+
}
|
|
1459
|
+
return output.text;
|
|
1460
|
+
}
|
|
1461
|
+
async function importTypeScriptModule(entryPath) {
|
|
1462
|
+
const tempDir = await mkdtemp(path2.join(tmpdir(), "dreamboard-ts-module-"));
|
|
1463
|
+
const outfile = path2.join(
|
|
1464
|
+
tempDir,
|
|
1465
|
+
`${path2.basename(entryPath).replace(/\.[^.]+$/u, "")}.mjs`
|
|
1466
|
+
);
|
|
1467
|
+
try {
|
|
1468
|
+
const bundledText = await bundleTypeScriptModuleText(entryPath);
|
|
1469
|
+
await writeFile(outfile, bundledText);
|
|
1470
|
+
return await import(pathToFileURL(outfile).href);
|
|
1471
|
+
} finally {
|
|
1472
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
// src/services/gameplay-authority-submit.ts
|
|
1477
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1478
|
+
|
|
1479
|
+
// ../../node_modules/.pnpm/@dreamboard-games+gameplay-authority-protocol@0.1.0-alpha.0/node_modules/@dreamboard-games/gameplay-authority-protocol/dist/frames.js
|
|
1480
|
+
var GAMEPLAY_AUTHORITY_AUDIENCE = "dreamboard-gameplay-authority";
|
|
1481
|
+
var GAMEPLAY_TUNNEL_AUDIENCE = "dreamboard-gameplay-authority-tunnel";
|
|
1482
|
+
var GameplayPermissionSchema = external_exports.enum([
|
|
1483
|
+
"observe",
|
|
1484
|
+
"submit",
|
|
1485
|
+
"restore-history"
|
|
1486
|
+
]);
|
|
1487
|
+
var GameplayConnectionContextSchema = external_exports.object({
|
|
1488
|
+
sessionId: external_exports.string().uuid(),
|
|
1489
|
+
playerId: external_exports.string().min(1),
|
|
1490
|
+
permissions: external_exports.array(GameplayPermissionSchema)
|
|
1491
|
+
});
|
|
1492
|
+
var GameplayActorKindSchema = external_exports.enum(["user", "demo", "perf"]);
|
|
1493
|
+
var JsonPatchOperationSchema = external_exports.object({
|
|
1494
|
+
op: external_exports.string().min(1),
|
|
1495
|
+
path: external_exports.string()
|
|
1496
|
+
}).catchall(external_exports.unknown());
|
|
1497
|
+
var ProjectedDeltaSchema = external_exports.object({
|
|
1498
|
+
kind: external_exports.literal("delta"),
|
|
1499
|
+
generation: external_exports.number().int().nonnegative(),
|
|
1500
|
+
baseVersion: external_exports.number().int().nonnegative(),
|
|
1501
|
+
version: external_exports.number().int().nonnegative(),
|
|
1502
|
+
patch: external_exports.array(JsonPatchOperationSchema)
|
|
1503
|
+
});
|
|
1504
|
+
var ProjectedSnapshotSchema = external_exports.object({
|
|
1505
|
+
kind: external_exports.literal("snapshot"),
|
|
1506
|
+
generation: external_exports.number().int().nonnegative(),
|
|
1507
|
+
version: external_exports.number().int().nonnegative(),
|
|
1508
|
+
view: external_exports.unknown()
|
|
1509
|
+
});
|
|
1510
|
+
var BaseGameplayCapabilityClaimsSchema = external_exports.object({
|
|
1511
|
+
typ: external_exports.literal("gameplay-capability"),
|
|
1512
|
+
aud: external_exports.literal(GAMEPLAY_AUTHORITY_AUDIENCE),
|
|
1513
|
+
iss: external_exports.string().min(1),
|
|
1514
|
+
exp: external_exports.number().int().positive(),
|
|
1515
|
+
iat: external_exports.number().int().positive().optional(),
|
|
1516
|
+
jti: external_exports.string().min(1).optional(),
|
|
1517
|
+
sessionId: external_exports.string().uuid(),
|
|
1518
|
+
playerId: external_exports.string().min(1),
|
|
1519
|
+
permissions: external_exports.array(GameplayPermissionSchema)
|
|
1520
|
+
});
|
|
1521
|
+
var UserGameplayCapabilityClaimsSchema = BaseGameplayCapabilityClaimsSchema.extend({
|
|
1522
|
+
actorKind: external_exports.literal("user")
|
|
1523
|
+
});
|
|
1524
|
+
var DemoGameplayCapabilityClaimsSchema = BaseGameplayCapabilityClaimsSchema.extend({
|
|
1525
|
+
actorKind: external_exports.literal("demo")
|
|
1526
|
+
});
|
|
1527
|
+
var PerfGameplayCapabilityClaimsSchema = BaseGameplayCapabilityClaimsSchema.extend({
|
|
1528
|
+
actorKind: external_exports.literal("perf"),
|
|
1529
|
+
perfRunId: external_exports.string().uuid(),
|
|
1530
|
+
laneId: external_exports.string().min(1)
|
|
1531
|
+
});
|
|
1532
|
+
var GameplayCapabilityClaimsSchema = external_exports.discriminatedUnion("actorKind", [
|
|
1533
|
+
UserGameplayCapabilityClaimsSchema,
|
|
1534
|
+
DemoGameplayCapabilityClaimsSchema,
|
|
1535
|
+
PerfGameplayCapabilityClaimsSchema
|
|
1536
|
+
]);
|
|
1537
|
+
var GameplayTunnelClaimsSchema = external_exports.object({
|
|
1538
|
+
aud: external_exports.literal(GAMEPLAY_TUNNEL_AUDIENCE),
|
|
1539
|
+
iss: external_exports.string().min(1),
|
|
1540
|
+
exp: external_exports.number().int().positive(),
|
|
1541
|
+
iat: external_exports.number().int().positive().optional(),
|
|
1542
|
+
jti: external_exports.string().min(1).optional(),
|
|
1543
|
+
targetInstanceId: external_exports.string().min(1),
|
|
1544
|
+
sessionId: external_exports.string().uuid(),
|
|
1545
|
+
playerId: external_exports.string().min(1),
|
|
1546
|
+
// Permissions are cryptographically bound into the tunnel token so the owner
|
|
1547
|
+
// task trusts the signed claim set rather than the unauthenticated
|
|
1548
|
+
// `tunnel.bind` frame. An ingress task cannot grant a player capabilities the
|
|
1549
|
+
// issued token did not attest.
|
|
1550
|
+
permissions: external_exports.array(GameplayPermissionSchema)
|
|
1551
|
+
});
|
|
1552
|
+
var AuthConnectFrameSchema = external_exports.object({
|
|
1553
|
+
type: external_exports.literal("auth.connect"),
|
|
1554
|
+
capabilityToken: external_exports.string().min(1)
|
|
1555
|
+
});
|
|
1556
|
+
var AuthRefreshFrameSchema = external_exports.object({
|
|
1557
|
+
type: external_exports.literal("auth.refresh"),
|
|
1558
|
+
capabilityToken: external_exports.string().min(1)
|
|
1559
|
+
});
|
|
1560
|
+
var SessionResumeFrameSchema = external_exports.object({
|
|
1561
|
+
type: external_exports.literal("session.resume"),
|
|
1562
|
+
lastSeenGeneration: external_exports.number().int().nonnegative().nullable(),
|
|
1563
|
+
lastSeenVersion: external_exports.number().int().nonnegative().nullable(),
|
|
1564
|
+
unacknowledgedClientActionIds: external_exports.array(external_exports.string().min(1)).max(32)
|
|
1565
|
+
});
|
|
1566
|
+
var CommandSubmitFrameSchema = external_exports.object({
|
|
1567
|
+
type: external_exports.literal("command.submit"),
|
|
1568
|
+
clientActionId: external_exports.string().min(1).max(128),
|
|
1569
|
+
expectedVersion: external_exports.number().int().nonnegative(),
|
|
1570
|
+
actionSetVersion: external_exports.string().min(1),
|
|
1571
|
+
interactionId: external_exports.string().min(1),
|
|
1572
|
+
inputs: external_exports.record(external_exports.string(), external_exports.unknown())
|
|
1573
|
+
});
|
|
1574
|
+
var HistoryRestoreFrameSchema = external_exports.object({
|
|
1575
|
+
type: external_exports.literal("history.restore"),
|
|
1576
|
+
restoreId: external_exports.string().min(1).max(128),
|
|
1577
|
+
targetGeneration: external_exports.number().int().nonnegative(),
|
|
1578
|
+
targetVersion: external_exports.number().int().nonnegative()
|
|
1579
|
+
});
|
|
1580
|
+
var ClientGameplayFrameSchema = external_exports.discriminatedUnion("type", [
|
|
1581
|
+
AuthConnectFrameSchema,
|
|
1582
|
+
AuthRefreshFrameSchema,
|
|
1583
|
+
SessionResumeFrameSchema,
|
|
1584
|
+
CommandSubmitFrameSchema,
|
|
1585
|
+
HistoryRestoreFrameSchema
|
|
1586
|
+
]);
|
|
1587
|
+
var TunnelBindFrameSchema = external_exports.object({
|
|
1588
|
+
type: external_exports.literal("tunnel.bind"),
|
|
1589
|
+
context: GameplayConnectionContextSchema
|
|
1590
|
+
});
|
|
1591
|
+
var TunnelClientFrameSchema = external_exports.discriminatedUnion("type", [
|
|
1592
|
+
TunnelBindFrameSchema,
|
|
1593
|
+
SessionResumeFrameSchema,
|
|
1594
|
+
CommandSubmitFrameSchema,
|
|
1595
|
+
HistoryRestoreFrameSchema
|
|
1596
|
+
]);
|
|
1597
|
+
var CommandAcceptedFrameSchema = external_exports.object({
|
|
1598
|
+
type: external_exports.literal("command.accepted"),
|
|
1599
|
+
clientActionId: external_exports.string().min(1),
|
|
1600
|
+
generation: external_exports.number().int().nonnegative(),
|
|
1601
|
+
version: external_exports.number().int().nonnegative(),
|
|
1602
|
+
stateHash: external_exports.string().min(1),
|
|
1603
|
+
update: ProjectedSnapshotSchema
|
|
1604
|
+
});
|
|
1605
|
+
var CommandRejectedFrameSchema = external_exports.object({
|
|
1606
|
+
type: external_exports.literal("command.rejected"),
|
|
1607
|
+
clientActionId: external_exports.string().min(1).optional(),
|
|
1608
|
+
errorCode: external_exports.string().min(1),
|
|
1609
|
+
message: external_exports.string().min(1),
|
|
1610
|
+
currentGeneration: external_exports.number().int().nonnegative().optional(),
|
|
1611
|
+
currentVersion: external_exports.number().int().nonnegative().optional()
|
|
1612
|
+
});
|
|
1613
|
+
var HistoryRestoredFrameSchema = external_exports.object({
|
|
1614
|
+
type: external_exports.literal("history.restored"),
|
|
1615
|
+
restoreId: external_exports.string().min(1),
|
|
1616
|
+
generation: external_exports.number().int().nonnegative(),
|
|
1617
|
+
version: external_exports.number().int().nonnegative(),
|
|
1618
|
+
stateHash: external_exports.string().min(1),
|
|
1619
|
+
update: ProjectedSnapshotSchema
|
|
1620
|
+
});
|
|
1621
|
+
var HistoryRestoreRejectedFrameSchema = external_exports.object({
|
|
1622
|
+
type: external_exports.literal("history.restoreRejected"),
|
|
1623
|
+
restoreId: external_exports.string().min(1).optional(),
|
|
1624
|
+
errorCode: external_exports.string().min(1),
|
|
1625
|
+
message: external_exports.string().min(1),
|
|
1626
|
+
currentGeneration: external_exports.number().int().nonnegative().optional(),
|
|
1627
|
+
currentVersion: external_exports.number().int().nonnegative().optional()
|
|
1628
|
+
});
|
|
1629
|
+
var AuthAcceptedFrameSchema = external_exports.object({
|
|
1630
|
+
type: external_exports.literal("auth.accepted"),
|
|
1631
|
+
expiresAt: external_exports.string().datetime()
|
|
1632
|
+
});
|
|
1633
|
+
var AuthorityRecoveringFrameSchema = external_exports.object({
|
|
1634
|
+
type: external_exports.literal("authority.recovering"),
|
|
1635
|
+
retryAfterMs: external_exports.number().int().positive(),
|
|
1636
|
+
message: external_exports.string().min(1)
|
|
1637
|
+
});
|
|
1638
|
+
var SessionSnapshotFrameSchema = external_exports.object({
|
|
1639
|
+
type: external_exports.literal("session.snapshot"),
|
|
1640
|
+
generation: external_exports.number().int().nonnegative(),
|
|
1641
|
+
version: external_exports.number().int().nonnegative(),
|
|
1642
|
+
stateHash: external_exports.string().min(1),
|
|
1643
|
+
update: ProjectedSnapshotSchema
|
|
1644
|
+
});
|
|
1645
|
+
var SessionDeltaFrameSchema = external_exports.object({
|
|
1646
|
+
type: external_exports.literal("session.delta"),
|
|
1647
|
+
clientActionId: external_exports.string().min(1).optional(),
|
|
1648
|
+
update: ProjectedDeltaSchema
|
|
1649
|
+
});
|
|
1650
|
+
var ServerGameplayFrameSchema = external_exports.discriminatedUnion("type", [
|
|
1651
|
+
AuthAcceptedFrameSchema,
|
|
1652
|
+
AuthorityRecoveringFrameSchema,
|
|
1653
|
+
SessionSnapshotFrameSchema,
|
|
1654
|
+
SessionDeltaFrameSchema,
|
|
1655
|
+
CommandAcceptedFrameSchema,
|
|
1656
|
+
CommandRejectedFrameSchema,
|
|
1657
|
+
HistoryRestoredFrameSchema,
|
|
1658
|
+
HistoryRestoreRejectedFrameSchema
|
|
1659
|
+
]);
|
|
1660
|
+
var TunnelBoundFrameSchema = external_exports.object({
|
|
1661
|
+
type: external_exports.literal("tunnel.bound"),
|
|
1662
|
+
sessionId: external_exports.string().uuid(),
|
|
1663
|
+
playerId: external_exports.string().min(1)
|
|
1664
|
+
});
|
|
1665
|
+
var TunnelServerFrameSchema = external_exports.discriminatedUnion("type", [
|
|
1666
|
+
TunnelBoundFrameSchema,
|
|
1667
|
+
AuthorityRecoveringFrameSchema,
|
|
1668
|
+
SessionSnapshotFrameSchema,
|
|
1669
|
+
SessionDeltaFrameSchema,
|
|
1670
|
+
CommandAcceptedFrameSchema,
|
|
1671
|
+
CommandRejectedFrameSchema,
|
|
1672
|
+
HistoryRestoredFrameSchema,
|
|
1673
|
+
HistoryRestoreRejectedFrameSchema
|
|
1674
|
+
]);
|
|
1675
|
+
|
|
1676
|
+
// ../../node_modules/.pnpm/@dreamboard-games+gameplay-authority-client@0.1.0-alpha.1/node_modules/@dreamboard-games/gameplay-authority-client/dist/index.js
|
|
1677
|
+
var DEFAULT_OPEN_TIMEOUT_MS = 5e3;
|
|
1678
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 1e4;
|
|
1679
|
+
var GameplayAuthorityRecoveringError = class extends Error {
|
|
1680
|
+
retryAfterMs;
|
|
1681
|
+
constructor(message, retryAfterMs) {
|
|
1682
|
+
super(message);
|
|
1683
|
+
this.name = "GameplayAuthorityRecoveringError";
|
|
1684
|
+
this.retryAfterMs = retryAfterMs;
|
|
1685
|
+
}
|
|
1686
|
+
};
|
|
1687
|
+
async function connectGameplayAuthority(input) {
|
|
1688
|
+
const webSocketFactory = input.webSocketFactory ?? browserWebSocketFactory();
|
|
1689
|
+
const openTimeoutMs = input.openTimeoutMs ?? DEFAULT_OPEN_TIMEOUT_MS;
|
|
1690
|
+
const requestTimeoutMs = input.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
1691
|
+
const socket = new webSocketFactory(input.websocketUrl, input.webSocketInit);
|
|
1692
|
+
try {
|
|
1693
|
+
await waitUntilOpen(socket, openTimeoutMs);
|
|
1694
|
+
socket.send(JSON.stringify({
|
|
1695
|
+
type: "auth.connect",
|
|
1696
|
+
capabilityToken: input.capabilityToken
|
|
1697
|
+
}));
|
|
1698
|
+
await waitForServerFrame(socket, (frame) => frame.type === "auth.accepted", requestTimeoutMs);
|
|
1699
|
+
return new WebSocketGameplayAuthorityClient(socket, requestTimeoutMs);
|
|
1700
|
+
} catch (error) {
|
|
1701
|
+
socket.close();
|
|
1702
|
+
throw error;
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
var WebSocketGameplayAuthorityClient = class {
|
|
1706
|
+
socket;
|
|
1707
|
+
requestTimeoutMs;
|
|
1708
|
+
constructor(socket, requestTimeoutMs) {
|
|
1709
|
+
this.socket = socket;
|
|
1710
|
+
this.requestTimeoutMs = requestTimeoutMs;
|
|
1711
|
+
}
|
|
1712
|
+
resume(input = {}) {
|
|
1713
|
+
this.socket.send(JSON.stringify({
|
|
1714
|
+
type: "session.resume",
|
|
1715
|
+
lastSeenGeneration: input.lastSeenGeneration ?? null,
|
|
1716
|
+
lastSeenVersion: input.lastSeenVersion ?? null,
|
|
1717
|
+
unacknowledgedClientActionIds: input.unacknowledgedClientActionIds ?? []
|
|
1718
|
+
}));
|
|
1719
|
+
}
|
|
1720
|
+
async submitCommand(input) {
|
|
1721
|
+
this.socket.send(JSON.stringify({
|
|
1722
|
+
type: "command.submit",
|
|
1723
|
+
clientActionId: input.clientActionId,
|
|
1724
|
+
expectedVersion: input.expectedVersion,
|
|
1725
|
+
actionSetVersion: input.actionSetVersion,
|
|
1726
|
+
interactionId: input.interactionId,
|
|
1727
|
+
inputs: input.inputs
|
|
1728
|
+
}));
|
|
1729
|
+
const frame = await waitForServerFrame(this.socket, (candidate) => candidate.type === "authority.recovering" || candidate.type === "command.accepted" && candidate.clientActionId === input.clientActionId || candidate.type === "command.rejected" && (candidate.clientActionId === void 0 || candidate.clientActionId === input.clientActionId), this.requestTimeoutMs);
|
|
1730
|
+
throwIfRecovering(frame);
|
|
1731
|
+
if (frame.type !== "command.accepted" && frame.type !== "command.rejected") {
|
|
1732
|
+
throw new Error("Unexpected gameplay authority command frame.");
|
|
1733
|
+
}
|
|
1734
|
+
return frame;
|
|
1735
|
+
}
|
|
1736
|
+
async restoreHistory(input) {
|
|
1737
|
+
this.socket.send(JSON.stringify({
|
|
1738
|
+
type: "history.restore",
|
|
1739
|
+
restoreId: input.restoreId,
|
|
1740
|
+
targetGeneration: input.targetGeneration,
|
|
1741
|
+
targetVersion: input.targetVersion
|
|
1742
|
+
}));
|
|
1743
|
+
const frame = await waitForServerFrame(this.socket, (candidate) => candidate.type === "authority.recovering" || candidate.type === "history.restored" && candidate.restoreId === input.restoreId || candidate.type === "history.restoreRejected" && (candidate.restoreId === void 0 || candidate.restoreId === input.restoreId), this.requestTimeoutMs);
|
|
1744
|
+
throwIfRecovering(frame);
|
|
1745
|
+
if (frame.type !== "history.restored" && frame.type !== "history.restoreRejected") {
|
|
1746
|
+
throw new Error("Unexpected gameplay authority history frame.");
|
|
1747
|
+
}
|
|
1748
|
+
return frame;
|
|
1749
|
+
}
|
|
1750
|
+
frames(signal) {
|
|
1751
|
+
return readServerFrames(this.socket, signal);
|
|
1752
|
+
}
|
|
1753
|
+
close() {
|
|
1754
|
+
this.socket.close();
|
|
1755
|
+
}
|
|
1756
|
+
};
|
|
1757
|
+
function throwIfRecovering(frame) {
|
|
1758
|
+
if (frame.type === "authority.recovering") {
|
|
1759
|
+
throw new GameplayAuthorityRecoveringError(frame.message, frame.retryAfterMs);
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
function browserWebSocketFactory() {
|
|
1763
|
+
return BrowserGameplayAuthorityWebSocket;
|
|
1764
|
+
}
|
|
1765
|
+
var BrowserGameplayAuthorityWebSocket = class {
|
|
1766
|
+
socket;
|
|
1767
|
+
listeners = /* @__PURE__ */ new Map();
|
|
1768
|
+
constructor(url, init) {
|
|
1769
|
+
if (init?.headers && Object.keys(init.headers).length > 0) {
|
|
1770
|
+
throw new Error("Browser WebSocket does not support custom headers; provide a webSocketFactory for header-aware runtimes.");
|
|
1771
|
+
}
|
|
1772
|
+
this.socket = new WebSocket(url);
|
|
1773
|
+
}
|
|
1774
|
+
send(data) {
|
|
1775
|
+
this.socket.send(data);
|
|
1776
|
+
}
|
|
1777
|
+
close() {
|
|
1778
|
+
this.socket.close();
|
|
1779
|
+
}
|
|
1780
|
+
on(event, listener) {
|
|
1781
|
+
const wrapped = (observed) => {
|
|
1782
|
+
if (event === "message" && "data" in observed) {
|
|
1783
|
+
listener(observed.data);
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
listener(observed);
|
|
1787
|
+
};
|
|
1788
|
+
const listeners = this.listeners.get(event) ?? /* @__PURE__ */ new Map();
|
|
1789
|
+
listeners.set(listener, wrapped);
|
|
1790
|
+
this.listeners.set(event, listeners);
|
|
1791
|
+
this.socket.addEventListener(event, wrapped);
|
|
1792
|
+
}
|
|
1793
|
+
off(event, listener) {
|
|
1794
|
+
const listeners = this.listeners.get(event);
|
|
1795
|
+
const wrapped = listeners?.get(listener);
|
|
1796
|
+
if (!listeners || !wrapped) {
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
this.socket.removeEventListener(event, wrapped);
|
|
1800
|
+
listeners.delete(listener);
|
|
1801
|
+
}
|
|
1802
|
+
};
|
|
1803
|
+
async function waitUntilOpen(socket, timeoutMs) {
|
|
1804
|
+
await waitForEvent(socket, "open", timeoutMs);
|
|
1805
|
+
}
|
|
1806
|
+
async function waitForEvent(socket, event, timeoutMs) {
|
|
1807
|
+
return new Promise((resolve, reject) => {
|
|
1808
|
+
const cleanup = installSocketListeners(socket, {
|
|
1809
|
+
timeoutMs,
|
|
1810
|
+
onEvent(observed) {
|
|
1811
|
+
if (observed === event) {
|
|
1812
|
+
cleanup();
|
|
1813
|
+
resolve();
|
|
1814
|
+
}
|
|
1815
|
+
},
|
|
1816
|
+
onError(error) {
|
|
1817
|
+
cleanup();
|
|
1818
|
+
reject(error instanceof Error ? error : new Error("gameplay socket error"));
|
|
1819
|
+
},
|
|
1820
|
+
onClose() {
|
|
1821
|
+
cleanup();
|
|
1822
|
+
reject(new Error("gameplay socket closed"));
|
|
1823
|
+
},
|
|
1824
|
+
onTimeout() {
|
|
1825
|
+
cleanup();
|
|
1826
|
+
reject(new Error("gameplay socket timed out"));
|
|
1827
|
+
}
|
|
1828
|
+
});
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
async function waitForServerFrame(socket, predicate, timeoutMs) {
|
|
1832
|
+
return new Promise((resolve, reject) => {
|
|
1833
|
+
const cleanup = installSocketListeners(socket, {
|
|
1834
|
+
timeoutMs,
|
|
1835
|
+
onMessage(data) {
|
|
1836
|
+
let frame;
|
|
1837
|
+
try {
|
|
1838
|
+
frame = ServerGameplayFrameSchema.parse(JSON.parse(messageToString(data)));
|
|
1839
|
+
} catch (error) {
|
|
1840
|
+
cleanup();
|
|
1841
|
+
reject(error instanceof Error ? error : new Error("invalid gameplay authority frame"));
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
if (predicate(frame)) {
|
|
1845
|
+
cleanup();
|
|
1846
|
+
resolve(frame);
|
|
1847
|
+
}
|
|
1848
|
+
},
|
|
1849
|
+
onError(error) {
|
|
1850
|
+
cleanup();
|
|
1851
|
+
reject(error instanceof Error ? error : new Error("gameplay socket error"));
|
|
1852
|
+
},
|
|
1853
|
+
onClose() {
|
|
1854
|
+
cleanup();
|
|
1855
|
+
reject(new Error("gameplay socket closed"));
|
|
1856
|
+
},
|
|
1857
|
+
onTimeout() {
|
|
1858
|
+
cleanup();
|
|
1859
|
+
reject(new Error("gameplay socket timed out"));
|
|
1860
|
+
}
|
|
1861
|
+
});
|
|
1862
|
+
});
|
|
1863
|
+
}
|
|
1864
|
+
function installSocketListeners(socket, handlers) {
|
|
1865
|
+
const timer = setTimeout(handlers.onTimeout, handlers.timeoutMs);
|
|
1866
|
+
const onOpen = () => handlers.onEvent?.("open");
|
|
1867
|
+
const onMessage = (data) => handlers.onMessage?.(data);
|
|
1868
|
+
const onError = (error) => handlers.onError(error);
|
|
1869
|
+
const onClose = () => handlers.onClose();
|
|
1870
|
+
socket.on("open", onOpen);
|
|
1871
|
+
socket.on("message", onMessage);
|
|
1872
|
+
socket.on("error", onError);
|
|
1873
|
+
socket.on("close", onClose);
|
|
1874
|
+
return () => {
|
|
1875
|
+
clearTimeout(timer);
|
|
1876
|
+
socket.off?.("open", onOpen);
|
|
1877
|
+
socket.off?.("message", onMessage);
|
|
1878
|
+
socket.off?.("error", onError);
|
|
1879
|
+
socket.off?.("close", onClose);
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
function readServerFrames(socket, signal) {
|
|
1883
|
+
const queue = [];
|
|
1884
|
+
let wake = null;
|
|
1885
|
+
let closed = false;
|
|
1886
|
+
let error = null;
|
|
1887
|
+
const wakeReader = () => {
|
|
1888
|
+
wake?.();
|
|
1889
|
+
wake = null;
|
|
1890
|
+
};
|
|
1891
|
+
const onMessage = (data) => {
|
|
1892
|
+
try {
|
|
1893
|
+
queue.push(ServerGameplayFrameSchema.parse(JSON.parse(messageToString(data))));
|
|
1894
|
+
} catch (candidate) {
|
|
1895
|
+
error = candidate instanceof Error ? candidate : new Error("invalid gameplay authority frame");
|
|
1896
|
+
}
|
|
1897
|
+
wakeReader();
|
|
1898
|
+
};
|
|
1899
|
+
const onError = (candidate) => {
|
|
1900
|
+
error = candidate instanceof Error ? candidate : new Error("gameplay socket error");
|
|
1901
|
+
wakeReader();
|
|
1902
|
+
};
|
|
1903
|
+
const onClose = () => {
|
|
1904
|
+
closed = true;
|
|
1905
|
+
wakeReader();
|
|
1906
|
+
};
|
|
1907
|
+
const onAbort = () => {
|
|
1908
|
+
closed = true;
|
|
1909
|
+
wakeReader();
|
|
1910
|
+
};
|
|
1911
|
+
socket.on("message", onMessage);
|
|
1912
|
+
socket.on("error", onError);
|
|
1913
|
+
socket.on("close", onClose);
|
|
1914
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1915
|
+
const cleanup = () => {
|
|
1916
|
+
socket.off?.("message", onMessage);
|
|
1917
|
+
socket.off?.("error", onError);
|
|
1918
|
+
socket.off?.("close", onClose);
|
|
1919
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1920
|
+
};
|
|
1921
|
+
const iterator = {
|
|
1922
|
+
async next() {
|
|
1923
|
+
while (!closed && !signal?.aborted) {
|
|
1924
|
+
const next = queue.shift();
|
|
1925
|
+
if (next) {
|
|
1926
|
+
return { done: false, value: next };
|
|
1927
|
+
}
|
|
1928
|
+
if (error) {
|
|
1929
|
+
cleanup();
|
|
1930
|
+
throw error;
|
|
1931
|
+
}
|
|
1932
|
+
await new Promise((resolve) => {
|
|
1933
|
+
wake = resolve;
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
cleanup();
|
|
1937
|
+
if (error) {
|
|
1938
|
+
throw error;
|
|
1939
|
+
}
|
|
1940
|
+
return { done: true, value: void 0 };
|
|
1941
|
+
},
|
|
1942
|
+
async return(value) {
|
|
1943
|
+
closed = true;
|
|
1944
|
+
wakeReader();
|
|
1945
|
+
cleanup();
|
|
1946
|
+
return { done: true, value };
|
|
1947
|
+
},
|
|
1948
|
+
async throw(candidate) {
|
|
1949
|
+
closed = true;
|
|
1950
|
+
wakeReader();
|
|
1951
|
+
cleanup();
|
|
1952
|
+
throw candidate;
|
|
1953
|
+
},
|
|
1954
|
+
[Symbol.asyncIterator]() {
|
|
1955
|
+
return this;
|
|
1956
|
+
},
|
|
1957
|
+
async [Symbol.asyncDispose]() {
|
|
1958
|
+
await this.return(void 0);
|
|
1959
|
+
}
|
|
1960
|
+
};
|
|
1961
|
+
return iterator;
|
|
1962
|
+
}
|
|
1963
|
+
function messageToString(data) {
|
|
1964
|
+
if (typeof data === "string") {
|
|
1965
|
+
return data;
|
|
1966
|
+
}
|
|
1967
|
+
if (data instanceof ArrayBuffer) {
|
|
1968
|
+
return new TextDecoder().decode(data);
|
|
1969
|
+
}
|
|
1970
|
+
if (ArrayBuffer.isView(data)) {
|
|
1971
|
+
return new TextDecoder().decode(data);
|
|
1972
|
+
}
|
|
1973
|
+
return String(data);
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
// src/services/gameplay-authority-submit.ts
|
|
1977
|
+
var CLIENT_ACTION_ID_HEADER = "X-Dreamboard-Client-Action-Id";
|
|
1978
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
1979
|
+
async function submitGameplayAuthorityAction(options) {
|
|
1980
|
+
try {
|
|
1981
|
+
const requestCapability = options.capabilityRequester ?? createGameplayCapability;
|
|
1982
|
+
const connectAuthority = options.connectAuthority ?? connectGameplayAuthority;
|
|
1983
|
+
const capability = await requestCapability({
|
|
1984
|
+
path: {
|
|
1985
|
+
sessionId: options.path.sessionId,
|
|
1986
|
+
playerId: options.path.playerId
|
|
1987
|
+
}
|
|
1988
|
+
});
|
|
1989
|
+
if (capability.error || !capability.data) {
|
|
1990
|
+
return { error: capability.error ?? new Error("Missing capability.") };
|
|
1991
|
+
}
|
|
1992
|
+
const clientActionId = options.headers?.[CLIENT_ACTION_ID_HEADER] ?? options.clientActionIdFactory?.() ?? randomUUID2();
|
|
1993
|
+
const client2 = await connectAuthority({
|
|
1994
|
+
websocketUrl: capability.data.websocketUrl,
|
|
1995
|
+
capabilityToken: capability.data.token,
|
|
1996
|
+
openTimeoutMs: DEFAULT_TIMEOUT_MS,
|
|
1997
|
+
requestTimeoutMs: DEFAULT_TIMEOUT_MS
|
|
1998
|
+
});
|
|
1999
|
+
try {
|
|
2000
|
+
const frame = await client2.submitCommand({
|
|
2001
|
+
clientActionId,
|
|
2002
|
+
expectedVersion: options.body.expectedVersion,
|
|
2003
|
+
actionSetVersion: options.body.actionSetVersion,
|
|
2004
|
+
interactionId: options.path.interactionId,
|
|
2005
|
+
inputs: options.body.inputs ?? {}
|
|
2006
|
+
});
|
|
2007
|
+
if (frame.type === "command.rejected") {
|
|
2008
|
+
return {
|
|
2009
|
+
data: {
|
|
2010
|
+
success: false,
|
|
2011
|
+
accepted: false,
|
|
2012
|
+
version: typeof frame.currentVersion === "number" ? frame.currentVersion : options.body.expectedVersion,
|
|
2013
|
+
actionSetVersion: options.body.actionSetVersion,
|
|
2014
|
+
errorCode: typeof frame.errorCode === "string" ? frame.errorCode : void 0,
|
|
2015
|
+
message: typeof frame.message === "string" ? frame.message : void 0,
|
|
2016
|
+
clientActionId
|
|
2017
|
+
}
|
|
2018
|
+
};
|
|
2019
|
+
}
|
|
2020
|
+
const acceptedVersion = typeof frame.version === "number" ? frame.version : void 0;
|
|
2021
|
+
if (acceptedVersion === void 0) {
|
|
2022
|
+
return {
|
|
2023
|
+
error: new Error("Accepted frame did not include a numeric version.")
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
2026
|
+
const update = frame.update && typeof frame.update === "object" ? frame.update : {};
|
|
2027
|
+
const view = update.view && typeof update.view === "object" ? update.view : {};
|
|
2028
|
+
const projectedActionSetVersion = typeof view.actionSetVersion === "string" ? view.actionSetVersion : void 0;
|
|
2029
|
+
return {
|
|
2030
|
+
data: {
|
|
2031
|
+
success: true,
|
|
2032
|
+
accepted: true,
|
|
2033
|
+
durabilityStatus: "COMMITTED",
|
|
2034
|
+
version: acceptedVersion,
|
|
2035
|
+
actionSetVersion: projectedActionSetVersion ?? options.body.actionSetVersion,
|
|
2036
|
+
clientActionId
|
|
2037
|
+
}
|
|
2038
|
+
};
|
|
2039
|
+
} catch (error) {
|
|
2040
|
+
if (error instanceof GameplayAuthorityRecoveringError) {
|
|
2041
|
+
return {
|
|
2042
|
+
error: new Error(error.message || "Session authority is recovering.")
|
|
2043
|
+
};
|
|
2044
|
+
}
|
|
2045
|
+
throw error;
|
|
2046
|
+
} finally {
|
|
2047
|
+
client2.close();
|
|
2048
|
+
}
|
|
2049
|
+
} catch (error) {
|
|
2050
|
+
return { error };
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
// src/services/workflows/resolve-setup-profile.ts
|
|
2055
|
+
function resolveSetupProfileSelection(options) {
|
|
2056
|
+
const setupProfiles = options.manifest.setupProfiles ?? [];
|
|
2057
|
+
const requestedSetupProfileId = options.requestedSetupProfileId?.trim() || void 0;
|
|
2058
|
+
if (requestedSetupProfileId) {
|
|
2059
|
+
const requestedProfile = setupProfiles.find(
|
|
2060
|
+
(profile) => profile.id === requestedSetupProfileId
|
|
2061
|
+
);
|
|
2062
|
+
if (!requestedProfile) {
|
|
2063
|
+
const knownProfiles = setupProfiles.map((profile) => profile.id).join(", ");
|
|
2064
|
+
throw new Error(
|
|
2065
|
+
setupProfiles.length === 0 ? `Unknown setup profile '${requestedSetupProfileId}'. The manifest defines no setup profiles.` : `Unknown setup profile '${requestedSetupProfileId}'. Expected one of: ${knownProfiles}.`
|
|
2066
|
+
);
|
|
2067
|
+
}
|
|
2068
|
+
return {
|
|
2069
|
+
id: requestedProfile.id,
|
|
2070
|
+
name: requestedProfile.name,
|
|
2071
|
+
source: "explicit"
|
|
2072
|
+
};
|
|
2073
|
+
}
|
|
2074
|
+
if (setupProfiles.length === 0) {
|
|
2075
|
+
return {
|
|
2076
|
+
id: null,
|
|
2077
|
+
name: null,
|
|
2078
|
+
source: "none"
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
if (setupProfiles.length === 1) {
|
|
2082
|
+
return {
|
|
2083
|
+
id: setupProfiles[0]?.id ?? null,
|
|
2084
|
+
name: setupProfiles[0]?.name ?? null,
|
|
2085
|
+
source: "implicit-single"
|
|
2086
|
+
};
|
|
2087
|
+
}
|
|
2088
|
+
return {
|
|
2089
|
+
id: setupProfiles[0]?.id ?? null,
|
|
2090
|
+
name: setupProfiles[0]?.name ?? null,
|
|
2091
|
+
source: "implicit-first"
|
|
2092
|
+
};
|
|
2093
|
+
}
|
|
2094
|
+
async function resolveSetupProfileSelectionForSession(options) {
|
|
2095
|
+
const manifest = await loadManifest(options.projectRoot);
|
|
2096
|
+
return resolveSetupProfileSelection({
|
|
2097
|
+
manifest,
|
|
2098
|
+
requestedSetupProfileId: options.requestedSetupProfileId
|
|
2099
|
+
});
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
// src/utils/session-game-source.ts
|
|
2103
|
+
function projectIdFromSessionGameSource(source) {
|
|
2104
|
+
if (source.kind === "USER_COMPILED") {
|
|
2105
|
+
return source.projectId;
|
|
2106
|
+
}
|
|
2107
|
+
return `demo:${source.slug}:${source.revisionId}`;
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
// src/services/testing/reducer-native-test-harness.ts
|
|
2111
|
+
globalThis.__DREAMBOARD_AUTHORING_WARNINGS__ = true;
|
|
2112
|
+
var GENERATED_TESTING_TYPES_PREFIX = "// Generated by dreamboard";
|
|
2113
|
+
var TESTING_TYPES_STUB = "export function defineScenario(scenario) { return scenario; }\n";
|
|
2114
|
+
var BASE_SUFFIX = ".base.ts";
|
|
2115
|
+
var SCENARIO_SUFFIX = ".scenario.ts";
|
|
2116
|
+
var SDK_UI_RUNTIME_EXTERNALS = [
|
|
2117
|
+
"@radix-ui/react-accordion",
|
|
2118
|
+
"@radix-ui/react-dialog",
|
|
2119
|
+
"@radix-ui/react-label",
|
|
2120
|
+
"@radix-ui/react-select",
|
|
2121
|
+
"@radix-ui/react-slot",
|
|
2122
|
+
"@radix-ui/react-tooltip",
|
|
2123
|
+
"@use-gesture/react",
|
|
2124
|
+
"clsx",
|
|
2125
|
+
"framer-motion",
|
|
2126
|
+
"lucide-react",
|
|
2127
|
+
"react",
|
|
2128
|
+
"react-dom",
|
|
2129
|
+
"vaul"
|
|
2130
|
+
];
|
|
2131
|
+
function formatScenarioErrorForDisplay(options) {
|
|
2132
|
+
const message = options.error.message || "Scenario failed.";
|
|
2133
|
+
const scenarioFrame = findScenarioStackFrame({
|
|
2134
|
+
stack: options.error.stack,
|
|
2135
|
+
projectRoot: options.projectRoot,
|
|
2136
|
+
scenarioFilePath: options.scenarioFilePath
|
|
2137
|
+
});
|
|
2138
|
+
return scenarioFrame ? `${message}
|
|
2139
|
+
${scenarioFrame}` : message;
|
|
2140
|
+
}
|
|
2141
|
+
function findScenarioStackFrame(options) {
|
|
2142
|
+
if (!options.stack) {
|
|
2143
|
+
return null;
|
|
2144
|
+
}
|
|
2145
|
+
const absolutePath = path3.resolve(options.scenarioFilePath);
|
|
2146
|
+
const relativePath = path3.relative(options.projectRoot, absolutePath);
|
|
2147
|
+
const normalizedRelativePath = relativePath.split(path3.sep).join("/");
|
|
2148
|
+
const escapedAbsolutePath = escapeRegExp(absolutePath);
|
|
2149
|
+
const escapedRelativePath = escapeRegExp(normalizedRelativePath);
|
|
2150
|
+
const absoluteFrame = new RegExp(`${escapedAbsolutePath}:(\\d+):(\\d+)`);
|
|
2151
|
+
const relativeFrame = new RegExp(`${escapedRelativePath}:(\\d+):(\\d+)`);
|
|
2152
|
+
for (const line of options.stack.split("\n")) {
|
|
2153
|
+
const normalizedLine = line.split(path3.sep).join("/");
|
|
2154
|
+
const match = normalizedLine.match(absoluteFrame) ?? normalizedLine.match(relativeFrame);
|
|
2155
|
+
if (match?.[1] && match?.[2]) {
|
|
2156
|
+
return `at ${normalizedRelativePath}:${match[1]}:${match[2]}`;
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
return null;
|
|
2160
|
+
}
|
|
2161
|
+
function escapeRegExp(value) {
|
|
2162
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2163
|
+
}
|
|
2164
|
+
var projectReducerNativeModules = /* @__PURE__ */ new Map();
|
|
2165
|
+
function resolveProjectSdkModule(projectRoot, specifier) {
|
|
2166
|
+
const requireFromProject = createRequire(
|
|
2167
|
+
path3.join(projectRoot, "package.json")
|
|
2168
|
+
);
|
|
2169
|
+
return requireFromProject.resolve(specifier);
|
|
2170
|
+
}
|
|
2171
|
+
async function importProjectSdkModule(projectRoot, specifier) {
|
|
2172
|
+
const modulePath = resolveProjectSdkModule(projectRoot, specifier);
|
|
2173
|
+
return await import(pathToFileURL2(modulePath).href);
|
|
2174
|
+
}
|
|
2175
|
+
async function loadProjectReducerNativeModules(projectRoot) {
|
|
2176
|
+
const cacheKey = path3.resolve(projectRoot);
|
|
2177
|
+
const cached = projectReducerNativeModules.get(cacheKey);
|
|
2178
|
+
if (cached) {
|
|
2179
|
+
return cached;
|
|
2180
|
+
}
|
|
2181
|
+
const promise = Promise.all([
|
|
2182
|
+
importProjectSdkModule(cacheKey, "@dreamboard-games/sdk/reducer"),
|
|
2183
|
+
importProjectSdkModule(cacheKey, "@dreamboard-games/sdk/reducer-contract"),
|
|
2184
|
+
importProjectSdkModule(
|
|
2185
|
+
cacheKey,
|
|
2186
|
+
"@dreamboard-games/sdk/testing"
|
|
2187
|
+
)
|
|
2188
|
+
]).then(([reducerModule, reducerContractModule, testingModule]) => {
|
|
2189
|
+
if (typeof reducerModule.createReducerBundle !== "function" || typeof reducerModule.contractFingerprint !== "function" || typeof reducerContractModule.materializeManifestTable !== "function" || typeof testingModule.createExpectApi !== "function") {
|
|
2190
|
+
throw new Error(
|
|
2191
|
+
"Installed @dreamboard-games/sdk does not expose the reducer-native test helpers required by this CLI."
|
|
2192
|
+
);
|
|
2193
|
+
}
|
|
2194
|
+
return {
|
|
2195
|
+
createReducerBundle: reducerModule.createReducerBundle,
|
|
2196
|
+
contractFingerprint: reducerModule.contractFingerprint,
|
|
2197
|
+
materializeManifestTable: reducerContractModule.materializeManifestTable,
|
|
2198
|
+
createExpectApi: testingModule.createExpectApi
|
|
2199
|
+
};
|
|
2200
|
+
});
|
|
2201
|
+
projectReducerNativeModules.set(cacheKey, promise);
|
|
2202
|
+
return promise;
|
|
2203
|
+
}
|
|
2204
|
+
function createSubmissionError(errorCode, message, fallbackMessage) {
|
|
2205
|
+
const error = new Error(message ?? fallbackMessage);
|
|
2206
|
+
error.name = "SubmissionError";
|
|
2207
|
+
error.errorCode = errorCode;
|
|
2208
|
+
return error;
|
|
2209
|
+
}
|
|
2210
|
+
function deepEqual(left, right) {
|
|
2211
|
+
return isDeepStrictEqual(left, right);
|
|
2212
|
+
}
|
|
2213
|
+
function shouldRefreshReducerTestingTypes(existingContent) {
|
|
2214
|
+
if (existingContent === null || existingContent.trim().length === 0 || existingContent === TESTING_TYPES_STUB || existingContent.startsWith(GENERATED_TESTING_TYPES_PREFIX)) {
|
|
2215
|
+
return true;
|
|
2216
|
+
}
|
|
2217
|
+
return false;
|
|
2218
|
+
}
|
|
2219
|
+
async function discoverFiles(root, suffix) {
|
|
2220
|
+
if (!await exists(root)) {
|
|
2221
|
+
return [];
|
|
2222
|
+
}
|
|
2223
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
2224
|
+
const files = [];
|
|
2225
|
+
for (const entry of entries) {
|
|
2226
|
+
const entryPath = path3.join(root, entry.name);
|
|
2227
|
+
if (entry.isDirectory()) {
|
|
2228
|
+
files.push(...await discoverFiles(entryPath, suffix));
|
|
2229
|
+
continue;
|
|
2230
|
+
}
|
|
2231
|
+
if (entry.isFile() && entry.name.endsWith(suffix)) {
|
|
2232
|
+
files.push(entryPath);
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
files.sort();
|
|
2236
|
+
return files;
|
|
2237
|
+
}
|
|
2238
|
+
function isFunction(value) {
|
|
2239
|
+
return typeof value === "function";
|
|
2240
|
+
}
|
|
2241
|
+
function parseTypedBaseDefinition(value) {
|
|
2242
|
+
if (typeof value !== "object" || value === null || !("id" in value) || typeof value.id !== "string" || !("setup" in value) || !isFunction(value.setup)) {
|
|
2243
|
+
throw new Error("Invalid reducer-native base definition.");
|
|
2244
|
+
}
|
|
2245
|
+
const parentBaseId = "extends" in value && typeof value.extends === "string" ? value.extends : void 0;
|
|
2246
|
+
const seed = "seed" in value && typeof value.seed === "number" ? value.seed : void 0;
|
|
2247
|
+
const players = "players" in value && typeof value.players === "number" ? value.players : void 0;
|
|
2248
|
+
if ((seed === void 0 || players === void 0) && !parentBaseId) {
|
|
2249
|
+
throw new Error(
|
|
2250
|
+
"Invalid reducer-native base definition. Base definitions without --extends must declare numeric seed and players."
|
|
2251
|
+
);
|
|
2252
|
+
}
|
|
2253
|
+
const setupProfileId = "setupProfileId" in value && typeof value.setupProfileId === "string" ? value.setupProfileId : void 0;
|
|
2254
|
+
return {
|
|
2255
|
+
...value,
|
|
2256
|
+
seed,
|
|
2257
|
+
players,
|
|
2258
|
+
setupProfileId,
|
|
2259
|
+
extends: parentBaseId
|
|
2260
|
+
};
|
|
2261
|
+
}
|
|
2262
|
+
function parseTypedScenarioDefinition(value) {
|
|
2263
|
+
if (typeof value !== "object" || value === null || !("id" in value) || typeof value.id !== "string" || !("from" in value) || typeof value.from !== "string" || !("when" in value) || !isFunction(value.when) || !("then" in value) || !isFunction(value.then)) {
|
|
2264
|
+
throw new Error("Invalid reducer-native scenario definition.");
|
|
2265
|
+
}
|
|
2266
|
+
const phase = "phase" in value && typeof value.phase === "string" ? value.phase : void 0;
|
|
2267
|
+
const stage = "stage" in value && typeof value.stage === "string" ? value.stage : void 0;
|
|
2268
|
+
return {
|
|
2269
|
+
...value,
|
|
2270
|
+
phase,
|
|
2271
|
+
stage
|
|
2272
|
+
};
|
|
2273
|
+
}
|
|
2274
|
+
async function isReducerNativeTestingWorkspace(projectRoot) {
|
|
2275
|
+
return await exists(path3.join(projectRoot, "app", "game.ts")) && await exists(
|
|
2276
|
+
path3.join(projectRoot, "shared", "generated", "ui-contract.ts")
|
|
2277
|
+
);
|
|
2278
|
+
}
|
|
2279
|
+
async function ensureReducerNativeTestingFiles(projectRoot) {
|
|
2280
|
+
const testingTypesPath = "test/testing-types.ts";
|
|
2281
|
+
const testingContractPath = "test/generated/testing-contract.ts";
|
|
2282
|
+
const baseStatesPath = "test/generated/base-states.generated.ts";
|
|
2283
|
+
const baseStatesDtsPath = "test/generated/base-states.generated.d.ts";
|
|
2284
|
+
const scenarioManifestPath = "test/generated/scenario-manifest.generated.ts";
|
|
2285
|
+
await ensureDir(resolveWorkspacePath(projectRoot, "test"));
|
|
2286
|
+
await ensureDir(resolveWorkspacePath(projectRoot, "test/generated"));
|
|
2287
|
+
const existingTestingTypes = await readWorkspaceTextFileIfExists(
|
|
2288
|
+
projectRoot,
|
|
2289
|
+
testingTypesPath
|
|
2290
|
+
);
|
|
2291
|
+
if (shouldRefreshReducerTestingTypes(existingTestingTypes)) {
|
|
2292
|
+
await writeWorkspaceTextFile(
|
|
2293
|
+
projectRoot,
|
|
2294
|
+
testingTypesPath,
|
|
2295
|
+
REDUCER_TESTING_TYPES_WRAPPER_CONTENT
|
|
2296
|
+
);
|
|
2297
|
+
}
|
|
2298
|
+
const rejectionCodes = await collectKnownRejectionCodes(projectRoot);
|
|
2299
|
+
await writeWorkspaceTextFile(
|
|
2300
|
+
projectRoot,
|
|
2301
|
+
testingContractPath,
|
|
2302
|
+
buildReducerTestingContractContent({ rejectionCodes })
|
|
2303
|
+
);
|
|
2304
|
+
const header = "// Generated by dreamboard test. Do not edit by hand.\n";
|
|
2305
|
+
if (!await workspacePathExists(projectRoot, baseStatesPath)) {
|
|
2306
|
+
await writeWorkspaceTextFile(
|
|
2307
|
+
projectRoot,
|
|
2308
|
+
baseStatesPath,
|
|
2309
|
+
`${header}export const BASE_STATES = {} as const;
|
|
2310
|
+
export const BASE_STATES_CONTRACT_FINGERPRINT = undefined;
|
|
2311
|
+
`
|
|
2312
|
+
);
|
|
2313
|
+
} else {
|
|
2314
|
+
const existingBaseStates = await readWorkspaceTextFileIfExists(
|
|
2315
|
+
projectRoot,
|
|
2316
|
+
baseStatesPath
|
|
2317
|
+
);
|
|
2318
|
+
if (existingBaseStates && !existingBaseStates.includes("BASE_STATES_CONTRACT_FINGERPRINT")) {
|
|
2319
|
+
await writeWorkspaceTextFile(
|
|
2320
|
+
projectRoot,
|
|
2321
|
+
baseStatesPath,
|
|
2322
|
+
`${existingBaseStates.trimEnd()}
|
|
2323
|
+
export const BASE_STATES_CONTRACT_FINGERPRINT = undefined;
|
|
2324
|
+
`
|
|
2325
|
+
);
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
if (!await workspacePathExists(projectRoot, baseStatesDtsPath)) {
|
|
2329
|
+
await writeWorkspaceTextFile(
|
|
2330
|
+
projectRoot,
|
|
2331
|
+
baseStatesDtsPath,
|
|
2332
|
+
`${header}export declare const BASE_STATES: Record<string, unknown>;
|
|
2333
|
+
export declare const BASE_STATES_CONTRACT_FINGERPRINT: string | undefined;
|
|
2334
|
+
`
|
|
2335
|
+
);
|
|
2336
|
+
} else {
|
|
2337
|
+
const existingBaseStatesDts = await readWorkspaceTextFileIfExists(
|
|
2338
|
+
projectRoot,
|
|
2339
|
+
baseStatesDtsPath
|
|
2340
|
+
);
|
|
2341
|
+
if (existingBaseStatesDts && !existingBaseStatesDts.includes("BASE_STATES_CONTRACT_FINGERPRINT")) {
|
|
2342
|
+
await writeWorkspaceTextFile(
|
|
2343
|
+
projectRoot,
|
|
2344
|
+
baseStatesDtsPath,
|
|
2345
|
+
`${existingBaseStatesDts.trimEnd()}
|
|
2346
|
+
export declare const BASE_STATES_CONTRACT_FINGERPRINT: string | undefined;
|
|
2347
|
+
`
|
|
2348
|
+
);
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
if (!await workspacePathExists(projectRoot, scenarioManifestPath)) {
|
|
2352
|
+
await writeWorkspaceTextFile(
|
|
2353
|
+
projectRoot,
|
|
2354
|
+
scenarioManifestPath,
|
|
2355
|
+
`${header}export const SCENARIO_MANIFEST = [] as const;
|
|
2356
|
+
`
|
|
2357
|
+
);
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
var DEFAULT_REJECTION_CODES = [
|
|
2361
|
+
"NOT_YOUR_TURN",
|
|
2362
|
+
"action-unavailable",
|
|
2363
|
+
"invalid-action-params",
|
|
2364
|
+
"prompt-not-owned"
|
|
2365
|
+
];
|
|
2366
|
+
async function collectKnownRejectionCodes(projectRoot) {
|
|
2367
|
+
const knownCodes = new Set(DEFAULT_REJECTION_CODES);
|
|
2368
|
+
const gamePath = path3.join(projectRoot, "app", "game.ts");
|
|
2369
|
+
if (!await exists(gamePath)) {
|
|
2370
|
+
return Array.from(knownCodes).sort(
|
|
2371
|
+
(left, right) => left.localeCompare(right)
|
|
2372
|
+
);
|
|
2373
|
+
}
|
|
2374
|
+
try {
|
|
2375
|
+
const module = await importTypeScriptModule(gamePath);
|
|
2376
|
+
const phases = module.default?.phases ?? {};
|
|
2377
|
+
for (const phase of Object.values(phases)) {
|
|
2378
|
+
const interactions = phase.interactions ?? {};
|
|
2379
|
+
for (const interaction of Object.values(interactions)) {
|
|
2380
|
+
for (const errorCode of interaction.errorCodes ?? []) {
|
|
2381
|
+
if (typeof errorCode === "string" && errorCode.length > 0) {
|
|
2382
|
+
knownCodes.add(errorCode);
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
} catch {
|
|
2388
|
+
}
|
|
2389
|
+
return Array.from(knownCodes).sort(
|
|
2390
|
+
(left, right) => left.localeCompare(right)
|
|
2391
|
+
);
|
|
2392
|
+
}
|
|
2393
|
+
async function loadTypedBases(projectRoot) {
|
|
2394
|
+
const baseFiles = await discoverFiles(
|
|
2395
|
+
resolveWorkspacePath(projectRoot, "test/bases"),
|
|
2396
|
+
BASE_SUFFIX
|
|
2397
|
+
);
|
|
2398
|
+
const loaded = [];
|
|
2399
|
+
for (const filePath of baseFiles) {
|
|
2400
|
+
const externals = reducerNativeTestHelperExternals(projectRoot, filePath);
|
|
2401
|
+
const [module, bundledText] = await Promise.all([
|
|
2402
|
+
importTypeScriptModule(filePath),
|
|
2403
|
+
bundleTypeScriptModuleText(filePath, { external: externals })
|
|
2404
|
+
]);
|
|
2405
|
+
loaded.push({
|
|
2406
|
+
filePath,
|
|
2407
|
+
definition: parseTypedBaseDefinition(module.default),
|
|
2408
|
+
bundleHash: hashContent(bundledText)
|
|
2409
|
+
});
|
|
2410
|
+
}
|
|
2411
|
+
return loaded;
|
|
2412
|
+
}
|
|
2413
|
+
async function loadTypedScenarios(projectRoot, options) {
|
|
2414
|
+
const scenarioFiles = options.scenarioPath ? [resolveWorkspacePath(projectRoot, options.scenarioPath)] : await discoverFiles(
|
|
2415
|
+
resolveWorkspacePath(projectRoot, "test/scenarios"),
|
|
2416
|
+
SCENARIO_SUFFIX
|
|
2417
|
+
);
|
|
2418
|
+
const loaded = [];
|
|
2419
|
+
for (const filePath of scenarioFiles) {
|
|
2420
|
+
const externals = reducerNativeTestHelperExternals(projectRoot, filePath);
|
|
2421
|
+
const [module, bundledText] = await Promise.all([
|
|
2422
|
+
importTypeScriptModule(filePath),
|
|
2423
|
+
bundleTypeScriptModuleText(filePath, { external: externals })
|
|
2424
|
+
]);
|
|
2425
|
+
loaded.push({
|
|
2426
|
+
filePath,
|
|
2427
|
+
definition: parseTypedScenarioDefinition(module.default),
|
|
2428
|
+
bundleHash: hashContent(bundledText)
|
|
2429
|
+
});
|
|
2430
|
+
}
|
|
2431
|
+
return loaded;
|
|
2432
|
+
}
|
|
2433
|
+
function reducerNativeTestHelperExternals(projectRoot, filePath) {
|
|
2434
|
+
const testingTypesPath = path3.join(projectRoot, "test", "testing-types");
|
|
2435
|
+
const relative = path3.relative(path3.dirname(filePath), testingTypesPath).replaceAll("\\", "/");
|
|
2436
|
+
const specifier = relative.startsWith(".") ? relative : `./${relative}`;
|
|
2437
|
+
return [specifier, `${specifier}.ts`, ...SDK_UI_RUNTIME_EXTERNALS];
|
|
2438
|
+
}
|
|
2439
|
+
var JS_MAX_SAFE_INTEGER = 9007199254740991n;
|
|
2440
|
+
var JS_MIN_SAFE_INTEGER = -JS_MAX_SAFE_INTEGER;
|
|
2441
|
+
var JS_MAX_SAFE_INTEGER_EXCLUSIVE = JS_MAX_SAFE_INTEGER + 1n;
|
|
2442
|
+
var KotlinSeededRandom = class {
|
|
2443
|
+
x;
|
|
2444
|
+
y;
|
|
2445
|
+
z;
|
|
2446
|
+
w;
|
|
2447
|
+
v;
|
|
2448
|
+
addend;
|
|
2449
|
+
constructor(seed) {
|
|
2450
|
+
const seedBig = BigInt.asIntN(64, BigInt(Math.trunc(seed)));
|
|
2451
|
+
const seed1 = Number(BigInt.asIntN(32, seedBig));
|
|
2452
|
+
const seed2 = Number(BigInt.asIntN(32, seedBig >> 32n));
|
|
2453
|
+
this.x = seed1 | 0;
|
|
2454
|
+
this.y = seed2 | 0;
|
|
2455
|
+
this.z = 0;
|
|
2456
|
+
this.w = 0;
|
|
2457
|
+
this.v = ~seed1;
|
|
2458
|
+
this.addend = seed1 << 10 ^ seed2 >>> 4 | 0;
|
|
2459
|
+
if ((this.x | this.y | this.z | this.w | this.v) === 0) {
|
|
2460
|
+
throw new Error(
|
|
2461
|
+
"Kotlin random seed must initialize at least one state word."
|
|
2462
|
+
);
|
|
2463
|
+
}
|
|
2464
|
+
for (let index = 0; index < 64; index += 1) {
|
|
2465
|
+
this.nextInt();
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
nextInt() {
|
|
2469
|
+
let next = this.x | 0;
|
|
2470
|
+
next ^= next >>> 2;
|
|
2471
|
+
this.x = this.y;
|
|
2472
|
+
this.y = this.z;
|
|
2473
|
+
this.z = this.w;
|
|
2474
|
+
const currentV = this.v | 0;
|
|
2475
|
+
this.w = currentV;
|
|
2476
|
+
next = next ^ next << 1 ^ currentV ^ currentV << 4 | 0;
|
|
2477
|
+
this.v = next;
|
|
2478
|
+
this.addend = this.addend + 362437 | 0;
|
|
2479
|
+
return next + this.addend | 0;
|
|
2480
|
+
}
|
|
2481
|
+
nextBits(bitCount) {
|
|
2482
|
+
if (bitCount <= 0) {
|
|
2483
|
+
return 0;
|
|
2484
|
+
}
|
|
2485
|
+
return this.nextInt() >>> 32 - bitCount;
|
|
2486
|
+
}
|
|
2487
|
+
nextIntBound(bound) {
|
|
2488
|
+
if (bound <= 0) {
|
|
2489
|
+
throw new Error("bound must be positive");
|
|
2490
|
+
}
|
|
2491
|
+
if ((bound & -bound) === bound) {
|
|
2492
|
+
const bitCount = 31 - Math.clz32(bound);
|
|
2493
|
+
return this.nextBits(bitCount);
|
|
2494
|
+
}
|
|
2495
|
+
let value;
|
|
2496
|
+
while (true) {
|
|
2497
|
+
const bits = this.nextInt() >>> 1;
|
|
2498
|
+
value = bits % bound;
|
|
2499
|
+
if (bits - value + (bound - 1) >= 0) {
|
|
2500
|
+
return value;
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
nextLong() {
|
|
2505
|
+
return Number(this.nextLongBigInt());
|
|
2506
|
+
}
|
|
2507
|
+
nextLongBigInt() {
|
|
2508
|
+
const high = BigInt(this.nextInt());
|
|
2509
|
+
const low = BigInt(this.nextInt());
|
|
2510
|
+
return BigInt.asIntN(64, (high << 32n) + low);
|
|
2511
|
+
}
|
|
2512
|
+
nextReducerRuntimeSeed() {
|
|
2513
|
+
return Number(
|
|
2514
|
+
this.nextLongRange(JS_MIN_SAFE_INTEGER, JS_MAX_SAFE_INTEGER_EXCLUSIVE)
|
|
2515
|
+
);
|
|
2516
|
+
}
|
|
2517
|
+
nextLongRange(from, until) {
|
|
2518
|
+
if (until <= from) {
|
|
2519
|
+
throw new Error("until must be greater than from");
|
|
2520
|
+
}
|
|
2521
|
+
const bound = until - from;
|
|
2522
|
+
const mask = bound - 1n;
|
|
2523
|
+
if ((bound & mask) === 0n) {
|
|
2524
|
+
return from + (BigInt.asUintN(64, this.nextLongBigInt()) & mask);
|
|
2525
|
+
}
|
|
2526
|
+
while (true) {
|
|
2527
|
+
const bits = BigInt.asUintN(64, this.nextLongBigInt()) >> 1n;
|
|
2528
|
+
const value = bits % bound;
|
|
2529
|
+
if (BigInt.asIntN(64, bits + mask - value) >= 0n) {
|
|
2530
|
+
return from + value;
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
};
|
|
2535
|
+
function shuffleWithKotlinRandom(values, random) {
|
|
2536
|
+
const next = [...values];
|
|
2537
|
+
for (let index = next.length - 1; index > 0; index -= 1) {
|
|
2538
|
+
const swapIndex = random.nextIntBound(index + 1);
|
|
2539
|
+
const current = next[index];
|
|
2540
|
+
next[index] = next[swapIndex];
|
|
2541
|
+
next[swapIndex] = current;
|
|
2542
|
+
}
|
|
2543
|
+
return next;
|
|
2544
|
+
}
|
|
2545
|
+
function resolveEffectiveBaseSetup(options) {
|
|
2546
|
+
const inheritedSetupProfileId = options.base.setupProfileId ?? (options.base.extends && options.basesById ? resolveEffectiveBaseSetup({
|
|
2547
|
+
manifest: options.manifest,
|
|
2548
|
+
base: options.basesById.get(options.base.extends)?.definition ?? (() => {
|
|
2549
|
+
throw new Error(
|
|
2550
|
+
`Base '${options.base.id}' extends unknown parent '${options.base.extends}'.`
|
|
2551
|
+
);
|
|
2552
|
+
})(),
|
|
2553
|
+
basesById: options.basesById
|
|
2554
|
+
}).setupProfileId : void 0);
|
|
2555
|
+
const selection = resolveSetupProfileSelection({
|
|
2556
|
+
manifest: options.manifest,
|
|
2557
|
+
requestedSetupProfileId: inheritedSetupProfileId ?? void 0
|
|
2558
|
+
});
|
|
2559
|
+
return {
|
|
2560
|
+
setupProfileId: selection.id
|
|
2561
|
+
};
|
|
2562
|
+
}
|
|
2563
|
+
function resolveBaseDefinition(base, basesById) {
|
|
2564
|
+
const inherited = base.definition.extends !== void 0 ? resolveBaseDefinition(
|
|
2565
|
+
basesById.get(base.definition.extends) ?? (() => {
|
|
2566
|
+
throw new Error(
|
|
2567
|
+
`Base '${base.definition.id}' extends unknown parent '${base.definition.extends}'.`
|
|
2568
|
+
);
|
|
2569
|
+
})(),
|
|
2570
|
+
basesById
|
|
2571
|
+
) : null;
|
|
2572
|
+
const seed = base.definition.seed ?? inherited?.seed;
|
|
2573
|
+
const players = base.definition.players ?? inherited?.players;
|
|
2574
|
+
if (seed === void 0 || players === void 0) {
|
|
2575
|
+
throw new Error(
|
|
2576
|
+
`Base '${base.definition.id}' must resolve numeric seed and players.`
|
|
2577
|
+
);
|
|
2578
|
+
}
|
|
2579
|
+
return {
|
|
2580
|
+
...base.definition,
|
|
2581
|
+
seed,
|
|
2582
|
+
players,
|
|
2583
|
+
setupProfileId: base.definition.setupProfileId ?? inherited?.setupProfileId ?? void 0
|
|
2584
|
+
};
|
|
2585
|
+
}
|
|
2586
|
+
function summarizeTableValidationError(manifest, error) {
|
|
2587
|
+
const issues = error.message.split("; ").map((issue) => issue.trim()).filter((issue) => issue.length > 0);
|
|
2588
|
+
if (issues.length === 0 || !issues.every((issue) => issue.startsWith("table"))) {
|
|
2589
|
+
return error.message;
|
|
2590
|
+
}
|
|
2591
|
+
const boardBaseIdByRuntimeId = new Map(
|
|
2592
|
+
(manifest.boards ?? []).flatMap(
|
|
2593
|
+
(board) => board.scope === "perPlayer" ? Array.from(
|
|
2594
|
+
{ length: manifest.players.maxPlayers },
|
|
2595
|
+
(_, index) => [`${board.id}:player-${index + 1}`, board.id]
|
|
2596
|
+
) : [[board.id, board.id]]
|
|
2597
|
+
)
|
|
2598
|
+
);
|
|
2599
|
+
const representativeIssues = issues.slice(0, 3).map((issue) => {
|
|
2600
|
+
const [, runtimeBoardId = ""] = issue.match(/^table\.boards\.byId\.([^.]+)\./) ?? [];
|
|
2601
|
+
const boardBaseId = boardBaseIdByRuntimeId.get(runtimeBoardId);
|
|
2602
|
+
if (!boardBaseId) {
|
|
2603
|
+
return `- ${issue.replace(/^table\./, "")}`;
|
|
2604
|
+
}
|
|
2605
|
+
const boardSpecificPath = issue.replace(/^table\.boards\.byId\.[^.]+\./, "").replace(/^table\./, "");
|
|
2606
|
+
return `- board '${boardBaseId}' (${runtimeBoardId}): ${boardSpecificPath}`;
|
|
2607
|
+
});
|
|
2608
|
+
return [
|
|
2609
|
+
`Reducer-native table validation failed with ${issues.length} issue${issues.length === 1 ? "" : "s"}.`,
|
|
2610
|
+
"Likely root cause: the generated test table shape does not match the authored manifest topology.",
|
|
2611
|
+
...representativeIssues,
|
|
2612
|
+
"Pass --debug for the full validation dump."
|
|
2613
|
+
].join("\n");
|
|
2614
|
+
}
|
|
2615
|
+
var ShadowReducerRuntime = class {
|
|
2616
|
+
constructor(modules, manifest, gameModuleDefault, createInitialTable, seed, players, setupProfileId, debug = false) {
|
|
2617
|
+
this.modules = modules;
|
|
2618
|
+
this.manifest = manifest;
|
|
2619
|
+
this.gameModuleDefault = gameModuleDefault;
|
|
2620
|
+
this.createInitialTable = createInitialTable;
|
|
2621
|
+
this.seed = seed;
|
|
2622
|
+
this.players = players;
|
|
2623
|
+
this.setupProfileId = setupProfileId;
|
|
2624
|
+
this.debug = debug;
|
|
2625
|
+
if (typeof gameModuleDefault !== "object" || gameModuleDefault === null || !("contract" in gameModuleDefault)) {
|
|
2626
|
+
throw new Error(
|
|
2627
|
+
"app/game.ts must export a reducer-native project definition."
|
|
2628
|
+
);
|
|
2629
|
+
}
|
|
2630
|
+
this.bundle = this.modules.createReducerBundle(gameModuleDefault);
|
|
2631
|
+
this.runtime = this.bundle.createInProcessRuntime();
|
|
2632
|
+
this.playerIds = Array.from(
|
|
2633
|
+
{ length: players },
|
|
2634
|
+
(_, index) => `player-${index + 1}`
|
|
2635
|
+
);
|
|
2636
|
+
}
|
|
2637
|
+
bundle;
|
|
2638
|
+
runtime;
|
|
2639
|
+
playerIds;
|
|
2640
|
+
historyRecords = [];
|
|
2641
|
+
version = 0;
|
|
2642
|
+
started = false;
|
|
2643
|
+
async start() {
|
|
2644
|
+
if (this.started) {
|
|
2645
|
+
return;
|
|
2646
|
+
}
|
|
2647
|
+
const random = new KotlinSeededRandom(this.seed);
|
|
2648
|
+
const shuffleItems = (values) => shuffleWithKotlinRandom(values, random);
|
|
2649
|
+
const table = JSON.parse(
|
|
2650
|
+
JSON.stringify(
|
|
2651
|
+
this.createInitialTable?.({
|
|
2652
|
+
playerIds: this.playerIds,
|
|
2653
|
+
shuffleItems
|
|
2654
|
+
}) ?? this.modules.materializeManifestTable({
|
|
2655
|
+
manifest: this.manifest,
|
|
2656
|
+
playerIds: this.playerIds,
|
|
2657
|
+
shuffleItems
|
|
2658
|
+
})
|
|
2659
|
+
)
|
|
2660
|
+
);
|
|
2661
|
+
const rngSeed = random.nextReducerRuntimeSeed();
|
|
2662
|
+
try {
|
|
2663
|
+
await this.runtime.initialize({
|
|
2664
|
+
table,
|
|
2665
|
+
playerIds: this.playerIds,
|
|
2666
|
+
rngSeed,
|
|
2667
|
+
setup: this.setupProfileId ? {
|
|
2668
|
+
profileId: this.setupProfileId
|
|
2669
|
+
} : null
|
|
2670
|
+
});
|
|
2671
|
+
} catch (error) {
|
|
2672
|
+
if (!this.debug && error instanceof Error) {
|
|
2673
|
+
throw new Error(summarizeTableValidationError(this.manifest, error));
|
|
2674
|
+
}
|
|
2675
|
+
throw error;
|
|
2676
|
+
}
|
|
2677
|
+
this.version = 0;
|
|
2678
|
+
this.started = true;
|
|
2679
|
+
}
|
|
2680
|
+
phase() {
|
|
2681
|
+
return (this.runtime.unsafeState()?.domain?.flow?.currentPhase ?? null) || "";
|
|
2682
|
+
}
|
|
2683
|
+
playerOrder() {
|
|
2684
|
+
return this.playerIds;
|
|
2685
|
+
}
|
|
2686
|
+
rawState() {
|
|
2687
|
+
return this.runtime.snapshot();
|
|
2688
|
+
}
|
|
2689
|
+
currentVersion() {
|
|
2690
|
+
return this.version;
|
|
2691
|
+
}
|
|
2692
|
+
hydrate(snapshot, version = 0) {
|
|
2693
|
+
this.runtime.hydrate({ state: structuredClone(snapshot) });
|
|
2694
|
+
this.version = version;
|
|
2695
|
+
this.started = true;
|
|
2696
|
+
}
|
|
2697
|
+
patchState(mutator) {
|
|
2698
|
+
const next = structuredClone(this.rawState());
|
|
2699
|
+
mutator(next);
|
|
2700
|
+
this.hydrate(next, this.version + 1);
|
|
2701
|
+
}
|
|
2702
|
+
projectAllSeats() {
|
|
2703
|
+
const projection = this.runtime.projectSeatsDynamic({
|
|
2704
|
+
playerIds: this.playerIds
|
|
2705
|
+
});
|
|
2706
|
+
const interactionsByRef = projection.interactionsByRef && typeof projection.interactionsByRef === "object" && !Array.isArray(projection.interactionsByRef) ? projection.interactionsByRef : {};
|
|
2707
|
+
const seats = Object.fromEntries(
|
|
2708
|
+
Object.entries(projection.seats ?? {}).map(([playerId, seat]) => {
|
|
2709
|
+
const availableInteractionRefs = Array.isArray(
|
|
2710
|
+
seat.availableInteractionRefs
|
|
2711
|
+
) ? seat.availableInteractionRefs.filter(
|
|
2712
|
+
(ref) => typeof ref === "string"
|
|
2713
|
+
) : [];
|
|
2714
|
+
const availableInteractions = seat.availableInteractions ?? availableInteractionRefs.map((ref) => interactionsByRef[ref]).filter(
|
|
2715
|
+
(descriptor) => !!descriptor && typeof descriptor === "object"
|
|
2716
|
+
);
|
|
2717
|
+
return [
|
|
2718
|
+
playerId,
|
|
2719
|
+
{
|
|
2720
|
+
view: seat.view,
|
|
2721
|
+
availableInteractions,
|
|
2722
|
+
zones: seat.zones
|
|
2723
|
+
}
|
|
2724
|
+
];
|
|
2725
|
+
})
|
|
2726
|
+
);
|
|
2727
|
+
return {
|
|
2728
|
+
currentStage: projection.currentStage ?? null,
|
|
2729
|
+
stageSeats: projection.stageSeats ?? [],
|
|
2730
|
+
seats
|
|
2731
|
+
};
|
|
2732
|
+
}
|
|
2733
|
+
currentStage() {
|
|
2734
|
+
return this.projectAllSeats().currentStage;
|
|
2735
|
+
}
|
|
2736
|
+
view(playerId) {
|
|
2737
|
+
return this.runtime.projectSeatViewDynamic({
|
|
2738
|
+
playerId
|
|
2739
|
+
}) ?? null;
|
|
2740
|
+
}
|
|
2741
|
+
availableInteractionsForPlayer(playerId) {
|
|
2742
|
+
const projection = this.projectAllSeats();
|
|
2743
|
+
const seat = projection.seats[playerId];
|
|
2744
|
+
const descriptors = seat?.availableInteractions ?? [];
|
|
2745
|
+
return descriptors.filter(
|
|
2746
|
+
(descriptor) => !!descriptor && typeof descriptor.interactionId === "string"
|
|
2747
|
+
).map((descriptor) => descriptor.interactionId);
|
|
2748
|
+
}
|
|
2749
|
+
/**
|
|
2750
|
+
* Phase-kind interaction descriptors (action + prompt) available to
|
|
2751
|
+
* `playerId` in the current phase. Surfaces the same projection the UI
|
|
2752
|
+
* SDK consumes, so scenarios can assert addressee authorization and
|
|
2753
|
+
* availability without reaching into game-specific state. Returned as
|
|
2754
|
+
* loose records — the typed `InteractionDescriptor` shape is narrowed
|
|
2755
|
+
* downstream in the workspace-generated `testing-contract.ts`.
|
|
2756
|
+
*/
|
|
2757
|
+
interactionsForPlayer(playerId) {
|
|
2758
|
+
return this.projectAllSeats().seats[playerId]?.availableInteractions ?? [];
|
|
2759
|
+
}
|
|
2760
|
+
explain(playerId, interactionId) {
|
|
2761
|
+
const runtime = this.runtime;
|
|
2762
|
+
if (typeof runtime.explainInteraction !== "function") {
|
|
2763
|
+
throw new Error(
|
|
2764
|
+
"The installed @dreamboard-games/sdk does not expose interaction explanations. Update the workspace SDK pin."
|
|
2765
|
+
);
|
|
2766
|
+
}
|
|
2767
|
+
return runtime.explainInteraction({ playerId, interactionId });
|
|
2768
|
+
}
|
|
2769
|
+
clearHistory() {
|
|
2770
|
+
this.historyRecords.length = 0;
|
|
2771
|
+
}
|
|
2772
|
+
async applyInput(input) {
|
|
2773
|
+
const parsedInput = toReducerInput(input);
|
|
2774
|
+
const result = await this.runtime.dispatch({ input: parsedInput });
|
|
2775
|
+
if (result.kind === "reject") {
|
|
2776
|
+
const record = {
|
|
2777
|
+
input,
|
|
2778
|
+
accepted: false,
|
|
2779
|
+
errorCode: result.errorCode,
|
|
2780
|
+
message: result.message
|
|
2781
|
+
};
|
|
2782
|
+
this.historyRecords.push(record);
|
|
2783
|
+
throw createSubmissionError(
|
|
2784
|
+
result.errorCode,
|
|
2785
|
+
result.message,
|
|
2786
|
+
"Reducer input rejected."
|
|
2787
|
+
);
|
|
2788
|
+
}
|
|
2789
|
+
this.version += 1;
|
|
2790
|
+
this.historyRecords.push({ input, accepted: true });
|
|
2791
|
+
}
|
|
2792
|
+
async submitInteraction(playerId, interactionId, params) {
|
|
2793
|
+
await this.applyInput({
|
|
2794
|
+
kind: "interaction",
|
|
2795
|
+
playerId,
|
|
2796
|
+
interactionId,
|
|
2797
|
+
params
|
|
2798
|
+
});
|
|
2799
|
+
}
|
|
2800
|
+
};
|
|
2801
|
+
function toReducerInput(input) {
|
|
2802
|
+
return {
|
|
2803
|
+
kind: "interaction",
|
|
2804
|
+
playerId: input.playerId,
|
|
2805
|
+
interactionId: input.interactionId,
|
|
2806
|
+
params: input.params
|
|
2807
|
+
};
|
|
2808
|
+
}
|
|
2809
|
+
function sanitizeSnapshotSegment(value) {
|
|
2810
|
+
return value.replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
2811
|
+
}
|
|
2812
|
+
function createScenarioSnapshotMatcher(options) {
|
|
2813
|
+
return (filename, actual) => {
|
|
2814
|
+
const suffix = filename ? `.${sanitizeSnapshotSegment(filename)}` : "";
|
|
2815
|
+
const snapshotPath = resolveWorkspacePath(
|
|
2816
|
+
options.projectRoot,
|
|
2817
|
+
`test/generated/snapshots/${sanitizeSnapshotSegment(options.scenarioId)}${suffix}.snapshot.json`
|
|
2818
|
+
);
|
|
2819
|
+
const wrappedValue = {
|
|
2820
|
+
value: actual
|
|
2821
|
+
};
|
|
2822
|
+
const serialized = `${JSON.stringify(wrappedValue, null, 2)}
|
|
2823
|
+
`;
|
|
2824
|
+
mkdirSync(path3.dirname(snapshotPath), { recursive: true });
|
|
2825
|
+
if (!existsSync(snapshotPath) || options.updateSnapshots) {
|
|
2826
|
+
writeFileSync(snapshotPath, serialized, "utf8");
|
|
2827
|
+
return;
|
|
2828
|
+
}
|
|
2829
|
+
const previous = JSON.parse(readFileSync(snapshotPath, "utf8"));
|
|
2830
|
+
if (!deepEqual(previous.value, actual)) {
|
|
2831
|
+
throw new Error(
|
|
2832
|
+
`Snapshot mismatch for scenario '${options.scenarioId}'. Re-run with --update-snapshots to refresh ${path3.relative(options.projectRoot, snapshotPath)}.`
|
|
2833
|
+
);
|
|
2834
|
+
}
|
|
2835
|
+
};
|
|
2836
|
+
}
|
|
2837
|
+
async function createScenarioContext(options) {
|
|
2838
|
+
const api = {
|
|
2839
|
+
start: async () => {
|
|
2840
|
+
await options.shadow.start();
|
|
2841
|
+
},
|
|
2842
|
+
patchState: async (mutator) => {
|
|
2843
|
+
await api.start();
|
|
2844
|
+
if (options.live || options.actionPlan) {
|
|
2845
|
+
throw new Error(
|
|
2846
|
+
"game.patchState is only supported for reducer snapshot scenarios. Use it to materialize a state before --from-scenario or reducer tests, not inside live replay."
|
|
2847
|
+
);
|
|
2848
|
+
}
|
|
2849
|
+
options.shadow.patchState(mutator);
|
|
2850
|
+
},
|
|
2851
|
+
submit: async (playerId, interactionId, params) => {
|
|
2852
|
+
await api.start();
|
|
2853
|
+
const interactionParams = params ?? {};
|
|
2854
|
+
const normalizedInputs = interactionParams && typeof interactionParams === "object" && !Array.isArray(interactionParams) ? interactionParams : {};
|
|
2855
|
+
const previousVersion = options.live ? options.live.version : options.actionPlan ? options.shadow.currentVersion() : 0;
|
|
2856
|
+
if (options.live) {
|
|
2857
|
+
if (!options.live.actionSetVersion) {
|
|
2858
|
+
options.live.actionSetVersion = await fetchLiveActionSetVersion(
|
|
2859
|
+
options.live.sessionId,
|
|
2860
|
+
playerId
|
|
2861
|
+
);
|
|
2862
|
+
}
|
|
2863
|
+
const submittedActionSetVersion = options.live.actionSetVersion;
|
|
2864
|
+
const submitStartedAt = performance.now();
|
|
2865
|
+
const clientActionId = randomUUID3();
|
|
2866
|
+
const { data, error, response } = await submitGameplayAuthorityAction({
|
|
2867
|
+
path: {
|
|
2868
|
+
sessionId: options.live.sessionId,
|
|
2869
|
+
playerId,
|
|
2870
|
+
interactionId
|
|
2871
|
+
},
|
|
2872
|
+
body: {
|
|
2873
|
+
expectedVersion: previousVersion,
|
|
2874
|
+
actionSetVersion: options.live.actionSetVersion,
|
|
2875
|
+
inputs: normalizedInputs
|
|
2876
|
+
},
|
|
2877
|
+
headers: { "X-Dreamboard-Client-Action-Id": clientActionId }
|
|
2878
|
+
});
|
|
2879
|
+
const durationMs = performance.now() - submitStartedAt;
|
|
2880
|
+
options.live.diagnostics?.push({
|
|
2881
|
+
index: options.live.submitIndex ?? 0,
|
|
2882
|
+
playerId,
|
|
2883
|
+
interactionId,
|
|
2884
|
+
inputs: normalizedInputs,
|
|
2885
|
+
expectedVersion: previousVersion,
|
|
2886
|
+
actionSetVersion: submittedActionSetVersion,
|
|
2887
|
+
responseStatus: response?.status,
|
|
2888
|
+
responseBody: options.live.captureResponseBody === false ? void 0 : JSON.stringify(error ?? data ?? null),
|
|
2889
|
+
accepted: data?.accepted ?? void 0,
|
|
2890
|
+
errorCode: data?.accepted === false ? data.errorCode ?? void 0 : error && typeof error === "object" && "detail" in error ? String(error.detail) : void 0,
|
|
2891
|
+
backendTiming: response?.headers?.get("server-timing") ?? response?.headers?.get("x-dreamboard-timing") ?? void 0,
|
|
2892
|
+
durationMs
|
|
2893
|
+
});
|
|
2894
|
+
options.live.submitIndex = (options.live.submitIndex ?? 0) + 1;
|
|
2895
|
+
if (error) {
|
|
2896
|
+
throw createSubmissionError(
|
|
2897
|
+
"api-error",
|
|
2898
|
+
void 0,
|
|
2899
|
+
"Failed to submit interaction"
|
|
2900
|
+
);
|
|
2901
|
+
}
|
|
2902
|
+
if (data?.accepted === false) {
|
|
2903
|
+
const shadowError = await tryShadowSubmit(
|
|
2904
|
+
options.shadow,
|
|
2905
|
+
playerId,
|
|
2906
|
+
interactionId,
|
|
2907
|
+
interactionParams
|
|
2908
|
+
);
|
|
2909
|
+
if (!shadowError || shadowError.errorCode !== data.errorCode) {
|
|
2910
|
+
throw new Error(
|
|
2911
|
+
"Live session rejection diverged from reducer shadow state."
|
|
2912
|
+
);
|
|
2913
|
+
}
|
|
2914
|
+
throw createSubmissionError(
|
|
2915
|
+
data.errorCode ?? void 0,
|
|
2916
|
+
data.message ?? void 0,
|
|
2917
|
+
"Interaction rejected"
|
|
2918
|
+
);
|
|
2919
|
+
}
|
|
2920
|
+
options.live.version = typeof data?.version === "number" ? data.version : previousVersion + 1;
|
|
2921
|
+
options.live.actionSetVersion = data?.actionSetVersion ?? options.live.actionSetVersion;
|
|
2922
|
+
}
|
|
2923
|
+
await options.shadow.submitInteraction(
|
|
2924
|
+
playerId,
|
|
2925
|
+
interactionId,
|
|
2926
|
+
interactionParams
|
|
2927
|
+
);
|
|
2928
|
+
if (options.actionPlan) {
|
|
2929
|
+
options.actionPlan.diagnostics.push({
|
|
2930
|
+
index: options.actionPlan.submitIndex ?? 0,
|
|
2931
|
+
playerId,
|
|
2932
|
+
interactionId,
|
|
2933
|
+
inputs: normalizedInputs,
|
|
2934
|
+
expectedVersion: previousVersion,
|
|
2935
|
+
actionSetVersion: "",
|
|
2936
|
+
accepted: true,
|
|
2937
|
+
durationMs: 0
|
|
2938
|
+
});
|
|
2939
|
+
options.actionPlan.submitIndex = (options.actionPlan.submitIndex ?? 0) + 1;
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
};
|
|
2943
|
+
return {
|
|
2944
|
+
game: api,
|
|
2945
|
+
players: () => options.shadow.playerOrder(),
|
|
2946
|
+
seat: (index) => {
|
|
2947
|
+
const order = options.shadow.playerOrder();
|
|
2948
|
+
if (!Number.isInteger(index) || index < 0 || index >= order.length) {
|
|
2949
|
+
throw new Error(
|
|
2950
|
+
`seat(${index}) is out of range; scenario has ${order.length} player(s).`
|
|
2951
|
+
);
|
|
2952
|
+
}
|
|
2953
|
+
return order[index];
|
|
2954
|
+
},
|
|
2955
|
+
state: () => options.shadow.phase(),
|
|
2956
|
+
view: (playerId) => options.shadow.view(playerId),
|
|
2957
|
+
interactions: (playerId) => options.shadow.interactionsForPlayer(playerId),
|
|
2958
|
+
explain: (playerId, interactionId) => options.shadow.explain(playerId, interactionId),
|
|
2959
|
+
expect: options.expect
|
|
2960
|
+
};
|
|
2961
|
+
}
|
|
2962
|
+
async function tryShadowSubmit(shadow, playerId, interactionId, params) {
|
|
2963
|
+
try {
|
|
2964
|
+
await shadow.submitInteraction(playerId, interactionId, params);
|
|
2965
|
+
throw new Error("Expected shadow interaction to reject.");
|
|
2966
|
+
} catch (error) {
|
|
2967
|
+
if (error instanceof Error && error.name === "SubmissionError") {
|
|
2968
|
+
return error;
|
|
2969
|
+
}
|
|
2970
|
+
throw error;
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
async function createSessionFromScenario(options) {
|
|
2974
|
+
const startedAt = Date.now();
|
|
2975
|
+
const materialized = await materializeScenarioReducerState(options);
|
|
2976
|
+
const reducerHarnessMs = Date.now() - startedAt;
|
|
2977
|
+
const hydrateStartedAt = Date.now();
|
|
2978
|
+
const {
|
|
2979
|
+
data: snapshot,
|
|
2980
|
+
error,
|
|
2981
|
+
response
|
|
2982
|
+
} = await createProjectSessionFromReducerSnapshot({
|
|
2983
|
+
path: { projectId: options.projectId },
|
|
2984
|
+
body: {
|
|
2985
|
+
compiledResultId: options.compiledResultId,
|
|
2986
|
+
seed: materialized.seed,
|
|
2987
|
+
playerCount: materialized.playerCount,
|
|
2988
|
+
setupProfileId: materialized.setupProfileId ?? void 0,
|
|
2989
|
+
baseId: materialized.baseId,
|
|
2990
|
+
scenarioId: materialized.scenarioId,
|
|
2991
|
+
reducerState: materialized.reducerState,
|
|
2992
|
+
reducerStateVersion: materialized.reducerStateVersion,
|
|
2993
|
+
fingerprintMetadata: materialized.fingerprintMetadata
|
|
2994
|
+
}
|
|
2995
|
+
});
|
|
2996
|
+
const backendHydrateMs = Date.now() - hydrateStartedAt;
|
|
2997
|
+
if (error || !snapshot) {
|
|
2998
|
+
throw toDreamboardApiError(
|
|
2999
|
+
error,
|
|
3000
|
+
response,
|
|
3001
|
+
`Failed to materialize scenario '${options.scenarioId}' from reducer snapshot`
|
|
3002
|
+
);
|
|
3003
|
+
}
|
|
3004
|
+
return {
|
|
3005
|
+
sessionId: snapshot.context.sessionId,
|
|
3006
|
+
shortCode: snapshot.context.shortCode,
|
|
3007
|
+
projectId: projectIdFromSessionGameSource(snapshot.context.gameSource),
|
|
3008
|
+
seed: materialized.seed,
|
|
3009
|
+
playerCount: materialized.playerCount,
|
|
3010
|
+
setupProfileId: materialized.setupProfileId,
|
|
3011
|
+
scenarioId: materialized.scenarioId,
|
|
3012
|
+
phase: materialized.phase,
|
|
3013
|
+
stage: materialized.stage,
|
|
3014
|
+
materialization: {
|
|
3015
|
+
mode: "snapshot",
|
|
3016
|
+
totalMs: Date.now() - startedAt,
|
|
3017
|
+
reducerHarnessMs,
|
|
3018
|
+
backendHydrateMs
|
|
3019
|
+
}
|
|
3020
|
+
};
|
|
3021
|
+
}
|
|
3022
|
+
var MATERIALIZED_SCENARIO_CACHE_VERSION = 1;
|
|
3023
|
+
function materializedScenarioCachePath(options) {
|
|
3024
|
+
return [
|
|
3025
|
+
PROJECT_DIR_NAME,
|
|
3026
|
+
"dev",
|
|
3027
|
+
"scenario-cache",
|
|
3028
|
+
`${sanitizeSnapshotSegment(options.baseId)}.${sanitizeSnapshotSegment(
|
|
3029
|
+
options.scenarioId
|
|
3030
|
+
)}.${options.fingerprintHash}.json`
|
|
3031
|
+
].join("/");
|
|
3032
|
+
}
|
|
3033
|
+
function materializedScenarioCacheFilePath(options) {
|
|
3034
|
+
return resolveWorkspacePath(
|
|
3035
|
+
options.projectRoot,
|
|
3036
|
+
materializedScenarioCachePath(options)
|
|
3037
|
+
);
|
|
3038
|
+
}
|
|
3039
|
+
async function readMaterializedScenarioCache(options) {
|
|
3040
|
+
const text = await readWorkspaceTextFileIfExists(
|
|
3041
|
+
options.projectRoot,
|
|
3042
|
+
materializedScenarioCachePath(options)
|
|
3043
|
+
);
|
|
3044
|
+
if (!text) return null;
|
|
3045
|
+
try {
|
|
3046
|
+
const payload = JSON.parse(
|
|
3047
|
+
text
|
|
3048
|
+
);
|
|
3049
|
+
if (payload.cacheVersion !== MATERIALIZED_SCENARIO_CACHE_VERSION || payload.fingerprintHash !== options.fingerprintHash || !payload.materialized) {
|
|
3050
|
+
return null;
|
|
3051
|
+
}
|
|
3052
|
+
return payload.materialized;
|
|
3053
|
+
} catch {
|
|
3054
|
+
return null;
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
async function writeMaterializedScenarioCache(options) {
|
|
3058
|
+
const cachePath = materializedScenarioCacheFilePath(options);
|
|
3059
|
+
await ensureDir(path3.dirname(cachePath));
|
|
3060
|
+
await writeWorkspaceJsonFile(
|
|
3061
|
+
options.projectRoot,
|
|
3062
|
+
materializedScenarioCachePath(options),
|
|
3063
|
+
{
|
|
3064
|
+
cacheVersion: MATERIALIZED_SCENARIO_CACHE_VERSION,
|
|
3065
|
+
fingerprintHash: options.fingerprintHash,
|
|
3066
|
+
materialized: options.materialized
|
|
3067
|
+
}
|
|
3068
|
+
);
|
|
3069
|
+
}
|
|
3070
|
+
async function materializeScenarioReducerState(options) {
|
|
3071
|
+
await ensureReducerNativeTestingFiles(options.projectRoot);
|
|
3072
|
+
let preloadedGeneratedBaseStates;
|
|
3073
|
+
if (options.trustGeneratedFingerprint === true) {
|
|
3074
|
+
const [generatedBaseStates2, scenarioManifest] = await Promise.all([
|
|
3075
|
+
loadGeneratedBaseStates(options.projectRoot),
|
|
3076
|
+
loadGeneratedScenarioManifest(options.projectRoot)
|
|
3077
|
+
]);
|
|
3078
|
+
preloadedGeneratedBaseStates = generatedBaseStates2;
|
|
3079
|
+
const scenarioEntry = scenarioManifest.find(
|
|
3080
|
+
(entry) => entry.id === options.scenarioId
|
|
3081
|
+
);
|
|
3082
|
+
const generatedBase2 = scenarioEntry ? generatedBaseStates2?.[baseStateKey(scenarioEntry.base)] : null;
|
|
3083
|
+
if (generatedBase2 && generatedBase2.fingerprint.compiledResultId === options.compiledResultId && generatedBase2.fingerprint.projectId === options.projectId) {
|
|
3084
|
+
const cached2 = await readMaterializedScenarioCache({
|
|
3085
|
+
projectRoot: options.projectRoot,
|
|
3086
|
+
baseId: scenarioEntry.base,
|
|
3087
|
+
scenarioId: options.scenarioId,
|
|
3088
|
+
fingerprintHash: fingerprintHash(generatedBase2.fingerprint)
|
|
3089
|
+
});
|
|
3090
|
+
if (cached2) {
|
|
3091
|
+
return cached2;
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
const [bases, scenarios, generatedBaseStates, manifest] = await Promise.all([
|
|
3096
|
+
loadTypedBases(options.projectRoot),
|
|
3097
|
+
loadTypedScenarios(options.projectRoot, {}),
|
|
3098
|
+
preloadedGeneratedBaseStates === void 0 ? loadGeneratedBaseStates(options.projectRoot) : Promise.resolve(preloadedGeneratedBaseStates),
|
|
3099
|
+
loadManifest(options.projectRoot)
|
|
3100
|
+
]);
|
|
3101
|
+
const matchingScenarios = scenarios.filter(
|
|
3102
|
+
(scenario2) => scenario2.definition.id === options.scenarioId
|
|
3103
|
+
);
|
|
3104
|
+
if (matchingScenarios.length === 0) {
|
|
3105
|
+
throw new Error(
|
|
3106
|
+
`Unknown scenario '${options.scenarioId}'. Available scenarios: ${scenarios.map((scenario2) => scenario2.definition.id).sort().join(", ")}`
|
|
3107
|
+
);
|
|
3108
|
+
}
|
|
3109
|
+
if (matchingScenarios.length > 1) {
|
|
3110
|
+
throw new Error(
|
|
3111
|
+
`Scenario id '${options.scenarioId}' is defined more than once. Keep one scenario per file and one unique id per workspace.`
|
|
3112
|
+
);
|
|
3113
|
+
}
|
|
3114
|
+
const scenario = matchingScenarios[0];
|
|
3115
|
+
const basesById = new Map(bases.map((base2) => [base2.definition.id, base2]));
|
|
3116
|
+
validateTypedScenarioBases([scenario], basesById);
|
|
3117
|
+
const base = basesById.get(scenario.definition.from);
|
|
3118
|
+
if (!base) {
|
|
3119
|
+
throw new Error(
|
|
3120
|
+
`Missing typed base '${scenario.definition.from}' for scenario '${scenario.definition.id}'.`
|
|
3121
|
+
);
|
|
3122
|
+
}
|
|
3123
|
+
const generatedBase = generatedBaseStates?.[baseStateKey(base.definition.id)];
|
|
3124
|
+
if (!generatedBase) {
|
|
3125
|
+
throw new Error(
|
|
3126
|
+
`Missing generated base artifact for '${base.definition.id}'. Run 'dreamboard test' before using --from-scenario.`
|
|
3127
|
+
);
|
|
3128
|
+
}
|
|
3129
|
+
if (typeof generatedBase.version !== "number") {
|
|
3130
|
+
throw new Error(
|
|
3131
|
+
`Generated base artifact for '${base.definition.id}' is stale. Run 'dreamboard test' before using --from-scenario.`
|
|
3132
|
+
);
|
|
3133
|
+
}
|
|
3134
|
+
const canTrustGeneratedFingerprint = options.trustGeneratedFingerprint === true && generatedBase.fingerprint.compiledResultId === options.compiledResultId && generatedBase.fingerprint.projectId === options.projectId;
|
|
3135
|
+
const current = canTrustGeneratedFingerprint ? generatedBase.fingerprint : await currentFingerprint({
|
|
3136
|
+
projectRoot: options.projectRoot,
|
|
3137
|
+
base,
|
|
3138
|
+
basesById,
|
|
3139
|
+
compiledResultId: options.compiledResultId,
|
|
3140
|
+
projectId: options.projectId
|
|
3141
|
+
});
|
|
3142
|
+
if (!canTrustGeneratedFingerprint) {
|
|
3143
|
+
validateGeneratedFingerprint({
|
|
3144
|
+
generated: generatedBase.fingerprint,
|
|
3145
|
+
current
|
|
3146
|
+
});
|
|
3147
|
+
}
|
|
3148
|
+
const currentHash = fingerprintHash(current);
|
|
3149
|
+
const cached = await readMaterializedScenarioCache({
|
|
3150
|
+
projectRoot: options.projectRoot,
|
|
3151
|
+
baseId: base.definition.id,
|
|
3152
|
+
scenarioId: scenario.definition.id,
|
|
3153
|
+
fingerprintHash: currentHash
|
|
3154
|
+
});
|
|
3155
|
+
if (cached) {
|
|
3156
|
+
return cached;
|
|
3157
|
+
}
|
|
3158
|
+
const [gameModule, manifestContractModule, modules] = await Promise.all([
|
|
3159
|
+
importTypeScriptModule(
|
|
3160
|
+
path3.join(options.projectRoot, "app", "game.ts")
|
|
3161
|
+
),
|
|
3162
|
+
importTypeScriptModule(
|
|
3163
|
+
path3.join(options.projectRoot, "shared", "manifest-contract.ts")
|
|
3164
|
+
),
|
|
3165
|
+
loadProjectReducerNativeModules(options.projectRoot)
|
|
3166
|
+
]);
|
|
3167
|
+
const createInitialTable = typeof manifestContractModule.createInitialTable === "function" ? manifestContractModule.createInitialTable : null;
|
|
3168
|
+
const resolvedBase = resolveBaseDefinition(base, basesById);
|
|
3169
|
+
const effectiveSetup = resolveEffectiveBaseSetup({
|
|
3170
|
+
manifest,
|
|
3171
|
+
base: base.definition,
|
|
3172
|
+
basesById
|
|
3173
|
+
});
|
|
3174
|
+
const shadow = new ShadowReducerRuntime(
|
|
3175
|
+
modules,
|
|
3176
|
+
manifest,
|
|
3177
|
+
gameModule.default,
|
|
3178
|
+
createInitialTable,
|
|
3179
|
+
resolvedBase.seed,
|
|
3180
|
+
resolvedBase.players,
|
|
3181
|
+
effectiveSetup.setupProfileId,
|
|
3182
|
+
options.debug ?? false
|
|
3183
|
+
);
|
|
3184
|
+
shadow.hydrate(generatedBase.snapshot, generatedBase.version);
|
|
3185
|
+
const context = await createScenarioContext({
|
|
3186
|
+
shadow,
|
|
3187
|
+
expect: modules.createExpectApi()
|
|
3188
|
+
});
|
|
3189
|
+
shadow.clearHistory();
|
|
3190
|
+
await scenario.definition.when(context);
|
|
3191
|
+
if (scenario.definition.phase && shadow.phase() !== scenario.definition.phase) {
|
|
3192
|
+
throw new Error(
|
|
3193
|
+
`Scenario '${scenario.definition.id}' expected phase '${scenario.definition.phase}' but reached '${shadow.phase()}'.`
|
|
3194
|
+
);
|
|
3195
|
+
}
|
|
3196
|
+
if (scenario.definition.stage && shadow.currentStage() !== scenario.definition.stage) {
|
|
3197
|
+
throw new Error(
|
|
3198
|
+
`Scenario '${scenario.definition.id}' expected stage '${scenario.definition.stage}' but reached '${shadow.currentStage() ?? "null"}'.`
|
|
3199
|
+
);
|
|
3200
|
+
}
|
|
3201
|
+
const materialized = {
|
|
3202
|
+
baseId: base.definition.id,
|
|
3203
|
+
scenarioId: scenario.definition.id,
|
|
3204
|
+
seed: resolvedBase.seed,
|
|
3205
|
+
playerCount: resolvedBase.players,
|
|
3206
|
+
setupProfileId: effectiveSetup.setupProfileId,
|
|
3207
|
+
reducerState: shadow.rawState(),
|
|
3208
|
+
reducerStateVersion: shadow.currentVersion(),
|
|
3209
|
+
fingerprintMetadata: {
|
|
3210
|
+
baseFingerprintHash: fingerprintHash(generatedBase.fingerprint),
|
|
3211
|
+
currentFingerprintHash: fingerprintHash(current),
|
|
3212
|
+
baseBundleHash: generatedBase.fingerprint.baseBundleHash,
|
|
3213
|
+
manifestHash: generatedBase.fingerprint.manifestHash,
|
|
3214
|
+
appBundleHash: generatedBase.fingerprint.appBundleHash,
|
|
3215
|
+
uiContractHash: generatedBase.fingerprint.uiContractHash,
|
|
3216
|
+
baseId: base.definition.id,
|
|
3217
|
+
scenarioId: scenario.definition.id
|
|
3218
|
+
},
|
|
3219
|
+
phase: shadow.phase(),
|
|
3220
|
+
stage: shadow.currentStage()
|
|
3221
|
+
};
|
|
3222
|
+
await writeMaterializedScenarioCache({
|
|
3223
|
+
projectRoot: options.projectRoot,
|
|
3224
|
+
baseId: base.definition.id,
|
|
3225
|
+
scenarioId: scenario.definition.id,
|
|
3226
|
+
fingerprintHash: currentHash,
|
|
3227
|
+
materialized
|
|
3228
|
+
});
|
|
3229
|
+
return materialized;
|
|
3230
|
+
}
|
|
3231
|
+
async function fetchLiveActionSetVersion(sessionId, playerId) {
|
|
3232
|
+
const { data, error, response } = await getSessionSnapshot({
|
|
3233
|
+
path: { sessionId },
|
|
3234
|
+
query: { playerId }
|
|
3235
|
+
});
|
|
3236
|
+
if (error || !data || data.type !== "gameplay") {
|
|
3237
|
+
throw toDreamboardApiError(
|
|
3238
|
+
error ?? { detail: "Gameplay snapshot was unavailable." },
|
|
3239
|
+
response,
|
|
3240
|
+
"Failed to read live gameplay action set version"
|
|
3241
|
+
);
|
|
3242
|
+
}
|
|
3243
|
+
return data.gameplay.actionSetVersion;
|
|
3244
|
+
}
|
|
3245
|
+
function baseStateKey(baseId) {
|
|
3246
|
+
return baseId;
|
|
3247
|
+
}
|
|
3248
|
+
function fingerprintHash(fingerprint) {
|
|
3249
|
+
return hashContent(JSON.stringify(fingerprint));
|
|
3250
|
+
}
|
|
3251
|
+
function sortBasesForArtifacts(bases, basesById) {
|
|
3252
|
+
const ordered = [];
|
|
3253
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
3254
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3255
|
+
const visit = (base) => {
|
|
3256
|
+
if (visited.has(base.definition.id)) {
|
|
3257
|
+
return;
|
|
3258
|
+
}
|
|
3259
|
+
if (visiting.has(base.definition.id)) {
|
|
3260
|
+
throw new Error(
|
|
3261
|
+
`Cyclic reducer-native base inheritance detected at '${base.definition.id}'.`
|
|
3262
|
+
);
|
|
3263
|
+
}
|
|
3264
|
+
visiting.add(base.definition.id);
|
|
3265
|
+
if (base.definition.extends) {
|
|
3266
|
+
const parent = basesById.get(base.definition.extends);
|
|
3267
|
+
if (!parent) {
|
|
3268
|
+
throw new Error(
|
|
3269
|
+
`Base '${base.definition.id}' extends unknown parent '${base.definition.extends}'.`
|
|
3270
|
+
);
|
|
3271
|
+
}
|
|
3272
|
+
visit(parent);
|
|
3273
|
+
}
|
|
3274
|
+
visiting.delete(base.definition.id);
|
|
3275
|
+
visited.add(base.definition.id);
|
|
3276
|
+
ordered.push(base);
|
|
3277
|
+
};
|
|
3278
|
+
for (const base of bases) {
|
|
3279
|
+
visit(base);
|
|
3280
|
+
}
|
|
3281
|
+
return ordered;
|
|
3282
|
+
}
|
|
3283
|
+
function writeProjectionSnapshots(options) {
|
|
3284
|
+
const projectionDir = resolveWorkspacePath(
|
|
3285
|
+
options.projectRoot,
|
|
3286
|
+
`test/generated/bases/${sanitizeSnapshotSegment(options.baseId)}`
|
|
3287
|
+
);
|
|
3288
|
+
rmSync(projectionDir, { recursive: true, force: true });
|
|
3289
|
+
mkdirSync(projectionDir, { recursive: true });
|
|
3290
|
+
const projection = options.shadow.projectAllSeats();
|
|
3291
|
+
for (const [playerId, seatProjection] of Object.entries(projection.seats)) {
|
|
3292
|
+
const outputPath = resolveWorkspacePath(
|
|
3293
|
+
options.projectRoot,
|
|
3294
|
+
`test/generated/bases/${sanitizeSnapshotSegment(options.baseId)}/${sanitizeSnapshotSegment(playerId)}.projection.json`
|
|
3295
|
+
);
|
|
3296
|
+
writeFileSync(
|
|
3297
|
+
outputPath,
|
|
3298
|
+
`${JSON.stringify(
|
|
3299
|
+
{
|
|
3300
|
+
currentStage: projection.currentStage,
|
|
3301
|
+
stageSeats: projection.stageSeats,
|
|
3302
|
+
...seatProjection
|
|
3303
|
+
},
|
|
3304
|
+
null,
|
|
3305
|
+
2
|
|
3306
|
+
)}
|
|
3307
|
+
`,
|
|
3308
|
+
"utf8"
|
|
3309
|
+
);
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
function validateTypedScenarioBases(scenarios, basesById) {
|
|
3313
|
+
const available = new Set(basesById.keys());
|
|
3314
|
+
const invalid = scenarios.filter((scenario) => !available.has(scenario.definition.from)).map(
|
|
3315
|
+
(scenario) => path3.relative(process.cwd(), scenario.filePath) + ` -> '${scenario.definition.from}'`
|
|
3316
|
+
);
|
|
3317
|
+
if (invalid.length > 0) {
|
|
3318
|
+
throw new Error(
|
|
3319
|
+
`Unknown scenario base(s): ${invalid.join(", ")}. Available bases: ${Array.from(available).sort().join(", ")}`
|
|
3320
|
+
);
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
async function writeReducerNativeGeneratedFiles(options) {
|
|
3324
|
+
await ensureReducerNativeTestingFiles(options.projectRoot);
|
|
3325
|
+
const manifest = await loadManifest(options.projectRoot);
|
|
3326
|
+
const manifestHash = hashContent(JSON.stringify(manifest));
|
|
3327
|
+
const appBundleHash = hashContent(
|
|
3328
|
+
await bundleTypeScriptModuleText(
|
|
3329
|
+
path3.join(options.projectRoot, "app", "game.ts")
|
|
3330
|
+
)
|
|
3331
|
+
);
|
|
3332
|
+
const uiContractHash = hashContent(
|
|
3333
|
+
await bundleTypeScriptModuleText(
|
|
3334
|
+
path3.join(options.projectRoot, "shared", "generated", "ui-contract.ts"),
|
|
3335
|
+
{ external: SDK_UI_RUNTIME_EXTERNALS }
|
|
3336
|
+
)
|
|
3337
|
+
);
|
|
3338
|
+
const generatedDir = resolveWorkspacePath(
|
|
3339
|
+
options.projectRoot,
|
|
3340
|
+
"test/generated"
|
|
3341
|
+
);
|
|
3342
|
+
await ensureDir(generatedDir);
|
|
3343
|
+
const baseStates = {};
|
|
3344
|
+
const [gameModule, manifestContractModule, modules] = await Promise.all([
|
|
3345
|
+
importTypeScriptModule(
|
|
3346
|
+
path3.join(options.projectRoot, "app", "game.ts")
|
|
3347
|
+
),
|
|
3348
|
+
importTypeScriptModule(
|
|
3349
|
+
path3.join(options.projectRoot, "shared", "manifest-contract.ts")
|
|
3350
|
+
),
|
|
3351
|
+
loadProjectReducerNativeModules(options.projectRoot)
|
|
3352
|
+
]);
|
|
3353
|
+
const createInitialTable = typeof manifestContractModule.createInitialTable === "function" ? manifestContractModule.createInitialTable : null;
|
|
3354
|
+
const contractFingerprintValue = modules.contractFingerprint(
|
|
3355
|
+
gameModule.default
|
|
3356
|
+
).value;
|
|
3357
|
+
const basesById = new Map(
|
|
3358
|
+
options.bases.map((base) => [base.definition.id, base])
|
|
3359
|
+
);
|
|
3360
|
+
for (const base of sortBasesForArtifacts(options.bases, basesById)) {
|
|
3361
|
+
const resolvedBase = resolveBaseDefinition(base, basesById);
|
|
3362
|
+
const effectiveSetup = resolveEffectiveBaseSetup({
|
|
3363
|
+
manifest,
|
|
3364
|
+
base: base.definition,
|
|
3365
|
+
basesById
|
|
3366
|
+
});
|
|
3367
|
+
const shadow = new ShadowReducerRuntime(
|
|
3368
|
+
modules,
|
|
3369
|
+
manifest,
|
|
3370
|
+
gameModule.default,
|
|
3371
|
+
createInitialTable,
|
|
3372
|
+
resolvedBase.seed,
|
|
3373
|
+
resolvedBase.players,
|
|
3374
|
+
effectiveSetup.setupProfileId,
|
|
3375
|
+
options.debug ?? false
|
|
3376
|
+
);
|
|
3377
|
+
let parentFingerprintHash = null;
|
|
3378
|
+
if (base.definition.extends) {
|
|
3379
|
+
const parentArtifact = baseStates[baseStateKey(base.definition.extends)];
|
|
3380
|
+
if (!parentArtifact) {
|
|
3381
|
+
throw new Error(
|
|
3382
|
+
`Base '${base.definition.id}' extends '${base.definition.extends}', but the parent artifact does not exist yet.`
|
|
3383
|
+
);
|
|
3384
|
+
}
|
|
3385
|
+
if (parentArtifact.fingerprint.seed !== resolvedBase.seed) {
|
|
3386
|
+
throw new Error(
|
|
3387
|
+
`Base '${base.definition.id}' extends '${base.definition.extends}' but changes seed from ${parentArtifact.fingerprint.seed} to ${resolvedBase.seed}.`
|
|
3388
|
+
);
|
|
3389
|
+
}
|
|
3390
|
+
if (parentArtifact.fingerprint.players !== resolvedBase.players) {
|
|
3391
|
+
throw new Error(
|
|
3392
|
+
`Base '${base.definition.id}' extends '${base.definition.extends}' but changes players from ${parentArtifact.fingerprint.players} to ${resolvedBase.players}.`
|
|
3393
|
+
);
|
|
3394
|
+
}
|
|
3395
|
+
if ((parentArtifact.fingerprint.setupProfileId ?? null) !== (effectiveSetup.setupProfileId ?? null)) {
|
|
3396
|
+
throw new Error(
|
|
3397
|
+
`Base '${base.definition.id}' extends '${base.definition.extends}' but changes setup profile from ${parentArtifact.fingerprint.setupProfileId ?? "none"} to ${effectiveSetup.setupProfileId ?? "none"}.`
|
|
3398
|
+
);
|
|
3399
|
+
}
|
|
3400
|
+
parentFingerprintHash = fingerprintHash(parentArtifact.fingerprint);
|
|
3401
|
+
shadow.hydrate(parentArtifact.snapshot, parentArtifact.version);
|
|
3402
|
+
}
|
|
3403
|
+
const context = await createScenarioContext({
|
|
3404
|
+
shadow,
|
|
3405
|
+
expect: modules.createExpectApi()
|
|
3406
|
+
});
|
|
3407
|
+
await context.game.start();
|
|
3408
|
+
try {
|
|
3409
|
+
await base.definition.setup({
|
|
3410
|
+
game: context.game,
|
|
3411
|
+
players: context.players,
|
|
3412
|
+
seat: context.seat
|
|
3413
|
+
});
|
|
3414
|
+
} catch (error) {
|
|
3415
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3416
|
+
throw new Error(
|
|
3417
|
+
`Base '${base.definition.id}' setup failed with setupProfileId '${effectiveSetup.setupProfileId ?? "none"}': ${message}`,
|
|
3418
|
+
{ cause: error instanceof Error ? error : void 0 }
|
|
3419
|
+
);
|
|
3420
|
+
}
|
|
3421
|
+
baseStates[baseStateKey(base.definition.id)] = {
|
|
3422
|
+
key: baseStateKey(base.definition.id),
|
|
3423
|
+
base: base.definition.id,
|
|
3424
|
+
snapshot: shadow.rawState(),
|
|
3425
|
+
version: shadow.currentVersion(),
|
|
3426
|
+
fingerprint: {
|
|
3427
|
+
base: base.definition.id,
|
|
3428
|
+
seed: resolvedBase.seed,
|
|
3429
|
+
players: resolvedBase.players,
|
|
3430
|
+
baseBundleHash: base.bundleHash,
|
|
3431
|
+
manifestHash,
|
|
3432
|
+
appBundleHash,
|
|
3433
|
+
uiContractHash,
|
|
3434
|
+
setupProfileId: effectiveSetup.setupProfileId,
|
|
3435
|
+
compiledResultId: options.compiledResultId,
|
|
3436
|
+
projectId: options.projectId,
|
|
3437
|
+
parentBaseId: base.definition.extends ?? null,
|
|
3438
|
+
parentFingerprintHash,
|
|
3439
|
+
contractFingerprint: contractFingerprintValue
|
|
3440
|
+
}
|
|
3441
|
+
};
|
|
3442
|
+
writeProjectionSnapshots({
|
|
3443
|
+
projectRoot: options.projectRoot,
|
|
3444
|
+
baseId: base.definition.id,
|
|
3445
|
+
shadow
|
|
3446
|
+
});
|
|
3447
|
+
}
|
|
3448
|
+
const header = "// Generated by dreamboard test. Do not edit by hand.\n";
|
|
3449
|
+
await writeWorkspaceTextFile(
|
|
3450
|
+
options.projectRoot,
|
|
3451
|
+
"test/generated/base-states.generated.ts",
|
|
3452
|
+
`${header}export const BASE_STATES = ${JSON.stringify(baseStates, null, 2)} as const;
|
|
3453
|
+
export const BASE_STATES_CONTRACT_FINGERPRINT = ${JSON.stringify(contractFingerprintValue)};
|
|
3454
|
+
`
|
|
3455
|
+
);
|
|
3456
|
+
await writeWorkspaceTextFile(
|
|
3457
|
+
options.projectRoot,
|
|
3458
|
+
"test/generated/base-states.generated.d.ts",
|
|
3459
|
+
`${header}export declare const BASE_STATES: Record<string, unknown>;
|
|
3460
|
+
export declare const BASE_STATES_CONTRACT_FINGERPRINT: string | undefined;
|
|
3461
|
+
`
|
|
3462
|
+
);
|
|
3463
|
+
await writeWorkspaceTextFile(
|
|
3464
|
+
options.projectRoot,
|
|
3465
|
+
"test/generated/scenario-manifest.generated.ts",
|
|
3466
|
+
`${header}export const SCENARIO_MANIFEST = ${JSON.stringify(
|
|
3467
|
+
options.scenarios.map((scenario) => ({
|
|
3468
|
+
id: scenario.definition.id,
|
|
3469
|
+
filePath: path3.relative(generatedDir, scenario.filePath),
|
|
3470
|
+
base: scenario.definition.from
|
|
3471
|
+
})),
|
|
3472
|
+
null,
|
|
3473
|
+
2
|
|
3474
|
+
)} as const;
|
|
3475
|
+
`
|
|
3476
|
+
);
|
|
3477
|
+
await writeWorkspaceJsonFile(
|
|
3478
|
+
options.projectRoot,
|
|
3479
|
+
"test/generated/.generation-meta.json",
|
|
3480
|
+
{
|
|
3481
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3482
|
+
contractFingerprint: contractFingerprintValue,
|
|
3483
|
+
scenarioCount: options.scenarios.length,
|
|
3484
|
+
baseStateCount: options.bases.length
|
|
3485
|
+
}
|
|
3486
|
+
);
|
|
3487
|
+
}
|
|
3488
|
+
async function loadGeneratedBaseStates(projectRoot) {
|
|
3489
|
+
const filePath = path3.join(
|
|
3490
|
+
projectRoot,
|
|
3491
|
+
"test",
|
|
3492
|
+
"generated",
|
|
3493
|
+
"base-states.generated.ts"
|
|
3494
|
+
);
|
|
3495
|
+
if (!await exists(filePath)) {
|
|
3496
|
+
return null;
|
|
3497
|
+
}
|
|
3498
|
+
const module = await importTypeScriptModule(filePath);
|
|
3499
|
+
return module.BASE_STATES ?? null;
|
|
3500
|
+
}
|
|
3501
|
+
async function loadGeneratedScenarioManifest(projectRoot) {
|
|
3502
|
+
const filePath = path3.join(
|
|
3503
|
+
projectRoot,
|
|
3504
|
+
"test",
|
|
3505
|
+
"generated",
|
|
3506
|
+
"scenario-manifest.generated.ts"
|
|
3507
|
+
);
|
|
3508
|
+
if (!await exists(filePath)) {
|
|
3509
|
+
return [];
|
|
3510
|
+
}
|
|
3511
|
+
const module = await importTypeScriptModule(filePath);
|
|
3512
|
+
const entries = module.SCENARIO_MANIFEST;
|
|
3513
|
+
if (!Array.isArray(entries)) {
|
|
3514
|
+
return [];
|
|
3515
|
+
}
|
|
3516
|
+
return entries.flatMap((entry) => {
|
|
3517
|
+
if (entry && typeof entry === "object" && "id" in entry && "base" in entry && typeof entry.id === "string" && typeof entry.base === "string") {
|
|
3518
|
+
return [{ id: entry.id, base: entry.base }];
|
|
3519
|
+
}
|
|
3520
|
+
return [];
|
|
3521
|
+
});
|
|
3522
|
+
}
|
|
3523
|
+
function validateGeneratedFingerprint(options) {
|
|
3524
|
+
const mismatches = [];
|
|
3525
|
+
if (options.generated.contractFingerprint && options.current.contractFingerprint && options.generated.contractFingerprint !== options.current.contractFingerprint) {
|
|
3526
|
+
const error = new Error(
|
|
3527
|
+
`Base states were generated for contract ${options.generated.contractFingerprint} but the current contract is ${options.current.contractFingerprint}. Remedy: run \`dreamboard test\`, then re-run the tests.`
|
|
3528
|
+
);
|
|
3529
|
+
error.code = STALE_CONTRACT_ARTIFACT_CODE;
|
|
3530
|
+
throw error;
|
|
3531
|
+
}
|
|
3532
|
+
if (options.generated.baseBundleHash !== options.current.baseBundleHash) {
|
|
3533
|
+
mismatches.push("base module changed");
|
|
3534
|
+
}
|
|
3535
|
+
if (options.generated.manifestHash !== options.current.manifestHash) {
|
|
3536
|
+
mismatches.push("manifest changed");
|
|
3537
|
+
}
|
|
3538
|
+
if (options.generated.appBundleHash !== options.current.appBundleHash) {
|
|
3539
|
+
mismatches.push("project reducer bundle changed");
|
|
3540
|
+
}
|
|
3541
|
+
if (options.generated.uiContractHash !== options.current.uiContractHash) {
|
|
3542
|
+
mismatches.push("ui contract changed");
|
|
3543
|
+
}
|
|
3544
|
+
if (options.generated.seed !== options.current.seed || options.generated.players !== options.current.players) {
|
|
3545
|
+
mismatches.push("base seed/player count changed");
|
|
3546
|
+
}
|
|
3547
|
+
if ((options.generated.setupProfileId ?? null) !== (options.current.setupProfileId ?? null)) {
|
|
3548
|
+
mismatches.push("base setup profile changed");
|
|
3549
|
+
}
|
|
3550
|
+
if ((options.generated.parentBaseId ?? null) !== (options.current.parentBaseId ?? null)) {
|
|
3551
|
+
mismatches.push("base parent changed");
|
|
3552
|
+
}
|
|
3553
|
+
if ((options.generated.parentFingerprintHash ?? null) !== (options.current.parentFingerprintHash ?? null)) {
|
|
3554
|
+
mismatches.push("parent base artifact changed");
|
|
3555
|
+
}
|
|
3556
|
+
if (mismatches.length > 0) {
|
|
3557
|
+
throw new Error(
|
|
3558
|
+
`${mismatches.join("; ")}. Run 'dreamboard test' to refresh reducer-native base artifacts.`
|
|
3559
|
+
);
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3562
|
+
async function currentFingerprint(options) {
|
|
3563
|
+
const manifest = await loadManifest(options.projectRoot);
|
|
3564
|
+
const [gameModule, modules] = await Promise.all([
|
|
3565
|
+
importTypeScriptModule(
|
|
3566
|
+
path3.join(options.projectRoot, "app", "game.ts")
|
|
3567
|
+
),
|
|
3568
|
+
loadProjectReducerNativeModules(options.projectRoot)
|
|
3569
|
+
]);
|
|
3570
|
+
const contractFingerprintValue = modules.contractFingerprint(
|
|
3571
|
+
gameModule.default
|
|
3572
|
+
).value;
|
|
3573
|
+
const resolvedBase = resolveBaseDefinition(
|
|
3574
|
+
options.base,
|
|
3575
|
+
options.basesById ?? /* @__PURE__ */ new Map([[options.base.definition.id, options.base]])
|
|
3576
|
+
);
|
|
3577
|
+
const effectiveSetup = resolveEffectiveBaseSetup({
|
|
3578
|
+
manifest,
|
|
3579
|
+
base: options.base.definition,
|
|
3580
|
+
basesById: options.basesById
|
|
3581
|
+
});
|
|
3582
|
+
let parentFingerprintHash = null;
|
|
3583
|
+
if (options.base.definition.extends) {
|
|
3584
|
+
const parentBase = options.basesById?.get(options.base.definition.extends);
|
|
3585
|
+
if (!parentBase) {
|
|
3586
|
+
throw new Error(
|
|
3587
|
+
`Base '${options.base.definition.id}' extends unknown parent '${options.base.definition.extends}'.`
|
|
3588
|
+
);
|
|
3589
|
+
}
|
|
3590
|
+
parentFingerprintHash = fingerprintHash(
|
|
3591
|
+
await currentFingerprint({
|
|
3592
|
+
projectRoot: options.projectRoot,
|
|
3593
|
+
base: parentBase,
|
|
3594
|
+
basesById: options.basesById,
|
|
3595
|
+
compiledResultId: options.compiledResultId,
|
|
3596
|
+
projectId: options.projectId
|
|
3597
|
+
})
|
|
3598
|
+
);
|
|
3599
|
+
}
|
|
3600
|
+
return {
|
|
3601
|
+
base: options.base.definition.id,
|
|
3602
|
+
seed: resolvedBase.seed,
|
|
3603
|
+
players: resolvedBase.players,
|
|
3604
|
+
baseBundleHash: options.base.bundleHash,
|
|
3605
|
+
manifestHash: hashContent(JSON.stringify(manifest)),
|
|
3606
|
+
appBundleHash: hashContent(
|
|
3607
|
+
await bundleTypeScriptModuleText(
|
|
3608
|
+
path3.join(options.projectRoot, "app", "game.ts")
|
|
3609
|
+
)
|
|
3610
|
+
),
|
|
3611
|
+
uiContractHash: hashContent(
|
|
3612
|
+
await bundleTypeScriptModuleText(
|
|
3613
|
+
path3.join(options.projectRoot, "shared", "generated", "ui-contract.ts"),
|
|
3614
|
+
{ external: SDK_UI_RUNTIME_EXTERNALS }
|
|
3615
|
+
)
|
|
3616
|
+
),
|
|
3617
|
+
setupProfileId: effectiveSetup.setupProfileId,
|
|
3618
|
+
compiledResultId: options.compiledResultId,
|
|
3619
|
+
projectId: options.projectId,
|
|
3620
|
+
parentBaseId: options.base.definition.extends ?? null,
|
|
3621
|
+
parentFingerprintHash,
|
|
3622
|
+
contractFingerprint: contractFingerprintValue
|
|
3623
|
+
};
|
|
3624
|
+
}
|
|
3625
|
+
async function generateReducerNativeArtifacts(options) {
|
|
3626
|
+
await ensureReducerNativeTestingFiles(options.projectRoot);
|
|
3627
|
+
const [bases, scenarios] = await Promise.all([
|
|
3628
|
+
loadTypedBases(options.projectRoot),
|
|
3629
|
+
loadTypedScenarios(options.projectRoot, {
|
|
3630
|
+
scenarioPath: options.scenarioPath
|
|
3631
|
+
})
|
|
3632
|
+
]);
|
|
3633
|
+
const basesById = new Map(bases.map((base) => [base.definition.id, base]));
|
|
3634
|
+
validateTypedScenarioBases(scenarios, basesById);
|
|
3635
|
+
await writeReducerNativeGeneratedFiles({
|
|
3636
|
+
projectRoot: options.projectRoot,
|
|
3637
|
+
bases,
|
|
3638
|
+
scenarios,
|
|
3639
|
+
compiledResultId: options.compiledResultId,
|
|
3640
|
+
projectId: options.projectId,
|
|
3641
|
+
debug: options.debug
|
|
3642
|
+
});
|
|
3643
|
+
return { bases, scenarios };
|
|
3644
|
+
}
|
|
3645
|
+
async function runReducerNativeScenarios(options) {
|
|
3646
|
+
await ensureReducerNativeTestingFiles(options.projectRoot);
|
|
3647
|
+
const [
|
|
3648
|
+
bases,
|
|
3649
|
+
scenarios,
|
|
3650
|
+
initialGeneratedBaseStates,
|
|
3651
|
+
manifest,
|
|
3652
|
+
gameModule,
|
|
3653
|
+
manifestContractModule,
|
|
3654
|
+
modules
|
|
3655
|
+
] = await Promise.all([
|
|
3656
|
+
loadTypedBases(options.projectRoot),
|
|
3657
|
+
loadTypedScenarios(options.projectRoot, {
|
|
3658
|
+
scenarioPath: options.scenarioPath
|
|
3659
|
+
}),
|
|
3660
|
+
loadGeneratedBaseStates(options.projectRoot),
|
|
3661
|
+
loadManifest(options.projectRoot),
|
|
3662
|
+
importTypeScriptModule(
|
|
3663
|
+
path3.join(options.projectRoot, "app", "game.ts")
|
|
3664
|
+
),
|
|
3665
|
+
importTypeScriptModule(
|
|
3666
|
+
path3.join(options.projectRoot, "shared", "manifest-contract.ts")
|
|
3667
|
+
),
|
|
3668
|
+
loadProjectReducerNativeModules(options.projectRoot)
|
|
3669
|
+
]);
|
|
3670
|
+
let generatedBaseStates = initialGeneratedBaseStates;
|
|
3671
|
+
const createInitialTable = typeof manifestContractModule.createInitialTable === "function" ? manifestContractModule.createInitialTable : null;
|
|
3672
|
+
const basesById = new Map(bases.map((base) => [base.definition.id, base]));
|
|
3673
|
+
validateTypedScenarioBases(scenarios, basesById);
|
|
3674
|
+
if (options.updateSnapshots) {
|
|
3675
|
+
await writeReducerNativeGeneratedFiles({
|
|
3676
|
+
projectRoot: options.projectRoot,
|
|
3677
|
+
bases,
|
|
3678
|
+
scenarios,
|
|
3679
|
+
compiledResultId: options.compiledResultId,
|
|
3680
|
+
projectId: options.projectId,
|
|
3681
|
+
debug: options.debug
|
|
3682
|
+
});
|
|
3683
|
+
generatedBaseStates = await loadGeneratedBaseStates(options.projectRoot);
|
|
3684
|
+
}
|
|
3685
|
+
let passed = 0;
|
|
3686
|
+
let failed = 0;
|
|
3687
|
+
const results = [];
|
|
3688
|
+
for (const scenario of scenarios) {
|
|
3689
|
+
const base = basesById.get(scenario.definition.from);
|
|
3690
|
+
if (!base) {
|
|
3691
|
+
throw new Error(
|
|
3692
|
+
`Missing typed base '${scenario.definition.from}' for scenario '${scenario.definition.id}'.`
|
|
3693
|
+
);
|
|
3694
|
+
}
|
|
3695
|
+
if (generatedBaseStates?.[baseStateKey(base.definition.id)]) {
|
|
3696
|
+
validateGeneratedFingerprint({
|
|
3697
|
+
generated: generatedBaseStates[baseStateKey(base.definition.id)].fingerprint,
|
|
3698
|
+
current: await currentFingerprint({
|
|
3699
|
+
projectRoot: options.projectRoot,
|
|
3700
|
+
base,
|
|
3701
|
+
basesById,
|
|
3702
|
+
compiledResultId: options.compiledResultId,
|
|
3703
|
+
projectId: options.projectId
|
|
3704
|
+
})
|
|
3705
|
+
});
|
|
3706
|
+
}
|
|
3707
|
+
try {
|
|
3708
|
+
const resolvedBase = resolveBaseDefinition(base, basesById);
|
|
3709
|
+
const effectiveSetup = resolveEffectiveBaseSetup({
|
|
3710
|
+
manifest,
|
|
3711
|
+
base: base.definition,
|
|
3712
|
+
basesById
|
|
3713
|
+
});
|
|
3714
|
+
const shadow = new ShadowReducerRuntime(
|
|
3715
|
+
modules,
|
|
3716
|
+
manifest,
|
|
3717
|
+
gameModule.default,
|
|
3718
|
+
createInitialTable,
|
|
3719
|
+
resolvedBase.seed,
|
|
3720
|
+
resolvedBase.players,
|
|
3721
|
+
effectiveSetup.setupProfileId,
|
|
3722
|
+
options.debug ?? false
|
|
3723
|
+
);
|
|
3724
|
+
if (base.definition.extends) {
|
|
3725
|
+
const parentArtifact = generatedBaseStates?.[baseStateKey(base.definition.extends)];
|
|
3726
|
+
if (!parentArtifact) {
|
|
3727
|
+
throw new Error(
|
|
3728
|
+
`Base '${base.definition.id}' extends '${base.definition.extends}', but the parent artifact is missing. Run 'dreamboard test' first.`
|
|
3729
|
+
);
|
|
3730
|
+
}
|
|
3731
|
+
shadow.hydrate(parentArtifact.snapshot, parentArtifact.version);
|
|
3732
|
+
}
|
|
3733
|
+
const context = await createScenarioContext({
|
|
3734
|
+
shadow,
|
|
3735
|
+
expect: modules.createExpectApi({
|
|
3736
|
+
matchSnapshot: createScenarioSnapshotMatcher({
|
|
3737
|
+
projectRoot: options.projectRoot,
|
|
3738
|
+
scenarioId: scenario.definition.id,
|
|
3739
|
+
updateSnapshots: options.updateSnapshots ?? false
|
|
3740
|
+
})
|
|
3741
|
+
})
|
|
3742
|
+
});
|
|
3743
|
+
await context.game.start();
|
|
3744
|
+
await base.definition.setup({
|
|
3745
|
+
game: context.game,
|
|
3746
|
+
players: context.players,
|
|
3747
|
+
seat: context.seat
|
|
3748
|
+
});
|
|
3749
|
+
shadow.clearHistory();
|
|
3750
|
+
await scenario.definition.when(context);
|
|
3751
|
+
if (scenario.definition.phase && shadow.phase() !== scenario.definition.phase) {
|
|
3752
|
+
throw new Error(
|
|
3753
|
+
`Scenario '${scenario.definition.id}' expected phase '${scenario.definition.phase}' but reached '${shadow.phase()}'.`
|
|
3754
|
+
);
|
|
3755
|
+
}
|
|
3756
|
+
if (scenario.definition.stage && shadow.currentStage() !== scenario.definition.stage) {
|
|
3757
|
+
throw new Error(
|
|
3758
|
+
`Scenario '${scenario.definition.id}' expected stage '${scenario.definition.stage}' but reached '${shadow.currentStage() ?? "null"}'.`
|
|
3759
|
+
);
|
|
3760
|
+
}
|
|
3761
|
+
await scenario.definition.then(context);
|
|
3762
|
+
passed += 1;
|
|
3763
|
+
results.push({
|
|
3764
|
+
id: scenario.definition.id,
|
|
3765
|
+
success: true
|
|
3766
|
+
});
|
|
3767
|
+
} catch (error) {
|
|
3768
|
+
failed += 1;
|
|
3769
|
+
results.push({
|
|
3770
|
+
id: scenario.definition.id,
|
|
3771
|
+
success: false,
|
|
3772
|
+
errorCode: isStaleContractArtifactError(error) ? STALE_CONTRACT_ARTIFACT_CODE : void 0,
|
|
3773
|
+
error: error instanceof Error ? formatScenarioErrorForDisplay({
|
|
3774
|
+
error,
|
|
3775
|
+
projectRoot: options.projectRoot,
|
|
3776
|
+
scenarioFilePath: scenario.filePath
|
|
3777
|
+
}) : `Scenario '${scenario.definition.id}' failed.`
|
|
3778
|
+
});
|
|
3779
|
+
} finally {
|
|
3780
|
+
}
|
|
3781
|
+
}
|
|
3782
|
+
return { passed, failed, results };
|
|
3783
|
+
}
|
|
3784
|
+
|
|
3785
|
+
// src/services/project/local-maintainer-registry-shared.ts
|
|
3786
|
+
import crypto2 from "crypto";
|
|
3787
|
+
import path4 from "path";
|
|
3788
|
+
var localRegistryAddress = readLocalRegistryAddress();
|
|
3789
|
+
var LOCAL_REGISTRY_HOST = localRegistryAddress.host;
|
|
3790
|
+
var LOCAL_REGISTRY_PORT = localRegistryAddress.port;
|
|
3791
|
+
var LOCAL_REGISTRY_URL = `http://${LOCAL_REGISTRY_HOST}:${LOCAL_REGISTRY_PORT}`;
|
|
3792
|
+
var LOCAL_SCOPE_NPMRC_CONTENT = `@dreamboard-games:registry=${LOCAL_REGISTRY_URL}
|
|
3793
|
+
`;
|
|
3794
|
+
function readLocalRegistryAddress() {
|
|
3795
|
+
const urlOverride = process.env.DREAMBOARD_LOCAL_REGISTRY_URL?.trim();
|
|
3796
|
+
const hostOverride = process.env.DREAMBOARD_LOCAL_REGISTRY_HOST?.trim();
|
|
3797
|
+
const portOverride = process.env.DREAMBOARD_LOCAL_REGISTRY_PORT?.trim();
|
|
3798
|
+
if (urlOverride) {
|
|
3799
|
+
let parsed;
|
|
3800
|
+
try {
|
|
3801
|
+
parsed = new URL(urlOverride);
|
|
3802
|
+
} catch {
|
|
3803
|
+
throw new Error(
|
|
3804
|
+
`Invalid DREAMBOARD_LOCAL_REGISTRY_URL '${urlOverride}'. Expected an http://host:port URL.`
|
|
3805
|
+
);
|
|
3806
|
+
}
|
|
3807
|
+
if (parsed.protocol !== "http:" || !parsed.hostname || !parsed.port) {
|
|
3808
|
+
throw new Error(
|
|
3809
|
+
`Invalid DREAMBOARD_LOCAL_REGISTRY_URL '${urlOverride}'. Expected an http://host:port URL.`
|
|
3810
|
+
);
|
|
3811
|
+
}
|
|
3812
|
+
return {
|
|
3813
|
+
host: hostOverride || parsed.hostname,
|
|
3814
|
+
port: parseLocalRegistryPort(portOverride || parsed.port)
|
|
3815
|
+
};
|
|
3816
|
+
}
|
|
3817
|
+
return {
|
|
3818
|
+
host: hostOverride || "127.0.0.1",
|
|
3819
|
+
port: portOverride ? parseLocalRegistryPort(portOverride) : 4873
|
|
3820
|
+
};
|
|
3821
|
+
}
|
|
3822
|
+
function parseLocalRegistryPort(raw) {
|
|
3823
|
+
const port = Number.parseInt(raw, 10);
|
|
3824
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
3825
|
+
throw new Error(
|
|
3826
|
+
`Invalid DREAMBOARD_LOCAL_REGISTRY_PORT '${raw}'. Expected a TCP port from 1 to 65535.`
|
|
3827
|
+
);
|
|
3828
|
+
}
|
|
3829
|
+
return port;
|
|
3830
|
+
}
|
|
3831
|
+
function shortHash(value) {
|
|
3832
|
+
return crypto2.createHash("sha256").update(value).digest("hex").slice(0, 12);
|
|
3833
|
+
}
|
|
3834
|
+
function isLocalMaintainerRegistryEnabled(apiBaseUrl) {
|
|
3835
|
+
let isLocalLoopbackUrl = false;
|
|
3836
|
+
try {
|
|
3837
|
+
const parsed = new URL(apiBaseUrl);
|
|
3838
|
+
isLocalLoopbackUrl = parsed.protocol === "http:" && (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1");
|
|
3839
|
+
} catch {
|
|
3840
|
+
isLocalLoopbackUrl = false;
|
|
3841
|
+
}
|
|
3842
|
+
return !IS_PUBLISHED_BUILD && (apiBaseUrl === ENVIRONMENT_CONFIGS.local?.apiBaseUrl || isLocalLoopbackUrl) && BUILD_CHANNEL === "development";
|
|
3843
|
+
}
|
|
3844
|
+
function didLocalMaintainerSnapshotChange(previous, next) {
|
|
3845
|
+
if (!next) {
|
|
3846
|
+
return false;
|
|
3847
|
+
}
|
|
3848
|
+
return previous?.snapshotId !== next.snapshotId;
|
|
3849
|
+
}
|
|
3850
|
+
|
|
3851
|
+
export {
|
|
3852
|
+
createPkcePair,
|
|
3853
|
+
buildClerkAuthorizationUrl,
|
|
3854
|
+
exchangeClerkOAuthCode,
|
|
3855
|
+
createUserSessionManager,
|
|
3856
|
+
IS_PUBLISHED_BUILD,
|
|
3857
|
+
CAN_SELECT_ENVIRONMENT,
|
|
3858
|
+
PUBLISHED_ENVIRONMENT,
|
|
3859
|
+
resolveLocalHarnessAccessToken,
|
|
3860
|
+
resolveConfig,
|
|
3861
|
+
configureClient,
|
|
3862
|
+
requireAuth,
|
|
3863
|
+
getAuthTokenExpiry,
|
|
3864
|
+
resolveProjectContext,
|
|
3865
|
+
parseConfigFlags,
|
|
3866
|
+
parsePlayerCountFlags,
|
|
3867
|
+
parseNewCommandArgs,
|
|
3868
|
+
parseCloneCommandArgs,
|
|
3869
|
+
parseCommitScopedCommandArgs,
|
|
3870
|
+
parseBuildCommandArgs,
|
|
3871
|
+
parseReleasePublishCommandArgs,
|
|
3872
|
+
parseDevCommandArgs,
|
|
3873
|
+
parseAuthCommandArgs,
|
|
3874
|
+
CONFIG_FLAG_ARGS,
|
|
3875
|
+
STALE_CONTRACT_ARTIFACT_CODE,
|
|
3876
|
+
STALE_CONTRACT_ARTIFACT_EXIT_CODE,
|
|
3877
|
+
toDreamboardApiError,
|
|
3878
|
+
isDreamboardApiError,
|
|
3879
|
+
isStaleContractArtifactMessage,
|
|
3880
|
+
isStaleContractArtifactError,
|
|
3881
|
+
getCliErrorExitCode,
|
|
3882
|
+
presentCliError,
|
|
3883
|
+
formatCliError,
|
|
3884
|
+
normalizeSlug,
|
|
3885
|
+
titleFromSlug,
|
|
3886
|
+
parsePositiveInt,
|
|
3887
|
+
findCompiledResultsForAuthoringState,
|
|
3888
|
+
waitForCompiledResultJobSdk,
|
|
3889
|
+
importTypeScriptModule,
|
|
3890
|
+
resolveSetupProfileSelectionForSession,
|
|
3891
|
+
projectIdFromSessionGameSource,
|
|
3892
|
+
isReducerNativeTestingWorkspace,
|
|
3893
|
+
ensureReducerNativeTestingFiles,
|
|
3894
|
+
createSessionFromScenario,
|
|
3895
|
+
generateReducerNativeArtifacts,
|
|
3896
|
+
runReducerNativeScenarios,
|
|
3897
|
+
LOCAL_REGISTRY_URL,
|
|
3898
|
+
shortHash,
|
|
3899
|
+
isLocalMaintainerRegistryEnabled,
|
|
3900
|
+
didLocalMaintainerSnapshotChange
|
|
3901
|
+
};
|
|
3902
|
+
//# sourceMappingURL=chunk-5IYJOVUA.js.map
|