@pyric/cli 0.1.0-alpha.11 → 0.1.0-alpha.12

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 (301) hide show
  1. package/dist/conformance/.generated/can-i-use-browser.d.ts.map +1 -1
  2. package/dist/conformance/.generated/can-i-use-browser.js +1 -1
  3. package/dist/conformance/.generated/can-i-use-browser.js.map +1 -1
  4. package/dist/conformance/.generated/can-i-use.d.ts.map +1 -1
  5. package/dist/conformance/.generated/can-i-use.js +2 -2
  6. package/dist/conformance/.generated/can-i-use.js.map +1 -1
  7. package/dist/serve/docs-ui/docs/_rhythm/index.html +1 -1
  8. package/dist/serve/docs-ui/docs/agent/set-up-your-agent/index.html +1 -1
  9. package/dist/serve/docs-ui/docs/agent/watch-and-review/index.html +1 -1
  10. package/dist/serve/docs-ui/docs/agent/work-with-an-agent/index.html +1 -1
  11. package/dist/serve/docs-ui/docs/ai-compat/index.html +1 -1
  12. package/dist/serve/docs-ui/docs/api-reference/index.html +2 -2
  13. package/dist/serve/docs-ui/docs/api-reference.md +1 -1
  14. package/dist/serve/docs-ui/docs/app-compat/index.html +1 -1
  15. package/dist/serve/docs-ui/docs/auth-compat/index.html +1 -1
  16. package/dist/serve/docs-ui/docs/build/ai-logic/index.html +1 -1
  17. package/dist/serve/docs-ui/docs/build/authentication/index.html +1 -1
  18. package/dist/serve/docs-ui/docs/build/cloud-firestore/index.html +1 -1
  19. package/dist/serve/docs-ui/docs/build/cloud-messaging/index.html +1 -1
  20. package/dist/serve/docs-ui/docs/build/cloud-storage/index.html +1 -1
  21. package/dist/serve/docs-ui/docs/build/realtime-database/index.html +1 -1
  22. package/dist/serve/docs-ui/docs/conformance-scores/index.html +1 -1
  23. package/dist/serve/docs-ui/docs/create-pyric-reference-api/index.html +1 -1
  24. package/dist/serve/docs-ui/docs/database-compat/index.html +1 -1
  25. package/dist/serve/docs-ui/docs/firestore-compat/index.html +1 -1
  26. package/dist/serve/docs-ui/docs/functions-rtdb-compat/index.html +1 -1
  27. package/dist/serve/docs-ui/docs/get-started/how-the-swap-works/index.html +1 -1
  28. package/dist/serve/docs-ui/docs/get-started/start-building/index.html +1 -1
  29. package/dist/serve/docs-ui/docs/index.json +200 -25
  30. package/dist/serve/docs-ui/docs/messaging-compat/index.html +1 -1
  31. package/dist/serve/docs-ui/docs/observe/see-whats-happening/index.html +1 -1
  32. package/dist/serve/docs-ui/docs/observe/shape-your-data/index.html +1 -1
  33. package/dist/serve/docs-ui/docs/overview/index.html +1 -1
  34. package/dist/serve/docs-ui/docs/pyric-admin-app-reference-api/index.html +1 -1
  35. package/dist/serve/docs-ui/docs/pyric-admin-auth-reference-api/index.html +1 -1
  36. package/dist/serve/docs-ui/docs/pyric-admin-database-reference-api/index.html +1 -1
  37. package/dist/serve/docs-ui/docs/pyric-admin-firestore-reference-api/index.html +1 -1
  38. package/dist/serve/docs-ui/docs/pyric-admin-messaging-reference-api/index.html +916 -0
  39. package/dist/serve/docs-ui/docs/pyric-admin-messaging-reference-api.md +706 -0
  40. package/dist/serve/docs-ui/docs/pyric-admin-storage-reference-api/index.html +1 -1
  41. package/dist/serve/docs-ui/docs/pyric-ai-reference-api/index.html +1 -1
  42. package/dist/serve/docs-ui/docs/pyric-ai-scripting-reference-api/index.html +1 -1
  43. package/dist/serve/docs-ui/docs/pyric-app-reference-api/index.html +1 -1
  44. package/dist/serve/docs-ui/docs/pyric-auth-reference-api/index.html +1 -1
  45. package/dist/serve/docs-ui/docs/pyric-cli-assurance-browser-reference-api/index.html +1 -1
  46. package/dist/serve/docs-ui/docs/pyric-cli-assurance-reference-api/index.html +1 -1
  47. package/dist/serve/docs-ui/docs/pyric-cli-bridge-client-reference-api/index.html +1 -1
  48. package/dist/serve/docs-ui/docs/pyric-cli-bridge-reference-api/index.html +1 -1
  49. package/dist/serve/docs-ui/docs/pyric-cli-conformance-browser-reference-api/index.html +1 -1
  50. package/dist/serve/docs-ui/docs/pyric-cli-conformance-docs-reference-api/index.html +1 -1
  51. package/dist/serve/docs-ui/docs/pyric-cli-conformance-reference-api/index.html +1 -1
  52. package/dist/serve/docs-ui/docs/pyric-cli-credentials-node-reference-api/index.html +1 -1
  53. package/dist/serve/docs-ui/docs/pyric-cli-discover-reference-api/index.html +1 -1
  54. package/dist/serve/docs-ui/docs/pyric-cli-register-reference-api/index.html +1 -1
  55. package/dist/serve/docs-ui/docs/pyric-cli-remote-reference-api/index.html +1 -1
  56. package/dist/serve/docs-ui/docs/pyric-cli-serve-worker-reference-api/index.html +131 -66
  57. package/dist/serve/docs-ui/docs/pyric-cli-serve-worker-reference-api.md +130 -65
  58. package/dist/serve/docs-ui/docs/pyric-cli-verify-reference-api/index.html +1 -1
  59. package/dist/serve/docs-ui/docs/pyric-cli-vite-reference-api/index.html +1 -1
  60. package/dist/serve/docs-ui/docs/pyric-database-reference-api/index.html +1 -1
  61. package/dist/serve/docs-ui/docs/pyric-firestore-reference-api/index.html +1 -1
  62. package/dist/serve/docs-ui/docs/pyric-messaging-reference-api/index.html +1 -1
  63. package/dist/serve/docs-ui/docs/pyric-messaging-sw-reference-api/index.html +1 -1
  64. package/dist/serve/docs-ui/docs/pyric-rules-reference-api/index.html +1 -1
  65. package/dist/serve/docs-ui/docs/pyric-sandbox-database-reference-api/index.html +1 -1
  66. package/dist/serve/docs-ui/docs/pyric-sandbox-firestore-reference-api/index.html +1 -1
  67. package/dist/serve/docs-ui/docs/pyric-sandbox-reference-api/index.html +37 -14
  68. package/dist/serve/docs-ui/docs/pyric-sandbox-reference-api.md +28 -8
  69. package/dist/serve/docs-ui/docs/pyric-storage-reference-api/index.html +1 -1
  70. package/dist/serve/docs-ui/docs/pyric-ui-agents-reference-api/index.html +1 -1
  71. package/dist/serve/docs-ui/docs/pyric-ui-auth-hooks-reference-api/index.html +1 -1
  72. package/dist/serve/docs-ui/docs/pyric-ui-auth-reference-api/index.html +1 -1
  73. package/dist/serve/docs-ui/docs/pyric-ui-events-hooks-reference-api/index.html +1 -1
  74. package/dist/serve/docs-ui/docs/pyric-ui-events-reference-api/index.html +1 -1
  75. package/dist/serve/docs-ui/docs/pyric-ui-firestore-hooks-reference-api/index.html +1 -1
  76. package/dist/serve/docs-ui/docs/pyric-ui-firestore-reference-api/index.html +1 -1
  77. package/dist/serve/docs-ui/docs/pyric-ui-primitives-reference-api/index.html +1 -1
  78. package/dist/serve/docs-ui/docs/pyric-ui-rtdb-reference-api/index.html +1 -1
  79. package/dist/serve/docs-ui/docs/pyric-ui-rules-hooks-reference-api/index.html +1 -1
  80. package/dist/serve/docs-ui/docs/pyric-ui-rules-reference-api/index.html +1 -1
  81. package/dist/serve/docs-ui/docs/pyric-ui-storage-hooks-reference-api/index.html +1 -1
  82. package/dist/serve/docs-ui/docs/pyric-ui-storage-reference-api/index.html +1 -1
  83. package/dist/serve/docs-ui/docs/pyric-ui-traffic-hooks-reference-api/index.html +1 -1
  84. package/dist/serve/docs-ui/docs/pyric-ui-traffic-reference-api/index.html +1 -1
  85. package/dist/serve/docs-ui/docs/rules-compat/index.html +1 -1
  86. package/dist/serve/docs-ui/docs/secure/audit-your-rules/index.html +1 -1
  87. package/dist/serve/docs-ui/docs/secure/firestore-rules-limits/index.html +1 -1
  88. package/dist/serve/docs-ui/docs/secure/read-a-denial/index.html +1 -1
  89. package/dist/serve/docs-ui/docs/secure/rtdb-rules-in-typescript/index.html +1 -1
  90. package/dist/serve/docs-ui/docs/secure/rules-standard-library/index.html +1 -1
  91. package/dist/serve/docs-ui/docs/secure/secure-it-with-rules/index.html +1 -1
  92. package/dist/serve/docs-ui/docs/secure/simulate-and-lint/index.html +1 -1
  93. package/dist/serve/docs-ui/docs/secure/write-a-rules-test-suite/index.html +1 -1
  94. package/dist/serve/docs-ui/docs/ship/ship-to-production/index.html +1 -1
  95. package/dist/serve/docs-ui/docs/ship/test-in-node/index.html +1 -1
  96. package/dist/serve/docs-ui/docs/storage-compat/index.html +1 -1
  97. package/dist/serve/docs-ui/docs/trust/how-we-know-it-matches-firebase/index.html +1 -1
  98. package/dist/serve/docs-ui/docs/trust/versioning-and-compatibility/index.html +1 -1
  99. package/dist/serve/docs-ui/llms.txt +1 -1
  100. package/dist/serve/worker/host/core.js +1 -1
  101. package/dist/serve/worker/host/core.js.map +1 -1
  102. package/dist/serve/worker/host/firestore-writes.d.ts.map +1 -1
  103. package/dist/serve/worker/host/firestore-writes.js +3 -2
  104. package/dist/serve/worker/host/firestore-writes.js.map +1 -1
  105. package/dist/serve/worker/index.d.ts +2 -1
  106. package/dist/serve/worker/index.d.ts.map +1 -1
  107. package/dist/serve/worker/index.js +2 -1
  108. package/dist/serve/worker/index.js.map +1 -1
  109. package/dist/serve/worker/protocol.js +2 -2
  110. package/dist/serve/worker/protocol.js.map +1 -1
  111. package/package.json +5 -4
  112. package/src/assurance/.generated/conformance-verdicts.ts +1090 -0
  113. package/src/assurance/attachment.ts +211 -0
  114. package/src/assurance/browser.ts +80 -0
  115. package/src/assurance/campaign.ts +496 -0
  116. package/src/assurance/capabilities.ts +446 -0
  117. package/src/assurance/cases.ts +43 -0
  118. package/src/assurance/index.ts +75 -0
  119. package/src/assurance/runner.ts +885 -0
  120. package/src/assurance/tool-names.ts +15 -0
  121. package/src/assurance/tools.ts +830 -0
  122. package/src/assurance/types.ts +352 -0
  123. package/src/assurance/validation.ts +434 -0
  124. package/src/bridge/client/bridge.ts +550 -0
  125. package/src/bridge/client/dispatch.ts +137 -0
  126. package/src/bridge/client.ts +45 -0
  127. package/src/bridge/protocol.ts +351 -0
  128. package/src/bridge/server/audit.ts +53 -0
  129. package/src/bridge/server/bridge.ts +597 -0
  130. package/src/bridge/server/headless.ts +160 -0
  131. package/src/bridge/server/json-schema-to-zod.ts +108 -0
  132. package/src/bridge/server/local-bridge.ts +66 -0
  133. package/src/bridge/server/logger.ts +50 -0
  134. package/src/bridge/server/mcp-contract.ts +93 -0
  135. package/src/bridge/server/mcp.ts +129 -0
  136. package/src/bridge/server/peer.ts +232 -0
  137. package/src/bridge/server/standalone.ts +347 -0
  138. package/src/bridge/server/tool-metadata.ts +99 -0
  139. package/src/bridge/server.ts +28 -0
  140. package/src/cli/can-i-use.ts +50 -0
  141. package/src/cli/cli.test.ts +493 -0
  142. package/src/cli/database-rules.ts +329 -0
  143. package/src/cli/dev-runner.ts +282 -0
  144. package/src/cli/firebase-json.ts +100 -0
  145. package/src/cli/firestore-indexes.ts +70 -0
  146. package/src/cli/index.ts +425 -0
  147. package/src/cli/init.ts +260 -0
  148. package/src/cli/mcp-proxy.ts +196 -0
  149. package/src/cli/parse-args.ts +80 -0
  150. package/src/cli/rules.ts +259 -0
  151. package/src/cli/scope.ts +67 -0
  152. package/src/cli/serve.ts +996 -0
  153. package/src/cli/service-commands.ts +73 -0
  154. package/src/cli/snapshot.ts +164 -0
  155. package/src/cli/storage-rules.ts +172 -0
  156. package/src/cli/verify.ts +416 -0
  157. package/src/conformance/.generated/can-i-use-browser.ts +113 -0
  158. package/src/conformance/.generated/can-i-use.ts +117 -0
  159. package/src/conformance/.generated/conformance-docs.ts +14 -0
  160. package/src/conformance/browser.ts +24 -0
  161. package/src/conformance/can-i-use-tool.ts +51 -0
  162. package/src/conformance/can-i-use.ts +18 -0
  163. package/src/conformance/docs.ts +8 -0
  164. package/src/conformance/index.ts +20 -0
  165. package/src/conformance/tools.ts +16 -0
  166. package/src/credentials/core/memoize-ttl.ts +142 -0
  167. package/src/credentials/core/types.ts +10 -0
  168. package/src/credentials/node/from-adc.ts +96 -0
  169. package/src/credentials/node/from-service-account.ts +133 -0
  170. package/src/credentials/node/index.ts +6 -0
  171. package/src/discover/concurrency.ts +127 -0
  172. package/src/discover/crawler-adapter.ts +142 -0
  173. package/src/discover/crawler.ts +1127 -0
  174. package/src/discover/credential-free.ts +27 -0
  175. package/src/discover/findCollectionGroup.ts +131 -0
  176. package/src/discover/firestore-source.ts +59 -0
  177. package/src/discover/index.ts +5 -0
  178. package/src/discover/merge.ts +523 -0
  179. package/src/discover/session.ts +402 -0
  180. package/src/discover/tools.ts +201 -0
  181. package/src/discover/types.ts +187 -0
  182. package/src/discover/wire.ts +324 -0
  183. package/src/functions-rtdb/child.ts +382 -0
  184. package/src/functions-rtdb/delivery.ts +7 -0
  185. package/src/functions-rtdb/discovery.ts +136 -0
  186. package/src/functions-rtdb/event.ts +64 -0
  187. package/src/functions-rtdb/execution.ts +111 -0
  188. package/src/functions-rtdb/in-memory-delivery.ts +40 -0
  189. package/src/functions-rtdb/project.ts +74 -0
  190. package/src/functions-rtdb/projection.ts +104 -0
  191. package/src/functions-rtdb/reference-pattern.ts +20 -0
  192. package/src/functions-rtdb/remote-delivery.ts +23 -0
  193. package/src/pkg-version.ts +49 -0
  194. package/src/register/esm-exports.ts +63 -0
  195. package/src/register/hooks.ts +30 -0
  196. package/src/register/index.ts +150 -0
  197. package/src/register/mapping.ts +33 -0
  198. package/src/remote/index.ts +1050 -0
  199. package/src/rtdb/crawl-snapshot.ts +118 -0
  200. package/src/rtdb/inspection.ts +137 -0
  201. package/src/rtdb/load-rules-document.ts +56 -0
  202. package/src/rtdb/rules-generation-tool.ts +38 -0
  203. package/src/rtdb/rules-json.ts +29 -0
  204. package/src/serve/activity-guard.ts +29 -0
  205. package/src/serve/activity-route.ts +164 -0
  206. package/src/serve/activity-warning.ts +26 -0
  207. package/src/serve/bridge-mount.ts +200 -0
  208. package/src/serve/bundler.ts +493 -0
  209. package/src/serve/capture-store.ts +65 -0
  210. package/src/serve/discovery.ts +199 -0
  211. package/src/serve/entries/ai.ts +292 -0
  212. package/src/serve/entries/app-backend.ts +4 -0
  213. package/src/serve/entries/app-client.ts +26 -0
  214. package/src/serve/entries/app-session-store.ts +47 -0
  215. package/src/serve/entries/app.ts +11 -0
  216. package/src/serve/entries/auth-helper-core.ts +181 -0
  217. package/src/serve/entries/auth-helper-dom.ts +134 -0
  218. package/src/serve/entries/auth-helper-runtime.ts +20 -0
  219. package/src/serve/entries/auth.ts +255 -0
  220. package/src/serve/entries/bridge-url.ts +31 -0
  221. package/src/serve/entries/database.ts +129 -0
  222. package/src/serve/entries/firestore.ts +232 -0
  223. package/src/serve/entries/init.ts +46 -0
  224. package/src/serve/entries/keepalive.ts +46 -0
  225. package/src/serve/entries/messaging-sw.ts +75 -0
  226. package/src/serve/entries/messaging.ts +96 -0
  227. package/src/serve/entries/runtime.ts +540 -0
  228. package/src/serve/entries/session-store.ts +97 -0
  229. package/src/serve/entries/storage.ts +92 -0
  230. package/src/serve/entries/tab-sync-wiring.ts +274 -0
  231. package/src/serve/entries/worker-runtime.ts +75 -0
  232. package/src/serve/init-payload.ts +42 -0
  233. package/src/serve/namespace.ts +675 -0
  234. package/src/serve/open-browser.ts +68 -0
  235. package/src/serve/rules.ts +272 -0
  236. package/src/serve/sandbox-marker.ts +31 -0
  237. package/src/serve/server.ts +428 -0
  238. package/src/serve/standalone-assets.ts +180 -0
  239. package/src/serve/state-store.ts +166 -0
  240. package/src/serve/studio/disk-project-store.ts +185 -0
  241. package/src/serve/studio/disk-workspace.ts +162 -0
  242. package/src/serve/studio/index.ts +20 -0
  243. package/src/serve/studio/routes.ts +237 -0
  244. package/src/serve/studio/store-types.ts +48 -0
  245. package/src/serve/studio/studio-storage.test.ts +289 -0
  246. package/src/serve/vite-plugin.ts +1102 -0
  247. package/src/serve/worker/activity-bootstrap.ts +24 -0
  248. package/src/serve/worker/client/admin-firestore.ts +38 -0
  249. package/src/serve/worker/client/ai.ts +124 -0
  250. package/src/serve/worker/client/auth.ts +489 -0
  251. package/src/serve/worker/client/connection.ts +202 -0
  252. package/src/serve/worker/client/core.ts +350 -0
  253. package/src/serve/worker/client/disconnect.ts +45 -0
  254. package/src/serve/worker/client/firestore-reads.ts +181 -0
  255. package/src/serve/worker/client/firestore-refs.ts +226 -0
  256. package/src/serve/worker/client/firestore-writes.ts +232 -0
  257. package/src/serve/worker/client/handles.ts +91 -0
  258. package/src/serve/worker/client/messaging.ts +119 -0
  259. package/src/serve/worker/client/presence.ts +174 -0
  260. package/src/serve/worker/client/rtdb.ts +311 -0
  261. package/src/serve/worker/client/rules.ts +63 -0
  262. package/src/serve/worker/client/service-worker-connection.ts +59 -0
  263. package/src/serve/worker/client/snapshots.ts +86 -0
  264. package/src/serve/worker/client/storage.ts +195 -0
  265. package/src/serve/worker/client/studio.ts +88 -0
  266. package/src/serve/worker/client.ts +57 -0
  267. package/src/serve/worker/durable-persistence.ts +137 -0
  268. package/src/serve/worker/entry.ts +179 -0
  269. package/src/serve/worker/host/admin-firestore.ts +88 -0
  270. package/src/serve/worker/host/connection.ts +179 -0
  271. package/src/serve/worker/host/core.ts +387 -0
  272. package/src/serve/worker/host/dispatch.ts +316 -0
  273. package/src/serve/worker/host/firestore-reads.ts +117 -0
  274. package/src/serve/worker/host/firestore-writes.ts +456 -0
  275. package/src/serve/worker/host/presence.ts +312 -0
  276. package/src/serve/worker/host/rtdb.ts +136 -0
  277. package/src/serve/worker/host/rules.ts +128 -0
  278. package/src/serve/worker/host/storage.ts +312 -0
  279. package/src/serve/worker/host/studio.ts +75 -0
  280. package/src/serve/worker/host/subscriptions.ts +238 -0
  281. package/src/serve/worker/host-ai.ts +165 -0
  282. package/src/serve/worker/host-auth.ts +469 -0
  283. package/src/serve/worker/host-context.ts +273 -0
  284. package/src/serve/worker/host-events.ts +94 -0
  285. package/src/serve/worker/host-messaging.ts +240 -0
  286. package/src/serve/worker/host.ts +47 -0
  287. package/src/serve/worker/index.ts +196 -0
  288. package/src/serve/worker/presence-timing.ts +15 -0
  289. package/src/serve/worker/protocol.ts +1112 -0
  290. package/src/serve/worker/serve-init.ts +606 -0
  291. package/src/serve/worker/service-worker-channel.ts +33 -0
  292. package/src/serve/worker/service-worker-relay.ts +86 -0
  293. package/src/serve/writer-lock.ts +53 -0
  294. package/src/verify/cases.ts +233 -0
  295. package/src/verify/fixture.ts +258 -0
  296. package/src/verify/index.ts +519 -0
  297. package/src/verify/tools.ts +108 -0
  298. package/src/version/compat-target.ts +17 -0
  299. package/src/vite.ts +17 -0
  300. package/dist/serve/docs-ui/docs/pyric-firestore-values-reference-api/index.html +0 -31
  301. package/dist/serve/docs-ui/docs/pyric-firestore-values-reference-api.md +0 -29
