@dreamboard-games/cli 0.1.30-alpha.29 → 0.1.30-alpha.30
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 +2 -1
- package/dist/agent-verifier/agent-workspace-verifier.mjs +360 -17
- package/dist/agent-verifier/agent-workspace-verifier.mjs.map +1 -1
- package/dist/agent-verifier/{chunk-IWB4L2HV.mjs → chunk-FNSHNMDY.mjs} +51 -5
- package/dist/agent-verifier/chunk-FNSHNMDY.mjs.map +1 -0
- package/dist/agent-verifier/{chunk-RDYXWXXC.mjs → chunk-LMW66VBH.mjs} +2 -11
- package/dist/agent-verifier/{chunk-RDYXWXXC.mjs.map → chunk-LMW66VBH.mjs.map} +1 -1
- package/dist/agent-verifier/{chunk-TIDX3YLW.mjs → chunk-M6YNQZCC.mjs} +2 -2
- package/dist/agent-verifier/{chunk-Z7UBAREF.mjs → chunk-QMOBTQ5G.mjs} +7 -9
- package/dist/agent-verifier/chunk-QMOBTQ5G.mjs.map +1 -0
- 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-IXZLY4BS.mjs → global-config-SWWR2LP4.mjs} +3 -4
- package/dist/agent-verifier/{materialize-workspace-XYYCQQGB.mjs → materialize-workspace-K4WYFG5E.mjs} +5 -5
- package/dist/agent-verifier/{reducer-native-test-harness-BY5SZ7XE.mjs → reducer-native-test-harness-UFMSNNDY.mjs} +49 -576
- package/dist/agent-verifier/reducer-native-test-harness-UFMSNNDY.mjs.map +1 -0
- package/dist/agent-verifier/{static-scaffold-M7QPX76Z.mjs → static-scaffold-MHVM63HU.mjs} +4 -4
- package/dist/authoring-compatibility-internal.js +1 -1
- package/dist/{chunk-QIVDPQME.js → chunk-I4SZ7FA4.js} +9 -64
- package/dist/{chunk-QIVDPQME.js.map → chunk-I4SZ7FA4.js.map} +1 -1
- package/dist/{chunk-NFCRMXEV.js → chunk-RTNKVNQA.js} +52 -624
- package/dist/chunk-RTNKVNQA.js.map +1 -0
- package/dist/index.js +318 -369
- package/dist/index.js.map +1 -1
- package/dist/internal.js +23 -3
- package/dist/internal.js.map +1 -1
- package/package.json +1 -1
- package/release/authoring-release-set.json +2 -2
- package/skills/dreamboard/SKILL.md +1 -1
- package/skills/dreamboard/references/cli.md +2 -3
- package/skills/dreamboard/references/quickstart.md +1 -1
- package/skills/dreamboard/references/testing.md +0 -7
- package/dist/agent-verifier/chunk-B7M2TJSP.mjs +0 -363
- package/dist/agent-verifier/chunk-B7M2TJSP.mjs.map +0 -1
- package/dist/agent-verifier/chunk-F2DIOJJZ.mjs.map +0 -1
- package/dist/agent-verifier/chunk-IWB4L2HV.mjs.map +0 -1
- package/dist/agent-verifier/chunk-UXGTT25Q.mjs +0 -59
- package/dist/agent-verifier/chunk-UXGTT25Q.mjs.map +0 -1
- package/dist/agent-verifier/chunk-Z7UBAREF.mjs.map +0 -1
- package/dist/agent-verifier/reducer-native-test-harness-BY5SZ7XE.mjs.map +0 -1
- package/dist/chunk-NFCRMXEV.js.map +0 -1
- /package/dist/agent-verifier/{chunk-TIDX3YLW.mjs.map → chunk-M6YNQZCC.mjs.map} +0 -0
- /package/dist/agent-verifier/{global-config-IXZLY4BS.mjs.map → global-config-SWWR2LP4.mjs.map} +0 -0
- /package/dist/agent-verifier/{materialize-workspace-XYYCQQGB.mjs.map → materialize-workspace-K4WYFG5E.mjs.map} +0 -0
- /package/dist/agent-verifier/{static-scaffold-M7QPX76Z.mjs.map → static-scaffold-MHVM63HU.mjs.map} +0 -0
package/dist/internal.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import {
|
|
3
3
|
CONFIG_FLAG_ARGS,
|
|
4
4
|
configureClient,
|
|
5
|
-
configurePlaywrightBrowsersPath,
|
|
6
5
|
ensureReducerNativeTestingFiles,
|
|
7
6
|
findCompiledResultsForAuthoringState,
|
|
8
7
|
parseConfigFlags,
|
|
@@ -10,7 +9,7 @@ import {
|
|
|
10
9
|
resolveConfig,
|
|
11
10
|
resolveProjectContext,
|
|
12
11
|
shortHash
|
|
13
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-RTNKVNQA.js";
|
|
14
13
|
import {
|
|
15
14
|
applyWorkspaceCodegen,
|
|
16
15
|
loadManifest,
|
|
@@ -19,7 +18,7 @@ import {
|
|
|
19
18
|
setLatestCompileAttempt,
|
|
20
19
|
updateProjectState,
|
|
21
20
|
writeSnapshot
|
|
22
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-I4SZ7FA4.js";
|
|
23
22
|
import {
|
|
24
23
|
getStoredSession,
|
|
25
24
|
loadGlobalConfig
|
|
@@ -30,6 +29,27 @@ import {
|
|
|
30
29
|
writeJsonFile
|
|
31
30
|
} from "./chunk-EQNBQVIW.js";
|
|
32
31
|
import "./chunk-2H7UOFLK.js";
|
|
32
|
+
|
|
33
|
+
// src/ui/playwright-runner.ts
|
|
34
|
+
import os from "os";
|
|
35
|
+
import path from "path";
|
|
36
|
+
function configurePlaywrightBrowsersPath() {
|
|
37
|
+
if (process.env.PLAYWRIGHT_BROWSERS_PATH) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const runtimeHome = process.env.HOME;
|
|
41
|
+
let realHome;
|
|
42
|
+
try {
|
|
43
|
+
realHome = os.userInfo().homedir;
|
|
44
|
+
} catch {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (!runtimeHome || path.resolve(runtimeHome) === path.resolve(realHome)) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const browserCachePath = process.platform === "darwin" ? path.join(realHome, "Library", "Caches", "ms-playwright") : process.platform === "win32" ? path.join(realHome, "AppData", "Local", "ms-playwright") : path.join(realHome, ".cache", "ms-playwright");
|
|
51
|
+
process.env.PLAYWRIGHT_BROWSERS_PATH = browserCachePath;
|
|
52
|
+
}
|
|
33
53
|
export {
|
|
34
54
|
CONFIG_FLAG_ARGS,
|
|
35
55
|
ENVIRONMENT_CONFIGS,
|
package/dist/internal.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/ui/playwright-runner.ts"],"sourcesContent":["import os from \"node:os\";\nimport path from \"node:path\";\nimport type { ResolvedConfig } from \"../types.js\";\nimport { createUserTokenManager } from \"../auth/user-token-manager.js\";\nimport { resolveLocalHarnessAccessToken } from \"../config/local-harness-auth.js\";\n\n/**\n * Browser test runner helpers shared with reducer-native-test-harness (browser runner).\n * Screenshot / JSON scenario navigation helpers lived in the deleted `run` command.\n */\nexport function configurePlaywrightBrowsersPath(): void {\n if (process.env.PLAYWRIGHT_BROWSERS_PATH) {\n return;\n }\n\n const runtimeHome = process.env.HOME;\n let realHome: string;\n try {\n realHome = os.userInfo().homedir;\n } catch {\n return;\n }\n\n if (!runtimeHome || path.resolve(runtimeHome) === path.resolve(realHome)) {\n return;\n }\n\n const browserCachePath =\n process.platform === \"darwin\"\n ? path.join(realHome, \"Library\", \"Caches\", \"ms-playwright\")\n : process.platform === \"win32\"\n ? path.join(realHome, \"AppData\", \"Local\", \"ms-playwright\")\n : path.join(realHome, \".cache\", \"ms-playwright\");\n\n process.env.PLAYWRIGHT_BROWSERS_PATH = browserCachePath;\n}\n\nexport async function buildBrowserAuthInitScript(\n config: ResolvedConfig,\n): Promise<string | null> {\n const resolvedToken =\n resolveLocalHarnessAccessToken(config) ??\n (await createUserTokenManager(config).resolveApiToken())?.token;\n if (!resolvedToken) return null;\n\n return `(function(){localStorage.setItem('dreamboard_auth_token',${JSON.stringify(resolvedToken)});})();`;\n}\n\nexport async function waitForGameReady(\n page: import(\"playwright\").Page,\n timeoutMs = 60000,\n): Promise<void> {\n await page.waitForSelector('iframe[title=\"Game UI Plugin\"]', {\n timeout: timeoutMs,\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,OAAO,QAAQ;AACf,OAAO,UAAU;AASV,SAAS,kCAAwC;AACtD,MAAI,QAAQ,IAAI,0BAA0B;AACxC;AAAA,EACF;AAEA,QAAM,cAAc,QAAQ,IAAI;AAChC,MAAI;AACJ,MAAI;AACF,eAAW,GAAG,SAAS,EAAE;AAAA,EAC3B,QAAQ;AACN;AAAA,EACF;AAEA,MAAI,CAAC,eAAe,KAAK,QAAQ,WAAW,MAAM,KAAK,QAAQ,QAAQ,GAAG;AACxE;AAAA,EACF;AAEA,QAAM,mBACJ,QAAQ,aAAa,WACjB,KAAK,KAAK,UAAU,WAAW,UAAU,eAAe,IACxD,QAAQ,aAAa,UACnB,KAAK,KAAK,UAAU,WAAW,SAAS,eAAe,IACvD,KAAK,KAAK,UAAU,UAAU,eAAe;AAErD,UAAQ,IAAI,2BAA2B;AACzC;","names":[]}
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"packages": {
|
|
5
5
|
"cli": {
|
|
6
6
|
"name": "@dreamboard-games/cli",
|
|
7
|
-
"version": "0.1.30-alpha.
|
|
7
|
+
"version": "0.1.30-alpha.30"
|
|
8
8
|
},
|
|
9
9
|
"sdk": {
|
|
10
10
|
"name": "@dreamboard-games/sdk",
|
|
@@ -34,5 +34,5 @@
|
|
|
34
34
|
"portable": true
|
|
35
35
|
},
|
|
36
36
|
"packageManager": "pnpm@10.4.1",
|
|
37
|
-
"releaseSetId": "sha256:
|
|
37
|
+
"releaseSetId": "sha256:3bd4ab3361ecffc08feeb8557d6f9cec98997cc1b838984ec56bd37908539d01"
|
|
38
38
|
}
|
|
@@ -69,7 +69,7 @@ Use the commands for different kinds of state:
|
|
|
69
69
|
- `dreamboard verify --commit <rev>`
|
|
70
70
|
Verify one exact commit from a detached worktree.
|
|
71
71
|
- `dreamboard test`
|
|
72
|
-
Regenerate derived test artifacts as needed and run reducer
|
|
72
|
+
Regenerate derived test artifacts as needed and run offline reducer tests.
|
|
73
73
|
- `dreamboard dev [--from-scenario <id>]`
|
|
74
74
|
Start the local dev host for browser validation.
|
|
75
75
|
|
|
@@ -19,7 +19,7 @@ Use the CLI for:
|
|
|
19
19
|
- verifying an exact commit from a detached worktree
|
|
20
20
|
- building, previewing, and publishing pushed commits
|
|
21
21
|
- starting the local dev host
|
|
22
|
-
- running reducer
|
|
22
|
+
- running offline reducer tests
|
|
23
23
|
|
|
24
24
|
## Install targets
|
|
25
25
|
|
|
@@ -72,7 +72,7 @@ remote.
|
|
|
72
72
|
|
|
73
73
|
| Command | Use it for |
|
|
74
74
|
| --- | --- |
|
|
75
|
-
| `dreamboard test` | Run reducer
|
|
75
|
+
| `dreamboard test` | Run offline reducer tests |
|
|
76
76
|
| `dreamboard dev [--from-scenario <id>]` | Start the local project dev host |
|
|
77
77
|
| `dreamboard build --commit <rev>` | Create a server build for a pushed commit |
|
|
78
78
|
| `dreamboard preview --commit <rev>` | Create a preview for a pushed commit |
|
|
@@ -93,7 +93,6 @@ Use the scaffolded reducer-native test workspace for repeatable game assertions.
|
|
|
93
93
|
```bash
|
|
94
94
|
dreamboard test
|
|
95
95
|
dreamboard test --scenario test/scenarios/player-two-wins.scenario.ts
|
|
96
|
-
dreamboard test --runner remote --commit HEAD
|
|
97
96
|
```
|
|
98
97
|
|
|
99
98
|
## Start local server
|
|
@@ -59,7 +59,7 @@ dreamboard dev
|
|
|
59
59
|
Useful follow-up commands:
|
|
60
60
|
|
|
61
61
|
- `dreamboard project clone <project>` clones an existing project repository.
|
|
62
|
-
- `dreamboard test` runs reducer
|
|
62
|
+
- `dreamboard test` runs offline reducer tests.
|
|
63
63
|
- `dreamboard build --commit HEAD` creates a server build for a pushed commit.
|
|
64
64
|
- `dreamboard preview --commit HEAD` creates a preview for a pushed commit.
|
|
65
65
|
|
|
@@ -46,12 +46,6 @@ Run one scenario:
|
|
|
46
46
|
dreamboard test --scenario test/scenarios/win-the-game.scenario.ts
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
Run browser-backed tests:
|
|
50
|
-
|
|
51
|
-
```bash
|
|
52
|
-
dreamboard test --runner browser
|
|
53
|
-
```
|
|
54
|
-
|
|
55
49
|
`dreamboard test` regenerates reducer-native artifacts automatically when
|
|
56
50
|
changes affect the runtime shape of the game, including:
|
|
57
51
|
|
|
@@ -111,7 +105,6 @@ Use `defineScenario(...)` from `test/testing-types.ts`.
|
|
|
111
105
|
| `id` | Yes | Scenario identifier |
|
|
112
106
|
| `description` | No | Short human-readable summary |
|
|
113
107
|
| `from` | Yes | Base ID from `test/bases/*.base.ts` |
|
|
114
|
-
| `runners` | No | Defaults to reducer runner |
|
|
115
108
|
| `when` | Yes | Async action flow |
|
|
116
109
|
| `then` | Yes | Assertions over state, view, and history |
|
|
117
110
|
|
|
@@ -1,363 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
clearCredentials,
|
|
4
|
-
withCredentialLock
|
|
5
|
-
} from "./chunk-IWB4L2HV.mjs";
|
|
6
|
-
|
|
7
|
-
// src/build-target.ts
|
|
8
|
-
var injectedBuildChannel = true ? "development" : void 0;
|
|
9
|
-
var BUILD_CHANNEL = injectedBuildChannel === "published" ? "published" : "development";
|
|
10
|
-
var IS_PUBLISHED_BUILD = BUILD_CHANNEL === "published";
|
|
11
|
-
var PUBLISHED_ENVIRONMENT = "prod";
|
|
12
|
-
|
|
13
|
-
// src/auth/clerk-oauth.ts
|
|
14
|
-
import crypto from "crypto";
|
|
15
|
-
async function refreshClerkOAuthToken(input) {
|
|
16
|
-
const { clientId, tokenUrl } = assertConfigured(input.config);
|
|
17
|
-
const body = new URLSearchParams({
|
|
18
|
-
grant_type: "refresh_token",
|
|
19
|
-
client_id: clientId,
|
|
20
|
-
refresh_token: input.refreshToken
|
|
21
|
-
});
|
|
22
|
-
return requestClerkToken(tokenUrl, body);
|
|
23
|
-
}
|
|
24
|
-
function assertConfigured(config) {
|
|
25
|
-
const issuer = config.issuer?.trim().replace(/\/$/, "");
|
|
26
|
-
const clientId = config.clientId?.trim();
|
|
27
|
-
if (!issuer || !clientId) {
|
|
28
|
-
throw new Error(
|
|
29
|
-
[
|
|
30
|
-
"Clerk OAuth CLI is not configured for this environment.",
|
|
31
|
-
"The CLI expects first-party environments to be configured in its built-in registry.",
|
|
32
|
-
"If this environment has no registered public Clerk OAuth client, create one and release a CLI with its client id.",
|
|
33
|
-
"For emergency overrides, set the environment-specific DREAMBOARD_<ENV>_CLERK_OAUTH_* variables or DREAMBOARD_CLERK_OAUTH_*.",
|
|
34
|
-
"For local harness auth, use `pnpm auth:local` or the auto-bootstrapped local harness flows instead."
|
|
35
|
-
].join(" ")
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
return {
|
|
39
|
-
issuer,
|
|
40
|
-
clientId,
|
|
41
|
-
tokenUrl: config.tokenUrl?.trim() || new URL("/oauth/token", issuer).toString(),
|
|
42
|
-
scope: config.scope?.trim() || void 0
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
async function requestClerkToken(tokenUrl, body) {
|
|
46
|
-
const response = await fetch(tokenUrl, {
|
|
47
|
-
method: "POST",
|
|
48
|
-
headers: {
|
|
49
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
50
|
-
Accept: "application/json"
|
|
51
|
-
},
|
|
52
|
-
body
|
|
53
|
-
});
|
|
54
|
-
if (!response.ok) {
|
|
55
|
-
const detail = await response.text();
|
|
56
|
-
throw new Error(
|
|
57
|
-
`Clerk OAuth token request failed (${response.status}): ${detail}`
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
const payload = await response.json();
|
|
61
|
-
if (typeof payload.access_token !== "string") {
|
|
62
|
-
throw new Error("Clerk OAuth token response did not include access_token.");
|
|
63
|
-
}
|
|
64
|
-
if (typeof payload.refresh_token !== "string") {
|
|
65
|
-
throw new Error(
|
|
66
|
-
"Clerk OAuth token response did not include refresh_token."
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
const expiresAt = typeof payload.expires_in === "number" ? new Date(Date.now() + payload.expires_in * 1e3).toISOString() : void 0;
|
|
70
|
-
return {
|
|
71
|
-
accessToken: payload.access_token,
|
|
72
|
-
refreshToken: payload.refresh_token,
|
|
73
|
-
expiresAt,
|
|
74
|
-
tokenUrl
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// src/auth/token-exchange.ts
|
|
79
|
-
async function exchangeDreamboardUserToken(input) {
|
|
80
|
-
const fetchImpl = input.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
81
|
-
const response = await fetchImpl(
|
|
82
|
-
new URL("/api/auth/token-exchange", input.apiBaseUrl),
|
|
83
|
-
{
|
|
84
|
-
method: "POST",
|
|
85
|
-
headers: {
|
|
86
|
-
Authorization: `Bearer ${input.clerkAccessToken}`,
|
|
87
|
-
"Content-Type": "application/json",
|
|
88
|
-
Accept: "application/json"
|
|
89
|
-
},
|
|
90
|
-
body: JSON.stringify({ audience: input.audience })
|
|
91
|
-
}
|
|
92
|
-
);
|
|
93
|
-
if (!response.ok) {
|
|
94
|
-
throw new Error(
|
|
95
|
-
`Dreamboard token exchange failed (${response.status}). Run \`dreamboard auth login\` to authenticate again.`
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
const payload = await response.json();
|
|
99
|
-
if (typeof payload.accessToken !== "string" || payload.accessToken === "") {
|
|
100
|
-
throw new Error("Dreamboard token exchange response omitted accessToken.");
|
|
101
|
-
}
|
|
102
|
-
if (payload.tokenType !== "Bearer") {
|
|
103
|
-
throw new Error(
|
|
104
|
-
"Dreamboard token exchange response had invalid tokenType."
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
if (payload.audience !== input.audience) {
|
|
108
|
-
throw new Error("Dreamboard token exchange response had wrong audience.");
|
|
109
|
-
}
|
|
110
|
-
const expiresIn = typeof payload.expiresIn === "number" && Number.isFinite(payload.expiresIn) ? payload.expiresIn : void 0;
|
|
111
|
-
return {
|
|
112
|
-
accessToken: payload.accessToken,
|
|
113
|
-
tokenType: "Bearer",
|
|
114
|
-
audience: input.audience,
|
|
115
|
-
expiresIn,
|
|
116
|
-
expiresAt: expiresIn === void 0 ? void 0 : new Date(Date.now() + expiresIn * 1e3).toISOString()
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// src/auth/user-token-manager.ts
|
|
121
|
-
var TOKEN_REFRESH_WINDOW_MS = 60 * 1e3;
|
|
122
|
-
function createUserTokenManager(config) {
|
|
123
|
-
return {
|
|
124
|
-
async resolveApiToken() {
|
|
125
|
-
const localOrInjected = resolveNonStoredToken(config, "dreamboard-api");
|
|
126
|
-
if (localOrInjected) return localOrInjected;
|
|
127
|
-
if (!usesStoredSession(config)) return null;
|
|
128
|
-
return withCredentialLock(async (ops) => {
|
|
129
|
-
const stored = await ops.read();
|
|
130
|
-
const apiToken = freshStoredApiToken(stored);
|
|
131
|
-
if (apiToken) return apiToken;
|
|
132
|
-
const clerk = await resolveFreshClerkAccessToken(config, stored);
|
|
133
|
-
const exchanged = await exchangeDreamboardUserToken({
|
|
134
|
-
apiBaseUrl: config.apiBaseUrl,
|
|
135
|
-
clerkAccessToken: clerk.accessToken,
|
|
136
|
-
audience: "dreamboard-api"
|
|
137
|
-
});
|
|
138
|
-
await ops.writeFull({
|
|
139
|
-
...clerk,
|
|
140
|
-
dreamboardApiToken: exchanged.accessToken,
|
|
141
|
-
dreamboardApiExpiresAt: exchanged.expiresAt
|
|
142
|
-
});
|
|
143
|
-
return {
|
|
144
|
-
token: exchanged.accessToken,
|
|
145
|
-
expiresAt: exchanged.expiresAt,
|
|
146
|
-
audience: "dreamboard-api"
|
|
147
|
-
};
|
|
148
|
-
});
|
|
149
|
-
},
|
|
150
|
-
async resolveGitToken() {
|
|
151
|
-
if (!usesStoredSession(config) && config.authToken) {
|
|
152
|
-
const exchanged = await exchangeDreamboardUserToken({
|
|
153
|
-
apiBaseUrl: config.apiBaseUrl,
|
|
154
|
-
clerkAccessToken: config.authToken,
|
|
155
|
-
audience: "dreamboard-git"
|
|
156
|
-
});
|
|
157
|
-
return {
|
|
158
|
-
token: exchanged.accessToken,
|
|
159
|
-
expiresAt: exchanged.expiresAt,
|
|
160
|
-
audience: "dreamboard-git"
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
if (!usesStoredSession(config)) {
|
|
164
|
-
throw new Error(
|
|
165
|
-
"Missing Dreamboard session. Run `dreamboard auth login` to authenticate."
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
return withCredentialLock(async (ops) => {
|
|
169
|
-
const stored = await ops.read();
|
|
170
|
-
const clerk = await resolveFreshClerkAccessToken(config, stored);
|
|
171
|
-
const exchanged = await exchangeDreamboardUserToken({
|
|
172
|
-
apiBaseUrl: config.apiBaseUrl,
|
|
173
|
-
clerkAccessToken: clerk.accessToken,
|
|
174
|
-
audience: "dreamboard-git"
|
|
175
|
-
});
|
|
176
|
-
await ops.writeFull(clerk);
|
|
177
|
-
return {
|
|
178
|
-
token: exchanged.accessToken,
|
|
179
|
-
expiresAt: exchanged.expiresAt,
|
|
180
|
-
audience: "dreamboard-git"
|
|
181
|
-
};
|
|
182
|
-
});
|
|
183
|
-
},
|
|
184
|
-
async logout() {
|
|
185
|
-
await clearCredentials("user_token_manager_logout");
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
function resolveNonStoredToken(config, audience) {
|
|
190
|
-
if (usesStoredSession(config)) return null;
|
|
191
|
-
if (!config.authToken) return null;
|
|
192
|
-
return {
|
|
193
|
-
token: config.authToken,
|
|
194
|
-
expiresAt: config.tokenExpiresAt,
|
|
195
|
-
audience
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
function freshStoredApiToken(stored) {
|
|
199
|
-
if (!stored?.dreamboardApiToken) return null;
|
|
200
|
-
if (isFresh(stored.dreamboardApiExpiresAt, stored.dreamboardApiToken)) {
|
|
201
|
-
return {
|
|
202
|
-
token: stored.dreamboardApiToken,
|
|
203
|
-
expiresAt: stored.dreamboardApiExpiresAt,
|
|
204
|
-
audience: "dreamboard-api"
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
return null;
|
|
208
|
-
}
|
|
209
|
-
async function resolveFreshClerkAccessToken(config, stored) {
|
|
210
|
-
const accessToken = stored?.accessToken ?? config.clerkAccessToken;
|
|
211
|
-
const refreshToken = stored?.refreshToken ?? config.refreshToken;
|
|
212
|
-
const tokenExpiresAt = stored?.tokenExpiresAt ?? config.clerkAccessExpiresAt;
|
|
213
|
-
if (!refreshToken) {
|
|
214
|
-
throw new Error(
|
|
215
|
-
"Stored Dreamboard session is missing its refresh token. Run `dreamboard auth login` to authenticate again."
|
|
216
|
-
);
|
|
217
|
-
}
|
|
218
|
-
if (accessToken && isFresh(tokenExpiresAt, accessToken)) {
|
|
219
|
-
return {
|
|
220
|
-
accessToken,
|
|
221
|
-
refreshToken,
|
|
222
|
-
tokenExpiresAt,
|
|
223
|
-
dreamboardApiToken: stored?.dreamboardApiToken,
|
|
224
|
-
dreamboardApiExpiresAt: stored?.dreamboardApiExpiresAt,
|
|
225
|
-
clerkOAuthIssuer: stored?.clerkOAuthIssuer ?? config.clerkOAuthIssuer,
|
|
226
|
-
clerkOAuthClientId: stored?.clerkOAuthClientId ?? config.clerkOAuthClientId,
|
|
227
|
-
clerkOAuthTokenUrl: stored?.clerkOAuthTokenUrl ?? config.clerkOAuthTokenUrl,
|
|
228
|
-
environment: stored?.environment ?? config.environment
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
const payload = await refreshClerkOAuthToken({
|
|
232
|
-
config: {
|
|
233
|
-
issuer: stored?.clerkOAuthIssuer ?? config.clerkOAuthIssuer,
|
|
234
|
-
clientId: stored?.clerkOAuthClientId ?? config.clerkOAuthClientId,
|
|
235
|
-
tokenUrl: stored?.clerkOAuthTokenUrl ?? config.clerkOAuthTokenUrl
|
|
236
|
-
},
|
|
237
|
-
refreshToken
|
|
238
|
-
});
|
|
239
|
-
return {
|
|
240
|
-
accessToken: payload.accessToken,
|
|
241
|
-
refreshToken: payload.refreshToken,
|
|
242
|
-
tokenExpiresAt: payload.expiresAt,
|
|
243
|
-
clerkOAuthIssuer: stored?.clerkOAuthIssuer ?? config.clerkOAuthIssuer,
|
|
244
|
-
clerkOAuthClientId: stored?.clerkOAuthClientId ?? config.clerkOAuthClientId,
|
|
245
|
-
clerkOAuthTokenUrl: payload.tokenUrl,
|
|
246
|
-
environment: stored?.environment ?? config.environment
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
function isFresh(expiresAt, token) {
|
|
250
|
-
const expiry = expiresAt ? new Date(expiresAt) : getJwtExpiry(token);
|
|
251
|
-
return expiry !== null && Number.isFinite(expiry.getTime()) && expiry.getTime() > Date.now() + TOKEN_REFRESH_WINDOW_MS;
|
|
252
|
-
}
|
|
253
|
-
function getJwtExpiry(accessToken) {
|
|
254
|
-
if (!accessToken) return null;
|
|
255
|
-
const parts = accessToken.split(".");
|
|
256
|
-
if (parts.length !== 3) return null;
|
|
257
|
-
try {
|
|
258
|
-
const payload = JSON.parse(
|
|
259
|
-
Buffer.from(parts[1], "base64url").toString("utf8")
|
|
260
|
-
);
|
|
261
|
-
if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) {
|
|
262
|
-
return null;
|
|
263
|
-
}
|
|
264
|
-
return new Date(payload.exp * 1e3);
|
|
265
|
-
} catch {
|
|
266
|
-
return null;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
function usesStoredSession(config) {
|
|
270
|
-
return config.refreshTokenSource === "global";
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// src/config/local-harness-auth.ts
|
|
274
|
-
import { createHmac, randomUUID } from "crypto";
|
|
275
|
-
var DEFAULT_SUBJECT = "harness-smoke-local@dreamboard.local";
|
|
276
|
-
var DEFAULT_ISSUER = "dreamboard-local-harness";
|
|
277
|
-
var DEFAULT_SECRET = "dreamboard-local-harness-token-secret";
|
|
278
|
-
var LOCAL_AWS_ISSUER = "dreamboard-local-aws-harness";
|
|
279
|
-
var LOCAL_AWS_SECRET = "dreamboard-local-aws-harness-token-secret";
|
|
280
|
-
var DEFAULT_TTL_SECONDS = 8 * 60 * 60;
|
|
281
|
-
var mintedTokens = /* @__PURE__ */ new Map();
|
|
282
|
-
function resolveLocalHarnessAccessToken(config) {
|
|
283
|
-
if (IS_PUBLISHED_BUILD || config.environment !== "local") {
|
|
284
|
-
return void 0;
|
|
285
|
-
}
|
|
286
|
-
const profile = inferLocalHarnessProfile(config);
|
|
287
|
-
if (config.authToken && (profile !== "local-aws" || isExplicitTokenSource(config.authTokenSource))) {
|
|
288
|
-
return void 0;
|
|
289
|
-
}
|
|
290
|
-
const cacheKey = [
|
|
291
|
-
profile,
|
|
292
|
-
process.env.LOCAL_HARNESS_TOKEN_ISSUER ?? "",
|
|
293
|
-
process.env.LOCAL_HARNESS_TOKEN_SECRET ?? "",
|
|
294
|
-
process.env.LOCAL_HARNESS_SUBJECT ?? "",
|
|
295
|
-
process.env.HARNESS_USER_EMAIL ?? "",
|
|
296
|
-
process.env.LOCAL_HARNESS_EMAIL ?? "",
|
|
297
|
-
process.env.LOCAL_HARNESS_TOKEN_TTL_SECONDS ?? ""
|
|
298
|
-
].join("\0");
|
|
299
|
-
const cached = mintedTokens.get(cacheKey);
|
|
300
|
-
if (cached) return cached;
|
|
301
|
-
const token = mintLocalHarnessToken(profile);
|
|
302
|
-
mintedTokens.set(cacheKey, token);
|
|
303
|
-
return token;
|
|
304
|
-
}
|
|
305
|
-
function isExplicitTokenSource(source) {
|
|
306
|
-
return source === "flag" || source === "env" || source === "agent-env";
|
|
307
|
-
}
|
|
308
|
-
function inferLocalHarnessProfile(config) {
|
|
309
|
-
return isLocalAwsUrl(config.apiBaseUrl) || isLocalAwsUrl(config.webBaseUrl) ? "local-aws" : "local";
|
|
310
|
-
}
|
|
311
|
-
function mintLocalHarnessToken(profile) {
|
|
312
|
-
const subject = envValue(process.env.LOCAL_HARNESS_SUBJECT) ?? envValue(process.env.HARNESS_USER_EMAIL) ?? DEFAULT_SUBJECT;
|
|
313
|
-
const email = envValue(process.env.LOCAL_HARNESS_EMAIL) ?? (subject.includes("@") ? subject : void 0);
|
|
314
|
-
const issuer = envValue(process.env.LOCAL_HARNESS_TOKEN_ISSUER) ?? (profile === "local-aws" ? LOCAL_AWS_ISSUER : DEFAULT_ISSUER);
|
|
315
|
-
const secret = envValue(process.env.LOCAL_HARNESS_TOKEN_SECRET) ?? (profile === "local-aws" ? LOCAL_AWS_SECRET : DEFAULT_SECRET);
|
|
316
|
-
const ttlSeconds = Number(
|
|
317
|
-
envValue(process.env.LOCAL_HARNESS_TOKEN_TTL_SECONDS) ?? String(DEFAULT_TTL_SECONDS)
|
|
318
|
-
);
|
|
319
|
-
if (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0) {
|
|
320
|
-
throw new Error(
|
|
321
|
-
"LOCAL_HARNESS_TOKEN_TTL_SECONDS must be a positive number."
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
325
|
-
const payload = {
|
|
326
|
-
typ: "local_harness_access",
|
|
327
|
-
dreamboard_provider: "local-harness",
|
|
328
|
-
dreamboard_provider_subject: subject,
|
|
329
|
-
...email ? { email } : {},
|
|
330
|
-
iss: issuer,
|
|
331
|
-
sub: subject,
|
|
332
|
-
iat: now,
|
|
333
|
-
exp: now + Math.floor(ttlSeconds),
|
|
334
|
-
jti: randomUUID()
|
|
335
|
-
};
|
|
336
|
-
const headerPart = base64UrlJson({ alg: "HS256", typ: "JWT" });
|
|
337
|
-
const payloadPart = base64UrlJson(payload);
|
|
338
|
-
const signature = createHmac("sha256", secret).update(`${headerPart}.${payloadPart}`).digest("base64url");
|
|
339
|
-
return `${headerPart}.${payloadPart}.${signature}`;
|
|
340
|
-
}
|
|
341
|
-
function base64UrlJson(value) {
|
|
342
|
-
return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
|
|
343
|
-
}
|
|
344
|
-
function envValue(raw) {
|
|
345
|
-
return typeof raw === "string" && raw.trim().length > 0 ? raw.trim() : void 0;
|
|
346
|
-
}
|
|
347
|
-
function isLocalAwsUrl(rawUrl) {
|
|
348
|
-
if (!rawUrl) return false;
|
|
349
|
-
try {
|
|
350
|
-
const url = new URL(rawUrl);
|
|
351
|
-
return (url.hostname === "localhost" || url.hostname === "127.0.0.1") && (url.port === "18080" || url.port === "8088");
|
|
352
|
-
} catch {
|
|
353
|
-
return false;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
export {
|
|
358
|
-
IS_PUBLISHED_BUILD,
|
|
359
|
-
PUBLISHED_ENVIRONMENT,
|
|
360
|
-
createUserTokenManager,
|
|
361
|
-
resolveLocalHarnessAccessToken
|
|
362
|
-
};
|
|
363
|
-
//# sourceMappingURL=chunk-B7M2TJSP.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/build-target.ts","../../src/auth/clerk-oauth.ts","../../src/auth/token-exchange.ts","../../src/auth/user-token-manager.ts","../../src/config/local-harness-auth.ts"],"sourcesContent":["declare const __DREAMBOARD_BUILD_CHANNEL__: string | undefined;\n\nconst injectedBuildChannel =\n typeof __DREAMBOARD_BUILD_CHANNEL__ === \"string\"\n ? __DREAMBOARD_BUILD_CHANNEL__\n : undefined;\n\nexport const BUILD_CHANNEL =\n injectedBuildChannel === \"published\" ? \"published\" : \"development\";\n\nexport const IS_PUBLISHED_BUILD = BUILD_CHANNEL === \"published\";\nexport const PUBLISHED_ENVIRONMENT = \"prod\" as const;\n","import crypto from \"node:crypto\";\n\nexport type ClerkOAuthConfig = {\n issuer?: string;\n clientId?: string;\n tokenUrl?: string;\n scope?: string;\n};\n\nexport type ClerkOAuthTokenResponse = {\n accessToken: string;\n refreshToken: string;\n expiresAt?: string;\n tokenUrl: string;\n};\n\nexport function createPkcePair(): { verifier: string; challenge: string } {\n const verifier = base64Url(crypto.randomBytes(32));\n const challenge = base64Url(\n crypto.createHash(\"sha256\").update(verifier).digest(),\n );\n return { verifier, challenge };\n}\n\nexport function buildClerkAuthorizationUrl(input: {\n config: ClerkOAuthConfig;\n redirectUri: string;\n state: string;\n codeChallenge: string;\n}): URL {\n const { issuer, clientId, scope } = assertConfigured(input.config);\n const url = new URL(\"/oauth/authorize\", issuer);\n url.searchParams.set(\"response_type\", \"code\");\n url.searchParams.set(\"client_id\", clientId);\n url.searchParams.set(\"redirect_uri\", input.redirectUri);\n url.searchParams.set(\"state\", input.state);\n url.searchParams.set(\"code_challenge\", input.codeChallenge);\n url.searchParams.set(\"code_challenge_method\", \"S256\");\n url.searchParams.set(\"scope\", scope ?? \"openid profile email offline_access\");\n return url;\n}\n\nexport async function exchangeClerkOAuthCode(input: {\n config: ClerkOAuthConfig;\n code: string;\n redirectUri: string;\n codeVerifier: string;\n}): Promise<ClerkOAuthTokenResponse> {\n const { clientId, tokenUrl } = assertConfigured(input.config);\n const body = new URLSearchParams({\n grant_type: \"authorization_code\",\n client_id: clientId,\n code: input.code,\n redirect_uri: input.redirectUri,\n code_verifier: input.codeVerifier,\n });\n return requestClerkToken(tokenUrl, body);\n}\n\nexport async function refreshClerkOAuthToken(input: {\n config: ClerkOAuthConfig;\n refreshToken: string;\n}): Promise<ClerkOAuthTokenResponse> {\n const { clientId, tokenUrl } = assertConfigured(input.config);\n const body = new URLSearchParams({\n grant_type: \"refresh_token\",\n client_id: clientId,\n refresh_token: input.refreshToken,\n });\n return requestClerkToken(tokenUrl, body);\n}\n\nfunction assertConfigured(config: ClerkOAuthConfig): {\n issuer: string;\n clientId: string;\n tokenUrl: string;\n scope?: string;\n} {\n const issuer = config.issuer?.trim().replace(/\\/$/, \"\");\n const clientId = config.clientId?.trim();\n if (!issuer || !clientId) {\n throw new Error(\n [\n \"Clerk OAuth CLI is not configured for this environment.\",\n \"The CLI expects first-party environments to be configured in its built-in registry.\",\n \"If this environment has no registered public Clerk OAuth client, create one and release a CLI with its client id.\",\n \"For emergency overrides, set the environment-specific DREAMBOARD_<ENV>_CLERK_OAUTH_* variables or DREAMBOARD_CLERK_OAUTH_*.\",\n \"For local harness auth, use `pnpm auth:local` or the auto-bootstrapped local harness flows instead.\",\n ].join(\" \"),\n );\n }\n return {\n issuer,\n clientId,\n tokenUrl:\n config.tokenUrl?.trim() || new URL(\"/oauth/token\", issuer).toString(),\n scope: config.scope?.trim() || undefined,\n };\n}\n\nasync function requestClerkToken(\n tokenUrl: string,\n body: URLSearchParams,\n): Promise<ClerkOAuthTokenResponse> {\n const response = await fetch(tokenUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n Accept: \"application/json\",\n },\n body,\n });\n if (!response.ok) {\n const detail = await response.text();\n throw new Error(\n `Clerk OAuth token request failed (${response.status}): ${detail}`,\n );\n }\n const payload = (await response.json()) as {\n access_token?: unknown;\n refresh_token?: unknown;\n expires_in?: unknown;\n };\n if (typeof payload.access_token !== \"string\") {\n throw new Error(\"Clerk OAuth token response did not include access_token.\");\n }\n if (typeof payload.refresh_token !== \"string\") {\n throw new Error(\n \"Clerk OAuth token response did not include refresh_token.\",\n );\n }\n const expiresAt =\n typeof payload.expires_in === \"number\"\n ? new Date(Date.now() + payload.expires_in * 1000).toISOString()\n : undefined;\n return {\n accessToken: payload.access_token,\n refreshToken: payload.refresh_token,\n expiresAt,\n tokenUrl,\n };\n}\n\nfunction base64Url(bytes: Buffer): string {\n return bytes\n .toString(\"base64\")\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/g, \"\");\n}\n","export type DreamboardTokenAudience = \"dreamboard-api\" | \"dreamboard-git\";\n\nexport type DreamboardTokenResponse = {\n accessToken: string;\n tokenType: \"Bearer\";\n audience: DreamboardTokenAudience;\n expiresIn?: number;\n expiresAt?: string;\n};\n\nexport async function exchangeDreamboardUserToken(input: {\n apiBaseUrl: string;\n clerkAccessToken: string;\n audience: DreamboardTokenAudience;\n fetchImpl?: typeof fetch;\n}): Promise<DreamboardTokenResponse> {\n const fetchImpl = input.fetchImpl ?? globalThis.fetch.bind(globalThis);\n const response = await fetchImpl(\n new URL(\"/api/auth/token-exchange\", input.apiBaseUrl),\n {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${input.clerkAccessToken}`,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n body: JSON.stringify({ audience: input.audience }),\n },\n );\n\n if (!response.ok) {\n throw new Error(\n `Dreamboard token exchange failed (${response.status}). Run \\`dreamboard auth login\\` to authenticate again.`,\n );\n }\n\n const payload = (await response.json()) as {\n accessToken?: unknown;\n tokenType?: unknown;\n audience?: unknown;\n expiresIn?: unknown;\n };\n\n if (typeof payload.accessToken !== \"string\" || payload.accessToken === \"\") {\n throw new Error(\"Dreamboard token exchange response omitted accessToken.\");\n }\n if (payload.tokenType !== \"Bearer\") {\n throw new Error(\n \"Dreamboard token exchange response had invalid tokenType.\",\n );\n }\n if (payload.audience !== input.audience) {\n throw new Error(\"Dreamboard token exchange response had wrong audience.\");\n }\n\n const expiresIn =\n typeof payload.expiresIn === \"number\" && Number.isFinite(payload.expiresIn)\n ? payload.expiresIn\n : undefined;\n\n return {\n accessToken: payload.accessToken,\n tokenType: \"Bearer\",\n audience: input.audience,\n expiresIn,\n expiresAt:\n expiresIn === undefined\n ? undefined\n : new Date(Date.now() + expiresIn * 1000).toISOString(),\n };\n}\n","import type {\n Credentials,\n StoredSessionSnapshot,\n} from \"../config/credential-store.js\";\nimport {\n clearCredentials,\n withCredentialLock,\n} from \"../config/credential-store.js\";\nimport type {\n AccessToken,\n UserTokenManager,\n} from \"@dreamboard-games/cli-core\";\nimport { refreshClerkOAuthToken } from \"./clerk-oauth.js\";\nimport {\n exchangeDreamboardUserToken,\n type DreamboardTokenAudience,\n} from \"./token-exchange.js\";\nimport type { ResolvedConfig } from \"../types.js\";\n\nconst TOKEN_REFRESH_WINDOW_MS = 60 * 1000;\n\nexport function createUserTokenManager(\n config: ResolvedConfig,\n): UserTokenManager {\n return {\n async resolveApiToken() {\n const localOrInjected = resolveNonStoredToken(config, \"dreamboard-api\");\n if (localOrInjected) return localOrInjected;\n\n if (!usesStoredSession(config)) return null;\n\n return withCredentialLock(async (ops) => {\n const stored = await ops.read();\n const apiToken = freshStoredApiToken(stored);\n if (apiToken) return apiToken;\n\n const clerk = await resolveFreshClerkAccessToken(config, stored);\n const exchanged = await exchangeDreamboardUserToken({\n apiBaseUrl: config.apiBaseUrl,\n clerkAccessToken: clerk.accessToken,\n audience: \"dreamboard-api\",\n });\n\n await ops.writeFull({\n ...clerk,\n dreamboardApiToken: exchanged.accessToken,\n dreamboardApiExpiresAt: exchanged.expiresAt,\n });\n\n return {\n token: exchanged.accessToken,\n expiresAt: exchanged.expiresAt,\n audience: \"dreamboard-api\",\n };\n });\n },\n\n async resolveGitToken() {\n if (!usesStoredSession(config) && config.authToken) {\n const exchanged = await exchangeDreamboardUserToken({\n apiBaseUrl: config.apiBaseUrl,\n clerkAccessToken: config.authToken,\n audience: \"dreamboard-git\",\n });\n return {\n token: exchanged.accessToken,\n expiresAt: exchanged.expiresAt,\n audience: \"dreamboard-git\",\n };\n }\n\n if (!usesStoredSession(config)) {\n throw new Error(\n \"Missing Dreamboard session. Run `dreamboard auth login` to authenticate.\",\n );\n }\n\n return withCredentialLock(async (ops) => {\n const stored = await ops.read();\n const clerk = await resolveFreshClerkAccessToken(config, stored);\n const exchanged = await exchangeDreamboardUserToken({\n apiBaseUrl: config.apiBaseUrl,\n clerkAccessToken: clerk.accessToken,\n audience: \"dreamboard-git\",\n });\n\n await ops.writeFull(clerk);\n\n return {\n token: exchanged.accessToken,\n expiresAt: exchanged.expiresAt,\n audience: \"dreamboard-git\",\n };\n });\n },\n\n async logout() {\n await clearCredentials(\"user_token_manager_logout\");\n },\n };\n}\n\nfunction resolveNonStoredToken(\n config: ResolvedConfig,\n audience: DreamboardTokenAudience,\n): AccessToken | null {\n if (usesStoredSession(config)) return null;\n if (!config.authToken) return null;\n return {\n token: config.authToken,\n expiresAt: config.tokenExpiresAt,\n audience,\n };\n}\n\nfunction freshStoredApiToken(\n stored: StoredSessionSnapshot | null,\n): AccessToken | null {\n if (!stored?.dreamboardApiToken) return null;\n if (isFresh(stored.dreamboardApiExpiresAt, stored.dreamboardApiToken)) {\n return {\n token: stored.dreamboardApiToken,\n expiresAt: stored.dreamboardApiExpiresAt,\n audience: \"dreamboard-api\",\n };\n }\n return null;\n}\n\nasync function resolveFreshClerkAccessToken(\n config: ResolvedConfig,\n stored: StoredSessionSnapshot | null,\n): Promise<Credentials> {\n const accessToken = stored?.accessToken ?? config.clerkAccessToken;\n const refreshToken = stored?.refreshToken ?? config.refreshToken;\n const tokenExpiresAt = stored?.tokenExpiresAt ?? config.clerkAccessExpiresAt;\n\n if (!refreshToken) {\n throw new Error(\n \"Stored Dreamboard session is missing its refresh token. Run `dreamboard auth login` to authenticate again.\",\n );\n }\n\n if (accessToken && isFresh(tokenExpiresAt, accessToken)) {\n return {\n accessToken,\n refreshToken,\n tokenExpiresAt,\n dreamboardApiToken: stored?.dreamboardApiToken,\n dreamboardApiExpiresAt: stored?.dreamboardApiExpiresAt,\n clerkOAuthIssuer: stored?.clerkOAuthIssuer ?? config.clerkOAuthIssuer,\n clerkOAuthClientId:\n stored?.clerkOAuthClientId ?? config.clerkOAuthClientId,\n clerkOAuthTokenUrl:\n stored?.clerkOAuthTokenUrl ?? config.clerkOAuthTokenUrl,\n environment: stored?.environment ?? config.environment,\n };\n }\n\n const payload = await refreshClerkOAuthToken({\n config: {\n issuer: stored?.clerkOAuthIssuer ?? config.clerkOAuthIssuer,\n clientId: stored?.clerkOAuthClientId ?? config.clerkOAuthClientId,\n tokenUrl: stored?.clerkOAuthTokenUrl ?? config.clerkOAuthTokenUrl,\n },\n refreshToken,\n });\n\n return {\n accessToken: payload.accessToken,\n refreshToken: payload.refreshToken,\n tokenExpiresAt: payload.expiresAt,\n clerkOAuthIssuer: stored?.clerkOAuthIssuer ?? config.clerkOAuthIssuer,\n clerkOAuthClientId: stored?.clerkOAuthClientId ?? config.clerkOAuthClientId,\n clerkOAuthTokenUrl: payload.tokenUrl,\n environment: stored?.environment ?? config.environment,\n };\n}\n\nfunction isFresh(expiresAt: string | undefined, token: string): boolean {\n const expiry = expiresAt ? new Date(expiresAt) : getJwtExpiry(token);\n return (\n expiry !== null &&\n Number.isFinite(expiry.getTime()) &&\n expiry.getTime() > Date.now() + TOKEN_REFRESH_WINDOW_MS\n );\n}\n\nfunction getJwtExpiry(accessToken: string | undefined): Date | null {\n if (!accessToken) return null;\n const parts = accessToken.split(\".\");\n if (parts.length !== 3) return null;\n try {\n const payload = JSON.parse(\n Buffer.from(parts[1]!, \"base64url\").toString(\"utf8\"),\n ) as { exp?: unknown };\n if (typeof payload.exp !== \"number\" || !Number.isFinite(payload.exp)) {\n return null;\n }\n return new Date(payload.exp * 1000);\n } catch {\n return null;\n }\n}\n\nfunction usesStoredSession(config: ResolvedConfig): boolean {\n return config.refreshTokenSource === \"global\";\n}\n","import { createHmac, randomUUID } from \"node:crypto\";\nimport type { ResolvedConfig } from \"../types.js\";\nimport { IS_PUBLISHED_BUILD } from \"../build-target.js\";\n\ntype LocalHarnessProfile = \"local\" | \"local-aws\";\n\nconst DEFAULT_SUBJECT = \"harness-smoke-local@dreamboard.local\";\nconst DEFAULT_ISSUER = \"dreamboard-local-harness\";\nconst DEFAULT_SECRET = \"dreamboard-local-harness-token-secret\";\nconst LOCAL_AWS_ISSUER = \"dreamboard-local-aws-harness\";\nconst LOCAL_AWS_SECRET = \"dreamboard-local-aws-harness-token-secret\";\nconst DEFAULT_TTL_SECONDS = 8 * 60 * 60;\n\nconst mintedTokens = new Map<string, string>();\n\nexport function resolveLocalHarnessAccessToken(\n config: ResolvedConfig,\n): string | undefined {\n if (IS_PUBLISHED_BUILD || config.environment !== \"local\") {\n return undefined;\n }\n\n const profile = inferLocalHarnessProfile(config);\n if (\n config.authToken &&\n (profile !== \"local-aws\" || isExplicitTokenSource(config.authTokenSource))\n ) {\n return undefined;\n }\n\n const cacheKey = [\n profile,\n process.env.LOCAL_HARNESS_TOKEN_ISSUER ?? \"\",\n process.env.LOCAL_HARNESS_TOKEN_SECRET ?? \"\",\n process.env.LOCAL_HARNESS_SUBJECT ?? \"\",\n process.env.HARNESS_USER_EMAIL ?? \"\",\n process.env.LOCAL_HARNESS_EMAIL ?? \"\",\n process.env.LOCAL_HARNESS_TOKEN_TTL_SECONDS ?? \"\",\n ].join(\"\\0\");\n const cached = mintedTokens.get(cacheKey);\n if (cached) return cached;\n\n const token = mintLocalHarnessToken(profile);\n mintedTokens.set(cacheKey, token);\n return token;\n}\n\nfunction isExplicitTokenSource(\n source: ResolvedConfig[\"authTokenSource\"],\n): boolean {\n return source === \"flag\" || source === \"env\" || source === \"agent-env\";\n}\n\nexport function inferLocalHarnessProfile(\n config: Pick<ResolvedConfig, \"apiBaseUrl\" | \"webBaseUrl\">,\n): LocalHarnessProfile {\n return isLocalAwsUrl(config.apiBaseUrl) || isLocalAwsUrl(config.webBaseUrl)\n ? \"local-aws\"\n : \"local\";\n}\n\nfunction mintLocalHarnessToken(profile: LocalHarnessProfile): string {\n const subject =\n envValue(process.env.LOCAL_HARNESS_SUBJECT) ??\n envValue(process.env.HARNESS_USER_EMAIL) ??\n DEFAULT_SUBJECT;\n const email =\n envValue(process.env.LOCAL_HARNESS_EMAIL) ??\n (subject.includes(\"@\") ? subject : undefined);\n const issuer =\n envValue(process.env.LOCAL_HARNESS_TOKEN_ISSUER) ??\n (profile === \"local-aws\" ? LOCAL_AWS_ISSUER : DEFAULT_ISSUER);\n const secret =\n envValue(process.env.LOCAL_HARNESS_TOKEN_SECRET) ??\n (profile === \"local-aws\" ? LOCAL_AWS_SECRET : DEFAULT_SECRET);\n const ttlSeconds = Number(\n envValue(process.env.LOCAL_HARNESS_TOKEN_TTL_SECONDS) ??\n String(DEFAULT_TTL_SECONDS),\n );\n if (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0) {\n throw new Error(\n \"LOCAL_HARNESS_TOKEN_TTL_SECONDS must be a positive number.\",\n );\n }\n\n const now = Math.floor(Date.now() / 1000);\n const payload = {\n typ: \"local_harness_access\",\n dreamboard_provider: \"local-harness\",\n dreamboard_provider_subject: subject,\n ...(email ? { email } : {}),\n iss: issuer,\n sub: subject,\n iat: now,\n exp: now + Math.floor(ttlSeconds),\n jti: randomUUID(),\n };\n\n const headerPart = base64UrlJson({ alg: \"HS256\", typ: \"JWT\" });\n const payloadPart = base64UrlJson(payload);\n const signature = createHmac(\"sha256\", secret)\n .update(`${headerPart}.${payloadPart}`)\n .digest(\"base64url\");\n return `${headerPart}.${payloadPart}.${signature}`;\n}\n\nfunction base64UrlJson(value: unknown): string {\n return Buffer.from(JSON.stringify(value), \"utf8\").toString(\"base64url\");\n}\n\nfunction envValue(raw: string | undefined): string | undefined {\n return typeof raw === \"string\" && raw.trim().length > 0\n ? raw.trim()\n : undefined;\n}\n\nfunction isLocalAwsUrl(rawUrl: string | undefined): boolean {\n if (!rawUrl) return false;\n try {\n const url = new URL(rawUrl);\n return (\n (url.hostname === \"localhost\" || url.hostname === \"127.0.0.1\") &&\n (url.port === \"18080\" || url.port === \"8088\")\n );\n } catch {\n return false;\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/templates/testing-types-content.ts"],"sourcesContent":["const DEFAULT_REJECTION_CODES = [\n \"NOT_YOUR_TURN\",\n \"action-unavailable\",\n \"invalid-action-params\",\n \"prompt-not-owned\",\n] as const;\n\nfunction renderLiteralUnion(values: readonly string[]): string {\n if (values.length === 0) {\n return \"never\";\n }\n return values.map((value) => JSON.stringify(value)).join(\" | \");\n}\n\nexport const REDUCER_TESTING_TYPES_WRAPPER_CONTENT = `\\\n// Generated by dreamboard — do not edit by hand.\nimport game from \"../app/game\";\nimport {\n contractFingerprint,\n createReducerBundle,\n} from \"@dreamboard-games/sdk/reducer\";\nimport { createTestRuntime as createDreamboardTestRuntime } from \"@dreamboard-games/sdk/testing\";\nimport type { CreateTestRuntimeOptions } from \"@dreamboard-games/sdk/testing\";\nimport { literals } from \"../shared/manifest-contract\";\nimport type { PhaseName } from \"../shared/generated/ui-contract\";\nimport {\n BASE_STATES,\n BASE_STATES_CONTRACT_FINGERPRINT,\n} from \"./generated/base-states.generated\";\nimport type {\n BaseDefinition,\n ScenarioDefinition,\n TestRunner,\n} from \"./generated/testing-contract\";\n\nexport * from \"./generated/testing-contract\";\n\n/**\n * Workspace-narrowed \\`defineBase\\` wrapper. Accepts the generated\n * \\`BaseDefinition\\` so \\`setup({ seat, game })\\` is typed against the\n * workspace's player ids and interaction contract.\n */\nexport function defineBase<const Definition extends BaseDefinition>(\n definition: Definition,\n): Definition {\n return definition;\n}\n\n/**\n * Workspace-narrowed \\`defineScenario\\` wrapper. The generated\n * \\`ScenarioDefinition\\` narrows \\`ctx.view(playerId)\\` in \\`then\\` based on\n * the declared \\`phase\\`, keeps \\`when\\` union-typed, and constrains\n * \\`phase\\` / \\`stage\\` to the manifest-derived literal types.\n */\nexport function defineScenario<\n const Runners extends readonly TestRunner[] = readonly [\"reducer\"],\n const Phase extends PhaseName | undefined = undefined,\n>(\n definition: ScenarioDefinition<Runners, Phase>,\n): ScenarioDefinition<Runners, Phase> {\n return definition;\n}\n\nexport function createTestRuntime(options: {\n baseId: keyof typeof BASE_STATES & string;\n phase?: PhaseName;\n controllingPlayerId?: (typeof literals.playerIds)[number];\n userId?: string | null;\n}) {\n const reducerBundle =\n createReducerBundle(game) satisfies CreateTestRuntimeOptions[\"bundle\"];\n const baseStates =\n BASE_STATES satisfies CreateTestRuntimeOptions[\"baseStates\"];\n const runtime = createDreamboardTestRuntime({\n baseId: options.baseId,\n baseStates,\n bundle: reducerBundle,\n contractFingerprint: contractFingerprint(game).value,\n expectedBaseStateFingerprint: BASE_STATES_CONTRACT_FINGERPRINT,\n phase: options.phase,\n userId: options.userId ?? \"test-user\",\n playerIds: literals.playerIds.slice(\n 0,\n BASE_STATES[options.baseId]?.fingerprint.players ?? literals.playerIds.length,\n ),\n });\n\n if (options.controllingPlayerId) {\n runtime.setControllingPlayer(options.controllingPlayerId);\n }\n\n return runtime;\n}\n`;\n\nexport function buildReducerTestingContractContent(\n options: {\n rejectionCodes?: readonly string[];\n } = {},\n): string {\n const rejectionCodes = Array.from(\n new Set([...(options.rejectionCodes ?? []), ...DEFAULT_REJECTION_CODES]),\n ).sort((left, right) => left.localeCompare(right));\n\n return `\\\n// Generated by dreamboard — do not edit by hand.\nimport type game from \"../../app/game\";\nimport { literals, type SetupProfileId } from \"../../shared/manifest-contract\";\nimport {\n type GameView,\n type InteractionId,\n type InteractionKey,\n type InteractionParamsOf,\n type PhaseName,\n type StageName as WorkspaceStageName,\n} from \"../../shared/generated/ui-contract\";\nimport type {\n ExpectFn as SharedExpectFn,\n TestRunner as SharedTestRunner,\n} from \"@dreamboard-games/sdk/testing\";\nimport type { InteractionDescriptor } from \"@dreamboard-games/sdk/runtime\";\nimport { BASE_STATES } from \"./base-states.generated\";\n\nexport type GameDefinition = typeof game;\nexport type PlayerId = (typeof literals.playerIds)[number];\nexport type StateName = PhaseName;\nexport type BaseId = keyof typeof BASE_STATES & string;\nexport type InteractionDescriptorFor<Id extends string = string> =\n InteractionDescriptor<Id>;\nexport type InteractionExplanation = {\n interactionId: string;\n phase: string;\n step: string | null;\n availability:\n | \"available\"\n | \"notYourTurn\"\n | \"wrongPhase\"\n | \"wrongStep\"\n | \"blocked\";\n rules: ReadonlyArray<{\n ruleId: string;\n outcome: \"passed\" | \"failed\" | \"notEvaluated\";\n errorCode?: string;\n message?: string;\n }>;\n actor: { required: readonly string[]; playerIsActor: boolean };\n inputs: ReadonlyArray<{\n key: string;\n kind: string;\n eligibleCount: number | \"lazy\";\n }>;\n};\nexport type TestRunner = SharedTestRunner;\nexport type ExpectFn = SharedExpectFn;\nexport type KnownRejectionCode = ${renderLiteralUnion(rejectionCodes)};\nexport type RejectionCode = [KnownRejectionCode] extends [never]\n ? string\n : KnownRejectionCode;\n\ntype DefaultRunners = readonly [\"reducer\"];\ntype PhaseTaggedView<Phase extends PhaseName> = Extract<\n GameView,\n { phase: Phase } | { currentPhase: Phase } | { state: Phase }\n>;\ntype NarrowedView<Phase extends PhaseName> = [PhaseTaggedView<Phase>] extends [never]\n ? GameView\n : PhaseTaggedView<Phase>;\n\nexport type ViewByPhase = {\n [Phase in PhaseName]: NarrowedView<Phase>;\n};\n\ntype InteractionKeyForId<Id extends InteractionId> = Extract<\n InteractionKey,\n \\`\\${string}.\\${Id}\\`\n>;\ntype InteractionParamsForKey<Key extends InteractionKey> =\n Key extends InteractionKey ? InteractionParamsOf<Key> : never;\ntype InteractionParamsOfId<Id extends InteractionId> =\n InteractionParamsForKey<InteractionKeyForId<Id>>;\n\nexport interface BrowserRunnerSnapshot {\n sessionId: string | null;\n shortCode: string | null;\n version: number;\n currentPhase: string | null;\n controllingPlayerId: string;\n controllablePlayerIds: string[];\n view: unknown;\n availableInteractions?: string[];\n}\n\nexport interface BrowserRunnerBridge {\n snapshot(): Promise<BrowserRunnerSnapshot>;\n submitInteraction(\n playerId: PlayerId,\n interactionId: string,\n params: unknown,\n ): Promise<void>;\n}\n\nexport interface BrowserRunnerDriver {\n onReady?(bridge: BrowserRunnerBridge): Promise<void> | void;\n interaction?(\n bridge: BrowserRunnerBridge,\n input: { playerId: PlayerId; interactionId: string; params: unknown },\n ): Promise<boolean | void> | boolean | void;\n}\n\nexport interface ScenarioGameApi {\n start(): Promise<void>;\n /**\n * Patch the reducer snapshot for deterministic setup-heavy scenarios.\n * This is limited to reducer snapshot materialization and is rejected by\n * live replay/browser runners so authored gameplay verification still\n * submits real interactions.\n */\n patchState(mutator: (state: Record<string, unknown>) => void): Promise<void>;\n /**\n * Submit a player interaction (action-kind or prompt-kind) to the game.\n * The \\`interactionId\\` matches an \\`InteractionId\\` from the generated\n * \\`ui-contract\\`; \\`params\\` is typed per interaction id.\n */\n submit<Id extends InteractionId>(\n playerId: PlayerId,\n interactionId: Id,\n params?: InteractionParamsOfId<Id>,\n ): Promise<void>;\n}\n\nexport interface BaseContext {\n game: ScenarioGameApi;\n players(): readonly PlayerId[];\n /**\n * Resolve the seat at \\`index\\` in the base's players list.\n * Throws if the index is out of range. Prefer \\`seat(0)\\`/\\`seat(1)\\` over\n * literal player ids so bases stay portable across player counts and\n * we never hard-code wire-shape assumptions like \"player-1\".\n */\n seat(index: number): PlayerId;\n}\n\nexport interface SharedScenarioContext {\n game: ScenarioGameApi;\n players(): readonly PlayerId[];\n /**\n * Resolve the seat at \\`index\\` in the current scenario's players list.\n * Throws if the index is out of range. Prefer \\`seat(0)\\`/\\`seat(1)\\` over\n * literal player ids so scenarios stay portable across player counts and\n * we never hard-code wire-shape assumptions like \"player-1\".\n */\n seat(index: number): PlayerId;\n state(): StateName;\n view(playerId: PlayerId): GameView;\n interactions(playerId: PlayerId): readonly InteractionDescriptorFor[];\n explain(playerId: PlayerId, interactionId: InteractionId): InteractionExplanation;\n expect: ExpectFn;\n}\n\nexport type ScenarioContext<\n Phase extends PhaseName | undefined = undefined,\n> = Omit<SharedScenarioContext, \"state\" | \"view\"> & {\n state(): Phase extends PhaseName ? Phase : StateName;\n view(playerId: PlayerId): Phase extends PhaseName ? ViewByPhase[Phase] : GameView;\n};\n\nexport type ScenarioThenContext<\n _Runners extends readonly TestRunner[] = DefaultRunners,\n Phase extends PhaseName | undefined = undefined,\n> = ScenarioContext<Phase>;\n\nexport interface BaseDefinition {\n id: string;\n seed?: number;\n players?: number;\n setupProfileId?: SetupProfileId;\n extends?: BaseId | string;\n setup: (ctx: BaseContext) => void | Promise<void>;\n}\n\nexport interface ScenarioDefinition<\n Runners extends readonly TestRunner[] = DefaultRunners,\n Phase extends PhaseName | undefined = undefined,\n> {\n id: string;\n description?: string;\n from: BaseId | string;\n runners?: Runners;\n phase?: Phase;\n stage?: Phase extends PhaseName ? WorkspaceStageName<Phase> : never;\n when: (ctx: ScenarioContext<Phase>) => void | Promise<void>;\n then: (ctx: ScenarioThenContext<Runners, Phase>) => void | Promise<void>;\n}\n\nexport type {\n GameView,\n InteractionId,\n InteractionParamsOf,\n PhaseName,\n WorkspaceStageName,\n};\n`;\n}\n"],"mappings":";;;AAAA,IAAM,0BAA0B;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,SAAS,mBAAmB,QAAmC;AAC7D,MAAI,OAAO,WAAW,GAAG;AACvB,WAAO;AAAA,EACT;AACA,SAAO,OAAO,IAAI,CAAC,UAAU,KAAK,UAAU,KAAK,CAAC,EAAE,KAAK,KAAK;AAChE;AAEO,IAAM,wCAAwC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiF9C,SAAS,mCACd,UAEI,CAAC,GACG;AACR,QAAM,iBAAiB,MAAM;AAAA,IAC3B,oBAAI,IAAI,CAAC,GAAI,QAAQ,kBAAkB,CAAC,GAAI,GAAG,uBAAuB,CAAC;AAAA,EACzE,EAAE,KAAK,CAAC,MAAM,UAAU,KAAK,cAAc,KAAK,CAAC;AAEjD,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mCAkD0B,mBAAmB,cAAc,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoJrE;","names":[]}
|