@dreamboard-games/cli 0.1.30-alpha.17 → 0.1.30-alpha.19

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.
Files changed (74) hide show
  1. package/README.md +3 -1
  2. package/dist/agent-verifier/agent-workspace-verifier.mjs +12 -12
  3. package/dist/agent-verifier/{chunk-LKQ557TJ.mjs → chunk-334H4LE4.mjs} +3 -3
  4. package/dist/agent-verifier/{chunk-DWLTCUUX.mjs → chunk-3Y4FRMTK.mjs} +6 -6
  5. package/dist/agent-verifier/{chunk-NFL3Z4Z7.mjs → chunk-5D3OJBDT.mjs} +12 -8
  6. package/dist/agent-verifier/{chunk-NFL3Z4Z7.mjs.map → chunk-5D3OJBDT.mjs.map} +1 -1
  7. package/dist/agent-verifier/{chunk-AXXUGU7Q.mjs → chunk-6AKXIY37.mjs} +54 -36
  8. package/dist/agent-verifier/chunk-6AKXIY37.mjs.map +1 -0
  9. package/dist/agent-verifier/{chunk-H3XNWKJU.mjs → chunk-7LFDFXLS.mjs} +2 -2
  10. package/dist/agent-verifier/{chunk-CO3BRUD6.mjs → chunk-AWZ4M4NS.mjs} +15 -3
  11. package/dist/agent-verifier/chunk-AWZ4M4NS.mjs.map +1 -0
  12. package/dist/agent-verifier/{chunk-S4ZQSUPB.mjs → chunk-H5L4KK4Y.mjs} +9 -12
  13. package/dist/agent-verifier/chunk-H5L4KK4Y.mjs.map +1 -0
  14. package/dist/agent-verifier/{chunk-V6AQDR7W.mjs → chunk-HUBV22JQ.mjs} +3 -3
  15. package/dist/agent-verifier/chunk-HUBV22JQ.mjs.map +1 -0
  16. package/dist/agent-verifier/{chunk-A67WUYN2.mjs → chunk-LEWM26XR.mjs} +4 -4
  17. package/dist/agent-verifier/chunk-LEWM26XR.mjs.map +1 -0
  18. package/dist/agent-verifier/{chunk-WAFBU5U7.mjs → chunk-OJFZVGEL.mjs} +38 -13
  19. package/dist/agent-verifier/chunk-OJFZVGEL.mjs.map +1 -0
  20. package/dist/agent-verifier/{chunk-G2ECODRB.mjs → chunk-PLXXH5LY.mjs} +2 -2
  21. package/dist/agent-verifier/{chunk-DPYC2NDB.mjs → chunk-UMW24KZI.mjs} +2 -2
  22. package/dist/agent-verifier/{chunk-5GCZZ6NW.mjs → chunk-ZOR5FTIG.mjs} +2 -2
  23. package/dist/agent-verifier/{compile-SDQ25N7K.mjs → compile-MO2URO5Z.mjs} +12 -12
  24. package/dist/agent-verifier/{global-config-6UGFPLDA.mjs → global-config-SXR6X3OZ.mjs} +3 -3
  25. package/dist/agent-verifier/{keychain-backend-BQLW5VEC.mjs → keychain-backend-UF3Z26JM.mjs} +1 -1
  26. package/dist/agent-verifier/keychain-backend-UF3Z26JM.mjs.map +1 -0
  27. package/dist/agent-verifier/{local-files-WPHUV6GU.mjs → local-files-DAFIR7SN.mjs} +4 -4
  28. package/dist/agent-verifier/{materialize-workspace-ENPMB66P.mjs → materialize-workspace-PWNT6HQK.mjs} +6 -6
  29. package/dist/agent-verifier/{reducer-native-test-harness-GY2CCQWN.mjs → reducer-native-test-harness-X2KQYSCD.mjs} +8 -8
  30. package/dist/agent-verifier/{static-scaffold-K52P52SV.mjs → static-scaffold-HXUQLJVN.mjs} +4 -4
  31. package/dist/agent-verifier/{sync-JSATJXVJ.mjs → sync-5YM4CSXL.mjs} +13 -13
  32. package/dist/agent-verifier/{test-LQAGEQLY.mjs → test-CNNVTFIG.mjs} +11 -11
  33. package/dist/agent-verifier/{workspace-codegen-4IWICKLB.mjs → workspace-codegen-SPPVHURX.mjs} +3 -3
  34. package/dist/authoring-compatibility-internal.js +1 -1
  35. package/dist/{chunk-ITT6Y7MM.js → chunk-R6RB4EKH.js} +23 -9
  36. package/dist/chunk-R6RB4EKH.js.map +1 -0
  37. package/dist/{chunk-AVOAT522.js → chunk-VWMKJL4A.js} +49 -38
  38. package/dist/chunk-VWMKJL4A.js.map +1 -0
  39. package/dist/{chunk-LWJUF42S.js → chunk-YRSE5DLH.js} +45 -23
  40. package/dist/{chunk-LWJUF42S.js.map → chunk-YRSE5DLH.js.map} +1 -1
  41. package/dist/{global-config-NLGAFSRU.js → global-config-UHGWFJIK.js} +2 -2
  42. package/dist/index.js +403 -12
  43. package/dist/index.js.map +1 -1
  44. package/dist/internal.js +3 -3
  45. package/dist/{keychain-backend-47LZ5IX5.js → keychain-backend-GO34KGTG.js} +1 -1
  46. package/dist/keychain-backend-GO34KGTG.js.map +1 -0
  47. package/package.json +1 -1
  48. package/release/authoring-release-set.json +4 -4
  49. package/dist/agent-verifier/chunk-A67WUYN2.mjs.map +0 -1
  50. package/dist/agent-verifier/chunk-AXXUGU7Q.mjs.map +0 -1
  51. package/dist/agent-verifier/chunk-CO3BRUD6.mjs.map +0 -1
  52. package/dist/agent-verifier/chunk-S4ZQSUPB.mjs.map +0 -1
  53. package/dist/agent-verifier/chunk-V6AQDR7W.mjs.map +0 -1
  54. package/dist/agent-verifier/chunk-WAFBU5U7.mjs.map +0 -1
  55. package/dist/agent-verifier/keychain-backend-BQLW5VEC.mjs.map +0 -1
  56. package/dist/chunk-AVOAT522.js.map +0 -1
  57. package/dist/chunk-ITT6Y7MM.js.map +0 -1
  58. package/dist/keychain-backend-47LZ5IX5.js.map +0 -1
  59. /package/dist/agent-verifier/{chunk-LKQ557TJ.mjs.map → chunk-334H4LE4.mjs.map} +0 -0
  60. /package/dist/agent-verifier/{chunk-DWLTCUUX.mjs.map → chunk-3Y4FRMTK.mjs.map} +0 -0
  61. /package/dist/agent-verifier/{chunk-H3XNWKJU.mjs.map → chunk-7LFDFXLS.mjs.map} +0 -0
  62. /package/dist/agent-verifier/{chunk-G2ECODRB.mjs.map → chunk-PLXXH5LY.mjs.map} +0 -0
  63. /package/dist/agent-verifier/{chunk-DPYC2NDB.mjs.map → chunk-UMW24KZI.mjs.map} +0 -0
  64. /package/dist/agent-verifier/{chunk-5GCZZ6NW.mjs.map → chunk-ZOR5FTIG.mjs.map} +0 -0
  65. /package/dist/agent-verifier/{compile-SDQ25N7K.mjs.map → compile-MO2URO5Z.mjs.map} +0 -0
  66. /package/dist/agent-verifier/{global-config-6UGFPLDA.mjs.map → global-config-SXR6X3OZ.mjs.map} +0 -0
  67. /package/dist/agent-verifier/{local-files-WPHUV6GU.mjs.map → local-files-DAFIR7SN.mjs.map} +0 -0
  68. /package/dist/agent-verifier/{materialize-workspace-ENPMB66P.mjs.map → materialize-workspace-PWNT6HQK.mjs.map} +0 -0
  69. /package/dist/agent-verifier/{reducer-native-test-harness-GY2CCQWN.mjs.map → reducer-native-test-harness-X2KQYSCD.mjs.map} +0 -0
  70. /package/dist/agent-verifier/{static-scaffold-K52P52SV.mjs.map → static-scaffold-HXUQLJVN.mjs.map} +0 -0
  71. /package/dist/agent-verifier/{sync-JSATJXVJ.mjs.map → sync-5YM4CSXL.mjs.map} +0 -0
  72. /package/dist/agent-verifier/{test-LQAGEQLY.mjs.map → test-CNNVTFIG.mjs.map} +0 -0
  73. /package/dist/agent-verifier/{workspace-codegen-4IWICKLB.mjs.map → workspace-codegen-SPPVHURX.mjs.map} +0 -0
  74. /package/dist/{global-config-NLGAFSRU.js.map → global-config-UHGWFJIK.js.map} +0 -0
