feat(06-03): /api/guidance POST/GET/DELETE — Bearer-claim gated, TTL-only service beneath (GREEN)

- POST: WR-01 cap (MAX_GUIDANCE_BODY_CHARS 8192, pre/post-read double check),
  hand-rolled snapshot validation (slug destinationId, bounded name, finite
  metrics, integer startedAtSec, polyline6 geometry >= 2 points via
  decodePolyline6) -> mintGuidanceSession with the CLAIM's chat_id — a body
  chat_id is structurally ignored (T-06-02 injection probe GW4)
- GET/DELETE: resolve then OWN — envelope.chat_id === claim.chat_id else 404,
  foreign and absent indistinguishable (T-06-02b); DELETE actively removes
  the record (D-14's server half); non-POST/GET/DELETE -> 405
- category-only logging: [guidance] rejected: unauthorized|malformed —
  literal strings, no interpolation of body/claim values (D-14)
- RP2 re-pin ships WITH the change (gate re-pin law): the total
  @araharah/routing ban becomes an explicit allowlist — exactly the
  polyline6 codec may enter the Worker (geometry validation); the routing
  stack stays web-side; RP1 unchanged
- bot/package.json + lock: @araharah/routing workspace dep
This commit is contained in:
araharah-executor
2026-10-24 13:06:25 +08:00
parent 212aa57c14
commit 7875ed4d94
5 changed files with 201 additions and 29 deletions
+14 -9
View File
@@ -306,31 +306,34 @@ test("GW2 POST with a non-conformant Bearer token (charset gate) → 401", async
test("GW3 POST over-cap body (8192+) with a VALID claim → 413; zero writes; category-only log; lying Content-Length backstop → 413", async () => {
const kv = makeKV();
const token = await bearerFor(kv, 101);
const token = await bearerFor(kv, 101); // the claim mint writes its own 2 session keys
const writesBefore = kv.ttlLog.length;
const overCap = JSON.stringify({ ...VALID_BODY, destinationName: "x".repeat(9000) });
assert.ok(overCap.length > 8192, "fixture must exceed the cap");
const { result, seen } = await captureLogs(() => handler(guidanceReq("POST", { body: overCap, token }), makeEnv(kv), {}));
const res = await result;
assert.equal(res.status, 413, "the pre-read cap refuses to buffer");
assert.deepEqual(await res.json(), { error: "malformed" });
assert.equal(kv.ttlLog.length, 0);
assert.equal(kv.ttlLog.length, writesBefore, "an over-cap body writes nothing");
assert.deepEqual(seen, ["[guidance] rejected: malformed"], "category ONLY");
// lying header: declares 100, ships 9000+ → the post-read check closes the blind spot
const kv2 = makeKV();
const token2 = await bearerFor(kv2, 101);
const writesBefore2 = kv2.ttlLog.length;
const lying = await handler(
guidanceReq("POST", { body: overCap, token: token2, headers: { "content-length": "100" } }),
makeEnv(kv2),
{},
);
assert.equal(lying.status, 413, "the post-read length check must backstop a lying header");
assert.equal(kv2.ttlLog.length, 0);
assert.equal(kv2.ttlLog.length, writesBefore2);
});
test("GW4 POST happy → 201 {id: uuid}; exactly one 7200s put at guidance:<id>; body chat_id 999 IGNORED (T-06-02); extra fields dropped", async () => {
const kv = makeKV();
const token = await bearerFor(kv, 101);
const token = await bearerFor(kv, 101); // the claim mint writes its own 2 session keys
const writesBefore = kv.ttlLog.length;
const res = await handler(
guidanceReq("POST", { body: { ...VALID_BODY, chat_id: 999, extra: "junk" }, token }),
makeEnv(kv),
@@ -339,9 +342,10 @@ test("GW4 POST happy → 201 {id: uuid}; exactly one 7200s put at guidance:<id>;
assert.equal(res.status, 201, `expected 201, got ${res.status}: ${await res.clone().text()}`);
const { id } = await res.json();
assert.match(id, /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, "opaque UUID id");
assert.equal(kv.ttlLog.length, 1, "mint is exactly one KV write");
assert.equal(kv.ttlLog[0].key, `guidance:${id}`);
assert.equal(kv.ttlLog[0].expirationTtl, 7200, "TTL-only (GE1)");
assert.equal(kv.ttlLog.length, writesBefore + 1, "mint is exactly one KV write");
const guidanceWrite = kv.ttlLog[kv.ttlLog.length - 1];
assert.equal(guidanceWrite.key, `guidance:${id}`);
assert.equal(guidanceWrite.expirationTtl, 7200, "TTL-only (GE1)");
const stored = JSON.parse(kv.map.get(`guidance:${id}`));
assert.equal(stored.chat_id, 101, "identity comes from the CLAIM — the body's chat_id 999 is structurally ignored (T-06-02)");
assert.deepEqual(Object.keys(stored).sort(), ["chat_id", "created_at", "id", "route_snapshot"], "frozen 4-field envelope");
@@ -368,13 +372,14 @@ test("GW5 malformed snapshot classes → 400 {error:'malformed'} + category-only
];
for (const [label, body] of badBodies) {
const kv = makeKV();
const token = await bearerFor(kv, 101);
const token = await bearerFor(kv, 101); // the claim mint writes its own 2 session keys
const writesBefore = kv.ttlLog.length;
const { result, seen } = await captureLogs(() => handler(guidanceReq("POST", { body, token }), makeEnv(kv), {}));
const res = await result;
assert.equal(res.status, 400, `${label} must be a 400, got ${res.status}`);
assert.deepEqual(await res.json(), { error: "malformed" }, `${label} carries the category only`);
assert.deepEqual(seen, ["[guidance] rejected: malformed"], `${label} logs the category ONLY`);
assert.equal(kv.ttlLog.length, 0, `${label} writes nothing`);
assert.equal(kv.ttlLog.length, writesBefore, `${label} writes nothing`);
}
});