@pyric/cli 0.1.0-alpha.26 → 0.1.0-alpha.27
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/README.md +1 -1
- package/dist/assurance/.generated/conformance-verdicts.d.ts +5 -0
- package/dist/assurance/.generated/conformance-verdicts.d.ts.map +1 -1
- package/dist/assurance/.generated/conformance-verdicts.js +5 -0
- package/dist/assurance/.generated/conformance-verdicts.js.map +1 -1
- package/dist/assurance/capabilities.d.ts.map +1 -1
- package/dist/assurance/capabilities.js +24 -5
- package/dist/assurance/capabilities.js.map +1 -1
- package/dist/assurance/runner.d.ts.map +1 -1
- package/dist/assurance/runner.js +37 -50
- package/dist/assurance/runner.js.map +1 -1
- package/dist/assurance/types.d.ts +5 -0
- package/dist/assurance/types.d.ts.map +1 -1
- package/dist/assurance/types.js.map +1 -1
- package/dist/assurance/validation.d.ts +2 -16
- package/dist/assurance/validation.d.ts.map +1 -1
- package/dist/assurance/validation.js +121 -1
- package/dist/assurance/validation.js.map +1 -1
- package/dist/bridge/surface/arguments/assurance.d.ts +25 -0
- package/dist/bridge/surface/arguments/assurance.d.ts.map +1 -1
- package/dist/bridge/surface/arguments/assurance.js +1 -0
- package/dist/bridge/surface/arguments/assurance.js.map +1 -1
- package/dist/bridge/surface/methods/assurance/define.d.ts +2 -2
- package/dist/bridge/surface/methods/assurance/map.d.ts +23 -0
- package/dist/bridge/surface/methods/assurance/map.d.ts.map +1 -1
- package/dist/bridge/surface/methods/assurance/propose.d.ts +7 -0
- package/dist/bridge/surface/methods/assurance/propose.d.ts.map +1 -1
- package/dist/bridge/surface/render/discriminator-schemas.d.ts +2 -2
- package/dist/cli/database-rules.js +1 -1
- package/dist/cli/database-rules.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +13 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/pyric-config.d.ts +8 -0
- package/dist/cli/pyric-config.d.ts.map +1 -1
- package/dist/cli/pyric-config.js.map +1 -1
- package/dist/cli/serve.d.ts.map +1 -1
- package/dist/cli/serve.js +5 -3
- package/dist/cli/serve.js.map +1 -1
- package/dist/cli/verify.d.ts +6 -0
- package/dist/cli/verify.d.ts.map +1 -1
- package/dist/cli/verify.js +240 -15
- package/dist/cli/verify.js.map +1 -1
- package/dist/conformance/.generated/can-i-use.d.ts.map +1 -1
- package/dist/conformance/.generated/can-i-use.js +1 -1
- package/dist/conformance/.generated/can-i-use.js.map +1 -1
- package/dist/conformance/.generated/conformance-docs.d.ts.map +1 -1
- package/dist/conformance/.generated/conformance-docs.js +1 -1
- package/dist/conformance/.generated/conformance-docs.js.map +1 -1
- package/dist/serve/repro/denials.d.ts.map +1 -1
- package/dist/serve/repro/denials.js +2 -1
- package/dist/serve/repro/denials.js.map +1 -1
- package/dist/serve/site-ui/_astro/{app.BpEphDR7.js → app.CiMHEvEW.js} +15 -15
- package/dist/serve/site-ui/_astro/{app.DFdWhpVY.js → app.DpzsBfos.js} +1 -1
- package/dist/serve/site-ui/_astro/{seed.vvXV1ioI.js → seed.BpXSRzLo.js} +1 -1
- package/dist/serve/site-ui/auth/index.html +1 -1
- package/dist/serve/site-ui/docs/database-compat/index.html +4 -0
- package/dist/serve/site-ui/docs/database-compat.md +4 -0
- package/dist/serve/site-ui/docs/get-started/start-building/index.html +1 -0
- package/dist/serve/site-ui/docs/get-started/start-building.md +2 -0
- package/dist/serve/site-ui/docs/pyric-cli-assurance-reference-api/index.html +23 -4
- package/dist/serve/site-ui/docs/pyric-cli-assurance-reference-api.md +11 -4
- package/dist/serve/site-ui/docs/pyric-cli-verify-reference-api/index.html +15 -2
- package/dist/serve/site-ui/docs/pyric-cli-verify-reference-api.md +11 -10
- package/dist/serve/site-ui/docs/secure/write-a-rules-test-suite/index.html +1 -1
- package/dist/serve/site-ui/docs/secure/write-a-rules-test-suite.md +1 -1
- package/dist/serve/site-ui/docs/ship/ship-to-production/index.html +4 -1
- package/dist/serve/site-ui/docs/ship/ship-to-production.md +7 -1
- package/dist/serve/site-ui/examples/chess/index.html +1 -1
- package/dist/serve/site-ui/firestore/index.html +1 -1
- package/dist/serve/site-ui/rtdb/index.html +1 -1
- package/dist/serve/site-ui/settings/index.html +1 -1
- package/dist/serve/site-ui/storage/index.html +1 -1
- package/dist/serve/site-ui/studio/index.html +1 -1
- package/dist/serve/site-ui/traffic/index.html +1 -1
- package/dist/serve/verify-hint.d.ts +2 -0
- package/dist/serve/verify-hint.d.ts.map +1 -0
- package/dist/serve/verify-hint.js +10 -0
- package/dist/serve/verify-hint.js.map +1 -0
- package/dist/serve/vite-generation-rules-watch.d.ts.map +1 -1
- package/dist/serve/vite-generation-rules-watch.js +5 -3
- package/dist/serve/vite-generation-rules-watch.js.map +1 -1
- package/dist/verify/index.d.ts +16 -2
- package/dist/verify/index.d.ts.map +1 -1
- package/dist/verify/index.js +100 -18
- package/dist/verify/index.js.map +1 -1
- package/dist/verify/staging/engine.d.ts +102 -0
- package/dist/verify/staging/engine.d.ts.map +1 -0
- package/dist/verify/staging/engine.js +371 -0
- package/dist/verify/staging/engine.js.map +1 -0
- package/dist/verify/staging/rest.d.ts +68 -0
- package/dist/verify/staging/rest.d.ts.map +1 -0
- package/dist/verify/staging/rest.js +93 -0
- package/dist/verify/staging/rest.js.map +1 -0
- package/dist/verify/staging/target.d.ts +39 -0
- package/dist/verify/staging/target.d.ts.map +1 -0
- package/dist/verify/staging/target.js +117 -0
- package/dist/verify/staging/target.js.map +1 -0
- package/package.json +4 -4
- package/src/assurance/.generated/conformance-verdicts.ts +5 -0
- package/src/assurance/capabilities.ts +22 -4
- package/src/assurance/runner.ts +40 -58
- package/src/assurance/types.ts +3 -0
- package/src/assurance/validation.ts +137 -0
- package/src/bridge/surface/arguments/assurance.ts +1 -0
- package/src/cli/database-rules.ts +1 -1
- package/src/cli/index.ts +13 -2
- package/src/cli/pyric-config.ts +5 -0
- package/src/cli/serve.ts +6 -3
- package/src/cli/verify.ts +244 -18
- package/src/conformance/.generated/can-i-use.ts +1 -1
- package/src/conformance/.generated/conformance-docs.ts +1 -1
- package/src/serve/repro/denials.ts +2 -1
- package/src/serve/verify-hint.ts +8 -0
- package/src/serve/vite-generation-rules-watch.ts +6 -3
- package/src/verify/index.ts +119 -19
- package/src/verify/staging/engine.ts +455 -0
- package/src/verify/staging/rest.ts +129 -0
- package/src/verify/staging/target.ts +137 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
import{S}from"./app.
|
|
1
|
+
import{S}from"./app.CiMHEvEW.js";import"./sandbox-controls.Bp5PoonS.js";import"./index.DBy5LfQW.js";import"./writes.DHSW22XD.js";import"./index.DNyk7q8W.js";export{S as StudioApp};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{s as f,b7 as h,b8 as w}from"./sandbox-controls.Bp5PoonS.js";import{s as o,d as s}from"./writes.DHSW22XD.js";import{s as n,a as A,b as y,u as m,r as d}from"./app.
|
|
1
|
+
import{s as f,b7 as h,b8 as w}from"./sandbox-controls.Bp5PoonS.js";import{s as o,d as s}from"./writes.DHSW22XD.js";import{s as n,a as A,b as y,u as m,r as d}from"./app.CiMHEvEW.js";import"./index.DBy5LfQW.js";import"./index.DNyk7q8W.js";class g{static serverTimestamp(){return{__type:"serverTimestamp"}}static increment(a){return{__type:"increment",value:a}}static arrayUnion(...a){return{__type:"arrayUnion",values:a}}static arrayRemove(...a){return{__type:"arrayRemove",values:a}}static delete(){return{__type:"deleteField"}}}function p(){return g.serverTimestamp()}async function b(t){n.createUser(t,{uid:"alice",email:"alice@gmail.com",displayName:"Alice Nguyen",emailVerified:!0,photoUrl:"https://lh3.googleusercontent.com/a/alice",providerUserInfo:[{providerId:"google.com"}],customClaims:{plan:"pro"}}),n.createUser(t,{uid:"bob",email:"bob@example.com",password:"pyric-demo",displayName:"Bob Carter",emailVerified:!0,customClaims:{role:"editor"}}),n.createUser(t,{uid:"carol",email:"carol@example.com",displayName:"Carol Diaz",emailVerified:!1}),await A(t),await y(t)}const v=new w(37.7749,-122.4194);async function R(t){const a=Date.now(),i=e=>h.fromMillis(a-e*6e4),l=[{id:"alice",name:"Alice Nguyen",email:"alice@gmail.com",plan:"pro"},{id:"bob",name:"Bob Carter",email:"bob@example.com",plan:"free"},{id:"carol",name:"Carol Diaz",email:"carol@example.com",plan:"free"}];for(const e of l)await o(s(t,"users",e.id),{name:e.name,email:e.email,plan:e.plan,createdAt:p()});const r=["alice","bob","carol"],c=["Ship the redesign","Reply to carol","Buy milk","Draft the launch post","Review the rules","Plan offsite","Renew the domain","Read the spec","Fix the flaky test","Call the bank","Water the plants","Book the flights"];for(let e=0;e<c.length;e++){const u=r[e%r.length];await o(s(t,"notes",`note-${String(e+1).padStart(2,"0")}`),{title:c[e],done:e%3===0,pinned:e===0,priority:e%3+1,ownerId:u,ownerRef:`users/${u}`,tags:e%2===0?["work","urgent"]:["personal"],meta:{source:e%2===0?"app":"import",revision:e+1,flags:{starred:e%4===0,archived:!1}},location:e%3===0?v:null,createdAt:i(120-e*7),updatedAt:p()})}await o(s(t,"posts","post-hello"),{title:"Hello world",body:"First post from the seeded sandbox.",authorRef:"users/alice",published:!0,publishedAt:i(8)}),await o(s(t,"posts","post-draft"),{title:"Draft: roadmap",body:"Unpublished draft.",authorRef:"users/bob",published:!1})}async function C(t){const a=new TextEncoder,l=Uint8Array.from(atob("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="),r=>r.charCodeAt(0));await m(d(t,"avatars/alice.png"),l,{contentType:"image/png"}),await m(d(t,"notes/note-01/attachment.txt"),a.encode(`Attachment for the redesign note.
|
|
2
2
|
`),{contentType:"text/plain"})}const U=`rules_version = '2';
|
|
3
3
|
service cloud.firestore {
|
|
4
4
|
match /databases/{database}/documents {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
</script><link rel="preconnect" href="https://use.typekit.net" crossorigin><link rel="stylesheet" href="https://use.typekit.net/jwb3ycu.css"><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet"><link rel="stylesheet" href="/__pyric/ui/_astro/_studio_.CzyEuvVS.css">
|
|
7
7
|
<style>html{scrollbar-gutter:stable}
|
|
8
8
|
</style>
|
|
9
|
-
<link rel="stylesheet" href="/__pyric/ui/_astro/app.8TtSeQQX.css"></head> <body> <div id="root" data-astro-cid-w5sm6wlz> <style>astro-island,astro-slot,astro-static-slot{display:contents}</style><script>(()=>{var e=async t=>{await(await t())()};(self.Astro||(self.Astro={})).only=e;window.dispatchEvent(new Event("astro:only"));})();</script><script>(()=>{var A=Object.defineProperty;var g=(i,o,a)=>o in i?A(i,o,{enumerable:!0,configurable:!0,writable:!0,value:a}):i[o]=a;var d=(i,o,a)=>g(i,typeof o!="symbol"?o+"":o,a);{let i={0:t=>m(t),1:t=>a(t),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(a(t)),5:t=>new Set(a(t)),6:t=>BigInt(t),7:t=>new URL(t),8:t=>new Uint8Array(t),9:t=>new Uint16Array(t),10:t=>new Uint32Array(t),11:t=>1/0*t},o=t=>{let[l,e]=t;return l in i?i[l](e):void 0},a=t=>t.map(o),m=t=>typeof t!="object"||t===null?t:Object.fromEntries(Object.entries(t).map(([l,e])=>[l,o(e)]));class y extends HTMLElement{constructor(){super(...arguments);d(this,"Component");d(this,"hydrator");d(this,"hydrate",async()=>{var b;if(!this.hydrator||!this.isConnected)return;let e=(b=this.parentElement)==null?void 0:b.closest("astro-island[ssr]");if(e){e.addEventListener("astro:hydrate",this.hydrate,{once:!0});return}let c=this.querySelectorAll("astro-slot"),n={},h=this.querySelectorAll("template[data-astro-template]");for(let r of h){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("data-astro-template")||"default"]=r.innerHTML,r.remove())}for(let r of c){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("name")||"default"]=r.innerHTML)}let p;try{p=this.hasAttribute("props")?m(JSON.parse(this.getAttribute("props"))):{}}catch(r){let s=this.getAttribute("component-url")||"<unknown>",v=this.getAttribute("component-export");throw v&&(s+=` (export ${v})`),console.error(`[hydrate] Error parsing props for component ${s}`,this.getAttribute("props"),r),r}let u;await this.hydrator(this)(this.Component,p,n,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),this.dispatchEvent(new CustomEvent("astro:hydrate"))});d(this,"unmount",()=>{this.isConnected||this.dispatchEvent(new CustomEvent("astro:unmount"))})}disconnectedCallback(){document.removeEventListener("astro:after-swap",this.unmount),document.addEventListener("astro:after-swap",this.unmount,{once:!0})}connectedCallback(){if(!this.hasAttribute("await-children")||document.readyState==="interactive"||document.readyState==="complete")this.childrenConnectedCallback();else{let e=()=>{document.removeEventListener("DOMContentLoaded",e),c.disconnect(),this.childrenConnectedCallback()},c=new MutationObserver(()=>{var n;((n=this.lastChild)==null?void 0:n.nodeType)===Node.COMMENT_NODE&&this.lastChild.nodeValue==="astro:end"&&(this.lastChild.remove(),e())});c.observe(this,{childList:!0}),document.addEventListener("DOMContentLoaded",e)}}async childrenConnectedCallback(){let e=this.getAttribute("before-hydration-url");e&&await import(e),this.start()}async start(){let e=JSON.parse(this.getAttribute("opts")),c=this.getAttribute("client");if(Astro[c]===void 0){window.addEventListener(`astro:${c}`,()=>this.start(),{once:!0});return}try{await Astro[c](async()=>{let n=this.getAttribute("renderer-url"),[h,{default:p}]=await Promise.all([import(this.getAttribute("component-url")),n?import(n):()=>()=>{}]),u=this.getAttribute("component-export")||"default";if(!u.includes("."))this.Component=h[u];else{this.Component=h;for(let f of u.split("."))this.Component=this.Component[f]}return this.hydrator=p,this.hydrate},e,this)}catch(n){console.error(`[astro-island] Error hydrating ${this.getAttribute("component-url")}`,n)}}attributeChangedCallback(){this.hydrate()}}d(y,"observedAttributes",["props"]),customElements.get("astro-island")||customElements.define("astro-island",y)}})();</script><astro-island uid="b1btC" component-url="/__pyric/ui/_astro/app.
|
|
9
|
+
<link rel="stylesheet" href="/__pyric/ui/_astro/app.8TtSeQQX.css"></head> <body> <div id="root" data-astro-cid-w5sm6wlz> <style>astro-island,astro-slot,astro-static-slot{display:contents}</style><script>(()=>{var e=async t=>{await(await t())()};(self.Astro||(self.Astro={})).only=e;window.dispatchEvent(new Event("astro:only"));})();</script><script>(()=>{var A=Object.defineProperty;var g=(i,o,a)=>o in i?A(i,o,{enumerable:!0,configurable:!0,writable:!0,value:a}):i[o]=a;var d=(i,o,a)=>g(i,typeof o!="symbol"?o+"":o,a);{let i={0:t=>m(t),1:t=>a(t),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(a(t)),5:t=>new Set(a(t)),6:t=>BigInt(t),7:t=>new URL(t),8:t=>new Uint8Array(t),9:t=>new Uint16Array(t),10:t=>new Uint32Array(t),11:t=>1/0*t},o=t=>{let[l,e]=t;return l in i?i[l](e):void 0},a=t=>t.map(o),m=t=>typeof t!="object"||t===null?t:Object.fromEntries(Object.entries(t).map(([l,e])=>[l,o(e)]));class y extends HTMLElement{constructor(){super(...arguments);d(this,"Component");d(this,"hydrator");d(this,"hydrate",async()=>{var b;if(!this.hydrator||!this.isConnected)return;let e=(b=this.parentElement)==null?void 0:b.closest("astro-island[ssr]");if(e){e.addEventListener("astro:hydrate",this.hydrate,{once:!0});return}let c=this.querySelectorAll("astro-slot"),n={},h=this.querySelectorAll("template[data-astro-template]");for(let r of h){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("data-astro-template")||"default"]=r.innerHTML,r.remove())}for(let r of c){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("name")||"default"]=r.innerHTML)}let p;try{p=this.hasAttribute("props")?m(JSON.parse(this.getAttribute("props"))):{}}catch(r){let s=this.getAttribute("component-url")||"<unknown>",v=this.getAttribute("component-export");throw v&&(s+=` (export ${v})`),console.error(`[hydrate] Error parsing props for component ${s}`,this.getAttribute("props"),r),r}let u;await this.hydrator(this)(this.Component,p,n,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),this.dispatchEvent(new CustomEvent("astro:hydrate"))});d(this,"unmount",()=>{this.isConnected||this.dispatchEvent(new CustomEvent("astro:unmount"))})}disconnectedCallback(){document.removeEventListener("astro:after-swap",this.unmount),document.addEventListener("astro:after-swap",this.unmount,{once:!0})}connectedCallback(){if(!this.hasAttribute("await-children")||document.readyState==="interactive"||document.readyState==="complete")this.childrenConnectedCallback();else{let e=()=>{document.removeEventListener("DOMContentLoaded",e),c.disconnect(),this.childrenConnectedCallback()},c=new MutationObserver(()=>{var n;((n=this.lastChild)==null?void 0:n.nodeType)===Node.COMMENT_NODE&&this.lastChild.nodeValue==="astro:end"&&(this.lastChild.remove(),e())});c.observe(this,{childList:!0}),document.addEventListener("DOMContentLoaded",e)}}async childrenConnectedCallback(){let e=this.getAttribute("before-hydration-url");e&&await import(e),this.start()}async start(){let e=JSON.parse(this.getAttribute("opts")),c=this.getAttribute("client");if(Astro[c]===void 0){window.addEventListener(`astro:${c}`,()=>this.start(),{once:!0});return}try{await Astro[c](async()=>{let n=this.getAttribute("renderer-url"),[h,{default:p}]=await Promise.all([import(this.getAttribute("component-url")),n?import(n):()=>()=>{}]),u=this.getAttribute("component-export")||"default";if(!u.includes("."))this.Component=h[u];else{this.Component=h;for(let f of u.split("."))this.Component=this.Component[f]}return this.hydrator=p,this.hydrate},e,this)}catch(n){console.error(`[astro-island] Error hydrating ${this.getAttribute("component-url")}`,n)}}attributeChangedCallback(){this.hydrate()}}d(y,"observedAttributes",["props"]),customElements.get("astro-island")||customElements.define("astro-island",y)}})();</script><astro-island uid="b1btC" component-url="/__pyric/ui/_astro/app.DpzsBfos.js" component-export="StudioApp" renderer-url="/__pyric/ui/_astro/client.D2ORs4B-.js" props="{"data-astro-cid-w5sm6wlz":[0,true]}" ssr client="only" opts="{"name":"StudioApp","value":"react"}" await-children><p class="studio-loading" aria-live="polite" data-astro-cid-w5sm6wlz>
|
|
10
10
|
Starting Pyric Studio…
|
|
11
11
|
</p><template data-astro-template="fallback"><p class="studio-loading" aria-live="polite" data-astro-cid-w5sm6wlz>
|
|
12
12
|
Starting Pyric Studio…
|
|
@@ -503,6 +503,10 @@ html{scrollbar-gutter:stable}
|
|
|
503
503
|
<summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">pyric verify --rules rtdb:<instance>=<path></code><span class="compat-sub"><span class="compat-behavior">The verify capture records every instance a session held: the default instance's rules, data and URL, and each named instance's active rules and data under its name, on the SharedWorker, the Node host and the in-page runtime. RTDB events name the instance they hit. <code>pyric verify</code> replays each instance the session used on its own: only that instance's events, into the tree the session left it with, under that instance's candidate rules, so a write to one instance is never checked against another's rules or tree. Candidate rules come from <code>--rules rtdb=<path></code> for the default instance and <code>--rules rtdb:<instance>=<path></code> for a named one (a name or a database URL), else from <code>firebase.json</code> as <code>firebase deploy</code> resolves it: an array entry's <code>instance</code>, a <code>target</code> that <code>.firebaserc</code> maps for the project, and <code>database.rules.json</code> when there is no <code>database</code> key. The project's default instance name selects the default instance, and so does <code><projectId></code> or its <code>https://<projectId>.firebaseio.com</code> URL. A divergence on a named instance names it. An instance the session used with no candidate rules is an error that names it, with the reason a deploy target did not resolve; rules for an instance the session never used are refused with the instances it did use.</span></span></span></summary>
|
|
504
504
|
<div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/cli/test/verify/rtdb-instances.test.ts#L2" target="_blank" rel="noopener noreferrer"><code>packages/cli/test/verify/rtdb-instances.test.ts</code></a> captures a session on the default instance and <code>shard-00</code> and asserts each instance's rules, tree and the default URL in the capture, a <code>now-denied</code> on the shard alone under tightened shard rules, no drift under unchanged rules, drift in a tampered shard tree tagged with the shard, and <code>skippedInstances</code> for an instance without candidate rules. <a href="https://github.com/davideast/pyric/blob/main/packages/cli/test/cli/verify-rtdb-instances.test.ts#L6" target="_blank" rel="noopener noreferrer"><code>packages/cli/test/cli/verify-rtdb-instances.test.ts</code></a> runs <code>pyric verify</code> over a <code>firebase.json</code> instance array, a deploy target resolved through <code>.firebaserc</code> and <code>--project</code>, an unresolved target, <code>rtdb:<instance>=</code> by name, URL and default name, an override for an unused instance, and <code>database.rules.json</code> with no <code>database</code> key. <a href="https://github.com/davideast/pyric/blob/main/packages/cli/test/cli/verify-rtdb-legacy-default.test.ts#L11" target="_blank" rel="noopener noreferrer"><code>packages/cli/test/cli/verify-rtdb-legacy-default.test.ts</code></a> maps <code>rtdb:<projectId>=</code> and its URL to the default instance and reads the default's candidate rules from a <code>firebase.json</code> entry for <code><projectId></code>. <a href="https://github.com/davideast/pyric/blob/main/packages/cli/test/serve/worker/serve-init.test.ts#L20" target="_blank" rel="noopener noreferrer"><code>packages/cli/test/serve/worker/serve-init.test.ts</code></a> asserts the host capture holds each named instance, and <code>bundler.test.ts</code> that the in-page runtime bundles the capture. App scenario <code>test/apps/rtdb-named-instance-tools</code> runs the installed <code>pyric verify</code> on the <code>.pyric/last-session.json</code> the Node host wrote while Chromium wrote to both instances: clean under <code>firebase.json</code>, <code>now-denied</code> on <code>shard-00</code> only under stricter shard rules, and an error naming <code>shard-00</code> when <code>firebase.json</code> deploys it no rules. CDD assertion <code>rtdb-modular#MI6</code> pins the sandbox side: a replay selects one instance's events by name or URL and checks them under that instance's rules and tree. (Structured evidence: <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/database/cdd/lifecycle-and-identity.test.ts#L28" target="_blank" rel="noopener noreferrer"><code>lifecycle-and-identity.test.ts</code></a>)</div></div>
|
|
505
505
|
</details>
|
|
506
|
+
<details class="compat-row" data-status="ok">
|
|
507
|
+
<summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">pyric assurance / pyric.assurance.target.v1</code><span class="compat-sub"><span class="compat-behavior">Assurance campaigns (local-sandbox targets) declare named RTDB instances in <code>rules.rtdbInstances</code> and <code>state.rtdbInstances</code>, alongside the default instance. Operations name an <code>instance</code> (a name, URL, or <code>default</code>). The runner sets each instance's rules and data, executing operations on their target instance so mutations and access checks are isolated. An operation naming an instance the target does not declare is an invalid-input refusal listing the declared instances.</span></span></span></summary>
|
|
508
|
+
<div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/cli/test/assurance/rtdb-instances.test.ts#L2" target="_blank" rel="noopener noreferrer"><code>packages/cli/test/assurance/rtdb-instances.test.ts</code></a> runs a campaign where a named-instance probe under its own relaxed rules succeeds while a default-instance probe under locked rules fails, and asserts that naming an undeclared instance throws an invalid-input refusal naming the missing instance. The <code>pyric.assurance.campaign.v1</code> schema and the MCP worker protocol in <code>packages/cli/src/bridge/surface/arguments/assurance.ts</code> carry the instance and <code>rtdbInstances</code> fields. CDD assertion <code>rtdb-modular#MI7</code> pins the sandbox isolation: rules and data set on a named instance are strictly isolated from the default instance, and operations are decided by their specific rules. (Structured evidence: <a href="https://github.com/davideast/pyric/blob/main/test/apps/rtdb-named-instance-assurance/scenario.ts#L5" target="_blank" rel="noopener noreferrer"><code>scenario.ts</code></a>, <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/database/cdd/lifecycle-and-identity.test.ts#L28" target="_blank" rel="noopener noreferrer"><code>lifecycle-and-identity.test.ts</code></a>)</div></div>
|
|
509
|
+
</details>
|
|
506
510
|
</div>
|
|
507
511
|
<h3 id="refdb-path--child--parent--root"><code>ref(db, path)</code> / <code>child</code> / <code>parent</code> / <code>root</code></h3>
|
|
508
512
|
<div class="compat-list">
|
|
@@ -512,6 +512,10 @@ In production each database URL is its own instance, with its own data and its o
|
|
|
512
512
|
<summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">pyric verify --rules rtdb:<instance>=<path></code><span class="compat-sub"><span class="compat-behavior">The verify capture records every instance a session held: the default instance's rules, data and URL, and each named instance's active rules and data under its name, on the SharedWorker, the Node host and the in-page runtime. RTDB events name the instance they hit. <code>pyric verify</code> replays each instance the session used on its own: only that instance's events, into the tree the session left it with, under that instance's candidate rules, so a write to one instance is never checked against another's rules or tree. Candidate rules come from <code>--rules rtdb=<path></code> for the default instance and <code>--rules rtdb:<instance>=<path></code> for a named one (a name or a database URL), else from <code>firebase.json</code> as <code>firebase deploy</code> resolves it: an array entry's <code>instance</code>, a <code>target</code> that <code>.firebaserc</code> maps for the project, and <code>database.rules.json</code> when there is no <code>database</code> key. The project's default instance name selects the default instance, and so does <code><projectId></code> or its <code>https://<projectId>.firebaseio.com</code> URL. A divergence on a named instance names it. An instance the session used with no candidate rules is an error that names it, with the reason a deploy target did not resolve; rules for an instance the session never used are refused with the instances it did use.</span></span></span></summary>
|
|
513
513
|
<div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/cli/test/verify/rtdb-instances.test.ts#L2" target="_blank" rel="noopener noreferrer"><code>packages/cli/test/verify/rtdb-instances.test.ts</code></a> captures a session on the default instance and <code>shard-00</code> and asserts each instance's rules, tree and the default URL in the capture, a <code>now-denied</code> on the shard alone under tightened shard rules, no drift under unchanged rules, drift in a tampered shard tree tagged with the shard, and <code>skippedInstances</code> for an instance without candidate rules. <a href="https://github.com/davideast/pyric/blob/main/packages/cli/test/cli/verify-rtdb-instances.test.ts#L6" target="_blank" rel="noopener noreferrer"><code>packages/cli/test/cli/verify-rtdb-instances.test.ts</code></a> runs <code>pyric verify</code> over a <code>firebase.json</code> instance array, a deploy target resolved through <code>.firebaserc</code> and <code>--project</code>, an unresolved target, <code>rtdb:<instance>=</code> by name, URL and default name, an override for an unused instance, and <code>database.rules.json</code> with no <code>database</code> key. <a href="https://github.com/davideast/pyric/blob/main/packages/cli/test/cli/verify-rtdb-legacy-default.test.ts#L11" target="_blank" rel="noopener noreferrer"><code>packages/cli/test/cli/verify-rtdb-legacy-default.test.ts</code></a> maps <code>rtdb:<projectId>=</code> and its URL to the default instance and reads the default's candidate rules from a <code>firebase.json</code> entry for <code><projectId></code>. <a href="https://github.com/davideast/pyric/blob/main/packages/cli/test/serve/worker/serve-init.test.ts#L20" target="_blank" rel="noopener noreferrer"><code>packages/cli/test/serve/worker/serve-init.test.ts</code></a> asserts the host capture holds each named instance, and <code>bundler.test.ts</code> that the in-page runtime bundles the capture. App scenario <code>test/apps/rtdb-named-instance-tools</code> runs the installed <code>pyric verify</code> on the <code>.pyric/last-session.json</code> the Node host wrote while Chromium wrote to both instances: clean under <code>firebase.json</code>, <code>now-denied</code> on <code>shard-00</code> only under stricter shard rules, and an error naming <code>shard-00</code> when <code>firebase.json</code> deploys it no rules. CDD assertion <code>rtdb-modular#MI6</code> pins the sandbox side: a replay selects one instance's events by name or URL and checks them under that instance's rules and tree. (Structured evidence: <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/database/cdd/lifecycle-and-identity.test.ts#L28" target="_blank" rel="noopener noreferrer"><code>lifecycle-and-identity.test.ts</code></a>)</div></div>
|
|
514
514
|
</details>
|
|
515
|
+
<details class="compat-row" data-status="ok">
|
|
516
|
+
<summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">pyric assurance / pyric.assurance.target.v1</code><span class="compat-sub"><span class="compat-behavior">Assurance campaigns (local-sandbox targets) declare named RTDB instances in <code>rules.rtdbInstances</code> and <code>state.rtdbInstances</code>, alongside the default instance. Operations name an <code>instance</code> (a name, URL, or <code>default</code>). The runner sets each instance's rules and data, executing operations on their target instance so mutations and access checks are isolated. An operation naming an instance the target does not declare is an invalid-input refusal listing the declared instances.</span></span></span></summary>
|
|
517
|
+
<div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/cli/test/assurance/rtdb-instances.test.ts#L2" target="_blank" rel="noopener noreferrer"><code>packages/cli/test/assurance/rtdb-instances.test.ts</code></a> runs a campaign where a named-instance probe under its own relaxed rules succeeds while a default-instance probe under locked rules fails, and asserts that naming an undeclared instance throws an invalid-input refusal naming the missing instance. The <code>pyric.assurance.campaign.v1</code> schema and the MCP worker protocol in <code>packages/cli/src/bridge/surface/arguments/assurance.ts</code> carry the instance and <code>rtdbInstances</code> fields. CDD assertion <code>rtdb-modular#MI7</code> pins the sandbox isolation: rules and data set on a named instance are strictly isolated from the default instance, and operations are decided by their specific rules. (Structured evidence: <a href="https://github.com/davideast/pyric/blob/main/test/apps/rtdb-named-instance-assurance/scenario.ts#L5" target="_blank" rel="noopener noreferrer"><code>scenario.ts</code></a>, <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/database/cdd/lifecycle-and-identity.test.ts#L28" target="_blank" rel="noopener noreferrer"><code>lifecycle-and-identity.test.ts</code></a>)</div></div>
|
|
518
|
+
</details>
|
|
515
519
|
</div>
|
|
516
520
|
|
|
517
521
|
### `ref(db, path)` / `child` / `parent` / `root`
|
|
@@ -99,4 +99,5 @@ html{scrollbar-gutter:stable}
|
|
|
99
99
|
<p>You can also set <code>command</code>, <code>port</code>, <code>project</code>, and rules paths in <code>pyric.json</code>. See the <a href="../../reference/cli/">CLI reference</a>. <a href="../../ship/test-in-node/">Test in Node</a> covers an in-process sandbox for tests and scripts.</p>
|
|
100
100
|
<h2 id="what-remains-local">What remains local</h2>
|
|
101
101
|
<p>The mirrored services do not connect to a production Firebase project. Local data changes stay in the sandbox. Local Security Rules changes are enforced there and are not deployed. Firebase configuration passed to <code>initializeApp(config)</code> is accepted so the application code stays unchanged, but it does not select a cloud backend during development.</p>
|
|
102
|
+
<p>Before you ship, check your rules against your captured sessions: <a href="../../ship/ship-to-production/">The same code goes live</a>.</p>
|
|
102
103
|
<p>Continue with <a href="../how-the-swap-works/">How the swap works</a>, then <a href="../../build/authentication/">develop with the Firebase APIs</a>.</p> </article> </main> <aside class="doc-toc" aria-label="On this page" data-astro-cid-mw7aashj> <h2 class="doc-toc-heading" data-astro-cid-mw7aashj>On this page</h2> <ul data-astro-cid-mw7aashj> <li data-depth="2" data-astro-cid-mw7aashj> <a href="#start-with-a-coding-agent" data-astro-cid-mw7aashj>Start with a coding agent</a> </li><li data-depth="2" data-astro-cid-mw7aashj> <a href="#start-from-the-terminal" data-astro-cid-mw7aashj>Start from the terminal</a> </li><li data-depth="2" data-astro-cid-mw7aashj> <a href="#add-pyric-to-an-existing-application" data-astro-cid-mw7aashj>Add Pyric to an existing application</a> </li><li data-depth="3" data-astro-cid-mw7aashj> <a href="#vite" data-astro-cid-mw7aashj>Vite</a> </li><li data-depth="3" data-astro-cid-mw7aashj> <a href="#nextjs" data-astro-cid-mw7aashj>Next.js</a> </li><li data-depth="2" data-astro-cid-mw7aashj> <a href="#use-a-static-application-or-node-process" data-astro-cid-mw7aashj>Use a static application or Node process</a> </li><li data-depth="2" data-astro-cid-mw7aashj> <a href="#what-remains-local" data-astro-cid-mw7aashj>What remains local</a> </li> </ul> </aside> </div> <script type="module">const a=document.querySelector("[data-api-symbol-filter]"),d=[...document.querySelectorAll("[data-api-symbol-group]")],n=document.querySelector("[data-api-symbol-empty]");a?.addEventListener("input",()=>{const t=a.value.trim().toLowerCase();let o=0;for(const i of d){let e=0;i.querySelectorAll("[data-api-symbol]").forEach(l=>{const r=!t||(l.dataset.apiSymbol??"").includes(t);l.hidden=!r,r&&e++}),i.hidden=e===0,o+=e}n&&(n.hidden=o>0)});</script> <script type="module">const s=new Map;document.querySelectorAll('.doc-toc a[href^="#"]').forEach(t=>s.set(decodeURIComponent(t.hash.slice(1)),t));if(s.size>0){const t=new Set,n=()=>{let o=null;for(const e of s.keys())if(t.has(e)){o=e;break}s.forEach((e,c)=>e.classList.toggle("active",c===o))},r=new IntersectionObserver(o=>{for(const e of o)e.isIntersecting?t.add(e.target.id):t.delete(e.target.id);n()},{rootMargin:"-72px 0px -60% 0px"});s.forEach((o,e)=>{const c=document.getElementById(e);c&&r.observe(c)})}</script> <script type="module">document.querySelector('.studio__nav [aria-current="page"]')?.scrollIntoView({block:"nearest",inline:"center"});</script> </body></html>
|
|
@@ -137,4 +137,6 @@ You can also set `command`, `port`, `project`, and rules paths in `pyric.json`.
|
|
|
137
137
|
|
|
138
138
|
The mirrored services do not connect to a production Firebase project. Local data changes stay in the sandbox. Local Security Rules changes are enforced there and are not deployed. Firebase configuration passed to `initializeApp(config)` is accepted so the application code stays unchanged, but it does not select a cloud backend during development.
|
|
139
139
|
|
|
140
|
+
Before you ship, check your rules against your captured sessions: [The same code goes live](../ship/ship-to-production.md).
|
|
141
|
+
|
|
140
142
|
Continue with [How the swap works](./how-the-swap-works.md), then [develop with the Firebase APIs](../build/authentication.md).
|
|
@@ -515,7 +515,10 @@ observation may name an actor that arrives beside it.</p>
|
|
|
515
515
|
<span class="line"><span style="color:#E06C75"> rtdb</span><span style="color:#C678DD">?:</span><span style="color:#ABB2BF"> {</span></span>
|
|
516
516
|
<span class="line"><span style="color:#E06C75"> rules</span><span style="color:#ABB2BF">: </span><span style="color:#E06C75">Record</span><span style="color:#56B6C2"><</span><span style="color:#E06C75">string</span><span style="color:#ABB2BF">, unknown>;</span></span>
|
|
517
517
|
<span class="line"><span style="color:#ABB2BF"> };</span></span>
|
|
518
|
-
<span class="line"><span style="color:#E06C75">
|
|
518
|
+
<span class="line"><span style="color:#E06C75"> rtdbInstances</span><span style="color:#C678DD">?:</span><span style="color:#E06C75"> Record</span><span style="color:#56B6C2"><</span><span style="color:#E06C75">string</span><span style="color:#ABB2BF">, {</span></span>
|
|
519
|
+
<span class="line"><span style="color:#E06C75"> rules</span><span style="color:#ABB2BF">: </span><span style="color:#E06C75">Record</span><span style="color:#56B6C2"><</span><span style="color:#E06C75">string</span><span style="color:#ABB2BF">, </span><span style="color:#E06C75">unknown</span><span style="color:#56B6C2">></span><span style="color:#ABB2BF">;</span></span>
|
|
520
|
+
<span class="line"><span style="color:#ABB2BF"> }>;</span></span>
|
|
521
|
+
<span class="line"><span style="color:#ABB2BF"> storage?: </span><span style="color:#E06C75">string</span><span style="color:#ABB2BF">;</span></span>
|
|
519
522
|
<span class="line"><span style="color:#ABB2BF"> };</span></span>
|
|
520
523
|
<span class="line"><span style="color:#ABB2BF">}): </span><span style="color:#E5C07B">Promise</span><span style="color:#56B6C2"><</span><span style="color:#E06C75">AuthorizationCampaignReport</span><span style="color:#56B6C2">></span><span style="color:#ABB2BF">;</span></span></code></pre>
|
|
521
524
|
<h6 id="parameters-19">Parameters</h6>
|
|
@@ -560,7 +563,11 @@ observation may name an actor that arrives beside it.</p>
|
|
|
560
563
|
|
|
561
564
|
|
|
562
565
|
|
|
563
|
-
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
<table><thead><tr><th align="left">Parameter</th><th align="left">Type</th></tr></thead><tbody><tr><td align="left"><code>options</code></td><td align="left">{ <code>id?</code>: <code>string</code>; <code>includeCandidates?</code>: <code>boolean</code>; <code>rules</code>: { <code>firestore?</code>: <code>string</code>; <code>rtdb?</code>: { <code>rules</code>: <code>Record</code><<code>string</code>, <code>unknown</code>>; }; <code>rtdbInstances?</code>: <code>Record</code><<code>string</code>, { <code>rules</code>: <code>Record</code><<code>string</code>, <code>unknown</code>>; }>; <code>storage?</code>: <code>string</code>; }; }</td></tr><tr><td align="left"><code>options.id?</code></td><td align="left"><code>string</code></td></tr><tr><td align="left"><code>options.includeCandidates?</code></td><td align="left"><code>boolean</code></td></tr><tr><td align="left"><code>options.rules</code></td><td align="left">{ <code>firestore?</code>: <code>string</code>; <code>rtdb?</code>: { <code>rules</code>: <code>Record</code><<code>string</code>, <code>unknown</code>>; }; <code>rtdbInstances?</code>: <code>Record</code><<code>string</code>, { <code>rules</code>: <code>Record</code><<code>string</code>, <code>unknown</code>>; }>; <code>storage?</code>: <code>string</code>; }</td></tr><tr><td align="left"><code>options.rules.firestore?</code></td><td align="left"><code>string</code></td></tr><tr><td align="left"><code>options.rules.rtdb?</code></td><td align="left">{ <code>rules</code>: <code>Record</code><<code>string</code>, <code>unknown</code>>; }</td></tr><tr><td align="left"><code>options.rules.rtdb.rules</code></td><td align="left"><code>Record</code><<code>string</code>, <code>unknown</code>></td></tr><tr><td align="left"><code>options.rules.rtdbInstances?</code></td><td align="left"><code>Record</code><<code>string</code>, { <code>rules</code>: <code>Record</code><<code>string</code>, <code>unknown</code>>; }></td></tr><tr><td align="left"><code>options.rules.storage?</code></td><td align="left"><code>string</code></td></tr></tbody></table></div>
|
|
564
571
|
<h6 id="returns-24">Returns</h6>
|
|
565
572
|
<p><code>Promise</code><<a href="#authorizationcampaignreport-1"><code>AuthorizationCampaignReport</code></a>></p>
|
|
566
573
|
<p><a id="visualization"></a></p>
|
|
@@ -1540,7 +1547,15 @@ observation may name an actor that arrives beside it.</p>
|
|
|
1540
1547
|
|
|
1541
1548
|
|
|
1542
1549
|
|
|
1543
|
-
|
|
1550
|
+
|
|
1551
|
+
|
|
1552
|
+
|
|
1553
|
+
|
|
1554
|
+
|
|
1555
|
+
|
|
1556
|
+
|
|
1557
|
+
|
|
1558
|
+
<table><thead><tr><th align="left">Property</th><th align="left">Type</th></tr></thead><tbody><tr><td align="left"><a id="network"></a> <code>network</code></td><td align="left"><code>"forbid"</code></td></tr><tr><td align="left"><a id="rules-1"></a> <code>rules</code></td><td align="left">{ <code>firestore?</code>: <code>string</code>; <code>rtdb?</code>: { <code>rules</code>: <code>Record</code><<code>string</code>, <code>unknown</code>>; }; <code>rtdbInstances?</code>: <code>Record</code><<code>string</code>, { <code>rules</code>: <code>Record</code><<code>string</code>, <code>unknown</code>>; }>; <code>storage?</code>: <code>string</code>; }</td></tr><tr><td align="left"><code>rules.firestore?</code></td><td align="left"><code>string</code></td></tr><tr><td align="left"><code>rules.rtdb?</code></td><td align="left">{ <code>rules</code>: <code>Record</code><<code>string</code>, <code>unknown</code>>; }</td></tr><tr><td align="left"><code>rules.rtdb.rules</code></td><td align="left"><code>Record</code><<code>string</code>, <code>unknown</code>></td></tr><tr><td align="left"><code>rules.rtdbInstances?</code></td><td align="left"><code>Record</code><<code>string</code>, { <code>rules</code>: <code>Record</code><<code>string</code>, <code>unknown</code>>; }></td></tr><tr><td align="left"><code>rules.storage?</code></td><td align="left"><code>string</code></td></tr><tr><td align="left"><a id="schema-4"></a> <code>schema</code></td><td align="left"><code>"pyric.assurance.target.v1"</code></td></tr><tr><td align="left"><a id="state"></a> <code>state</code></td><td align="left">{ <code>auth?</code>: { <code>users</code>: <a href="#authfixtureuser"><code>AuthFixtureUser</code></a>[]; }; <code>firestore?</code>: <code>Record</code><<code>string</code>, <code>Record</code><<code>string</code>, <code>unknown</code>>>; <code>rtdb?</code>: <code>unknown</code>; <code>rtdbInstances?</code>: <code>Record</code><<code>string</code>, <code>unknown</code>>; <code>storage?</code>: <a href="#storageobjectfixture"><code>StorageObjectFixture</code></a>[]; }</td></tr><tr><td align="left"><code>state.auth?</code></td><td align="left">{ <code>users</code>: <a href="#authfixtureuser"><code>AuthFixtureUser</code></a>[]; }</td></tr><tr><td align="left"><code>state.auth.users</code></td><td align="left"><a href="#authfixtureuser"><code>AuthFixtureUser</code></a>[]</td></tr><tr><td align="left"><code>state.firestore?</code></td><td align="left"><code>Record</code><<code>string</code>, <code>Record</code><<code>string</code>, <code>unknown</code>>></td></tr><tr><td align="left"><code>state.rtdb?</code></td><td align="left"><code>unknown</code></td></tr><tr><td align="left"><code>state.rtdbInstances?</code></td><td align="left"><code>Record</code><<code>string</code>, <code>unknown</code>></td></tr><tr><td align="left"><code>state.storage?</code></td><td align="left"><a href="#storageobjectfixture"><code>StorageObjectFixture</code></a>[]</td></tr></tbody></table></div>
|
|
1544
1559
|
<hr>
|
|
1545
1560
|
<p><a id="minimizationresult"></a></p>
|
|
1546
1561
|
<h3 id="minimizationresult">MinimizationResult</h3>
|
|
@@ -1812,7 +1827,11 @@ observation may name an actor that arrives beside it.</p>
|
|
|
1812
1827
|
|
|
1813
1828
|
|
|
1814
1829
|
|
|
1815
|
-
|
|
1830
|
+
|
|
1831
|
+
|
|
1832
|
+
|
|
1833
|
+
|
|
1834
|
+
<table><thead><tr><th align="left">Property</th><th align="left">Type</th></tr></thead><tbody><tr><td align="left"><a id="data-1"></a> <code>data?</code></td><td align="left"><code>unknown</code></td></tr><tr><td align="left"><a id="instance"></a> <code>instance?</code></td><td align="left"><code>string</code></td></tr><tr><td align="left"><a id="method-2"></a> <code>method</code></td><td align="left"><code>"get"</code> | <code>"update"</code> | <code>"set"</code> | <code>"remove"</code></td></tr><tr><td align="left"><a id="path-2"></a> <code>path</code></td><td align="left"><code>string</code></td></tr><tr><td align="left"><a id="query-1"></a> <code>query?</code></td><td align="left">{ <code>endAt?</code>: { <code>key?</code>: <code>string</code>; <code>value</code>: <code>unknown</code>; }; <code>endBefore?</code>: { <code>key?</code>: <code>string</code>; <code>value</code>: <code>unknown</code>; }; <code>equalTo?</code>: { <code>key?</code>: <code>string</code>; <code>value</code>: <code>unknown</code>; }; <code>limitToFirst?</code>: <code>number</code>; <code>limitToLast?</code>: <code>number</code>; <code>orderBy?</code>: | { <code>kind</code>: <code>"child"</code>; <code>path</code>: <code>string</code>; } | { <code>kind</code>: <code>"key"</code>; } | { <code>kind</code>: <code>"value"</code>; }; <code>startAfter?</code>: { <code>key?</code>: <code>string</code>; <code>value</code>: <code>unknown</code>; }; <code>startAt?</code>: { <code>key?</code>: <code>string</code>; <code>value</code>: <code>unknown</code>; }; }</td></tr><tr><td align="left"><code>query.endAt?</code></td><td align="left">{ <code>key?</code>: <code>string</code>; <code>value</code>: <code>unknown</code>; }</td></tr><tr><td align="left"><code>query.endAt.key?</code></td><td align="left"><code>string</code></td></tr><tr><td align="left"><code>query.endAt.value</code></td><td align="left"><code>unknown</code></td></tr><tr><td align="left"><code>query.endBefore?</code></td><td align="left">{ <code>key?</code>: <code>string</code>; <code>value</code>: <code>unknown</code>; }</td></tr><tr><td align="left"><code>query.endBefore.key?</code></td><td align="left"><code>string</code></td></tr><tr><td align="left"><code>query.endBefore.value</code></td><td align="left"><code>unknown</code></td></tr><tr><td align="left"><code>query.equalTo?</code></td><td align="left">{ <code>key?</code>: <code>string</code>; <code>value</code>: <code>unknown</code>; }</td></tr><tr><td align="left"><code>query.equalTo.key?</code></td><td align="left"><code>string</code></td></tr><tr><td align="left"><code>query.equalTo.value</code></td><td align="left"><code>unknown</code></td></tr><tr><td align="left"><code>query.limitToFirst?</code></td><td align="left"><code>number</code></td></tr><tr><td align="left"><code>query.limitToLast?</code></td><td align="left"><code>number</code></td></tr><tr><td align="left"><code>query.orderBy?</code></td><td align="left">| { <code>kind</code>: <code>"child"</code>; <code>path</code>: <code>string</code>; } | { <code>kind</code>: <code>"key"</code>; } | { <code>kind</code>: <code>"value"</code>; }</td></tr><tr><td align="left"><code>query.startAfter?</code></td><td align="left">{ <code>key?</code>: <code>string</code>; <code>value</code>: <code>unknown</code>; }</td></tr><tr><td align="left"><code>query.startAfter.key?</code></td><td align="left"><code>string</code></td></tr><tr><td align="left"><code>query.startAfter.value</code></td><td align="left"><code>unknown</code></td></tr><tr><td align="left"><code>query.startAt?</code></td><td align="left">{ <code>key?</code>: <code>string</code>; <code>value</code>: <code>unknown</code>; }</td></tr><tr><td align="left"><code>query.startAt.key?</code></td><td align="left"><code>string</code></td></tr><tr><td align="left"><code>query.startAt.value</code></td><td align="left"><code>unknown</code></td></tr><tr><td align="left"><a id="service-3"></a> <code>service</code></td><td align="left"><code>"rtdb"</code></td></tr></tbody></table></div>
|
|
1816
1835
|
<hr>
|
|
1817
1836
|
<p><a id="runsecuritycasesinput"></a></p>
|
|
1818
1837
|
<h3 id="runsecuritycasesinput">RunSecurityCasesInput</h3>
|
|
@@ -492,6 +492,9 @@ verifyRules(options: {
|
|
|
492
492
|
rtdb?: {
|
|
493
493
|
rules: Record<string, unknown>;
|
|
494
494
|
};
|
|
495
|
+
rtdbInstances?: Record<string, {
|
|
496
|
+
rules: Record<string, unknown>;
|
|
497
|
+
}>;
|
|
495
498
|
storage?: string;
|
|
496
499
|
};
|
|
497
500
|
}): Promise<AuthorizationCampaignReport>;
|
|
@@ -501,13 +504,14 @@ verifyRules(options: {
|
|
|
501
504
|
|
|
502
505
|
| Parameter | Type |
|
|
503
506
|
| :------ | :------ |
|
|
504
|
-
| `options` | \{ `id?`: `string`; `includeCandidates?`: `boolean`; `rules`: \{ `firestore?`: `string`; `rtdb?`: \{ `rules`: `Record`\<`string`, `unknown`\>; \}; `storage?`: `string`; \}; \} |
|
|
507
|
+
| `options` | \{ `id?`: `string`; `includeCandidates?`: `boolean`; `rules`: \{ `firestore?`: `string`; `rtdb?`: \{ `rules`: `Record`\<`string`, `unknown`\>; \}; `rtdbInstances?`: `Record`\<`string`, \{ `rules`: `Record`\<`string`, `unknown`\>; \}\>; `storage?`: `string`; \}; \} |
|
|
505
508
|
| `options.id?` | `string` |
|
|
506
509
|
| `options.includeCandidates?` | `boolean` |
|
|
507
|
-
| `options.rules` | \{ `firestore?`: `string`; `rtdb?`: \{ `rules`: `Record`\<`string`, `unknown`\>; \}; `storage?`: `string`; \} |
|
|
510
|
+
| `options.rules` | \{ `firestore?`: `string`; `rtdb?`: \{ `rules`: `Record`\<`string`, `unknown`\>; \}; `rtdbInstances?`: `Record`\<`string`, \{ `rules`: `Record`\<`string`, `unknown`\>; \}\>; `storage?`: `string`; \} |
|
|
508
511
|
| `options.rules.firestore?` | `string` |
|
|
509
512
|
| `options.rules.rtdb?` | \{ `rules`: `Record`\<`string`, `unknown`\>; \} |
|
|
510
513
|
| `options.rules.rtdb.rules` | `Record`\<`string`, `unknown`\> |
|
|
514
|
+
| `options.rules.rtdbInstances?` | `Record`\<`string`, \{ `rules`: `Record`\<`string`, `unknown`\>; \}\> |
|
|
511
515
|
| `options.rules.storage?` | `string` |
|
|
512
516
|
|
|
513
517
|
###### Returns
|
|
@@ -941,17 +945,19 @@ Credential-free projection safe to hand to Studio visualization code.
|
|
|
941
945
|
| Property | Type |
|
|
942
946
|
| :------ | :------ |
|
|
943
947
|
| <a id="network"></a> `network` | `"forbid"` |
|
|
944
|
-
| <a id="rules-1"></a> `rules` | \{ `firestore?`: `string`; `rtdb?`: \{ `rules`: `Record`\<`string`, `unknown`\>; \}; `storage?`: `string`; \} |
|
|
948
|
+
| <a id="rules-1"></a> `rules` | \{ `firestore?`: `string`; `rtdb?`: \{ `rules`: `Record`\<`string`, `unknown`\>; \}; `rtdbInstances?`: `Record`\<`string`, \{ `rules`: `Record`\<`string`, `unknown`\>; \}\>; `storage?`: `string`; \} |
|
|
945
949
|
| `rules.firestore?` | `string` |
|
|
946
950
|
| `rules.rtdb?` | \{ `rules`: `Record`\<`string`, `unknown`\>; \} |
|
|
947
951
|
| `rules.rtdb.rules` | `Record`\<`string`, `unknown`\> |
|
|
952
|
+
| `rules.rtdbInstances?` | `Record`\<`string`, \{ `rules`: `Record`\<`string`, `unknown`\>; \}\> |
|
|
948
953
|
| `rules.storage?` | `string` |
|
|
949
954
|
| <a id="schema-4"></a> `schema` | `"pyric.assurance.target.v1"` |
|
|
950
|
-
| <a id="state"></a> `state` | \{ `auth?`: \{ `users`: [`AuthFixtureUser`](#authfixtureuser)[]; \}; `firestore?`: `Record`\<`string`, `Record`\<`string`, `unknown`\>\>; `rtdb?`: `unknown`; `storage?`: [`StorageObjectFixture`](#storageobjectfixture)[]; \} |
|
|
955
|
+
| <a id="state"></a> `state` | \{ `auth?`: \{ `users`: [`AuthFixtureUser`](#authfixtureuser)[]; \}; `firestore?`: `Record`\<`string`, `Record`\<`string`, `unknown`\>\>; `rtdb?`: `unknown`; `rtdbInstances?`: `Record`\<`string`, `unknown`\>; `storage?`: [`StorageObjectFixture`](#storageobjectfixture)[]; \} |
|
|
951
956
|
| `state.auth?` | \{ `users`: [`AuthFixtureUser`](#authfixtureuser)[]; \} |
|
|
952
957
|
| `state.auth.users` | [`AuthFixtureUser`](#authfixtureuser)[] |
|
|
953
958
|
| `state.firestore?` | `Record`\<`string`, `Record`\<`string`, `unknown`\>\> |
|
|
954
959
|
| `state.rtdb?` | `unknown` |
|
|
960
|
+
| `state.rtdbInstances?` | `Record`\<`string`, `unknown`\> |
|
|
955
961
|
| `state.storage?` | [`StorageObjectFixture`](#storageobjectfixture)[] |
|
|
956
962
|
|
|
957
963
|
***
|
|
@@ -1050,6 +1056,7 @@ Credential-free projection safe to hand to Studio visualization code.
|
|
|
1050
1056
|
| Property | Type |
|
|
1051
1057
|
| :------ | :------ |
|
|
1052
1058
|
| <a id="data-1"></a> `data?` | `unknown` |
|
|
1059
|
+
| <a id="instance"></a> `instance?` | `string` |
|
|
1053
1060
|
| <a id="method-2"></a> `method` | `"get"` \| `"update"` \| `"set"` \| `"remove"` |
|
|
1054
1061
|
| <a id="path-2"></a> `path` | `string` |
|
|
1055
1062
|
| <a id="query-1"></a> `query?` | \{ `endAt?`: \{ `key?`: `string`; `value`: `unknown`; \}; `endBefore?`: \{ `key?`: `string`; `value`: `unknown`; \}; `equalTo?`: \{ `key?`: `string`; `value`: `unknown`; \}; `limitToFirst?`: `number`; `limitToLast?`: `number`; `orderBy?`: \| \{ `kind`: `"child"`; `path`: `string`; \} \| \{ `kind`: `"key"`; \} \| \{ `kind`: `"value"`; \}; `startAfter?`: \{ `key?`: `string`; `value`: `unknown`; \}; `startAt?`: \{ `key?`: `string`; `value`: `unknown`; \}; \} |
|
|
@@ -598,7 +598,20 @@ Generated from the TypeScript declarations shipped at this import path.
|
|
|
598
598
|
|
|
599
599
|
|
|
600
600
|
|
|
601
|
-
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
<table><thead><tr><th align="left">Property</th><th align="left">Type</th><th align="left">Description</th></tr></thead><tbody><tr><td align="left"><a id="casederivation"></a> <code>caseDerivation?</code></td><td align="left"><code>Omit</code><<a href="#deriverulestestcasesoptions"><code>DeriveRulesTestCasesOptions</code></a>, <code>"service"</code>></td><td align="left">-</td></tr><tr><td align="left"><a id="engines"></a> <code>engines?</code></td><td align="left"><a href="#verifyengine"><code>VerifyEngine</code></a>[]</td><td align="left">-</td></tr><tr><td align="left"><a id="rules"></a> <code>rules</code></td><td align="left"><a href="#verifyrulesinput"><code>VerifyRulesInput</code></a></td><td align="left">-</td></tr><tr><td align="left"><a id="rulestestapi"></a> <code>rulesTestApi?</code></td><td align="left">{ <code>expressionReportLevel?</code>: <code>ExpressionReportLevel</code>; <code>scope</code>: <code>ProjectScope</code>; }</td><td align="left">-</td></tr><tr><td align="left"><code>rulesTestApi.expressionReportLevel?</code></td><td align="left"><code>ExpressionReportLevel</code></td><td align="left">-</td></tr><tr><td align="left"><code>rulesTestApi.scope</code></td><td align="left"><code>ProjectScope</code></td><td align="left">-</td></tr><tr><td align="left"><a id="services-1"></a> <code>services?</code></td><td align="left"><a href="#verifiableservice"><code>VerifiableService</code></a>[]</td><td align="left">-</td></tr><tr><td align="left"><a id="staging"></a> <code>staging?</code></td><td align="left">{ <code>client</code>: <code>RtdbRestClient</code>; <code>projectId</code>: <code>string</code>; } & <code>StagingRunHooks</code></td><td align="left">The staging engine’s instance: a client for a dedicated RTDB instance enrolled with <code>pyric verify staging init</code>, and the project it is enrolled for. Required when <code>engines</code> includes <code>staging</code>.</td></tr></tbody></table></div>
|
|
602
615
|
<hr>
|
|
603
616
|
<p><a id="verifyresult"></a></p>
|
|
604
617
|
<h3 id="verifyresult">VerifyResult</h3>
|
|
@@ -826,7 +839,7 @@ Generated from the TypeScript declarations shipped at this import path.
|
|
|
826
839
|
<hr>
|
|
827
840
|
<p><a id="verifyengine"></a></p>
|
|
828
841
|
<h3 id="verifyengine">VerifyEngine</h3>
|
|
829
|
-
<pre class="astro-code one-dark-pro" style="background-color:#282c34;color:#abb2bf; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#C678DD">type</span><span style="color:#E5C07B"> VerifyEngine</span><span style="color:#56B6C2"> =</span><span style="color:#98C379"> "sandbox"</span><span style="color:#ABB2BF"> | </span><span style="color:#98C379">"rulesTestApi"</span><span style="color:#ABB2BF">;</span></span></code></pre>
|
|
842
|
+
<pre class="astro-code one-dark-pro" style="background-color:#282c34;color:#abb2bf; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#C678DD">type</span><span style="color:#E5C07B"> VerifyEngine</span><span style="color:#56B6C2"> =</span><span style="color:#98C379"> "sandbox"</span><span style="color:#ABB2BF"> | </span><span style="color:#98C379">"rulesTestApi"</span><span style="color:#ABB2BF"> | </span><span style="color:#98C379">"staging"</span><span style="color:#ABB2BF">;</span></span></code></pre>
|
|
830
843
|
<hr>
|
|
831
844
|
<p><a id="verifyfixturewarning"></a></p>
|
|
832
845
|
<h3 id="verifyfixturewarning">VerifyFixtureWarning</h3>
|
|
@@ -245,15 +245,16 @@ No credentials were found, with the sources that were checked for one.
|
|
|
245
245
|
|
|
246
246
|
#### Properties
|
|
247
247
|
|
|
248
|
-
| Property | Type |
|
|
249
|
-
| :------ | :------ |
|
|
250
|
-
| <a id="casederivation"></a> `caseDerivation?` | `Omit`\<[`DeriveRulesTestCasesOptions`](#deriverulestestcasesoptions), `"service"`\> |
|
|
251
|
-
| <a id="engines"></a> `engines?` | [`VerifyEngine`](#verifyengine)[] |
|
|
252
|
-
| <a id="rules"></a> `rules` | [`VerifyRulesInput`](#verifyrulesinput) |
|
|
253
|
-
| <a id="rulestestapi"></a> `rulesTestApi?` | \{ `expressionReportLevel?`: `ExpressionReportLevel`; `scope`: `ProjectScope`; \} |
|
|
254
|
-
| `rulesTestApi.expressionReportLevel?` | `ExpressionReportLevel` |
|
|
255
|
-
| `rulesTestApi.scope` | `ProjectScope` |
|
|
256
|
-
| <a id="services-1"></a> `services?` | [`VerifiableService`](#verifiableservice)[] |
|
|
248
|
+
| Property | Type | Description |
|
|
249
|
+
| :------ | :------ | :------ |
|
|
250
|
+
| <a id="casederivation"></a> `caseDerivation?` | `Omit`\<[`DeriveRulesTestCasesOptions`](#deriverulestestcasesoptions), `"service"`\> | - |
|
|
251
|
+
| <a id="engines"></a> `engines?` | [`VerifyEngine`](#verifyengine)[] | - |
|
|
252
|
+
| <a id="rules"></a> `rules` | [`VerifyRulesInput`](#verifyrulesinput) | - |
|
|
253
|
+
| <a id="rulestestapi"></a> `rulesTestApi?` | \{ `expressionReportLevel?`: `ExpressionReportLevel`; `scope`: `ProjectScope`; \} | - |
|
|
254
|
+
| `rulesTestApi.expressionReportLevel?` | `ExpressionReportLevel` | - |
|
|
255
|
+
| `rulesTestApi.scope` | `ProjectScope` | - |
|
|
256
|
+
| <a id="services-1"></a> `services?` | [`VerifiableService`](#verifiableservice)[] | - |
|
|
257
|
+
| <a id="staging"></a> `staging?` | \{ `client`: `RtdbRestClient`; `projectId`: `string`; \} & `StagingRunHooks` | The staging engine's instance: a client for a dedicated RTDB instance enrolled with `pyric verify staging init`, and the project it is enrolled for. Required when `engines` includes `staging`. |
|
|
257
258
|
|
|
258
259
|
***
|
|
259
260
|
|
|
@@ -424,7 +425,7 @@ optional instance: string;
|
|
|
424
425
|
### VerifyEngine
|
|
425
426
|
|
|
426
427
|
```ts
|
|
427
|
-
type VerifyEngine = "sandbox" | "rulesTestApi";
|
|
428
|
+
type VerifyEngine = "sandbox" | "rulesTestApi" | "staging";
|
|
428
429
|
```
|
|
429
430
|
|
|
430
431
|
***
|
|
@@ -256,7 +256,7 @@ html{scrollbar-gutter:stable}
|
|
|
256
256
|
<h3 id="replay-a-captured-session-against-new-rules">Replay a captured session against new rules</h3>
|
|
257
257
|
<p><code>pyric verify</code> replays a session captured during development against a candidate ruleset and reports each operation whose verdict changed:</p>
|
|
258
258
|
<pre class="astro-code one-dark-pro" style="background-color:#282c34;color:#abb2bf; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#61AFEF">pyric</span><span style="color:#98C379"> verify</span><span style="color:#D19A66"> --service</span><span style="color:#98C379"> rtdb</span><span style="color:#D19A66"> --rules</span><span style="color:#98C379"> rtdb=database.rules.json</span></span></code></pre>
|
|
259
|
-
<p>RTDB verification runs on the sandbox engine
|
|
259
|
+
<p>RTDB verification runs on the sandbox engine by default. The hosted Rules Test API evaluates Firestore rules, so <code>--engine rules-test-api</code> is refused for <code>rtdb</code>. For Realtime Database, run <code>--engine staging</code> to test the session against a real instance you provide, enrolled with <code>pyric verify staging init</code>. See <a href="../../ship/ship-to-production/">ship to production</a>.</p>
|
|
260
260
|
<h2 id="run-the-suite-through-an-agent">Run the suite through an agent</h2>
|
|
261
261
|
<p>An agent can run the local loop through <code>firestore_simulate_rules</code>, which means the rules it writes can arrive with explicit passing cases instead of a promise. For Realtime Database, the agent calls <code>rtdb_simulate_access</code> with an <code>operation</code> (<code>read</code>, <code>write</code>, <code>update</code>, or <code>validate</code>), a <code>path</code>, and optionally an <code>auth</code> of <code>{ uid, claims }</code>, a <code>newData</code> value, a <code>query</code>, and a <code>now</code>. The tool evaluates against the rules and data currently loaded in the sandbox, so it needs no separate rules-loading call. It uses the sandbox clock for <code>now</code> unless you pass one, and it returns <code>decision</code>, <code>matchedPath</code>, <code>matchedRule</code>, <code>reason</code>, and the <code>trace</code>. See <a href="../../agent/work-with-an-agent/">Work with an agent</a>.</p>
|
|
262
262
|
<h2 id="where-to-go-next">Where to go next</h2>
|
|
@@ -287,7 +287,7 @@ This layer also covers listeners, transactions, and `.indexOn` behavior, which a
|
|
|
287
287
|
```bash
|
|
288
288
|
pyric verify --service rtdb --rules rtdb=database.rules.json
|
|
289
289
|
```
|
|
290
|
-
RTDB verification runs on the sandbox engine
|
|
290
|
+
RTDB verification runs on the sandbox engine by default. The hosted Rules Test API evaluates Firestore rules, so `--engine rules-test-api` is refused for `rtdb`. For Realtime Database, run `--engine staging` to test the session against a real instance you provide, enrolled with `pyric verify staging init`. See [ship to production](../ship/ship-to-production.md).
|
|
291
291
|
|
|
292
292
|
## Run the suite through an agent
|
|
293
293
|
|
|
@@ -20,7 +20,7 @@ html{scrollbar-gutter:stable}
|
|
|
20
20
|
<p>The output names each operation whose verdict changed:</p>
|
|
21
21
|
<pre class="astro-code one-dark-pro" style="background-color:#282c34;color:#abb2bf; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>✗ chat - rtdb: 1 failure(s)</span></span>
|
|
22
22
|
<span class="line"><span> [rtdb] now-denied: set /rooms/r1/messages/m1 (PERMISSION_DENIED)</span></span></code></pre>
|
|
23
|
-
<p><code>now-denied</code> means a write that succeeded during development would be rejected under the new rules. <code>now-allowed</code> means
|
|
23
|
+
<p><code>now-denied</code> means a read, listener, <code>onDisconnect</code> registration, or write that succeeded during development would be rejected under the new rules. <code>now-allowed</code> means a loosening where a captured denial is now permitted. Auto-id aliases and timestamp drift are reported as informational, not failures, because replaying generated values is supposed to produce them. The exit code is 1 on any real divergence, which makes the CI gate one line:</p>
|
|
24
24
|
<pre class="astro-code one-dark-pro" style="background-color:#282c34;color:#abb2bf; overflow-x: auto;" tabindex="0" data-language="yaml"><code><span class="line"><span style="color:#ABB2BF">- </span><span style="color:#E06C75">run</span><span style="color:#ABB2BF">: </span><span style="color:#98C379">pyric verify journeys/ --rules firestore=firestore.rules</span></span></code></pre>
|
|
25
25
|
<p>If your app uses more than one Realtime Database instance, <code>pyric verify</code> checks each instance the session used against that instance’s own rules, read from <code>firebase.json</code> the way <code>firebase deploy</code> reads them. To try different rules on one instance, name it with <code>rtdb:<instance></code>; <code>rtdb=</code> stays the default instance:</p>
|
|
26
26
|
<pre class="astro-code one-dark-pro" style="background-color:#282c34;color:#abb2bf; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#61AFEF">pyric</span><span style="color:#98C379"> verify</span><span style="color:#D19A66"> --rules</span><span style="color:#98C379"> rtdb:shard-00=shard.rules.json</span></span></code></pre>
|
|
@@ -30,6 +30,9 @@ html{scrollbar-gutter:stable}
|
|
|
30
30
|
<p>By default verification runs on the local sandbox engine. For Firestore you can also send the derived cases to Firebase’s hosted Rules Test API, or run both:</p>
|
|
31
31
|
<pre class="astro-code one-dark-pro" style="background-color:#282c34;color:#abb2bf; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#61AFEF">pyric</span><span style="color:#98C379"> verify</span><span style="color:#D19A66"> --service</span><span style="color:#98C379"> firestore</span><span style="color:#D19A66"> --engine</span><span style="color:#98C379"> both</span><span style="color:#D19A66"> --project</span><span style="color:#98C379"> my-app</span><span style="color:#D19A66"> --rules</span><span style="color:#98C379"> firestore=firestore.rules</span></span></code></pre>
|
|
32
32
|
<p><code>both</code> cross-checks the two engines against each other and flags any disagreement. That checks your rules and, at the same time, checks Pyric’s own engine against Google’s answer for your exact traffic. The Rules Test API evaluates cases only: it does not deploy rules or change a project. Hosted verification needs a project id plus <code>FIREBASE_SA_BASE64</code> or <code>GOOGLE_APPLICATION_CREDENTIALS</code> (or ADC); build a <code>ProjectScope</code> programmatically with <code>@pyric/cli/credentials/node</code> (<code>fromServiceAccount</code> / <code>fromAdc</code>).</p>
|
|
33
|
+
<p>For Realtime Database, run <code>--engine staging</code> to test the session against a real instance you provide, enrolled with <code>pyric verify staging init</code>:</p>
|
|
34
|
+
<pre class="astro-code one-dark-pro" style="background-color:#282c34;color:#abb2bf; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#61AFEF">pyric</span><span style="color:#98C379"> verify</span><span style="color:#D19A66"> --engine</span><span style="color:#98C379"> sandbox</span><span style="color:#D19A66"> --engine</span><span style="color:#98C379"> staging</span><span style="color:#D19A66"> --rules</span><span style="color:#98C379"> rtdb=open.rules.json</span></span></code></pre>
|
|
35
|
+
<p>This deploys the rules to your enrolled instance, replays the session via the REST API, and compares production’s verdicts to the local sandbox replay.</p>
|
|
33
36
|
<h2 id="deploy-hosting-and-functions">Deploy hosting and functions</h2>
|
|
34
37
|
<pre class="astro-code one-dark-pro" style="background-color:#282c34;color:#abb2bf; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#61AFEF">firebase</span><span style="color:#98C379"> deploy</span><span style="color:#D19A66"> --only</span><span style="color:#98C379"> hosting</span><span style="color:#7F848E;font-style:italic"> # preview channels via firebase hosting:channel:deploy</span></span>
|
|
35
38
|
<span class="line"><span style="color:#61AFEF">firebase</span><span style="color:#98C379"> deploy</span><span style="color:#D19A66"> --only</span><span style="color:#98C379"> functions</span></span></code></pre>
|
|
@@ -33,7 +33,7 @@ The output names each operation whose verdict changed:
|
|
|
33
33
|
✗ chat - rtdb: 1 failure(s)
|
|
34
34
|
[rtdb] now-denied: set /rooms/r1/messages/m1 (PERMISSION_DENIED)
|
|
35
35
|
```
|
|
36
|
-
`now-denied` means a write that succeeded during development would be rejected under the new rules. `now-allowed` means
|
|
36
|
+
`now-denied` means a read, listener, `onDisconnect` registration, or write that succeeded during development would be rejected under the new rules. `now-allowed` means a loosening where a captured denial is now permitted. Auto-id aliases and timestamp drift are reported as informational, not failures, because replaying generated values is supposed to produce them. The exit code is 1 on any real divergence, which makes the CI gate one line:
|
|
37
37
|
```yaml
|
|
38
38
|
- run: pyric verify journeys/ --rules firestore=firestore.rules
|
|
39
39
|
```
|
|
@@ -53,6 +53,12 @@ pyric verify --service firestore --engine both --project my-app --rules firestor
|
|
|
53
53
|
```
|
|
54
54
|
`both` cross-checks the two engines against each other and flags any disagreement. That checks your rules and, at the same time, checks Pyric's own engine against Google's answer for your exact traffic. The Rules Test API evaluates cases only: it does not deploy rules or change a project. Hosted verification needs a project id plus `FIREBASE_SA_BASE64` or `GOOGLE_APPLICATION_CREDENTIALS` (or ADC); build a `ProjectScope` programmatically with `@pyric/cli/credentials/node` (`fromServiceAccount` / `fromAdc`).
|
|
55
55
|
|
|
56
|
+
For Realtime Database, run `--engine staging` to test the session against a real instance you provide, enrolled with `pyric verify staging init`:
|
|
57
|
+
```bash
|
|
58
|
+
pyric verify --engine sandbox --engine staging --rules rtdb=open.rules.json
|
|
59
|
+
```
|
|
60
|
+
This deploys the rules to your enrolled instance, replays the session via the REST API, and compares production's verdicts to the local sandbox replay.
|
|
61
|
+
|
|
56
62
|
## Deploy hosting and functions
|
|
57
63
|
```bash
|
|
58
64
|
firebase deploy --only hosting # preview channels via firebase hosting:channel:deploy
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
<!DOCTYPE html><html lang="en"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"><title>Play chess against Security Rules</title><link rel="icon" type="image/svg+xml" href="/__pyric/ui/pyric-logo.svg"><link rel="preconnect" href="https://use.typekit.net" crossorigin><link rel="stylesheet" href="https://use.typekit.net/jwb3ycu.css"><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet"><style>html{scrollbar-gutter:stable}
|
|
2
2
|
</style>
|
|
3
3
|
<link rel="stylesheet" href="/__pyric/ui/_astro/_example_.B9DG9HLJ.css">
|
|
4
|
-
<link rel="stylesheet" href="/__pyric/ui/_astro/app.8TtSeQQX.css"></head> <body> <style>astro-island,astro-slot,astro-static-slot{display:contents}</style><script>(()=>{var e=async t=>{await(await t())()};(self.Astro||(self.Astro={})).only=e;window.dispatchEvent(new Event("astro:only"));})();</script><script>(()=>{var A=Object.defineProperty;var g=(i,o,a)=>o in i?A(i,o,{enumerable:!0,configurable:!0,writable:!0,value:a}):i[o]=a;var d=(i,o,a)=>g(i,typeof o!="symbol"?o+"":o,a);{let i={0:t=>m(t),1:t=>a(t),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(a(t)),5:t=>new Set(a(t)),6:t=>BigInt(t),7:t=>new URL(t),8:t=>new Uint8Array(t),9:t=>new Uint16Array(t),10:t=>new Uint32Array(t),11:t=>1/0*t},o=t=>{let[l,e]=t;return l in i?i[l](e):void 0},a=t=>t.map(o),m=t=>typeof t!="object"||t===null?t:Object.fromEntries(Object.entries(t).map(([l,e])=>[l,o(e)]));class y extends HTMLElement{constructor(){super(...arguments);d(this,"Component");d(this,"hydrator");d(this,"hydrate",async()=>{var b;if(!this.hydrator||!this.isConnected)return;let e=(b=this.parentElement)==null?void 0:b.closest("astro-island[ssr]");if(e){e.addEventListener("astro:hydrate",this.hydrate,{once:!0});return}let c=this.querySelectorAll("astro-slot"),n={},h=this.querySelectorAll("template[data-astro-template]");for(let r of h){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("data-astro-template")||"default"]=r.innerHTML,r.remove())}for(let r of c){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("name")||"default"]=r.innerHTML)}let p;try{p=this.hasAttribute("props")?m(JSON.parse(this.getAttribute("props"))):{}}catch(r){let s=this.getAttribute("component-url")||"<unknown>",v=this.getAttribute("component-export");throw v&&(s+=` (export ${v})`),console.error(`[hydrate] Error parsing props for component ${s}`,this.getAttribute("props"),r),r}let u;await this.hydrator(this)(this.Component,p,n,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),this.dispatchEvent(new CustomEvent("astro:hydrate"))});d(this,"unmount",()=>{this.isConnected||this.dispatchEvent(new CustomEvent("astro:unmount"))})}disconnectedCallback(){document.removeEventListener("astro:after-swap",this.unmount),document.addEventListener("astro:after-swap",this.unmount,{once:!0})}connectedCallback(){if(!this.hasAttribute("await-children")||document.readyState==="interactive"||document.readyState==="complete")this.childrenConnectedCallback();else{let e=()=>{document.removeEventListener("DOMContentLoaded",e),c.disconnect(),this.childrenConnectedCallback()},c=new MutationObserver(()=>{var n;((n=this.lastChild)==null?void 0:n.nodeType)===Node.COMMENT_NODE&&this.lastChild.nodeValue==="astro:end"&&(this.lastChild.remove(),e())});c.observe(this,{childList:!0}),document.addEventListener("DOMContentLoaded",e)}}async childrenConnectedCallback(){let e=this.getAttribute("before-hydration-url");e&&await import(e),this.start()}async start(){let e=JSON.parse(this.getAttribute("opts")),c=this.getAttribute("client");if(Astro[c]===void 0){window.addEventListener(`astro:${c}`,()=>this.start(),{once:!0});return}try{await Astro[c](async()=>{let n=this.getAttribute("renderer-url"),[h,{default:p}]=await Promise.all([import(this.getAttribute("component-url")),n?import(n):()=>()=>{}]),u=this.getAttribute("component-export")||"default";if(!u.includes("."))this.Component=h[u];else{this.Component=h;for(let f of u.split("."))this.Component=this.Component[f]}return this.hydrator=p,this.hydrate},e,this)}catch(n){console.error(`[astro-island] Error hydrating ${this.getAttribute("component-url")}`,n)}}attributeChangedCallback(){this.hydrate()}}d(y,"observedAttributes",["props"]),customElements.get("astro-island")||customElements.define("astro-island",y)}})();</script><astro-island uid="
|
|
4
|
+
<link rel="stylesheet" href="/__pyric/ui/_astro/app.8TtSeQQX.css"></head> <body> <style>astro-island,astro-slot,astro-static-slot{display:contents}</style><script>(()=>{var e=async t=>{await(await t())()};(self.Astro||(self.Astro={})).only=e;window.dispatchEvent(new Event("astro:only"));})();</script><script>(()=>{var A=Object.defineProperty;var g=(i,o,a)=>o in i?A(i,o,{enumerable:!0,configurable:!0,writable:!0,value:a}):i[o]=a;var d=(i,o,a)=>g(i,typeof o!="symbol"?o+"":o,a);{let i={0:t=>m(t),1:t=>a(t),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(a(t)),5:t=>new Set(a(t)),6:t=>BigInt(t),7:t=>new URL(t),8:t=>new Uint8Array(t),9:t=>new Uint16Array(t),10:t=>new Uint32Array(t),11:t=>1/0*t},o=t=>{let[l,e]=t;return l in i?i[l](e):void 0},a=t=>t.map(o),m=t=>typeof t!="object"||t===null?t:Object.fromEntries(Object.entries(t).map(([l,e])=>[l,o(e)]));class y extends HTMLElement{constructor(){super(...arguments);d(this,"Component");d(this,"hydrator");d(this,"hydrate",async()=>{var b;if(!this.hydrator||!this.isConnected)return;let e=(b=this.parentElement)==null?void 0:b.closest("astro-island[ssr]");if(e){e.addEventListener("astro:hydrate",this.hydrate,{once:!0});return}let c=this.querySelectorAll("astro-slot"),n={},h=this.querySelectorAll("template[data-astro-template]");for(let r of h){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("data-astro-template")||"default"]=r.innerHTML,r.remove())}for(let r of c){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("name")||"default"]=r.innerHTML)}let p;try{p=this.hasAttribute("props")?m(JSON.parse(this.getAttribute("props"))):{}}catch(r){let s=this.getAttribute("component-url")||"<unknown>",v=this.getAttribute("component-export");throw v&&(s+=` (export ${v})`),console.error(`[hydrate] Error parsing props for component ${s}`,this.getAttribute("props"),r),r}let u;await this.hydrator(this)(this.Component,p,n,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),this.dispatchEvent(new CustomEvent("astro:hydrate"))});d(this,"unmount",()=>{this.isConnected||this.dispatchEvent(new CustomEvent("astro:unmount"))})}disconnectedCallback(){document.removeEventListener("astro:after-swap",this.unmount),document.addEventListener("astro:after-swap",this.unmount,{once:!0})}connectedCallback(){if(!this.hasAttribute("await-children")||document.readyState==="interactive"||document.readyState==="complete")this.childrenConnectedCallback();else{let e=()=>{document.removeEventListener("DOMContentLoaded",e),c.disconnect(),this.childrenConnectedCallback()},c=new MutationObserver(()=>{var n;((n=this.lastChild)==null?void 0:n.nodeType)===Node.COMMENT_NODE&&this.lastChild.nodeValue==="astro:end"&&(this.lastChild.remove(),e())});c.observe(this,{childList:!0}),document.addEventListener("DOMContentLoaded",e)}}async childrenConnectedCallback(){let e=this.getAttribute("before-hydration-url");e&&await import(e),this.start()}async start(){let e=JSON.parse(this.getAttribute("opts")),c=this.getAttribute("client");if(Astro[c]===void 0){window.addEventListener(`astro:${c}`,()=>this.start(),{once:!0});return}try{await Astro[c](async()=>{let n=this.getAttribute("renderer-url"),[h,{default:p}]=await Promise.all([import(this.getAttribute("component-url")),n?import(n):()=>()=>{}]),u=this.getAttribute("component-export")||"default";if(!u.includes("."))this.Component=h[u];else{this.Component=h;for(let f of u.split("."))this.Component=this.Component[f]}return this.hydrator=p,this.hydrate},e,this)}catch(n){console.error(`[astro-island] Error hydrating ${this.getAttribute("component-url")}`,n)}}attributeChangedCallback(){this.hydrate()}}d(y,"observedAttributes",["props"]),customElements.get("astro-island")||customElements.define("astro-island",y)}})();</script><astro-island uid="1ddy8K" component-url="/__pyric/ui/_astro/chess-showcase.CZe_3fJZ.js" component-export="ChessShowcase" renderer-url="/__pyric/ui/_astro/client.D2ORs4B-.js" props="{}" ssr client="only" opts="{"name":"ChessShowcase","value":"react"}"></astro-island> </body></html>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
</script><link rel="preconnect" href="https://use.typekit.net" crossorigin><link rel="stylesheet" href="https://use.typekit.net/jwb3ycu.css"><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet"><link rel="stylesheet" href="/__pyric/ui/_astro/_studio_.CzyEuvVS.css">
|
|
7
7
|
<style>html{scrollbar-gutter:stable}
|
|
8
8
|
</style>
|
|
9
|
-
<link rel="stylesheet" href="/__pyric/ui/_astro/app.8TtSeQQX.css"></head> <body> <div id="root" data-astro-cid-w5sm6wlz> <style>astro-island,astro-slot,astro-static-slot{display:contents}</style><script>(()=>{var e=async t=>{await(await t())()};(self.Astro||(self.Astro={})).only=e;window.dispatchEvent(new Event("astro:only"));})();</script><script>(()=>{var A=Object.defineProperty;var g=(i,o,a)=>o in i?A(i,o,{enumerable:!0,configurable:!0,writable:!0,value:a}):i[o]=a;var d=(i,o,a)=>g(i,typeof o!="symbol"?o+"":o,a);{let i={0:t=>m(t),1:t=>a(t),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(a(t)),5:t=>new Set(a(t)),6:t=>BigInt(t),7:t=>new URL(t),8:t=>new Uint8Array(t),9:t=>new Uint16Array(t),10:t=>new Uint32Array(t),11:t=>1/0*t},o=t=>{let[l,e]=t;return l in i?i[l](e):void 0},a=t=>t.map(o),m=t=>typeof t!="object"||t===null?t:Object.fromEntries(Object.entries(t).map(([l,e])=>[l,o(e)]));class y extends HTMLElement{constructor(){super(...arguments);d(this,"Component");d(this,"hydrator");d(this,"hydrate",async()=>{var b;if(!this.hydrator||!this.isConnected)return;let e=(b=this.parentElement)==null?void 0:b.closest("astro-island[ssr]");if(e){e.addEventListener("astro:hydrate",this.hydrate,{once:!0});return}let c=this.querySelectorAll("astro-slot"),n={},h=this.querySelectorAll("template[data-astro-template]");for(let r of h){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("data-astro-template")||"default"]=r.innerHTML,r.remove())}for(let r of c){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("name")||"default"]=r.innerHTML)}let p;try{p=this.hasAttribute("props")?m(JSON.parse(this.getAttribute("props"))):{}}catch(r){let s=this.getAttribute("component-url")||"<unknown>",v=this.getAttribute("component-export");throw v&&(s+=` (export ${v})`),console.error(`[hydrate] Error parsing props for component ${s}`,this.getAttribute("props"),r),r}let u;await this.hydrator(this)(this.Component,p,n,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),this.dispatchEvent(new CustomEvent("astro:hydrate"))});d(this,"unmount",()=>{this.isConnected||this.dispatchEvent(new CustomEvent("astro:unmount"))})}disconnectedCallback(){document.removeEventListener("astro:after-swap",this.unmount),document.addEventListener("astro:after-swap",this.unmount,{once:!0})}connectedCallback(){if(!this.hasAttribute("await-children")||document.readyState==="interactive"||document.readyState==="complete")this.childrenConnectedCallback();else{let e=()=>{document.removeEventListener("DOMContentLoaded",e),c.disconnect(),this.childrenConnectedCallback()},c=new MutationObserver(()=>{var n;((n=this.lastChild)==null?void 0:n.nodeType)===Node.COMMENT_NODE&&this.lastChild.nodeValue==="astro:end"&&(this.lastChild.remove(),e())});c.observe(this,{childList:!0}),document.addEventListener("DOMContentLoaded",e)}}async childrenConnectedCallback(){let e=this.getAttribute("before-hydration-url");e&&await import(e),this.start()}async start(){let e=JSON.parse(this.getAttribute("opts")),c=this.getAttribute("client");if(Astro[c]===void 0){window.addEventListener(`astro:${c}`,()=>this.start(),{once:!0});return}try{await Astro[c](async()=>{let n=this.getAttribute("renderer-url"),[h,{default:p}]=await Promise.all([import(this.getAttribute("component-url")),n?import(n):()=>()=>{}]),u=this.getAttribute("component-export")||"default";if(!u.includes("."))this.Component=h[u];else{this.Component=h;for(let f of u.split("."))this.Component=this.Component[f]}return this.hydrator=p,this.hydrate},e,this)}catch(n){console.error(`[astro-island] Error hydrating ${this.getAttribute("component-url")}`,n)}}attributeChangedCallback(){this.hydrate()}}d(y,"observedAttributes",["props"]),customElements.get("astro-island")||customElements.define("astro-island",y)}})();</script><astro-island uid="b1btC" component-url="/__pyric/ui/_astro/app.
|
|
9
|
+
<link rel="stylesheet" href="/__pyric/ui/_astro/app.8TtSeQQX.css"></head> <body> <div id="root" data-astro-cid-w5sm6wlz> <style>astro-island,astro-slot,astro-static-slot{display:contents}</style><script>(()=>{var e=async t=>{await(await t())()};(self.Astro||(self.Astro={})).only=e;window.dispatchEvent(new Event("astro:only"));})();</script><script>(()=>{var A=Object.defineProperty;var g=(i,o,a)=>o in i?A(i,o,{enumerable:!0,configurable:!0,writable:!0,value:a}):i[o]=a;var d=(i,o,a)=>g(i,typeof o!="symbol"?o+"":o,a);{let i={0:t=>m(t),1:t=>a(t),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(a(t)),5:t=>new Set(a(t)),6:t=>BigInt(t),7:t=>new URL(t),8:t=>new Uint8Array(t),9:t=>new Uint16Array(t),10:t=>new Uint32Array(t),11:t=>1/0*t},o=t=>{let[l,e]=t;return l in i?i[l](e):void 0},a=t=>t.map(o),m=t=>typeof t!="object"||t===null?t:Object.fromEntries(Object.entries(t).map(([l,e])=>[l,o(e)]));class y extends HTMLElement{constructor(){super(...arguments);d(this,"Component");d(this,"hydrator");d(this,"hydrate",async()=>{var b;if(!this.hydrator||!this.isConnected)return;let e=(b=this.parentElement)==null?void 0:b.closest("astro-island[ssr]");if(e){e.addEventListener("astro:hydrate",this.hydrate,{once:!0});return}let c=this.querySelectorAll("astro-slot"),n={},h=this.querySelectorAll("template[data-astro-template]");for(let r of h){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("data-astro-template")||"default"]=r.innerHTML,r.remove())}for(let r of c){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("name")||"default"]=r.innerHTML)}let p;try{p=this.hasAttribute("props")?m(JSON.parse(this.getAttribute("props"))):{}}catch(r){let s=this.getAttribute("component-url")||"<unknown>",v=this.getAttribute("component-export");throw v&&(s+=` (export ${v})`),console.error(`[hydrate] Error parsing props for component ${s}`,this.getAttribute("props"),r),r}let u;await this.hydrator(this)(this.Component,p,n,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),this.dispatchEvent(new CustomEvent("astro:hydrate"))});d(this,"unmount",()=>{this.isConnected||this.dispatchEvent(new CustomEvent("astro:unmount"))})}disconnectedCallback(){document.removeEventListener("astro:after-swap",this.unmount),document.addEventListener("astro:after-swap",this.unmount,{once:!0})}connectedCallback(){if(!this.hasAttribute("await-children")||document.readyState==="interactive"||document.readyState==="complete")this.childrenConnectedCallback();else{let e=()=>{document.removeEventListener("DOMContentLoaded",e),c.disconnect(),this.childrenConnectedCallback()},c=new MutationObserver(()=>{var n;((n=this.lastChild)==null?void 0:n.nodeType)===Node.COMMENT_NODE&&this.lastChild.nodeValue==="astro:end"&&(this.lastChild.remove(),e())});c.observe(this,{childList:!0}),document.addEventListener("DOMContentLoaded",e)}}async childrenConnectedCallback(){let e=this.getAttribute("before-hydration-url");e&&await import(e),this.start()}async start(){let e=JSON.parse(this.getAttribute("opts")),c=this.getAttribute("client");if(Astro[c]===void 0){window.addEventListener(`astro:${c}`,()=>this.start(),{once:!0});return}try{await Astro[c](async()=>{let n=this.getAttribute("renderer-url"),[h,{default:p}]=await Promise.all([import(this.getAttribute("component-url")),n?import(n):()=>()=>{}]),u=this.getAttribute("component-export")||"default";if(!u.includes("."))this.Component=h[u];else{this.Component=h;for(let f of u.split("."))this.Component=this.Component[f]}return this.hydrator=p,this.hydrate},e,this)}catch(n){console.error(`[astro-island] Error hydrating ${this.getAttribute("component-url")}`,n)}}attributeChangedCallback(){this.hydrate()}}d(y,"observedAttributes",["props"]),customElements.get("astro-island")||customElements.define("astro-island",y)}})();</script><astro-island uid="b1btC" component-url="/__pyric/ui/_astro/app.DpzsBfos.js" component-export="StudioApp" renderer-url="/__pyric/ui/_astro/client.D2ORs4B-.js" props="{"data-astro-cid-w5sm6wlz":[0,true]}" ssr client="only" opts="{"name":"StudioApp","value":"react"}" await-children><p class="studio-loading" aria-live="polite" data-astro-cid-w5sm6wlz>
|
|
10
10
|
Starting Pyric Studio…
|
|
11
11
|
</p><template data-astro-template="fallback"><p class="studio-loading" aria-live="polite" data-astro-cid-w5sm6wlz>
|
|
12
12
|
Starting Pyric Studio…
|