@aporthq/aport-agent-guardrails 1.0.8

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.
Files changed (237) hide show
  1. package/LICENSE +217 -0
  2. package/README.md +481 -0
  3. package/bin/agent-guardrails +133 -0
  4. package/bin/aport-create-passport.sh +444 -0
  5. package/bin/aport-cursor-hook.sh +90 -0
  6. package/bin/aport-guardrail-api.sh +108 -0
  7. package/bin/aport-guardrail-bash.sh +394 -0
  8. package/bin/aport-guardrail-v2.sh +5 -0
  9. package/bin/aport-guardrail.sh +5 -0
  10. package/bin/aport-resolve-paths.sh +71 -0
  11. package/bin/aport-status.sh +276 -0
  12. package/bin/frameworks/crewai.sh +49 -0
  13. package/bin/frameworks/cursor.sh +95 -0
  14. package/bin/frameworks/langchain.sh +48 -0
  15. package/bin/frameworks/n8n.sh +36 -0
  16. package/bin/frameworks/openclaw.sh +19 -0
  17. package/bin/lib/allowlist.sh +18 -0
  18. package/bin/lib/common.sh +28 -0
  19. package/bin/lib/config.sh +46 -0
  20. package/bin/lib/constants.sh +232 -0
  21. package/bin/lib/detect.sh +65 -0
  22. package/bin/lib/error.sh +269 -0
  23. package/bin/lib/passport.sh +19 -0
  24. package/bin/lib/templates/.gitkeep +1 -0
  25. package/bin/lib/templates/config.yaml +6 -0
  26. package/bin/lib/validation.sh +206 -0
  27. package/bin/openclaw +660 -0
  28. package/docs/ADDING_A_FRAMEWORK.md +87 -0
  29. package/docs/AGENTS.md.example +40 -0
  30. package/docs/CODE_REVIEW.md +192 -0
  31. package/docs/DEPLOYMENT_READINESS.md +81 -0
  32. package/docs/FAQ_SECURITY_SCANNERS.md +373 -0
  33. package/docs/FRAMEWORK_ROADMAP.md +41 -0
  34. package/docs/HOSTED_PASSPORT_SETUP.md +362 -0
  35. package/docs/IMPLEMENTING_YOUR_OWN_EVALUATOR.md +433 -0
  36. package/docs/OPENCLAW_COMPATIBILITY.md +73 -0
  37. package/docs/OPENCLAW_LOCAL_INTEGRATION.md +596 -0
  38. package/docs/OPENCLAW_TOOLS_AND_POLICIES.md +54 -0
  39. package/docs/QUICKSTART.md +470 -0
  40. package/docs/QUICKSTART_OPENCLAW_PLUGIN.md +470 -0
  41. package/docs/README.md +28 -0
  42. package/docs/RELEASE.md +87 -0
  43. package/docs/REPO_LAYOUT.md +47 -0
  44. package/docs/SKILLS_ECOSYSTEM_ANALYSIS_FEB17.md +1260 -0
  45. package/docs/TOOL_POLICY_MAPPING.md +46 -0
  46. package/docs/UPGRADE.md +46 -0
  47. package/docs/VERIFICATION_METHODS.md +97 -0
  48. package/docs/assets/README.md +8 -0
  49. package/docs/assets/porter.svg +54 -0
  50. package/docs/development/ERROR_CODES.md +616 -0
  51. package/docs/frameworks/GITHUB_ISSUE_PROPOSALS.md +1105 -0
  52. package/docs/frameworks/crewai.md +114 -0
  53. package/docs/frameworks/cursor.md +159 -0
  54. package/docs/frameworks/langchain.md +72 -0
  55. package/docs/frameworks/n8n.md +40 -0
  56. package/docs/frameworks/openclaw.md +40 -0
  57. package/docs/launch/ADD_APORT_AWESOME_LISTS_INSTRUCTIONS.md +146 -0
  58. package/docs/launch/ANNOUNCEMENT_GUIDE.md +266 -0
  59. package/docs/launch/AWESOME_REPOS.md +53 -0
  60. package/docs/launch/CURSOR_VSCODE_HOOKS_RESEARCH.md +77 -0
  61. package/docs/launch/DEMO_TERMINAL_OUTPUT.txt +48 -0
  62. package/docs/launch/DRY_AND_PLAN_CHECKLIST.md +47 -0
  63. package/docs/launch/EVIDENCE_README.md +61 -0
  64. package/docs/launch/EVIDENCE_TERMINAL_CAPTURE.txt +10 -0
  65. package/docs/launch/FRAMEWORK_SUPPORT_PLAN.md +1640 -0
  66. package/docs/launch/LAUNCH_READINESS_CHECKLIST.md +237 -0
  67. package/docs/launch/LAUNCH_STRATEGY_SUMMARY.md +464 -0
  68. package/docs/launch/OPENCLAW_FEEDBACK_AND_FIXES.md +85 -0
  69. package/docs/launch/POST_1_VALENTINE_IMPROVED.md +233 -0
  70. package/docs/launch/POST_2_GUARDRAIL_IMPROVED.md +369 -0
  71. package/docs/launch/PRE_LAUNCH_FIXES.md +766 -0
  72. package/docs/launch/QUICK_LAUNCH_CHECKLIST.md +400 -0
  73. package/docs/launch/READINESS_SUMMARY.md +262 -0
  74. package/docs/launch/README.md +68 -0
  75. package/docs/launch/USER_STORIES.md +327 -0
  76. package/docs/launch/scripts/add-aport-awesome-pr.sh +69 -0
  77. package/docs/operations/MONITORING.md +588 -0
  78. package/docs/reviews/2026-02-18-staff-review.md +268 -0
  79. package/extensions/openclaw-aport/README.md +415 -0
  80. package/extensions/openclaw-aport/index.js +625 -0
  81. package/extensions/openclaw-aport/openclaw-aport.js +7 -0
  82. package/extensions/openclaw-aport/openclaw.plugin.json +46 -0
  83. package/extensions/openclaw-aport/package.json +36 -0
  84. package/extensions/openclaw-aport/test.js +307 -0
  85. package/external/aport-policies/README.md +363 -0
  86. package/external/aport-policies/agent.session.create.v1/README.md +345 -0
  87. package/external/aport-policies/agent.session.create.v1/policy.json +162 -0
  88. package/external/aport-policies/agent.tool.register.v1/README.md +361 -0
  89. package/external/aport-policies/agent.tool.register.v1/policy.json +172 -0
  90. package/external/aport-policies/code.release.publish.v1/README.md +51 -0
  91. package/external/aport-policies/code.release.publish.v1/policy.json +121 -0
  92. package/external/aport-policies/code.repository.merge.v1/README.md +287 -0
  93. package/external/aport-policies/code.repository.merge.v1/express.example.js +332 -0
  94. package/external/aport-policies/code.repository.merge.v1/fastapi.example.py +370 -0
  95. package/external/aport-policies/code.repository.merge.v1/policy.json +162 -0
  96. package/external/aport-policies/data.export.create.v1/README.md +226 -0
  97. package/external/aport-policies/data.export.create.v1/express.example.js +172 -0
  98. package/external/aport-policies/data.export.create.v1/fastapi.example.py +165 -0
  99. package/external/aport-policies/data.export.create.v1/policy.json +133 -0
  100. package/external/aport-policies/data.report.ingest.v1/README.md +134 -0
  101. package/external/aport-policies/data.report.ingest.v1/express.example.js +105 -0
  102. package/external/aport-policies/data.report.ingest.v1/minimal-example.js +68 -0
  103. package/external/aport-policies/data.report.ingest.v1/policy.json +174 -0
  104. package/external/aport-policies/finance.crypto.trade.v1/README.md +146 -0
  105. package/external/aport-policies/finance.crypto.trade.v1/express.example.js +109 -0
  106. package/external/aport-policies/finance.crypto.trade.v1/minimal-example.js +65 -0
  107. package/external/aport-policies/finance.crypto.trade.v1/policy.json +176 -0
  108. package/external/aport-policies/finance.payment.charge.v1/README.md +326 -0
  109. package/external/aport-policies/finance.payment.charge.v1/express.example.js +250 -0
  110. package/external/aport-policies/finance.payment.charge.v1/fastapi.example.py +227 -0
  111. package/external/aport-policies/finance.payment.charge.v1/minimal-example.js +64 -0
  112. package/external/aport-policies/finance.payment.charge.v1/policy.json +224 -0
  113. package/external/aport-policies/finance.payment.charge.v1/tests/contexts.jsonl +12 -0
  114. package/external/aport-policies/finance.payment.charge.v1/tests/expected.jsonl +12 -0
  115. package/external/aport-policies/finance.payment.charge.v1/tests/passport.instance.json +42 -0
  116. package/external/aport-policies/finance.payment.charge.v1/tests/passport.template.json +40 -0
  117. package/external/aport-policies/finance.payment.charge.v1/tests/payments-charge-policy.test.js +817 -0
  118. package/external/aport-policies/finance.payment.charge.v1/tests/test_payments_charge_policy.py +486 -0
  119. package/external/aport-policies/finance.payment.payout.v1/README.md +78 -0
  120. package/external/aport-policies/finance.payment.payout.v1/policy.json +181 -0
  121. package/external/aport-policies/finance.payment.refund.v1/README.md +275 -0
  122. package/external/aport-policies/finance.payment.refund.v1/express.example.js +167 -0
  123. package/external/aport-policies/finance.payment.refund.v1/fastapi.example.py +136 -0
  124. package/external/aport-policies/finance.payment.refund.v1/minimal-example.js +183 -0
  125. package/external/aport-policies/finance.payment.refund.v1/policy.json +216 -0
  126. package/external/aport-policies/finance.payment.refund.v1/tests/refunds-policy.test.js +924 -0
  127. package/external/aport-policies/finance.payment.refund.v1/tests/test_refunds_policy.py +778 -0
  128. package/external/aport-policies/finance.transaction.execute.v1/README.md +309 -0
  129. package/external/aport-policies/finance.transaction.execute.v1/express.example.js +261 -0
  130. package/external/aport-policies/finance.transaction.execute.v1/fastapi.example.py +231 -0
  131. package/external/aport-policies/finance.transaction.execute.v1/minimal-example.js +78 -0
  132. package/external/aport-policies/finance.transaction.execute.v1/policy.json +189 -0
  133. package/external/aport-policies/finance.transaction.execute.v1/tests/contexts.jsonl +12 -0
  134. package/external/aport-policies/finance.transaction.execute.v1/tests/expected.jsonl +12 -0
  135. package/external/aport-policies/finance.transaction.execute.v1/tests/passport.instance.json +42 -0
  136. package/external/aport-policies/finance.transaction.execute.v1/tests/passport.template.json +42 -0
  137. package/external/aport-policies/finance.transaction.execute.v1/tests/test_transactions_policy.py +214 -0
  138. package/external/aport-policies/finance.transaction.execute.v1/tests/transactions-policy.test.js +306 -0
  139. package/external/aport-policies/governance.data.access.v1/README.md +292 -0
  140. package/external/aport-policies/governance.data.access.v1/express.example.js +321 -0
  141. package/external/aport-policies/governance.data.access.v1/fastapi.example.py +279 -0
  142. package/external/aport-policies/governance.data.access.v1/minimal-example.js +65 -0
  143. package/external/aport-policies/governance.data.access.v1/policy.json +208 -0
  144. package/external/aport-policies/governance.data.access.v1/tests/contexts.jsonl +12 -0
  145. package/external/aport-policies/governance.data.access.v1/tests/data-access-policy.test.js +308 -0
  146. package/external/aport-policies/governance.data.access.v1/tests/expected.jsonl +12 -0
  147. package/external/aport-policies/governance.data.access.v1/tests/passport.instance.json +56 -0
  148. package/external/aport-policies/governance.data.access.v1/tests/passport.template.json +56 -0
  149. package/external/aport-policies/governance.data.access.v1/tests/test_data_access_policy.py +214 -0
  150. package/external/aport-policies/legal.contract.review.v1/README.md +109 -0
  151. package/external/aport-policies/legal.contract.review.v1/policy.json +378 -0
  152. package/external/aport-policies/legal.contract.review.v1/tests/legal-contract-review-policy.test.js +609 -0
  153. package/external/aport-policies/legal.contract.review.v1/tests/passport.template.json +49 -0
  154. package/external/aport-policies/mcp.tool.execute.v1/README.md +301 -0
  155. package/external/aport-policies/mcp.tool.execute.v1/policy.json +141 -0
  156. package/external/aport-policies/messaging.message.send.v1/README.md +230 -0
  157. package/external/aport-policies/messaging.message.send.v1/express.example.js +183 -0
  158. package/external/aport-policies/messaging.message.send.v1/fastapi.example.py +193 -0
  159. package/external/aport-policies/messaging.message.send.v1/policy.json +144 -0
  160. package/external/aport-policies/policy-template.json +107 -0
  161. package/external/aport-policies/system.command.execute.v1/README.md +275 -0
  162. package/external/aport-policies/system.command.execute.v1/policy.json +146 -0
  163. package/external/aport-spec/CONTRIBUTING.md +273 -0
  164. package/external/aport-spec/LICENSE +21 -0
  165. package/external/aport-spec/README.md +168 -0
  166. package/external/aport-spec/conformance/README.md +294 -0
  167. package/external/aport-spec/conformance/cases/data.export.v1/contexts/allow_users.json +6 -0
  168. package/external/aport-spec/conformance/cases/data.export.v1/contexts/deny_pii.json +6 -0
  169. package/external/aport-spec/conformance/cases/data.export.v1/expected/allow_users.decision.json +19 -0
  170. package/external/aport-spec/conformance/cases/data.export.v1/expected/deny_pii.decision.json +19 -0
  171. package/external/aport-spec/conformance/cases/data.export.v1/passports/template.json +29 -0
  172. package/external/aport-spec/conformance/cases/payments.refunds.v1/contexts/allow_50usd.json +9 -0
  173. package/external/aport-spec/conformance/cases/payments.refunds.v1/contexts/deny_150usd.json +9 -0
  174. package/external/aport-spec/conformance/cases/payments.refunds.v1/contexts/deny_currency.json +9 -0
  175. package/external/aport-spec/conformance/cases/payments.refunds.v1/expected/allow_50usd.decision.json +19 -0
  176. package/external/aport-spec/conformance/cases/payments.refunds.v1/expected/deny_150usd.decision.json +19 -0
  177. package/external/aport-spec/conformance/cases/payments.refunds.v1/expected/deny_currency.decision.json +19 -0
  178. package/external/aport-spec/conformance/cases/payments.refunds.v1/passports/template.json +42 -0
  179. package/external/aport-spec/conformance/package.json +44 -0
  180. package/external/aport-spec/conformance/pnpm-lock.yaml +642 -0
  181. package/external/aport-spec/conformance/src/cases.ts +371 -0
  182. package/external/aport-spec/conformance/src/ed25519.ts +167 -0
  183. package/external/aport-spec/conformance/src/jcs.ts +85 -0
  184. package/external/aport-spec/conformance/src/runner.ts +533 -0
  185. package/external/aport-spec/conformance/src/validators.ts +185 -0
  186. package/external/aport-spec/conformance/test-runner.js +315 -0
  187. package/external/aport-spec/conformance/tsconfig.json +21 -0
  188. package/external/aport-spec/error-schema.json +192 -0
  189. package/external/aport-spec/index.json +12 -0
  190. package/external/aport-spec/integrations/clawmoat/README.md +12 -0
  191. package/external/aport-spec/integrations/shield/README.md +245 -0
  192. package/external/aport-spec/integrations/shield/adapters/index.js +116 -0
  193. package/external/aport-spec/integrations/shield/adapters/system-command-execute.js +133 -0
  194. package/external/aport-spec/integrations/shield/test/README.md +58 -0
  195. package/external/aport-spec/integrations/shield/test/shield.md +40 -0
  196. package/external/aport-spec/integrations/shield/test/test-shield-to-verify.js +274 -0
  197. package/external/aport-spec/metrics-schema.json +504 -0
  198. package/external/aport-spec/oap/CHANGELOG.md +54 -0
  199. package/external/aport-spec/oap/VERSION.md +40 -0
  200. package/external/aport-spec/oap/capability-registry.md +229 -0
  201. package/external/aport-spec/oap/conformance.md +257 -0
  202. package/external/aport-spec/oap/decision-schema.json +114 -0
  203. package/external/aport-spec/oap/examples/context.refund.usd.50.json +9 -0
  204. package/external/aport-spec/oap/examples/decision.allow.sample.json +20 -0
  205. package/external/aport-spec/oap/examples/decision.deny.sample.json +23 -0
  206. package/external/aport-spec/oap/examples/passport.instance.v1.json +50 -0
  207. package/external/aport-spec/oap/examples/passport.template.v1.json +71 -0
  208. package/external/aport-spec/oap/oap-spec.md +426 -0
  209. package/external/aport-spec/oap/passport-schema.json +396 -0
  210. package/external/aport-spec/oap/security.md +213 -0
  211. package/external/aport-spec/oap/vc/context-oap-v1.jsonld +137 -0
  212. package/external/aport-spec/oap/vc/examples/oap-decision-vc.json +37 -0
  213. package/external/aport-spec/oap/vc/examples/oap-passport-vc.json +68 -0
  214. package/external/aport-spec/oap/vc/tools/INTEGRATION.md +375 -0
  215. package/external/aport-spec/oap/vc/tools/README.md +278 -0
  216. package/external/aport-spec/oap/vc/tools/examples/decision-to-vc.js +66 -0
  217. package/external/aport-spec/oap/vc/tools/examples/passport-to-vc.js +83 -0
  218. package/external/aport-spec/oap/vc/tools/examples/vc-to-decision.js +77 -0
  219. package/external/aport-spec/oap/vc/tools/examples/vc-to-passport.js +94 -0
  220. package/external/aport-spec/oap/vc/tools/package.json +38 -0
  221. package/external/aport-spec/oap/vc/tools/pnpm-lock.yaml +472 -0
  222. package/external/aport-spec/oap/vc/tools/src/cli.ts +226 -0
  223. package/external/aport-spec/oap/vc/tools/src/crypto-utils.ts +427 -0
  224. package/external/aport-spec/oap/vc/tools/src/index.ts +653 -0
  225. package/external/aport-spec/oap/vc/tools/src/test.ts +148 -0
  226. package/external/aport-spec/oap/vc/tools/src/vp.ts +382 -0
  227. package/external/aport-spec/oap/vc/tools/test-simple.js +214 -0
  228. package/external/aport-spec/oap/vc/tools/tsconfig.json +19 -0
  229. package/external/aport-spec/oap/vc/vc-mapping.md +443 -0
  230. package/external/aport-spec/passport-schema.json +586 -0
  231. package/external/aport-spec/rate-limiting.md +136 -0
  232. package/external/aport-spec/transport-profile.md +325 -0
  233. package/external/aport-spec/webhook-spec.md +314 -0
  234. package/package.json +70 -0
  235. package/skills/aport-agent-guardrail/SKILL.md +314 -0
  236. package/src/evaluator.js +252 -0
  237. package/src/server/index.js +72 -0
