Skip to content

Commit 96e814c

Browse files
author
DavidQ
committed
Block fake session IDs from creating saved library entries - PR 11.246
1 parent 5d33e9a commit 96e814c

3 files changed

Lines changed: 342 additions & 12 deletions

File tree

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# PR_11_246 — Block Fake Session IDs From Creating Saved Library Entries
2+
3+
## Summary
4+
Updated Workspace V2 Session Library Save/Overwrite to require exact session ID resolution from `sessionStorage` by entered `hostContextId`. This blocks fake/arbitrary IDs from creating or overwriting saved library entries.
5+
6+
## Files Changed
7+
- `tools/workspace-v2/index.js`
8+
- `tests/runtime/V2BlockFakeSessionSave.test.mjs`
9+
10+
## Implementation Details
11+
- Save/Overwrite source resolution is now strict:
12+
- `readSessionPayloadForLibraryWrite(sessionId)` reads only `sessionStorage[sessionId]` and validates parsed JSON payload.
13+
- Removed active-payload fallback path for Save/Overwrite when entered ID does not resolve exactly.
14+
- Save behavior:
15+
- existing library ID -> `Saved session already exists. Use Overwrite Session.`
16+
- non-existing ID with valid `sessionStorage` payload -> `Saved session created.`
17+
- unknown/fake ID -> `Session ID does not resolve to a valid Workspace V2 session.`
18+
- Overwrite behavior:
19+
- unknown/fake ID -> `Session ID does not resolve to a valid Workspace V2 session.`
20+
- valid resolvable ID but no saved entry -> `Saved session not found. Use Save Session to create it first.`
21+
- valid resolvable ID with saved entry -> `Saved session overwritten.`
22+
- Added stale invalid fake-entry cleanup helper:
23+
- `cleanupStaleInvalidSavedEntries(library)`
24+
- removes only high-confidence stale fake host-like IDs that:
25+
- do not resolve to matching `sessionStorage` payload, and
26+
- do not match payload host context metadata or tool metadata.
27+
- preserves valid saved entries, including custom user labels.
28+
29+
## Validation Commands Run
30+
```powershell
31+
node --check tools/workspace-v2/index.js
32+
node --check tests/runtime/V2BlockFakeSessionSave.test.mjs
33+
node --check tests/runtime/V2SaveLibraryFromRecentSession.test.mjs
34+
node tests/runtime/V2BlockFakeSessionSave.test.mjs
35+
node tests/runtime/V2SaveLibraryFromRecentSession.test.mjs
36+
node tests/runtime/V2SavedSessionDeleteFeedback.test.mjs
37+
```
38+
39+
## Validation Results
40+
- `node --check tools/workspace-v2/index.js` -> PASS
41+
- `node --check tests/runtime/V2BlockFakeSessionSave.test.mjs` -> PASS
42+
- `node --check tests/runtime/V2SaveLibraryFromRecentSession.test.mjs` -> PASS
43+
- `node tests/runtime/V2BlockFakeSessionSave.test.mjs` -> PASS
44+
- output: `tmp/v2-block-fake-session-save-results.json`
45+
- failures: `[]`
46+
- `node tests/runtime/V2SaveLibraryFromRecentSession.test.mjs` -> PASS
47+
- output: `tmp/v2-save-library-from-recent-session-results.json`
48+
- failures: `[]`
49+
- `node tests/runtime/V2SavedSessionDeleteFeedback.test.mjs` -> PASS
50+
- output: `tmp/v2-saved-session-delete-feedback-results.json`
51+
- failures: `[]`
52+
53+
## Requirement Coverage
54+
- Fake unknown ID cannot create saved entry -> PASS
55+
- Fake unknown ID cannot overwrite saved entry -> PASS
56+
- Valid recent/sessionStorage ID can be saved -> PASS
57+
- Duplicate save is blocked -> PASS
58+
- Overwrite valid existing saved ID succeeds -> PASS
59+
- Active payload is not saved under unrelated input ID -> PASS
60+
- Stale invalid fake-entry cleanup preserves valid saved entries -> PASS
61+
62+
## Full Samples Smoke
63+
Skipped.
64+
65+
Reason: scope is limited to Workspace V2 Session Library Save/Overwrite behavior and targeted runtime tests; no schemas/samples/games/shared sample infrastructure changed.
66+
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import assert from "node:assert/strict";
2+
import fs from "node:fs";
3+
import path from "node:path";
4+
import { execFileSync } from "node:child_process";
5+
import { fileURLToPath, pathToFileURL } from "node:url";
6+
7+
const __filename = fileURLToPath(import.meta.url);
8+
const __dirname = path.dirname(__filename);
9+
const repoRoot = path.resolve(__dirname, "..", "..");
10+
const jsPath = path.join(repoRoot, "tools", "workspace-v2", "index.js");
11+
const resultsPath = path.join(repoRoot, "tmp", "v2-block-fake-session-save-results.json");
12+
13+
function readText(filePath) {
14+
return fs.readFileSync(filePath, "utf8");
15+
}
16+
17+
function checkSyntax(filePath) {
18+
try {
19+
execFileSync(process.execPath, ["--check", filePath], {
20+
cwd: repoRoot,
21+
stdio: ["ignore", "pipe", "pipe"]
22+
});
23+
return { ok: true, error: "" };
24+
} catch (error) {
25+
return { ok: false, error: (error?.stderr || error?.stdout || error?.message || "").toString().trim() };
26+
}
27+
}
28+
29+
function isValidPayload(payload) {
30+
return Boolean(payload && typeof payload === "object" && !Array.isArray(payload));
31+
}
32+
33+
function readSessionPayloadForLibraryWrite(sessionId, storageMap) {
34+
const id = typeof sessionId === "string" ? sessionId.trim() : "";
35+
if (!id) return null;
36+
const raw = storageMap[id];
37+
if (typeof raw !== "string") return null;
38+
try {
39+
const parsed = JSON.parse(raw);
40+
return isValidPayload(parsed) ? parsed : null;
41+
} catch {
42+
return null;
43+
}
44+
}
45+
46+
function looksLikeWorkspaceHostContextId(sessionId) {
47+
const id = typeof sessionId === "string" ? sessionId.trim() : "";
48+
if (!id) return false;
49+
return /-v2-\d{13}-[a-z0-9]{8}$/i.test(id);
50+
}
51+
52+
function cleanupStaleInvalidSavedEntries(library, storageMap) {
53+
const nextLibrary = { ...library };
54+
const removed = [];
55+
Object.keys(nextLibrary).forEach((sessionId) => {
56+
const payload = nextLibrary[sessionId];
57+
if (!isValidPayload(payload)) return;
58+
if (!looksLikeWorkspaceHostContextId(sessionId)) return;
59+
const storagePayload = readSessionPayloadForLibraryWrite(sessionId, storageMap);
60+
const hasMatchingStorage = isValidPayload(storagePayload);
61+
const payloadHostContextId = typeof payload.hostContextId === "string" ? payload.hostContextId.trim() : "";
62+
const payloadToolId = typeof payload.toolId === "string" ? payload.toolId.trim() : "";
63+
const idMatchesPayloadHostContext = Boolean(payloadHostContextId && payloadHostContextId === sessionId);
64+
const idMatchesToolMetadata = Boolean(payloadToolId && sessionId.startsWith(`${payloadToolId}-`));
65+
if (!hasMatchingStorage && !idMatchesPayloadHostContext && !idMatchesToolMetadata) {
66+
delete nextLibrary[sessionId];
67+
removed.push(sessionId);
68+
}
69+
});
70+
return { nextLibrary, removed };
71+
}
72+
73+
function simulateSave(inputId, library, storageMap) {
74+
const sessionId = typeof inputId === "string" ? inputId.trim() : "";
75+
const nextLibrary = { ...library };
76+
if (!sessionId) return { message: "Enter a session ID before saving.", nextLibrary };
77+
if (Object.prototype.hasOwnProperty.call(nextLibrary, sessionId)) {
78+
return { message: "Saved session already exists. Use Overwrite Session.", nextLibrary };
79+
}
80+
const payload = readSessionPayloadForLibraryWrite(sessionId, storageMap);
81+
if (!isValidPayload(payload)) {
82+
return { message: "Session ID does not resolve to a valid Workspace V2 session.", nextLibrary };
83+
}
84+
nextLibrary[sessionId] = payload;
85+
return { message: "Saved session created.", nextLibrary };
86+
}
87+
88+
function simulateOverwrite(inputId, library, storageMap) {
89+
const sessionId = typeof inputId === "string" ? inputId.trim() : "";
90+
const nextLibrary = { ...library };
91+
if (!sessionId) return { message: "Enter a session ID before overwriting.", nextLibrary };
92+
const payload = readSessionPayloadForLibraryWrite(sessionId, storageMap);
93+
if (!isValidPayload(payload)) {
94+
return { message: "Session ID does not resolve to a valid Workspace V2 session.", nextLibrary };
95+
}
96+
if (!Object.prototype.hasOwnProperty.call(nextLibrary, sessionId)) {
97+
return { message: "Saved session not found. Use Save Session to create it first.", nextLibrary };
98+
}
99+
nextLibrary[sessionId] = payload;
100+
return { message: "Saved session overwritten.", nextLibrary };
101+
}
102+
103+
export function run() {
104+
const failures = [];
105+
const jsExists = fs.existsSync(jsPath);
106+
const js = jsExists ? readText(jsPath) : "";
107+
const jsSyntax = checkSyntax(jsPath);
108+
const testSyntax = checkSyntax(path.join(repoRoot, "tests", "runtime", "V2BlockFakeSessionSave.test.mjs"));
109+
110+
if (!jsExists) failures.push("Missing tools/workspace-v2/index.js.");
111+
if (!jsSyntax.ok) failures.push("tools/workspace-v2/index.js failed syntax check.");
112+
if (!testSyntax.ok) failures.push("tests/runtime/V2BlockFakeSessionSave.test.mjs failed syntax check.");
113+
114+
const requiredTokens = [
115+
"readSessionPayloadForLibraryWrite(sessionId)",
116+
"looksLikeWorkspaceHostContextId(sessionId)",
117+
"cleanupStaleInvalidSavedEntries(library)",
118+
"Session ID does not resolve to a valid Workspace V2 session.",
119+
"Saved session already exists. Use Overwrite Session.",
120+
"Saved session not found. Use Save Session to create it first.",
121+
"Saved session created.",
122+
"Saved session overwritten."
123+
];
124+
requiredTokens.forEach((token) => {
125+
if (!js.includes(token)) failures.push(`Missing required implementation token/message: ${token}`);
126+
});
127+
128+
const validId = "asset-browser-v2-1777676088718-3eff5h3y";
129+
const fakeId = "fake-manager-v2-1777676067919-bzuo73di";
130+
const storage = {
131+
[validId]: JSON.stringify({ version: "v2", toolId: "asset-browser-v2", payloadJson: { source: "sessionStorage" } })
132+
};
133+
const activePayload = { version: "v2", toolId: "asset-browser-v2", payloadJson: { source: "active-only" } };
134+
135+
const saveFake = simulateSave(fakeId, {}, storage);
136+
if (saveFake.message !== "Session ID does not resolve to a valid Workspace V2 session.") {
137+
failures.push("Fake unknown ID should not create saved entry.");
138+
}
139+
if (Object.prototype.hasOwnProperty.call(saveFake.nextLibrary, fakeId)) {
140+
failures.push("Fake unknown ID unexpectedly created saved entry.");
141+
}
142+
143+
const overwriteFake = simulateOverwrite(fakeId, { [validId]: { version: "v2", toolId: "asset-browser-v2" } }, storage);
144+
if (overwriteFake.message !== "Session ID does not resolve to a valid Workspace V2 session.") {
145+
failures.push("Fake unknown ID should not overwrite saved entry.");
146+
}
147+
148+
const saveValid = simulateSave(validId, {}, storage);
149+
if (saveValid.message !== "Saved session created." || !Object.prototype.hasOwnProperty.call(saveValid.nextLibrary, validId)) {
150+
failures.push("Valid recent/sessionStorage ID should be saved.");
151+
}
152+
153+
const duplicateSave = simulateSave(validId, saveValid.nextLibrary, storage);
154+
if (duplicateSave.message !== "Saved session already exists. Use Overwrite Session.") {
155+
failures.push("Duplicate save should be blocked.");
156+
}
157+
158+
const overwriteValid = simulateOverwrite(validId, saveValid.nextLibrary, storage);
159+
if (overwriteValid.message !== "Saved session overwritten.") {
160+
failures.push("Overwrite valid existing saved ID should succeed.");
161+
}
162+
163+
const saveUnknownWithActive = simulateSave(fakeId, {}, {});
164+
if (saveUnknownWithActive.message !== "Session ID does not resolve to a valid Workspace V2 session.") {
165+
failures.push("Active payload fallback must not save under unrelated ID.");
166+
}
167+
if (activePayload.payloadJson.source !== "active-only") {
168+
failures.push("Test fixture mutated unexpectedly.");
169+
}
170+
171+
const libraryBeforeCleanup = {
172+
[fakeId]: { version: "v2", toolId: "asset-browser-v2", payloadJson: { old: true } },
173+
[validId]: { version: "v2", toolId: "asset-browser-v2", payloadJson: { valid: true } },
174+
"custom-user-label": { version: "v2", toolId: "asset-browser-v2", payloadJson: { custom: true } },
175+
"palette-manager-v2-1777676088718-zzzzzzzz": { version: "v2", toolId: "palette-manager-v2", payloadJson: { keep: true } }
176+
};
177+
const cleanupResult = cleanupStaleInvalidSavedEntries(libraryBeforeCleanup, storage);
178+
if (Object.prototype.hasOwnProperty.call(cleanupResult.nextLibrary, fakeId)) {
179+
failures.push("Stale invalid fake entry should be cleaned up.");
180+
}
181+
if (!Object.prototype.hasOwnProperty.call(cleanupResult.nextLibrary, validId)) {
182+
failures.push("Valid saved entry should not be removed by cleanup.");
183+
}
184+
if (!Object.prototype.hasOwnProperty.call(cleanupResult.nextLibrary, "custom-user-label")) {
185+
failures.push("Custom valid saved entry should not be removed by cleanup.");
186+
}
187+
if (!Object.prototype.hasOwnProperty.call(cleanupResult.nextLibrary, "palette-manager-v2-1777676088718-zzzzzzzz")) {
188+
failures.push("Host-like entry with matching tool metadata should not be removed.");
189+
}
190+
191+
fs.mkdirSync(path.dirname(resultsPath), { recursive: true });
192+
fs.writeFileSync(resultsPath, `${JSON.stringify({
193+
generatedAt: new Date().toISOString(),
194+
failures,
195+
checks: {
196+
jsExists,
197+
jsSyntax,
198+
testSyntax
199+
},
200+
scenarios: {
201+
saveFake,
202+
overwriteFake,
203+
saveValid,
204+
duplicateSave,
205+
overwriteValid,
206+
saveUnknownWithActive,
207+
cleanupResult
208+
}
209+
}, null, 2)}\n`, "utf8");
210+
211+
console.log(`v2 block-fake-session-save results: ${resultsPath}`);
212+
assert.equal(failures.length, 0, `V2 block-fake-session-save failures: ${failures.join(" | ")}`);
213+
return { failures };
214+
}
215+
216+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
217+
try {
218+
const summary = run();
219+
console.log(JSON.stringify(summary, null, 2));
220+
} catch (error) {
221+
console.error(error);
222+
process.exitCode = 1;
223+
}
224+
}

