@dreamboard-games/cli 0.1.30-alpha.18 → 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.
- package/dist/agent-verifier/agent-workspace-verifier.mjs +10 -10
- package/dist/agent-verifier/{chunk-7MAOGFFP.mjs → chunk-3Y4FRMTK.mjs} +2 -2
- package/dist/agent-verifier/{chunk-RP7ZWFVH.mjs → chunk-5D3OJBDT.mjs} +4 -4
- package/dist/agent-verifier/{chunk-AQ6UQHPT.mjs → chunk-6AKXIY37.mjs} +52 -8
- package/dist/agent-verifier/chunk-6AKXIY37.mjs.map +1 -0
- package/dist/agent-verifier/{chunk-AG5J3SMN.mjs → chunk-AWZ4M4NS.mjs} +6 -2
- package/dist/agent-verifier/chunk-AWZ4M4NS.mjs.map +1 -0
- package/dist/agent-verifier/{chunk-PWPOLHTW.mjs → chunk-H5L4KK4Y.mjs} +5 -5
- package/dist/agent-verifier/{chunk-PWPOLHTW.mjs.map → chunk-H5L4KK4Y.mjs.map} +1 -1
- package/dist/agent-verifier/{chunk-B42OHJNY.mjs → chunk-LEWM26XR.mjs} +2 -2
- package/dist/agent-verifier/chunk-LEWM26XR.mjs.map +1 -0
- package/dist/agent-verifier/{chunk-RCYO6HWW.mjs → chunk-UMW24KZI.mjs} +2 -2
- package/dist/agent-verifier/{chunk-JB7VXCMB.mjs → chunk-ZOR5FTIG.mjs} +2 -2
- package/dist/agent-verifier/{compile-VOBO2I6D.mjs → compile-MO2URO5Z.mjs} +8 -8
- package/dist/agent-verifier/{global-config-L7PLLUK5.mjs → global-config-SXR6X3OZ.mjs} +3 -3
- package/dist/agent-verifier/{materialize-workspace-PAC75NSP.mjs → materialize-workspace-PWNT6HQK.mjs} +2 -2
- package/dist/agent-verifier/{reducer-native-test-harness-HSXRUGOR.mjs → reducer-native-test-harness-X2KQYSCD.mjs} +4 -4
- package/dist/agent-verifier/{static-scaffold-KSOTKJNQ.mjs → static-scaffold-HXUQLJVN.mjs} +2 -2
- package/dist/agent-verifier/{sync-MQJJEZAA.mjs → sync-5YM4CSXL.mjs} +8 -8
- package/dist/agent-verifier/{test-R6HC6CYZ.mjs → test-CNNVTFIG.mjs} +7 -7
- package/dist/authoring-compatibility-internal.js +1 -1
- package/dist/{chunk-2WB3DYW4.js → chunk-R6RB4EKH.js} +9 -4
- package/dist/chunk-R6RB4EKH.js.map +1 -0
- package/dist/{chunk-J3CWZHY7.js → chunk-VWMKJL4A.js} +47 -10
- package/dist/chunk-VWMKJL4A.js.map +1 -0
- package/dist/{chunk-2XMBZPL5.js → chunk-YRSE5DLH.js} +5 -5
- package/dist/{chunk-2XMBZPL5.js.map → chunk-YRSE5DLH.js.map} +1 -1
- package/dist/{global-config-VQWFTIAV.js → global-config-UHGWFJIK.js} +2 -2
- package/dist/index.js +401 -10
- package/dist/index.js.map +1 -1
- package/dist/internal.js +3 -3
- package/package.json +1 -1
- package/release/authoring-release-set.json +4 -4
- package/skills/dreamboard/SKILL.md +0 -8
- package/skills/dreamboard/references/building-your-first-game.md +15 -37
- package/skills/dreamboard/references/game-interface.md +2 -15
- package/skills/dreamboard/references/manifest-authoring.md +0 -8
- package/skills/dreamboard/references/reducer.md +2 -47
- package/skills/dreamboard/references/rule-authoring.md +0 -10
- package/skills/dreamboard/references/testing.md +3 -7
- package/dist/agent-verifier/chunk-AG5J3SMN.mjs.map +0 -1
- package/dist/agent-verifier/chunk-AQ6UQHPT.mjs.map +0 -1
- package/dist/agent-verifier/chunk-B42OHJNY.mjs.map +0 -1
- package/dist/chunk-2WB3DYW4.js.map +0 -1
- package/dist/chunk-J3CWZHY7.js.map +0 -1
- package/skills/dreamboard/references/canonical-concepts.md +0 -74
- /package/dist/agent-verifier/{chunk-7MAOGFFP.mjs.map → chunk-3Y4FRMTK.mjs.map} +0 -0
- /package/dist/agent-verifier/{chunk-RP7ZWFVH.mjs.map → chunk-5D3OJBDT.mjs.map} +0 -0
- /package/dist/agent-verifier/{chunk-RCYO6HWW.mjs.map → chunk-UMW24KZI.mjs.map} +0 -0
- /package/dist/agent-verifier/{chunk-JB7VXCMB.mjs.map → chunk-ZOR5FTIG.mjs.map} +0 -0
- /package/dist/agent-verifier/{compile-VOBO2I6D.mjs.map → compile-MO2URO5Z.mjs.map} +0 -0
- /package/dist/agent-verifier/{global-config-L7PLLUK5.mjs.map → global-config-SXR6X3OZ.mjs.map} +0 -0
- /package/dist/agent-verifier/{materialize-workspace-PAC75NSP.mjs.map → materialize-workspace-PWNT6HQK.mjs.map} +0 -0
- /package/dist/agent-verifier/{reducer-native-test-harness-HSXRUGOR.mjs.map → reducer-native-test-harness-X2KQYSCD.mjs.map} +0 -0
- /package/dist/agent-verifier/{static-scaffold-KSOTKJNQ.mjs.map → static-scaffold-HXUQLJVN.mjs.map} +0 -0
- /package/dist/agent-verifier/{sync-MQJJEZAA.mjs.map → sync-5YM4CSXL.mjs.map} +0 -0
- /package/dist/agent-verifier/{test-R6HC6CYZ.mjs.map → test-CNNVTFIG.mjs.map} +0 -0
- /package/dist/{global-config-VQWFTIAV.js.map → global-config-UHGWFJIK.js.map} +0 -0
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-R6RB4EKH.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-YRSE5DLH.js";
|
|
23
23
|
import {
|
|
24
24
|
getStoredSession,
|
|
25
25
|
loadGlobalConfig
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-VWMKJL4A.js";
|
|
27
27
|
import {
|
|
28
28
|
ENVIRONMENT_CONFIGS,
|
|
29
29
|
readJsonFile,
|
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.19"
|
|
8
8
|
},
|
|
9
9
|
"sdk": {
|
|
10
10
|
"name": "@dreamboard-games/sdk",
|
|
11
|
-
"version": "0.4.0-alpha.
|
|
11
|
+
"version": "0.4.0-alpha.6"
|
|
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.19"
|
|
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:94f9e944361da712ec7f0b420643d3690e73b93a2c23d3a893b0f4183a93a847"
|
|
38
38
|
}
|
|
@@ -40,8 +40,6 @@ 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)
|
|
45
43
|
|
|
46
44
|
## Current Scaffold
|
|
47
45
|
|
|
@@ -101,12 +99,6 @@ Use this order by default:
|
|
|
101
99
|
- Keep reducer-owned UI data in views; do not reintroduce the old `shared/ui-args.ts` pattern in new scaffolds.
|
|
102
100
|
- 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.
|
|
103
101
|
- 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).
|
|
110
102
|
|
|
111
103
|
## Editable Surface
|
|
112
104
|
|
|
@@ -162,7 +162,7 @@ const playerId = manifestContract.ids.playerId;
|
|
|
162
162
|
|
|
163
163
|
const publicStateSchema = z.object({
|
|
164
164
|
currentPlayerId: playerId,
|
|
165
|
-
|
|
165
|
+
winnerPlayerId: playerId.nullable(),
|
|
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
|
+
winnerPlayerId: null,
|
|
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
|
+
winnerPlayerId: z.string().nullable(),
|
|
217
217
|
lastRoll: z.number().nullable(),
|
|
218
218
|
scores: z.record(z.string(), z.number()),
|
|
219
219
|
isMyTurn: z.boolean(),
|
|
@@ -223,12 +223,10 @@ export const playerView = defineView<GameContract>()({
|
|
|
223
223
|
project({ state, playerId }) {
|
|
224
224
|
return {
|
|
225
225
|
currentPlayerId: state.publicState.currentPlayerId,
|
|
226
|
-
|
|
226
|
+
winnerPlayerId: state.publicState.winnerPlayerId,
|
|
227
227
|
lastRoll: state.publicState.lastRoll,
|
|
228
228
|
scores: state.publicState.scores,
|
|
229
|
-
isMyTurn:
|
|
230
|
-
!state.publicState.gameOver &&
|
|
231
|
-
state.publicState.currentPlayerId === playerId,
|
|
229
|
+
isMyTurn: state.publicState.currentPlayerId === playerId,
|
|
232
230
|
targetScore: 10,
|
|
233
231
|
turnDieId: "turn-die",
|
|
234
232
|
};
|
|
@@ -253,7 +251,7 @@ const TURN_DIE_ID = "turn-die" as const;
|
|
|
253
251
|
const rollDie = defineAction<GameContract>()({
|
|
254
252
|
params: z.object({}),
|
|
255
253
|
validate({ state, input }) {
|
|
256
|
-
if (state.publicState.
|
|
254
|
+
if (state.publicState.winnerPlayerId) {
|
|
257
255
|
return {
|
|
258
256
|
errorCode: "GAME_ALREADY_ENDED",
|
|
259
257
|
message: "The game has already ended.",
|
|
@@ -269,7 +267,7 @@ const rollDie = defineAction<GameContract>()({
|
|
|
269
267
|
|
|
270
268
|
return null;
|
|
271
269
|
},
|
|
272
|
-
reduce({ state, input, accept
|
|
270
|
+
reduce({ state, input, accept }) {
|
|
273
271
|
const nextRoll = (state.hiddenState.rollCount % 6) + 1;
|
|
274
272
|
const nextScore = state.publicState.scores[input.playerId] + nextRoll;
|
|
275
273
|
const nextScores = {
|
|
@@ -291,13 +289,13 @@ const rollDie = defineAction<GameContract>()({
|
|
|
291
289
|
},
|
|
292
290
|
};
|
|
293
291
|
|
|
294
|
-
|
|
292
|
+
return accept({
|
|
295
293
|
...state,
|
|
296
294
|
table: nextTable,
|
|
297
295
|
publicState: {
|
|
298
296
|
...state.publicState,
|
|
299
297
|
currentPlayerId: nextScore >= TARGET_SCORE ? input.playerId : nextPlayerId,
|
|
300
|
-
|
|
298
|
+
winnerPlayerId: nextScore >= TARGET_SCORE ? input.playerId : null,
|
|
301
299
|
lastRoll: nextRoll,
|
|
302
300
|
scores: nextScores,
|
|
303
301
|
},
|
|
@@ -305,27 +303,7 @@ const rollDie = defineAction<GameContract>()({
|
|
|
305
303
|
...state.hiddenState,
|
|
306
304
|
rollCount: state.hiddenState.rollCount + 1,
|
|
307
305
|
},
|
|
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);
|
|
306
|
+
});
|
|
329
307
|
},
|
|
330
308
|
});
|
|
331
309
|
|
|
@@ -334,7 +312,7 @@ export const takeTurn = definePhase<GameContract>()({
|
|
|
334
312
|
state: z.object({}),
|
|
335
313
|
initialState: () => ({}),
|
|
336
314
|
enter({ state, accept }) {
|
|
337
|
-
if (state.publicState.
|
|
315
|
+
if (state.publicState.winnerPlayerId) {
|
|
338
316
|
return accept(state);
|
|
339
317
|
}
|
|
340
318
|
|
|
@@ -426,8 +404,8 @@ export default function App() {
|
|
|
426
404
|
|
|
427
405
|
<p>Last roll: {view.lastRoll ?? "not rolled yet"}</p>
|
|
428
406
|
|
|
429
|
-
{view.
|
|
430
|
-
<p>
|
|
407
|
+
{view.winnerPlayerId ? (
|
|
408
|
+
<p>Winner: {view.winnerPlayerId}</p>
|
|
431
409
|
) : (
|
|
432
410
|
<button onClick={() => void rollDie()} disabled={!view.isMyTurn}>
|
|
433
411
|
Roll die
|
|
@@ -486,8 +464,8 @@ export default defineScenario({
|
|
|
486
464
|
expect(state.lastRoll).toBe(6);
|
|
487
465
|
expect(state.scores["player-1"]).toBe(9);
|
|
488
466
|
expect(state.scores["player-2"]).toBe(12);
|
|
489
|
-
expect(state.
|
|
490
|
-
expect(view("player-2").
|
|
467
|
+
expect(state.winnerPlayerId).toBe("player-2");
|
|
468
|
+
expect(view("player-2").winnerPlayerId).toBe("player-2");
|
|
491
469
|
expect(history().accepted().length).toBe(6);
|
|
492
470
|
},
|
|
493
471
|
});
|
|
@@ -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
|
+
| `GameEndDisplay` | Final rankings and scores |
|
|
383
383
|
| `DiceRoller` | Reducer-driven dice results |
|
|
384
384
|
| `Drawer` and related exports | Mobile overflow or detail panels |
|
|
385
385
|
|
|
@@ -462,9 +462,6 @@ 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
|
-
|
|
468
465
|
### `ActionPanel` and `ActionGroup`
|
|
469
466
|
|
|
470
467
|
Use these to keep the action surface grouped by phase or intent.
|
|
@@ -505,7 +502,7 @@ data.
|
|
|
505
502
|
| Export | Use it for |
|
|
506
503
|
| --- | --- |
|
|
507
504
|
| `TrackBoard` | Linear, circular, or branching tracks |
|
|
508
|
-
| `SquareGrid`
|
|
505
|
+
| `SquareGrid` | Chess-like or tile-grid boards |
|
|
509
506
|
| `HexGrid` | Hex maps with tiles, edges, and vertices |
|
|
510
507
|
| `NetworkGraph` | Nodes, connections, and route graphs |
|
|
511
508
|
| `ZoneMap` | Region or area control boards |
|
|
@@ -535,16 +532,6 @@ Board renderers are still UI code. The reducer should provide:
|
|
|
535
532
|
- labels and annotations
|
|
536
533
|
- grouped piece and card data in the shape the component expects
|
|
537
534
|
|
|
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
|
-
|
|
548
535
|
## Exported types
|
|
549
536
|
|
|
550
537
|
Use these root exports when the UI needs to type reducer-owned runtime data.
|
|
@@ -14,10 +14,6 @@ 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
|
-
|
|
21
17
|
## Top-level shape
|
|
22
18
|
|
|
23
19
|
```json
|
|
@@ -106,10 +102,6 @@ Use `home` on cards, piece seeds, and die seeds to place authored inventory.
|
|
|
106
102
|
| `vertex` | `boardId`, `ref` | Place onto a hex vertex identified by three spaces |
|
|
107
103
|
| `slot` | `hostComponentId`, `slotId` | Place into a component-owned slot |
|
|
108
104
|
|
|
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
|
-
|
|
113
105
|
### `ComponentVisibilitySpec`
|
|
114
106
|
|
|
115
107
|
| Field | Required | Notes |
|
|
@@ -17,7 +17,6 @@ Use the reducer framework for:
|
|
|
17
17
|
- prompts, continuations, and reducer-owned windows
|
|
18
18
|
- player-facing view projection
|
|
19
19
|
- setup bootstrap steps
|
|
20
|
-
- terminal outcomes, automated procedures, and game-event evidence
|
|
21
20
|
|
|
22
21
|
## Purity
|
|
23
22
|
|
|
@@ -64,7 +63,6 @@ import {
|
|
|
64
63
|
moveCardFromPlayerZoneToSharedZone,
|
|
65
64
|
setActivePlayers,
|
|
66
65
|
setPhaseState,
|
|
67
|
-
endGame,
|
|
68
66
|
} from "@dreamboard/app-sdk/reducer";
|
|
69
67
|
```
|
|
70
68
|
|
|
@@ -105,8 +103,7 @@ export const gameContract = defineGameContract({
|
|
|
105
103
|
state: {
|
|
106
104
|
public: z.object({
|
|
107
105
|
currentJudgeId: ids.playerId,
|
|
108
|
-
|
|
109
|
-
gameOver: z.boolean(),
|
|
106
|
+
winnerPlayerId: ids.playerId.nullable(),
|
|
110
107
|
}),
|
|
111
108
|
private: z.object({
|
|
112
109
|
secretNotes: z.array(z.string()),
|
|
@@ -117,8 +114,7 @@ export const gameContract = defineGameContract({
|
|
|
117
114
|
initial: {
|
|
118
115
|
public: ({ playerIds }) => ({
|
|
119
116
|
currentJudgeId: playerIds[0],
|
|
120
|
-
|
|
121
|
-
gameOver: false,
|
|
117
|
+
winnerPlayerId: null,
|
|
122
118
|
}),
|
|
123
119
|
private: () => ({
|
|
124
120
|
secretNotes: [],
|
|
@@ -131,43 +127,6 @@ export const gameContract = defineGameContract({
|
|
|
131
127
|
});
|
|
132
128
|
```
|
|
133
129
|
|
|
134
|
-
Use state fields for ongoing game progress. Use `endGame(...)` and
|
|
135
|
-
`GameOutcome` for terminal results instead of preserving a parallel
|
|
136
|
-
`winnerPlayerId` or score-map convention as the result contract.
|
|
137
|
-
|
|
138
|
-
### Terminal outcomes
|
|
139
|
-
|
|
140
|
-
Call `endGame(state, outcome)` when the reducer reaches a terminal state.
|
|
141
|
-
`GameOutcome` owns the result reason and ordered standings, including ties,
|
|
142
|
-
score breakdowns, and tie-break evidence.
|
|
143
|
-
|
|
144
|
-
```ts
|
|
145
|
-
return endGame(nextState, {
|
|
146
|
-
reason: {
|
|
147
|
-
code: "ROUND_LIMIT_REACHED",
|
|
148
|
-
message: "The final round is complete.",
|
|
149
|
-
},
|
|
150
|
-
standings: [
|
|
151
|
-
{
|
|
152
|
-
playerId: "player-1",
|
|
153
|
-
rank: 1,
|
|
154
|
-
result: "win",
|
|
155
|
-
score: 18,
|
|
156
|
-
scoreBreakdown: [{ id: "routes", label: "Routes", value: 12 }],
|
|
157
|
-
tieBreaks: [{ id: "cards-left", label: "Cards left", value: 2 }],
|
|
158
|
-
},
|
|
159
|
-
{
|
|
160
|
-
playerId: "player-2",
|
|
161
|
-
rank: 2,
|
|
162
|
-
result: "loss",
|
|
163
|
-
score: 15,
|
|
164
|
-
},
|
|
165
|
-
],
|
|
166
|
-
});
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
The UI may present this result, but reducer authority decides it.
|
|
170
|
-
|
|
171
130
|
## Generated `shared/manifest-contract`
|
|
172
131
|
|
|
173
132
|
The generated `shared/manifest-contract` module exports more than the
|
|
@@ -458,10 +417,6 @@ Continuation IDs and window IDs default to their registry keys when you omit
|
|
|
458
417
|
Keep hard legality checks in `validate(...)` or `reduce(...)`. `available(...)`
|
|
459
418
|
only filters the surfaced available-action list.
|
|
460
419
|
|
|
461
|
-
When an action is unavailable for a player-facing reason, project that reason
|
|
462
|
-
through the reducer descriptor or view. React should display reducer authority;
|
|
463
|
-
it should not recompute a second legality model.
|
|
464
|
-
|
|
465
420
|
```ts
|
|
466
421
|
import { z } from "zod";
|
|
467
422
|
import { defineAction } from "@dreamboard/app-sdk/reducer";
|
|
@@ -73,7 +73,6 @@ Describe how players gain or lose progress.
|
|
|
73
73
|
- when scoring happens
|
|
74
74
|
- round-end or trick-end resolution
|
|
75
75
|
- tie-breakers for intermediate rankings
|
|
76
|
-
- named score breakdowns players need to understand the result
|
|
77
76
|
|
|
78
77
|
### `Winning conditions`
|
|
79
78
|
|
|
@@ -84,10 +83,6 @@ State the exact end trigger and winner resolution rule.
|
|
|
84
83
|
- how ties are broken
|
|
85
84
|
- what happens if multiple end conditions become true at once
|
|
86
85
|
|
|
87
|
-
Write outcome rules in enough detail for a coding agent to produce a
|
|
88
|
-
`GameOutcome`: result reason, ordered standings, tie ranks, tie-break rows, and
|
|
89
|
-
score breakdowns where relevant.
|
|
90
|
-
|
|
91
86
|
### `Special rules and edge cases`
|
|
92
87
|
|
|
93
88
|
List rules that are easy to miss during implementation.
|
|
@@ -97,11 +92,6 @@ List rules that are easy to miss during implementation.
|
|
|
97
92
|
- no-op or invalid actions
|
|
98
93
|
- forced actions versus optional actions
|
|
99
94
|
- what happens when a player cannot act
|
|
100
|
-
- deterministic solo or automa procedures, including what event should be shown
|
|
101
|
-
to players after the procedure runs
|
|
102
|
-
|
|
103
|
-
Automa and environment behavior should be described as game procedures, not as
|
|
104
|
-
extra participants. Session seats belong to human players.
|
|
105
95
|
|
|
106
96
|
## Example template
|
|
107
97
|
|
|
@@ -136,17 +136,13 @@ export default defineScenario({
|
|
|
136
136
|
expect(state.lastRoll).toBe(6);
|
|
137
137
|
expect(state.scores["player-1"]).toBe(9);
|
|
138
138
|
expect(state.scores["player-2"]).toBe(12);
|
|
139
|
-
expect(state.
|
|
140
|
-
expect(view("player-2").
|
|
139
|
+
expect(state.winnerPlayerId).toBe("player-2");
|
|
140
|
+
expect(view("player-2").winnerPlayerId).toBe("player-2");
|
|
141
141
|
expect(history().accepted().length).toBe(6);
|
|
142
142
|
},
|
|
143
143
|
});
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
-
For terminal games, assert the reducer-owned outcome when the generated
|
|
147
|
-
scenario helpers expose it. Do not infer the winner in tests by sorting scores
|
|
148
|
-
unless that derived view is itself part of the rules.
|
|
149
|
-
|
|
150
146
|
## Scenario context
|
|
151
147
|
|
|
152
148
|
`when(...)` and `then(...)` receive typed helpers from `test/testing-types.ts`.
|
|
@@ -228,7 +224,7 @@ then: ({ phase, history, expect, publicState }) => {
|
|
|
228
224
|
expect(history().rejected().length).toBe(1);
|
|
229
225
|
expect(history().accepted().length).toBe(0);
|
|
230
226
|
expect(phase()).toBe("takeTurn");
|
|
231
|
-
expect(publicState().
|
|
227
|
+
expect(publicState().winnerPlayerId).toBe(null);
|
|
232
228
|
};
|
|
233
229
|
```
|
|
234
230
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/build-target.ts","../../src/auth/clerk-oauth.ts","../../src/auth/token-exchange.ts","../../src/auth/user-token-manager.ts","../../src/config/local-harness-auth.ts"],"sourcesContent":["declare const __DREAMBOARD_BUILD_CHANNEL__: string | undefined;\n\nconst injectedBuildChannel =\n typeof __DREAMBOARD_BUILD_CHANNEL__ === \"string\"\n ? __DREAMBOARD_BUILD_CHANNEL__\n : undefined;\n\nexport const BUILD_CHANNEL =\n injectedBuildChannel === \"published\" ? \"published\" : \"development\";\n\nexport const IS_PUBLISHED_BUILD = BUILD_CHANNEL === \"published\";\nexport const PUBLISHED_ENVIRONMENT = \"prod\" as const;\n","import crypto from \"node:crypto\";\n\nexport type ClerkOAuthConfig = {\n issuer?: string;\n clientId?: string;\n tokenUrl?: string;\n scope?: string;\n};\n\nexport type ClerkOAuthTokenResponse = {\n accessToken: string;\n refreshToken: string;\n expiresAt?: string;\n tokenUrl: string;\n};\n\nexport function createPkcePair(): { verifier: string; challenge: string } {\n const verifier = base64Url(crypto.randomBytes(32));\n const challenge = base64Url(\n crypto.createHash(\"sha256\").update(verifier).digest(),\n );\n return { verifier, challenge };\n}\n\nexport function buildClerkAuthorizationUrl(input: {\n config: ClerkOAuthConfig;\n redirectUri: string;\n state: string;\n codeChallenge: string;\n}): URL {\n const { issuer, clientId, scope } = assertConfigured(input.config);\n const url = new URL(\"/oauth/authorize\", issuer);\n url.searchParams.set(\"response_type\", \"code\");\n url.searchParams.set(\"client_id\", clientId);\n url.searchParams.set(\"redirect_uri\", input.redirectUri);\n url.searchParams.set(\"state\", input.state);\n url.searchParams.set(\"code_challenge\", input.codeChallenge);\n url.searchParams.set(\"code_challenge_method\", \"S256\");\n url.searchParams.set(\"scope\", scope ?? \"openid profile email offline_access\");\n return url;\n}\n\nexport async function exchangeClerkOAuthCode(input: {\n config: ClerkOAuthConfig;\n code: string;\n redirectUri: string;\n codeVerifier: string;\n}): Promise<ClerkOAuthTokenResponse> {\n const { clientId, tokenUrl } = assertConfigured(input.config);\n const body = new URLSearchParams({\n grant_type: \"authorization_code\",\n client_id: clientId,\n code: input.code,\n redirect_uri: input.redirectUri,\n code_verifier: input.codeVerifier,\n });\n return requestClerkToken(tokenUrl, body);\n}\n\nexport async function refreshClerkOAuthToken(input: {\n config: ClerkOAuthConfig;\n refreshToken: string;\n}): Promise<ClerkOAuthTokenResponse> {\n const { clientId, tokenUrl } = assertConfigured(input.config);\n const body = new URLSearchParams({\n grant_type: \"refresh_token\",\n client_id: clientId,\n refresh_token: input.refreshToken,\n });\n return requestClerkToken(tokenUrl, body);\n}\n\nfunction assertConfigured(config: ClerkOAuthConfig): {\n issuer: string;\n clientId: string;\n tokenUrl: string;\n scope?: string;\n} {\n const issuer = config.issuer?.trim().replace(/\\/$/, \"\");\n const clientId = config.clientId?.trim();\n if (!issuer || !clientId) {\n throw new Error(\n [\n \"Clerk OAuth CLI is not configured for this environment.\",\n \"The CLI expects first-party environments to be configured in its built-in registry.\",\n \"If this environment has no registered public Clerk OAuth client, create one and release a CLI with its client id.\",\n \"For emergency overrides, set the environment-specific DREAMBOARD_<ENV>_CLERK_OAUTH_* variables or DREAMBOARD_CLERK_OAUTH_*.\",\n \"For local harness auth, use `pnpm auth:local` or the auto-bootstrapped local harness flows instead.\",\n ].join(\" \"),\n );\n }\n return {\n issuer,\n clientId,\n tokenUrl:\n config.tokenUrl?.trim() || new URL(\"/oauth/token\", issuer).toString(),\n scope: config.scope?.trim() || undefined,\n };\n}\n\nasync function requestClerkToken(\n tokenUrl: string,\n body: URLSearchParams,\n): Promise<ClerkOAuthTokenResponse> {\n const response = await fetch(tokenUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n Accept: \"application/json\",\n },\n body,\n });\n if (!response.ok) {\n const detail = await response.text();\n throw new Error(\n `Clerk OAuth token request failed (${response.status}): ${detail}`,\n );\n }\n const payload = (await response.json()) as {\n access_token?: unknown;\n refresh_token?: unknown;\n expires_in?: unknown;\n };\n if (typeof payload.access_token !== \"string\") {\n throw new Error(\"Clerk OAuth token response did not include access_token.\");\n }\n if (typeof payload.refresh_token !== \"string\") {\n throw new Error(\n \"Clerk OAuth token response did not include refresh_token.\",\n );\n }\n const expiresAt =\n typeof payload.expires_in === \"number\"\n ? new Date(Date.now() + payload.expires_in * 1000).toISOString()\n : undefined;\n return {\n accessToken: payload.access_token,\n refreshToken: payload.refresh_token,\n expiresAt,\n tokenUrl,\n };\n}\n\nfunction base64Url(bytes: Buffer): string {\n return bytes\n .toString(\"base64\")\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/g, \"\");\n}\n","export type DreamboardTokenAudience = \"dreamboard-api\" | \"dreamboard-git\";\n\nexport type DreamboardTokenResponse = {\n accessToken: string;\n tokenType: \"Bearer\";\n audience: DreamboardTokenAudience;\n expiresIn?: number;\n expiresAt?: string;\n};\n\nexport async function exchangeDreamboardUserToken(input: {\n apiBaseUrl: string;\n clerkAccessToken: string;\n audience: DreamboardTokenAudience;\n fetchImpl?: typeof fetch;\n}): Promise<DreamboardTokenResponse> {\n const fetchImpl = input.fetchImpl ?? globalThis.fetch.bind(globalThis);\n const response = await fetchImpl(\n new URL(\"/api/auth/token-exchange\", input.apiBaseUrl),\n {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${input.clerkAccessToken}`,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n body: JSON.stringify({ audience: input.audience }),\n },\n );\n\n if (!response.ok) {\n throw new Error(\n `Dreamboard token exchange failed (${response.status}). Run \\`dreamboard login\\` to authenticate again.`,\n );\n }\n\n const payload = (await response.json()) as {\n accessToken?: unknown;\n tokenType?: unknown;\n audience?: unknown;\n expiresIn?: unknown;\n };\n\n if (typeof payload.accessToken !== \"string\" || payload.accessToken === \"\") {\n throw new Error(\"Dreamboard token exchange response omitted accessToken.\");\n }\n if (payload.tokenType !== \"Bearer\") {\n throw new Error(\n \"Dreamboard token exchange response had invalid tokenType.\",\n );\n }\n if (payload.audience !== input.audience) {\n throw new Error(\"Dreamboard token exchange response had wrong audience.\");\n }\n\n const expiresIn =\n typeof payload.expiresIn === \"number\" && Number.isFinite(payload.expiresIn)\n ? payload.expiresIn\n : undefined;\n\n return {\n accessToken: payload.accessToken,\n tokenType: \"Bearer\",\n audience: input.audience,\n expiresIn,\n expiresAt:\n expiresIn === undefined\n ? undefined\n : new Date(Date.now() + expiresIn * 1000).toISOString(),\n };\n}\n","import type {\n Credentials,\n StoredSessionSnapshot,\n} from \"../config/credential-store.js\";\nimport { withCredentialLock } from \"../config/credential-store.js\";\nimport { refreshClerkOAuthToken } from \"./clerk-oauth.js\";\nimport {\n exchangeDreamboardUserToken,\n type DreamboardTokenAudience,\n} from \"./token-exchange.js\";\nimport type { ResolvedConfig } from \"../types.js\";\n\nconst TOKEN_REFRESH_WINDOW_MS = 60 * 1000;\n\nexport type AccessToken = {\n readonly token: string;\n readonly expiresAt?: string;\n readonly audience: DreamboardTokenAudience;\n};\n\nexport interface UserTokenManager {\n resolveApiToken(): Promise<AccessToken | null>;\n resolveGitToken(): Promise<AccessToken>;\n}\n\nexport function createUserTokenManager(\n config: ResolvedConfig,\n): UserTokenManager {\n return {\n async resolveApiToken() {\n const localOrInjected = resolveNonStoredToken(config, \"dreamboard-api\");\n if (localOrInjected) return localOrInjected;\n\n if (!usesStoredSession(config)) return null;\n\n return withCredentialLock(async (ops) => {\n const stored = await ops.read();\n const apiToken = freshStoredApiToken(stored);\n if (apiToken) return apiToken;\n\n const clerk = await resolveFreshClerkAccessToken(config, stored);\n const exchanged = await exchangeDreamboardUserToken({\n apiBaseUrl: config.apiBaseUrl,\n clerkAccessToken: clerk.accessToken,\n audience: \"dreamboard-api\",\n });\n\n await ops.writeFull({\n ...clerk,\n dreamboardApiToken: exchanged.accessToken,\n dreamboardApiExpiresAt: exchanged.expiresAt,\n });\n\n return {\n token: exchanged.accessToken,\n expiresAt: exchanged.expiresAt,\n audience: \"dreamboard-api\",\n };\n });\n },\n\n async resolveGitToken() {\n const localOrInjected = resolveNonStoredToken(config, \"dreamboard-git\");\n if (localOrInjected) return localOrInjected;\n\n if (!usesStoredSession(config)) {\n throw new Error(\n \"Missing Dreamboard session. Run `dreamboard login` to authenticate.\",\n );\n }\n\n return withCredentialLock(async (ops) => {\n const stored = await ops.read();\n const clerk = await resolveFreshClerkAccessToken(config, stored);\n const exchanged = await exchangeDreamboardUserToken({\n apiBaseUrl: config.apiBaseUrl,\n clerkAccessToken: clerk.accessToken,\n audience: \"dreamboard-git\",\n });\n\n await ops.writeFull(clerk);\n\n return {\n token: exchanged.accessToken,\n expiresAt: exchanged.expiresAt,\n audience: \"dreamboard-git\",\n };\n });\n },\n };\n}\n\nfunction resolveNonStoredToken(\n config: ResolvedConfig,\n audience: DreamboardTokenAudience,\n): AccessToken | null {\n if (usesStoredSession(config)) return null;\n if (!config.authToken) return null;\n return {\n token: config.authToken,\n expiresAt: config.tokenExpiresAt,\n audience,\n };\n}\n\nfunction freshStoredApiToken(\n stored: StoredSessionSnapshot | null,\n): AccessToken | null {\n if (!stored?.dreamboardApiToken) return null;\n if (isFresh(stored.dreamboardApiExpiresAt, stored.dreamboardApiToken)) {\n return {\n token: stored.dreamboardApiToken,\n expiresAt: stored.dreamboardApiExpiresAt,\n audience: \"dreamboard-api\",\n };\n }\n return null;\n}\n\nasync function resolveFreshClerkAccessToken(\n config: ResolvedConfig,\n stored: StoredSessionSnapshot | null,\n): Promise<Credentials> {\n const accessToken = stored?.accessToken ?? config.clerkAccessToken;\n const refreshToken = stored?.refreshToken ?? config.refreshToken;\n const tokenExpiresAt = stored?.tokenExpiresAt ?? config.clerkAccessExpiresAt;\n\n if (!refreshToken) {\n throw new Error(\n \"Stored Dreamboard session is missing its refresh token. Run `dreamboard login` to authenticate again.\",\n );\n }\n\n if (accessToken && isFresh(tokenExpiresAt, accessToken)) {\n return {\n accessToken,\n refreshToken,\n tokenExpiresAt,\n dreamboardApiToken: stored?.dreamboardApiToken,\n dreamboardApiExpiresAt: stored?.dreamboardApiExpiresAt,\n clerkOAuthIssuer: stored?.clerkOAuthIssuer ?? config.clerkOAuthIssuer,\n clerkOAuthClientId:\n stored?.clerkOAuthClientId ?? config.clerkOAuthClientId,\n clerkOAuthTokenUrl:\n stored?.clerkOAuthTokenUrl ?? config.clerkOAuthTokenUrl,\n environment: stored?.environment ?? config.environment,\n };\n }\n\n const payload = await refreshClerkOAuthToken({\n config: {\n issuer: stored?.clerkOAuthIssuer ?? config.clerkOAuthIssuer,\n clientId: stored?.clerkOAuthClientId ?? config.clerkOAuthClientId,\n tokenUrl: stored?.clerkOAuthTokenUrl ?? config.clerkOAuthTokenUrl,\n },\n refreshToken,\n });\n\n return {\n accessToken: payload.accessToken,\n refreshToken: payload.refreshToken,\n tokenExpiresAt: payload.expiresAt,\n clerkOAuthIssuer: stored?.clerkOAuthIssuer ?? config.clerkOAuthIssuer,\n clerkOAuthClientId: stored?.clerkOAuthClientId ?? config.clerkOAuthClientId,\n clerkOAuthTokenUrl: payload.tokenUrl,\n environment: stored?.environment ?? config.environment,\n };\n}\n\nfunction isFresh(expiresAt: string | undefined, token: string): boolean {\n const expiry = expiresAt ? new Date(expiresAt) : getJwtExpiry(token);\n return (\n expiry !== null &&\n Number.isFinite(expiry.getTime()) &&\n expiry.getTime() > Date.now() + TOKEN_REFRESH_WINDOW_MS\n );\n}\n\nfunction getJwtExpiry(accessToken: string | undefined): Date | null {\n if (!accessToken) return null;\n const parts = accessToken.split(\".\");\n if (parts.length !== 3) return null;\n try {\n const payload = JSON.parse(\n Buffer.from(parts[1]!, \"base64url\").toString(\"utf8\"),\n ) as { exp?: unknown };\n if (typeof payload.exp !== \"number\" || !Number.isFinite(payload.exp)) {\n return null;\n }\n return new Date(payload.exp * 1000);\n } catch {\n return null;\n }\n}\n\nfunction usesStoredSession(config: ResolvedConfig): boolean {\n return config.refreshTokenSource === \"global\";\n}\n","import { createHmac, randomUUID } from \"node:crypto\";\nimport type { ResolvedConfig } from \"../types.js\";\nimport { IS_PUBLISHED_BUILD } from \"../build-target.js\";\n\ntype LocalHarnessProfile = \"local\" | \"local-aws\";\n\nconst DEFAULT_SUBJECT = \"harness-smoke-local@dreamboard.local\";\nconst DEFAULT_ISSUER = \"dreamboard-local-harness\";\nconst DEFAULT_SECRET = \"dreamboard-local-harness-token-secret\";\nconst LOCAL_AWS_ISSUER = \"dreamboard-local-aws-harness\";\nconst LOCAL_AWS_SECRET = \"dreamboard-local-aws-harness-token-secret\";\nconst DEFAULT_TTL_SECONDS = 8 * 60 * 60;\n\nconst mintedTokens = new Map<string, string>();\n\nexport function resolveLocalHarnessAccessToken(\n config: ResolvedConfig,\n): string | undefined {\n if (IS_PUBLISHED_BUILD || config.environment !== \"local\") {\n return undefined;\n }\n\n const profile = inferLocalHarnessProfile(config);\n if (\n config.authToken &&\n (profile !== \"local-aws\" || isExplicitTokenSource(config.authTokenSource))\n ) {\n return undefined;\n }\n\n const cacheKey = [\n profile,\n process.env.LOCAL_HARNESS_TOKEN_ISSUER ?? \"\",\n process.env.LOCAL_HARNESS_TOKEN_SECRET ?? \"\",\n process.env.LOCAL_HARNESS_SUBJECT ?? \"\",\n process.env.HARNESS_USER_EMAIL ?? \"\",\n process.env.LOCAL_HARNESS_EMAIL ?? \"\",\n process.env.LOCAL_HARNESS_TOKEN_TTL_SECONDS ?? \"\",\n ].join(\"\\0\");\n const cached = mintedTokens.get(cacheKey);\n if (cached) return cached;\n\n const token = mintLocalHarnessToken(profile);\n mintedTokens.set(cacheKey, token);\n return token;\n}\n\nfunction isExplicitTokenSource(\n source: ResolvedConfig[\"authTokenSource\"],\n): boolean {\n return source === \"flag\" || source === \"env\" || source === \"agent-env\";\n}\n\nexport function inferLocalHarnessProfile(\n config: Pick<ResolvedConfig, \"apiBaseUrl\" | \"webBaseUrl\">,\n): LocalHarnessProfile {\n return isLocalAwsUrl(config.apiBaseUrl) || isLocalAwsUrl(config.webBaseUrl)\n ? \"local-aws\"\n : \"local\";\n}\n\nfunction mintLocalHarnessToken(profile: LocalHarnessProfile): string {\n const subject =\n envValue(process.env.LOCAL_HARNESS_SUBJECT) ??\n envValue(process.env.HARNESS_USER_EMAIL) ??\n DEFAULT_SUBJECT;\n const email =\n envValue(process.env.LOCAL_HARNESS_EMAIL) ??\n (subject.includes(\"@\") ? subject : undefined);\n const issuer =\n envValue(process.env.LOCAL_HARNESS_TOKEN_ISSUER) ??\n (profile === \"local-aws\" ? LOCAL_AWS_ISSUER : DEFAULT_ISSUER);\n const secret =\n envValue(process.env.LOCAL_HARNESS_TOKEN_SECRET) ??\n (profile === \"local-aws\" ? LOCAL_AWS_SECRET : DEFAULT_SECRET);\n const ttlSeconds = Number(\n envValue(process.env.LOCAL_HARNESS_TOKEN_TTL_SECONDS) ??\n String(DEFAULT_TTL_SECONDS),\n );\n if (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0) {\n throw new Error(\n \"LOCAL_HARNESS_TOKEN_TTL_SECONDS must be a positive number.\",\n );\n }\n\n const now = Math.floor(Date.now() / 1000);\n const payload = {\n typ: \"local_harness_access\",\n dreamboard_provider: \"local-harness\",\n dreamboard_provider_subject: subject,\n ...(email ? { email } : {}),\n iss: issuer,\n sub: subject,\n iat: now,\n exp: now + Math.floor(ttlSeconds),\n jti: randomUUID(),\n };\n\n const headerPart = base64UrlJson({ alg: \"HS256\", typ: \"JWT\" });\n const payloadPart = base64UrlJson(payload);\n const signature = createHmac(\"sha256\", secret)\n .update(`${headerPart}.${payloadPart}`)\n .digest(\"base64url\");\n return `${headerPart}.${payloadPart}.${signature}`;\n}\n\nfunction base64UrlJson(value: unknown): string {\n return Buffer.from(JSON.stringify(value), \"utf8\").toString(\"base64url\");\n}\n\nfunction envValue(raw: string | undefined): string | undefined {\n return typeof raw === \"string\" && raw.trim().length > 0\n ? raw.trim()\n : undefined;\n}\n\nfunction isLocalAwsUrl(rawUrl: string | undefined): boolean {\n if (!rawUrl) return false;\n try {\n const url = new URL(rawUrl);\n return (\n (url.hostname === \"localhost\" || url.hostname === \"127.0.0.1\") &&\n (url.port === \"18080\" || url.port === \"8088\")\n );\n } catch {\n return false;\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/config/credential-store.ts"],"sourcesContent":["/**\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":[]}
|