@@ -15,20 +15,36 @@ 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
18
+ var credentialDirectoryOverrideForTests = null;
19
+ function getCredentialDirectory() {
20
+ return credentialDirectoryOverrideForTests ?? path.join(os.homedir(), PROJECT_DIR_NAME);
21
+ }
26
22
  function getCredentialFilePath() {
27
- return path.join(os.homedir(), PROJECT_DIR_NAME, "auth.json");
23
+ return path.join(getCredentialDirectory(), "auth.json");
24
+ }
25
+ function getCredentialAuditLogPath() {
26
+ return path.join(getCredentialDirectory(), "auth-events.log");
28
27
  }
29
28
  function getCredentialLockPath() {
30
29
  return `${getCredentialFilePath()}.lock`;
31
30
  }
31
+ async function appendCredentialAuditEvent(event) {
32
+ try {
33
+ const logPath = getCredentialAuditLogPath();
34
+ await fs.mkdir(path.dirname(logPath), { recursive: true, mode: 448 });
35
+ await fs.appendFile(
36
+ logPath,
37
+ `${JSON.stringify({
38
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
39
+ pid: process.pid,
40
+ ...event
41
+ })}
42
+ `,
43
+ { mode: 384 }
44
+ );
45
+ } catch {
46
+ }
47
+ }
32
48
  async function fileRead() {
33
49
  const filePath = getCredentialFilePath();
34
50
  let data;
@@ -94,12 +110,27 @@ async function fileWriteAccessOnly(accessToken) {
94
110
  }
95
111
  await writeFilePayload({ authToken: accessToken });
96
112
  }
