@pyric/cli 0.1.0-alpha.25 → 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/rtdb/inspect-deployed-rules.d.ts +3 -17
- package/dist/rtdb/inspect-deployed-rules.d.ts.map +1 -1
- package/dist/rtdb/inspect-deployed-rules.js +4 -54
- package/dist/rtdb/inspect-deployed-rules.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/index.json +35 -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/pyric-rules-reference-api/index.html +372 -201
- package/dist/serve/site-ui/docs/pyric-rules-reference-api.md +148 -9
- package/dist/serve/site-ui/docs/secure/rtdb-rules-in-typescript/index.html +56 -1
- package/dist/serve/site-ui/docs/secure/rtdb-rules-in-typescript.md +33 -0
- 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/rtdb/inspect-deployed-rules.ts +5 -77
- 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
|
@@ -2,6 +2,51 @@
|
|
|
2
2
|
|
|
3
3
|
## Classes
|
|
4
4
|
|
|
5
|
+
<a id="rtdbexpressionsyntaxerror"></a>
|
|
6
|
+
|
|
7
|
+
### RtdbExpressionSyntaxError
|
|
8
|
+
|
|
9
|
+
Why [canonicalRtdbExpression](#canonicalrtdbexpression) could not read its input.
|
|
10
|
+
|
|
11
|
+
#### Extends
|
|
12
|
+
|
|
13
|
+
- `Error`
|
|
14
|
+
|
|
15
|
+
#### Constructors
|
|
16
|
+
|
|
17
|
+
<a id="constructor"></a>
|
|
18
|
+
|
|
19
|
+
##### Constructor
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
new RtdbExpressionSyntaxError(raw: string, message: string): RtdbExpressionSyntaxError;
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
###### Parameters
|
|
26
|
+
|
|
27
|
+
| Parameter | Type |
|
|
28
|
+
| :------ | :------ |
|
|
29
|
+
| `raw` | `string` |
|
|
30
|
+
| `message` | `string` |
|
|
31
|
+
|
|
32
|
+
###### Returns
|
|
33
|
+
|
|
34
|
+
[`RtdbExpressionSyntaxError`](#rtdbexpressionsyntaxerror)
|
|
35
|
+
|
|
36
|
+
###### Overrides
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
Error.constructor
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
#### Properties
|
|
43
|
+
|
|
44
|
+
| Property | Modifier | Type |
|
|
45
|
+
| :------ | :------ | :------ |
|
|
46
|
+
| <a id="raw"></a> `raw` | `readonly` | `string` |
|
|
47
|
+
|
|
48
|
+
***
|
|
49
|
+
|
|
5
50
|
<a id="rulesassertionerror"></a>
|
|
6
51
|
|
|
7
52
|
### RulesAssertionError
|
|
@@ -17,7 +62,7 @@ without extra wiring.
|
|
|
17
62
|
|
|
18
63
|
#### Constructors
|
|
19
64
|
|
|
20
|
-
<a id="constructor"></a>
|
|
65
|
+
<a id="constructor-1"></a>
|
|
21
66
|
|
|
22
67
|
##### Constructor
|
|
23
68
|
|
|
@@ -57,7 +102,7 @@ caller can surface them without re-parsing.
|
|
|
57
102
|
|
|
58
103
|
#### Constructors
|
|
59
104
|
|
|
60
|
-
<a id="constructor-
|
|
105
|
+
<a id="constructor-2"></a>
|
|
61
106
|
|
|
62
107
|
##### Constructor
|
|
63
108
|
|
|
@@ -106,7 +151,7 @@ fail on a known simulator gap.
|
|
|
106
151
|
|
|
107
152
|
#### Constructors
|
|
108
153
|
|
|
109
|
-
<a id="constructor-
|
|
154
|
+
<a id="constructor-3"></a>
|
|
110
155
|
|
|
111
156
|
##### Constructor
|
|
112
157
|
|
|
@@ -158,6 +203,18 @@ The outcome of running one Firestore case through `simulate`. Never a
|
|
|
158
203
|
|
|
159
204
|
***
|
|
160
205
|
|
|
206
|
+
<a id="diffrtdbrulesoptions"></a>
|
|
207
|
+
|
|
208
|
+
### DiffRtdbRulesOptions
|
|
209
|
+
|
|
210
|
+
#### Properties
|
|
211
|
+
|
|
212
|
+
| Property | Type | Description |
|
|
213
|
+
| :------ | :------ | :------ |
|
|
214
|
+
| <a id="ignoreequivalent"></a> `ignoreEquivalent?` | `boolean` | Treat two `.read`, `.write` or `.validate` expressions as the same rule when their [canonicalRtdbExpression](#canonicalrtdbexpression) texts are equal: spellings that cannot change any decision, such as `===` for `==` or `child('a/' + $id)` for `child('a').child($id)`. A changed entry still reports both expressions as written. An expression that does not parse is compared as written. Default `false`: expressions compare as text. |
|
|
215
|
+
|
|
216
|
+
***
|
|
217
|
+
|
|
161
218
|
<a id="evaluatedruleinfo"></a>
|
|
162
219
|
|
|
163
220
|
### EvaluatedRuleInfo
|
|
@@ -664,6 +721,26 @@ a file system.
|
|
|
664
721
|
|
|
665
722
|
***
|
|
666
723
|
|
|
724
|
+
<a id="rtdbrulechange"></a>
|
|
725
|
+
|
|
726
|
+
### RtdbRuleChange
|
|
727
|
+
|
|
728
|
+
One rule expression that differs. `deployed` is absent for `added` and
|
|
729
|
+
`local` is absent for `removed`: the change is what deploying the local
|
|
730
|
+
ruleset would do to the deployed one.
|
|
731
|
+
|
|
732
|
+
#### Properties
|
|
733
|
+
|
|
734
|
+
| Property | Type |
|
|
735
|
+
| :------ | :------ |
|
|
736
|
+
| <a id="change"></a> `change` | `"added"` \| `"removed"` \| `"changed"` |
|
|
737
|
+
| <a id="deployed"></a> `deployed?` | `string` |
|
|
738
|
+
| <a id="kind-3"></a> `kind` | [`RtdbRuleKind`](#rtdbrulekind) |
|
|
739
|
+
| <a id="local"></a> `local?` | `string` |
|
|
740
|
+
| <a id="path-2"></a> `path` | `string` |
|
|
741
|
+
|
|
742
|
+
***
|
|
743
|
+
|
|
667
744
|
<a id="rtdbrulecoverage"></a>
|
|
668
745
|
|
|
669
746
|
### RtdbRuleCoverage
|
|
@@ -678,9 +755,9 @@ a file system.
|
|
|
678
755
|
| <a id="evaluated"></a> `evaluated` | `number` | Evaluations that produced a verdict: `allow + deny + error`. |
|
|
679
756
|
| <a id="expression-1"></a> `expression` | `string` | The rule expression, or the declared index list for `indexOn`. |
|
|
680
757
|
| <a id="file-3"></a> `file?` | `string` | The rules file holding the node, when the summary covers several files. |
|
|
681
|
-
| <a id="kind-
|
|
758
|
+
| <a id="kind-4"></a> `kind` | [`RtdbCoverageKind`](#rtdbcoveragekind) | - |
|
|
682
759
|
| <a id="line-2"></a> `line?` | `number` | 1-indexed line in the rules file, when a line lookup was supplied. |
|
|
683
|
-
| <a id="path-
|
|
760
|
+
| <a id="path-3"></a> `path` | `string` | The rule node's path, such as `/rooms/$roomId`. |
|
|
684
761
|
| <a id="status"></a> `status` | [`RtdbCoverageStatus`](#rtdbcoveragestatus) | - |
|
|
685
762
|
| <a id="unsupported-5"></a> `unsupported` | `number` | - |
|
|
686
763
|
|
|
@@ -926,7 +1003,7 @@ throws when `firebase.json` deploys none to the default instance.
|
|
|
926
1003
|
| :------ | :------ |
|
|
927
1004
|
| <a id="code"></a> `code` | `string` |
|
|
928
1005
|
| <a id="message"></a> `message` | `string` |
|
|
929
|
-
| <a id="path-
|
|
1006
|
+
| <a id="path-4"></a> `path` | `string` |
|
|
930
1007
|
| <a id="rule-2"></a> `rule` | [`RtdbRulesFindingRule`](#rtdbrulesfindingrule-1) |
|
|
931
1008
|
|
|
932
1009
|
***
|
|
@@ -993,7 +1070,7 @@ returns an empty `trace` and surfaces the wire text on `TestResult.notes`.
|
|
|
993
1070
|
| <a id="line-4"></a> `line?` | `number` | 1-indexed source line, when known. |
|
|
994
1071
|
| <a id="message-2"></a> `message` | `string` | Human-readable description. |
|
|
995
1072
|
| <a id="origin"></a> `origin` | [`RuleIssueOrigin`](#ruleissueorigin-1) | - |
|
|
996
|
-
| <a id="path-
|
|
1073
|
+
| <a id="path-5"></a> `path?` | `string` | Rules path the issue applies to, when known (e.g. `'/users/{uid}'`). |
|
|
997
1074
|
| <a id="rule-3"></a> `rule?` | `".read"` \| `".write"` \| `".validate"` | The Realtime Database rule at `path` the issue applies to. Absent for Firestore issues and for an RTDB issue about the whole ruleset. |
|
|
998
1075
|
| <a id="severity"></a> `severity` | [`RuleIssueSeverity`](#ruleissueseverity-1) | - |
|
|
999
1076
|
|
|
@@ -1009,7 +1086,7 @@ Context passed to the callback overload of ruleset().
|
|
|
1009
1086
|
|
|
1010
1087
|
| Property | Type |
|
|
1011
1088
|
| :------ | :------ |
|
|
1012
|
-
| <a id="path-
|
|
1089
|
+
| <a id="path-6"></a> `path` | (`path`: `string`, `def`: [`PathDef`](#pathdef)) => `void` |
|
|
1013
1090
|
|
|
1014
1091
|
***
|
|
1015
1092
|
|
|
@@ -1035,7 +1112,7 @@ expression the request was evaluating when the budget ran out.
|
|
|
1035
1112
|
| Property | Type |
|
|
1036
1113
|
| :------ | :------ |
|
|
1037
1114
|
| <a id="column-2"></a> `column?` | `number` |
|
|
1038
|
-
| <a id="kind-
|
|
1115
|
+
| <a id="kind-5"></a> `kind` | `"expressions"` \| `"document-lookups"` |
|
|
1039
1116
|
| <a id="limit"></a> `limit` | `number` |
|
|
1040
1117
|
| <a id="line-5"></a> `line?` | `number` |
|
|
1041
1118
|
| <a id="message-3"></a> `message` | `string` |
|
|
@@ -1208,6 +1285,16 @@ type RtdbRuleEvaluation = z.infer<typeof RtdbRuleEvaluationSchema>;
|
|
|
1208
1285
|
|
|
1209
1286
|
***
|
|
1210
1287
|
|
|
1288
|
+
<a id="rtdbrulekind"></a>
|
|
1289
|
+
|
|
1290
|
+
### RtdbRuleKind
|
|
1291
|
+
|
|
1292
|
+
```ts
|
|
1293
|
+
type RtdbRuleKind = ".read" | ".write" | ".validate" | ".indexOn";
|
|
1294
|
+
```
|
|
1295
|
+
|
|
1296
|
+
***
|
|
1297
|
+
|
|
1211
1298
|
<a id="rtdbrulesfindingrule-1"></a>
|
|
1212
1299
|
|
|
1213
1300
|
### RtdbRulesFindingRule
|
|
@@ -2363,6 +2450,31 @@ A `bytes` value.
|
|
|
2363
2450
|
|
|
2364
2451
|
***
|
|
2365
2452
|
|
|
2453
|
+
<a id="canonicalrtdbexpression"></a>
|
|
2454
|
+
|
|
2455
|
+
### canonicalRtdbExpression()
|
|
2456
|
+
|
|
2457
|
+
```ts
|
|
2458
|
+
function canonicalRtdbExpression(raw: string): string;
|
|
2459
|
+
```
|
|
2460
|
+
|
|
2461
|
+
Canonical text of an RTDB rule expression: two expressions with equal
|
|
2462
|
+
canonical texts evaluate identically for every request. Deterministic and
|
|
2463
|
+
idempotent. Throws [RtdbExpressionSyntaxError](#rtdbexpressionsyntaxerror) for text that is not a
|
|
2464
|
+
rule expression.
|
|
2465
|
+
|
|
2466
|
+
#### Parameters
|
|
2467
|
+
|
|
2468
|
+
| Parameter | Type |
|
|
2469
|
+
| :------ | :------ |
|
|
2470
|
+
| `raw` | `string` |
|
|
2471
|
+
|
|
2472
|
+
#### Returns
|
|
2473
|
+
|
|
2474
|
+
`string`
|
|
2475
|
+
|
|
2476
|
+
***
|
|
2477
|
+
|
|
2366
2478
|
<a id="definertdbrules"></a>
|
|
2367
2479
|
|
|
2368
2480
|
### defineRtdbRules()
|
|
@@ -2383,6 +2495,33 @@ function defineRtdbRules(definition: RtdbRulesDefinition): RtdbRulesDocument;
|
|
|
2383
2495
|
|
|
2384
2496
|
***
|
|
2385
2497
|
|
|
2498
|
+
<a id="diffrtdbrules"></a>
|
|
2499
|
+
|
|
2500
|
+
### diffRtdbRules()
|
|
2501
|
+
|
|
2502
|
+
```ts
|
|
2503
|
+
function diffRtdbRules(
|
|
2504
|
+
deployed: Record<string, unknown>,
|
|
2505
|
+
local: Record<string, unknown>,
|
|
2506
|
+
options?: DiffRtdbRulesOptions): RtdbRuleChange[];
|
|
2507
|
+
```
|
|
2508
|
+
|
|
2509
|
+
Node-level diff of two `rules` trees, ordered by path then rule kind.
|
|
2510
|
+
|
|
2511
|
+
#### Parameters
|
|
2512
|
+
|
|
2513
|
+
| Parameter | Type |
|
|
2514
|
+
| :------ | :------ |
|
|
2515
|
+
| `deployed` | `Record`\<`string`, `unknown`\> |
|
|
2516
|
+
| `local` | `Record`\<`string`, `unknown`\> |
|
|
2517
|
+
| `options?` | [`DiffRtdbRulesOptions`](#diffrtdbrulesoptions) |
|
|
2518
|
+
|
|
2519
|
+
#### Returns
|
|
2520
|
+
|
|
2521
|
+
[`RtdbRuleChange`](#rtdbrulechange)[]
|
|
2522
|
+
|
|
2523
|
+
***
|
|
2524
|
+
|
|
2386
2525
|
<a id="duration"></a>
|
|
2387
2526
|
|
|
2388
2527
|
### duration()
|
|
@@ -68,6 +68,61 @@ html{scrollbar-gutter:stable}
|
|
|
68
68
|
<li><code>pyric database rules validate database.rules.json</code> prints the error findings only. It exits <code>2</code> when there is one or when the file is not rules JSON, <code>1</code> when the file can’t be read, and <code>0</code> otherwise.</li>
|
|
69
69
|
<li><code>pyric rules simulate --service database --rules-file database.rules.json --operation read --path /notes/n1</code> evaluates one request against the file.</li>
|
|
70
70
|
</ul>
|
|
71
|
+
<h2 id="compare-compiled-rules-with-the-rules-you-wrote-by-hand">Compare compiled rules with the rules you wrote by hand</h2>
|
|
72
|
+
<p>When you move hand-written rules to TypeScript, the compiled JSON spells the same rules differently: <code>auth != null</code> where you wrote <code>auth.uid !== null</code>, <code>child('a').child($id)</code> where you wrote <code>child('a/' + $id)</code>. <code>diffRtdbRules</code> with <code>ignoreEquivalent</code> reports only the rules that can decide differently:</p>
|
|
73
|
+
<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">import</span><span style="color:#ABB2BF"> { </span><span style="color:#E06C75">readFileSync</span><span style="color:#ABB2BF"> } </span><span style="color:#C678DD">from</span><span style="color:#98C379"> 'node:fs'</span><span style="color:#ABB2BF">;</span></span>
|
|
74
|
+
<span class="line"><span style="color:#C678DD">import</span><span style="color:#ABB2BF"> { </span><span style="color:#E06C75">diffRtdbRules</span><span style="color:#ABB2BF">, </span><span style="color:#E06C75">canonicalRtdbExpression</span><span style="color:#ABB2BF"> } </span><span style="color:#C678DD">from</span><span style="color:#98C379"> 'pyric/rules'</span><span style="color:#ABB2BF">;</span></span>
|
|
75
|
+
<span class="line"></span>
|
|
76
|
+
<span class="line"><span style="color:#C678DD">const</span><span style="color:#E5C07B"> handWritten</span><span style="color:#56B6C2"> =</span><span style="color:#E5C07B"> JSON</span><span style="color:#ABB2BF">.</span><span style="color:#61AFEF">parse</span><span style="color:#ABB2BF">(</span><span style="color:#61AFEF">readFileSync</span><span style="color:#ABB2BF">(</span><span style="color:#98C379">'legacy/database.rules.json'</span><span style="color:#ABB2BF">, </span><span style="color:#98C379">'utf8'</span><span style="color:#ABB2BF">)).</span><span style="color:#E06C75">rules</span><span style="color:#ABB2BF">;</span></span>
|
|
77
|
+
<span class="line"><span style="color:#C678DD">const</span><span style="color:#E5C07B"> compiled</span><span style="color:#56B6C2"> =</span><span style="color:#E5C07B"> rules</span><span style="color:#ABB2BF">.</span><span style="color:#61AFEF">toJSON</span><span style="color:#ABB2BF">().</span><span style="color:#E06C75">rules</span><span style="color:#ABB2BF">;</span></span>
|
|
78
|
+
<span class="line"></span>
|
|
79
|
+
<span class="line"><span style="color:#61AFEF">diffRtdbRules</span><span style="color:#ABB2BF">(</span><span style="color:#E06C75">handWritten</span><span style="color:#ABB2BF">, </span><span style="color:#E06C75">compiled</span><span style="color:#ABB2BF">); </span><span style="color:#7F848E;font-style:italic">// every rule whose text differs</span></span>
|
|
80
|
+
<span class="line"><span style="color:#61AFEF">diffRtdbRules</span><span style="color:#ABB2BF">(</span><span style="color:#E06C75">handWritten</span><span style="color:#ABB2BF">, </span><span style="color:#E06C75">compiled</span><span style="color:#ABB2BF">, { </span><span style="color:#E06C75">ignoreEquivalent</span><span style="color:#ABB2BF">: </span><span style="color:#D19A66">true</span><span style="color:#ABB2BF"> }); </span><span style="color:#7F848E;font-style:italic">// only rules that can decide differently</span></span>
|
|
81
|
+
<span class="line"><span style="color:#7F848E;font-style:italic">// → [{ path, kind: '.read' | '.write' | '.validate' | '.indexOn', change: 'added' | 'removed' | 'changed', deployed?, local? }]</span></span>
|
|
82
|
+
<span class="line"></span>
|
|
83
|
+
<span class="line"><span style="color:#61AFEF">canonicalRtdbExpression</span><span style="color:#ABB2BF">(</span><span style="color:#98C379">"$uid === auth.uid"</span><span style="color:#ABB2BF">); </span><span style="color:#7F848E;font-style:italic">// "$uid == auth.uid"</span></span>
|
|
84
|
+
<span class="line"><span style="color:#61AFEF">canonicalRtdbExpression</span><span style="color:#ABB2BF">(</span><span style="color:#98C379">"auth.uid !== null"</span><span style="color:#ABB2BF">); </span><span style="color:#7F848E;font-style:italic">// "!(auth == null)"</span></span></code></pre>
|
|
85
|
+
<p>A changed entry keeps both expressions exactly as written. Without the option the diff compares text.</p>
|
|
86
|
+
<p>Two expressions are equivalent when their canonical texts are equal. The canonical form applies only rewrites that cannot change a decision, errors included (a rule that errors fails, and <code>||</code> or <code>!</code> does not rescue it):</p>
|
|
87
|
+
<div class="table-scroll">
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
<table><thead><tr><th>Treated as the same</th><th>Why</th></tr></thead><tbody><tr><td>whitespace, grouping parentheses, <code>a && (b && c)</code> and <code>a && b && c</code></td><td>parentheses only shape the tree; <code>&&</code> and <code>||</code> evaluate left to right either way</td></tr><tr><td><code>"a"</code> and <code>'a'</code>, for a string literal written without a backslash</td><td>the same string; a literal with any escape (<code>'\d'</code>, <code>'\u0041'</code>) keeps its spelling, since production’s reading of most escapes has not been observed</td></tr><tr><td><code>===</code> and <code>==</code>, <code>!==</code> and <code>!=</code></td><td>RTDB compares without type conversion in both</td></tr><tr><td><code>auth.uid == null</code> and <code>auth == null</code></td><td><code>auth</code> is null exactly when signed out, and a signed-in <code>auth</code> always has a uid</td></tr><tr><td><code>child('a/b/' + $id + '/c')</code> and <code>child('a').child('b').child($id).child('c')</code></td><td>a <code>$</code> variable is exactly one key, and <code>child()</code> reads <code>/</code> as a separator</td></tr><tr><td><code>a != b</code> and <code>!(a == b)</code></td><td>both fail on the same inputs; otherwise <code>==</code> gives a boolean</td></tr><tr><td><code>a == b</code> and <code>b == a</code></td><td>equality is symmetric; operands that could fail other than with a rule error, or that index into an array, keep their order</td></tr></tbody></table></div>
|
|
125
|
+
<p>Deliberately not treated as the same: a concatenated part that is not a string literal or a <code>$</code> variable, <code>auth.uid</code> included (<code>child('m/' + auth.uid)</code> is not <code>child('m').child(auth.uid)</code>, because production has not been observed reading a uid such as <code>/x</code> or <code>a//b</code> with empty segments the same way in both forms); a literal glued to a variable (<code>'user_' + $id</code>) or written with an escape; empty segments and key-invalid characters in a path; the order of <code>&&</code> and <code>||</code> operands; <code>>=</code> and <code>></code>; double negation; and arithmetic or constant folding. A rule that falls in one of those is reported as changed, so read those entries yourself.</p>
|
|
71
126
|
<h2 id="turn-enforcement-from-a-deployed-game">Turn enforcement, from a deployed game</h2>
|
|
72
127
|
<p>This turn-enforcement example is built from the same parts:</p>
|
|
73
128
|
<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">import</span><span style="color:#ABB2BF"> {</span></span>
|
|
@@ -93,4 +148,4 @@ html{scrollbar-gutter:stable}
|
|
|
93
148
|
<h2 id="and-from-an-agent">And from an agent</h2>
|
|
94
149
|
<p>An agent authors and checks the same way you do: lint and simulate locally (<code>pyric rules lint --service database</code>, <code>pyric rules simulate --service database</code>, or <code>rtdb_simulate_access</code> against the connected sandbox), generate JSON with <code>pyric database rules generate</code>, then ship with <code>firebase-tools</code>. See the RTDB task in <a href="../../agent/work-with-an-agent/">Work with an agent</a>.</p>
|
|
95
150
|
<h2 id="where-to-go-next">Where to go next</h2>
|
|
96
|
-
<p>The full builder catalog, with the exact expression each one produces, is in the <a href="../../pyric-database-reference-api/">constraints reference</a>. For matches, turns, results, counters, presence, rate limits and validated shapes, compose the <a href="../rtdb-rules-standard-library/">RTDB rules standard library</a> instead of writing the expressions yourself. For the data side of RTDB, see <a href="../../build/realtime-database/">Sync realtime data</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="#define-the-rules-in-code" data-astro-cid-mw7aashj>Define the rules in code</a> </li><li data-depth="2" data-astro-cid-mw7aashj> <a href="#check-it-before-anything-runs" data-astro-cid-mw7aashj>Check it before anything runs</a> </li><li data-depth="2" data-astro-cid-mw7aashj> <a href="#simulate-a-request" data-astro-cid-mw7aashj>Simulate a request</a> </li><li data-depth="2" data-astro-cid-mw7aashj> <a href="#ship-the-json" data-astro-cid-mw7aashj>Ship the JSON</a> </li><li data-depth="2" data-astro-cid-mw7aashj> <a href="#turn-enforcement-from-a-deployed-game" data-astro-cid-mw7aashj>Turn enforcement, from a deployed game</a> </li><li data-depth="2" data-astro-cid-mw7aashj> <a href="#and-from-an-agent" data-astro-cid-mw7aashj>And from an agent</a> </li><li data-depth="2" data-astro-cid-mw7aashj> <a href="#where-to-go-next" data-astro-cid-mw7aashj>Where to go next</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>
|
|
151
|
+
<p>The full builder catalog, with the exact expression each one produces, is in the <a href="../../pyric-database-reference-api/">constraints reference</a>. For matches, turns, results, counters, presence, rate limits and validated shapes, compose the <a href="../rtdb-rules-standard-library/">RTDB rules standard library</a> instead of writing the expressions yourself. For the data side of RTDB, see <a href="../../build/realtime-database/">Sync realtime data</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="#define-the-rules-in-code" data-astro-cid-mw7aashj>Define the rules in code</a> </li><li data-depth="2" data-astro-cid-mw7aashj> <a href="#check-it-before-anything-runs" data-astro-cid-mw7aashj>Check it before anything runs</a> </li><li data-depth="2" data-astro-cid-mw7aashj> <a href="#simulate-a-request" data-astro-cid-mw7aashj>Simulate a request</a> </li><li data-depth="2" data-astro-cid-mw7aashj> <a href="#ship-the-json" data-astro-cid-mw7aashj>Ship the JSON</a> </li><li data-depth="2" data-astro-cid-mw7aashj> <a href="#compare-compiled-rules-with-the-rules-you-wrote-by-hand" data-astro-cid-mw7aashj>Compare compiled rules with the rules you wrote by hand</a> </li><li data-depth="2" data-astro-cid-mw7aashj> <a href="#turn-enforcement-from-a-deployed-game" data-astro-cid-mw7aashj>Turn enforcement, from a deployed game</a> </li><li data-depth="2" data-astro-cid-mw7aashj> <a href="#and-from-an-agent" data-astro-cid-mw7aashj>And from an agent</a> </li><li data-depth="2" data-astro-cid-mw7aashj> <a href="#where-to-go-next" data-astro-cid-mw7aashj>Where to go next</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>
|
|
@@ -82,6 +82,39 @@ The CLI checks the JSON file itself, so CI can gate on it without TypeScript in
|
|
|
82
82
|
- `pyric database rules validate database.rules.json` prints the error findings only. It exits `2` when there is one or when the file is not rules JSON, `1` when the file can't be read, and `0` otherwise.
|
|
83
83
|
- `pyric rules simulate --service database --rules-file database.rules.json --operation read --path /notes/n1` evaluates one request against the file.
|
|
84
84
|
|
|
85
|
+
## Compare compiled rules with the rules you wrote by hand
|
|
86
|
+
|
|
87
|
+
When you move hand-written rules to TypeScript, the compiled JSON spells the same rules differently: `auth != null` where you wrote `auth.uid !== null`, `child('a').child($id)` where you wrote `child('a/' + $id)`. `diffRtdbRules` with `ignoreEquivalent` reports only the rules that can decide differently:
|
|
88
|
+
```ts
|
|
89
|
+
import { readFileSync } from 'node:fs';
|
|
90
|
+
import { diffRtdbRules, canonicalRtdbExpression } from 'pyric/rules';
|
|
91
|
+
|
|
92
|
+
const handWritten = JSON.parse(readFileSync('legacy/database.rules.json', 'utf8')).rules;
|
|
93
|
+
const compiled = rules.toJSON().rules;
|
|
94
|
+
|
|
95
|
+
diffRtdbRules(handWritten, compiled); // every rule whose text differs
|
|
96
|
+
diffRtdbRules(handWritten, compiled, { ignoreEquivalent: true }); // only rules that can decide differently
|
|
97
|
+
// → [{ path, kind: '.read' | '.write' | '.validate' | '.indexOn', change: 'added' | 'removed' | 'changed', deployed?, local? }]
|
|
98
|
+
|
|
99
|
+
canonicalRtdbExpression("$uid === auth.uid"); // "$uid == auth.uid"
|
|
100
|
+
canonicalRtdbExpression("auth.uid !== null"); // "!(auth == null)"
|
|
101
|
+
```
|
|
102
|
+
A changed entry keeps both expressions exactly as written. Without the option the diff compares text.
|
|
103
|
+
|
|
104
|
+
Two expressions are equivalent when their canonical texts are equal. The canonical form applies only rewrites that cannot change a decision, errors included (a rule that errors fails, and `||` or `!` does not rescue it):
|
|
105
|
+
|
|
106
|
+
| Treated as the same | Why |
|
|
107
|
+
| --- | --- |
|
|
108
|
+
| whitespace, grouping parentheses, `a && (b && c)` and `a && b && c` | parentheses only shape the tree; `&&` and `\|\|` evaluate left to right either way |
|
|
109
|
+
| `"a"` and `'a'`, for a string literal written without a backslash | the same string; a literal with any escape (`'\d'`, `'\u0041'`) keeps its spelling, since production's reading of most escapes has not been observed |
|
|
110
|
+
| `===` and `==`, `!==` and `!=` | RTDB compares without type conversion in both |
|
|
111
|
+
| `auth.uid == null` and `auth == null` | `auth` is null exactly when signed out, and a signed-in `auth` always has a uid |
|
|
112
|
+
| `child('a/b/' + $id + '/c')` and `child('a').child('b').child($id).child('c')` | a `$` variable is exactly one key, and `child()` reads `/` as a separator |
|
|
113
|
+
| `a != b` and `!(a == b)` | both fail on the same inputs; otherwise `==` gives a boolean |
|
|
114
|
+
| `a == b` and `b == a` | equality is symmetric; operands that could fail other than with a rule error, or that index into an array, keep their order |
|
|
115
|
+
|
|
116
|
+
Deliberately not treated as the same: a concatenated part that is not a string literal or a `$` variable, `auth.uid` included (`child('m/' + auth.uid)` is not `child('m').child(auth.uid)`, because production has not been observed reading a uid such as `/x` or `a//b` with empty segments the same way in both forms); a literal glued to a variable (`'user_' + $id`) or written with an escape; empty segments and key-invalid characters in a path; the order of `&&` and `||` operands; `>=` and `>`; double negation; and arithmetic or constant folding. A rule that falls in one of those is reported as changed, so read those entries yourself.
|
|
117
|
+
|
|
85
118
|
## Turn enforcement, from a deployed game
|
|
86
119
|
|
|
87
120
|
This turn-enforcement example is built from the same parts:
|
|
@@ -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…
|
|
@@ -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…
|
|
@@ -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…
|