@sudobility/sider_lib 0.0.4 → 0.0.6

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/gates.js CHANGED
@@ -44,18 +44,19 @@ export function evaluateGates(req, ctx) {
44
44
  if (!b.ok)
45
45
  return b;
46
46
  }
47
- // --- Gate C: mutations require confirmation -----------------------------
48
- // Bind to the HTTP method, not just the server-declared (and therefore
49
- // untrusted) safetyClass — a recipe labeled "read" with a POST/PUT/PATCH/
50
- // DELETE still mutates and must not run unconfirmed.
47
+ // --- Gate C: mutations respect site policy; financial requires confirm ---
48
+ // Bind "is a mutation" to the HTTP method as well as the declared class (a
49
+ // recipe labeled "read" with a POST still mutates). Product decision
50
+ // 2026-07-22: only `financial` actions need user confirmation; ordinary
51
+ // writes (add-to-cart level) run automatically. `allowMutations` remains the
52
+ // per-site hard stop for ALL mutations.
51
53
  const idempotent = req.method === "GET" || req.method === "HEAD" || req.method === "OPTIONS";
52
- if (req.safetyClass !== "read" || !idempotent) {
53
- if (ctx.allowMutations === false) {
54
- return { ok: false, gate: "safety", reason: `mutations disabled for ${ctx.siteOrigin}` };
55
- }
56
- if (!ctx.confirmed) {
57
- return { ok: false, gate: "safety", reason: `mutating request requires confirmation` };
58
- }
54
+ const mutating = req.safetyClass !== "read" || !idempotent;
55
+ if (mutating && ctx.allowMutations === false) {
56
+ return { ok: false, gate: "safety", reason: `mutations disabled for ${ctx.siteOrigin}` };
57
+ }
58
+ if (req.safetyClass === "financial" && !ctx.confirmed) {
59
+ return { ok: false, gate: "safety", reason: `financial request requires confirmation` };
59
60
  }
60
61
  return { ok: true };
61
62
  }
@@ -64,8 +64,31 @@ test("Gate B: refuses when the sentinel is absent from its declared location", (
64
64
  if (!out.ok)
65
65
  expect(out.gate).toBe("injection_location");
66
66
  });
