@gspothq/cli 0.0.0-stage → 0.1.1

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.
Files changed (232) hide show
  1. package/README.md +18 -2
  2. package/configurations/database/postgres/manifest.toml +114 -0
  3. package/configurations/database/postgres/rules/POSTGRES.md +78 -0
  4. package/configurations/framework/astro/eslint-presets.json +138 -0
  5. package/configurations/framework/astro/manifest.toml +95 -0
  6. package/configurations/framework/astro/rules/ASTRO.md +39 -0
  7. package/configurations/framework/expo/manifest.toml +43 -0
  8. package/configurations/framework/express/manifest.toml +17 -0
  9. package/configurations/framework/express/rules/EXPRESS.md +73 -0
  10. package/configurations/framework/express/semgrep/express.yml +71 -0
  11. package/configurations/framework/fastapi/manifest.toml +11 -0
  12. package/configurations/framework/fastapi/rules/FASTAPI.md +100 -0
  13. package/configurations/framework/fastapi/semgrep/fastapi.yml +44 -0
  14. package/configurations/framework/nestjs/eslint-presets.json +62 -0
  15. package/configurations/framework/nestjs/manifest.toml +52 -0
  16. package/configurations/framework/nestjs/rules/NESTJS.md +44 -0
  17. package/configurations/framework/nextjs/eslint-presets.json +73 -0
  18. package/configurations/framework/nextjs/manifest.toml +119 -0
  19. package/configurations/framework/nextjs/rules/NEXTJS.md +43 -0
  20. package/configurations/framework/react/eslint-presets.json +77 -0
  21. package/configurations/framework/react/manifest.toml +93 -0
  22. package/configurations/framework/react/rules/REACT.md +108 -0
  23. package/configurations/framework/react-dom/eslint-presets.json +110 -0
  24. package/configurations/framework/react-dom/manifest.toml +24 -0
  25. package/configurations/framework/react-native/manifest.toml +46 -0
  26. package/configurations/framework/react-native/rules/REACT-NATIVE.md +51 -0
  27. package/configurations/framework/site/manifest.toml +170 -0
  28. package/configurations/framework/site/rules/SITE.md +20 -0
  29. package/configurations/framework/svelte/eslint-presets.json +67 -0
  30. package/configurations/framework/svelte/manifest.toml +83 -0
  31. package/configurations/framework/svelte/rules/SVELTE.md +48 -0
  32. package/configurations/framework/vue/eslint-presets.json +180 -0
  33. package/configurations/framework/vue/manifest.toml +83 -0
  34. package/configurations/framework/vue/rules/VUE.md +41 -0
  35. package/configurations/general/commits/manifest.toml +111 -0
  36. package/configurations/general/commits/rules/GIT.md +31 -0
  37. package/configurations/general/coverage/manifest.toml +3 -0
  38. package/configurations/general/dependencies/manifest.toml +159 -0
  39. package/configurations/general/dependencies/rules/DEPENDENCIES.md +30 -0
  40. package/configurations/general/dependencies/syncpack.json +15 -0
  41. package/configurations/general/docs/manifest.toml +79 -0
  42. package/configurations/general/docs/rules/DOCS.md +86 -0
  43. package/configurations/general/duplication/manifest.toml +32 -0
  44. package/configurations/general/engineering/manifest.toml +8 -0
  45. package/configurations/general/engineering/rules/agent/PLANNING.md +42 -0
  46. package/configurations/general/engineering/rules/agent/SUPPRESSIONS.md +16 -0
  47. package/configurations/general/engineering/rules/agent/TALKING.md +3 -0
  48. package/configurations/general/engineering/rules/agent/TASKS.md +19 -0
  49. package/configurations/general/engineering/rules/agent/WORKING.md +83 -0
  50. package/configurations/general/engineering/rules/code/ACCESSIBILITY.md +32 -0
  51. package/configurations/general/engineering/rules/code/CLI.md +37 -0
  52. package/configurations/general/engineering/rules/code/COMMENTS.md +38 -0
  53. package/configurations/general/engineering/rules/code/CONFIGURATION.md +25 -0
  54. package/configurations/general/engineering/rules/code/ERRORS.md +49 -0
  55. package/configurations/general/engineering/rules/code/HTTP.md +46 -0
  56. package/configurations/general/engineering/rules/code/LOGGING.md +55 -0
  57. package/configurations/general/engineering/rules/code/TESTING.md +58 -0
  58. package/configurations/general/files/manifest.toml +141 -0
  59. package/configurations/general/files/rules/YAML.md +19 -0
  60. package/configurations/general/format/manifest.toml +152 -0
  61. package/configurations/general/gspot/manifest.toml +65 -0
  62. package/configurations/general/gspot/rules/GENERATED.md +10 -0
  63. package/configurations/general/licenses/manifest.toml +33 -0
  64. package/configurations/general/naming/defaults.json +423 -0
  65. package/configurations/general/naming/manifest.toml +35 -0
  66. package/configurations/general/naming/rules/NAMING.md +179 -0
  67. package/configurations/general/prose/manifest.toml +86 -0
  68. package/configurations/general/prose/rules/WRITING.md +31 -0
  69. package/configurations/general/prose/vale/styles/LICENSES.md +55 -0
  70. package/configurations/general/prose/vale/styles/gspot/ampm.yml +8 -0
  71. package/configurations/general/prose/vale/styles/gspot/conscious-language.yml +16 -0
  72. package/configurations/general/prose/vale/styles/gspot/contractions.yml +43 -0
  73. package/configurations/general/prose/vale/styles/gspot/currency.yml +8 -0
  74. package/configurations/general/prose/vale/styles/gspot/dashes.yml +8 -0
  75. package/configurations/general/prose/vale/styles/gspot/date-ordinals.yml +41 -0
  76. package/configurations/general/prose/vale/styles/gspot/dates.yml +7 -0
  77. package/configurations/general/prose/vale/styles/gspot/file-paths.yml +7 -0
  78. package/configurations/general/prose/vale/styles/gspot/future.yml +17 -0
  79. package/configurations/general/prose/vale/styles/gspot/hedging.yml +22 -0
  80. package/configurations/general/prose/vale/styles/gspot/history.yml +16 -0
  81. package/configurations/general/prose/vale/styles/gspot/idioms.yml +21 -0
  82. package/configurations/general/prose/vale/styles/gspot/interface-verbs.yml +11 -0
  83. package/configurations/general/prose/vale/styles/gspot/jargon.yml +11 -0
  84. package/configurations/general/prose/vale/styles/gspot/leading-conjunctions.yml +12 -0
  85. package/configurations/general/prose/vale/styles/gspot/link-text.yml +8 -0
  86. package/configurations/general/prose/vale/styles/gspot/locations.yml +14 -0
  87. package/configurations/general/prose/vale/styles/gspot/marketing.yml +26 -0
  88. package/configurations/general/prose/vale/styles/gspot/merge-conflict-markers.yml +13 -0
  89. package/configurations/general/prose/vale/styles/gspot/militaristic-language.yml +22 -0
  90. package/configurations/general/prose/vale/styles/gspot/modals.yml +9 -0
  91. package/configurations/general/prose/vale/styles/gspot/numbered-units.yml +18 -0
  92. package/configurations/general/prose/vale/styles/gspot/obvious-fields.yml +14 -0
  93. package/configurations/general/prose/vale/styles/gspot/ordinal-adverbs.yml +15 -0
  94. package/configurations/general/prose/vale/styles/gspot/percentage-numbers.yml +8 -0
  95. package/configurations/general/prose/vale/styles/gspot/placeholders.yml +7 -0
  96. package/configurations/general/prose/vale/styles/gspot/product-centric-writing.yml +8 -0
  97. package/configurations/general/prose/vale/styles/gspot/punctuated-headings.yml +11 -0
  98. package/configurations/general/prose/vale/styles/gspot/self-reference.yml +15 -0
  99. package/configurations/general/prose/vale/styles/gspot/session-identifiers.yml +11 -0
  100. package/configurations/general/prose/vale/styles/gspot/since.yml +6 -0
  101. package/configurations/general/prose/vale/styles/gspot/straight-quotes.yml +11 -0
  102. package/configurations/general/prose/vale/styles/gspot/suspended-hyphens.yml +8 -0
  103. package/configurations/general/prose/vale/styles/gspot/symbols.yml +8 -0
  104. package/configurations/general/prose/vale/styles/gspot/time-ranges.yml +14 -0
  105. package/configurations/general/prose/vale/styles/gspot/url-prepositions.yml +10 -0
  106. package/configurations/general/prose/vale/styles/gspot/version-range.yml +8 -0
  107. package/configurations/general/repository/manifest.toml +24 -0
  108. package/configurations/general/secrets/gitleaks.toml +2 -0
  109. package/configurations/general/secrets/manifest.toml +125 -0
  110. package/configurations/general/secrets/rules/SECRETS.md +13 -0
  111. package/configurations/general/security/manifest.toml +77 -0
  112. package/configurations/general/security/rules/SECURITY.md +55 -0
  113. package/configurations/general/spelling/manifest.toml +46 -0
  114. package/configurations/general/structure/manifest.toml +70 -0
  115. package/configurations/infrastructure/ansible/manifest.toml +33 -0
  116. package/configurations/infrastructure/docker/manifest.toml +127 -0
  117. package/configurations/infrastructure/docker/rules/DOCKER.md +135 -0
  118. package/configurations/infrastructure/docker/trivyignore +2 -0
  119. package/configurations/infrastructure/github-actions/manifest.toml +76 -0
  120. package/configurations/infrastructure/github-actions/rules/GITHUB-ACTIONS.md +43 -0
  121. package/configurations/infrastructure/github-actions/zizmor.yml +3 -0
  122. package/configurations/infrastructure/nginx/manifest.toml +51 -0
  123. package/configurations/infrastructure/nginx/rules/NGINX.md +38 -0
  124. package/configurations/infrastructure/openapi/manifest.toml +50 -0
  125. package/configurations/infrastructure/openapi/rules/OPENAPI.md +39 -0
  126. package/configurations/infrastructure/xcode/manifest.toml +131 -0
  127. package/configurations/infrastructure/xcode/rules/XCODE.md +34 -0
  128. package/configurations/language/bash/manifest.toml +261 -0
  129. package/configurations/language/bash/rules/BASH.md +106 -0
  130. package/configurations/language/bash/rules/LANGUAGE.md +213 -0
  131. package/configurations/language/bash/rules/NAMING.md +62 -0
  132. package/configurations/language/bash/rules/OPERATIONS.md +77 -0
  133. package/configurations/language/bash/rules/SAFETY.md +222 -0
  134. package/configurations/language/bash/semgrep/bash.yml +30 -0
  135. package/configurations/language/css/manifest.toml +98 -0
  136. package/configurations/language/css/rules/CSS.md +28 -0
  137. package/configurations/language/css/rules/NAMING.md +17 -0
  138. package/configurations/language/css/rules/TAILWIND.md +26 -0
  139. package/configurations/language/css/stylelint-rule-names.json +89 -0
  140. package/configurations/language/html/manifest.toml +59 -0
  141. package/configurations/language/html/rules/HTML.md +27 -0
  142. package/configurations/language/html/rules/NAMING.md +16 -0
  143. package/configurations/language/javascript/eslint-presets.json +997 -0
  144. package/configurations/language/javascript/eslint-rule-names.json +299 -0
  145. package/configurations/language/javascript/eslint-runtime-names.json +61 -0
  146. package/configurations/language/javascript/manifest.toml +393 -0
  147. package/configurations/language/javascript/rules/BUN.md +13 -0
  148. package/configurations/language/javascript/rules/COMPONENTS.md +18 -0
  149. package/configurations/language/javascript/rules/DENO.md +15 -0
  150. package/configurations/language/javascript/rules/JAVASCRIPT.md +60 -0
  151. package/configurations/language/javascript/rules/NAMING.md +31 -0
  152. package/configurations/language/javascript/rules/NODE.md +21 -0
  153. package/configurations/language/javascript/rules/PLAYWRIGHT.md +34 -0
  154. package/configurations/language/javascript/semgrep/javascript.yml +84 -0
  155. package/configurations/language/markdown/manifest.toml +85 -0
  156. package/configurations/language/markdown/markdownlint-cli2.mjs +19 -0
  157. package/configurations/language/python/manifest.toml +621 -0
  158. package/configurations/language/python/rules/DESIGN.md +136 -0
  159. package/configurations/language/python/rules/FLOW.md +124 -0
  160. package/configurations/language/python/rules/NAMING.md +53 -0
  161. package/configurations/language/python/rules/PACKAGING.md +49 -0
  162. package/configurations/language/python/rules/PYTHON.md +136 -0
  163. package/configurations/language/python/rules/TYPING.md +132 -0
  164. package/configurations/language/sql/manifest.toml +68 -0
  165. package/configurations/language/sql/rules/NAMING.md +90 -0
  166. package/configurations/language/sql/rules/SQL.md +9 -0
  167. package/configurations/language/swift/manifest.toml +164 -0
  168. package/configurations/language/swift/rules/NAMING.md +88 -0
  169. package/configurations/language/swift/rules/SWIFT.md +134 -0
  170. package/configurations/language/swift/rules/SWIFTUI.md +61 -0
  171. package/configurations/language/swift/rules/UIKIT.md +99 -0
  172. package/configurations/language/swift/semgrep/swift.yml +126 -0
  173. package/configurations/language/typescript/eslint-presets.json +215 -0
  174. package/configurations/language/typescript/manifest.toml +72 -0
  175. package/configurations/language/typescript/rules/NAMING.md +73 -0
  176. package/configurations/language/typescript/rules/TYPESCRIPT.md +79 -0
  177. package/configurations/library/drizzle/manifest.toml +68 -0
  178. package/configurations/library/drizzle/rules/DRIZZLE.md +44 -0
  179. package/configurations/library/react-hook-form/manifest.toml +15 -0
  180. package/configurations/library/react-hook-form/rules/REACT-HOOK-FORM.md +48 -0
  181. package/configurations/library/swift-snapshot-testing/manifest.toml +32 -0
  182. package/configurations/library/tanstack-query/eslint-presets.json +21 -0
  183. package/configurations/library/tanstack-query/manifest.toml +26 -0
  184. package/configurations/library/tanstack-query/rules/TANSTACK-QUERY.md +41 -0
  185. package/configurations/library/testing-library/eslint-presets.json +191 -0
  186. package/configurations/library/testing-library/manifest.toml +47 -0
  187. package/configurations/library/translations/manifest.toml +40 -0
  188. package/configurations/library/translations/rules/NEXT-INTL.md +33 -0
  189. package/configurations/library/translations/rules/TRANSLATIONS.md +102 -0
  190. package/configurations/library/trpc/manifest.toml +14 -0
  191. package/configurations/library/trpc/rules/TRPC.md +43 -0
  192. package/configurations/library/zod/manifest.toml +25 -0
  193. package/configurations/library/zod/rules/ZOD.md +44 -0
  194. package/configurations/library/zustand/manifest.toml +18 -0
  195. package/configurations/library/zustand/rules/ZUSTAND.md +106 -0
  196. package/configurations/platform/cloudflare/manifest.toml +94 -0
  197. package/configurations/platform/cloudflare/rules/CLOUDFLARE.md +23 -0
  198. package/configurations/platform/cloudflare/semgrep/cloudflare.yml +12 -0
  199. package/configurations/platform/supabase/manifest.toml +112 -0
  200. package/configurations/platform/supabase/rules/SUPABASE.md +129 -0
  201. package/configurations/platform/supabase/semgrep/supabase.yml +60 -0
  202. package/configurations/testing/jest/manifest.toml +54 -0
  203. package/configurations/testing/pytest/manifest.toml +53 -0
  204. package/configurations/testing/swift-tests/manifest.toml +52 -0
  205. package/configurations/testing/swift-tests/rules/SWIFT-TESTS.md +79 -0
  206. package/configurations/testing/vitest/eslint-presets.json +22 -0
  207. package/configurations/testing/vitest/manifest.toml +69 -0
  208. package/configurations/testing/vitest/rules/VITEST.md +18 -0
  209. package/dist/LICENSE.md +201 -0
  210. package/dist/THIRD-PARTY-NOTICES.md +25 -0
  211. package/dist/eslint-rule-levels.json +10 -0
  212. package/dist/gspot.js +30740 -0
  213. package/dist/linkinator-program.js +65 -0
  214. package/dist/purgecss-program.js +51 -0
  215. package/dist/tool-versions.json +3 -0
  216. package/grammars/bash.wasm +0 -0
  217. package/grammars/css.wasm +0 -0
  218. package/grammars/html.wasm +0 -0
  219. package/grammars/javascript.wasm +0 -0
  220. package/grammars/licenses/tree-sitter-bash.txt +21 -0
  221. package/grammars/licenses/tree-sitter-css.txt +21 -0
  222. package/grammars/licenses/tree-sitter-html.txt +21 -0
  223. package/grammars/licenses/tree-sitter-javascript.txt +21 -0
  224. package/grammars/licenses/tree-sitter-python.txt +21 -0
  225. package/grammars/licenses/tree-sitter-swift.txt +21 -0
  226. package/grammars/licenses/tree-sitter-typescript.txt +21 -0
  227. package/grammars/python.wasm +0 -0
  228. package/grammars/swift.wasm +0 -0
  229. package/grammars/tsx.wasm +0 -0
  230. package/grammars/typescript.wasm +0 -0
  231. package/gspot.schema.json +45991 -0
  232. package/package.json +93 -5