@@ -0,0 +1,885 @@
1
+ import { getAuth, sandbox as authSandbox, type SeedUser } from "pyric/auth";
2
+ import {
3
+ endAt as endAtRtdb,
4
+ endBefore as endBeforeRtdb,
5
+ equalTo as equalToRtdb,
6
+ getAdminDatabase,
7
+ getDatabase,
8
+ get as getRtdb,
9
+ limitToFirst as limitToFirstRtdb,
10
+ limitToLast as limitToLastRtdb,
11
+ orderByChild as orderByChildRtdb,
12
+ orderByKey as orderByKeyRtdb,
13
+ orderByValue as orderByValueRtdb,
14
+ query as queryRtdb,
15
+ ref as rtdbRef,
16
+ remove as removeRtdb,
17
+ sandbox as rtdbSandbox,
18
+ set as setRtdb,
19
+ startAfter as startAfterRtdb,
20
+ startAt as startAtRtdb,
21
+ update as updateRtdb,
22
+ type Database,
23
+ type QueryConstraint as RtdbQueryConstraint,
24
+ } from "pyric/database";
25
+ import {
26
+ collection,
27
+ deleteDoc,
28
+ doc,
29
+ getDoc,
30
+ getDocs,
31
+ getFirestore,
32
+ limit,
33
+ orderBy,
34
+ query,
35
+ setDoc,
36
+ updateDoc,
37
+ where,
38
+ type Firestore,
39
+ type Query,
40
+ type QueryConstraint,
41
+ } from "pyric/firestore";
42
+ import { seedDocuments, setRules, snapshotDocuments } from "pyric/sandbox/firestore";
43
+ import {
44
+ deleteObject,
45
+ getBlob,
46
+ getMetadata,
47
+ getStorageSandbox,
48
+ listAll,
49
+ ref as storageRef,
50
+ updateMetadata,
51
+ uploadBytes,
52
+ type FirebaseStorage,
53
+ } from "pyric/storage";
54
+ import { getAdminStorageSandbox } from "pyric/storage/internal";
55
+ import {
56
+ initializeSandbox,
57
+ type AuthState,
58
+ type LocalSandbox,
59
+ type SandboxEvent,
60
+ } from "pyric/sandbox";
61
+ import { qualifyProbe } from "./capabilities.js";
62
+ import {
63
+ assertActor,
64
+ assertInvariant,
65
+ assertProbe,
66
+ assertTarget,
67
+ } from "./validation.js";
68
+ import {
69
+ ASSURANCE_CAMPAIGN_SCHEMA,
70
+ ASSURANCE_REPORT_SCHEMA,
71
+ AssuranceInputError,
72
+ type ActorEvidence,
73
+ type AssuranceActor,
74
+ type AssuranceDecision,
75
+ type AssuranceEventEvidence,
76
+ type AssuranceProbe,
77
+ type AssuranceProbeResult,
78
+ type AuthorizationCampaignReport,
79
+ type AuthorizationCampaignSpec,
80
+ type FirebaseOperation,
81
+ type LocalFirebaseTarget,
82
+ type OperationEvidence,
83
+ type ProbeClassification,
84
+ type SecurityInvariant,
85
+ type StateDiff,
86
+ type StorageOperation,
87
+ } from "./types.js";
88
+
89
+ interface ActorRuntime {
90
+ evidence: ActorEvidence;
91
+ auth: AuthState | null;
92
+ }
93
+
94
+ interface ServiceRuntime {
95
+ sandbox: LocalSandbox;
96
+ actor: ActorRuntime;
97
+ firestore?: Firestore;
98
+ rtdb?: Database;
99
+ rtdbAdmin?: Database;
100
+ storage?: FirebaseStorage;
101
+ storageAdmin?: FirebaseStorage;
102
+ }
103
+
104
+ interface ExecutedOperation {
105
+ actorEvidence: ActorEvidence;
106
+ evidence: OperationEvidence;
107
+ stateDiff: StateDiff;
108
+ }
109
+
110
+ let storageRunSequence = 0;
111
+
112
+ function assertCampaign(spec: AuthorizationCampaignSpec): void {
113
+ if (spec.schema !== ASSURANCE_CAMPAIGN_SCHEMA) {
114
+ throw new AssuranceInputError(
115
+ `campaign schema must be '${ASSURANCE_CAMPAIGN_SCHEMA}'.`,
116
+ );
117
+ }
118
+ if (!spec.id.trim())
119
+ throw new AssuranceInputError("campaign id is required.");
120
+ assertTarget(spec.target);
121
+ if (!Array.isArray(spec.actors)) {
122
+ throw new AssuranceInputError("campaign actors must be an array.");
123
+ }
124
+ if (!Array.isArray(spec.invariants)) {
125
+ throw new AssuranceInputError("campaign invariants must be an array.");
126
+ }
127
+ if (!Array.isArray(spec.probes)) {
128
+ throw new AssuranceInputError("campaign probes must be an array.");
129
+ }
130
+ for (const actor of spec.actors) assertActor(actor);
131
+ for (const invariant of spec.invariants) assertInvariant(invariant);
132
+ for (const probe of spec.probes) assertProbe(probe);
133
+ uniqueIds(spec.actors, "actor");
134
+ uniqueIds(spec.invariants, "invariant");
135
+ uniqueIds(spec.probes, "probe");
136
+
137
+ for (const probe of spec.probes) {
138
+ const actor = spec.actors.find((item) => item.id === probe.actorId);
139
+ const invariant = spec.invariants.find(
140
+ (item) => item.id === probe.invariantId,
141
+ );
142
+ if (!actor)
143
+ throw new AssuranceInputError(
144
+ `probe '${probe.id}' references unknown actor '${probe.actorId}'.`,
145
+ );
146
+ if (!invariant) {
147
+ throw new AssuranceInputError(
148
+ `probe '${probe.id}' references unknown invariant '${probe.invariantId}'.`,
149
+ );
150
+ }
151
+ if (
152
+ invariant.service !== "cross-service" &&
153
+ invariant.service !== probe.control.service
154
+ ) {
155
+ throw new AssuranceInputError(
156
+ `probe '${probe.id}' service does not match invariant '${invariant.id}'.`,
157
+ );
158
+ }
159
+ }
160
+ }
161
+
162
+ function uniqueIds(items: ReadonlyArray<{ id: string }>, label: string): void {
163
+ const seen = new Set<string>();
164
+ for (const item of items) {
165
+ if (!item.id.trim())
166
+ throw new AssuranceInputError(`${label} id is required.`);
167
+ if (seen.has(item.id))
168
+ throw new AssuranceInputError(`duplicate ${label} id '${item.id}'.`);
169
+ seen.add(item.id);
170
+ }
171
+ }
172
+
173
+ function stableStringify(value: unknown): string {
174
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
175
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
176
+ const record = value as Record<string, unknown>;
177
+ return `{${Object.keys(record)
178
+ .sort()
179
+ .map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`)
180
+ .join(",")}}`;
181
+ }
182
+
183
+ function targetHash(target: LocalFirebaseTarget): string {
184
+ const input = stableStringify(target);
185
+ let hash = 0x811c9dc5;
186
+ for (let index = 0; index < input.length; index++) {
187
+ hash ^= input.charCodeAt(index);
188
+ hash = Math.imul(hash, 0x01000193) >>> 0;
189
+ }
190
+ return `fnv1a32-${hash.toString(16).padStart(8, "0")}`;
191
+ }
192
+
193
+ function authUsers(target: LocalFirebaseTarget): SeedUser[] {
194
+ return (target.state.auth?.users ?? []).map((user) => ({
195
+ uid: user.uid,
196
+ email: user.email ?? `${user.uid}@fixture.pyric.invalid`,
197
+ password: user.password ?? "__pyric_fixture_password__",
198
+ ...(user.customClaims ? { customClaims: user.customClaims } : {}),
199
+ }));
200
+ }
201
+
202
+ function seedAuth(
203
+ sandbox: LocalSandbox,
204
+ target: LocalFirebaseTarget,
205
+ ): ReturnType<typeof getAuth> {
206
+ const auth = getAuth(sandbox);
207
+ authSandbox.seedUsers(auth, authUsers(target));
208
+ for (const user of target.state.auth?.users ?? []) {
209
+ if (user.disabled !== undefined || user.emailVerified !== undefined) {
210
+ authSandbox.updateUser(auth, user.uid, {
211
+ ...(user.disabled !== undefined ? { disabled: user.disabled } : {}),
212
+ ...(user.emailVerified !== undefined
213
+ ? { emailVerified: user.emailVerified }
214
+ : {}),
215
+ });
216
+ }
217
+ }
218
+ return auth;
219
+ }
220
+
221
+ function acquireActor(
222
+ sandbox: LocalSandbox,
223
+ target: LocalFirebaseTarget,
224
+ actor: AssuranceActor,
225
+ ): ActorRuntime {
226
+ const auth = seedAuth(sandbox, target);
227
+ try {
228
+ switch (actor.acquisition.kind) {
229
+ case "anonymous-request":
230
+ return {
231
+ auth: null,
232
+ evidence: {
233
+ actorId: actor.id,
234
+ acquisition: actor.acquisition.kind,
235
+ reachability: "reachable",
236
+ },
237
+ };
238
+ case "anonymous-account": {
239
+ const session = authSandbox.mintSession(auth, { kind: "anonymous" });
240
+ return {
241
+ auth: session.state,
242
+ evidence: {
243
+ actorId: actor.id,
244
+ acquisition: actor.acquisition.kind,
245
+ reachability: "reachable",
246
+ uid: session.user.uid,
247
+ },
248
+ };
249
+ }
250
+ case "password": {
251
+ const session = authSandbox.mintSession(auth, {
252
+ kind: "password",
253
+ email: actor.acquisition.email,
254
+ password: actor.acquisition.password,
255
+ });
256
+ return {
257
+ auth: session.state,
258
+ evidence: {
259
+ actorId: actor.id,
260
+ acquisition: actor.acquisition.kind,
261
+ reachability: "reachable",
262
+ uid: session.user.uid,
263
+ },
264
+ };
265
+ }
266
+ case "fixture-user": {
267
+ const session = authSandbox.mintSession(auth, {
268
+ kind: "uid",
269
+ uid: actor.acquisition.uid,
270
+ });
271
+ return {
272
+ auth: session.state,
273
+ evidence: {
274
+ actorId: actor.id,
275
+ acquisition: actor.acquisition.kind,
276
+ reachability: "reachable",
277
+ uid: session.user.uid,
278
+ },
279
+ };
280
+ }
281
+ case "synthetic":
282
+ return {
283
+ auth: {
284
+ uid: actor.acquisition.uid,
285
+ token: actor.acquisition.token ?? {},
286
+ },
287
+ evidence: {
288
+ actorId: actor.id,
289
+ acquisition: actor.acquisition.kind,
290
+ reachability: "synthetic",
291
+ uid: actor.acquisition.uid,
292
+ },
293
+ };
294
+ }
295
+ } catch (error) {
296
+ return {
297
+ auth: null,
298
+ evidence: {
299
+ actorId: actor.id,
300
+ acquisition: actor.acquisition.kind,
301
+ reachability: "unreachable",
302
+ error: error instanceof Error ? error.message : String(error),
303
+ },
304
+ };
305
+ }
306
+ }
307
+
308
+ async function createRuntime(
309
+ campaignId: string,
310
+ probeId: string,
311
+ phase: "control" | "mutation",
312
+ target: LocalFirebaseTarget,
313
+ actorRuntime: ActorRuntime,
314
+ service: FirebaseOperation["service"],
315
+ ): Promise<ServiceRuntime> {
316
+ const sandbox = initializeSandbox();
317
+ seedAuth(sandbox, target);
318
+ const context = sandbox.withAuth(actorRuntime.auth);
319
+ const runtime: ServiceRuntime = { sandbox, actor: actorRuntime };
320
+
321
+ if (service === "firestore") {
322
+ const firestore = getFirestore(context);
323
+ setRules(sandbox, target.rules.firestore!);
324
+ seedDocuments(sandbox, target.state.firestore ?? {});
325
+ runtime.firestore = firestore;
326
+ } else if (service === "rtdb") {
327
+ const rtdb = getDatabase(context);
328
+ const admin = getAdminDatabase(sandbox);
329
+ rtdbSandbox.setRules(rtdb, target.rules.rtdb!);
330
+ rtdbSandbox.setData(admin, { "/": target.state.rtdb ?? {} });
331
+ runtime.rtdb = rtdb;
332
+ runtime.rtdbAdmin = admin;
333
+ } else {
334
+ const dbName = [
335
+ "pyric-assurance",
336
+ campaignId,
337
+ probeId,
338
+ phase,
339
+ Date.now(),
340
+ storageRunSequence++,
341
+ ]
342
+ .join("-")
343
+ .replace(/[^a-zA-Z0-9_-]/g, "_");
344
+ const admin = getAdminStorageSandbox(sandbox, {
345
+ dbName,
346
+ rules: target.rules.storage!,
347
+ });
348
+ for (const object of target.state.storage ?? []) {
349
+ await uploadBytes(
350
+ storageRef(admin, object.path),
351
+ decodeBase64(object.dataBase64),
352
+ {
353
+ ...(object.contentType ? { contentType: object.contentType } : {}),
354
+ ...(object.customMetadata
355
+ ? { customMetadata: object.customMetadata }
356
+ : {}),
357
+ },
358
+ );
359
+ }
360
+ runtime.storageAdmin = admin;
361
+ runtime.storage = getStorageSandbox(context, { dbName });
362
+ }
363
+ return runtime;
364
+ }
365
+
366
+ function decodeBase64(value: string): Uint8Array {
367
+ const binary = atob(value);
368
+ return Uint8Array.from(binary, (char) => char.charCodeAt(0));
369
+ }
370
+
371
+ function eventEvidence(events: SandboxEvent[]): AssuranceEventEvidence[] {
372
+ return events.map((event) => {
373
+ const raw = event as unknown as Record<string, unknown>;
374
+ return {
375
+ ...(typeof raw.id === "string" ? { id: raw.id } : {}),
376
+ ...(typeof raw.at === "number" ? { at: raw.at } : {}),
377
+ ...(typeof raw.kind === "string" ? { kind: raw.kind } : {}),
378
+ ...(typeof raw.service === "string" ? { service: raw.service } : {}),
379
+ ...(typeof raw.method === "string" ? { method: raw.method } : {}),
380
+ ...(typeof raw.op === "string" ? { op: raw.op } : {}),
381
+ ...(typeof raw.path === "string" ? { path: raw.path } : {}),
382
+ ...(typeof raw.result === "string" ? { result: raw.result } : {}),
383
+ ...(raw.auth !== undefined ? { auth: raw.auth } : {}),
384
+ ...(raw.actor !== undefined ? { actor: raw.actor } : {}),
385
+ ...(raw.authLens !== undefined ? { authLens: raw.authLens } : {}),
386
+ ...(Array.isArray(raw.reasons) &&
387
+ raw.reasons.every((item) => typeof item === "string")
388
+ ? { reasons: raw.reasons as string[] }
389
+ : {}),
390
+ ...(raw.origin !== undefined ? { origin: raw.origin } : {}),
391
+ ...(raw.request !== undefined ? { request: raw.request } : {}),
392
+ ...(raw.resourceBefore !== undefined
393
+ ? { resourceBefore: raw.resourceBefore }
394
+ : {}),
395
+ ...(raw.matchedRule !== undefined
396
+ ? { matchedRule: raw.matchedRule }
397
+ : {}),
398
+ ...(raw.evaluatedRule !== undefined
399
+ ? { evaluatedRule: raw.evaluatedRule }
400
+ : {}),
401
+ ...(raw.rules !== undefined ? { rules: raw.rules } : {}),
402
+ };
403
+ });
404
+ }
405
+
406
+ function errorDecision(error: unknown): {
407
+ decision: AssuranceDecision;
408
+ error: { code?: string; message: string };
409
+ } {
410
+ const code = (error as { code?: unknown } | null)?.code;
411
+ const message = error instanceof Error ? error.message : String(error);
412
+ const codeString = typeof code === "string" ? code : undefined;
413
+ const denied =
414
+ /permission[-_ ]?denied|unauthorized/i.test(message) ||
415
+ (codeString
416
+ ? /permission[-_ ]?denied|unauthorized/i.test(codeString)
417
+ : false);
418
+ return {
419
+ decision: denied ? "DENY" : "ERROR",
420
+ error: { ...(codeString ? { code: codeString } : {}), message },
421
+ };
422
+ }
423
+
424
+ async function executeFirestore(
425
+ operation: FirebaseOperation,
426
+ db: Firestore,
427
+ ): Promise<unknown> {
428
+ if (operation.service !== "firestore")
429
+ throw new Error("Expected a Firestore operation.");
430
+ switch (operation.method) {
431
+ case "get": {
432
+ const snapshot = await getDoc(doc(db, operation.path));
433
+ const exists =
434
+ typeof snapshot.exists === "function"
435
+ ? snapshot.exists()
436
+ : snapshot.exists;
437
+ return exists ? snapshot.data() : null;
438
+ }
439
+ case "list": {
440
+ const constraints: QueryConstraint[] = [];
441
+ for (const item of operation.query?.where ?? []) {
442
+ constraints.push(where(item.field, item.op, item.value));
443
+ }
444
+ for (const item of operation.query?.orderBy ?? []) {
445
+ constraints.push(orderBy(item.field, item.direction));
446
+ }
447
+ if (operation.query?.limit !== undefined)
448
+ constraints.push(limit(operation.query.limit));
449
+ const source = constraints.length
450
+ ? query(collection(db, operation.path), ...constraints)
451
+ : collection(db, operation.path);
452
+ const snapshot = await getDocs(source as Query);
453
+ return snapshot.docs.map((item) => ({
454
+ path: `${operation.path.replace(/\/$/, "")}/${item.id}`,
455
+ data: item.data(),
456
+ }));
457
+ }
458
+ case "create":
459
+ case "set":
460
+ await setDoc(doc(db, operation.path), operation.data ?? {});
461
+ return null;
462
+ case "merge":
463
+ await setDoc(doc(db, operation.path), operation.data ?? {}, {
464
+ merge: true,
465
+ });
466
+ return null;
467
+ case "update":
468
+ await updateDoc(doc(db, operation.path), operation.data ?? {});
469
+ return null;
470
+ case "delete":
471
+ await deleteDoc(doc(db, operation.path));
472
+ return null;
473
+ }
474
+ }
475
+
476
+ async function executeRtdb(
477
+ operation: FirebaseOperation,
478
+ db: Database,
479
+ ): Promise<unknown> {
480
+ if (operation.service !== "rtdb")
481
+ throw new Error("Expected an RTDB operation.");
482
+ const target = rtdbRef(db, operation.path);
483
+ switch (operation.method) {
484
+ case "get": {
485
+ const constraints: RtdbQueryConstraint[] = [];
486
+ const querySpec = operation.query;
487
+ if (querySpec?.orderBy?.kind === "child") {
488
+ constraints.push(orderByChildRtdb(querySpec.orderBy.path));
489
+ } else if (querySpec?.orderBy?.kind === "key") {
490
+ constraints.push(orderByKeyRtdb());
491
+ } else if (querySpec?.orderBy?.kind === "value") {
492
+ constraints.push(orderByValueRtdb());
493
+ }
494
+ if (querySpec?.startAt) {
495
+ constraints.push(
496
+ startAtRtdb(querySpec.startAt.value as never, querySpec.startAt.key),
497
+ );
498
+ }
499
+ if (querySpec?.startAfter) {
500
+ constraints.push(
501
+ startAfterRtdb(
502
+ querySpec.startAfter.value as never,
503
+ querySpec.startAfter.key,
504
+ ),
505
+ );
506
+ }
507
+ if (querySpec?.endAt) {
508
+ constraints.push(
509
+ endAtRtdb(querySpec.endAt.value as never, querySpec.endAt.key),
510
+ );
511
+ }
512
+ if (querySpec?.endBefore) {
513
+ constraints.push(
514
+ endBeforeRtdb(
515
+ querySpec.endBefore.value as never,
516
+ querySpec.endBefore.key,
517
+ ),
518
+ );
519
+ }
520
+ if (querySpec?.equalTo) {
521
+ constraints.push(
522
+ equalToRtdb(querySpec.equalTo.value as never, querySpec.equalTo.key),
523
+ );
524
+ }
525
+ if (querySpec?.limitToFirst !== undefined) {
526
+ constraints.push(limitToFirstRtdb(querySpec.limitToFirst));
527
+ }
528
+ if (querySpec?.limitToLast !== undefined) {
529
+ constraints.push(limitToLastRtdb(querySpec.limitToLast));
530
+ }
531
+ const source =
532
+ constraints.length > 0 ? queryRtdb(target, ...constraints) : target;
533
+ return (await getRtdb(source)).val();
534
+ }
535
+ case "set":
536
+ await setRtdb(target, operation.data);
537
+ return null;
538
+ case "update":
539
+ if (
540
+ !operation.data ||
541
+ typeof operation.data !== "object" ||
542
+ Array.isArray(operation.data)
543
+ ) {
544
+ throw new Error("RTDB update data must be an object.");
545
+ }
546
+ await updateRtdb(target, operation.data as Record<string, unknown>);
547
+ return null;
548
+ case "remove":
549
+ await removeRtdb(target);
550
+ return null;
551
+ }
552
+ }
553
+
554
+ async function executeStorage(
555
+ operation: StorageOperation,
556
+ storage: FirebaseStorage,
557
+ ): Promise<unknown> {
558
+ const target = storageRef(storage, operation.path);
559
+ switch (operation.method) {
560
+ case "get": {
561
+ const [blob, metadata] = await Promise.all([
562
+ getBlob(target),
563
+ getMetadata(target),
564
+ ]);
565
+ return {
566
+ text: await blob.text(),
567
+ contentType: metadata.contentType,
568
+ customMetadata: metadata.customMetadata ?? {},
569
+ };
570
+ }
571
+ case "list": {
572
+ const result = await listAll(target);
573
+ return {
574
+ items: result.items.map((item) => item.fullPath),
575
+ prefixes: result.prefixes.map((item) => item.fullPath),
576
+ };
577
+ }
578
+ case "upload": {
579
+ if (!operation.dataBase64)
580
+ throw new Error("Storage upload requires dataBase64.");
581
+ const result = await uploadBytes(
582
+ target,
583
+ decodeBase64(operation.dataBase64),
584
+ {
585
+ ...(operation.contentType
586
+ ? { contentType: operation.contentType }
587
+ : {}),
588
+ ...(operation.customMetadata
589
+ ? { customMetadata: operation.customMetadata }
590
+ : {}),
591
+ },
592
+ );
593
+ return result.metadata;
594
+ }
595
+ case "updateMetadata":
596
+ return updateMetadata(target, {
597
+ ...(operation.contentType
598
+ ? { contentType: operation.contentType }
599
+ : {}),
600
+ ...(operation.customMetadata
601
+ ? { customMetadata: operation.customMetadata }
602
+ : {}),
603
+ });
604
+ case "delete":
605
+ await deleteObject(target);
606
+ return null;
607
+ }
608
+ }
609
+
610
+ async function readState(
611
+ operation: FirebaseOperation,
612
+ runtime: ServiceRuntime,
613
+ ): Promise<unknown> {
614
+ if (operation.service === "firestore") {
615
+ const state = snapshotDocuments(runtime.sandbox);
616
+ if (operation.method === "list") {
617
+ const prefix = `${operation.path.replace(/\/$/, "")}/`;
618
+ return Object.fromEntries(
619
+ Object.entries(state).filter(([path]) => path.startsWith(prefix)),
620
+ );
621
+ }
622
+ return state[operation.path] ?? null;
623
+ }
624
+ if (operation.service === "rtdb") {
625
+ return (await getRtdb(rtdbRef(runtime.rtdbAdmin!, operation.path))).val();
626
+ }
627
+ return readStorageState(operation, runtime.storageAdmin!);
628
+ }
629
+
630
+ async function readStorageState(
631
+ operation: StorageOperation,
632
+ storage: FirebaseStorage,
633
+ ): Promise<unknown> {
634
+ if (operation.method === "list") {
635
+ const result = await listAll(storageRef(storage, operation.path));
636
+ return {
637
+ items: result.items.map((item) => item.fullPath),
638
+ prefixes: result.prefixes.map((item) => item.fullPath),
639
+ };
640
+ }
641
+ const target = storageRef(storage, operation.path);
642
+ try {
643
+ const [blob, metadata] = await Promise.all([
644
+ getBlob(target),
645
+ getMetadata(target),
646
+ ]);
647
+ return {
648
+ text: await blob.text(),
649
+ contentType: metadata.contentType,
650
+ customMetadata: metadata.customMetadata ?? {},
651
+ };
652
+ } catch (error) {
653
+ const code = (error as { code?: string } | null)?.code;
654
+ if (code === "storage/object-not-found") return null;
655
+ throw error;
656
+ }
657
+ }
658
+
659
+ async function executeOperation(
660
+ campaignId: string,
661
+ probeId: string,
662
+ phase: "control" | "mutation",
663
+ target: LocalFirebaseTarget,
664
+ actorRuntime: ActorRuntime,
665
+ operation: FirebaseOperation,
666
+ ): Promise<ExecutedOperation> {
667
+ const runtime = await createRuntime(
668
+ campaignId,
669
+ probeId,
670
+ phase,
671
+ target,
672
+ actorRuntime,
673
+ operation.service,
674
+ );
675
+ const before = await readState(operation, runtime);
676
+ const eventOffset = runtime.sandbox.history().length;
677
+ let decision: AssuranceDecision = "ALLOW";
678
+ let output: unknown;
679
+ let failure: { code?: string; message: string } | undefined;
680
+ try {
681
+ output =
682
+ operation.service === "firestore"
683
+ ? await executeFirestore(operation, runtime.firestore!)
684
+ : operation.service === "rtdb"
685
+ ? await executeRtdb(operation, runtime.rtdb!)
686
+ : await executeStorage(operation, runtime.storage!);
687
+ } catch (error) {
688
+ const result = errorDecision(error);
689
+ decision = result.decision;
690
+ failure = result.error;
691
+ }
692
+ const events = eventEvidence(runtime.sandbox.history().slice(eventOffset));
693
+ const after = await readState(operation, runtime);
694
+ return {
695
+ actorEvidence: runtime.actor.evidence,
696
+ evidence: {
697
+ operation,
698
+ decision,
699
+ ...(output !== undefined ? { output } : {}),
700
+ ...(failure ? { error: failure } : {}),
701
+ events,
702
+ },
703
+ stateDiff: {
704
+ changed: stableStringify(before) !== stableStringify(after),
705
+ before,
706
+ after,
707
+ },
708
+ };
709
+ }
710
+
711
+ function skippedEvidence(
712
+ operation: FirebaseOperation,
713
+ decision: AssuranceDecision,
714
+ ): OperationEvidence {
715
+ return { operation, decision, events: [] };
716
+ }
717
+
718
+ function classify(
719
+ actor: ActorEvidence,
720
+ invariant: SecurityInvariant,
721
+ control: OperationEvidence,
722
+ mutation: OperationEvidence,
723
+ stateDiff: StateDiff,
724
+ ): ProbeClassification {
725
+ if (actor.reachability === "unreachable") return "invalid-probe";
726
+ if (control.decision !== "ALLOW") return "invalid-probe";
727
+ if (mutation.decision === "ERROR" || mutation.decision === "UNSUPPORTED")
728
+ return "invalid-probe";
729
+ if (mutation.decision === invariant.expected) return "no-counterexample";
730
+ if (invariant.expected === "DENY" && mutation.decision === "ALLOW") {
731
+ const hasImpact =
732
+ stateDiff.changed ||
733
+ (mutation.output !== undefined && mutation.output !== null);
734
+ return actor.reachability === "reachable" &&
735
+ invariant.confidence !== "tentative" &&
736
+ hasImpact
737
+ ? "local-counterexample"
738
+ : "candidate-signal";
739
+ }
740
+ return "candidate-signal";
741
+ }
742
+
743
+ async function runProbe(
744
+ spec: AuthorizationCampaignSpec,
745
+ probe: AssuranceProbe,
746
+ hash: string,
747
+ ): Promise<AssuranceProbeResult> {
748
+ const actor = spec.actors.find((item) => item.id === probe.actorId)!;
749
+ const invariant = spec.invariants.find(
750
+ (item) => item.id === probe.invariantId,
751
+ )!;
752
+ const qualification = qualifyProbe(spec.target, probe);
753
+ if (!qualification.supported) {
754
+ return {
755
+ campaignId: spec.id,
756
+ probeId: probe.id,
757
+ targetHash: hash,
758
+ actorEvidence: {
759
+ actorId: actor.id,
760
+ acquisition: actor.acquisition.kind,
761
+ reachability:
762
+ actor.acquisition.kind === "synthetic" ? "synthetic" : "unreachable",
763
+ },
764
+ invariant,
765
+ mutationSpec: probe.mutation,
766
+ control: skippedEvidence(probe.control, "UNSUPPORTED"),
767
+ mutation: skippedEvidence(probe.mutation.operation, "UNSUPPORTED"),
768
+ qualification,
769
+ classification: qualification.classification ?? "engine-gap",
770
+ };
771
+ }
772
+
773
+ const actorRuntime = acquireActor(initializeSandbox(), spec.target, actor);
774
+ if (actorRuntime.evidence.reachability === "unreachable") {
775
+ const error = {
776
+ message: actorRuntime.evidence.error ?? "Actor acquisition failed.",
777
+ };
778
+ return {
779
+ campaignId: spec.id,
780
+ probeId: probe.id,
781
+ targetHash: hash,
782
+ actorEvidence: actorRuntime.evidence,
783
+ invariant,
784
+ mutationSpec: probe.mutation,
785
+ control: { ...skippedEvidence(probe.control, "ERROR"), error },
786
+ mutation: {
787
+ ...skippedEvidence(probe.mutation.operation, "ERROR"),
788
+ error,
789
+ },
790
+ qualification,
791
+ classification: "invalid-probe",
792
+ };
793
+ }
794
+
795
+ const control = await executeOperation(
796
+ spec.id,
797
+ probe.id,
798
+ "control",
799
+ spec.target,
800
+ actorRuntime,
801
+ probe.control,
802
+ );
803
+ const mutation = await executeOperation(
804
+ spec.id,
805
+ probe.id,
806
+ "mutation",
807
+ spec.target,
808
+ actorRuntime,
809
+ probe.mutation.operation,
810
+ );
811
+ return {
812
+ campaignId: spec.id,
813
+ probeId: probe.id,
814
+ targetHash: hash,
815
+ actorEvidence: control.actorEvidence,
816
+ invariant,
817
+ mutationSpec: probe.mutation,
818
+ control: control.evidence,
819
+ mutation: mutation.evidence,
820
+ stateDiff: mutation.stateDiff,
821
+ qualification,
822
+ classification: classify(
823
+ control.actorEvidence,
824
+ invariant,
825
+ control.evidence,
826
+ mutation.evidence,
827
+ mutation.stateDiff,
828
+ ),
829
+ };
830
+ }
831
+
832
+ function summarize(
833
+ results: AssuranceProbeResult[],
834
+ ): AuthorizationCampaignReport["summary"] {
835
+ return {
836
+ probes: results.length,
837
+ controlsPassed: results.filter(
838
+ (result) => result.control.decision === "ALLOW",
839
+ ).length,
840
+ localCounterexamples: results.filter(
841
+ (result) => result.classification === "local-counterexample",
842
+ ).length,
843
+ candidateSignals: results.filter(
844
+ (result) => result.classification === "candidate-signal",
845
+ ).length,
846
+ noCounterexamples: results.filter(
847
+ (result) => result.classification === "no-counterexample",
848
+ ).length,
849
+ engineGaps: results.filter(
850
+ (result) => result.classification === "engine-gap",
851
+ ).length,
852
+ invalidProbes: results.filter(
853
+ (result) => result.classification === "invalid-probe",
854
+ ).length,
855
+ };
856
+ }
857
+
858
+ export async function runAuthorizationCampaign(
859
+ spec: AuthorizationCampaignSpec,
860
+ ): Promise<AuthorizationCampaignReport> {
861
+ assertCampaign(spec);
862
+ if (
863
+ typeof globalThis.indexedDB === "undefined" &&
864
+ spec.probes.some((probe) => probe.control.service === "storage")
865
+ ) {
866
+ const fake = await import("fake-indexeddb");
867
+ Object.assign(globalThis, {
868
+ indexedDB: fake.indexedDB,
869
+ IDBKeyRange: fake.IDBKeyRange,
870
+ });
871
+ }
872
+ const hash = targetHash(spec.target);
873
+ const results: AssuranceProbeResult[] = [];
874
+ for (const probe of spec.probes) {
875
+ results.push(await runProbe(spec, probe, hash));
876
+ }
877
+ return {
878
+ schema: ASSURANCE_REPORT_SCHEMA,
879
+ campaignId: spec.id,
880
+ targetHash: hash,
881
+ localOnly: { network: "forbid", engine: "pyric-local-sandboxes" },
882
+ results,
883
+ summary: summarize(results),
884
+ };
885
+ }