tools/workspace-v2/index.js

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -223,15 +223,52 @@ class WorkspaceV2SessionProducer {
223223
}
224224

225225
readSessionPayloadForLibraryWrite(sessionId) {
226-
const recentPayload = this.readSessionPayloadFromRecentSessionId(sessionId);
227-
if (this.isValidSessionPayload(recentPayload)) {
228-
return recentPayload;
226+
if (typeof sessionId !== "string" || !sessionId.trim()) {
227+
return null;
229228
}
230-
const activePayload = this.readActiveSessionPayloadForLibraryActions();
231-
if (this.isValidSessionPayload(activePayload)) {
232-
return activePayload;
229+
const raw = sessionStorage.getItem(sessionId.trim());
230+
if (typeof raw !== "string") {
231+
return null;
233232
}
234-
return null;
233+
const parsed = this.safeParseJson(raw);
234+
if (!parsed.ok || !this.isValidSessionPayload(parsed.value)) {
235+
return null;
236+
}
237+
return parsed.value;
238+
}
239+
240+
looksLikeWorkspaceHostContextId(sessionId) {
241+
if (typeof sessionId !== "string" || !sessionId.trim()) {
242+
return false;
243+
}
244+
return /-v2-\d{13}-[a-z0-9]{8}$/i.test(sessionId.trim());
245+
}
246+
247+
cleanupStaleInvalidSavedEntries(library) {
248+
if (!library || typeof library !== "object" || Array.isArray(library)) {
249+
return false;
250+
}
251+
let removedAny = false;
252+
Object.keys(library).forEach((sessionId) => {
253+
const payload = library[sessionId];
254+
if (!this.isValidSessionPayload(payload)) {
255+
return;
256+
}
257+
if (!this.looksLikeWorkspaceHostContextId(sessionId)) {
258+
return;
259+
}
260+
const storagePayload = this.readSessionPayloadForLibraryWrite(sessionId);
261+
const hasMatchingStorage = this.isValidSessionPayload(storagePayload);
262+
const payloadHostContextId = typeof payload.hostContextId === "string" ? payload.hostContextId.trim() : "";
263+
const payloadToolId = typeof payload.toolId === "string" ? payload.toolId.trim() : "";
264+
const idMatchesPayloadHostContext = payloadHostContextId && payloadHostContextId === sessionId;
265+
const idMatchesToolMetadata = payloadToolId && sessionId.startsWith(`${payloadToolId}-`);
266+
if (!hasMatchingStorage && !idMatchesPayloadHostContext && !idMatchesToolMetadata) {
267+
delete library[sessionId];
268+
removedAny = true;
269+
}
270+
});
271+
return removedAny;
235272
}
236273

237274
fixturePathForTool(toolId) {
@@ -388,6 +425,9 @@ class WorkspaceV2SessionProducer {
388425
this.libraryEmptyState.textContent = "Session library is invalid. Fix stored JSON or clear v2-session-library.";
389426
return;
390427
}
428+
if (this.cleanupStaleInvalidSavedEntries(library)) {
429+
localStorage.setItem(this.libraryStorageKey, JSON.stringify(library));
430+
}
391431
const sessionNames = Object.keys(library).sort((left, right) => left.localeCompare(right));
392432
this.sessionListNode.replaceChildren();
393433
this.libraryEmptyState.hidden = sessionNames.length > 0;
@@ -1728,11 +1768,6 @@ class WorkspaceV2SessionProducer {
17281768
this.setLibraryStatus(overwriteExisting ? "Enter a session ID before overwriting." : "Enter a session ID before saving.");
17291769
return;
17301770
}
1731-
const payloadForWrite = this.readSessionPayloadForLibraryWrite(sessionName);
1732-
if (!this.isValidSessionPayload(payloadForWrite)) {
1733-
this.setLibraryStatus("Session ID does not resolve to a valid Workspace V2 session.");
1734-
return;
1735-
}
17361771
const library = this.readSessionLibrary();
17371772
if (library === null) {
17381773
return;
@@ -1742,6 +1777,11 @@ class WorkspaceV2SessionProducer {
17421777
this.setLibraryStatus("Saved session already exists. Use Overwrite Session.");
17431778
return;
17441779
}
1780+
const payloadForWrite = this.readSessionPayloadForLibraryWrite(sessionName);
1781+
if (!this.isValidSessionPayload(payloadForWrite)) {
1782+
this.setLibraryStatus("Session ID does not resolve to a valid Workspace V2 session.");
1783+
return;
1784+
}
17451785
if (overwriteExisting && !exists) {
17461786
this.setLibraryStatus("Saved session not found. Use Save Session to create it first.");
17471787
return;

0 commit comments

Comments
 (0)