97
- async function fileClear() {
113
+ async function fileClear(reason = "credential_store_clear") {
98
114
  const filePath = getCredentialFilePath();
99
115
  try {
100
116
  await fs.unlink(filePath);
117
+ await appendCredentialAuditEvent({
118
+ event: "auth_file_deleted",
119
+ reason,
120
+ authPath: filePath,
121
+ backend: "file"
122
+ });
101
123
  } catch (err) {
102
- if (err.code !== "ENOENT") throw err;
124
+ if (err.code === "ENOENT") {
125
+ await appendCredentialAuditEvent({
126
+ event: "auth_file_delete_missing",
127
+ reason,
128
+ authPath: filePath,
129
+ backend: "file"
130
+ });
131
+ return;
132
+ }
133
+ throw err;
103
134
  }
104
135
  }
105
136
  var fileCredentialBackend = {
@@ -121,19 +152,6 @@ var migrationCompleted = false;
121
152
  var backendResolver = defaultBackendResolver;
122
153
  async function defaultBackendResolver() {
123
154
  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
155
  if (override === "file") {
138
156
  return fileCredentialBackend;
139
157
  }
@@ -146,7 +164,7 @@ async function defaultBackendResolver() {
146
164
  if (!useKeychain) {
147
165
  return fileCredentialBackend;
148
166
  }
149
- const { tryKeychainBackend } = await import("./keychain-backend-47LZ5IX5.js");
167
+ const { tryKeychainBackend } = await import("./keychain-backend-GO34KGTG.js");
150
168
  const keychain = await tryKeychainBackend();
151
169
  if (keychain.available) {
152
170
  return keychain.backend;
@@ -155,7 +173,7 @@ async function defaultBackendResolver() {
155
173
  }
156
174
  async function readCredentialBackendPreference() {
157
175
  try {
158
- const { loadGlobalConfig: loadGlobalConfig2 } = await import("./global-config-NLGAFSRU.js");
176
+ const { loadGlobalConfig: loadGlobalConfig2 } = await import("./global-config-UHGWFJIK.js");
159
177
  const config = await loadGlobalConfig2();
160
178
  return config.credentialBackend === "keychain";
161
179
  } catch {
@@ -166,9 +184,7 @@ async function getCredentialBackend() {
166
184
  if (cachedBackend === null) {
167
185
  cachedBackend = await backendResolver();
168
186
  if (!migrationCompleted && cachedBackend.name !== "file") {
169
- await migrateFromFileBackendIfNeeded(cachedBackend, {
170
- failClosed: IS_PUBLISHED_BUILD
171
- });
187
+ await migrateFromFileBackendIfNeeded(cachedBackend);
172
188
  }
173
189
  migrationCompleted = true;
174
190
  }
@@ -182,7 +198,6 @@ async function migrateFromFileBackendIfNeeded(target, options = {}) {
182
198
  ]);
183
199
  if (!onDisk) return;
184
200
  if (onTarget) {
185
- await fileCredentialBackend.clear();
186
201
  return;
187
202
  }
188
203
  if (onDisk.accessToken && onDisk.refreshToken) {
@@ -208,7 +223,6 @@ async function migrateFromFileBackendIfNeeded(target, options = {}) {
208
223
  } else {
209
224
  return;
210
225
  }
211
- await fileCredentialBackend.clear();
212
226
  } catch (error) {
213
227
  if (options.failClosed) {
214
228
  throw new CredentialStoreUnavailableError(
@@ -246,10 +260,10 @@ async function setAccessOnlySession(accessToken) {
246
260
  await backend.writeAccessOnly(accessToken);
247
261
  });
248
262
  }
249
- async function clearCredentials() {
263
+ async function clearCredentials(reason = "credential_store_clear") {
250
264
  await withFileLock(getCredentialLockPath(), async () => {
251
265
  const backend = await getCredentialBackend();
252
- await backend.clear();
266
+ await backend.clear(reason);
253
267
  });
254
268
  }
255
269
  async function withCredentialLock(fn, options) {
@@ -262,7 +276,7 @@ async function withCredentialLock(fn, options) {
262
276
  read: () => backend.read(),
263
277
  writeFull: (creds) => backend.writeFull(creds),
264
278
  writeAccessOnly: (accessToken) => backend.writeAccessOnly(accessToken),
265
- clear: () => backend.clear()
279
+ clear: (reason) => backend.clear(reason)
266
280
  };
267
281
  return fn(ops);
268
282
  },
@@ -306,9 +320,6 @@ async function saveGlobalConfig(config) {
306
320
  }
307
321
 
308
322
  export {
309
- BUILD_CHANNEL,
310
- IS_PUBLISHED_BUILD,
311
- PUBLISHED_ENVIRONMENT,
312
323
  getActiveCredentialBackendName,
313
324
  getStoredSession,
314
325
  setCredentials,
@@ -320,4 +331,4 @@ export {
320
331
  loadGlobalConfig,
321
332
  saveGlobalConfig
322
333
  };
323
- //# sourceMappingURL=chunk-AVOAT522.js.map
334
+ //# sourceMappingURL=chunk-VWMKJL4A.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(reason?: CredentialClearReason): 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(reason?: CredentialClearReason): Promise<void>;\n};\n\nexport type CredentialClearReason =\n | \"auth_clear_command\"\n | \"logout_command\"\n | \"user_token_manager_logout\"\n | \"credential_store_clear\";\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\nlet credentialDirectoryOverrideForTests: string | null = null;\n\nfunction getCredentialDirectory(): string {\n return (\n credentialDirectoryOverrideForTests ??\n path.join(os.homedir(), PROJECT_DIR_NAME)\n );\n}\n\nexport function getCredentialFilePath(): string {\n return path.join(getCredentialDirectory(), \"auth.json\");\n}\n\nexport function getCredentialAuditLogPath(): string {\n return path.join(getCredentialDirectory(), \"auth-events.log\");\n}\n\nfunction getCredentialLockPath(): string {\n return `${getCredentialFilePath()}.lock`;\n}\n\nasync function appendCredentialAuditEvent(event: {\n readonly event: \"auth_file_deleted\" | \"auth_file_delete_missing\";\n readonly reason: CredentialClearReason;\n readonly authPath: string;\n readonly backend: CredentialBackendName;\n}): Promise<void> {\n try {\n const logPath = getCredentialAuditLogPath();\n await fs.mkdir(path.dirname(logPath), { recursive: true, mode: 0o700 });\n await fs.appendFile(\n logPath,\n `${JSON.stringify({\n timestamp: new Date().toISOString(),\n pid: process.pid,\n ...event,\n })}\\n`,\n { mode: 0o600 },\n );\n } catch {\n // Credential clearing must not fail because local diagnostic logging failed.\n }\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(\n reason: CredentialClearReason = \"credential_store_clear\",\n): Promise<void> {\n const filePath = getCredentialFilePath();\n try {\n await fs.unlink(filePath);\n await appendCredentialAuditEvent({\n event: \"auth_file_deleted\",\n reason,\n authPath: filePath,\n backend: \"file\",\n });\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n await appendCredentialAuditEvent({\n event: \"auth_file_delete_missing\",\n reason,\n authPath: filePath,\n backend: \"file\",\n });\n return;\n }\n 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. The file is intentionally left in place; implicit backend\n // migration must not make a working CLI session appear to vanish from\n // the default file-backed view.\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. Leave the\n // file copy alone so a transient keychain override/probe cannot remove\n // the visible file-backed session.\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 } 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(\n reason: CredentialClearReason = \"credential_store_clear\",\n): Promise<void> {\n await withFileLock(getCredentialLockPath(), async () => {\n const backend = await getCredentialBackend();\n await backend.clear(reason);\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: (reason) => backend.clear(reason),\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 credentialDirectoryOverrideForTests = null;\n}\n\n/** Test-only override of the credential directory. Not exported through the barrel. */\nexport function _setCredentialDirectoryForTests(directory: string | null): void {\n credentialDirectoryOverrideForTests = directory;\n cachedBackend = null;\n migrationCompleted = false;\n}\n"],"mappings":"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","names":["os","path","loadGlobalConfig","path","os"]}
@@ -17897,8 +17897,19 @@ function isValidGeneratedPath(relativePath) {
17897
17897
  }
17898
17898
  return !relativePath.split("/").some((segment) => segment.length === 0 || segment === "." || segment === "..");
17899
17899
  }
17900
+ var ALLOWED_GENERATED_PREFIXES = [
17901
+ "app/",
17902
+ "shared/",
17903
+ "test/generated/",
17904
+ "ui/"
17905
+ ];
17906
+ function isAllowedGeneratedPath(relativePath) {
17907
+ return ALLOWED_GENERATED_PREFIXES.some(
17908
+ (prefix) => relativePath.startsWith(prefix)
17909
+ );
17910
+ }
17900
17911
  function assertGeneratedPath(pathValue, label) {
17901
- if (typeof pathValue !== "string" || !isValidGeneratedPath(pathValue)) {
17912
+ if (typeof pathValue !== "string" || !isValidGeneratedPath(pathValue) || !isAllowedGeneratedPath(pathValue)) {
17902
17913
  throw new ProjectAuthoringError(
17903
17914
  "GENERATED_PATH_CONTRACT_INVALID",
17904
17915
  `${label} must be a normalized relative workspace path.`
@@ -18000,9 +18011,23 @@ function validateProjectAuthoringAdapter(adapter) {
18000
18011
  }
18001
18012
  seen.add(normalized);
18002
18013
  }
18014
+ if (adapter.generatedPathPatterns !== void 0 && !Array.isArray(adapter.generatedPathPatterns)) {
18015
+ throw new ProjectAuthoringError(
18016
+ "GENERATED_PATH_CONTRACT_INVALID",
18017
+ "SDK authoring adapter generatedPathPatterns must be an array."
18018
+ );
18019
+ }
18020
+ for (const [index, pattern] of (adapter.generatedPathPatterns ?? []).entries()) {
18021
+ if (!isRecord(pattern) || typeof pattern.prefix !== "string" || typeof pattern.suffix !== "string" || !isValidGeneratedPath(`${pattern.prefix}placeholder${pattern.suffix}`) || !isAllowedGeneratedPath(`${pattern.prefix}placeholder${pattern.suffix}`)) {
18022
+ throw new ProjectAuthoringError(
18023
+ "GENERATED_PATH_CONTRACT_INVALID",
18024
+ `generatedPathPatterns[${index}] must describe a normalized allowlisted workspace path.`
18025
+ );
18026
+ }
18027
+ }
18003
18028
  return adapter;
18004
18029
  }
18005
- function validateGeneratedArtifacts(artifacts) {
18030
+ function validateGeneratedArtifacts(adapter, artifacts) {
18006
18031
  const seen = /* @__PURE__ */ new Set();
18007
18032
  const validated = artifacts.map(assertGeneratedArtifact);
18008
18033
  for (const artifact of validated) {
@@ -18012,6 +18037,15 @@ function validateGeneratedArtifacts(artifacts) {
18012
18037
  `Generated artifact path '${artifact.path}' was emitted more than once.`
18013
18038
  );
18014
18039
  }
18040
+ const declared = adapter.generatedPaths.includes(artifact.path) || (adapter.generatedPathPatterns ?? []).some(
18041
+ (pattern) => artifact.path.startsWith(pattern.prefix) && artifact.path.endsWith(pattern.suffix) && artifact.path.length > pattern.prefix.length + pattern.suffix.length
18042
+ );
18043
+ if (!declared) {
18044
+ throw new ProjectAuthoringError(
18045
+ "GENERATED_PATH_CONTRACT_INVALID",
18046
+ `Generated artifact path '${artifact.path}' is not declared by the SDK authoring adapter.`
18047
+ );
18048
+ }
18015
18049
  seen.add(artifact.path);
18016
18050
  }
18017
18051
  return validated;
@@ -18040,12 +18074,6 @@ function assertResolvedInsidePackage(options) {
18040
18074
  );
18041
18075
  }
18042
18076
  }
18043
- function isAuthoringMetadataVersionCompatible(options) {
18044
- if (options.metadataVersion === options.packageVersion) {
18045
- return true;
18046
- }
18047
- return options.packageVersion.startsWith(`${options.metadataVersion}-local.`);
18048
- }
18049
18077
  function isRecord2(value) {
18050
18078
  return typeof value === "object" && value !== null && !Array.isArray(value);
18051
18079
  }
@@ -18150,10 +18178,7 @@ async function loadProjectAuthoringAdapter(projectRoot) {
18150
18178
  const adapter = validateProjectAuthoringAdapter(
18151
18179
  moduleRecord.projectAuthoringAdapter ?? moduleRecord.default
18152
18180
  );
18153
- if (!isAuthoringMetadataVersionCompatible({
18154
- metadataVersion: adapter.metadata.sdkVersion,
18155
- packageVersion: packageJson.version
18156
- })) {
18181
+ if (adapter.metadata.sdkVersion !== packageJson.version) {
18157
18182
  throw new ProjectAuthoringError(
18158
18183
  "SDK_METADATA_MISMATCH",
18159
18184
  `SDK authoring adapter reports version ${adapter.metadata.sdkVersion}, but package metadata is ${packageJson.version}.`
@@ -18678,7 +18703,7 @@ function isFrameworkOwnedSetupProfilesSeed(content) {
18678
18703
  async function applyWorkspaceCodegen(options) {
18679
18704
  const { projectRoot, manifest } = options;
18680
18705
  const { adapter } = await loadProjectAuthoringAdapter(projectRoot);
18681
- const artifacts = validateGeneratedArtifacts([
18706
+ const artifacts = validateGeneratedArtifacts(adapter, [
18682
18707
  ...adapter.generateWorkspaceArtifacts(manifest),
18683
18708
  ...adapter.generateTestArtifacts({ manifest })
18684
18709
  ]);
@@ -18970,11 +18995,11 @@ var AUTHORING_RELEASE_SET = {
18970
18995
  "packages": {
18971
18996
  "cli": {
18972
18997
  "name": "@dreamboard-games/cli",
18973
- "version": "0.1.30-alpha.17"
18998
+ "version": "0.1.30-alpha.19"
18974
18999
  },
18975
19000
  "sdk": {
18976
19001
  "name": "@dreamboard-games/sdk",
18977
- "version": "0.4.0-alpha.5"
19002
+ "version": "0.4.0-alpha.6"
18978
19003
  },
18979
19004
  "apiClient": {
18980
19005
  "name": "@dreamboard-games/api-client",
@@ -18982,7 +19007,7 @@ var AUTHORING_RELEASE_SET = {
18982
19007
  },
18983
19008
  "devHost": {
18984
19009
  "name": "@dreamboard-games/dev-host",
18985
- "version": "0.1.30-alpha.17"
19010
+ "version": "0.1.30-alpha.19"
18986
19011
  }
18987
19012
  },
18988
19013
  "protocols": {
@@ -19000,7 +19025,7 @@ var AUTHORING_RELEASE_SET = {
19000
19025
  "portable": true
19001
19026
  },
19002
19027
  "packageManager": "pnpm@10.4.1",
19003
- "releaseSetId": "sha256:379c5fbf570f4d63e100777b13f90707adfcd900276434bb6b863240dc43f1b6"
19028
+ "releaseSetId": "sha256:94f9e944361da712ec7f0b420643d3690e73b93a2c23d3a893b0f4183a93a847"
19004
19029
  };
19005
19030
 
19006
19031
  // src/templates/testing-types-content.ts
@@ -19356,14 +19381,11 @@ export default defineScenario({
19356
19381
  "Sanity check that the scaffolded workspace boots into its initial phase.",
19357
19382
  from: "initial-turn",
19358
19383
  when: async () => undefined,
19359
- then: ({ expect, interactions, players, state }) => {
19384
+ then: ({ expect, players, state }) => {
19360
19385
  const playerIds = players();
19361
19386
  expect(playerIds).toHaveLength(playerIds.length);
19362
19387
  expect(playerIds.length).toBeGreaterThanOrEqual(1);
19363
19388
  expect(state()).toBe("setup");
19364
- for (const playerId of playerIds) {
19365
- expect(interactions(playerId)).toEqual([]);
19366
- }
19367
19389
  },
19368
19390
  });
19369
19391
  `;
@@ -19570,7 +19592,7 @@ async function writeInitialScenario(projectRoot, mode) {
19570
19592
  projectRoot,
19571
19593
  "test/scenarios/smoke-initial-turn.scenario.ts"
19572
19594
  );
19573
- if (existing === null || existing.trim().length === 0 || existing === INITIAL_SCENARIO_CONTENT) {
19595
+ if (existing === null || existing.trim().length === 0 || existing.startsWith(GENERATED_SCENARIO_PREFIX)) {
19574
19596
  await writeWorkspaceTextFile(
19575
19597
  projectRoot,
19576
19598
  "test/scenarios/smoke-initial-turn.scenario.ts",
@@ -20422,4 +20444,4 @@ export {
20422
20444
  findProjectRoot,
20423
20445
  materializeWorkspaceProject
20424
20446
  };
20425
- //# sourceMappingURL=chunk-LWJUF42S.js.map
20447
+ //# sourceMappingURL=chunk-YRSE5DLH.js.map