67
- test("Gate C: a POST (method-bound) requires confirmation even if labeled read", () => {
68
- const out = evaluateGates(req({ method: "POST", safetyClass: "read" }), ctx());
67
+ test("Gate C: a POST labeled read runs unconfirmed (financial-only policy)", () => {
68
+ expect(evaluateGates(req({ method: "POST", safetyClass: "read" }), ctx())).toEqual({ ok: true });
69
+ });
70
+ test("Gate C: write POST runs unconfirmed (financial-only policy)", () => {
71
+ const out = evaluateGates(req({ method: "POST", safetyClass: "write" }), ctx());
72
+ expect(out).toEqual({ ok: true });
73
+ });
74
+ test("Gate C: financial request without confirmation is blocked", () => {
75
+ const out = evaluateGates(req({ method: "POST", safetyClass: "financial" }), ctx());
76
+ expect(out.ok).toBe(false);
77
+ if (!out.ok)
78
+ expect(out.gate).toBe("safety");
79
+ });
80
+ test("Gate C: financial request with confirmation passes", () => {
81
+ const out = evaluateGates(req({ method: "POST", safetyClass: "financial" }), ctx({ confirmed: true }));
82
+ expect(out).toEqual({ ok: true });
83
+ });
84
+ test("Gate C: GET declared financial still requires confirmation", () => {
85
+ const out = evaluateGates(req({ safetyClass: "financial" }), ctx());
86
+ expect(out.ok).toBe(false);
87
+ if (!out.ok)
88
+ expect(out.gate).toBe("safety");
89
+ });
90
+ test("Gate C: allowMutations=false still blocks an unconfirmed write", () => {
91
+ const out = evaluateGates(req({ method: "POST", safetyClass: "write" }), ctx({ allowMutations: false }));
69
92
  expect(out.ok).toBe(false);
70
93
  if (!out.ok)
71
94
  expect(out.gate).toBe("safety");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sudobility/sider_lib",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Sider business logic — recipe compiler, egress gates, tokenizer, clustering. Pure, no Chrome/DOM/server.",
5
5
  "license": "BUSL-1.1",
6
6
  "publishConfig": {
@@ -19,7 +19,7 @@
19
19
  "test": "bun test"
20
20
  },
21
21
  "dependencies": {
22
- "@sudobility/sider_types": "^0.0.5"
22
+ "@sudobility/sider_types": "^0.0.6"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@types/bun": "^1.3.14",
package/src/gates.test.ts CHANGED
@@ -74,8 +74,34 @@ test("Gate B: refuses when the sentinel is absent from its declared location", (
74
74
  if (!out.ok) expect(out.gate).toBe("injection_location");
75
75
  });
76
76
 
77
- test("Gate C: a POST (method-bound) requires confirmation even if labeled read", () => {
78
- const out = evaluateGates(req({ method: "POST", safetyClass: "read" }), ctx());
77
+ test("Gate C: a POST labeled read runs unconfirmed (financial-only policy)", () => {
78
+ expect(evaluateGates(req({ method: "POST", safetyClass: "read" }), ctx())).toEqual({ ok: true });
79
+ });
80
+
81
+ test("Gate C: write POST runs unconfirmed (financial-only policy)", () => {
82
+ const out = evaluateGates(req({ method: "POST", safetyClass: "write" }), ctx());
83
+ expect(out).toEqual({ ok: true });
84
+ });
85
+
86
+ test("Gate C: financial request without confirmation is blocked", () => {
87
+ const out = evaluateGates(req({ method: "POST", safetyClass: "financial" }), ctx());
88
+ expect(out.ok).toBe(false);
89
+ if (!out.ok) expect(out.gate).toBe("safety");
90
+ });
91
+
92
+ test("Gate C: financial request with confirmation passes", () => {
93
+ const out = evaluateGates(req({ method: "POST", safetyClass: "financial" }), ctx({ confirmed: true }));
94
+ expect(out).toEqual({ ok: true });
95
+ });
96
+
97
+ test("Gate C: GET declared financial still requires confirmation", () => {
98
+ const out = evaluateGates(req({ safetyClass: "financial" }), ctx());
99
+ expect(out.ok).toBe(false);
100
+ if (!out.ok) expect(out.gate).toBe("safety");
101
+ });
102
+
103
+ test("Gate C: allowMutations=false still blocks an unconfirmed write", () => {
104
+ const out = evaluateGates(req({ method: "POST", safetyClass: "write" }), ctx({ allowMutations: false }));
79
105
  expect(out.ok).toBe(false);
80
106
  if (!out.ok) expect(out.gate).toBe("safety");
81
107
  });
package/src/gates.ts CHANGED
@@ -59,18 +59,19 @@ export function evaluateGates(req: CompiledRequest, ctx: GateContext): GateOutco
59
59
  if (!b.ok) return b;
60
60
  }
61
61
 
62
- // --- Gate C: mutations require confirmation -----------------------------
63
- // Bind to the HTTP method, not just the server-declared (and therefore
64
- // untrusted) safetyClass — a recipe labeled "read" with a POST/PUT/PATCH/
65
- // DELETE still mutates and must not run unconfirmed.
62
+ // --- Gate C: mutations respect site policy; financial requires confirm ---
63
+ // Bind "is a mutation" to the HTTP method as well as the declared class (a
64
+ // recipe labeled "read" with a POST still mutates). Product decision
65
+ // 2026-07-22: only `financial` actions need user confirmation; ordinary
66
+ // writes (add-to-cart level) run automatically. `allowMutations` remains the
67
+ // per-site hard stop for ALL mutations.
66
68
  const idempotent = req.method === "GET" || req.method === "HEAD" || req.method === "OPTIONS";
67
- if (req.safetyClass !== "read" || !idempotent) {
68
- if (ctx.allowMutations === false) {
69
- return { ok: false, gate: "safety", reason: `mutations disabled for ${ctx.siteOrigin}` };
70
- }
71
- if (!ctx.confirmed) {
72
- return { ok: false, gate: "safety", reason: `mutating request requires confirmation` };
73
- }
69
+ const mutating = req.safetyClass !== "read" || !idempotent;
70
+ if (mutating && ctx.allowMutations === false) {
71
+ return { ok: false, gate: "safety", reason: `mutations disabled for ${ctx.siteOrigin}` };
72
+ }
73
+ if (req.safetyClass === "financial" && !ctx.confirmed) {
74
+ return { ok: false, gate: "safety", reason: `financial request requires confirmation` };
74
75
  }
75
76
 
76
77
  return { ok: true };