@@ -0,0 +1,817 @@
1
+ /**
2
+ * Comprehensive tests for finance.payment.charge.v1 policy
3
+ * Tests all enforcement rules, edge cases, and OAP compliance
4
+ */
5
+
6
+ const {
7
+ test,
8
+ expect,
9
+ describe,
10
+ beforeEach,
11
+ afterEach,
12
+ } = require("@jest/globals");
13
+
14
+ // Mock the policy verification endpoint
15
+ const mockPolicyVerification = (response) => {
16
+ global.fetch = jest.fn().mockResolvedValue({
17
+ ok: true,
18
+ json: () => Promise.resolve(response),
19
+ });
20
+ };
21
+
22
+ const mockPolicyVerificationError = (status = 500) => {
23
+ global.fetch = jest.fn().mockResolvedValue({
24
+ ok: false,
25
+ status,
26
+ text: () => Promise.resolve("Policy verification failed"),
27
+ });
28
+ };
29
+
30
+ describe("Payments Charge v1 Policy - OAP Compliance", () => {
31
+ beforeEach(() => {
32
+ // Reset mocks
33
+ jest.clearAllMocks();
34
+ });
35
+
36
+ describe("OAP Decision Structure", () => {
37
+ test("should return OAP-compliant decision structure", async () => {
38
+ const validContext = {
39
+ amount: 1299,
40
+ currency: "USD",
41
+ merchant_id: "merch_abc",
42
+ region: "US",
43
+ shipping_country: "US",
44
+ items: [{ sku: "SKU-1", qty: 1, category: "electronics" }],
45
+ idempotency_key: "charge-ord-1001",
46
+ };
47
+
48
+ const expectedDecision = {
49
+ decision_id: "550e8400-e29b-41d4-a716-446655440002",
50
+ policy_id: "finance.payment.charge.v1",
51
+ agent_id: "550e8400-e29b-41d4-a716-446655440001",
52
+ owner_id: "org_demo_co",
53
+ assurance_level: "L2",
54
+ allow: true,
55
+ reasons: [
56
+ {
57
+ code: "oap.allowed",
58
+ message: "Transaction within limits and policy requirements",
59
+ },
60
+ ],
61
+ created_at: "2025-01-30T10:30:00Z",
62
+ expires_in: 3600,
63
+ passport_digest:
64
+ "sha256:abcd1234efgh5678ijkl9012mnop3456qrst7890uvwx1234yzab5678cdef",
65
+ signature:
66
+ "ed25519:abcd1234efgh5678ijkl9012mnop3456qrst7890uvwx1234yzab5678cdef==",
67
+ kid: "oap:registry:key-2025-01",
68
+ };
69
+
70
+ mockPolicyVerification(expectedDecision);
71
+
72
+ const response = await fetch(
73
+ "/api/verify/policy/finance.payment.charge.v1",
74
+ {
75
+ method: "POST",
76
+ headers: { "Content-Type": "application/json" },
77
+ body: JSON.stringify({
78
+ agent_id: "550e8400-e29b-41d4-a716-446655440001",
79
+ context: validContext,
80
+ }),
81
+ }
82
+ );
83
+
84
+ const decision = await response.json();
85
+
86
+ expect(decision).toMatchObject({
87
+ decision_id: expect.stringMatching(
88
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
89
+ ),
90
+ policy_id: "finance.payment.charge.v1",
91
+ agent_id: expect.stringMatching(
92
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
93
+ ),
94
+ owner_id: expect.any(String),
95
+ assurance_level: expect.stringMatching(/^L[0-4](KYC|FIN)?$/),
96
+ allow: expect.any(Boolean),
97
+ reasons: expect.arrayContaining([
98
+ expect.objectContaining({
99
+ code: expect.any(String),
100
+ message: expect.any(String),
101
+ }),
102
+ ]),
103
+ created_at: expect.stringMatching(
104
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/
105
+ ),
106
+ expires_in: expect.any(Number),
107
+ passport_digest: expect.stringMatching(/^sha256:[a-f0-9]{64}$/),
108
+ signature: expect.stringMatching(/^ed25519:[A-Za-z0-9+/=]+$/),
109
+ kid: expect.stringMatching(/^oap:(registry|owner):[a-zA-Z0-9._-]+$/),
110
+ });
111
+ });
112
+ });
113
+
114
+ describe("Required Context Validation", () => {
115
+ test("should allow charge with all required fields", async () => {
116
+ const validContext = {
117
+ amount: 1299,
118
+ currency: "USD",
119
+ merchant_id: "merch_abc",
120
+ region: "US",
121
+ items: [{ sku: "SKU-1", qty: 1, category: "electronics" }],
122
+ idempotency_key: "charge-ord-1001",
123
+ };
124
+
125
+ mockPolicyVerification({
126
+ allow: true,
127
+ decision_id: "dec_123",
128
+ reasons: [
129
+ { code: "oap.allowed", message: "Transaction within limits" },
130
+ ],
131
+ });
132
+
133
+ const response = await fetch(
134
+ "/api/verify/policy/finance.payment.charge.v1",
135
+ {
136
+ method: "POST",
137
+ headers: { "Content-Type": "application/json" },
138
+ body: JSON.stringify({
139
+ agent_id: "550e8400-e29b-41d4-a716-446655440001",
140
+ context: validContext,
141
+ }),
142
+ }
143
+ );
144
+
145
+ expect(response.ok).toBe(true);
146
+ const result = await response.json();
147
+ expect(result.allow).toBe(true);
148
+ });
149
+
150
+ test("should deny charge with missing required fields", async () => {
151
+ const invalidContext = {
152
+ amount: 1299,
153
+ currency: "USD",
154
+ // Missing merchant_id, region, items, idempotency_key
155
+ };
156
+
157
+ mockPolicyVerificationError(400);
158
+
159
+ const response = await fetch(
160
+ "/api/verify/policy/finance.payment.charge.v1",
161
+ {
162
+ method: "POST",
163
+ headers: { "Content-Type": "application/json" },
164
+ body: JSON.stringify({
165
+ agent_id: "550e8400-e29b-41d4-a716-446655440001",
166
+ context: invalidContext,
167
+ }),
168
+ }
169
+ );
170
+
171
+ expect(response.ok).toBe(false);
172
+ });
173
+ });
174
+
175
+ describe("Currency Validation", () => {
176
+ test("should allow supported currencies", async () => {
177
+ const validContext = {
178
+ amount: 1000,
179
+ currency: "EUR",
180
+ merchant_id: "merch_abc",
181
+ region: "EU",
182
+ items: [{ sku: "SKU-1", qty: 1 }],
183
+ idempotency_key: "charge-ord-1002",
184
+ };
185
+
186
+ mockPolicyVerification({
187
+ allow: true,
188
+ decision_id: "dec_124",
189
+ reasons: [
190
+ { code: "oap.allowed", message: "Transaction within limits" },
191
+ ],
192
+ });
193
+
194
+ const response = await fetch(
195
+ "/api/verify/policy/finance.payment.charge.v1",
196
+ {
197
+ method: "POST",
198
+ headers: { "Content-Type": "application/json" },
199
+ body: JSON.stringify({
200
+ agent_id: "550e8400-e29b-41d4-a716-446655440001",
201
+ context: validContext,
202
+ }),
203
+ }
204
+ );
205
+
206
+ expect(response.ok).toBe(true);
207
+ });
208
+
209
+ test("should deny unsupported currencies", async () => {
210
+ const invalidContext = {
211
+ amount: 1000,
212
+ currency: "GBP", // Not supported
213
+ merchant_id: "merch_abc",
214
+ region: "US",
215
+ items: [{ sku: "SKU-1", qty: 1 }],
216
+ idempotency_key: "charge-ord-1003",
217
+ };
218
+
219
+ mockPolicyVerification({
220
+ allow: false,
221
+ decision_id: "dec_125",
222
+ reasons: [
223
+ {
224
+ code: "oap.currency_unsupported",
225
+ message: "Currency GBP is not supported",
226
+ },
227
+ ],
228
+ });
229
+
230
+ const response = await fetch(
231
+ "/api/verify/policy/finance.payment.charge.v1",
232
+ {
233
+ method: "POST",
234
+ headers: { "Content-Type": "application/json" },
235
+ body: JSON.stringify({
236
+ agent_id: "550e8400-e29b-41d4-a716-446655440001",
237
+ context: invalidContext,
238
+ }),
239
+ }
240
+ );
241
+
242
+ expect(response.ok).toBe(true);
243
+ const result = await response.json();
244
+ expect(result.allow).toBe(false);
245
+ expect(result.reasons[0].code).toBe("oap.currency_unsupported");
246
+ });
247
+ });
248
+
249
+ describe("Amount Limits", () => {
250
+ test("should allow amounts within per-transaction limit", async () => {
251
+ const validContext = {
252
+ amount: 15000, // Within 20000 limit
253
+ currency: "USD",
254
+ merchant_id: "merch_abc",
255
+ region: "US",
256
+ items: [{ sku: "SKU-1", qty: 1 }],
257
+ idempotency_key: "charge-ord-1004",
258
+ };
259
+
260
+ mockPolicyVerification({
261
+ allow: true,
262
+ decision_id: "dec_126",
263
+ reasons: [
264
+ { code: "oap.allowed", message: "Transaction within limits" },
265
+ ],
266
+ });
267
+
268
+ const response = await fetch(
269
+ "/api/verify/policy/finance.payment.charge.v1",
270
+ {
271
+ method: "POST",
272
+ headers: { "Content-Type": "application/json" },
273
+ body: JSON.stringify({
274
+ agent_id: "550e8400-e29b-41d4-a716-446655440001",
275
+ context: validContext,
276
+ }),
277
+ }
278
+ );
279
+
280
+ expect(response.ok).toBe(true);
281
+ });
282
+
283
+ test("should deny amounts exceeding per-transaction limit", async () => {
284
+ const invalidContext = {
285
+ amount: 25000, // Exceeds 20000 limit
286
+ currency: "USD",
287
+ merchant_id: "merch_abc",
288
+ region: "US",
289
+ items: [{ sku: "SKU-1", qty: 1 }],
290
+ idempotency_key: "charge-ord-1005",
291
+ };
292
+
293
+ mockPolicyVerification({
294
+ allow: false,
295
+ decision_id: "dec_127",
296
+ reasons: [
297
+ {
298
+ code: "oap.limit_exceeded",
299
+ message: "Amount exceeds per-transaction limit",
300
+ },
301
+ ],
302
+ });
303
+
304
+ const response = await fetch(
305
+ "/api/verify/policy/finance.payment.charge.v1",
306
+ {
307
+ method: "POST",
308
+ headers: { "Content-Type": "application/json" },
309
+ body: JSON.stringify({
310
+ agent_id: "550e8400-e29b-41d4-a716-446655440001",
311
+ context: invalidContext,
312
+ }),
313
+ }
314
+ );
315
+
316
+ expect(response.ok).toBe(true);
317
+ const result = await response.json();
318
+ expect(result.allow).toBe(false);
319
+ expect(result.reasons[0].code).toBe("oap.limit_exceeded");
320
+ });
321
+ });
322
+
323
+ describe("Item Count Limits", () => {
324
+ test("should allow items within count limit", async () => {
325
+ const validContext = {
326
+ amount: 5000,
327
+ currency: "USD",
328
+ merchant_id: "merch_abc",
329
+ region: "US",
330
+ items: [
331
+ { sku: "SKU-1", qty: 1 },
332
+ { sku: "SKU-2", qty: 1 },
333
+ { sku: "SKU-3", qty: 1 },
334
+ ], // 3 items, within 5 limit
335
+ idempotency_key: "charge-ord-1006",
336
+ };
337
+
338
+ mockPolicyVerification({
339
+ allow: true,
340
+ decision_id: "dec_128",
341
+ reasons: [
342
+ { code: "oap.allowed", message: "Transaction within limits" },
343
+ ],
344
+ });
345
+
346
+ const response = await fetch(
347
+ "/api/verify/policy/finance.payment.charge.v1",
348
+ {
349
+ method: "POST",
350
+ headers: { "Content-Type": "application/json" },
351
+ body: JSON.stringify({
352
+ agent_id: "550e8400-e29b-41d4-a716-446655440001",
353
+ context: validContext,
354
+ }),
355
+ }
356
+ );
357
+
358
+ expect(response.ok).toBe(true);
359
+ });
360
+
361
+ test("should deny items exceeding count limit", async () => {
362
+ const invalidContext = {
363
+ amount: 5000,
364
+ currency: "USD",
365
+ merchant_id: "merch_abc",
366
+ region: "US",
367
+ items: [
368
+ { sku: "A", qty: 1 },
369
+ { sku: "B", qty: 1 },
370
+ { sku: "C", qty: 1 },
371
+ { sku: "D", qty: 1 },
372
+ { sku: "E", qty: 1 },
373
+ { sku: "F", qty: 1 },
374
+ ], // 6 items, exceeds 5 limit
375
+ idempotency_key: "charge-ord-1007",
376
+ };
377
+
378
+ mockPolicyVerification({
379
+ allow: false,
380
+ decision_id: "dec_129",
381
+ reasons: [
382
+ {
383
+ code: "oap.limit_exceeded",
384
+ message: "Item count exceeds maximum allowed",
385
+ },
386
+ ],
387
+ });
388
+
389
+ const response = await fetch(
390
+ "/api/verify/policy/finance.payment.charge.v1",
391
+ {
392
+ method: "POST",
393
+ headers: { "Content-Type": "application/json" },
394
+ body: JSON.stringify({
395
+ agent_id: "550e8400-e29b-41d4-a716-446655440001",
396
+ context: invalidContext,
397
+ }),
398
+ }
399
+ );
400
+
401
+ expect(response.ok).toBe(true);
402
+ const result = await response.json();
403
+ expect(result.allow).toBe(false);
404
+ expect(result.reasons[0].code).toBe("oap.limit_exceeded");
405
+ });
406
+ });
407
+
408
+ describe("Merchant Validation", () => {
409
+ test("should allow charges from allowed merchants", async () => {
410
+ const validContext = {
411
+ amount: 5000,
412
+ currency: "USD",
413
+ merchant_id: "merch_abc", // In allowlist
414
+ region: "US",
415
+ items: [{ sku: "SKU-1", qty: 1 }],
416
+ idempotency_key: "charge-ord-1008",
417
+ };
418
+
419
+ mockPolicyVerification({
420
+ allow: true,
421
+ decision_id: "dec_130",
422
+ reasons: [
423
+ { code: "oap.allowed", message: "Transaction within limits" },
424
+ ],
425
+ });
426
+
427
+ const response = await fetch(
428
+ "/api/verify/policy/finance.payment.charge.v1",
429
+ {
430
+ method: "POST",
431
+ headers: { "Content-Type": "application/json" },
432
+ body: JSON.stringify({
433
+ agent_id: "550e8400-e29b-41d4-a716-446655440001",
434
+ context: validContext,
435
+ }),
436
+ }
437
+ );
438
+
439
+ expect(response.ok).toBe(true);
440
+ });
441
+
442
+ test("should deny charges from forbidden merchants", async () => {
443
+ const invalidContext = {
444
+ amount: 5000,
445
+ currency: "USD",
446
+ merchant_id: "merch_bad", // Not in allowlist
447
+ region: "US",
448
+ items: [{ sku: "SKU-1", qty: 1 }],
449
+ idempotency_key: "charge-ord-1009",
450
+ };
451
+
452
+ mockPolicyVerification({
453
+ allow: false,
454
+ decision_id: "dec_131",
455
+ reasons: [
456
+ {
457
+ code: "oap.merchant_forbidden",
458
+ message: "Merchant not in allowlist",
459
+ },
460
+ ],
461
+ });
462
+
463
+ const response = await fetch(
464
+ "/api/verify/policy/finance.payment.charge.v1",
465
+ {
466
+ method: "POST",
467
+ headers: { "Content-Type": "application/json" },
468
+ body: JSON.stringify({
469
+ agent_id: "550e8400-e29b-41d4-a716-446655440001",
470
+ context: invalidContext,
471
+ }),
472
+ }
473
+ );
474
+
475
+ expect(response.ok).toBe(true);
476
+ const result = await response.json();
477
+ expect(result.allow).toBe(false);
478
+ expect(result.reasons[0].code).toBe("oap.merchant_forbidden");
479
+ });
480
+ });
481
+
482
+ describe("Country Validation", () => {
483
+ test("should allow charges to allowed countries", async () => {
484
+ const validContext = {
485
+ amount: 5000,
486
+ currency: "USD",
487
+ merchant_id: "merch_abc",
488
+ region: "US",
489
+ shipping_country: "US", // In allowlist
490
+ items: [{ sku: "SKU-1", qty: 1 }],
491
+ idempotency_key: "charge-ord-1010",
492
+ };
493
+
494
+ mockPolicyVerification({
495
+ allow: true,
496
+ decision_id: "dec_132",
497
+ reasons: [
498
+ { code: "oap.allowed", message: "Transaction within limits" },
499
+ ],
500
+ });
501
+
502
+ const response = await fetch(
503
+ "/api/verify/policy/finance.payment.charge.v1",
504
+ {
505
+ method: "POST",
506
+ headers: { "Content-Type": "application/json" },
507
+ body: JSON.stringify({
508
+ agent_id: "550e8400-e29b-41d4-a716-446655440001",
509
+ context: validContext,
510
+ }),
511
+ }
512
+ );
513
+
514
+ expect(response.ok).toBe(true);
515
+ });
516
+
517
+ test("should deny charges to blocked countries", async () => {
518
+ const invalidContext = {
519
+ amount: 5000,
520
+ currency: "USD",
521
+ merchant_id: "merch_abc",
522
+ region: "US",
523
+ shipping_country: "BR", // Not in allowlist
524
+ items: [{ sku: "SKU-1", qty: 1 }],
525
+ idempotency_key: "charge-ord-1011",
526
+ };
527
+
528
+ mockPolicyVerification({
529
+ allow: false,
530
+ decision_id: "dec_133",
531
+ reasons: [
532
+ {
533
+ code: "oap.region_blocked",
534
+ message: "Shipping country not allowed",
535
+ },
536
+ ],
537
+ });
538
+
539
+ const response = await fetch(
540
+ "/api/verify/policy/finance.payment.charge.v1",
541
+ {
542
+ method: "POST",
543
+ headers: { "Content-Type": "application/json" },
544
+ body: JSON.stringify({
545
+ agent_id: "550e8400-e29b-41d4-a716-446655440001",
546
+ context: invalidContext,
547
+ }),
548
+ }
549
+ );
550
+
551
+ expect(response.ok).toBe(true);
552
+ const result = await response.json();
553
+ expect(result.allow).toBe(false);
554
+ expect(result.reasons[0].code).toBe("oap.region_blocked");
555
+ });
556
+ });
557
+
558
+ describe("Category Blocking", () => {
559
+ test("should allow charges for allowed categories", async () => {
560
+ const validContext = {
561
+ amount: 5000,
562
+ currency: "USD",
563
+ merchant_id: "merch_abc",
564
+ region: "US",
565
+ items: [
566
+ { sku: "SKU-1", qty: 1, category: "electronics" }, // Not blocked
567
+ ],
568
+ idempotency_key: "charge-ord-1012",
569
+ };
570
+
571
+ mockPolicyVerification({
572
+ allow: true,
573
+ decision_id: "dec_134",
574
+ reasons: [
575
+ { code: "oap.allowed", message: "Transaction within limits" },
576
+ ],
577
+ });
578
+
579
+ const response = await fetch(
580
+ "/api/verify/policy/finance.payment.charge.v1",
581
+ {
582
+ method: "POST",
583
+ headers: { "Content-Type": "application/json" },
584
+ body: JSON.stringify({
585
+ agent_id: "550e8400-e29b-41d4-a716-446655440001",
586
+ context: validContext,
587
+ }),
588
+ }
589
+ );
590
+
591
+ expect(response.ok).toBe(true);
592
+ });
593
+
594
+ test("should deny charges for blocked categories", async () => {
595
+ const invalidContext = {
596
+ amount: 5000,
597
+ currency: "USD",
598
+ merchant_id: "merch_abc",
599
+ region: "US",
600
+ items: [
601
+ { sku: "SKU-1", qty: 1, category: "weapons" }, // Blocked category
602
+ ],
603
+ idempotency_key: "charge-ord-1013",
604
+ };
605
+
606
+ mockPolicyVerification({
607
+ allow: false,
608
+ decision_id: "dec_135",
609
+ reasons: [
610
+ { code: "oap.category_blocked", message: "Item category is blocked" },
611
+ ],
612
+ });
613
+
614
+ const response = await fetch(
615
+ "/api/verify/policy/finance.payment.charge.v1",
616
+ {
617
+ method: "POST",
618
+ headers: { "Content-Type": "application/json" },
619
+ body: JSON.stringify({
620
+ agent_id: "550e8400-e29b-41d4-a716-446655440001",
621
+ context: invalidContext,
622
+ }),
623
+ }
624
+ );
625
+
626
+ expect(response.ok).toBe(true);
627
+ const result = await response.json();
628
+ expect(result.allow).toBe(false);
629
+ expect(result.reasons[0].code).toBe("oap.category_blocked");
630
+ });
631
+ });
632
+
633
+ describe("Idempotency Validation", () => {
634
+ test("should allow charges with unique idempotency keys", async () => {
635
+ const validContext = {
636
+ amount: 5000,
637
+ currency: "USD",
638
+ merchant_id: "merch_abc",
639
+ region: "US",
640
+ items: [{ sku: "SKU-1", qty: 1 }],
641
+ idempotency_key: "charge-ord-unique-123",
642
+ };
643
+
644
+ mockPolicyVerification({
645
+ allow: true,
646
+ decision_id: "dec_136",
647
+ reasons: [
648
+ { code: "oap.allowed", message: "Transaction within limits" },
649
+ ],
650
+ });
651
+
652
+ const response = await fetch(
653
+ "/api/verify/policy/finance.payment.charge.v1",
654
+ {
655
+ method: "POST",
656
+ headers: { "Content-Type": "application/json" },
657
+ body: JSON.stringify({
658
+ agent_id: "550e8400-e29b-41d4-a716-446655440001",
659
+ context: validContext,
660
+ }),
661
+ }
662
+ );
663
+
664
+ expect(response.ok).toBe(true);
665
+ });
666
+
667
+ test("should deny charges with duplicate idempotency keys", async () => {
668
+ const invalidContext = {
669
+ amount: 5000,
670
+ currency: "USD",
671
+ merchant_id: "merch_abc",
672
+ region: "US",
673
+ items: [{ sku: "SKU-1", qty: 1 }],
674
+ idempotency_key: "charge-ord-1001", // Already used
675
+ };
676
+
677
+ mockPolicyVerification({
678
+ allow: false,
679
+ decision_id: "dec_137",
680
+ reasons: [
681
+ {
682
+ code: "oap.idempotency_conflict",
683
+ message: "Idempotency key already used",
684
+ },
685
+ ],
686
+ });
687
+
688
+ const response = await fetch(
689
+ "/api/verify/policy/finance.payment.charge.v1",
690
+ {
691
+ method: "POST",
692
+ headers: { "Content-Type": "application/json" },
693
+ body: JSON.stringify({
694
+ agent_id: "550e8400-e29b-41d4-a716-446655440001",
695
+ context: invalidContext,
696
+ }),
697
+ }
698
+ );
699
+
700
+ expect(response.ok).toBe(true);
701
+ const result = await response.json();
702
+ expect(result.allow).toBe(false);
703
+ expect(result.reasons[0].code).toBe("oap.idempotency_conflict");
704
+ });
705
+ });
706
+
707
+ describe("Assurance Level Validation", () => {
708
+ test("should allow charges with sufficient assurance level", async () => {
709
+ const validContext = {
710
+ amount: 5000,
711
+ currency: "USD",
712
+ merchant_id: "merch_abc",
713
+ region: "US",
714
+ items: [{ sku: "SKU-1", qty: 1 }],
715
+ idempotency_key: "charge-ord-1014",
716
+ };
717
+
718
+ mockPolicyVerification({
719
+ allow: true,
720
+ decision_id: "dec_138",
721
+ reasons: [
722
+ { code: "oap.allowed", message: "Transaction within limits" },
723
+ ],
724
+ });
725
+
726
+ const response = await fetch(
727
+ "/api/verify/policy/finance.payment.charge.v1",
728
+ {
729
+ method: "POST",
730
+ headers: { "Content-Type": "application/json" },
731
+ body: JSON.stringify({
732
+ agent_id: "550e8400-e29b-41d4-a716-446655440001",
733
+ context: validContext,
734
+ }),
735
+ }
736
+ );
737
+
738
+ expect(response.ok).toBe(true);
739
+ });
740
+
741
+ test("should deny charges with insufficient assurance level", async () => {
742
+ const invalidContext = {
743
+ amount: 5000,
744
+ currency: "USD",
745
+ merchant_id: "merch_abc",
746
+ region: "US",
747
+ items: [{ sku: "SKU-1", qty: 1 }],
748
+ idempotency_key: "charge-ord-1015",
749
+ };
750
+
751
+ mockPolicyVerification({
752
+ allow: false,
753
+ decision_id: "dec_139",
754
+ reasons: [
755
+ {
756
+ code: "oap.assurance_insufficient",
757
+ message: "Assurance level too low",
758
+ },
759
+ ],
760
+ });
761
+
762
+ const response = await fetch(
763
+ "/api/verify/policy/finance.payment.charge.v1",
764
+ {
765
+ method: "POST",
766
+ headers: { "Content-Type": "application/json" },
767
+ body: JSON.stringify({
768
+ agent_id: "550e8400-e29b-41d4-a716-446655440001",
769
+ context: invalidContext,
770
+ }),
771
+ }
772
+ );
773
+
774
+ expect(response.ok).toBe(true);
775
+ const result = await response.json();
776
+ expect(result.allow).toBe(false);
777
+ expect(result.reasons[0].code).toBe("oap.assurance_insufficient");
778
+ });
779
+ });
780
+
781
+ describe("Error Handling", () => {
782
+ test("should handle policy verification errors gracefully", async () => {
783
+ mockPolicyVerificationError(500);
784
+
785
+ const response = await fetch(
786
+ "/api/verify/policy/finance.payment.charge.v1",
787
+ {
788
+ method: "POST",
789
+ headers: { "Content-Type": "application/json" },
790
+ body: JSON.stringify({
791
+ agent_id: "550e8400-e29b-41d4-a716-446655440001",
792
+ context: {},
793
+ }),
794
+ }
795
+ );
796
+
797
+ expect(response.ok).toBe(false);
798
+ expect(response.status).toBe(500);
799
+ });
800
+
801
+ test("should handle malformed requests", async () => {
802
+ mockPolicyVerificationError(400);
803
+
804
+ const response = await fetch(
805
+ "/api/verify/policy/finance.payment.charge.v1",
806
+ {
807
+ method: "POST",
808
+ headers: { "Content-Type": "application/json" },
809
+ body: "invalid json",
810
+ }
811
+ );
812
+
813
+ expect(response.ok).toBe(false);
814
+ expect(response.status).toBe(400);
815
+ });
816
+ });
817
+ });