- b_page_state 맨 위 로케일 가져오기가 이 파일을 Node 로 돌리는 시험(346-4 등)을 깨던 것 - 넘침 안내 때만 토스트 · 로케일을 함께 동적으로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn
131 lines
5.3 KiB
JavaScript
131 lines
5.3 KiB
JavaScript
/* 388 보안 — 진짜 TS(b_page_state · b_project_switch · design_store)를 가짜 저장소 위에서 돌려
|
|
* JSON 으로 냄: 로그아웃 지움 · 계정 바뀜 지움 · 세션 넘침 · 탭이 연 프로젝트 고정. */
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
|
|
|
const ROOT = path.join(__dirname, "..", "..");
|
|
const load = (rel, requireStub) => {
|
|
const js = ts.transpileModule(fs.readFileSync(path.join(ROOT, rel), "utf8"), {
|
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
|
}).outputText;
|
|
const exports = {};
|
|
new Function("exports", "require", "window", js)(exports, requireStub, global.window);
|
|
return exports;
|
|
};
|
|
|
|
/** 진짜 Storage 처럼 Object.keys 가 열쇠를 냄 · limit 글자 넘으면 QuotaExceededError. */
|
|
function fakeStorage(limit = Infinity) {
|
|
const data = {};
|
|
const size = () => Object.entries(data).reduce((n, [k, v]) => n + k.length + v.length, 0);
|
|
const api = {
|
|
length: { get: () => Object.keys(data).length },
|
|
key: { value: (i) => Object.keys(data)[i] ?? null },
|
|
getItem: { value: (k) => (k in data ? data[k] : null) },
|
|
setItem: {
|
|
value: (k, v) => {
|
|
const old = data[k];
|
|
data[k] = String(v);
|
|
if (size() > limit) {
|
|
if (old === undefined) delete data[k];
|
|
else data[k] = old;
|
|
const error = new Error("full");
|
|
error.name = "QuotaExceededError";
|
|
throw error;
|
|
}
|
|
},
|
|
},
|
|
removeItem: { value: (k) => delete data[k] },
|
|
clear: { value: () => Object.keys(data).forEach((k) => delete data[k]) },
|
|
};
|
|
return new Proxy(data, {
|
|
get: (t, p) => (p in api ? (api[p].get ? api[p].get() : api[p].value) : t[p]),
|
|
});
|
|
}
|
|
global.window = { sessionStorage: fakeStorage(), localStorage: fakeStorage() };
|
|
|
|
const toasts = [];
|
|
const pageState = load("A00_Common/b_page_state.ts", () => ({
|
|
t: (key) => key,
|
|
showToast: (msg, kind) => toasts.push([msg, kind]),
|
|
}));
|
|
const purged = [];
|
|
const config = { CURRENT_PROJECT_ID_KEY: "frd_current_project_id" };
|
|
const sw = load("A00_Common/b_project_switch.ts", (id) => {
|
|
if (id.includes("b_page_state")) return pageState;
|
|
if (id.includes("b_asset_cache")) return { purgeOtherProjects: async (p) => purged.push(p) };
|
|
if (id.includes("config_frontend")) return config;
|
|
return { showConfirmDialog: async () => true };
|
|
});
|
|
const store = load("A00_Common/design/design_store.ts", (id) =>
|
|
id.includes("b_page_state") ? pageState : config,
|
|
);
|
|
|
|
const P = "aaaa1111";
|
|
const seed = () => {
|
|
global.window.sessionStorage = fakeStorage();
|
|
const s = global.window.sessionStorage;
|
|
s.setItem(`aislo:draft:structures:${P}`, '[{"id":1}]');
|
|
s.setItem(`aislo:result:section-detail:${P}:7`, '{"d":1}');
|
|
s.setItem("aislo:entered-project", P);
|
|
s.setItem("b05:pipes", "[1]");
|
|
global.window.localStorage.setItem("frd_current_project_id", P);
|
|
global.window.localStorage.setItem("aislo:pref:panel:x", "1");
|
|
purged.length = 0;
|
|
};
|
|
const sessionKeys = () => Object.keys(global.window.sessionStorage).sort();
|
|
const local = (k) => global.window.localStorage.getItem(k);
|
|
const tick = () => new Promise((r) => setTimeout(r, 0));
|
|
|
|
(async () => {
|
|
const out = {};
|
|
|
|
seed();
|
|
await sw.wipeAccountCache();
|
|
out.logout = {
|
|
session: sessionKeys(),
|
|
project: local("frd_current_project_id"),
|
|
pref: local("aislo:pref:panel:x"),
|
|
purged: [...purged],
|
|
};
|
|
|
|
global.window.localStorage.removeItem("aislo:owner");
|
|
seed();
|
|
out.owner = { first: await sw.wipeIfOwnerChanged("u1"), firstKeys: sessionKeys().length };
|
|
seed();
|
|
out.owner.same = await sw.wipeIfOwnerChanged("u1");
|
|
out.owner.sameKeys = sessionKeys().length;
|
|
out.owner.other = await sw.wipeIfOwnerChanged("u2");
|
|
out.owner.otherKeys = sessionKeys().length;
|
|
out.owner.otherProject = local("frd_current_project_id");
|
|
out.owner.stored = local("aislo:owner");
|
|
// 서버 id 가 수로 옴 — 같은 계정 수 id 는 두 번째에 안 지움(새로고침 되풀이 막음)
|
|
await sw.wipeIfOwnerChanged(3);
|
|
out.owner.numericAgain = await sw.wipeIfOwnerChanged(3);
|
|
|
|
// 세션 넘침 — 결과 칸 비우고 다시 · 그래도 넘치면 안내
|
|
global.window.sessionStorage = fakeStorage(400);
|
|
const s = global.window.sessionStorage;
|
|
s.setItem(`aislo:result:section-detail:${P}:7`, "r".repeat(200));
|
|
s.setItem(`aislo:draft:structures:${P}`, "d".repeat(50));
|
|
pageState.writeByKey(`aislo:draft:crossw:${P}:7`, "c".repeat(100));
|
|
out.overflow = {
|
|
saved: s.getItem(`aislo:draft:crossw:${P}:7`)?.length ?? 0,
|
|
result: s.getItem(`aislo:result:section-detail:${P}:7`),
|
|
draft: s.getItem(`aislo:draft:structures:${P}`)?.length ?? 0,
|
|
toasts: toasts.length,
|
|
};
|
|
pageState.writeByKey(`aislo:draft:crossw:${P}:8`, "c".repeat(500));
|
|
await tick();
|
|
out.full = { saved: s.getItem(`aislo:draft:crossw:${P}:8`), toasts: [...toasts] };
|
|
|
|
// 탭이 연 프로젝트 고정 — 다른 탭이 현재 프로젝트를 바꿔도 그대로
|
|
global.window.localStorage.setItem("frd_current_project_id", "first");
|
|
out.pin = { before: store.draftProjectId() };
|
|
store.pinDraftProject("first");
|
|
global.window.localStorage.setItem("frd_current_project_id", "other-tab");
|
|
out.pin.after = store.draftProjectId();
|
|
out.pin.explicit = store.draftProjectId("given");
|
|
process.stdout.write(JSON.stringify(out));
|
|
})();
|