feat(02-02): TTL-only KV sessions with rotate-on-new + PRIV-02 storage gates (D-06/D-07/D-08/D-14)
- sessions.ts: mintSession (crypto.randomUUID opaque token, T-02-05), rotate-on-new deletes prior session record via chat pointer (D-07), both puts carry expirationTtl: SESSION_TTL_SECONDS on the call line (PRIV-02) - SESSION_TTL_SECONDS = 900 pairs the session envelope with the D-13 freshness window - SessionClaim is exactly D-08: user_id, chat_id, issued_at, language_code — nothing else serializes to storage - sessions.test.mjs: in-memory KV fake asserting finite expirationTtl on every put; D-08 key-set equality; rotate removes old token; expired-token → null (D-14 class) - privacy.test.mjs: PG1 same-line expirationTtl source gate (auto-tightens to webhook.ts when plan 03 adds it) + PG2 no persistence-shaped exports - 18/18 PASS across initdata + sessions + privacy suites; full plan GREEN
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
* privacy.test.mjs — PRIV-02 architecture gates (zero-KV, source-introspection).
|
||||
* ------------------------------------------------------------------------------
|
||||
* The storage promise is pinned by reading the source itself (D-17:
|
||||
* "verifiable by reading the code's storage calls"):
|
||||
*
|
||||
* PG1 every `.put(` line in the TTL storage module carries
|
||||
* `expirationTtl` on the same line — no TTL-free write path can ship
|
||||
* PG2 the storage module exposes no persistence-shaped exports
|
||||
* (no saveProfile / storeLocation / saveLocation / storeHistory)
|
||||
*
|
||||
* src/webhook.ts is included when it exists (plan 03 builds it) so the
|
||||
* gate tightens automatically as call sites grow — future write paths
|
||||
* fail this suite unless they, too, are TTL-carried.
|
||||
*/
|
||||
import { strict as assert } from "node:assert";
|
||||
import { readFileSync, existsSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import path from "node:path";
|
||||
import { test } from "./harness.mjs";
|
||||
|
||||
const SRC = path.join(fileURLToPath(new URL(import.meta.url)), "..", "..", "src");
|
||||
|
||||
const storageModules = ["kv/sessions.ts"].map((rel) => path.join(SRC, ...rel.split("/")));
|
||||
const webhookModule = path.join(SRC, "webhook.ts");
|
||||
if (existsSync(webhookModule)) storageModules.push(webhookModule);
|
||||
|
||||
/* ————— PG1 — same-line expirationTtl gate on every write path ————— */
|
||||
|
||||
test("PG1 every .put( in storage sources carries expirationTtl on the same line", () => {
|
||||
for (const file of storageModules) {
|
||||
const lines = readFileSync(file, "utf8").split("\n");
|
||||
lines.forEach((line, i) => {
|
||||
if (line.includes(".put(")) {
|
||||
assert.ok(
|
||||
line.includes("expirationTtl"),
|
||||
`${file}:${i + 1} — KV write without expirationTtl (PRIV-02 gate): ${line.trim()}`,
|
||||
);
|
||||
const ttl = line.match(/expirationTtl:\s*([A-Za-z0-9_]+)/)?.[1];
|
||||
assert.ok(
|
||||
ttl && /^[A-Z_]{2,}$/.test(ttl),
|
||||
`${file}:${i + 1} — expirationTtl must reference a named TTL constant, not an inline literal`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/* ————— PG2 — no persistent-shaped surface ————— */
|
||||
|
||||
test("PG2 storage module exports nothing persistence-shaped (no profiles, no saved locations)", () => {
|
||||
for (const file of storageModules) {
|
||||
const source = readFileSync(file, "utf8");
|
||||
assert.doesNotMatch(
|
||||
source,
|
||||
/\b(?:saveProfile|storeProfile|saveLocation|storeLocation|saveHistory|storeHistory|putHistory|setLocation)\b/,
|
||||
`${file} — persistence-shaped export found (D-08 claim surface violation)`,
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user