acuvo-code 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +328 -0
- package/ENTERPRISE.md +927 -0
- package/LICENSE +120 -0
- package/README.md +1245 -0
- package/ROADMAP.md +556 -0
- package/bin/acuvo-mcp.mjs +208 -0
- package/bin/acuvo.mjs +3108 -0
- package/lib/acceptance-consent.mjs +168 -0
- package/lib/acceptance.mjs +859 -0
- package/lib/account.mjs +226 -0
- package/lib/acuvo-dir.mjs +72 -0
- package/lib/acuvo-models.mjs +141 -0
- package/lib/apply-patch.mjs +570 -0
- package/lib/ask-user.mjs +173 -0
- package/lib/audit.mjs +530 -0
- package/lib/auto-lease.mjs +174 -0
- package/lib/background.mjs +842 -0
- package/lib/best-of.mjs +334 -0
- package/lib/board.mjs +232 -0
- package/lib/breaker.mjs +93 -0
- package/lib/budget.mjs +1375 -0
- package/lib/builtin-skills.mjs +135 -0
- package/lib/cache-floor.mjs +204 -0
- package/lib/chain.mjs +303 -0
- package/lib/changed-paths.mjs +84 -0
- package/lib/chat.mjs +434 -0
- package/lib/checkpoint.mjs +637 -0
- package/lib/child-lifetime.mjs +71 -0
- package/lib/cli-args.mjs +1255 -0
- package/lib/code-review.mjs +1382 -0
- package/lib/colour.mjs +82 -0
- package/lib/command.mjs +2847 -0
- package/lib/compact.mjs +1151 -0
- package/lib/completion.mjs +515 -0
- package/lib/creative-engines.mjs +779 -0
- package/lib/db-inspect.mjs +1624 -0
- package/lib/delete.mjs +111 -0
- package/lib/design-loop.mjs +570 -0
- package/lib/diff-preview.mjs +1044 -0
- package/lib/doctor.mjs +2139 -0
- package/lib/dropped.mjs +216 -0
- package/lib/edit-diagnostics.mjs +277 -0
- package/lib/edit.mjs +460 -0
- package/lib/env-file.mjs +250 -0
- package/lib/escalate.mjs +702 -0
- package/lib/evaluate.mjs +284 -0
- package/lib/fetch-text.mjs +952 -0
- package/lib/fleet-budget.mjs +256 -0
- package/lib/gh.mjs +1536 -0
- package/lib/git.mjs +1341 -0
- package/lib/github.mjs +261 -0
- package/lib/h2.mjs +194 -0
- package/lib/handoff.mjs +417 -0
- package/lib/hooks.mjs +626 -0
- package/lib/http-probe.mjs +907 -0
- package/lib/image-director.mjs +322 -0
- package/lib/image-edit.mjs +522 -0
- package/lib/imagegen.mjs +998 -0
- package/lib/interrupt.mjs +234 -0
- package/lib/learned.mjs +353 -0
- package/lib/lease-watch.mjs +115 -0
- package/lib/lease.mjs +868 -0
- package/lib/localize.mjs +834 -0
- package/lib/log-tail.mjs +1052 -0
- package/lib/login.mjs +157 -0
- package/lib/lsp.mjs +1613 -0
- package/lib/mcp-consent.mjs +377 -0
- package/lib/mcp-defaults.mjs +780 -0
- package/lib/mcp-server.mjs +1343 -0
- package/lib/mcp.mjs +1263 -0
- package/lib/media.mjs +1283 -0
- package/lib/memory-workspace.mjs +179 -0
- package/lib/model-json.mjs +132 -0
- package/lib/model-tier.mjs +171 -0
- package/lib/model.mjs +1445 -0
- package/lib/parallel.mjs +144 -0
- package/lib/perchance.mjs +210 -0
- package/lib/plan-coherence.mjs +1461 -0
- package/lib/plan-ledger.mjs +981 -0
- package/lib/plan.mjs +461 -0
- package/lib/policy.mjs +783 -0
- package/lib/prefix-order.mjs +38 -0
- package/lib/project-memory.mjs +127 -0
- package/lib/prompt.mjs +109 -0
- package/lib/python.mjs +862 -0
- package/lib/rcfile.mjs +853 -0
- package/lib/read-window.mjs +743 -0
- package/lib/refute-tools.mjs +34 -0
- package/lib/refute.mjs +806 -0
- package/lib/repl-driver.mjs +264 -0
- package/lib/repl.mjs +324 -0
- package/lib/replay.mjs +1218 -0
- package/lib/repo-map.mjs +1101 -0
- package/lib/report.mjs +419 -0
- package/lib/search-rank.mjs +99 -0
- package/lib/search.mjs +659 -0
- package/lib/secret-paths.mjs +54 -0
- package/lib/session.mjs +1017 -0
- package/lib/skills.mjs +703 -0
- package/lib/slash.mjs +356 -0
- package/lib/spawn-argv.mjs +1151 -0
- package/lib/spend.mjs +250 -0
- package/lib/steer.mjs +280 -0
- package/lib/stream.mjs +253 -0
- package/lib/stuck.mjs +712 -0
- package/lib/subagent.mjs +749 -0
- package/lib/terminal-graphics.mjs +171 -0
- package/lib/tool-prefix.mjs +226 -0
- package/lib/tool-shortlist.mjs +162 -0
- package/lib/tools.mjs +2333 -0
- package/lib/tsserver.mjs +423 -0
- package/lib/turn.mjs +5672 -0
- package/lib/untrusted-block.mjs +271 -0
- package/lib/verify-claim.mjs +299 -0
- package/lib/vision.mjs +330 -0
- package/lib/voice-task.mjs +561 -0
- package/lib/warm-provider.mjs +255 -0
- package/lib/websearch.mjs +401 -0
- package/lib/workspace.mjs +928 -0
- package/lib/write-approval.mjs +235 -0
- package/lib/write-many.mjs +162 -0
- package/package.json +62 -0
- package/scripts/bundle.mjs +768 -0
- package/scripts/cache-floor.mjs +176 -0
- package/scripts/machine.mjs +226 -0
- package/scripts/test.mjs +139 -0
- package/skills/accessibility.md +87 -0
- package/skills/acuvo-design-system.md +123 -0
- package/skills/animation.md +84 -0
- package/skills/api-design.md +82 -0
- package/skills/auth-and-sessions.md +78 -0
- package/skills/build-with-a-framework.md +101 -0
- package/skills/colour-and-contrast.md +112 -0
- package/skills/creative-engines.md +81 -0
- package/skills/css-layout.md +85 -0
- package/skills/data-and-charts.md +77 -0
- package/skills/debugging.md +76 -0
- package/skills/designing-by-looking.md +84 -0
- package/skills/error-handling.md +78 -0
- package/skills/forms-and-validation.md +93 -0
- package/skills/nextjs-app-router.md +75 -0
- package/skills/page-composition.md +103 -0
- package/skills/performance.md +77 -0
- package/skills/plan-before-building.md +52 -0
- package/skills/planning-and-delegating.md +72 -0
- package/skills/refactoring.md +70 -0
- package/skills/security-basics.md +76 -0
- package/skills/state-management.md +73 -0
- package/skills/supabase-multitenant.md +72 -0
- package/skills/typescript-strict.md +90 -0
- package/skills/typography.md +135 -0
- package/skills/verify-your-own-work.md +62 -0
- package/skills/web-app-quality.md +62 -0
- package/skills/working-in-the-background.md +64 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: performance
|
|
3
|
+
description: Measure before optimising — images, bundle size, the render loop, and the costs that dominate
|
|
4
|
+
when: When a page feels slow, before optimising anything, or when adding a dependency or an image
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Performance
|
|
8
|
+
|
|
9
|
+
## ⚠️⚠️ Measure first. Optimising by intuition is how a day disappears.
|
|
10
|
+
|
|
11
|
+
Intuition about what is slow is wrong often enough that acting on it unmeasured
|
|
12
|
+
is gambling. Open the profiler, or time it:
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
console.time('render'); doTheThing(); console.timeEnd('render');
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
⭐ And know what "slow" means here — a 400ms function called once on load is
|
|
19
|
+
irrelevant; a 4ms function called 3,000 times in a scroll handler is the bug.
|
|
20
|
+
|
|
21
|
+
## The costs that actually dominate a web page
|
|
22
|
+
|
|
23
|
+
In rough order of how often they matter:
|
|
24
|
+
|
|
25
|
+
1. **Images.** Usually most of the bytes. Wrong-sized hero images are the single
|
|
26
|
+
most common cause of a slow page.
|
|
27
|
+
2. **Blocking requests** in the head — a font, a stylesheet, a synchronous
|
|
28
|
+
script — each one delays first paint.
|
|
29
|
+
3. **JavaScript bundle size** — download, then parse, then execute.
|
|
30
|
+
4. **Layout thrash** — reading a layout property after writing one, in a loop.
|
|
31
|
+
5. Everything else.
|
|
32
|
+
|
|
33
|
+
## Images
|
|
34
|
+
|
|
35
|
+
- Size them for the box they land in. A 4000px photo in a 400px card wastes 99%
|
|
36
|
+
of its bytes.
|
|
37
|
+
- Modern format (WebP/AVIF) with a fallback.
|
|
38
|
+
- `loading="lazy"` for anything below the fold — never for the hero, which is
|
|
39
|
+
the one thing you want early.
|
|
40
|
+
- **Always set `width` and `height`** (or `aspect-ratio`). Without them the page
|
|
41
|
+
reflows when each image arrives, which is both ugly and a Core Web Vital.
|
|
42
|
+
|
|
43
|
+
## The render loop
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
✗ items.forEach(i => list.appendChild(render(i))); // layout per item
|
|
47
|
+
✓ const frag = document.createDocumentFragment();
|
|
48
|
+
items.forEach(i => frag.appendChild(render(i)));
|
|
49
|
+
list.appendChild(frag); // one layout
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
⚠️ **Reading forces a recalculation.** `offsetHeight`, `getBoundingClientRect`
|
|
53
|
+
and friends flush pending layout. Reading one in a loop that also writes makes
|
|
54
|
+
the browser recompute every iteration. Batch: read everything, then write
|
|
55
|
+
everything.
|
|
56
|
+
|
|
57
|
+
Debounce input handlers; throttle scroll and resize.
|
|
58
|
+
|
|
59
|
+
## ⚠️ Every dependency is bytes the user downloads
|
|
60
|
+
|
|
61
|
+
Before adding one, check what it costs and whether a few lines would do. A date
|
|
62
|
+
library for one `toLocaleDateString` is a bad trade. Import only what you use —
|
|
63
|
+
`import { debounce } from 'lodash-es'`, never the whole namespace.
|
|
64
|
+
|
|
65
|
+
## Do less work
|
|
66
|
+
|
|
67
|
+
The fastest request is the one not made, and the fastest render is the one
|
|
68
|
+
skipped. Cache what does not change; paginate instead of loading everything
|
|
69
|
+
(see `api-design`); do not recompute a derived value on every render when it
|
|
70
|
+
only changes when its inputs do.
|
|
71
|
+
|
|
72
|
+
## ⚠️ Perceived speed is real speed
|
|
73
|
+
|
|
74
|
+
A skeleton that appears in 100ms feels faster than a blank screen for 400ms and
|
|
75
|
+
then everything at once — same total time. Show structure early, respond to
|
|
76
|
+
input immediately even if the result takes a moment, and never leave a click
|
|
77
|
+
with no visible acknowledgement.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plan-before-building
|
|
3
|
+
description: Use the planning verbs and the whiteboard to think before writing code
|
|
4
|
+
when: Any task with more than about three steps, or any ambiguous request
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Think first, then build
|
|
8
|
+
|
|
9
|
+
## Why this pays
|
|
10
|
+
|
|
11
|
+
An agent that starts typing immediately rewrites files it already wrote, loses
|
|
12
|
+
track of what is done, and produces the same file three times. Planning is not
|
|
13
|
+
ceremony — it is what keeps a long build coherent.
|
|
14
|
+
|
|
15
|
+
Use `plan_start` / `plan_step` / `plan_status`. They exist so the plan survives
|
|
16
|
+
the context, not just your intention.
|
|
17
|
+
|
|
18
|
+
## The shape of a good plan
|
|
19
|
+
|
|
20
|
+
1. **Restate the goal in one sentence.** If you cannot, you do not have one yet.
|
|
21
|
+
2. **List what you do not know** and how you will find out — read a file, run a
|
|
22
|
+
command, check a schema. Do this BEFORE writing code that assumes an answer.
|
|
23
|
+
3. **Order by risk, not by ease.** The step most likely to invalidate the others
|
|
24
|
+
goes first. Discovering the API shape after building the UI around it is the
|
|
25
|
+
expensive order.
|
|
26
|
+
4. **Name what "done" looks like** in terms someone else could check.
|
|
27
|
+
|
|
28
|
+
## Read before you write
|
|
29
|
+
|
|
30
|
+
Before editing a file, read it. Before adding a helper, search for one that
|
|
31
|
+
exists. Before designing a schema, look at the tables already there.
|
|
32
|
+
|
|
33
|
+
Most duplicated code is not a naming failure — it is someone writing before
|
|
34
|
+
looking. `list_files` and `search_text` are cheap; a second implementation of an
|
|
35
|
+
existing thing is not.
|
|
36
|
+
|
|
37
|
+
## Use the whiteboard to think, not just to output
|
|
38
|
+
|
|
39
|
+
A whiteboard or diagram is most valuable BEFORE the code: laying out the screens,
|
|
40
|
+
the data flow, the states. Using it only to render a finished result wastes the
|
|
41
|
+
half where it actually helps.
|
|
42
|
+
|
|
43
|
+
Sketch: what are the screens, what data does each need, where does that data come
|
|
44
|
+
from, what happens when it is missing.
|
|
45
|
+
|
|
46
|
+
## When to stop and ask
|
|
47
|
+
|
|
48
|
+
Ask when two readings of the request produce materially different work and you
|
|
49
|
+
cannot tell which is meant. Do not ask about things you can determine yourself by
|
|
50
|
+
reading the code — that is slower for everyone and it is your job.
|
|
51
|
+
|
|
52
|
+
State the assumption and keep moving when the cost of being wrong is low.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: planning-and-delegating
|
|
3
|
+
description: Multi-deliverable work — declare the plan, track it honestly, delegate to save context
|
|
4
|
+
when: When a task has more than one deliverable, when the user names commands that must pass, or when a subtask would flood your context
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Planning And Delegating
|
|
8
|
+
|
|
9
|
+
## ⭐⭐ Declare the deliverables BEFORE starting
|
|
10
|
+
|
|
11
|
+
`plan_start` records what a task's deliverables are. Every later tool result
|
|
12
|
+
then carries a line telling you how many remain.
|
|
13
|
+
|
|
14
|
+
**Why it matters more than it looks:** the common failure on a five-part request
|
|
15
|
+
is not doing a part badly, it is **finishing three and reporting done.** A plan
|
|
16
|
+
you cannot see is a plan you will drift off. The reminder rides along with work
|
|
17
|
+
you were doing anyway.
|
|
18
|
+
|
|
19
|
+
Use it whenever the request has more than one deliverable — *"add the endpoint,
|
|
20
|
+
write the test, and update the docs"* is three, not one.
|
|
21
|
+
|
|
22
|
+
## ⚠️⚠️ `plan_step` done means the deliverable EXISTS and you CHECKED
|
|
23
|
+
|
|
24
|
+
Marking a step done because you wrote the code that should produce it is how a
|
|
25
|
+
plan becomes fiction. **Nothing else marks a step done** — so if the plan says
|
|
26
|
+
done, that is a claim you made, and the user will read it as verified.
|
|
27
|
+
|
|
28
|
+
`blocked` is a real state and an honest one. Use it. A blocked step that says so
|
|
29
|
+
is worth far more than a done step that is not.
|
|
30
|
+
|
|
31
|
+
⭐ **`plan_status` when you have lost the thread** — after a long detour, or
|
|
32
|
+
resuming. Cheaper than re-reading the conversation, and it is authoritative.
|
|
33
|
+
|
|
34
|
+
## ⭐⭐⭐ Acceptance criteria are the USER'S words, recorded early
|
|
35
|
+
|
|
36
|
+
When the user says *"it's done when `npm test` passes"*, call
|
|
37
|
+
**`declare_acceptance` with their command, verbatim, ONCE, before doing the
|
|
38
|
+
work.**
|
|
39
|
+
|
|
40
|
+
⚠️ **A criterion chosen AFTER the work is a criterion chosen to pass.** That is
|
|
41
|
+
the whole reason it is recorded first — a test you picked because it goes green
|
|
42
|
+
proves nothing about what was asked.
|
|
43
|
+
|
|
44
|
+
**`check_acceptance` is the only thing that clears them.** Running the command
|
|
45
|
+
yourself and seeing it pass does not; `evaluate` returning true does not. If the
|
|
46
|
+
criteria are outstanding, the work is outstanding, however good the code looks.
|
|
47
|
+
|
|
48
|
+
## ⭐⭐ `delegate` protects the thing you cannot get back: context
|
|
49
|
+
|
|
50
|
+
Hand off a self-contained piece of work and you get back **a short summary, not
|
|
51
|
+
everything the helper read.** The helper burns its own context; yours stays for
|
|
52
|
+
the work only you can do.
|
|
53
|
+
|
|
54
|
+
**Good delegation:** *"find every call site of `readRows` and report which ones
|
|
55
|
+
ignore the status field"* — a big search, a small answer.
|
|
56
|
+
|
|
57
|
+
⚠️ **Bad delegation:** anything where you need the intermediate detail, or where
|
|
58
|
+
the task is not self-contained. The helper cannot see your conversation. If the
|
|
59
|
+
brief needs three paragraphs of background, you are better off doing it.
|
|
60
|
+
|
|
61
|
+
⭐ **It only READS by default.** That is the safe setting and it should stay the
|
|
62
|
+
default in your head: send it to find out, come back to decide.
|
|
63
|
+
|
|
64
|
+
## ⭐ `remember` for what a future session would otherwise get WRONG
|
|
65
|
+
|
|
66
|
+
Not a diary. One durable fact, with **why**, and only if a future session would
|
|
67
|
+
otherwise repeat a mistake or rediscover something expensive.
|
|
68
|
+
|
|
69
|
+
*"The dev server needs `npm install` first — `node_modules` ships incomplete"* is
|
|
70
|
+
worth remembering. *"Fixed the header"* is not.
|
|
71
|
+
|
|
72
|
+
Related skills: `plan-before-building`, `verify-your-own-work`, `debugging`.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: refactoring
|
|
3
|
+
description: Changing code safely without a rewrite — small steps, a green check between each, no behaviour change
|
|
4
|
+
when: Before restructuring code, when tempted to rewrite, or when a change touches many files
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Refactoring
|
|
8
|
+
|
|
9
|
+
## ⚠️⚠️ Refactoring means behaviour does NOT change
|
|
10
|
+
|
|
11
|
+
If the output changes, it is not a refactor — it is a rewrite with a reassuring
|
|
12
|
+
name, and it will be reviewed as if nothing could have broken.
|
|
13
|
+
|
|
14
|
+
Do them one at a time:
|
|
15
|
+
|
|
16
|
+
- **Refactor**, verify it still behaves identically, commit.
|
|
17
|
+
- **Then** change the behaviour, in its own commit.
|
|
18
|
+
|
|
19
|
+
Mixing the two produces a diff where nobody — including you next month — can
|
|
20
|
+
tell which lines were meant to change anything.
|
|
21
|
+
|
|
22
|
+
## Small steps with a green check between each
|
|
23
|
+
|
|
24
|
+
The safety comes from the size of the step, not from care. Rename, run. Extract,
|
|
25
|
+
run. Move, run. If something breaks you know exactly what did it, because you
|
|
26
|
+
did exactly one thing.
|
|
27
|
+
|
|
28
|
+
⚠️ A refactor that cannot be verified between steps is a rewrite. **Get a check
|
|
29
|
+
in place FIRST** — if the code has no test, the first commit is a test that
|
|
30
|
+
pins current behaviour, before you touch anything.
|
|
31
|
+
|
|
32
|
+
⭐ And confirm that check can fail: break the code deliberately, see it go red,
|
|
33
|
+
restore. A green suite that would stay green through a mistake gives you
|
|
34
|
+
confidence you have not earned (`verify-your-own-work`).
|
|
35
|
+
|
|
36
|
+
## ⚠️ The rewrite trap
|
|
37
|
+
|
|
38
|
+
"This is a mess, I will just rewrite it" loses everything the mess encodes: the
|
|
39
|
+
edge cases, the bug fixes, the reason for the strange branch on line 40 that
|
|
40
|
+
turns out to be a customer's data. The ugly code has been in production; the
|
|
41
|
+
clean replacement has not.
|
|
42
|
+
|
|
43
|
+
Rewrite when the requirements genuinely changed. Refactor when the code is hard
|
|
44
|
+
to work with. They are different problems and only one is solved by starting
|
|
45
|
+
over.
|
|
46
|
+
|
|
47
|
+
## What to do first
|
|
48
|
+
|
|
49
|
+
1. **Rename** to what things actually are. Free, reversible, and often the
|
|
50
|
+
whole problem — a lot of "confusing code" is code with lying names.
|
|
51
|
+
2. **Extract** a well-named function from a comment that says what the next ten
|
|
52
|
+
lines do.
|
|
53
|
+
3. **Delete** what nothing calls. Dead code is read, maintained and believed by
|
|
54
|
+
everyone who comes after.
|
|
55
|
+
4. **Split** only once the seams are obvious. Splitting early puts boundaries in
|
|
56
|
+
the wrong places, and a wrong boundary is harder to remove than no boundary.
|
|
57
|
+
|
|
58
|
+
## Duplication is cheaper than the wrong abstraction
|
|
59
|
+
|
|
60
|
+
Two similar blocks that evolve differently are fine. One "shared" helper with
|
|
61
|
+
four boolean flags to serve four callers is worse than the duplication it
|
|
62
|
+
replaced. **Wait for the third occurrence** before generalising — by then you
|
|
63
|
+
can see what is genuinely common.
|
|
64
|
+
|
|
65
|
+
## Leave the campsite tidy, not rebuilt
|
|
66
|
+
|
|
67
|
+
Touching a file is a good moment to fix the name you had to squint at. It is not
|
|
68
|
+
a good moment to restructure the module — that lands in someone's review of an
|
|
69
|
+
unrelated change, and it is where "small fix" becomes a 400-line diff nobody can
|
|
70
|
+
approve with confidence.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: security-basics
|
|
3
|
+
description: The handful of vulnerabilities that actually occur — XSS, injection, secrets, and safe defaults
|
|
4
|
+
when: When handling user input, rendering it back, calling a database, or touching a credential
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Security basics
|
|
8
|
+
|
|
9
|
+
Not the whole OWASP list — the few that actually turn up in application code.
|
|
10
|
+
|
|
11
|
+
## ⚠️⚠️ XSS: never build HTML by concatenating input
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
✗ el.innerHTML = `<p>Hello ${name}</p>`; // name = "<img src=x onerror=alert(1)>"
|
|
15
|
+
✓ el.textContent = `Hello ${name}`;
|
|
16
|
+
✓ el.append(document.createTextNode(name));
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`textContent` cannot execute anything. Reach for `innerHTML` only with a string
|
|
20
|
+
you built entirely yourself, and treat every alternative as better.
|
|
21
|
+
|
|
22
|
+
Also XSS, and easier to miss:
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
✗ <a href={userSupplied}> // "javascript:…" runs on click
|
|
26
|
+
✗ el.setAttribute('onclick', …)
|
|
27
|
+
✗ eval(…) / new Function(userInput) / setTimeout("string")
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
For a URL, check the scheme against an allowlist (`https:`, `http:`, `mailto:`)
|
|
31
|
+
rather than looking for bad ones.
|
|
32
|
+
|
|
33
|
+
## Injection: the query is code, the input is data
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
✗ db.query(`SELECT * FROM users WHERE email = '${email}'`)
|
|
37
|
+
✓ db.query('SELECT * FROM users WHERE email = $1', [email])
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Parameterise. Escaping by hand fails on the case you did not think of. The same
|
|
41
|
+
rule applies to shell commands: pass an argument array, never build a string.
|
|
42
|
+
|
|
43
|
+
## ⚠️⚠️ Secrets
|
|
44
|
+
|
|
45
|
+
- Never in client code. Anything shipped to a browser is public — a "hidden"
|
|
46
|
+
API key in a bundle is a published API key.
|
|
47
|
+
- Never in the repository. `.env` is gitignored; `.env.example` holds the names
|
|
48
|
+
with no values.
|
|
49
|
+
- Never in a log line, an error message, or a URL (URLs land in history and in
|
|
50
|
+
access logs).
|
|
51
|
+
- A leaked key is rotated, not deleted from the history — assume it was read.
|
|
52
|
+
|
|
53
|
+
## CSRF
|
|
54
|
+
|
|
55
|
+
A cookie-authenticated state-changing request needs more than the cookie, since
|
|
56
|
+
the browser attaches it automatically from any origin. `SameSite=Lax` covers
|
|
57
|
+
most of it; add a token for the rest. `GET` must never change anything.
|
|
58
|
+
|
|
59
|
+
## Validate on the server, always
|
|
60
|
+
|
|
61
|
+
The client's validation is for the user's benefit. Re-check types, ranges,
|
|
62
|
+
lengths and permissions on arrival — every value, every time, including ones a
|
|
63
|
+
dropdown "could only" have produced.
|
|
64
|
+
|
|
65
|
+
## Safe defaults
|
|
66
|
+
|
|
67
|
+
- **Deny by default.** New endpoint, no explicit permission check → it should
|
|
68
|
+
fail closed, not open.
|
|
69
|
+
- **Least privilege.** The database user for the app does not need `DROP`.
|
|
70
|
+
- **Do not tell an attacker why.** "Email or password is incorrect" — not
|
|
71
|
+
"no such user", which turns your login form into a list of who has an account.
|
|
72
|
+
|
|
73
|
+
## ⚠️ Dependencies are your code
|
|
74
|
+
|
|
75
|
+
A package you added runs with your permissions. Prefer fewer, prefer maintained,
|
|
76
|
+
and read what a postinstall script does before it runs on your machine.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: state-management
|
|
3
|
+
description: One source of truth, derive the rest, and knowing when local state stops being enough
|
|
4
|
+
when: When adding state, when two components disagree, or before reaching for a state library
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# State
|
|
8
|
+
|
|
9
|
+
## ⚠️⚠️ One source of truth. Derive everything else.
|
|
10
|
+
|
|
11
|
+
The bug where two parts of the screen disagree is almost always two copies of
|
|
12
|
+
one fact.
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
✗ let items = [...]; let itemCount = 0; // now they can differ, and they will
|
|
16
|
+
✓ let items = [...]; const count = items.length; // derived, cannot drift
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
If a value can be computed from another value, **compute it**. Store only what
|
|
20
|
+
cannot be derived. A cached derived value is a second source of truth wearing a
|
|
21
|
+
disguise.
|
|
22
|
+
|
|
23
|
+
## Where state should live
|
|
24
|
+
|
|
25
|
+
In order — take the first that works:
|
|
26
|
+
|
|
27
|
+
1. **In the component that uses it.** Most state is local and should stay there.
|
|
28
|
+
2. **In the nearest common parent**, when two siblings need it.
|
|
29
|
+
3. **In the URL**, when it should survive a refresh or be shareable — filters,
|
|
30
|
+
the current tab, a search query, pagination. This is the most under-used
|
|
31
|
+
option and it is free.
|
|
32
|
+
4. **On the server**, when it is data rather than interface state.
|
|
33
|
+
5. **In a global store**, only when many distant components genuinely need it.
|
|
34
|
+
|
|
35
|
+
⚠️ Reaching for a global store first makes every piece of state everyone's
|
|
36
|
+
business, and nothing can then be changed locally with confidence.
|
|
37
|
+
|
|
38
|
+
## Server data is not UI state
|
|
39
|
+
|
|
40
|
+
Data fetched from an API has its own concerns — loading, error, stale, refetch,
|
|
41
|
+
cache — and modelling it as plain state means re-implementing all of them badly.
|
|
42
|
+
Keep it separate from interface state (which panel is open, what is typed).
|
|
43
|
+
|
|
44
|
+
⭐ And remember the third outcome: a request is **loading**, **failed**, or
|
|
45
|
+
**succeeded**. A component modelling only "have data / no data" shows an empty
|
|
46
|
+
state during loading and after a failure, which are three different things
|
|
47
|
+
flattened into one. See `error-handling`.
|
|
48
|
+
|
|
49
|
+
## Never mutate what you are about to compare
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
✗ state.items.push(x); // same reference — a change detector sees nothing
|
|
53
|
+
✓ state = { ...state, items: [...state.items, x] };
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This is the cause of "the data is right but the screen did not update".
|
|
57
|
+
|
|
58
|
+
## ⚠️ Do not put derived data in the URL or in storage
|
|
59
|
+
|
|
60
|
+
Persist the minimum: an id, not the whole object it points at; a filter, not the
|
|
61
|
+
filtered result. Anything stored is a copy that can go stale, and stored copies
|
|
62
|
+
outlive the code that wrote them — `localStorage` from a version you shipped
|
|
63
|
+
last year will be handed to today's code.
|
|
64
|
+
|
|
65
|
+
Validate anything read back from storage. It is untrusted input: the user can
|
|
66
|
+
edit it, and an old version of your app may have written it.
|
|
67
|
+
|
|
68
|
+
## Keep updates close to the event
|
|
69
|
+
|
|
70
|
+
State that changes in five places for one user action is state nobody can
|
|
71
|
+
follow. One action → one update → the screen follows from the new state. If you
|
|
72
|
+
cannot say what a click changes in a sentence, the shape is wrong, not the
|
|
73
|
+
library.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: supabase-multitenant
|
|
3
|
+
description: Supabase with real multi-tenant safety — RLS, service role, and the leaks that look like working code
|
|
4
|
+
when: Reading or writing any database table, or adding a migration
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Supabase, multi-tenant
|
|
8
|
+
|
|
9
|
+
## The two clients, and why mixing them leaks data
|
|
10
|
+
|
|
11
|
+
- **Anon / user client** — respects Row Level Security. Use for anything a user
|
|
12
|
+
triggers about their own data.
|
|
13
|
+
- **Service role** — BYPASSES RLS entirely. Use only in operator/admin paths,
|
|
14
|
+
and never where a tenant id came from the request.
|
|
15
|
+
|
|
16
|
+
⚠️⚠️ **The classic leak looks like working code:**
|
|
17
|
+
```ts
|
|
18
|
+
// WRONG — the client chose whose data to read
|
|
19
|
+
const { data } = await admin.from('invoices').select('*').eq('tenant_id', body.tenantId);
|
|
20
|
+
```
|
|
21
|
+
Derive the tenant from the session, never from the payload.
|
|
22
|
+
|
|
23
|
+
## RLS: a GRANT and a POLICY are different things
|
|
24
|
+
|
|
25
|
+
- `GRANT` missing → `42501 permission denied`
|
|
26
|
+
- `POLICY` missing → **zero rows, no error**
|
|
27
|
+
|
|
28
|
+
⚠️ The second is the dangerous one: it renders as an innocent empty state. If a
|
|
29
|
+
table "has no rows" and you are sure it should, check the policy before the code.
|
|
30
|
+
|
|
31
|
+
```sql
|
|
32
|
+
alter table app.invoices enable row level security;
|
|
33
|
+
grant select on app.invoices to authenticated;
|
|
34
|
+
create policy invoices_own on app.invoices
|
|
35
|
+
for select to authenticated
|
|
36
|
+
using (tenant_id in (select tenant_id from app.tenant_users where user_id = auth.uid()));
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
⚠️ **A VIEW does not inherit the underlying table's RLS** — it runs with the
|
|
40
|
+
definer's rights. A view over a tenant table is a leak unless it filters
|
|
41
|
+
explicitly or is operator-only.
|
|
42
|
+
|
|
43
|
+
## Reading honestly
|
|
44
|
+
|
|
45
|
+
A failed read and an empty result are DIFFERENT FACTS and must render
|
|
46
|
+
differently. Never `?? []`:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
const { data, error } = await supabase.from('x').select('*');
|
|
50
|
+
if (error) return { rows: [], unreadable: error.message }; // "we could not read"
|
|
51
|
+
return { rows: data ?? [], unreadable: null }; // "there is nothing"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Counting and caps
|
|
55
|
+
|
|
56
|
+
`.limit(n)` without a count silently turns a partial sample into a confident
|
|
57
|
+
total. Ask for the count and say when you hit the cap:
|
|
58
|
+
```ts
|
|
59
|
+
const { data, count } = await supabase.from('x').select('*', { count: 'exact' }).limit(5000);
|
|
60
|
+
const truncated = (count ?? 0) > (data?.length ?? 0);
|
|
61
|
+
```
|
|
62
|
+
⭐ The real fix at scale is aggregating in SQL (a view with `sum()`/`group by`)
|
|
63
|
+
so the row count stops mattering. Raising the limit only moves the cliff.
|
|
64
|
+
|
|
65
|
+
## Migrations
|
|
66
|
+
|
|
67
|
+
- Number them sequentially and **check the highest number on disk first** — two
|
|
68
|
+
files claiming one number breaks whoever applies them next.
|
|
69
|
+
- Idempotent always: `add column if not exists`, `create index if not exists`.
|
|
70
|
+
- A `GENERATED` column cannot be written to. If code needs to insert it, it must
|
|
71
|
+
be a plain column.
|
|
72
|
+
- Add the `GRANT` and the `POLICY` in the same migration as the table.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: typescript-strict
|
|
3
|
+
description: The types that catch real bugs versus the ones that are ceremony — and why `any` costs more than it saves
|
|
4
|
+
when: When adding types, when tempted to write `any` or a cast, or when configuring tsconfig
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# TypeScript
|
|
8
|
+
|
|
9
|
+
## Turn `strict` on. It is where the value is.
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{ "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true } }
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Without `strict`, `null` and `undefined` are assignable to everything and the
|
|
16
|
+
compiler cannot catch the single most common runtime error there is. Without
|
|
17
|
+
`noUncheckedIndexedAccess`, `arr[10]` is typed as present when it is not.
|
|
18
|
+
|
|
19
|
+
## ⚠️⚠️ `any` does not silence one error, it disables checking downstream
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
✗ const data: any = await res.json();
|
|
23
|
+
data.user.name // no error here, and no error anywhere it flows
|
|
24
|
+
✓ const data: unknown = await res.json();
|
|
25
|
+
// now you MUST narrow it — which is the check you actually wanted
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`unknown` is the honest version of `any`. It says "we do not know yet" and
|
|
29
|
+
forces exactly one narrowing at the boundary, instead of letting a wrong shape
|
|
30
|
+
travel silently through ten functions.
|
|
31
|
+
|
|
32
|
+
The same applies to `as`. A cast is you telling the compiler to stop checking:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
✗ const user = json as User; // asserted, never verified
|
|
36
|
+
✓ if (!isUser(json)) throw new Error('unexpected response shape');
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Types that catch bugs
|
|
40
|
+
|
|
41
|
+
**Make illegal states unrepresentable.** This is the highest-value thing types
|
|
42
|
+
do:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
✗ { loading: boolean; error?: string; data?: Invoice[] }
|
|
46
|
+
// loading AND error AND data — eight states, five of them nonsense
|
|
47
|
+
✓ | { status: 'loading' }
|
|
48
|
+
| { status: 'error'; error: string }
|
|
49
|
+
| { status: 'ready'; data: Invoice[] }
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Now "loading with an error" cannot be written, and every consumer is forced to
|
|
53
|
+
handle all three — which is the `error-handling` rule (empty ≠ failed ≠ never
|
|
54
|
+
ran) enforced by the compiler.
|
|
55
|
+
|
|
56
|
+
**Distinguish things that are both strings:**
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
type TenantId = string & { readonly __brand: 'TenantId' };
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Passing a `UserId` where a `TenantId` belongs is a multi-tenant data leak, and
|
|
63
|
+
it is a compile error rather than an incident.
|
|
64
|
+
|
|
65
|
+
## Ceremony to skip
|
|
66
|
+
|
|
67
|
+
- Annotating what is obvious: `const n: number = 5`. Inference is not weaker
|
|
68
|
+
typing — it is the same type, without the noise.
|
|
69
|
+
- An `interface` per function argument list used once.
|
|
70
|
+
- `Promise<void>` return annotations on every async function.
|
|
71
|
+
- Enums where a union of string literals reads better and needs no runtime.
|
|
72
|
+
|
|
73
|
+
⭐ Type the BOUNDARIES — function signatures, exported values, anything crossing
|
|
74
|
+
the network — and let inference handle the inside.
|
|
75
|
+
|
|
76
|
+
## ⚠️ Types are erased, so they are not validation
|
|
77
|
+
|
|
78
|
+
A `User` type proves nothing about what the API actually sent; it is a comment
|
|
79
|
+
the compiler checks *your* code against. Data arriving from outside — a
|
|
80
|
+
response, `localStorage`, a query param — must be checked at runtime, and only
|
|
81
|
+
then does its type mean anything (`security-basics`).
|
|
82
|
+
|
|
83
|
+
## `satisfies`, when you want both
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
const config = { port: 3000 } satisfies Config; // checked AND still literal
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Checks the shape without widening the type, which is what `as Config` would
|
|
90
|
+
have done while also disabling the check.
|