@dreamboard-games/cli 0.1.30-alpha.16 → 0.1.30-alpha.18
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 +3 -1
- package/dist/agent-verifier/agent-workspace-verifier.mjs +12 -12
- package/dist/agent-verifier/{chunk-LKQ557TJ.mjs → chunk-334H4LE4.mjs} +3 -3
- package/dist/agent-verifier/{chunk-H3XNWKJU.mjs → chunk-7LFDFXLS.mjs} +2 -2
- package/dist/agent-verifier/{chunk-DWLTCUUX.mjs → chunk-7MAOGFFP.mjs} +6 -6
- package/dist/agent-verifier/{chunk-CO3BRUD6.mjs → chunk-AG5J3SMN.mjs} +11 -3
- package/dist/agent-verifier/chunk-AG5J3SMN.mjs.map +1 -0
- package/dist/agent-verifier/{chunk-AXXUGU7Q.mjs → chunk-AQ6UQHPT.mjs} +4 -30
- package/dist/agent-verifier/chunk-AQ6UQHPT.mjs.map +1 -0
- package/dist/agent-verifier/{chunk-A67WUYN2.mjs → chunk-B42OHJNY.mjs} +3 -3
- package/dist/agent-verifier/{chunk-V6AQDR7W.mjs → chunk-HUBV22JQ.mjs} +3 -3
- package/dist/agent-verifier/chunk-HUBV22JQ.mjs.map +1 -0
- package/dist/agent-verifier/{chunk-5GCZZ6NW.mjs → chunk-JB7VXCMB.mjs} +2 -2
- package/dist/agent-verifier/{chunk-WAFBU5U7.mjs → chunk-OJFZVGEL.mjs} +38 -13
- package/dist/agent-verifier/chunk-OJFZVGEL.mjs.map +1 -0
- package/dist/agent-verifier/{chunk-G2ECODRB.mjs → chunk-PLXXH5LY.mjs} +2 -2
- package/dist/agent-verifier/{chunk-655VJLXA.mjs → chunk-PWPOLHTW.mjs} +9 -12
- package/dist/agent-verifier/chunk-PWPOLHTW.mjs.map +1 -0
- package/dist/agent-verifier/{chunk-DPYC2NDB.mjs → chunk-RCYO6HWW.mjs} +2 -2
- package/dist/agent-verifier/{chunk-NFL3Z4Z7.mjs → chunk-RP7ZWFVH.mjs} +12 -8
- package/dist/agent-verifier/{chunk-NFL3Z4Z7.mjs.map → chunk-RP7ZWFVH.mjs.map} +1 -1
- package/dist/agent-verifier/{compile-4VSMC275.mjs → compile-VOBO2I6D.mjs} +12 -12
- package/dist/agent-verifier/{global-config-6UGFPLDA.mjs → global-config-L7PLLUK5.mjs} +3 -3
- package/dist/agent-verifier/{keychain-backend-BQLW5VEC.mjs → keychain-backend-UF3Z26JM.mjs} +1 -1
- package/dist/agent-verifier/keychain-backend-UF3Z26JM.mjs.map +1 -0
- package/dist/agent-verifier/{local-files-WPHUV6GU.mjs → local-files-DAFIR7SN.mjs} +4 -4
- package/dist/agent-verifier/{materialize-workspace-CV2JNXLU.mjs → materialize-workspace-PAC75NSP.mjs} +6 -6
- package/dist/agent-verifier/{reducer-native-test-harness-GY2CCQWN.mjs → reducer-native-test-harness-HSXRUGOR.mjs} +8 -8
- package/dist/agent-verifier/{static-scaffold-DJJRKMNB.mjs → static-scaffold-KSOTKJNQ.mjs} +4 -4
- package/dist/agent-verifier/{sync-YWK4SPWV.mjs → sync-MQJJEZAA.mjs} +13 -13
- package/dist/agent-verifier/{test-LQAGEQLY.mjs → test-R6HC6CYZ.mjs} +11 -11
- package/dist/agent-verifier/{workspace-codegen-4IWICKLB.mjs → workspace-codegen-SPPVHURX.mjs} +3 -3
- package/dist/authoring-compatibility-internal.js +1 -1
- package/dist/{chunk-2R4L2YDX.js → chunk-2WB3DYW4.js} +17 -8
- package/dist/chunk-2WB3DYW4.js.map +1 -0
- package/dist/{chunk-PW7D2W5S.js → chunk-2XMBZPL5.js} +45 -23
- package/dist/{chunk-PW7D2W5S.js.map → chunk-2XMBZPL5.js.map} +1 -1
- package/dist/{chunk-AVOAT522.js → chunk-J3CWZHY7.js} +4 -30
- package/dist/chunk-J3CWZHY7.js.map +1 -0
- package/dist/{global-config-NLGAFSRU.js → global-config-VQWFTIAV.js} +2 -2
- package/dist/index.js +5 -5
- package/dist/internal.js +3 -3
- package/dist/{keychain-backend-47LZ5IX5.js → keychain-backend-GO34KGTG.js} +1 -1
- package/dist/keychain-backend-GO34KGTG.js.map +1 -0
- package/package.json +1 -1
- package/release/authoring-release-set.json +4 -4
- package/skills/dreamboard/SKILL.md +8 -0
- package/skills/dreamboard/references/building-your-first-game.md +37 -15
- package/skills/dreamboard/references/canonical-concepts.md +74 -0
- package/skills/dreamboard/references/game-interface.md +15 -2
- package/skills/dreamboard/references/manifest-authoring.md +8 -0
- package/skills/dreamboard/references/reducer.md +47 -2
- package/skills/dreamboard/references/rule-authoring.md +10 -0
- package/skills/dreamboard/references/testing.md +7 -3
- package/dist/agent-verifier/chunk-655VJLXA.mjs.map +0 -1
- package/dist/agent-verifier/chunk-AXXUGU7Q.mjs.map +0 -1
- package/dist/agent-verifier/chunk-CO3BRUD6.mjs.map +0 -1
- package/dist/agent-verifier/chunk-V6AQDR7W.mjs.map +0 -1
- package/dist/agent-verifier/chunk-WAFBU5U7.mjs.map +0 -1
- package/dist/agent-verifier/keychain-backend-BQLW5VEC.mjs.map +0 -1
- package/dist/chunk-2R4L2YDX.js.map +0 -1
- package/dist/chunk-AVOAT522.js.map +0 -1
- package/dist/keychain-backend-47LZ5IX5.js.map +0 -1
- /package/dist/agent-verifier/{chunk-LKQ557TJ.mjs.map → chunk-334H4LE4.mjs.map} +0 -0
- /package/dist/agent-verifier/{chunk-H3XNWKJU.mjs.map → chunk-7LFDFXLS.mjs.map} +0 -0
- /package/dist/agent-verifier/{chunk-DWLTCUUX.mjs.map → chunk-7MAOGFFP.mjs.map} +0 -0
- /package/dist/agent-verifier/{chunk-A67WUYN2.mjs.map → chunk-B42OHJNY.mjs.map} +0 -0
- /package/dist/agent-verifier/{chunk-5GCZZ6NW.mjs.map → chunk-JB7VXCMB.mjs.map} +0 -0
- /package/dist/agent-verifier/{chunk-G2ECODRB.mjs.map → chunk-PLXXH5LY.mjs.map} +0 -0
- /package/dist/agent-verifier/{chunk-DPYC2NDB.mjs.map → chunk-RCYO6HWW.mjs.map} +0 -0
- /package/dist/agent-verifier/{compile-4VSMC275.mjs.map → compile-VOBO2I6D.mjs.map} +0 -0
- /package/dist/agent-verifier/{global-config-6UGFPLDA.mjs.map → global-config-L7PLLUK5.mjs.map} +0 -0
- /package/dist/agent-verifier/{local-files-WPHUV6GU.mjs.map → local-files-DAFIR7SN.mjs.map} +0 -0
- /package/dist/agent-verifier/{materialize-workspace-CV2JNXLU.mjs.map → materialize-workspace-PAC75NSP.mjs.map} +0 -0
- /package/dist/agent-verifier/{reducer-native-test-harness-GY2CCQWN.mjs.map → reducer-native-test-harness-HSXRUGOR.mjs.map} +0 -0
- /package/dist/agent-verifier/{static-scaffold-DJJRKMNB.mjs.map → static-scaffold-KSOTKJNQ.mjs.map} +0 -0
- /package/dist/agent-verifier/{sync-YWK4SPWV.mjs.map → sync-MQJJEZAA.mjs.map} +0 -0
- /package/dist/agent-verifier/{test-LQAGEQLY.mjs.map → test-R6HC6CYZ.mjs.map} +0 -0
- /package/dist/agent-verifier/{workspace-codegen-4IWICKLB.mjs.map → workspace-codegen-SPPVHURX.mjs.map} +0 -0
- /package/dist/{global-config-NLGAFSRU.js.map → global-config-VQWFTIAV.js.map} +0 -0
|
@@ -15,14 +15,6 @@ import path2 from "path";
|
|
|
15
15
|
import os from "os";
|
|
16
16
|
import path from "path";
|
|
17
17
|
import { promises as fs } from "fs";
|
|
18
|
-
|
|
19
|
-
// src/build-target.ts
|
|
20
|
-
var injectedBuildChannel = true ? "published" : void 0;
|
|
21
|
-
var BUILD_CHANNEL = injectedBuildChannel === "published" ? "published" : "development";
|
|
22
|
-
var IS_PUBLISHED_BUILD = BUILD_CHANNEL === "published";
|
|
23
|
-
var PUBLISHED_ENVIRONMENT = "prod";
|
|
24
|
-
|
|
25
|
-
// src/config/credential-store.ts
|
|
26
18
|
function getCredentialFilePath() {
|
|
27
19
|
return path.join(os.homedir(), PROJECT_DIR_NAME, "auth.json");
|
|
28
20
|
}
|
|
@@ -121,19 +113,6 @@ var migrationCompleted = false;
|
|
|
121
113
|
var backendResolver = defaultBackendResolver;
|
|
122
114
|
async function defaultBackendResolver() {
|
|
123
115
|
const override = (process.env.DREAMBOARD_CREDENTIAL_BACKEND ?? "").trim().toLowerCase();
|
|
124
|
-
if (IS_PUBLISHED_BUILD) {
|
|
125
|
-
if (override && override !== "keychain" && override !== "auto") {
|
|
126
|
-
throw new CredentialStoreUnavailableError(
|
|
127
|
-
"published builds require the OS credential store"
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
const { tryKeychainBackend: tryKeychainBackend2 } = await import("./keychain-backend-47LZ5IX5.js");
|
|
131
|
-
const keychain2 = await tryKeychainBackend2();
|
|
132
|
-
if (keychain2.available) {
|
|
133
|
-
return keychain2.backend;
|
|
134
|
-
}
|
|
135
|
-
throw new CredentialStoreUnavailableError(keychain2.reason);
|
|
136
|
-
}
|
|
137
116
|
if (override === "file") {
|
|
138
117
|
return fileCredentialBackend;
|
|
139
118
|
}
|
|
@@ -146,7 +125,7 @@ async function defaultBackendResolver() {
|
|
|
146
125
|
if (!useKeychain) {
|
|
147
126
|
return fileCredentialBackend;
|
|
148
127
|
}
|
|
149
|
-
const { tryKeychainBackend } = await import("./keychain-backend-
|
|
128
|
+
const { tryKeychainBackend } = await import("./keychain-backend-GO34KGTG.js");
|
|
150
129
|
const keychain = await tryKeychainBackend();
|
|
151
130
|
if (keychain.available) {
|
|
152
131
|
return keychain.backend;
|
|
@@ -155,7 +134,7 @@ async function defaultBackendResolver() {
|
|
|
155
134
|
}
|
|
156
135
|
async function readCredentialBackendPreference() {
|
|
157
136
|
try {
|
|
158
|
-
const { loadGlobalConfig: loadGlobalConfig2 } = await import("./global-config-
|
|
137
|
+
const { loadGlobalConfig: loadGlobalConfig2 } = await import("./global-config-VQWFTIAV.js");
|
|
159
138
|
const config = await loadGlobalConfig2();
|
|
160
139
|
return config.credentialBackend === "keychain";
|
|
161
140
|
} catch {
|
|
@@ -166,9 +145,7 @@ async function getCredentialBackend() {
|
|
|
166
145
|
if (cachedBackend === null) {
|
|
167
146
|
cachedBackend = await backendResolver();
|
|
168
147
|
if (!migrationCompleted && cachedBackend.name !== "file") {
|
|
169
|
-
await migrateFromFileBackendIfNeeded(cachedBackend
|
|
170
|
-
failClosed: IS_PUBLISHED_BUILD
|
|
171
|
-
});
|
|
148
|
+
await migrateFromFileBackendIfNeeded(cachedBackend);
|
|
172
149
|
}
|
|
173
150
|
migrationCompleted = true;
|
|
174
151
|
}
|
|
@@ -306,9 +283,6 @@ async function saveGlobalConfig(config) {
|
|
|
306
283
|
}
|
|
307
284
|
|
|
308
285
|
export {
|
|
309
|
-
BUILD_CHANNEL,
|
|
310
|
-
IS_PUBLISHED_BUILD,
|
|
311
|
-
PUBLISHED_ENVIRONMENT,
|
|
312
286
|
getActiveCredentialBackendName,
|
|
313
287
|
getStoredSession,
|
|
314
288
|
setCredentials,
|
|
@@ -320,4 +294,4 @@ export {
|
|
|
320
294
|
loadGlobalConfig,
|
|
321
295
|
saveGlobalConfig
|
|
322
296
|
};
|
|
323
|
-
//# sourceMappingURL=chunk-
|
|
297
|
+
//# sourceMappingURL=chunk-J3CWZHY7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/config/global-config.ts","../src/config/credential-store.ts"],"sourcesContent":["import os from \"node:os\";\nimport path from \"node:path\";\nimport type { CredentialBackendPreference, GlobalConfig } from \"../types.js\";\nimport { PROJECT_DIR_NAME } from \"../constants.js\";\nimport { ensureDir, readJsonFile } from \"../utils/fs.js\";\nimport { atomicWriteFile } from \"../utils/atomic-file.js\";\nimport { getCredentialFilePath } from \"./credential-store.js\";\n\nfunction normalizeCredentialBackend(\n value: unknown,\n): CredentialBackendPreference | undefined {\n if (value === \"file\" || value === \"keychain\") return value;\n // Tolerate unknown / malformed values rather than refusing to load the\n // whole config - an unrecognised backend name should degrade to \"use\n // the default\" instead of locking the user out of their CLI.\n return undefined;\n}\n\nexport function getGlobalConfigPath(): string {\n return path.join(os.homedir(), PROJECT_DIR_NAME, \"config.json\");\n}\n\n/**\n * Path to the on-disk credential file used by the file backend of\n * `CredentialStore`. Re-exported here to avoid circular / ad-hoc imports\n * in UI surface (`auth status`, `config show`, etc).\n */\nexport function getGlobalAuthPath(): string {\n return getCredentialFilePath();\n}\n\n/**\n * Load non-credential CLI configuration.\n *\n * Note: this function used to also load `authToken` / `refreshToken`\n * from `auth.json` and flatten them onto `GlobalConfig`. That shape\n * enabled the refresh-token-wipe bug: `saveGlobalConfig({ ...config })`\n * without explicit auth fields erased the stored refresh token.\n *\n * Credentials are now owned exclusively by `CredentialStore`. Callers\n * that need them must import `getCredentials()` directly.\n */\nexport async function loadGlobalConfig(): Promise<GlobalConfig> {\n const config = await readJsonFile<GlobalConfig>(getGlobalConfigPath()).catch(\n () => ({}) as GlobalConfig,\n );\n return {\n environment: config.environment,\n credentialBackend: normalizeCredentialBackend(config.credentialBackend),\n };\n}\n\n/**\n * Persist non-credential CLI configuration.\n *\n * This function cannot write credentials, by construction: the\n * `GlobalConfig` type has no credential fields. Credentials must be\n * persisted through `setCredentials` / `clearCredentials` from\n * `credential-store.ts`.\n */\nexport async function saveGlobalConfig(config: GlobalConfig): Promise<void> {\n const configDir = path.join(os.homedir(), PROJECT_DIR_NAME);\n await ensureDir(configDir);\n const normalized: GlobalConfig = {\n environment: config.environment,\n credentialBackend: normalizeCredentialBackend(config.credentialBackend),\n };\n await atomicWriteFile(\n getGlobalConfigPath(),\n `${JSON.stringify(normalized, null, 2)}\\n`,\n { mode: 0o600 },\n );\n}\n","/**\n * Single writer for the long-lived Dreamboard session credentials.\n *\n * Design invariants (enforced at the type level and tested in\n * `credential-store.test.ts`):\n *\n * 1. This module is the ONLY place in the CLI that writes credentials to\n * disk or the OS keychain. `global-config.ts` used to own both the\n * config and the credentials via `saveGlobalConfig`, which made it\n * trivial to wipe a refresh token by accident. The `GlobalConfig` type\n * no longer carries credentials, so attempting to persist one through\n * the config path is a type error.\n *\n * 2. The mutating surface is intentionally narrow:\n * - `setCredentials(c)` for refreshable sessions (both tokens present)\n * - `setAccessOnlySession(accessToken)` for the `auth set` / `config set\n * --token` power-user path, which has no refresh token by\n * construction\n * - `clearCredentials()` wipes the file entirely\n * There is no \"partial update\" API. `Credentials` requires both\n * `accessToken` and `refreshToken`, so it is impossible to persist a\n * half-populated refreshable session.\n *\n * 3. Writes go through `atomicWriteFile` + `withFileLock`, so a crash or\n * interrupt during `dreamboard sync`/`compile` cannot leave `auth.json`\n * truncated, and parallel CLI invocations cannot clobber each other's\n * rotated refresh tokens.\n *\n * 4. The on-disk JSON shape for the file backend is kept backward\n * compatible: we continue to read/write `authToken` + `refreshToken`\n * so existing users are not forced to log in again after this change.\n * A newer `accessToken` key is also accepted for read to ease any\n * future format bump.\n *\n * 5. All builds default to the file backend. The OS keychain is an explicit\n * opt-in through config or `DREAMBOARD_CREDENTIAL_BACKEND=keychain`.\n */\n\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { promises as fs } from \"node:fs\";\nimport { PROJECT_DIR_NAME } from \"../constants.js\";\nimport {\n atomicWriteFile,\n withFileLock,\n type FileLockOptions,\n} from \"../utils/atomic-file.js\";\n\n/**\n * Fully refreshable session. `accessToken` is the Clerk OAuth bootstrap token\n * retained for refresh/exchange compatibility; ordinary API calls use\n * `dreamboardApiToken`.\n */\nexport type Credentials = {\n readonly accessToken: string;\n readonly refreshToken: string;\n readonly tokenExpiresAt?: string;\n readonly dreamboardApiToken?: string;\n readonly dreamboardApiExpiresAt?: string;\n readonly clerkOAuthIssuer?: string;\n readonly clerkOAuthClientId?: string;\n readonly clerkOAuthTokenUrl?: string;\n readonly environment?: string;\n};\n\n/**\n * Raw on-disk snapshot. Either or both fields may be present. The refresh\n * coordinator only acts on snapshots that have both tokens populated.\n */\nexport type StoredSessionSnapshot = {\n readonly accessToken?: string;\n readonly refreshToken?: string;\n readonly tokenExpiresAt?: string;\n readonly dreamboardApiToken?: string;\n readonly dreamboardApiExpiresAt?: string;\n readonly clerkOAuthIssuer?: string;\n readonly clerkOAuthClientId?: string;\n readonly clerkOAuthTokenUrl?: string;\n readonly environment?: string;\n};\n\nexport type CredentialBackendName = \"file\" | \"keychain\";\n\nexport type CredentialBackend = {\n readonly name: CredentialBackendName;\n read(): Promise<StoredSessionSnapshot | null>;\n writeFull(creds: Credentials): Promise<void>;\n writeAccessOnly(accessToken: string): Promise<void>;\n clear(): Promise<void>;\n};\n\nexport type CredentialLockOps = {\n readonly backendName: CredentialBackendName;\n read(): Promise<StoredSessionSnapshot | null>;\n writeFull(creds: Credentials): Promise<void>;\n writeAccessOnly(accessToken: string): Promise<void>;\n clear(): Promise<void>;\n};\n\ntype DiskShape = Partial<{\n clerkAccessToken: string;\n clerkAccessExpiresAt: string;\n accessToken: string;\n authToken: string;\n refreshToken: string;\n tokenExpiresAt: string;\n dreamboardApiToken: string;\n dreamboardApiExpiresAt: string;\n clerkOAuthIssuer: string;\n clerkOAuthClientId: string;\n clerkOAuthTokenUrl: string;\n environment: string;\n}>;\n\nexport function getCredentialFilePath(): string {\n return path.join(os.homedir(), PROJECT_DIR_NAME, \"auth.json\");\n}\n\nfunction getCredentialLockPath(): string {\n return `${getCredentialFilePath()}.lock`;\n}\n\nasync function fileRead(): Promise<StoredSessionSnapshot | null> {\n const filePath = getCredentialFilePath();\n let data: string;\n try {\n data = await fs.readFile(filePath, \"utf8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw err;\n }\n if (data.trim().length === 0) {\n return null;\n }\n let parsed: DiskShape;\n try {\n parsed = JSON.parse(data) as DiskShape;\n } catch {\n return null;\n }\n const accessToken =\n parsed.clerkAccessToken ?? parsed.accessToken ?? parsed.authToken;\n const refreshToken = parsed.refreshToken;\n if (!accessToken && !refreshToken) return null;\n return {\n accessToken: accessToken || undefined,\n refreshToken: refreshToken || undefined,\n tokenExpiresAt:\n parsed.clerkAccessExpiresAt || parsed.tokenExpiresAt || undefined,\n dreamboardApiToken: parsed.dreamboardApiToken || undefined,\n dreamboardApiExpiresAt: parsed.dreamboardApiExpiresAt || undefined,\n clerkOAuthIssuer: parsed.clerkOAuthIssuer || undefined,\n clerkOAuthClientId: parsed.clerkOAuthClientId || undefined,\n clerkOAuthTokenUrl: parsed.clerkOAuthTokenUrl || undefined,\n environment: parsed.environment || undefined,\n };\n}\n\nasync function writeFilePayload(payload: DiskShape): Promise<void> {\n await atomicWriteFile(\n getCredentialFilePath(),\n `${JSON.stringify(payload, null, 2)}\\n`,\n { mode: 0o600 },\n );\n}\n\nasync function fileWriteFull(creds: Credentials): Promise<void> {\n if (!creds.accessToken || !creds.refreshToken) {\n throw new Error(\n \"Refusing to persist credentials with an empty accessToken or refreshToken.\",\n );\n }\n await writeFilePayload({\n clerkAccessToken: creds.accessToken,\n refreshToken: creds.refreshToken,\n clerkAccessExpiresAt: creds.tokenExpiresAt,\n dreamboardApiToken: creds.dreamboardApiToken,\n dreamboardApiExpiresAt: creds.dreamboardApiExpiresAt,\n clerkOAuthIssuer: creds.clerkOAuthIssuer,\n clerkOAuthClientId: creds.clerkOAuthClientId,\n clerkOAuthTokenUrl: creds.clerkOAuthTokenUrl,\n environment: creds.environment,\n });\n}\n\nasync function fileWriteAccessOnly(accessToken: string): Promise<void> {\n if (!accessToken) {\n throw new Error(\"Refusing to persist an empty access token.\");\n }\n await writeFilePayload({ authToken: accessToken });\n}\n\nasync function fileClear(): Promise<void> {\n const filePath = getCredentialFilePath();\n try {\n await fs.unlink(filePath);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") throw err;\n }\n}\n\nexport const fileCredentialBackend: CredentialBackend = {\n name: \"file\",\n read: fileRead,\n writeFull: fileWriteFull,\n writeAccessOnly: fileWriteAccessOnly,\n clear: fileClear,\n};\n\nexport type BackendResolver = () =>\n | CredentialBackend\n | Promise<CredentialBackend>;\n\nexport class CredentialStoreUnavailableError extends Error {\n readonly code = \"CREDENTIAL_STORE_UNAVAILABLE\";\n\n constructor(reason: string) {\n super(`Credential store unavailable: ${reason}`);\n this.name = \"CredentialStoreUnavailableError\";\n }\n}\n\nlet cachedBackend: CredentialBackend | null = null;\nlet migrationCompleted = false;\nlet backendResolver: BackendResolver = defaultBackendResolver;\n\n/**\n * Resolver precedence for all builds:\n *\n * 1. `DREAMBOARD_CREDENTIAL_BACKEND` env var (debugging / CI override).\n * - \"file\" -> force file\n * - \"keychain\" -> force keychain (falls back to file if the native\n * module or the OS keyring is unavailable)\n * - \"auto\" -> same as unset (use config)\n * - unknown -> throw so typos fail loud\n * 2. `credentialBackend` in `~/.dreamboard/config.json`.\n * - \"keychain\" -> opt in to the OS keychain (with file fallback)\n * - \"file\" / unset / malformed -> file\n * 3. Default: file backend.\n *\n * Keychain is opt-in because on macOS the OS login-keychain prompts for\n * the user's password the first time a new binary tries to write to an\n * item, and re-prompts whenever the Node binary signature changes. We\n * would rather ship a zero-prompt default and let users who care about\n * encrypted-at-rest storage enable it.\n *\n * The resolver is async because the keychain probe requires a dynamic\n * `@napi-rs/keyring` import.\n */\nasync function defaultBackendResolver(): Promise<CredentialBackend> {\n const override = (process.env.DREAMBOARD_CREDENTIAL_BACKEND ?? \"\")\n .trim()\n .toLowerCase();\n if (override === \"file\") {\n return fileCredentialBackend;\n }\n if (override && override !== \"keychain\" && override !== \"auto\") {\n // Fail loud on typos rather than silently falling back: this env\n // var exists specifically for users who are debugging auth issues\n // and need to know their override took effect.\n throw new Error(\n `Unknown DREAMBOARD_CREDENTIAL_BACKEND value \"${override}\" (expected \"file\", \"keychain\", or \"auto\").`,\n );\n }\n\n const useKeychain =\n override === \"keychain\" || (await readCredentialBackendPreference());\n if (!useKeychain) {\n return fileCredentialBackend;\n }\n\n const { tryKeychainBackend } = await import(\"./keychain-backend.js\");\n const keychain = await tryKeychainBackend();\n if (keychain.available) {\n return keychain.backend;\n }\n // The user explicitly asked for keychain but the platform can't\n // provide one (no libsecret on Linux, missing native module, etc).\n // Silently degrade to the file backend so the CLI stays usable; the\n // active backend is still visible through `dreamboard auth status`.\n return fileCredentialBackend;\n}\n\nasync function readCredentialBackendPreference(): Promise<boolean> {\n try {\n // Dynamic import to avoid a top-level cycle with `global-config.ts`\n // (which imports `getCredentialFilePath` from this module). Using\n // the async path keeps the cycle purely lazy.\n const { loadGlobalConfig } = await import(\"./global-config.js\");\n const config = await loadGlobalConfig();\n return config.credentialBackend === \"keychain\";\n } catch {\n // If the config file is unreadable or the dynamic import fails\n // (e.g. during early bootstrap), fall back to the file-backed\n // default rather than crashing credential lookups.\n return false;\n }\n}\n\n/**\n * Override which backend is used. Tests use this to inject in-memory\n * backends; production code uses the file-default resolver.\n */\nexport function setCredentialBackendResolver(resolver: BackendResolver): void {\n backendResolver = resolver;\n cachedBackend = null;\n migrationCompleted = false;\n}\n\nexport async function getCredentialBackend(): Promise<CredentialBackend> {\n if (cachedBackend === null) {\n cachedBackend = await backendResolver();\n // One-time migration: if we resolved to a non-file backend and\n // `auth.json` still has credentials from the old layout, copy them\n // over and remove the file. We only do this when the new backend is\n // empty, so repeated migrations cannot stomp a newer keychain\n // session with a stale file session.\n if (!migrationCompleted && cachedBackend.name !== \"file\") {\n await migrateFromFileBackendIfNeeded(cachedBackend);\n }\n migrationCompleted = true;\n }\n return cachedBackend;\n}\n\nasync function migrateFromFileBackendIfNeeded(\n target: CredentialBackend,\n options: { failClosed?: boolean } = {},\n): Promise<void> {\n try {\n const [onDisk, onTarget] = await Promise.all([\n fileCredentialBackend.read(),\n target.read(),\n ]);\n if (!onDisk) return;\n if (onTarget) {\n // Target already has a session - the user has already migrated.\n // Remove the file so it cannot get re-used accidentally.\n await fileCredentialBackend.clear();\n return;\n }\n if (onDisk.accessToken && onDisk.refreshToken) {\n const migrated: Credentials = {\n accessToken: onDisk.accessToken,\n refreshToken: onDisk.refreshToken,\n tokenExpiresAt: onDisk.tokenExpiresAt,\n dreamboardApiToken: onDisk.dreamboardApiToken,\n dreamboardApiExpiresAt: onDisk.dreamboardApiExpiresAt,\n clerkOAuthIssuer: onDisk.clerkOAuthIssuer,\n clerkOAuthClientId: onDisk.clerkOAuthClientId,\n clerkOAuthTokenUrl: onDisk.clerkOAuthTokenUrl,\n environment: onDisk.environment,\n };\n await target.writeFull(migrated);\n await verifyMigratedSession(target, migrated);\n } else if (onDisk.accessToken) {\n await target.writeAccessOnly(onDisk.accessToken);\n const migrated = await target.read();\n if (migrated?.accessToken !== onDisk.accessToken) {\n throw new Error(\"Credential migration verification failed.\");\n }\n } else {\n return;\n }\n await fileCredentialBackend.clear();\n } catch (error) {\n if (options.failClosed) {\n throw new CredentialStoreUnavailableError(\n error instanceof Error ? error.message : String(error),\n );\n }\n // Migration is best-effort. A failure here should not block CLI\n // operation; on next run the file backend is still consulted\n // directly because the keychain backend's `read` returns null and\n // callers fall through to \"missing session\" → login prompt.\n }\n}\n\nasync function verifyMigratedSession(\n target: CredentialBackend,\n expected: Credentials,\n): Promise<void> {\n const migrated = await target.read();\n if (\n migrated?.accessToken !== expected.accessToken ||\n migrated.refreshToken !== expected.refreshToken\n ) {\n throw new Error(\"Credential migration verification failed.\");\n }\n}\n\nexport async function getActiveCredentialBackendName(): Promise<CredentialBackendName> {\n const backend = await getCredentialBackend();\n return backend.name;\n}\n\n/** Loose read: returns whatever is on disk, including access-only sessions. */\nexport async function getStoredSession(): Promise<StoredSessionSnapshot | null> {\n if (process.env.DREAMBOARD_AGENT_TOKEN?.trim()) {\n return null;\n }\n const backend = await getCredentialBackend();\n return backend.read();\n}\n\n/** Strict read: returns a refreshable pair, or null if either token is missing. */\nexport async function getCredentials(): Promise<Credentials | null> {\n const snapshot = await getStoredSession();\n if (!snapshot) return null;\n const { accessToken, refreshToken } = snapshot;\n if (!accessToken || !refreshToken) return null;\n return {\n accessToken,\n refreshToken,\n tokenExpiresAt: snapshot.tokenExpiresAt,\n dreamboardApiToken: snapshot.dreamboardApiToken,\n dreamboardApiExpiresAt: snapshot.dreamboardApiExpiresAt,\n clerkOAuthIssuer: snapshot.clerkOAuthIssuer,\n clerkOAuthClientId: snapshot.clerkOAuthClientId,\n clerkOAuthTokenUrl: snapshot.clerkOAuthTokenUrl,\n environment: snapshot.environment,\n };\n}\n\nexport async function setCredentials(creds: Credentials): Promise<void> {\n await withFileLock(getCredentialLockPath(), async () => {\n const backend = await getCredentialBackend();\n await backend.writeFull(creds);\n });\n}\n\nexport async function setAccessOnlySession(accessToken: string): Promise<void> {\n await withFileLock(getCredentialLockPath(), async () => {\n const backend = await getCredentialBackend();\n await backend.writeAccessOnly(accessToken);\n });\n}\n\nexport async function clearCredentials(): Promise<void> {\n await withFileLock(getCredentialLockPath(), async () => {\n const backend = await getCredentialBackend();\n await backend.clear();\n });\n}\n\n/**\n * Run `fn` while holding the cross-process credential lock. `fn` receives\n * an ops handle that reads/writes the active backend without re-acquiring\n * the lock (avoiding deadlock).\n *\n * This is the only correct way to perform a read-modify-write on stored\n * credentials (e.g. CLI refresh rotation) in the presence of\n * concurrent CLI invocations.\n */\nexport async function withCredentialLock<T>(\n fn: (ops: CredentialLockOps) => Promise<T>,\n options?: FileLockOptions,\n): Promise<T> {\n return withFileLock(\n getCredentialLockPath(),\n async () => {\n const backend = await getCredentialBackend();\n const ops: CredentialLockOps = {\n backendName: backend.name,\n read: () => backend.read(),\n writeFull: (creds) => backend.writeFull(creds),\n writeAccessOnly: (accessToken) => backend.writeAccessOnly(accessToken),\n clear: () => backend.clear(),\n };\n return fn(ops);\n },\n options,\n );\n}\n\n/** Test-only reset of module state. Not exported through the barrel. */\nexport function _resetCredentialStoreForTests(): void {\n cachedBackend = null;\n migrationCompleted = false;\n backendResolver = defaultBackendResolver;\n}\n"],"mappings":"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","names":["os","path","loadGlobalConfig","path","os"]}
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
getGlobalConfigPath,
|
|
5
5
|
loadGlobalConfig,
|
|
6
6
|
saveGlobalConfig
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-J3CWZHY7.js";
|
|
8
8
|
import "./chunk-FFO2IJL3.js";
|
|
9
9
|
import "./chunk-2H7UOFLK.js";
|
|
10
10
|
export {
|
|
@@ -13,4 +13,4 @@ export {
|
|
|
13
13
|
loadGlobalConfig,
|
|
14
14
|
saveGlobalConfig
|
|
15
15
|
};
|
|
16
|
-
//# sourceMappingURL=global-config-
|
|
16
|
+
//# sourceMappingURL=global-config-VQWFTIAV.js.map
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
CONFIG_FLAG_ARGS,
|
|
4
|
+
IS_PUBLISHED_BUILD,
|
|
4
5
|
LOCAL_REGISTRY_URL,
|
|
6
|
+
PUBLISHED_ENVIRONMENT,
|
|
5
7
|
STALE_CONTRACT_ARTIFACT_CODE,
|
|
6
8
|
STALE_CONTRACT_ARTIFACT_EXIT_CODE,
|
|
7
9
|
buildClerkAuthorizationUrl,
|
|
@@ -62,7 +64,7 @@ import {
|
|
|
62
64
|
toDreamboardApiError,
|
|
63
65
|
valueOrUndefined,
|
|
64
66
|
waitForCompiledResultJobSdk
|
|
65
|
-
} from "./chunk-
|
|
67
|
+
} from "./chunk-2WB3DYW4.js";
|
|
66
68
|
import {
|
|
67
69
|
applyWorkspaceCodegen,
|
|
68
70
|
assertCliStaticScaffoldComplete,
|
|
@@ -114,10 +116,8 @@ import {
|
|
|
114
116
|
writeSnapshotFromFiles,
|
|
115
117
|
writeSourceFiles,
|
|
116
118
|
writeWorkspaceTextFile
|
|
117
|
-
} from "./chunk-
|
|
119
|
+
} from "./chunk-2XMBZPL5.js";
|
|
118
120
|
import {
|
|
119
|
-
IS_PUBLISHED_BUILD,
|
|
120
|
-
PUBLISHED_ENVIRONMENT,
|
|
121
121
|
clearCredentials,
|
|
122
122
|
getActiveCredentialBackendName,
|
|
123
123
|
getGlobalAuthPath,
|
|
@@ -127,7 +127,7 @@ import {
|
|
|
127
127
|
saveGlobalConfig,
|
|
128
128
|
setAccessOnlySession,
|
|
129
129
|
setCredentials
|
|
130
|
-
} from "./chunk-
|
|
130
|
+
} from "./chunk-J3CWZHY7.js";
|
|
131
131
|
import {
|
|
132
132
|
DEFAULT_LOGIN_TIMEOUT_MS,
|
|
133
133
|
MANIFEST_FILE,
|
package/dist/internal.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
resolveConfig,
|
|
11
11
|
resolveProjectContext,
|
|
12
12
|
shortHash
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-2WB3DYW4.js";
|
|
14
14
|
import {
|
|
15
15
|
applyWorkspaceCodegen,
|
|
16
16
|
loadManifest,
|
|
@@ -19,11 +19,11 @@ import {
|
|
|
19
19
|
setLatestCompileAttempt,
|
|
20
20
|
updateProjectState,
|
|
21
21
|
writeSnapshot
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-2XMBZPL5.js";
|
|
23
23
|
import {
|
|
24
24
|
getStoredSession,
|
|
25
25
|
loadGlobalConfig
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-J3CWZHY7.js";
|
|
27
27
|
import {
|
|
28
28
|
ENVIRONMENT_CONFIGS,
|
|
29
29
|
readJsonFile,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/config/keychain-backend.ts"],"sourcesContent":["/**\n * OS keychain-backed `CredentialBackend` built on top of `@napi-rs/keyring`.\n *\n * Keychain is an optional storage backend. Users can opt in with\n * `credentialBackend: \"keychain\"` in `~/.dreamboard/config.json`\n * (see `credential-store.ts` for the resolver).\n * When enabled, it gives us:\n * - A refresh token encrypted at rest by the OS (Keychain on macOS,\n * Credential Vault on Windows, Secret Service on Linux).\n * - Protection against other processes running as the same user tailing\n * `~/.dreamboard/auth.json` to scrape the token.\n *\n * This module is loaded optionally: `@napi-rs/keyring` is declared as an\n * `optionalDependencies` entry. If the native binary or OS keyring is\n * unavailable, the resolver falls back to the file backend.\n *\n * One-time migration: when the active backend is keychain and `auth.json` still\n * has tokens, `credential-store.ts` copies them into the keychain, verifies the\n * keychain read, and deletes the file. This is the only path that intentionally\n * mutates both backends.\n */\n\nimport type {\n CredentialBackend,\n Credentials,\n StoredSessionSnapshot,\n} from \"./credential-store.js\";\n\n/** Keychain service id. Shared across all Dreamboard CLI builds. */\nconst KEYCHAIN_SERVICE = \"dreamboard-cli\";\n/**\n * Keychain account id. The `user@host` shape is conventional but we keep\n * it fixed for now because the CLI only cares about \"the session for this\n * OS user\", not per-process sessions.\n */\nconst KEYCHAIN_ACCOUNT = \"session\";\n\ntype EntryInstance = {\n setPassword(value: string): void;\n getPassword(): string | null | undefined;\n deletePassword(): boolean;\n};\n\ntype KeyringModule = {\n Entry: new (service: string, account: string) => EntryInstance;\n};\n\nlet cachedModule: KeyringModule | null | undefined;\n\nasync function loadKeyringModule(): Promise<KeyringModule | null> {\n if (cachedModule !== undefined) return cachedModule;\n try {\n // `@napi-rs/keyring` is an optional dependency. If the native binary is\n // missing on this platform the dynamic import throws; resolver policy in\n // credential-store decides whether that is fatal.\n const mod = (await import(\"@napi-rs/keyring\")) as unknown as KeyringModule;\n cachedModule = mod;\n } catch {\n cachedModule = null;\n }\n return cachedModule;\n}\n\nfunction keychainProbe(entry: EntryInstance): boolean {\n // Some platforms have the module installed but no accessible keyring\n // (e.g. headless Linux without DBus). Touch getPassword to verify we\n // can talk to the service without side effects.\n try {\n entry.getPassword();\n return true;\n } catch {\n return false;\n }\n}\n\ntype KeychainPayload = {\n clerkAccessToken?: string;\n accessToken?: string;\n refreshToken?: string;\n clerkAccessExpiresAt?: string;\n tokenExpiresAt?: string;\n dreamboardApiToken?: string;\n dreamboardApiExpiresAt?: string;\n clerkOAuthIssuer?: string;\n clerkOAuthClientId?: string;\n clerkOAuthTokenUrl?: string;\n environment?: string;\n};\n\nfunction parsePayload(\n raw: string | null | undefined,\n): StoredSessionSnapshot | null {\n if (raw === null || raw === undefined) return null;\n const trimmed = raw.trim();\n if (trimmed.length === 0) return null;\n try {\n const parsed = JSON.parse(trimmed) as KeychainPayload;\n const accessToken = parsed.clerkAccessToken ?? parsed.accessToken;\n if (!accessToken && !parsed.refreshToken) return null;\n return {\n accessToken: accessToken || undefined,\n refreshToken: parsed.refreshToken || undefined,\n tokenExpiresAt:\n parsed.clerkAccessExpiresAt || parsed.tokenExpiresAt || undefined,\n dreamboardApiToken: parsed.dreamboardApiToken || undefined,\n dreamboardApiExpiresAt: parsed.dreamboardApiExpiresAt || undefined,\n clerkOAuthIssuer: parsed.clerkOAuthIssuer || undefined,\n clerkOAuthClientId: parsed.clerkOAuthClientId || undefined,\n clerkOAuthTokenUrl: parsed.clerkOAuthTokenUrl || undefined,\n environment: parsed.environment || undefined,\n };\n } catch {\n return null;\n }\n}\n\nfunction writeFull(entry: EntryInstance, creds: Credentials): void {\n if (!creds.accessToken || !creds.refreshToken) {\n throw new Error(\n \"Refusing to persist credentials with an empty accessToken or refreshToken.\",\n );\n }\n const payload: KeychainPayload = {\n clerkAccessToken: creds.accessToken,\n refreshToken: creds.refreshToken,\n clerkAccessExpiresAt: creds.tokenExpiresAt,\n dreamboardApiToken: creds.dreamboardApiToken,\n dreamboardApiExpiresAt: creds.dreamboardApiExpiresAt,\n clerkOAuthIssuer: creds.clerkOAuthIssuer,\n clerkOAuthClientId: creds.clerkOAuthClientId,\n clerkOAuthTokenUrl: creds.clerkOAuthTokenUrl,\n environment: creds.environment,\n };\n entry.setPassword(JSON.stringify(payload));\n}\n\nfunction writeAccessOnly(entry: EntryInstance, accessToken: string): void {\n if (!accessToken) {\n throw new Error(\"Refusing to persist an empty access token.\");\n }\n const payload: KeychainPayload = { accessToken };\n entry.setPassword(JSON.stringify(payload));\n}\n\nfunction clear(entry: EntryInstance): void {\n try {\n entry.deletePassword();\n } catch {\n // keyring-rs throws when the entry does not exist. That is fine -\n // `clearCredentials` contracts as idempotent.\n }\n}\n\nexport type KeychainAvailability =\n | { available: true; backend: CredentialBackend }\n | { available: false; reason: string };\n\n/**\n * Attempt to construct a keychain-backed `CredentialBackend`. Returns an\n * `available: false` result (with a reason) if the native module, the\n * OS keyring, or the probe fails.\n */\nexport async function tryKeychainBackend(): Promise<KeychainAvailability> {\n const mod = await loadKeyringModule();\n if (!mod) {\n return {\n available: false,\n reason: \"@napi-rs/keyring is not installed for this platform\",\n };\n }\n\n let entry: EntryInstance;\n try {\n entry = new mod.Entry(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);\n } catch (err) {\n return {\n available: false,\n reason: `Failed to construct keyring entry: ${String((err as Error).message ?? err)}`,\n };\n }\n\n if (!keychainProbe(entry)) {\n return {\n available: false,\n reason: \"OS keyring is not accessible from this process\",\n };\n }\n\n const backend: CredentialBackend = {\n name: \"keychain\",\n async read() {\n try {\n return parsePayload(entry.getPassword());\n } catch (err) {\n const message = String((err as Error).message ?? err);\n // Transient keychain access errors (e.g. Touch ID prompt\n // cancelled) should not surface as \"session wiped\". Treat the\n // unreadable state as \"no session\" so the caller can fall back\n // to prompting for login.\n if (/no matching entry|not found/i.test(message)) {\n return null;\n }\n throw err;\n }\n },\n async writeFull(creds) {\n writeFull(entry, creds);\n },\n async writeAccessOnly(accessToken) {\n writeAccessOnly(entry, accessToken);\n },\n async clear() {\n clear(entry);\n },\n };\n return { available: true, backend };\n}\n\n/**\n * Test-only escape hatch so unit tests can install a fake keyring module\n * without going through the dynamic import cache.\n */\nexport function _setKeyringModuleForTests(mod: KeyringModule | null): void {\n cachedModule = mod;\n}\n\nexport function _resetKeyringModuleForTests(): void {\n cachedModule = undefined;\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
"packages": {
|
|
5
5
|
"cli": {
|
|
6
6
|
"name": "@dreamboard-games/cli",
|
|
7
|
-
"version": "0.1.30-alpha.
|
|
7
|
+
"version": "0.1.30-alpha.18"
|
|
8
8
|
},
|
|
9
9
|
"sdk": {
|
|
10
10
|
"name": "@dreamboard-games/sdk",
|
|
11
|
-
"version": "0.4.0-alpha.
|
|
11
|
+
"version": "0.4.0-alpha.5"
|
|
12
12
|
},
|
|
13
13
|
"apiClient": {
|
|
14
14
|
"name": "@dreamboard-games/api-client",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"devHost": {
|
|
18
18
|
"name": "@dreamboard-games/dev-host",
|
|
19
|
-
"version": "0.1.30-alpha.
|
|
19
|
+
"version": "0.1.30-alpha.18"
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
"protocols": {
|
|
@@ -34,5 +34,5 @@
|
|
|
34
34
|
"portable": true
|
|
35
35
|
},
|
|
36
36
|
"packageManager": "pnpm@10.4.1",
|
|
37
|
-
"releaseSetId": "sha256:
|
|
37
|
+
"releaseSetId": "sha256:ef52cd34cbdc789fc58354981361c0ef516e4be7fef48c45a1e7534fd0b32c2e"
|
|
38
38
|
}
|
|
@@ -40,6 +40,8 @@ See [tutorials/building-your-first-game.md](references/building-your-first-game.
|
|
|
40
40
|
[references/game-interface.md](references/game-interface.md)
|
|
41
41
|
- Testing:
|
|
42
42
|
[references/testing.md](references/testing.md)
|
|
43
|
+
- Canonical concepts:
|
|
44
|
+
[references/canonical-concepts.md](references/canonical-concepts.md)
|
|
43
45
|
|
|
44
46
|
## Current Scaffold
|
|
45
47
|
|
|
@@ -99,6 +101,12 @@ Use this order by default:
|
|
|
99
101
|
- Keep reducer-owned UI data in views; do not reintroduce the old `shared/ui-args.ts` pattern in new scaffolds.
|
|
100
102
|
- When a game exposes clickable hands, markets, boards, or prompts, prove the same interaction works through `dreamboard dev` in a browser. A direct scenario submission can pass even when the rendered surface does not collect the input.
|
|
101
103
|
- For interactive card hands, render generated surfaces such as `handSurface.Hand` and `handSurface.Card` consistently. Do not swap a surface card for a raw `Card` or custom tile based on `me.canAct`; the surface primitive is responsible for disabling unavailable interactions.
|
|
104
|
+
- For scorecards, grids, and compact tracks, use board topology and generated board surfaces. Do not invent a separate sheet model or duplicate cell state outside reducer authority.
|
|
105
|
+
- Derive inventory from `manifest.json` and generated manifest helpers instead of declaring cards, dice, pieces, resources, or boards twice.
|
|
106
|
+
- Use reducer-owned terminal outcomes for final results. Do not infer winners in UI from a `winnerPlayerId` convention or score sorting.
|
|
107
|
+
- Display descriptor availability and reducer-projected disabled reasons. Do not reimplement action legality in React.
|
|
108
|
+
- Model solo procedures and automa rivals as deterministic reducer phases, state transitions, and game events. Do not create fake player seats for non-human behavior.
|
|
109
|
+
- Start from the smallest matching canonical reference game before inventing framework structure. See [references/canonical-concepts.md](references/canonical-concepts.md).
|
|
102
110
|
|
|
103
111
|
## Editable Surface
|
|
104
112
|
|
|
@@ -162,7 +162,7 @@ const playerId = manifestContract.ids.playerId;
|
|
|
162
162
|
|
|
163
163
|
const publicStateSchema = z.object({
|
|
164
164
|
currentPlayerId: playerId,
|
|
165
|
-
|
|
165
|
+
gameOver: z.boolean(),
|
|
166
166
|
lastRoll: z.number().int().min(1).max(6).nullable(),
|
|
167
167
|
scores: z.record(playerId, z.number().int().nonnegative()),
|
|
168
168
|
});
|
|
@@ -182,7 +182,7 @@ export const gameContract = defineGameContract({
|
|
|
182
182
|
initial: {
|
|
183
183
|
public: ({ playerIds }) => ({
|
|
184
184
|
currentPlayerId: playerIds[0],
|
|
185
|
-
|
|
185
|
+
gameOver: false,
|
|
186
186
|
lastRoll: null,
|
|
187
187
|
scores: Object.fromEntries(playerIds.map((playerId) => [playerId, 0])),
|
|
188
188
|
}),
|
|
@@ -213,7 +213,7 @@ import type { GameContract } from "./game-contract";
|
|
|
213
213
|
export const playerView = defineView<GameContract>()({
|
|
214
214
|
schema: z.object({
|
|
215
215
|
currentPlayerId: z.string(),
|
|
216
|
-
|
|
216
|
+
gameOver: z.boolean(),
|
|
217
217
|
lastRoll: z.number().nullable(),
|
|
218
218
|
scores: z.record(z.string(), z.number()),
|
|
219
219
|
isMyTurn: z.boolean(),
|
|
@@ -223,10 +223,12 @@ export const playerView = defineView<GameContract>()({
|
|
|
223
223
|
project({ state, playerId }) {
|
|
224
224
|
return {
|
|
225
225
|
currentPlayerId: state.publicState.currentPlayerId,
|
|
226
|
-
|
|
226
|
+
gameOver: state.publicState.gameOver,
|
|
227
227
|
lastRoll: state.publicState.lastRoll,
|
|
228
228
|
scores: state.publicState.scores,
|
|
229
|
-
isMyTurn:
|
|
229
|
+
isMyTurn:
|
|
230
|
+
!state.publicState.gameOver &&
|
|
231
|
+
state.publicState.currentPlayerId === playerId,
|
|
230
232
|
targetScore: 10,
|
|
231
233
|
turnDieId: "turn-die",
|
|
232
234
|
};
|
|
@@ -251,7 +253,7 @@ const TURN_DIE_ID = "turn-die" as const;
|
|
|
251
253
|
const rollDie = defineAction<GameContract>()({
|
|
252
254
|
params: z.object({}),
|
|
253
255
|
validate({ state, input }) {
|
|
254
|
-
if (state.publicState.
|
|
256
|
+
if (state.publicState.gameOver) {
|
|
255
257
|
return {
|
|
256
258
|
errorCode: "GAME_ALREADY_ENDED",
|
|
257
259
|
message: "The game has already ended.",
|
|
@@ -267,7 +269,7 @@ const rollDie = defineAction<GameContract>()({
|
|
|
267
269
|
|
|
268
270
|
return null;
|
|
269
271
|
},
|
|
270
|
-
reduce({ state, input, accept }) {
|
|
272
|
+
reduce({ state, input, accept, endGame }) {
|
|
271
273
|
const nextRoll = (state.hiddenState.rollCount % 6) + 1;
|
|
272
274
|
const nextScore = state.publicState.scores[input.playerId] + nextRoll;
|
|
273
275
|
const nextScores = {
|
|
@@ -289,13 +291,13 @@ const rollDie = defineAction<GameContract>()({
|
|
|
289
291
|
},
|
|
290
292
|
};
|
|
291
293
|
|
|
292
|
-
|
|
294
|
+
const nextState = {
|
|
293
295
|
...state,
|
|
294
296
|
table: nextTable,
|
|
295
297
|
publicState: {
|
|
296
298
|
...state.publicState,
|
|
297
299
|
currentPlayerId: nextScore >= TARGET_SCORE ? input.playerId : nextPlayerId,
|
|
298
|
-
|
|
300
|
+
gameOver: nextScore >= TARGET_SCORE,
|
|
299
301
|
lastRoll: nextRoll,
|
|
300
302
|
scores: nextScores,
|
|
301
303
|
},
|
|
@@ -303,7 +305,27 @@ const rollDie = defineAction<GameContract>()({
|
|
|
303
305
|
...state.hiddenState,
|
|
304
306
|
rollCount: state.hiddenState.rollCount + 1,
|
|
305
307
|
},
|
|
306
|
-
}
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
if (nextScore < TARGET_SCORE) {
|
|
311
|
+
return accept(nextState);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const orderedPlayerIds = Object.keys(nextScores).sort();
|
|
315
|
+
const outcome = {
|
|
316
|
+
reason: {
|
|
317
|
+
code: "TARGET_SCORE_REACHED",
|
|
318
|
+
message: `${input.playerId} reached ${TARGET_SCORE} points.`,
|
|
319
|
+
},
|
|
320
|
+
standings: orderedPlayerIds.map((playerId) => ({
|
|
321
|
+
playerId,
|
|
322
|
+
rank: playerId === input.playerId ? 1 : 2,
|
|
323
|
+
result: playerId === input.playerId ? "win" : "loss",
|
|
324
|
+
score: nextScores[playerId] ?? 0,
|
|
325
|
+
})),
|
|
326
|
+
} as const;
|
|
327
|
+
|
|
328
|
+
return endGame(nextState, outcome);
|
|
307
329
|
},
|
|
308
330
|
});
|
|
309
331
|
|
|
@@ -312,7 +334,7 @@ export const takeTurn = definePhase<GameContract>()({
|
|
|
312
334
|
state: z.object({}),
|
|
313
335
|
initialState: () => ({}),
|
|
314
336
|
enter({ state, accept }) {
|
|
315
|
-
if (state.publicState.
|
|
337
|
+
if (state.publicState.gameOver) {
|
|
316
338
|
return accept(state);
|
|
317
339
|
}
|
|
318
340
|
|
|
@@ -404,8 +426,8 @@ export default function App() {
|
|
|
404
426
|
|
|
405
427
|
<p>Last roll: {view.lastRoll ?? "not rolled yet"}</p>
|
|
406
428
|
|
|
407
|
-
{view.
|
|
408
|
-
<p>
|
|
429
|
+
{view.gameOver ? (
|
|
430
|
+
<p>Game over. Check the result summary.</p>
|
|
409
431
|
) : (
|
|
410
432
|
<button onClick={() => void rollDie()} disabled={!view.isMyTurn}>
|
|
411
433
|
Roll die
|
|
@@ -464,8 +486,8 @@ export default defineScenario({
|
|
|
464
486
|
expect(state.lastRoll).toBe(6);
|
|
465
487
|
expect(state.scores["player-1"]).toBe(9);
|
|
466
488
|
expect(state.scores["player-2"]).toBe(12);
|
|
467
|
-
expect(state.
|
|
468
|
-
expect(view("player-2").
|
|
489
|
+
expect(state.gameOver).toBe(true);
|
|
490
|
+
expect(view("player-2").gameOver).toBe(true);
|
|
469
491
|
expect(history().accepted().length).toBe(6);
|
|
470
492
|
},
|
|
471
493
|
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<!-- Generated by apps/dreamboard-cli/scripts/sync-skill-docs.ts. -->
|
|
2
|
+
<!-- Source: docs/reference/canonical-concepts.mdx -->
|
|
3
|
+
|
|
4
|
+
# Canonical concepts
|
|
5
|
+
|
|
6
|
+
How coding agents should choose Dreamboard concepts for common board-game patterns.
|
|
7
|
+
|
|
8
|
+
Coding agents should start from the smallest canonical concept that matches the
|
|
9
|
+
designer's rules. Do not invent a parallel model when the SDK already has an
|
|
10
|
+
authoritative one.
|
|
11
|
+
|
|
12
|
+
## Choose the concept
|
|
13
|
+
|
|
14
|
+
| Designer asks for | Use | Avoid |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| Roll-and-write scorecards, grids, or compact tracks | Board topology plus `Board.SquareGrid` for square spaces | A separate sheet model, duplicated cell state, or custom target protocol |
|
|
17
|
+
| Ranked winners, ties, score breakdowns, or tie-break evidence | `GameOutcome` with ordered standings | Inferring winners from ad hoc `winnerPlayerId` or score maps |
|
|
18
|
+
| Setup, phase, or action instructions | Reducer-projected guidance and descriptor help | Static UI copy that can drift from reducer authority |
|
|
19
|
+
| Unavailable choices | Descriptor availability and reducer-owned disabled reasons | Recomputing legal actions in React |
|
|
20
|
+
| Solo countdowns or environment procedures | Auto phases and deterministic `GameEvent` output | Fake player seats for the environment |
|
|
21
|
+
| Automa or rival behavior | Deterministic reducer state, system actions, and events | Authenticated bot users or non-human session actors |
|
|
22
|
+
| Existing cards, dice, pieces, boards, or resources | Manifest inventory and generated helpers | Declaring the same inventory again in reducer constants |
|
|
23
|
+
|
|
24
|
+
## Reference examples
|
|
25
|
+
|
|
26
|
+
Use the closest shipped example before writing new architecture.
|
|
27
|
+
|
|
28
|
+
| Game family | Reference game | Main lesson |
|
|
29
|
+
| --- | --- | --- |
|
|
30
|
+
| Trick-taking | `hearts` | Follow-suit rules and hidden hands |
|
|
31
|
+
| Simultaneous drafting | `simultaneous-card-drafting` | Simultaneous choices and reveal timing |
|
|
32
|
+
| Deck-building market | `deck-building-market` | Shared market, costs, and deck zones |
|
|
33
|
+
| Worker placement | `worker-placement-tableau` | Occupied spaces, action slots, and cleanup |
|
|
34
|
+
| Route/network building | `hex-network-trading` | Hex boards, routes, and resource trades |
|
|
35
|
+
| Roll and write | `roll-and-write-scorecard` | Per-player square boards and mobile marks |
|
|
36
|
+
| Multiplayer ranking | `multiplayer-ranking-and-ties` | Standings, ties, and tie-break rows |
|
|
37
|
+
| Solo puzzle | `solo-countdown-puzzle` | Auto phases and deterministic environment events |
|
|
38
|
+
| Automa rival | `automa-river-rival` | Rival state/actions without a fake seat |
|
|
39
|
+
|
|
40
|
+
## Scorecards and square boards
|
|
41
|
+
|
|
42
|
+
Model scorecards as boards. The manifest owns the stable square spaces; reducer
|
|
43
|
+
state owns marks, resources, dice results, and legal targets. UI renders the
|
|
44
|
+
board with generated surface bindings and `Board.SquareGrid`.
|
|
45
|
+
|
|
46
|
+
Do not add a second sheet schema for gameplay state. If a mark changes what a
|
|
47
|
+
player can do, it belongs in reducer state and must be submitted through the
|
|
48
|
+
normal action/collector path.
|
|
49
|
+
|
|
50
|
+
## Outcomes
|
|
51
|
+
|
|
52
|
+
Use `GameOutcome` for terminal results. It carries a reason and ordered
|
|
53
|
+
standings, so ties, losses, scoreless results, score breakdowns, and tie-break
|
|
54
|
+
evidence all survive reducer, host, and product boundaries.
|
|
55
|
+
|
|
56
|
+
Do not decide the winner in UI by sorting scores. React can present standings,
|
|
57
|
+
but reducer authority owns the result.
|
|
58
|
+
|
|
59
|
+
## Guidance and unavailable actions
|
|
60
|
+
|
|
61
|
+
The reducer should project what the player needs to know now: setup guidance,
|
|
62
|
+
phase objective, action help, and why an action is unavailable. The UI should
|
|
63
|
+
display those descriptors. It should not maintain a separate eligibility engine
|
|
64
|
+
or second set of validation messages.
|
|
65
|
+
|
|
66
|
+
## Solo and automa procedures
|
|
67
|
+
|
|
68
|
+
Automated game behavior is still game logic. Implement deterministic
|
|
69
|
+
environment and rival steps as reducer phases, state transitions, and
|
|
70
|
+
`GameEvent` entries. Keep session actors for real humans only.
|
|
71
|
+
|
|
72
|
+
Use events to make automated behavior inspectable after reconnects and history
|
|
73
|
+
navigation. A replay should explain what happened without requiring hidden UI
|
|
74
|
+
state.
|
|
@@ -379,7 +379,7 @@ explicit props.
|
|
|
379
379
|
| `ResourceCounter` | Resource totals |
|
|
380
380
|
| `CostDisplay` | Cost breakdowns |
|
|
381
381
|
| `PhaseIndicator` | Current phase or step status |
|
|
382
|
-
| `
|
|
382
|
+
| `OutcomeDialog` / `StandingsTable` | Reducer-owned terminal outcomes |
|
|
383
383
|
| `DiceRoller` | Reducer-driven dice results |
|
|
384
384
|
| `Drawer` and related exports | Mobile overflow or detail panels |
|
|
385
385
|
|
|
@@ -462,6 +462,9 @@ resource affordability, and loading state.
|
|
|
462
462
|
/>
|
|
463
463
|
```
|
|
464
464
|
|
|
465
|
+
Use reducer-projected descriptor help and availability reasons for disabled
|
|
466
|
+
actions. Do not disable a button by reimplementing reducer rules in React.
|
|
467
|
+
|
|
465
468
|
### `ActionPanel` and `ActionGroup`
|
|
466
469
|
|
|
467
470
|
Use these to keep the action surface grouped by phase or intent.
|
|
@@ -502,7 +505,7 @@ data.
|
|
|
502
505
|
| Export | Use it for |
|
|
503
506
|
| --- | --- |
|
|
504
507
|
| `TrackBoard` | Linear, circular, or branching tracks |
|
|
505
|
-
| `SquareGrid` | Chess-like
|
|
508
|
+
| `SquareGrid` / `Board.SquareGrid` | Chess-like, tile-grid, or roll-and-write square boards |
|
|
506
509
|
| `HexGrid` | Hex maps with tiles, edges, and vertices |
|
|
507
510
|
| `NetworkGraph` | Nodes, connections, and route graphs |
|
|
508
511
|
| `ZoneMap` | Region or area control boards |
|
|
@@ -532,6 +535,16 @@ Board renderers are still UI code. The reducer should provide:
|
|
|
532
535
|
- labels and annotations
|
|
533
536
|
- grouped piece and card data in the shape the component expects
|
|
534
537
|
|
|
538
|
+
For scorecards, use generated board surfaces plus `Board.SquareGrid` instead
|
|
539
|
+
of a separate sheet model. The board topology remains the gameplay target; the
|
|
540
|
+
game UI chooses how to frame and style it.
|
|
541
|
+
|
|
542
|
+
### OutcomeDialog and StandingsTable
|
|
543
|
+
|
|
544
|
+
Use outcome presentation components for `GameOutcome` data produced by the
|
|
545
|
+
reducer. They should receive the outcome payload and player labels; they should
|
|
546
|
+
not sort scores or decide winners themselves.
|
|
547
|
+
|
|
535
548
|
## Exported types
|
|
536
549
|
|
|
537
550
|
Use these root exports when the UI needs to type reducer-owned runtime data.
|
|
@@ -14,6 +14,10 @@ manifest should define stable hex spaces such as `cell-01`, `cell-02`, and
|
|
|
14
14
|
`cell-03`, while the reducer shuffles terrain pieces and number tokens onto
|
|
15
15
|
those spaces at runtime.
|
|
16
16
|
|
|
17
|
+
For roll-and-write scorecards and compact tracks, use board topology rather
|
|
18
|
+
than a separate sheet model. The manifest defines the stable square spaces and
|
|
19
|
+
the reducer owns marks, dice results, legal targets, and scoring state.
|
|
20
|
+
|
|
17
21
|
## Top-level shape
|
|
18
22
|
|
|
19
23
|
```json
|
|
@@ -102,6 +106,10 @@ Use `home` on cards, piece seeds, and die seeds to place authored inventory.
|
|
|
102
106
|
| `vertex` | `boardId`, `ref` | Place onto a hex vertex identified by three spaces |
|
|
103
107
|
| `slot` | `hostComponentId`, `slotId` | Place into a component-owned slot |
|
|
104
108
|
|
|
109
|
+
Do not duplicate authored inventory in reducer constants. Cards, dice, pieces,
|
|
110
|
+
resources, zones, and boards should be read from the generated manifest helpers
|
|
111
|
+
or initialized through manifest-backed defaults.
|
|
112
|
+
|
|
105
113
|
### `ComponentVisibilitySpec`
|
|
106
114
|
|
|
107
115
|
| Field | Required | Notes |
|