@ttt-productions/repo-guards 0.1.37 → 0.1.38
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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client-guards.d.ts","sourceRoot":"","sources":["../../../src/modules/firestore/client-guards.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAEzE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAqB,KAAK,GAAG,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"client-guards.d.ts","sourceRoot":"","sources":["../../../src/modules/firestore/client-guards.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAEzE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAqB,KAAK,GAAG,EAAE,MAAM,wBAAwB,CAAC;AAsBrE,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,QAAQ,GAAG,GAAG,EAAE,CAkC1D;AAED,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,SAAS,QAAQ,EAAE,CAAC;CAChC;AAED,eAAO,MAAM,qBAAqB,EAAE,sBAAsB,CAAC,qBAAqB,CA2C/E,CAAC;AAEF,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7B,UAAU,EAAE,SAAS,QAAQ,EAAE,CAAC;CACjC;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,QAAQ,GAAG,GAAG,EAAE,CAoB7D;AAED,eAAO,MAAM,yBAAyB,EAAE,sBAAsB,CAAC,yBAAyB,CA+CvF,CAAC;AAWF,MAAM,WAAW,0BAA0B;IACzC,UAAU,EAAE,SAAS,QAAQ,EAAE,CAAC;CACjC;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,QAAQ,GAAG,GAAG,EAAE,CAwB9D;AAED,eAAO,MAAM,0BAA0B,EAAE,sBAAsB,CAAC,0BAA0B,CAoCzF,CAAC"}
|
|
@@ -6,6 +6,18 @@ import { moduleImports } from '../../support/imports.js';
|
|
|
6
6
|
import { isUnder } from '../../support/text.js';
|
|
7
7
|
const FIRESTORE_SDK = /^firebase\/firestore(?:\/|$)/;
|
|
8
8
|
const APP_SOURCE = { roots: ['app'], kinds: ['code'], tests: false };
|
|
9
|
+
function propertyName(node) {
|
|
10
|
+
if (ts.isShorthandPropertyAssignment(node))
|
|
11
|
+
return node.name.text;
|
|
12
|
+
if (ts.isPropertyAssignment(node) || ts.isMethodDeclaration(node))
|
|
13
|
+
return ts.isIdentifier(node.name) || ts.isStringLiteral(node.name) ? node.name.text : null;
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
// An object holding `read` beside `subscribe` is an external store source (query-core's
|
|
17
|
+
// `LiveSessionSource`); no Firestore read hook takes a `read` option.
|
|
18
|
+
function isStoreSource(object) {
|
|
19
|
+
return object.properties.some((property) => propertyName(property) === 'read');
|
|
20
|
+
}
|
|
9
21
|
export function liveSubscriptionHits(file) {
|
|
10
22
|
const source = file.sourceFile;
|
|
11
23
|
const snapshotNames = new Set(['onSnapshot']);
|
|
@@ -23,11 +35,14 @@ export function liveSubscriptionHits(file) {
|
|
|
23
35
|
const at = (node, detail) => hits.push({ index: node.getStart(source), detail });
|
|
24
36
|
forEachNode(source, (node) => {
|
|
25
37
|
if (ts.isPropertyAssignment(node) && ts.isObjectLiteralExpression(node.parent)) {
|
|
26
|
-
|
|
27
|
-
|
|
38
|
+
if (propertyName(node) === 'subscribe' &&
|
|
39
|
+
node.initializer.kind !== ts.SyntaxKind.FalseKeyword &&
|
|
40
|
+
!isStoreSource(node.parent))
|
|
28
41
|
at(node, `subscribe: ${node.initializer.getText(source)}`);
|
|
29
42
|
}
|
|
30
|
-
else if (ts.isShorthandPropertyAssignment(node) &&
|
|
43
|
+
else if (ts.isShorthandPropertyAssignment(node) &&
|
|
44
|
+
node.name.text === 'subscribe' &&
|
|
45
|
+
!isStoreSource(node.parent)) {
|
|
31
46
|
at(node, '{ subscribe }');
|
|
32
47
|
}
|
|
33
48
|
else if (ts.isCallExpression(node)) {
|
|
@@ -55,7 +70,8 @@ export const liveSubscriptionGuard = {
|
|
|
55
70
|
description: 'A live read bills a document read on every change for as long as it is mounted, so it is a reviewed exception: ' +
|
|
56
71
|
'a file that declares live mode — a subscribe option set to anything but a literal false, the { subscribe } ' +
|
|
57
72
|
"shorthand, or an onSnapshot call under any name the file bound the Firestore SDK's to — is on the reviewed " +
|
|
58
|
-
'list with why it needs real-time delivery.
|
|
73
|
+
'list with why it needs real-time delivery. An object holding read beside subscribe is a store source (a ' +
|
|
74
|
+
'session feed), not a read option, and is not live mode. Fix: read once and keep it fresh through the cache, or add the ' +
|
|
59
75
|
'reviewed entry. An entry whose file no longer subscribes is stale.',
|
|
60
76
|
scope: APP_SOURCE,
|
|
61
77
|
match: liveSubscriptionHits,
|
|
@@ -73,6 +89,8 @@ export const liveSubscriptionGuard = {
|
|
|
73
89
|
'useFirestoreDoc({ docPath, queryKey, subscribe: false });',
|
|
74
90
|
'const unsubscribe = cache.subscribe((event) => handle(event));',
|
|
75
91
|
'return useSyncExternalStore(subscribe, readValue, getServerValue);',
|
|
92
|
+
'const LIVE_SESSION: LiveSessionSource = { read: liveUid, subscribe: subscribeSession };',
|
|
93
|
+
'<MutationSessionProvider liveSession={{ read, subscribe }} />;',
|
|
76
94
|
'// subscribe: true is a reviewed exception\nconst x = 1;',
|
|
77
95
|
],
|
|
78
96
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client-guards.js","sourceRoot":"","sources":["../../../src/modules/firestore/client-guards.ts"],"names":[],"mappings":"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","sourcesContent":["import ts from 'typescript';\nimport type { GenericGuardDefinition } from '../../node/suite/config.js';\nimport type { Scope } from '../../node/reader.js';\nimport type { Reviewed } from '../../node/layout.js';\nimport type { FileView } from '../../reader/view.js';\nimport { reviewedAllowlist, type Hit } from '../../shapes/shapes.js';\nimport { forEachNode } from '../../support/ast.js';\nimport { namesProblem, reviewedProblems } from '../../support/facts.js';\nimport { moduleImports } from '../../support/imports.js';\nimport { isUnder } from '../../support/text.js';\n\nconst FIRESTORE_SDK = /^firebase\\/firestore(?:\\/|$)/;\nconst APP_SOURCE: Scope = { roots: ['app'], kinds: ['code'], tests: false };\n\nexport function liveSubscriptionHits(file: FileView): Hit[] {\n const source = file.sourceFile;\n const snapshotNames = new Set(['onSnapshot']);\n const namespaces = new Set<string>();\n for (const site of moduleImports(source, ['import'])) {\n if (!site.topLevel || !FIRESTORE_SDK.test(site.spec)) continue;\n if (site.namespace) namespaces.add(site.namespace);\n for (const name of site.named)\n if (name.imported === 'onSnapshot') snapshotNames.add(name.local);\n }\n const hits: Hit[] = [];\n const at = (node: ts.Node, detail: string) => hits.push({ index: node.getStart(source), detail });\n forEachNode(source, (node) => {\n if (ts.isPropertyAssignment(node) && ts.isObjectLiteralExpression(node.parent)) {\n const key =\n ts.isIdentifier(node.name) || ts.isStringLiteral(node.name) ? node.name.text : null;\n if (key === 'subscribe' && node.initializer.kind !== ts.SyntaxKind.FalseKeyword)\n at(node, `subscribe: ${node.initializer.getText(source)}`);\n } else if (ts.isShorthandPropertyAssignment(node) && node.name.text === 'subscribe') {\n at(node, '{ subscribe }');\n } else if (ts.isCallExpression(node)) {\n const callee = node.expression;\n if (ts.isIdentifier(callee) && snapshotNames.has(callee.text)) at(node, `${callee.text}(…)`);\n else if (ts.isPropertyAccessExpression(callee) && callee.name.text === 'onSnapshot')\n at(node, `${callee.getText(source)}(…)`);\n }\n });\n return hits;\n}\n\nexport interface LiveSubscriptionFacts {\n allowlist: readonly Reviewed[];\n}\n\nexport const liveSubscriptionGuard: GenericGuardDefinition<LiveSubscriptionFacts> = {\n id: 'firestoreLiveSubscriptions',\n absentWhen: 'the app stores nothing in Firestore',\n factProblems(facts) {\n const problems: string[] = [];\n reviewedProblems(facts?.allowlist, 'allowlist', problems);\n return problems;\n },\n build(facts) {\n return [\n reviewedAllowlist({\n title: 'every live Firestore subscription is on the reviewed list',\n description:\n 'A live read bills a document read on every change for as long as it is mounted, so it is a reviewed exception: ' +\n 'a file that declares live mode — a subscribe option set to anything but a literal false, the { subscribe } ' +\n \"shorthand, or an onSnapshot call under any name the file bound the Firestore SDK's to — is on the reviewed \" +\n 'list with why it needs real-time delivery. Fix: read once and keep it fresh through the cache, or add the ' +\n 'reviewed entry. An entry whose file no longer subscribes is stale.',\n scope: APP_SOURCE,\n match: liveSubscriptionHits,\n allowlist: facts.allowlist,\n samples: {\n flags: [\n 'useFirestoreDoc({ docPath, queryKey, subscribe: true });',\n 'useBatchFirestoreDocs({ ids, subscribe: isLive });',\n 'useFirestoreCollection({ collectionPath, subscribe, enabled });',\n \"import { doc, onSnapshot as listen } from 'firebase/firestore';\\nconst stop = listen(doc(db, p), render);\",\n \"import * as fs from 'firebase/firestore';\\nconst stop = fs.onSnapshot(ref, handle);\",\n ],\n passes: [\n \"useBatchFirestoreDocs({ db, collectionPath, ids, transport: 'get' });\",\n 'useFirestoreDoc({ docPath, queryKey, subscribe: false });',\n 'const unsubscribe = cache.subscribe((event) => handle(event));',\n 'return useSyncExternalStore(subscribe, readValue, getServerValue);',\n '// subscribe: true is a reviewed exception\\nconst x = 1;',\n ],\n },\n }),\n ];\n },\n};\n\nexport interface FirestoreRuntimeInUiFacts {\n uiFolders: readonly string[];\n sanctioned: readonly Reviewed[];\n}\n\nexport function firestoreRuntimeImports(file: FileView): Hit[] {\n const source = file.sourceFile;\n const hits: Hit[] = [];\n for (const site of moduleImports(source, ['import', 'export', 'dynamic'])) {\n if (!FIRESTORE_SDK.test(site.spec)) continue;\n const at = site.node.getStart(source);\n if (site.kind === 'import') {\n const typeOnly =\n !site.sideEffect &&\n (site.typeOnly ||\n (!site.defaultName &&\n !site.namespace &&\n site.named.length > 0 &&\n site.named.every((n) => n.typeOnly)));\n if (!typeOnly) hits.push({ index: at, detail: `import from '${site.spec}'` });\n } else if (site.kind === 'export') {\n if (!site.typeOnly) hits.push({ index: at, detail: `re-export from '${site.spec}'` });\n } else hits.push({ index: at, detail: `import('${site.spec}')` });\n }\n return hits;\n}\n\nexport const firestoreRuntimeInUiGuard: GenericGuardDefinition<FirestoreRuntimeInUiFacts> = {\n id: 'firestoreRuntimeInUi',\n absentWhen: 'the app stores nothing in Firestore',\n factProblems(facts) {\n const problems: string[] = [];\n namesProblem(facts?.uiFolders, 'uiFolders', problems);\n reviewedProblems(facts?.sanctioned, 'sanctioned', problems);\n return problems;\n },\n build(facts) {\n const inUi = (path: string) => facts.uiFolders.some((folder) => isUnder(path, folder));\n return [\n reviewedAllowlist({\n title: 'no component or route imports the Firestore runtime',\n description:\n 'A component or route that reaches into Firestore bypasses the cache, the query keys, and every server check ' +\n 'at once. Flagged: a runtime import, re-export, or dynamic import of the Firestore SDK (a type-only import ' +\n 'passes) in the UI folders. Fix: read through a hook, write through a callable. A sanctioned root provider is ' +\n 'a reviewed entry with its why; one that no longer imports the runtime is stale.',\n scope: APP_SOURCE,\n match: (file) => (inUi(file.path) ? firestoreRuntimeImports(file) : []),\n allowlist: facts.sanctioned,\n samples: {\n flags: [\n {\n path: `${facts.uiFolders[0]}/probe.tsx`,\n text: \"import { doc, getDoc } from 'firebase/firestore';\",\n },\n {\n path: `${facts.uiFolders[0]}/probe.tsx`,\n text: 'import {\\n updateDoc,\\n} from \"firebase/firestore\";',\n },\n ],\n passes: [\n {\n path: `${facts.uiFolders[0]}/typed.tsx`,\n text: \"import type { Timestamp } from 'firebase/firestore';\",\n },\n {\n path: `${facts.uiFolders[0]}/typed.tsx`,\n text: \"import { type Timestamp } from 'firebase/firestore';\",\n },\n ],\n },\n }),\n ];\n },\n};\n\nconst CLIENT_WRITES = new Set([\n 'setDoc',\n 'updateDoc',\n 'deleteDoc',\n 'addDoc',\n 'writeBatch',\n 'runTransaction',\n]);\n\nexport interface FirestoreClientWritesFacts {\n sanctioned: readonly Reviewed[];\n}\n\nexport function firestoreClientWriteHits(file: FileView): Hit[] {\n const source = file.sourceFile;\n const hits: Hit[] = [];\n const namespaces = new Set<string>();\n for (const site of moduleImports(source, ['import', 'export'])) {\n if (!FIRESTORE_SDK.test(site.spec) || site.typeOnly) continue;\n if (site.namespace) namespaces.add(site.namespace);\n for (const name of site.named)\n if (!name.typeOnly && CLIENT_WRITES.has(name.imported))\n hits.push({\n index: site.node.getStart(source),\n detail: `${name.imported} from '${site.spec}'`,\n });\n }\n forEachNode(source, (node) => {\n if (\n ts.isPropertyAccessExpression(node) &&\n ts.isIdentifier(node.expression) &&\n namespaces.has(node.expression.text) &&\n CLIENT_WRITES.has(node.name.text)\n )\n hits.push({ index: node.getStart(source), detail: node.getText(source) });\n });\n return hits;\n}\n\nexport const firestoreClientWritesGuard: GenericGuardDefinition<FirestoreClientWritesFacts> = {\n id: 'firestoreClientWrites',\n absentWhen: 'the app stores nothing in Firestore',\n factProblems(facts) {\n const problems: string[] = [];\n reviewedProblems(facts?.sanctioned, 'sanctioned', problems);\n return problems;\n },\n build(facts) {\n return [\n reviewedAllowlist({\n title: 'the client never writes Firestore',\n description:\n 'Every write goes through a callable that checks it on the server; a client SDK write skips that check and ' +\n 'every rule the callable enforces. Flagged: a runtime import of a Firestore write function (' +\n `${[...CLIENT_WRITES].join(', ')}) from the Firestore SDK, or a call to one through a namespace import, in ` +\n \"the app's client source. Fix: call the callable that owns the write. A sanctioned site is a reviewed entry \" +\n 'with its why; one that no longer writes is stale.',\n scope: APP_SOURCE,\n match: firestoreClientWriteHits,\n allowlist: facts.sanctioned,\n samples: {\n flags: [\n \"import { doc, setDoc } from 'firebase/firestore';\",\n \"import { writeBatch as batchOf } from 'firebase/firestore';\",\n \"import * as fs from 'firebase/firestore';\\nawait fs.updateDoc(ref, patch);\",\n ],\n passes: [\n \"import { doc, getDoc, onSnapshot } from 'firebase/firestore';\",\n \"import { type WriteBatch } from 'firebase/firestore';\",\n 'await callable({ id });',\n ],\n },\n }),\n ];\n },\n};\n"]}
|
|
1
|
+
{"version":3,"file":"client-guards.js","sourceRoot":"","sources":["../../../src/modules/firestore/client-guards.ts"],"names":[],"mappings":"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","sourcesContent":["import ts from 'typescript';\nimport type { GenericGuardDefinition } from '../../node/suite/config.js';\nimport type { Scope } from '../../node/reader.js';\nimport type { Reviewed } from '../../node/layout.js';\nimport type { FileView } from '../../reader/view.js';\nimport { reviewedAllowlist, type Hit } from '../../shapes/shapes.js';\nimport { forEachNode } from '../../support/ast.js';\nimport { namesProblem, reviewedProblems } from '../../support/facts.js';\nimport { moduleImports } from '../../support/imports.js';\nimport { isUnder } from '../../support/text.js';\n\nconst FIRESTORE_SDK = /^firebase\\/firestore(?:\\/|$)/;\nconst APP_SOURCE: Scope = { roots: ['app'], kinds: ['code'], tests: false };\n\nfunction propertyName(node: ts.ObjectLiteralElementLike): string | null {\n if (ts.isShorthandPropertyAssignment(node)) return node.name.text;\n if (ts.isPropertyAssignment(node) || ts.isMethodDeclaration(node))\n return ts.isIdentifier(node.name) || ts.isStringLiteral(node.name) ? node.name.text : null;\n return null;\n}\n\n// An object holding `read` beside `subscribe` is an external store source (query-core's\n// `LiveSessionSource`); no Firestore read hook takes a `read` option.\nfunction isStoreSource(object: ts.ObjectLiteralExpression): boolean {\n return object.properties.some((property) => propertyName(property) === 'read');\n}\n\nexport function liveSubscriptionHits(file: FileView): Hit[] {\n const source = file.sourceFile;\n const snapshotNames = new Set(['onSnapshot']);\n const namespaces = new Set<string>();\n for (const site of moduleImports(source, ['import'])) {\n if (!site.topLevel || !FIRESTORE_SDK.test(site.spec)) continue;\n if (site.namespace) namespaces.add(site.namespace);\n for (const name of site.named)\n if (name.imported === 'onSnapshot') snapshotNames.add(name.local);\n }\n const hits: Hit[] = [];\n const at = (node: ts.Node, detail: string) => hits.push({ index: node.getStart(source), detail });\n forEachNode(source, (node) => {\n if (ts.isPropertyAssignment(node) && ts.isObjectLiteralExpression(node.parent)) {\n if (\n propertyName(node) === 'subscribe' &&\n node.initializer.kind !== ts.SyntaxKind.FalseKeyword &&\n !isStoreSource(node.parent)\n )\n at(node, `subscribe: ${node.initializer.getText(source)}`);\n } else if (\n ts.isShorthandPropertyAssignment(node) &&\n node.name.text === 'subscribe' &&\n !isStoreSource(node.parent)\n ) {\n at(node, '{ subscribe }');\n } else if (ts.isCallExpression(node)) {\n const callee = node.expression;\n if (ts.isIdentifier(callee) && snapshotNames.has(callee.text)) at(node, `${callee.text}(…)`);\n else if (ts.isPropertyAccessExpression(callee) && callee.name.text === 'onSnapshot')\n at(node, `${callee.getText(source)}(…)`);\n }\n });\n return hits;\n}\n\nexport interface LiveSubscriptionFacts {\n allowlist: readonly Reviewed[];\n}\n\nexport const liveSubscriptionGuard: GenericGuardDefinition<LiveSubscriptionFacts> = {\n id: 'firestoreLiveSubscriptions',\n absentWhen: 'the app stores nothing in Firestore',\n factProblems(facts) {\n const problems: string[] = [];\n reviewedProblems(facts?.allowlist, 'allowlist', problems);\n return problems;\n },\n build(facts) {\n return [\n reviewedAllowlist({\n title: 'every live Firestore subscription is on the reviewed list',\n description:\n 'A live read bills a document read on every change for as long as it is mounted, so it is a reviewed exception: ' +\n 'a file that declares live mode — a subscribe option set to anything but a literal false, the { subscribe } ' +\n \"shorthand, or an onSnapshot call under any name the file bound the Firestore SDK's to — is on the reviewed \" +\n 'list with why it needs real-time delivery. An object holding read beside subscribe is a store source (a ' +\n 'session feed), not a read option, and is not live mode. Fix: read once and keep it fresh through the cache, or add the ' +\n 'reviewed entry. An entry whose file no longer subscribes is stale.',\n scope: APP_SOURCE,\n match: liveSubscriptionHits,\n allowlist: facts.allowlist,\n samples: {\n flags: [\n 'useFirestoreDoc({ docPath, queryKey, subscribe: true });',\n 'useBatchFirestoreDocs({ ids, subscribe: isLive });',\n 'useFirestoreCollection({ collectionPath, subscribe, enabled });',\n \"import { doc, onSnapshot as listen } from 'firebase/firestore';\\nconst stop = listen(doc(db, p), render);\",\n \"import * as fs from 'firebase/firestore';\\nconst stop = fs.onSnapshot(ref, handle);\",\n ],\n passes: [\n \"useBatchFirestoreDocs({ db, collectionPath, ids, transport: 'get' });\",\n 'useFirestoreDoc({ docPath, queryKey, subscribe: false });',\n 'const unsubscribe = cache.subscribe((event) => handle(event));',\n 'return useSyncExternalStore(subscribe, readValue, getServerValue);',\n 'const LIVE_SESSION: LiveSessionSource = { read: liveUid, subscribe: subscribeSession };',\n '<MutationSessionProvider liveSession={{ read, subscribe }} />;',\n '// subscribe: true is a reviewed exception\\nconst x = 1;',\n ],\n },\n }),\n ];\n },\n};\n\nexport interface FirestoreRuntimeInUiFacts {\n uiFolders: readonly string[];\n sanctioned: readonly Reviewed[];\n}\n\nexport function firestoreRuntimeImports(file: FileView): Hit[] {\n const source = file.sourceFile;\n const hits: Hit[] = [];\n for (const site of moduleImports(source, ['import', 'export', 'dynamic'])) {\n if (!FIRESTORE_SDK.test(site.spec)) continue;\n const at = site.node.getStart(source);\n if (site.kind === 'import') {\n const typeOnly =\n !site.sideEffect &&\n (site.typeOnly ||\n (!site.defaultName &&\n !site.namespace &&\n site.named.length > 0 &&\n site.named.every((n) => n.typeOnly)));\n if (!typeOnly) hits.push({ index: at, detail: `import from '${site.spec}'` });\n } else if (site.kind === 'export') {\n if (!site.typeOnly) hits.push({ index: at, detail: `re-export from '${site.spec}'` });\n } else hits.push({ index: at, detail: `import('${site.spec}')` });\n }\n return hits;\n}\n\nexport const firestoreRuntimeInUiGuard: GenericGuardDefinition<FirestoreRuntimeInUiFacts> = {\n id: 'firestoreRuntimeInUi',\n absentWhen: 'the app stores nothing in Firestore',\n factProblems(facts) {\n const problems: string[] = [];\n namesProblem(facts?.uiFolders, 'uiFolders', problems);\n reviewedProblems(facts?.sanctioned, 'sanctioned', problems);\n return problems;\n },\n build(facts) {\n const inUi = (path: string) => facts.uiFolders.some((folder) => isUnder(path, folder));\n return [\n reviewedAllowlist({\n title: 'no component or route imports the Firestore runtime',\n description:\n 'A component or route that reaches into Firestore bypasses the cache, the query keys, and every server check ' +\n 'at once. Flagged: a runtime import, re-export, or dynamic import of the Firestore SDK (a type-only import ' +\n 'passes) in the UI folders. Fix: read through a hook, write through a callable. A sanctioned root provider is ' +\n 'a reviewed entry with its why; one that no longer imports the runtime is stale.',\n scope: APP_SOURCE,\n match: (file) => (inUi(file.path) ? firestoreRuntimeImports(file) : []),\n allowlist: facts.sanctioned,\n samples: {\n flags: [\n {\n path: `${facts.uiFolders[0]}/probe.tsx`,\n text: \"import { doc, getDoc } from 'firebase/firestore';\",\n },\n {\n path: `${facts.uiFolders[0]}/probe.tsx`,\n text: 'import {\\n updateDoc,\\n} from \"firebase/firestore\";',\n },\n ],\n passes: [\n {\n path: `${facts.uiFolders[0]}/typed.tsx`,\n text: \"import type { Timestamp } from 'firebase/firestore';\",\n },\n {\n path: `${facts.uiFolders[0]}/typed.tsx`,\n text: \"import { type Timestamp } from 'firebase/firestore';\",\n },\n ],\n },\n }),\n ];\n },\n};\n\nconst CLIENT_WRITES = new Set([\n 'setDoc',\n 'updateDoc',\n 'deleteDoc',\n 'addDoc',\n 'writeBatch',\n 'runTransaction',\n]);\n\nexport interface FirestoreClientWritesFacts {\n sanctioned: readonly Reviewed[];\n}\n\nexport function firestoreClientWriteHits(file: FileView): Hit[] {\n const source = file.sourceFile;\n const hits: Hit[] = [];\n const namespaces = new Set<string>();\n for (const site of moduleImports(source, ['import', 'export'])) {\n if (!FIRESTORE_SDK.test(site.spec) || site.typeOnly) continue;\n if (site.namespace) namespaces.add(site.namespace);\n for (const name of site.named)\n if (!name.typeOnly && CLIENT_WRITES.has(name.imported))\n hits.push({\n index: site.node.getStart(source),\n detail: `${name.imported} from '${site.spec}'`,\n });\n }\n forEachNode(source, (node) => {\n if (\n ts.isPropertyAccessExpression(node) &&\n ts.isIdentifier(node.expression) &&\n namespaces.has(node.expression.text) &&\n CLIENT_WRITES.has(node.name.text)\n )\n hits.push({ index: node.getStart(source), detail: node.getText(source) });\n });\n return hits;\n}\n\nexport const firestoreClientWritesGuard: GenericGuardDefinition<FirestoreClientWritesFacts> = {\n id: 'firestoreClientWrites',\n absentWhen: 'the app stores nothing in Firestore',\n factProblems(facts) {\n const problems: string[] = [];\n reviewedProblems(facts?.sanctioned, 'sanctioned', problems);\n return problems;\n },\n build(facts) {\n return [\n reviewedAllowlist({\n title: 'the client never writes Firestore',\n description:\n 'Every write goes through a callable that checks it on the server; a client SDK write skips that check and ' +\n 'every rule the callable enforces. Flagged: a runtime import of a Firestore write function (' +\n `${[...CLIENT_WRITES].join(', ')}) from the Firestore SDK, or a call to one through a namespace import, in ` +\n \"the app's client source. Fix: call the callable that owns the write. A sanctioned site is a reviewed entry \" +\n 'with its why; one that no longer writes is stale.',\n scope: APP_SOURCE,\n match: firestoreClientWriteHits,\n allowlist: facts.sanctioned,\n samples: {\n flags: [\n \"import { doc, setDoc } from 'firebase/firestore';\",\n \"import { writeBatch as batchOf } from 'firebase/firestore';\",\n \"import * as fs from 'firebase/firestore';\\nawait fs.updateDoc(ref, patch);\",\n ],\n passes: [\n \"import { doc, getDoc, onSnapshot } from 'firebase/firestore';\",\n \"import { type WriteBatch } from 'firebase/firestore';\",\n 'await callable({ id });',\n ],\n },\n }),\n ];\n },\n};\n"]}
|