@@ -0,0 +1,112 @@
1
+ allowed_words = ["Supabase"]
2
+ [configuration]
3
+ title = "Supabase"
4
+ requires = ["postgres", "javascript"]
5
+ suggests = ["typescript"]
6
+ description = "A Supabase project: a project file that parses, edge functions under Deno, and migrations named as the CLI names them. Every storage bucket has a policy, and privileged keys stay in their allowed files."
7
+
8
+ [detect]
9
+ paths = ["**/supabase/config.toml"]
10
+ project_files = ["supabase/config.toml"]
11
+
12
+ [files]
13
+ paths = ["supabase/config.toml", "supabase/functions/**", "supabase/seed.sql"]
14
+
15
+ [[tool]]
16
+ name = "deno"
17
+ version = "2.6.6"
18
+ min_version = "2.0.0"
19
+ mise = "deno"
20
+ brew = "deno"
21
+ github = { name = "denoland/deno", version = "v2.6.6" }
22
+ scoop = "deno"
23
+
24
+ [[tool]]
25
+ name = "supabase"
26
+ system = true
27
+ version_command = ["--version"]
28
+ min_version = "2.0.0"
29
+ mise = "supabase"
30
+ brew = "supabase/tap/supabase"
31
+ npm = "supabase"
32
+ scoop = "supabase"
33
+
34
+ [[check]]
35
+ name = "project-file"
36
+ example = "A function entry in `supabase/config.toml` naming a function absent from the repository reports a stale function configuration. Restore the function source or remove its obsolete configuration table."
37
+ level = "recommended"
38
+ stage = "commit"
39
+ runs = "scope"
40
+ summary = "Checks that the project file parses, and that every function it configures has a folder with an index file."
41
+ why = "The CLI reads this file at every deploy, and a function it configures with no code fails there."
42
+ help = "Correct the file at the place the message names, or remove the table of a function the repository does not hold."
43
+
44
+ [[check]]
45
+ name = "migration-names"
46
+ example = "A Supabase migration named `add_users.sql` lacks the required timestamp prefix. Create it with `supabase migration new add_users`, then move the intended SQL into the generated migration file."
47
+ level = "recommended"
48
+ stage = "commit"
49
+ runs = "scope"
50
+ summary = "Checks that every migration is named fourteen digits, an underscore, and snake case words."
51
+ why = "The CLI orders and records migrations by this name, and it skips a file it cannot read a version from."
52
+ help = "Create the migration with supabase migration new `<name>`, which writes the name."
53
+
54
+ [[check]]
55
+ name = "storage-policies"
56
+ example = "A declared storage bucket with no matching policy on `storage.objects` reports missing access policy. Add a migration defining the intended access policy for that bucket and rerun."
57
+ level = "recommended"
58
+ stage = "commit"
59
+ runs = "scope"
60
+ summary = "Checks that every storage bucket in the project file has a policy on storage.objects that names it."
61
+ why = "A bucket with no policy is closed to everyone or, when it is public, open to everyone, and neither is a decision anybody wrote down."
62
+ help = "Add a policy on storage.objects for the bucket in a migration."
63
+
64
+ [[check]]
65
+ name = "service-role-key"
66
+ example = "A client source reading a Supabase secret or service-role key reports an administrative-key boundary finding. Move privileged operations and the key read to a server-only path, exposing only the required server operation to the client."
67
+ level = "recommended"
68
+ stage = "commit"
69
+ runs = "scope"
70
+ summary = "Checks that secret and service role keys are named only in edge functions and test files."
71
+ why = "Secret and service role keys bypass row level security, so a client bundle that holds either exposes privileged database access."
72
+ help = "Move code to edge functions or tests, or run gspot ignore supabase/service-role-key with a reason."
73
+
74
+ [[check]]
75
+ name = "deno-lint"
76
+ example = "An Edge Function declaring `var greeting = \"hello\";` reports `no-var`. Use `const greeting = \"hello\";` when the value does not change, then rerun with the pinned Deno tool available."
77
+ level = "recommended"
78
+ stage = "commit"
79
+ runs = "scope"
80
+ tool = "deno"
81
+ summary = "Runs deno lint over every edge function, with the function's own deno.json."
82
+ why = "Edge functions run under Deno, and its linter knows the runtime the Node rules do not."
83
+ help = "Change the code the way the rule says; deno lint --rules explains each one."
84
+
85
+ [[check]]
86
+ name = "deno-check"
87
+ example = "An Edge Function assigning a string to a number-typed variable reports a Deno type error. Correct the value or type to match the intended contract, then rerun."
88
+ level = "recommended"
89
+ stage = "push"
90
+ runs = "scope"
91
+ tool = "deno"
92
+ summary = "Type checks the entry file of every edge function with deno check."
93
+ why = "The project compiler does not resolve the import map of a function, so only Deno sees its types."
94
+ help = "Fix the first error Deno prints; later ones are often the same mistake."
95
+
96
+ [[check]]
97
+ name = "stale-types"
98
+ command = ["supabase", "gen", "types", "typescript", "--local", "{each:--schema:supabase.schemas}"]
99
+ generated_paths = [{ stdout = "{setting:supabase.types_file}" }]
100
+ example = "A database types file that differs from the configured local Supabase schema reports drift. With the local database ready, regenerate using `supabase gen types typescript --local`, write the output to the configured file, and rerun."
101
+ level = "recommended"
102
+ stage = "push"
103
+ runs = "scope"
104
+ tool = "supabase"
105
+ needs = ["docker"]
106
+ summary = "Compares supabase.types_file with the types the CLI writes from the local database."
107
+ why = "Stale types let code compile against a column the database dropped."
108
+ help = "Run supabase gen types typescript --local and write the output to the file."
109
+
110
+ [defaults]
111
+ "postgres.client_schemas" = ["public"]
112
+ "tools.squawk.assume_in_transaction" = true
@@ -0,0 +1,129 @@
1
+ # Supabase
2
+
3
+ Layout, database access, storage, Edge Functions, configuration, and secrets of a Supabase
4
+ project, on top of the Postgres rules. Supabase uses the API roles `anon`, `authenticated`, and
5
+ `service_role`.
6
+
7
+ ## Layout and migrations
8
+
9
+ - Use the established Supabase layout for migrations, Edge Functions, storage assets, and tests.
10
+ - A schema or contract change that affects another project updates that consumer deliberately.
11
+
12
+ ## Database access
13
+
14
+ - `[api].schemas` in `config.toml` lists the schemas the Data API exposes. Treat those schemas as
15
+ externally reachable.
16
+ - row level security (RLS) with no policy denies access through the API roles. Use that choice for tables that clients
17
+ must not read.
18
+ - Roles Supabase creates for auth, cron, or storage stay scoped to the functions and schemas they need.
19
+ - Each policy chooses `TO anon`, `TO authenticated`, or `TO service_role` deliberately. The service
20
+ role never stands in for precise grants or hides a missing policy.
21
+ - A table can skip the foreign key to `auth.users` so its history survives account deletion.
22
+ - Keep `[api].max_rows` conservative to bound accidental or malicious payloads.
23
+
24
+ ## Direct database connections
25
+
26
+ Choose the Supabase connection mode for the workload.
27
+
28
+ | Workload | Connection choice |
29
+ | ----------------------------------------------- | --------------------------------------------------------------------- |
30
+ | Short-lived serverless application requests | Transaction pooler, with a compatible driver |
31
+ | Persistent server with direct network access | Direct connection with a bounded application pool |
32
+ | Persistent server on an IPv4-only network | Session pooler when the direct endpoint is unavailable |
33
+ | Migrations, dumps, or session-dependent tooling | Direct connection; use a supported session alternative when necessary |
34
+
35
+ A separate migration credential holds schema privileges. The application role holds only what
36
+ runtime needs
37
+ See [connection modes](https://supabase.com/docs/guides/database/connecting-to-postgres). Postgres.js
38
+ uses the transaction pooler's supported configuration.
39
+ Set explicit connection, idle, and query timeouts. Budget connections across instances and
40
+ workers. With Postgres.js through transaction pooling, set `prepare: false` and confirm the setting
41
+ after a driver or pooler change.
42
+
43
+ A direct database connection carries no Supabase identity. When an application passes identity into
44
+ PostgreSQL, one server operation verifies the token's issuer, signature, audience, and expiry. It
45
+ maps the identity to a fixed set of allowed roles, then opens a transaction on a restricted
46
+ connection. Use parameterized transaction-local `set_config` for claims and fixed trusted SQL for
47
+ the role. Every protected query uses that transaction. Commit or rollback ends its context.
48
+
49
+ When Drizzle generates migrations that the Supabase CLI applies, keep the CLI's migration format
50
+ and one authoritative applied-history ledger. Declare provider-managed schemas and roles as
51
+ existing. Check replay through the chosen runner. See
52
+ [migration history](https://supabase.com/docs/guides/local-development/database-migrations) and
53
+ [verified claims](https://supabase.com/docs/reference/javascript/auth-getclaims).
54
+
55
+ ## Storage
56
+
57
+ - Define buckets in migrations and mirror them in `config.toml`. A change to `public`,
58
+ `file_size_limit`, or `allowed_mime_types` updates both in the same change.
59
+ - Bucket restrictions enforce file size and MIME type, not only application validation.
60
+ - RLS policies on `storage.objects` and `storage.buckets` control access. The service controls metadata in the `storage` schema: migrations add only policies, indexes for RLS or validation, triggers,
61
+ and bucket configuration. Object rows change only through the Storage API.
62
+
63
+ ### Storage names
64
+
65
+ <!-- level: all -->
66
+
67
+ - Storage buckets use `lower_snake_case`.
68
+ - Use stable, explicit storage object keys.
69
+ - Validate user-controlled key segments and limit their length before use.
70
+ - Do not embed secrets, provider tokens, raw user text, or private identifiers in
71
+ object keys.
72
+ - Do not use display text as storage identity.
73
+
74
+ | Avoid | Prefer | Meaning |
75
+ | ------------------------------------------- | -------------------------------------- | ------------------------------------ |
76
+ | `Avatar Pictures` | `avatar_images` | A bucket. |
77
+ | `avatars/john@example.com/avatar image.png` | `avatars/user_01hxx8j2r6/avatar.png` | Object key without personal content. |
78
+ | `uploads/latest` | `reports/report_01hxx8j2r6/export.pdf` | A key tied to a specific report. |
79
+
80
+ ## Edge functions
81
+
82
+ - Each deployable function lives under `functions/<function-name>/` in the CLI's functions root,
83
+ with a `[functions.<name>]` entry whose `entrypoint` starts from the configuration file directory.
84
+ Shared code lives in `functions/_shared/`, which Supabase never deploys.
85
+ - Name a function folder in kebab-case for its callable operation, not its technology:
86
+ `submit-order`, never `SubmitOrder` or `provider_handler`. The folder, configuration entry, and
87
+ deployed name match.
88
+ - Edge Functions run on Deno, and the Deno rules hold.
89
+ - Keep `verify_jwt = true` for user-authenticated functions. Set it to `false` only when the function
90
+ exposes only public, harmless behavior, verifies a provider webhook signature, checks a service secret, or takes
91
+ a Vault secret from cron.
92
+ - A publishable or secret API key differs from a user JSON Web Token (JWT). Never send that key as
93
+ `Authorization: Bearer <key>`. Forward the caller's `Authorization` header to the Supabase
94
+ client, so RLS runs as the user.
95
+ - A service-role client serves only admin operations that cannot run as a user, on a path that
96
+ code and tests make explicit.
97
+ - Keep handlers small: parsing, validation, responses, provider calls, and database operations move
98
+ into local modules as complexity grows. JSON responses, `OPTIONS`, and Cross-Origin Resource Sharing (CORS) go through one helper
99
+ in `functions/_shared/`.
100
+ - Functions have runtime, CPU, memory, bundle-size, and log-rate limits, so long CPU-heavy work
101
+ runs elsewhere. Raw Supabase errors that expose credentials stay out of logs.
102
+ - When you write tests, cover function logic in unit tests. Integration tests against a local
103
+ database and function server cover the client, auth, RLS, and the invocation path.
104
+
105
+ ## Configuration and secrets
106
+
107
+ `config.toml` defines the desired local and remote configuration.
108
+
109
+ Ask the user before changing signup, anonymous sign-in, providers, hooks, JWT expiry, or password policy.
110
+ Also ask before weakening auth settings, row-level security, grants, or `verify_jwt`.
111
+ Proceed when the task already authorizes the change. Write the reason in the commit when the user authorizes a commit.
112
+
113
+ - Service keys, Edge secrets, Vault values, database URLs, and provider API keys stay backend-only.
114
+ They never appear in app code, checked-in files, logs, migration comments, tests, or test data.
115
+ - Local secrets live in ignored env files or the caller's environment.
116
+ - `pg_cron` and `pg_net` change only through migrations and deploy scripts. Cron reads its bearer
117
+ tokens from `vault.decrypted_secrets` at call time, never from its SQL.
118
+ - A change to a cron target's auth updates the Vault secret, the function's check, and the cron
119
+ SQL together.
120
+
121
+ ## References
122
+
123
+ [Database migrations](https://supabase.com/docs/guides/deployment/database-migrations),
124
+ [row-level security](https://supabase.com/docs/guides/database/postgres/row-level-security),
125
+ [Edge Functions](https://supabase.com/docs/guides/functions) and their
126
+ [auth](https://supabase.com/docs/guides/functions/auth) and
127
+ [configuration](https://supabase.com/docs/guides/functions/function-configuration),
128
+ [storage access control](https://supabase.com/docs/guides/storage/security/access-control), and
129
+ [scheduling functions](https://supabase.com/docs/guides/functions/schedule-functions).
@@ -0,0 +1,60 @@
1
+ rules:
2
+ - id: gspot.supabase.postgrest-filter-interpolation
3
+ patterns:
4
+ - pattern-either:
5
+ - pattern: $CLIENT.or(`...${$VAR}...`, ...)
6
+ - pattern: $CLIENT.filter($COLUMN, "in", `...${$VAR}...`, ...)
7
+ - pattern: $CLIENT.textSearch($COLUMN, `...${$VAR}...`, ...)
8
+ - pattern-not: '$CLIENT.textSearch(..., {..., type: "plain", ...})'
9
+ - pattern-not: '$CLIENT.textSearch(..., {..., type: "phrase", ...})'
10
+ - pattern-not: '$CLIENT.textSearch(..., {..., type: "websearch", ...})'
11
+ message: An interpolated value enters raw PostgREST filter syntax. Escape it for
12
+ the operator or use a typed filter.
13
+ languages:
14
+ - typescript
15
+ - javascript
16
+ severity: ERROR
17
+ metadata:
18
+ cwe:
19
+ - CWE-20
20
+ category: security
21
+ - id: gspot.supabase.edge-cors-wildcard-with-credentials
22
+ pattern: '{..., "Access-Control-Allow-Origin": "*", ...,
23
+ "Access-Control-Allow-Credentials": "true", ...}'
24
+ message: Wildcard origins cannot be used with credentialed requests. List the
25
+ permitted origins.
26
+ languages:
27
+ - typescript
28
+ - javascript
29
+ severity: ERROR
30
+ paths:
31
+ include:
32
+ - /supabase/functions/**
33
+ metadata:
34
+ cwe:
35
+ - CWE-346
36
+ category: security
37
+ - id: gspot.supabase.edge-unvalidated-json-body
38
+ patterns:
39
+ - pattern: await req.json()
40
+ - pattern-not-inside: |-
41
+ const $BODY = await req.json()
42
+ ...
43
+ $SCHEMA.parse($BODY)
44
+ - pattern-not-inside: |-
45
+ const $BODY = await req.json()
46
+ ...
47
+ $SCHEMA.safeParse($BODY)
48
+ message: Request body consumed without schema validation. Use zod or similar to
49
+ validate the parsed JSON.
50
+ languages:
51
+ - typescript
52
+ - javascript
53
+ severity: ERROR
54
+ paths:
55
+ include:
56
+ - /supabase/functions/**
57
+ metadata:
58
+ cwe:
59
+ - CWE-20
60
+ category: security
@@ -0,0 +1,54 @@
1
+ [configuration]
2
+ title = "Jest"
3
+ requires = ["javascript", "coverage"]
4
+ description = "Jest test rules and coverage floors for repository-owned test suites."
5
+
6
+ [detect]
7
+ dependencies = ["jest"]
8
+ filenames = ["jest.config.*"]
9
+
10
+ [files]
11
+ paths = ["{setting:test_files}", "**/jest.config.*"]
12
+
13
+ [[tool]]
14
+ name = "jest"
15
+ system = true
16
+
17
+ [[tool]]
18
+ name = "eslint-plugin-jest"
19
+ rule_prefix = "jest"
20
+ kind = "library"
21
+ version = "29.16.6"
22
+ npm = "eslint-plugin-jest"
23
+
24
+ [[check]]
25
+ name = "coverage"
26
+ example = "Jest reports `coverage threshold for lines (90%) not met: 85%`. Add tests for the lines its report marks uncovered."
27
+ level = "recommended"
28
+ stage = "push"
29
+ runs = "scope"
30
+ cwd = "scope"
31
+ tool = "jest"
32
+ command = ["jest", "--ci", "--rootDir={root}/{scope}", "--coverage", "--coverageReporters=text-summary", "--coverageThreshold={\"global\":{\"lines\":{setting:coverage.lines},\"branches\":{setting:coverage.branches},\"functions\":{setting:coverage.functions},\"statements\":{setting:coverage.statements}}}"]
33
+ run_in_copy = true
34
+ exit_codes = [1]
35
+ crash_pattern = "^No tests found|^ ● Test suite failed to run"
36
+ summary = "Runs Jest in a disposable source copy and checks all four coverage floors."
37
+ why = "A coverage floor keeps new code from arriving untested."
38
+ help = "Correct failed tests and cover the reported branches, functions, lines, and statements."
39
+
40
+ [check.files]
41
+ extensions = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]
42
+
43
+ [check.output]
44
+ format = "regex"
45
+ pattern = "^Jest: \"global\" (?<message>coverage threshold for .+ not met: .+)$"
46
+
47
+ [eslint]
48
+ all_level_rules = ["jest/prefer-strict-equal"]
49
+
50
+ [[tool_file]]
51
+ target = ".gspot/config/eslint.config.mjs"
52
+ tool = "eslint"
53
+ fragment = true
54
+ required_tools = ["eslint-plugin-jest"]
@@ -0,0 +1,53 @@
1
+ [configuration]
2
+ title = "pytest"
3
+ requires = ["python", "coverage"]
4
+ description = "pytest suites: Ruff pytest style rules over the tests, pytest test-name prefixes, and a coverage floor at push."
5
+
6
+ [detect]
7
+ dependencies = ["pytest"]
8
+ filenames = ["conftest.py", "pytest.ini"]
9
+
10
+ [[tool]]
11
+ name = "pytest"
12
+ install_hint = "Run: python -m pip install pytest"
13
+ system = true
14
+ suppression = { marker = "\\bpragma: ?no cover\\b", reason = "#[^#]*# ?(?<reason>\\S.*)" }
15
+ min_version = "7.0.0"
16
+ pypi = "pytest"
17
+ version_pattern = "(\\d+\\.\\d+\\.\\d+)"
18
+
19
+ [[tool]]
20
+ name = "pytest-cov"
21
+ install_hint = "Install pytest-cov in the Python environment used by pytest."
22
+ kind = "library"
23
+ system = true
24
+ pypi = "pytest-cov"
25
+ version_command = ["pytest", "--version", "--version"]
26
+ version_pattern = "pytest-cov-(\\d+(?:\\.\\d+)+)"
27
+
28
+ [[check]]
29
+ name = "coverage"
30
+ tool = ["pytest", "pytest-cov"]
31
+ example = "pytest-cov reports `FAIL Required test coverage of 90% not reached. Total coverage: 85%`. Add a Python test for the uncovered behavior."
32
+ level = "recommended"
33
+ stage = "push"
34
+ runs = "scope"
35
+ cwd = "scope"
36
+ command = ["pytest", "-q", "--strict-markers", "--strict-config", "--cov", "--cov-fail-under={setting:coverage.lines}", "--no-header", "-p", "no:cacheprovider"]
37
+ exit_codes = [1]
38
+ files = { extensions = [".py"] }
39
+ output = { format = "regex", pattern = "^(?<message>(?:FAIL Required test coverage|FAILED |ERROR ).*)$" }
40
+ summary = "Runs the test suite with coverage, and fails when a test fails or the covered share falls under the floor."
41
+ why = "Python coverage reveals branches and lines the test suite does not exercise before a push."
42
+ help = "Fix the failing test, or add tests for the code the report shows uncovered. Lower coverage.lines with a reason."
43
+
44
+ [ruff_rules]
45
+ recommended = ["PT008", "PT009", "PT010", "PT011", "PT012", "PT013", "PT014", "PT015", "PT016", "PT017", "PT020", "PT021", "PT024", "PT025", "PT027", "PT028", "PT030", "PT031"]
46
+ all = ["PT001", "PT002", "PT003", "PT006", "PT007", "PT018", "PT019", "PT022", "PT023", "PT026"]
47
+
48
+ [[naming.override]]
49
+ paths = ["**/test_*.py", "**/*_test.py", "**/tests/**/*.py", "**/conftest.py"]
50
+ languages = ["python"]
51
+ categories = ["functions", "methods"]
52
+ ignored_prefix = "^test_"
53
+ reason = "The test runner finds a test by this prefix."
@@ -0,0 +1,52 @@
1
+ [configuration]
2
+ title = "Swift Tests"
3
+ requires = ["swift", "coverage"]
4
+ description = "Swift test files under XCTest and Swift Testing. A disabled test says why, no test sleeps, and coverage holds a floor."
5
+
6
+ [detect]
7
+ tags = ["swift-test", "swift-test-target"]
8
+ paths = ["**/*.xctestplan"]
9
+
10
+ [files]
11
+ paths = ["**/*Tests/**/*.swift"]
12
+ filenames = ["*.xctestplan"]
13
+
14
+ [[tool_file]]
15
+ target = ".gspot/config/swiftlint.yml"
16
+ tool = "swiftlint"
17
+ per_scope = true
18
+ fragment = true
19
+ pointer = { path = ".swiftlint.yml", directories = ["**/*Tests"], body = "parent_config: {target_json}\ndisabled_rules:\n - force_unwrapping\n - missing_docs\n - no_magic_numbers\n" }
20
+
21
+ [[check]]
22
+ name = "skip-reasons"
23
+ example = "In a Swift test, `throw XCTSkip()` reports a missing reason. Restore the test, or supply a nonempty reason such as `throw XCTSkip(\"Requires the hardware test device\")`. The check requires a reason; it does not validate when the test will be enabled."
24
+ level = "recommended"
25
+ stage = "commit"
26
+ files = { tags = ["swift-test"] }
27
+ summary = "Checks that skip calls and disabled traits name a nonempty reason, and unavailable tests carry an explanation."
28
+ why = "A test turned off with no reason stays off, because nobody knows when it is safe to turn on."
29
+ help = "Say why the test is off and what turns it back on, or delete the test."
30
+
31
+ [[check]]
32
+ name = "sleep"
33
+ example = "A test using a fixed sleep to wait for asynchronous work reports a timing finding. Wait for the actual completion expectation or poll the required condition with a bounded timeout."
34
+ level = "all"
35
+ stage = "commit"
36
+ files = { tags = ["swift-test"] }
37
+ summary = "Refuses sleep calls in test files."
38
+ why = "A sleep makes a test slow when it passes and flaky when the machine is slower than the number."
39
+ help = "Wait on an expectation or a confirmation, or poll a condition with a timeout."
40
+
41
+ [[check]]
42
+ name = "coverage"
43
+ example = "A Swift target reports 85% line coverage against a 90% floor. Add a test that exercises its uncovered lines, then rerun the target."
44
+ level = "recommended"
45
+ stage = "push"
46
+ runs = "scope"
47
+ needs = ["build"]
48
+ operating_systems = ["macos", "linux"]
49
+ files = { extensions = [".swift"] }
50
+ summary = "Measures Xcode projects and Swift packages, holding each source target against coverage.lines and its coverage.override floor."
51
+ why = "A target-specific coverage floor exposes Swift code that its test target has not exercised."
52
+ help = "Add tests for the target, or lower its floor with a reason."
@@ -0,0 +1,79 @@
1
+ # Swift Tests
2
+
3
+ When you write tests, these rules cover XCTest, Swift Testing, and snapshot tests.
4
+
5
+ ## What to test
6
+
7
+ - Unit test domain entities, value objects, mapping, and application operations. When the
8
+ project uses view models, test their behavior too.
9
+ - Test a use case with mocked repository protocols or network and database clients.
10
+ - Test repository mapping apart from use case behavior.
11
+ - When the project uses view models, assert state transitions and what the application shows to the user, never private method calls.
12
+ - Test invalid payload mapping and validate external data when it enters the program.
13
+ - Test cancellation and stale results for asynchronous operations.
14
+ - Do not mock SwiftUI or test its framework behavior.
15
+ - Do not test trivial getters, setters, or framework behavior.
16
+ - Do not add a protocol only to make a mock when the code already accepts the network or database client as an argument.
17
+
18
+ ## Coverage by layer
19
+
20
+ - Domain tests cover value validation, invariants, domain errors, and computed business properties.
21
+ - Use case tests cover success, repository failure, validation failure, cancellation, and policy decisions.
22
+ - Repository tests cover record mapping and error translation.
23
+ - Network client tests cover the URL, the method, the headers, and the encoded body.
24
+ - Network client tests also cover a non-HTTP response, a status outside 200 to 299, a decoding failure, and cancellation.
25
+ - When the project uses view models, test initial, loading, success, empty, and error states,
26
+ then retry and stale results.
27
+ - When the project uses coordinators, test route stack changes, modal state, dismissal, root
28
+ resets, and deep link translation.
29
+
30
+ ## Test data
31
+
32
+ - Test data shows the behavior under test.
33
+ - Prefer private builders or stubs with sensible defaults when model construction hides the point of the test.
34
+ - Keep test builders beside the tests that use them, unless many test files share them on purpose.
35
+ - Decode JSON test data only when the test covers decoding, mapping, or the format of an external payload.
36
+ - Do not add a production default to make a test shorter.
37
+ - Prefer stable identifiers, dates, clocks, and ordering.
38
+
39
+ ```swift
40
+ private extension Message {
41
+ static func stub(
42
+ id: Message.ID = .init("message-1"),
43
+ subject: String = "Subject",
44
+ sender: Person = .stub()
45
+ ) -> Self {
46
+ Self(id: id, subject: subject, sender: sender)
47
+ }
48
+ }
49
+ ```
50
+
51
+ Test builders belong in test code. A test setup API never becomes a second model layer.
52
+
53
+ ## Style
54
+
55
+ <!-- level: all -->
56
+
57
+ - In XCTest, prefer throwing test methods with `try XCTUnwrap`.
58
+ - In Swift Testing, prefer `try #require`.
59
+ - Avoid `guard` in a test when an assertion helper states the failure directly.
60
+ - Make test helpers and test data private unless several tests need them.
61
+
62
+ ## Disabled tests
63
+
64
+ - Pass the reason to `XCTSkip` or `.disabled`. Name the issue, release, or platform version
65
+ that lets the test run again.
66
+ - Delete a test that nobody plans to turn back on.
67
+
68
+ ## Time
69
+
70
+ - Inject the clock, so a test advances time and never waits for it.
71
+
72
+ ## Snapshot and interface tests
73
+
74
+ - Snapshot test reusable visual states and regressions that unit tests cannot cover.
75
+ - Look at every reference image before committing it.
76
+ - Keep each folder of references beside the test file it belongs to, and delete both together.
77
+ - UI test critical navigation, authentication, submission, purchase, and recovery flows.
78
+
79
+ Name Swift test files `<Type>Tests.swift`.
@@ -0,0 +1,22 @@
1
+ {
2
+ "recommended": {
3
+ "package": "@vitest/eslint-plugin",
4
+ "version": "1.3.4",
5
+ "source": "configs.recommended",
6
+ "blocks": [
7
+ {
8
+ "parser": false,
9
+ "rules": {
10
+ "vitest/expect-expect": "error",
11
+ "vitest/no-identical-title": "error",
12
+ "vitest/no-commented-out-tests": "error",
13
+ "vitest/valid-title": "error",
14
+ "vitest/valid-expect": "error",
15
+ "vitest/valid-describe-callback": "error",
16
+ "vitest/require-local-test-context-for-concurrent-snapshots": "error",
17
+ "vitest/no-import-node-test": "error"
18
+ }
19
+ }
20
+ ]
21
+ }
22
+ }
@@ -0,0 +1,69 @@
1
+ allowed_words = ["Vitest"]
2
+
3
+ [prefix_collisions]
4
+ prefixes = ["vitest"]
5
+
6
+ [configuration]
7
+ title = "Vitest"
8
+ requires = ["javascript", "coverage"]
9
+ description = "Vitest test files: the test rules of ESLint over them, and coverage thresholds at push."
10
+
11
+ [detect]
12
+ dependencies = ["vitest"]
13
+
14
+ [files]
15
+ paths = ["**/vitest.config.*", "**/vitest.workspace.*"]
16
+
17
+ [[tool]]
18
+ name = "vitest"
19
+ install_hint = "Run: npm install --save-dev vitest"
20
+ system = true
21
+ min_version = "2.0.0"
22
+ npm = "vitest"
23
+
24
+ [[tool]]
25
+ name = "@vitest/eslint-plugin"
26
+ rule_prefix = "vitest"
27
+ kind = "library"
28
+ version = "1.3.4"
29
+ npm = "@vitest/eslint-plugin"
30
+
31
+ [[tool]]
32
+ name = "@vitest/coverage-v8"
33
+ install_hint = "Run: npm install --save-dev @vitest/coverage-v8"
34
+ kind = "library"
35
+ system = true
36
+ npm = "@vitest/coverage-v8"
37
+
38
+ [[check]]
39
+ name = "coverage"
40
+ tool = ["vitest", "@vitest/coverage-v8"]
41
+ example = "Vitest reports `ERROR: Coverage for lines (85%) does not meet global threshold (90%)`. Exercise the missing lines before the push check."
42
+ level = "recommended"
43
+ stage = "push"
44
+ runs = "scope"
45
+ cwd = "scope"
46
+ command = ["vitest", "run", "--coverage", "--coverage.reporter=text-summary", "--coverage.thresholds.lines={setting:coverage.lines}", "--coverage.thresholds.branches={setting:coverage.branches}", "--coverage.thresholds.functions={setting:coverage.functions}", "--coverage.thresholds.statements={setting:coverage.statements}"]
47
+ summary = "Runs the test suite with coverage, and fails when lines, branches, functions, or statements fall under their floors."
48
+ why = "A floor keeps new code from arriving untested, which is when its behavior is cheapest to pin down."
49
+ help = "Add tests for the code the coverage report shows uncovered, or lower one floor under coverage with a reason."
50
+
51
+ [check.files]
52
+ extensions = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]
53
+
54
+ [check.output]
55
+ format = "regex"
56
+ pattern = "^ERROR: (?<message>Coverage for .*)$"
57
+
58
+ [eslint]
59
+ all_level_rules = ["vitest/no-import-node-test", "vitest/prefer-strict-equal", "vitest/valid-title"]
60
+
61
+ [eslint.presets.recommended]
62
+ package = "@vitest/eslint-plugin"
63
+ source = "configs.recommended"
64
+
65
+ [[tool_file]]
66
+ target = ".gspot/config/eslint.config.mjs"
67
+ tool = "eslint"
68
+ fragment = true
69
+ required_tools = ["@vitest/eslint-plugin"]
@@ -0,0 +1,18 @@
1
+ # Vitest
2
+
3
+ The general testing rules apply. Use the Vitest APIs for its runtime boundaries.
4
+
5
+ - Use `vi.stubGlobal('fetch', ...)` or the project's interceptor to control external HTTP. Restore
6
+ stubbed globals through `vi.unstubAllGlobals()`.
7
+ - Use `vi.mocked()` for typed access to a mocked dependency. A partial module mock calls
8
+ `importOriginal` when the unchanged exports still matter.
9
+ - Restore fake timers with `vi.useRealTimers()` and mock implementations with `vi.restoreAllMocks()`.
10
+ - Use `it.each` for one behavior across several inputs. Stop `describe()` nesting when it separates
11
+ no distinct behavior.
12
+
13
+ ```ts
14
+ vi.mock('./email-client.js', async (importOriginal) => ({
15
+ ...(await importOriginal()),
16
+ sendEmail: vi.fn(),
17
+ }));
18
+ ```