@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
|
@@ -9,25 +9,14 @@
|
|
|
9
9
|
* URL, and it is not returned in any result or message.
|
|
10
10
|
*/
|
|
11
11
|
import type { ToolHandler } from 'pyric/sandbox/internal';
|
|
12
|
+
import { diffRtdbRules, type RtdbRuleChange } from 'pyric/rules/internal/rtdb';
|
|
12
13
|
import { parseRtdbRulesText } from './rules-json.js';
|
|
13
14
|
import { HOSTED_CREDENTIAL_SOURCES } from '../credentials/node/scope.js';
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
export
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* One rule expression that differs. `deployed` is absent for `added` and
|
|
21
|
-
* `local` is absent for `removed`: the change is what deploying the local
|
|
22
|
-
* ruleset would do to the deployed one.
|
|
23
|
-
*/
|
|
24
|
-
export interface RtdbRuleChange {
|
|
25
|
-
path: string;
|
|
26
|
-
kind: RtdbRuleKind;
|
|
27
|
-
change: 'added' | 'removed' | 'changed';
|
|
28
|
-
deployed?: string;
|
|
29
|
-
local?: string;
|
|
30
|
-
}
|
|
16
|
+
// The diff itself is pure and lives in `pyric` (public from `pyric/rules`);
|
|
17
|
+
// it is re-exported here so this module's exports stay as they were.
|
|
18
|
+
export { DIFFED_RULE_KINDS, diffRtdbRules } from 'pyric/rules/internal/rtdb';
|
|
19
|
+
export type { DiffRtdbRulesOptions, RtdbRuleChange, RtdbRuleKind } from 'pyric/rules/internal/rtdb';
|
|
31
20
|
|
|
32
21
|
export interface InspectDeployedRtdbRulesOptions {
|
|
33
22
|
scope: { resolveToken(): Promise<string> };
|
|
@@ -96,67 +85,6 @@ export async function inspectDeployedRtdbRules(
|
|
|
96
85
|
return { ok: true, deployed, diff: diffRtdbRules(deployed.rules, local.rules) };
|
|
97
86
|
}
|
|
98
87
|
|
|
99
|
-
/** Node-level diff of two `rules` trees, ordered by path then rule kind. */
|
|
100
|
-
export function diffRtdbRules(
|
|
101
|
-
deployed: Record<string, unknown>,
|
|
102
|
-
local: Record<string, unknown>,
|
|
103
|
-
): RtdbRuleChange[] {
|
|
104
|
-
const deployedRules = collectRules(deployed);
|
|
105
|
-
const localRules = collectRules(local);
|
|
106
|
-
const changes: RtdbRuleChange[] = [];
|
|
107
|
-
for (const key of new Set([...deployedRules.keys(), ...localRules.keys()])) {
|
|
108
|
-
const d = deployedRules.get(key);
|
|
109
|
-
const l = localRules.get(key);
|
|
110
|
-
if (d?.value === l?.value) continue;
|
|
111
|
-
const node = (d ?? l)!;
|
|
112
|
-
const change: RtdbRuleChange = {
|
|
113
|
-
path: node.path,
|
|
114
|
-
kind: node.kind,
|
|
115
|
-
change: d === undefined ? 'added' : l === undefined ? 'removed' : 'changed',
|
|
116
|
-
};
|
|
117
|
-
if (d !== undefined) change.deployed = d.value;
|
|
118
|
-
if (l !== undefined) change.local = l.value;
|
|
119
|
-
changes.push(change);
|
|
120
|
-
}
|
|
121
|
-
return changes.sort((a, b) => compare(a.path, b.path) || compare(a.kind, b.kind));
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const compare = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0);
|
|
125
|
-
|
|
126
|
-
interface CollectedRule {
|
|
127
|
-
path: string;
|
|
128
|
-
kind: RtdbRuleKind;
|
|
129
|
-
value: string;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function formatRuleValue(kind: RtdbRuleKind, value: unknown): string {
|
|
133
|
-
if (kind === '.indexOn') {
|
|
134
|
-
const normalized = typeof value === 'string' ? [value] : value;
|
|
135
|
-
return JSON.stringify(normalized);
|
|
136
|
-
}
|
|
137
|
-
// A boolean and its string form are one expression: `true` and "true".
|
|
138
|
-
return typeof value === 'string' ? value : JSON.stringify(value);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function collectRules(root: Record<string, unknown>): Map<string, CollectedRule> {
|
|
142
|
-
const out = new Map<string, CollectedRule>();
|
|
143
|
-
const walk = (node: Record<string, unknown>, path: string): void => {
|
|
144
|
-
for (const [key, child] of Object.entries(node)) {
|
|
145
|
-
if (key.startsWith('.')) {
|
|
146
|
-
if ((DIFFED_RULE_KINDS as readonly string[]).includes(key)) {
|
|
147
|
-
const kind = key as RtdbRuleKind;
|
|
148
|
-
const value = formatRuleValue(kind, child);
|
|
149
|
-
out.set(`${path}\u0000${kind}`, { path, kind, value });
|
|
150
|
-
}
|
|
151
|
-
} else if (typeof child === 'object' && child !== null && !Array.isArray(child)) {
|
|
152
|
-
walk(child as Record<string, unknown>, `${path === '/' ? '' : path}/${key}`);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
};
|
|
156
|
-
walk(root, '/');
|
|
157
|
-
return out;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
88
|
export interface RtdbInspectRulesToolDeps {
|
|
161
89
|
scope: { resolveToken(): Promise<string> };
|
|
162
90
|
databaseURL: string;
|
|
@@ -179,5 +179,6 @@ export function formatDenials(groups: DenialGroup[], loggedFrom?: number): strin
|
|
|
179
179
|
for (const reason of group.trace) lines.push(` trace: ${reason}`);
|
|
180
180
|
return lines.join('\n');
|
|
181
181
|
});
|
|
182
|
-
|
|
182
|
+
const tip = '\nTip: After changing rules, run `pyric verify` to replay the captured session against them and confirm your fixes.';
|
|
183
|
+
return `${bounded}${blocks.join('\n\n')}\n${tip}\n`;
|
|
183
184
|
}
|
|
@@ -2,6 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import type { ViteDevServer } from 'vite';
|
|
3
3
|
import { formatDatabaseRulesRemoved, formatStorageRulesRemoved } from './rules.js';
|
|
4
4
|
import type { SandboxSession } from './sandbox-session.js';
|
|
5
|
+
import { createVerifyHint } from './verify-hint.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Adapt Vite's watcher to the session's last-good rules reload operations.
|
|
@@ -17,6 +18,8 @@ export function watchViteGenerationRules(input: {
|
|
|
17
18
|
server: ViteDevServer;
|
|
18
19
|
session: SandboxSession;
|
|
19
20
|
}): () => void {
|
|
21
|
+
const getVerifyHint = createVerifyHint();
|
|
22
|
+
|
|
20
23
|
const { server, session } = input;
|
|
21
24
|
const databaseFiles = session.databaseRulesFiles();
|
|
22
25
|
const databaseFileByResolved = new Map(databaseFiles.map((file) => [path.resolve(file), file]));
|
|
@@ -53,7 +56,7 @@ export function watchViteGenerationRules(input: {
|
|
|
53
56
|
server.watcher.add([...session.firestoreRulesFiles()]);
|
|
54
57
|
const isReloaded = result.kind === 'reloaded';
|
|
55
58
|
if (isReloaded) {
|
|
56
|
-
server.config.logger.info(` ↻ [pyric] rules reloaded (${result.rulesHash})`);
|
|
59
|
+
server.config.logger.info(` ↻ [pyric] rules reloaded (${result.rulesHash})${getVerifyHint()}`);
|
|
57
60
|
if (result.fixtureCheck && result.fixtureCheck.ran > 0) {
|
|
58
61
|
const logFn = result.fixtureCheck.ok
|
|
59
62
|
? (m: string) => server.config.logger.info(m)
|
|
@@ -74,7 +77,7 @@ export function watchViteGenerationRules(input: {
|
|
|
74
77
|
void session.reloadDatabaseRules(databaseFile).then((results) => {
|
|
75
78
|
for (const { instance, result } of results) {
|
|
76
79
|
if (result.kind === 'reloaded') {
|
|
77
|
-
server.config.logger.info(` ↻ [pyric] rtdb rules reloaded for ${instance} (${result.rulesHash})`);
|
|
80
|
+
server.config.logger.info(` ↻ [pyric] rtdb rules reloaded for ${instance} (${result.rulesHash})${getVerifyHint()}`);
|
|
78
81
|
if (result.fixtureCheck && result.fixtureCheck.ran > 0) {
|
|
79
82
|
const logFn = result.fixtureCheck.ok
|
|
80
83
|
? (m: string) => server.config.logger.info(m)
|
|
@@ -94,7 +97,7 @@ export function watchViteGenerationRules(input: {
|
|
|
94
97
|
if (isStorageMatch) {
|
|
95
98
|
void session.reloadStorageRules().then((result) => {
|
|
96
99
|
if (result.kind === 'reloaded') {
|
|
97
|
-
server.config.logger.info(` ↻ [pyric] storage rules reloaded (${result.rulesHash})`);
|
|
100
|
+
server.config.logger.info(` ↻ [pyric] storage rules reloaded (${result.rulesHash})${getVerifyHint()}`);
|
|
98
101
|
} else if (result.kind === 'rejected') {
|
|
99
102
|
server.config.logger.warn(
|
|
100
103
|
` ⚠ [pyric] storage rules NOT reloaded (last-good stays live): ${result.error.message}`,
|
package/src/verify/index.ts
CHANGED
|
@@ -39,6 +39,8 @@ import {
|
|
|
39
39
|
VERIFY_FIXTURE_SCHEMA,
|
|
40
40
|
type PyricVerifyFixture,
|
|
41
41
|
} from './fixture.js';
|
|
42
|
+
import { runStagingInstance, StagingDeployRefusedError, withStagingInstance, type StagingRunHooks } from './staging/engine.js';
|
|
43
|
+
import type { RtdbRestClient } from './staging/rest.js';
|
|
42
44
|
import {
|
|
43
45
|
recordedRtdbInstances,
|
|
44
46
|
recordsDefaultRtdbInstance,
|
|
@@ -94,7 +96,7 @@ export {
|
|
|
94
96
|
} from './hosted-rules-test.js';
|
|
95
97
|
|
|
96
98
|
export type VerifiableService = 'firestore' | 'rtdb' | 'storage';
|
|
97
|
-
export type VerifyEngine = 'sandbox' | 'rulesTestApi';
|
|
99
|
+
export type VerifyEngine = 'sandbox' | 'rulesTestApi' | 'staging';
|
|
98
100
|
|
|
99
101
|
type RtdbCandidateRules = { rules: Record<string, unknown> } | RtdbRulesDocument;
|
|
100
102
|
|
|
@@ -120,6 +122,12 @@ export interface VerifyFixtureOptions {
|
|
|
120
122
|
expressionReportLevel?: ExpressionReportLevel;
|
|
121
123
|
};
|
|
122
124
|
caseDerivation?: Omit<DeriveRulesTestCasesOptions, 'service'>;
|
|
125
|
+
/**
|
|
126
|
+
* The staging engine's instance: a client for a dedicated RTDB instance
|
|
127
|
+
* enrolled with `pyric verify staging init`, and the project it is
|
|
128
|
+
* enrolled for. Required when `engines` includes `staging`.
|
|
129
|
+
*/
|
|
130
|
+
staging?: { client: RtdbRestClient; projectId: string } & StagingRunHooks;
|
|
123
131
|
}
|
|
124
132
|
|
|
125
133
|
export interface VerifyResult {
|
|
@@ -198,7 +206,10 @@ type VerifyDivergenceKind =
|
|
|
198
206
|
path?: string;
|
|
199
207
|
method?: string;
|
|
200
208
|
sandbox: string;
|
|
201
|
-
|
|
209
|
+
/** Firestore: the hosted Rules Test API's decision. */
|
|
210
|
+
rulesTestApi?: string;
|
|
211
|
+
/** RTDB: what production did on the staging instance. */
|
|
212
|
+
staging?: string;
|
|
202
213
|
reason?: string;
|
|
203
214
|
};
|
|
204
215
|
|
|
@@ -224,10 +235,15 @@ export async function verifyFixture(
|
|
|
224
235
|
|
|
225
236
|
const services: Partial<Record<VerifiableService, VerifyServiceResult>> = {};
|
|
226
237
|
for (const service of selected) {
|
|
238
|
+
if (engines.includes('staging') && service !== 'rtdb') {
|
|
239
|
+
throw new VerifyInputError(
|
|
240
|
+
`the staging engine checks RTDB only; ${service} is not supported by it yet. Select --service rtdb, or the sandbox engine for ${service}.`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
227
243
|
if (service === 'firestore') {
|
|
228
244
|
services.firestore = await verifyFirestore(fixture, requireFirestoreRules(opts.rules), engines, opts);
|
|
229
245
|
} else if (service === 'rtdb') {
|
|
230
|
-
services.rtdb = await verifyRtdb(fixture, opts.rules, engines);
|
|
246
|
+
services.rtdb = await verifyRtdb(fixture, opts.rules, engines, opts.staging);
|
|
231
247
|
} else if (service === 'storage') {
|
|
232
248
|
services.storage = await verifyStorage(fixture, opts.rules, engines);
|
|
233
249
|
} else {
|
|
@@ -578,19 +594,24 @@ async function verifyRtdb(
|
|
|
578
594
|
fixture: PyricVerifyFixture,
|
|
579
595
|
rules: VerifyRulesInput,
|
|
580
596
|
engines: VerifyEngine[],
|
|
597
|
+
staging: VerifyFixtureOptions['staging'],
|
|
581
598
|
): Promise<VerifyServiceResult> {
|
|
582
599
|
const rtdb = fixture.services.rtdb;
|
|
583
600
|
if (!rtdb) {
|
|
584
601
|
throw new VerifyInputError('fixture does not contain services.rtdb.');
|
|
585
602
|
}
|
|
586
603
|
if (engines.includes('rulesTestApi')) {
|
|
587
|
-
throw new VerifyInputError('rulesTestApi verification is Firestore-only; select --service firestore or use the sandbox engine for RTDB.');
|
|
604
|
+
throw new VerifyInputError('rulesTestApi verification is Firestore-only; select --service firestore or use the sandbox or staging engine for RTDB.');
|
|
605
|
+
}
|
|
606
|
+
if (engines.includes('staging') && staging === undefined) {
|
|
607
|
+
throw new VerifyInputError('the staging engine needs a staging instance (opts.staging).');
|
|
588
608
|
}
|
|
589
609
|
|
|
590
|
-
|
|
610
|
+
// The instances to check, each with its candidate rules.
|
|
611
|
+
const checks: Array<{ instance?: string; rules: RtdbRulesJson; capturedState: unknown }> = [];
|
|
591
612
|
const checksDefault = rules.rtdb !== undefined || recordsDefaultRtdbInstance(fixture);
|
|
592
613
|
if (checksDefault) {
|
|
593
|
-
|
|
614
|
+
checks.push({ rules: requireRtdbRules(rules.rtdb, 'missing candidate RTDB rules. Pass rules.rtdb.'), capturedState: rtdb.state.tree });
|
|
594
615
|
}
|
|
595
616
|
const skippedInstances: string[] = [];
|
|
596
617
|
for (const instance of recordedRtdbInstances(fixture)) {
|
|
@@ -600,28 +621,107 @@ async function verifyRtdb(
|
|
|
600
621
|
continue;
|
|
601
622
|
}
|
|
602
623
|
const parsed = requireRtdbRules(candidate, `missing candidate RTDB rules for instance "${instance}".`, instance);
|
|
603
|
-
|
|
624
|
+
checks.push({ instance, rules: parsed, capturedState: rtdb.instances?.[instance]?.state.tree });
|
|
604
625
|
}
|
|
605
626
|
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
627
|
+
const engineResults: Partial<Record<VerifyEngine, VerifyEngineResult>> = {};
|
|
628
|
+
if (engines.includes('sandbox')) {
|
|
629
|
+
const replays = [];
|
|
630
|
+
for (const check of checks) replays.push(await replayRtdbInstance(fixture, check.rules, check.instance));
|
|
631
|
+
const divergences = replays.flatMap((replayed) => replayed.divergences.map(mapRtdbReplayDivergence));
|
|
632
|
+
engineResults.sandbox = {
|
|
633
|
+
engine: 'sandbox',
|
|
634
|
+
ok: divergences.every(isInformational),
|
|
635
|
+
checkedEvents: replays.reduce((total, replayed) => total + replayed.checkedEvents, 0),
|
|
636
|
+
divergences,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
if (engines.includes('staging') && staging !== undefined) {
|
|
640
|
+
engineResults.staging = await verifyRtdbOnStaging(fixture, checks, staging);
|
|
641
|
+
}
|
|
642
|
+
if (engineResults.sandbox && engineResults.staging) {
|
|
643
|
+
addRtdbEngineDrift(engineResults.sandbox, engineResults.staging);
|
|
644
|
+
}
|
|
614
645
|
|
|
646
|
+
const divergences = collectEngineDivergences(engineResults);
|
|
615
647
|
return {
|
|
616
648
|
service: 'rtdb',
|
|
617
649
|
ok: divergences.every(isInformational),
|
|
618
|
-
checkedEvents,
|
|
650
|
+
checkedEvents: engineResults.sandbox?.checkedEvents ?? engineResults.staging?.checkedEvents ?? 0,
|
|
619
651
|
divergences,
|
|
620
|
-
engines:
|
|
652
|
+
engines: engineResults,
|
|
621
653
|
...(skippedInstances.length > 0 ? { skippedInstances } : {}),
|
|
622
654
|
};
|
|
623
655
|
}
|
|
624
656
|
|
|
657
|
+
/**
|
|
658
|
+
* Replay each instance on the staging instance in turn, each run guarded and
|
|
659
|
+
* the instance put back before the next. Production refusing to deploy an
|
|
660
|
+
* instance's candidate rules is reported for that instance; the others run.
|
|
661
|
+
*/
|
|
662
|
+
async function verifyRtdbOnStaging(
|
|
663
|
+
fixture: PyricVerifyFixture,
|
|
664
|
+
checks: Array<{ instance?: string; rules: RtdbRulesJson; capturedState: unknown }>,
|
|
665
|
+
staging: NonNullable<VerifyFixtureOptions['staging']>,
|
|
666
|
+
): Promise<VerifyEngineResult> {
|
|
667
|
+
const divergences: VerifyDivergence[] = [];
|
|
668
|
+
let checkedEvents = 0;
|
|
669
|
+
for (const check of checks) {
|
|
670
|
+
const tag = check.instance === undefined ? {} : { instance: check.instance };
|
|
671
|
+
try {
|
|
672
|
+
const { result } = await withStagingInstance(staging.client, staging.projectId, () =>
|
|
673
|
+
runStagingInstance({
|
|
674
|
+
client: staging.client,
|
|
675
|
+
events: fixture.events,
|
|
676
|
+
rules: check.rules,
|
|
677
|
+
capturedState: check.capturedState,
|
|
678
|
+
...(check.instance === undefined ? {} : { instance: check.instance }),
|
|
679
|
+
...(staging.signal ? { signal: staging.signal } : {}),
|
|
680
|
+
}), staging);
|
|
681
|
+
checkedEvents += result.checkedEvents;
|
|
682
|
+
divergences.push(...result.divergences.map((divergence) => ({ service: 'rtdb', ...divergence, ...tag })));
|
|
683
|
+
} catch (error) {
|
|
684
|
+
if (!(error instanceof StagingDeployRefusedError)) throw error;
|
|
685
|
+
divergences.push({ service: 'rtdb', kind: 'unsupported', reason: error.message, ...tag });
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return { engine: 'staging', ok: divergences.every(isInformational), checkedEvents, divergences };
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Where the sandbox and production decided a captured step differently: one
|
|
693
|
+
* engine reports `now-denied`, `now-allowed` or `unsupported` for a step and
|
|
694
|
+
* the other reports something else, or nothing. Steps are matched by their
|
|
695
|
+
* index in the replay both engines walk.
|
|
696
|
+
*/
|
|
697
|
+
function addRtdbEngineDrift(sandbox: VerifyEngineResult, staging: VerifyEngineResult): void {
|
|
698
|
+
const decisive = new Set(['now-denied', 'now-allowed', 'unsupported']);
|
|
699
|
+
const keyOf = (divergence: VerifyDivergence) =>
|
|
700
|
+
`${divergence.instance ?? ''}|${(divergence as { step?: number }).step ?? `${(divergence as { method?: string }).method}|${divergence.path}`}`;
|
|
701
|
+
const byKey = (divergences: VerifyDivergence[]) =>
|
|
702
|
+
new Map(divergences.filter((divergence) => decisive.has(divergence.kind)).map((divergence) => [keyOf(divergence), divergence]));
|
|
703
|
+
const local = byKey(sandbox.divergences);
|
|
704
|
+
const remote = byKey(staging.divergences);
|
|
705
|
+
for (const key of new Set([...local.keys(), ...remote.keys()])) {
|
|
706
|
+
const sandboxKind = local.get(key)?.kind ?? 'unchanged';
|
|
707
|
+
const stagingKind = remote.get(key)?.kind ?? 'unchanged';
|
|
708
|
+
if (sandboxKind === stagingKind) continue;
|
|
709
|
+
const sample = (local.get(key) ?? remote.get(key))!;
|
|
710
|
+
staging.divergences.push({
|
|
711
|
+
service: 'rtdb',
|
|
712
|
+
kind: 'engine-drift',
|
|
713
|
+
path: sample.path,
|
|
714
|
+
method: (sample as { method?: string }).method,
|
|
715
|
+
sandbox: sandboxKind,
|
|
716
|
+
staging: stagingKind,
|
|
717
|
+
reason: `the sandbox reported ${sandboxKind}; production reported ${stagingKind}.`,
|
|
718
|
+
...(sample.instance === undefined ? {} : { instance: sample.instance }),
|
|
719
|
+
...((sample as { step?: number }).step === undefined ? {} : { step: (sample as { step?: number }).step }),
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
staging.ok = staging.divergences.every(isInformational);
|
|
723
|
+
}
|
|
724
|
+
|
|
625
725
|
async function verifyStorage(
|
|
626
726
|
fixture: PyricVerifyFixture,
|
|
627
727
|
rules: VerifyRulesInput,
|
|
@@ -864,7 +964,7 @@ function normalizeEngines(input: VerifyEngine[] | undefined): VerifyEngine[] {
|
|
|
864
964
|
const engines = input ?? ['sandbox'];
|
|
865
965
|
const out: VerifyEngine[] = [];
|
|
866
966
|
for (const engine of engines) {
|
|
867
|
-
if (engine !== 'sandbox' && engine !== 'rulesTestApi') {
|
|
967
|
+
if (engine !== 'sandbox' && engine !== 'rulesTestApi' && engine !== 'staging') {
|
|
868
968
|
throw new VerifyInputError(`unsupported verify engine: ${String(engine)}`);
|
|
869
969
|
}
|
|
870
970
|
if (!out.includes(engine)) out.push(engine);
|
|
@@ -881,7 +981,7 @@ function collectEngineDivergences(
|
|
|
881
981
|
function serviceCheckedEvents(
|
|
882
982
|
engines: Partial<Record<VerifyEngine, VerifyEngineResult>>,
|
|
883
983
|
): number {
|
|
884
|
-
return engines.sandbox?.checkedEvents ?? engines.rulesTestApi?.checkedEvents ?? 0;
|
|
984
|
+
return engines.sandbox?.checkedEvents ?? engines.rulesTestApi?.checkedEvents ?? engines.staging?.checkedEvents ?? 0;
|
|
885
985
|
}
|
|
886
986
|
|
|
887
987
|
function mapUnsupportedEvent(event: VerifyUnsupportedEvent): VerifyDivergence {
|