@ttt-productions/repo-guards 0.1.31 → 0.1.32

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":"server-guards.d.ts","sourceRoot":"","sources":["../../../src/modules/firestore/server-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;AAIrD,OAAO,EAAe,KAAK,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvE,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,EAAE,CA6B/D;AAED,eAAO,MAAM,oBAAoB,EAAE,sBAAsB,CAAC,OAAO,CAkBhE,CAAC;AAOF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,EAAE,CAiB5D;AAED,eAAO,MAAM,qBAAqB,EAAE,sBAAsB,CAAC,OAAO,CAuBjE,CAAC;AAEF,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,SAAS,QAAQ,EAAE,CAAC;CACpC;AAED,eAAO,MAAM,eAAe,EAAE,sBAAsB,CAAC,eAAe,CA+BnE,CAAC;AASF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,EAAE,CAmB3D;AAED,eAAO,MAAM,iBAAiB,EAAE,sBAAsB,CAAC,OAAO,CAgB7D,CAAC"}
1
+ {"version":3,"file":"server-guards.d.ts","sourceRoot":"","sources":["../../../src/modules/firestore/server-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;AAIrD,OAAO,EAAe,KAAK,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,cAAc,CAAC;AAiEvE,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,EAAE,CA0B/D;AAED,eAAO,MAAM,oBAAoB,EAAE,sBAAsB,CAAC,OAAO,CAoBhE,CAAC;AAOF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,EAAE,CAiB5D;AAED,eAAO,MAAM,qBAAqB,EAAE,sBAAsB,CAAC,OAAO,CAuBjE,CAAC;AAEF,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,SAAS,QAAQ,EAAE,CAAC;CACpC;AAED,eAAO,MAAM,eAAe,EAAE,sBAAsB,CAAC,eAAe,CA+BnE,CAAC;AASF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,EAAE,CAmB3D;AAED,eAAO,MAAM,iBAAiB,EAAE,sBAAsB,CAAC,OAAO,CAgB7D,CAAC"}
@@ -1,19 +1,73 @@
1
1
  import ts from 'typescript';
2
2
  import { matchPattern, reviewedAllowlist } from '../../shapes/shapes.js';
3
- import { calleeName, forEachNode } from '../../support/ast.js';
3
+ import { calleeName, forEachNode, unwrap } from '../../support/ast.js';
4
4
  import { reviewedProblems, takesNoFacts } from '../../support/facts.js';
5
5
  import { bannedGuard } from './support.js';
6
+ const isFunctionNode = (node) => ts.isFunctionDeclaration(node) ||
7
+ ts.isFunctionExpression(node) ||
8
+ ts.isArrowFunction(node) ||
9
+ ts.isMethodDeclaration(node);
10
+ function isTransactionType(type) {
11
+ if (!type || !ts.isTypeReferenceNode(type))
12
+ return false;
13
+ const name = type.typeName;
14
+ return (ts.isIdentifier(name) ? name : name.right).text === 'Transaction';
15
+ }
16
+ function requiredTransactionParameter(fn) {
17
+ const param = fn.parameters.find((p) => !p.questionToken && !p.initializer && isTransactionType(p.type));
18
+ return param && ts.isIdentifier(param.name) ? param.name.text : undefined;
19
+ }
20
+ function namedFunction(source, name) {
21
+ let found;
22
+ forEachNode(source, (node) => {
23
+ if (found)
24
+ return;
25
+ if (ts.isFunctionDeclaration(node) && node.name?.text === name)
26
+ found = node;
27
+ else if (ts.isVariableDeclaration(node) &&
28
+ ts.isIdentifier(node.name) &&
29
+ node.name.text === name &&
30
+ node.initializer) {
31
+ const init = unwrap(node.initializer);
32
+ if (ts.isArrowFunction(init) || ts.isFunctionExpression(init))
33
+ found = init;
34
+ }
35
+ });
36
+ return found;
37
+ }
38
+ function transactionFunctions(source) {
39
+ const found = new Map();
40
+ forEachNode(source, (node) => {
41
+ if (ts.isCallExpression(node) && calleeName(node) === 'runTransaction') {
42
+ const arg = node.arguments[0] ? unwrap(node.arguments[0]) : undefined;
43
+ const callback = arg && (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg))
44
+ ? arg
45
+ : arg && ts.isIdentifier(arg)
46
+ ? namedFunction(source, arg.text)
47
+ : undefined;
48
+ if (callback && !found.has(callback)) {
49
+ const first = callback.parameters[0]?.name;
50
+ found.set(callback, first && ts.isIdentifier(first) ? first.text : undefined);
51
+ }
52
+ }
53
+ else if (isFunctionNode(node) && !found.has(node)) {
54
+ const transaction = requiredTransactionParameter(node);
55
+ if (transaction)
56
+ found.set(node, transaction);
57
+ }
58
+ });
59
+ return found;
60
+ }
6
61
  export function strayTransactionReads(file) {
7
62
  const source = file.sourceFile;
8
63
  const hits = [];
9
- forEachNode(source, (node) => {
10
- if (!ts.isCallExpression(node) || calleeName(node) !== 'runTransaction')
11
- return;
12
- const callback = node.arguments.find((arg) => ts.isArrowFunction(arg) || ts.isFunctionExpression(arg));
13
- if (!callback)
14
- return;
15
- const transaction = callback.parameters[0]?.name.getText(source);
64
+ const functions = transactionFunctions(source);
65
+ for (const [fn, transaction] of functions) {
66
+ if (!fn.body)
67
+ continue;
16
68
  const inspect = (inner) => {
69
+ if (functions.has(inner))
70
+ return;
17
71
  if (ts.isCallExpression(inner) && ts.isPropertyAccessExpression(inner.expression)) {
18
72
  const method = inner.expression.name.text;
19
73
  const receiver = inner.expression.expression.getText(source);
@@ -21,15 +75,15 @@ export function strayTransactionReads(file) {
21
75
  receiver !== transaction) {
22
76
  hits.push({
23
77
  node: inner,
24
- detail: `${receiver}.${method}(…) inside a runTransaction callback reads outside the transaction`,
78
+ detail: `${receiver}.${method}(…) inside a transaction's work reads outside the transaction`,
25
79
  });
26
80
  }
27
81
  }
28
82
  ts.forEachChild(inner, inspect);
29
83
  };
30
- ts.forEachChild(callback.body, inspect);
31
- });
32
- return hits;
84
+ ts.forEachChild(fn.body, inspect);
85
+ }
86
+ return hits.sort((a, b) => a.node.getStart(source) - b.node.getStart(source));
33
87
  }
34
88
  export const transactionReadGuard = {
35
89
  id: 'firestoreTransactionReads',
@@ -39,10 +93,12 @@ export const transactionReadGuard = {
39
93
  return [
40
94
  bannedGuard({
41
95
  title: 'a transaction callback reads only through its transaction',
42
- description: 'Inside an inline runTransaction callback, every Firestore read goes through the callback transaction ' +
43
- '(transaction.get(ref | query), transaction.getAll(…)). A plain ref.get() / query.get() / db.getAll(…) there is ' +
44
- 'outside the read set, so the write it decides can commit after the value changed. Fix: read through the ' +
45
- 'transaction. A read made before the transaction, and a callback passed by name, are left to review.',
96
+ description: 'In the work a transaction does, every Firestore read goes through that transaction (transaction.get(ref | query), ' +
97
+ 'transaction.getAll(…)). That work is a runTransaction callback — inline, or passed by name and declared in ' +
98
+ 'the same file — and every function with a required Transaction parameter. A plain ref.get() / query.get() / ' +
99
+ 'db.getAll(…) there is outside the read set, so the write it decides can commit after the value changed. Fix: ' +
100
+ 'read through the transaction. A function whose Transaction parameter is optional reads plainly when called ' +
101
+ 'without one and is left out, as is a read made before the transaction.',
46
102
  scope: { roots: ['functions'], kinds: ['code'], tests: false },
47
103
  match: strayTransactionReads,
48
104
  }),
@@ -1 +1 @@
1
- {"version":3,"file":"server-guards.js","sourceRoot":"","sources":["../../../src/modules/firestore/server-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 { matchPattern, reviewedAllowlist } from '../../shapes/shapes.js';\nimport { calleeName, forEachNode } from '../../support/ast.js';\nimport { reviewedProblems, takesNoFacts } from '../../support/facts.js';\nimport { bannedGuard, type NoFacts, type NodeHit } from './support.js';\n\nexport function strayTransactionReads(file: FileView): NodeHit[] {\n const source = file.sourceFile;\n const hits: NodeHit[] = [];\n forEachNode(source, (node) => {\n if (!ts.isCallExpression(node) || calleeName(node) !== 'runTransaction') return;\n const callback = node.arguments.find(\n (arg) => ts.isArrowFunction(arg) || ts.isFunctionExpression(arg),\n ) as ts.ArrowFunction | ts.FunctionExpression | undefined;\n if (!callback) return;\n const transaction = callback.parameters[0]?.name.getText(source);\n const inspect = (inner: ts.Node) => {\n if (ts.isCallExpression(inner) && ts.isPropertyAccessExpression(inner.expression)) {\n const method = inner.expression.name.text;\n const receiver = inner.expression.expression.getText(source);\n if (\n ((method === 'get' && inner.arguments.length === 0) || method === 'getAll') &&\n receiver !== transaction\n ) {\n hits.push({\n node: inner,\n detail: `${receiver}.${method}(…) inside a runTransaction callback reads outside the transaction`,\n });\n }\n }\n ts.forEachChild(inner, inspect);\n };\n ts.forEachChild(callback.body, inspect);\n });\n return hits;\n}\n\nexport const transactionReadGuard: GenericGuardDefinition<NoFacts> = {\n id: 'firestoreTransactionReads',\n absentWhen: 'the app stores nothing in Firestore',\n factProblems: takesNoFacts,\n build() {\n return [\n bannedGuard({\n title: 'a transaction callback reads only through its transaction',\n description:\n 'Inside an inline runTransaction callback, every Firestore read goes through the callback transaction ' +\n '(transaction.get(ref | query), transaction.getAll(…)). A plain ref.get() / query.get() / db.getAll(…) there is ' +\n 'outside the read set, so the write it decides can commit after the value changed. Fix: read through the ' +\n 'transaction. A read made before the transaction, and a callback passed by name, are left to review.',\n scope: { roots: ['functions'], kinds: ['code'], tests: false },\n match: strayTransactionReads,\n }),\n ];\n },\n};\n\nfunction isNegative(arg: ts.Expression): boolean {\n if (ts.isPrefixUnaryExpression(arg) && arg.operator === ts.SyntaxKind.MinusToken) return true;\n return ts.isParenthesizedExpression(arg) ? isNegative(arg.expression) : false;\n}\n\nexport function negativeIncrements(file: FileView): NodeHit[] {\n const hits: NodeHit[] = [];\n forEachNode(file.sourceFile, (node) => {\n if (\n ts.isCallExpression(node) &&\n ts.isPropertyAccessExpression(node.expression) &&\n node.expression.name.text === 'increment' &&\n node.arguments.length === 1 &&\n isNegative(node.arguments[0])\n ) {\n hits.push({\n node,\n detail: `${node.getText(file.sourceFile).slice(0, 80)} lowers a count blind`,\n });\n }\n });\n return hits;\n}\n\nexport const counterDecrementGuard: GenericGuardDefinition<NoFacts> = {\n id: 'firestoreCounterDecrements',\n absentWhen: 'the app stores nothing in Firestore',\n factProblems: takesNoFacts,\n build() {\n return [\n bannedGuard({\n title: 'no stored count is lowered by a blind decrement',\n description:\n 'A count is lowered by reading it inside the transaction and writing the lowered value, never below zero — ' +\n 'never by an increment of a negative amount, which drives a missing, already-zero, or twice-decremented count ' +\n 'negative. Flagged: any .increment(-n) / .increment(-(…)) in server code and scripts. Fix: read the count in the ' +\n \"transaction and write max(0, value - n), through that count's one decrement path.\",\n scope: {\n roots: ['functions', 'scripts'],\n optionalRoots: ['functions', 'scripts'],\n kinds: ['code'],\n tests: false,\n },\n match: negativeIncrements,\n }),\n ];\n },\n};\n\nexport interface RandomUuidFacts {\n reviewedSites: readonly Reviewed[];\n}\n\nexport const randomUuidGuard: GenericGuardDefinition<RandomUuidFacts> = {\n id: 'firestoreRandomUuidSites',\n absentWhen: 'the app stores nothing in Firestore',\n factProblems(facts) {\n const problems: string[] = [];\n reviewedProblems(facts?.reviewedSites, 'reviewedSites', problems);\n return problems;\n },\n build(facts) {\n return [\n reviewedAllowlist({\n title: 'randomUUID appears only at reviewed sites that mint no document id',\n description:\n 'A Firestore document id comes from the auto-id a collection reference mints, never randomUUID. A ' +\n 'non-persisted operation or request id, or a scratch file name, may use it; text cannot tell that use from a ' +\n 'document id, so every server file that names randomUUID is a reviewed site with its why. Fix: mint the ' +\n 'document id from the collection reference, or add the reviewed site. An entry whose file no longer names ' +\n 'randomUUID is stale.',\n scope: { roots: ['functions'], kinds: ['code'], tests: false },\n match: matchPattern(/\\brandomUUID\\b/),\n allowlist: facts.reviewedSites,\n samples: {\n flags: [\"import { randomUUID } from 'node:crypto';\", 'const id = crypto.randomUUID();'],\n passes: [\n '// never a randomUUID for a document id\\nconst ref = col.doc();',\n 'const id = ref.id;',\n ],\n },\n }),\n ];\n },\n};\n\nconst TIMESTAMP_NOW_SCOPE: Scope = {\n roots: ['app', 'functions'],\n optionalRoots: ['functions'],\n kinds: ['code'],\n tests: false,\n};\n\nexport function timestampNowCalls(file: FileView): NodeHit[] {\n const hits: NodeHit[] = [];\n forEachNode(file.sourceFile, (node) => {\n if (\n !ts.isCallExpression(node) ||\n !ts.isPropertyAccessExpression(node.expression) ||\n node.expression.name.text !== 'now'\n )\n return;\n const receiver = node.expression.expression;\n const name = ts.isIdentifier(receiver)\n ? receiver.text\n : ts.isPropertyAccessExpression(receiver)\n ? receiver.name.text\n : null;\n if (name === 'Timestamp')\n hits.push({ node, detail: `${node.getText(file.sourceFile)} mints a Firestore Timestamp` });\n });\n return hits;\n}\n\nexport const timestampNowGuard: GenericGuardDefinition<NoFacts> = {\n id: 'firestoreTimestampNow',\n absentWhen: 'the app stores nothing in Firestore',\n factProblems: takesNoFacts,\n build() {\n return [\n bannedGuard({\n title: 'no Firestore Timestamp is minted with Timestamp.now()',\n description:\n 'A stored time is a Date.now() number, never a Firestore Timestamp. Flagged: Timestamp.now() on any receiver ' +\n '(Timestamp, admin.firestore.Timestamp, …) in app and server code. Fix: store Date.now().',\n scope: TIMESTAMP_NOW_SCOPE,\n match: timestampNowCalls,\n }),\n ];\n },\n};\n"]}
1
+ {"version":3,"file":"server-guards.js","sourceRoot":"","sources":["../../../src/modules/firestore/server-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 { matchPattern, reviewedAllowlist } from '../../shapes/shapes.js';\nimport { calleeName, forEachNode, unwrap } from '../../support/ast.js';\nimport { reviewedProblems, takesNoFacts } from '../../support/facts.js';\nimport { bannedGuard, type NoFacts, type NodeHit } from './support.js';\n\ntype FunctionNode =\n ts.FunctionDeclaration | ts.FunctionExpression | ts.ArrowFunction | ts.MethodDeclaration;\n\nconst isFunctionNode = (node: ts.Node): node is FunctionNode =>\n ts.isFunctionDeclaration(node) ||\n ts.isFunctionExpression(node) ||\n ts.isArrowFunction(node) ||\n ts.isMethodDeclaration(node);\n\nfunction isTransactionType(type: ts.TypeNode | undefined): boolean {\n if (!type || !ts.isTypeReferenceNode(type)) return false;\n const name = type.typeName;\n return (ts.isIdentifier(name) ? name : name.right).text === 'Transaction';\n}\n\nfunction requiredTransactionParameter(fn: FunctionNode): string | undefined {\n const param = fn.parameters.find(\n (p) => !p.questionToken && !p.initializer && isTransactionType(p.type),\n );\n return param && ts.isIdentifier(param.name) ? param.name.text : undefined;\n}\n\nfunction namedFunction(source: ts.SourceFile, name: string): FunctionNode | undefined {\n let found: FunctionNode | undefined;\n forEachNode(source, (node) => {\n if (found) return;\n if (ts.isFunctionDeclaration(node) && node.name?.text === name) found = node;\n else if (\n ts.isVariableDeclaration(node) &&\n ts.isIdentifier(node.name) &&\n node.name.text === name &&\n node.initializer\n ) {\n const init = unwrap(node.initializer);\n if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) found = init;\n }\n });\n return found;\n}\n\nfunction transactionFunctions(source: ts.SourceFile): Map<FunctionNode, string | undefined> {\n const found = new Map<FunctionNode, string | undefined>();\n forEachNode(source, (node) => {\n if (ts.isCallExpression(node) && calleeName(node) === 'runTransaction') {\n const arg = node.arguments[0] ? unwrap(node.arguments[0]) : undefined;\n const callback =\n arg && (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg))\n ? arg\n : arg && ts.isIdentifier(arg)\n ? namedFunction(source, arg.text)\n : undefined;\n if (callback && !found.has(callback)) {\n const first = callback.parameters[0]?.name;\n found.set(callback, first && ts.isIdentifier(first) ? first.text : undefined);\n }\n } else if (isFunctionNode(node) && !found.has(node)) {\n const transaction = requiredTransactionParameter(node);\n if (transaction) found.set(node, transaction);\n }\n });\n return found;\n}\n\nexport function strayTransactionReads(file: FileView): NodeHit[] {\n const source = file.sourceFile;\n const hits: NodeHit[] = [];\n const functions = transactionFunctions(source);\n for (const [fn, transaction] of functions) {\n if (!fn.body) continue;\n const inspect = (inner: ts.Node) => {\n if (functions.has(inner as FunctionNode)) return;\n if (ts.isCallExpression(inner) && ts.isPropertyAccessExpression(inner.expression)) {\n const method = inner.expression.name.text;\n const receiver = inner.expression.expression.getText(source);\n if (\n ((method === 'get' && inner.arguments.length === 0) || method === 'getAll') &&\n receiver !== transaction\n ) {\n hits.push({\n node: inner,\n detail: `${receiver}.${method}(…) inside a transaction's work reads outside the transaction`,\n });\n }\n }\n ts.forEachChild(inner, inspect);\n };\n ts.forEachChild(fn.body, inspect);\n }\n return hits.sort((a, b) => a.node.getStart(source) - b.node.getStart(source));\n}\n\nexport const transactionReadGuard: GenericGuardDefinition<NoFacts> = {\n id: 'firestoreTransactionReads',\n absentWhen: 'the app stores nothing in Firestore',\n factProblems: takesNoFacts,\n build() {\n return [\n bannedGuard({\n title: 'a transaction callback reads only through its transaction',\n description:\n 'In the work a transaction does, every Firestore read goes through that transaction (transaction.get(ref | query), ' +\n 'transaction.getAll(…)). That work is a runTransaction callback — inline, or passed by name and declared in ' +\n 'the same file — and every function with a required Transaction parameter. A plain ref.get() / query.get() / ' +\n 'db.getAll(…) there is outside the read set, so the write it decides can commit after the value changed. Fix: ' +\n 'read through the transaction. A function whose Transaction parameter is optional reads plainly when called ' +\n 'without one and is left out, as is a read made before the transaction.',\n scope: { roots: ['functions'], kinds: ['code'], tests: false },\n match: strayTransactionReads,\n }),\n ];\n },\n};\n\nfunction isNegative(arg: ts.Expression): boolean {\n if (ts.isPrefixUnaryExpression(arg) && arg.operator === ts.SyntaxKind.MinusToken) return true;\n return ts.isParenthesizedExpression(arg) ? isNegative(arg.expression) : false;\n}\n\nexport function negativeIncrements(file: FileView): NodeHit[] {\n const hits: NodeHit[] = [];\n forEachNode(file.sourceFile, (node) => {\n if (\n ts.isCallExpression(node) &&\n ts.isPropertyAccessExpression(node.expression) &&\n node.expression.name.text === 'increment' &&\n node.arguments.length === 1 &&\n isNegative(node.arguments[0])\n ) {\n hits.push({\n node,\n detail: `${node.getText(file.sourceFile).slice(0, 80)} lowers a count blind`,\n });\n }\n });\n return hits;\n}\n\nexport const counterDecrementGuard: GenericGuardDefinition<NoFacts> = {\n id: 'firestoreCounterDecrements',\n absentWhen: 'the app stores nothing in Firestore',\n factProblems: takesNoFacts,\n build() {\n return [\n bannedGuard({\n title: 'no stored count is lowered by a blind decrement',\n description:\n 'A count is lowered by reading it inside the transaction and writing the lowered value, never below zero — ' +\n 'never by an increment of a negative amount, which drives a missing, already-zero, or twice-decremented count ' +\n 'negative. Flagged: any .increment(-n) / .increment(-(…)) in server code and scripts. Fix: read the count in the ' +\n \"transaction and write max(0, value - n), through that count's one decrement path.\",\n scope: {\n roots: ['functions', 'scripts'],\n optionalRoots: ['functions', 'scripts'],\n kinds: ['code'],\n tests: false,\n },\n match: negativeIncrements,\n }),\n ];\n },\n};\n\nexport interface RandomUuidFacts {\n reviewedSites: readonly Reviewed[];\n}\n\nexport const randomUuidGuard: GenericGuardDefinition<RandomUuidFacts> = {\n id: 'firestoreRandomUuidSites',\n absentWhen: 'the app stores nothing in Firestore',\n factProblems(facts) {\n const problems: string[] = [];\n reviewedProblems(facts?.reviewedSites, 'reviewedSites', problems);\n return problems;\n },\n build(facts) {\n return [\n reviewedAllowlist({\n title: 'randomUUID appears only at reviewed sites that mint no document id',\n description:\n 'A Firestore document id comes from the auto-id a collection reference mints, never randomUUID. A ' +\n 'non-persisted operation or request id, or a scratch file name, may use it; text cannot tell that use from a ' +\n 'document id, so every server file that names randomUUID is a reviewed site with its why. Fix: mint the ' +\n 'document id from the collection reference, or add the reviewed site. An entry whose file no longer names ' +\n 'randomUUID is stale.',\n scope: { roots: ['functions'], kinds: ['code'], tests: false },\n match: matchPattern(/\\brandomUUID\\b/),\n allowlist: facts.reviewedSites,\n samples: {\n flags: [\"import { randomUUID } from 'node:crypto';\", 'const id = crypto.randomUUID();'],\n passes: [\n '// never a randomUUID for a document id\\nconst ref = col.doc();',\n 'const id = ref.id;',\n ],\n },\n }),\n ];\n },\n};\n\nconst TIMESTAMP_NOW_SCOPE: Scope = {\n roots: ['app', 'functions'],\n optionalRoots: ['functions'],\n kinds: ['code'],\n tests: false,\n};\n\nexport function timestampNowCalls(file: FileView): NodeHit[] {\n const hits: NodeHit[] = [];\n forEachNode(file.sourceFile, (node) => {\n if (\n !ts.isCallExpression(node) ||\n !ts.isPropertyAccessExpression(node.expression) ||\n node.expression.name.text !== 'now'\n )\n return;\n const receiver = node.expression.expression;\n const name = ts.isIdentifier(receiver)\n ? receiver.text\n : ts.isPropertyAccessExpression(receiver)\n ? receiver.name.text\n : null;\n if (name === 'Timestamp')\n hits.push({ node, detail: `${node.getText(file.sourceFile)} mints a Firestore Timestamp` });\n });\n return hits;\n}\n\nexport const timestampNowGuard: GenericGuardDefinition<NoFacts> = {\n id: 'firestoreTimestampNow',\n absentWhen: 'the app stores nothing in Firestore',\n factProblems: takesNoFacts,\n build() {\n return [\n bannedGuard({\n title: 'no Firestore Timestamp is minted with Timestamp.now()',\n description:\n 'A stored time is a Date.now() number, never a Firestore Timestamp. Flagged: Timestamp.now() on any receiver ' +\n '(Timestamp, admin.firestore.Timestamp, …) in app and server code. Fix: store Date.now().',\n scope: TIMESTAMP_NOW_SCOPE,\n match: timestampNowCalls,\n }),\n ];\n },\n};\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttt-productions/repo-guards",
3
- "version": "0.1.31",
3
+ "version": "0.1.32",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/ttt-productions/ttt-packages.git",