sanook-cli 0.4.0 → 0.5.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.
Files changed (235) hide show
  1. package/.env.example +19 -0
  2. package/CHANGELOG.md +144 -0
  3. package/README.md +153 -20
  4. package/README.th.md +136 -0
  5. package/dist/agentContext.js +4 -0
  6. package/dist/approval.js +6 -0
  7. package/dist/bin.js +394 -51
  8. package/dist/brain.js +92 -59
  9. package/dist/brand.js +47 -0
  10. package/dist/checkpoint.js +37 -0
  11. package/dist/commands.js +86 -6
  12. package/dist/compaction.js +76 -5
  13. package/dist/config.js +100 -12
  14. package/dist/cost.js +60 -3
  15. package/dist/doctor.js +92 -0
  16. package/dist/gateway/auth.js +2 -2
  17. package/dist/gateway/ledger.js +2 -2
  18. package/dist/gateway/scheduler.js +1 -0
  19. package/dist/gateway/serve.js +6 -4
  20. package/dist/gateway/server.js +10 -2
  21. package/dist/git.js +11 -2
  22. package/dist/hooks.js +43 -17
  23. package/dist/knowledge.js +48 -49
  24. package/dist/loop.js +182 -66
  25. package/dist/lsp/client.js +173 -0
  26. package/dist/lsp/framing.js +56 -0
  27. package/dist/lsp/index.js +138 -0
  28. package/dist/lsp/servers.js +82 -0
  29. package/dist/mcp-server.js +244 -0
  30. package/dist/mcp.js +184 -29
  31. package/dist/memory-store.js +559 -0
  32. package/dist/memory.js +143 -29
  33. package/dist/orchestrate.js +150 -0
  34. package/dist/providers/codex.js +2 -2
  35. package/dist/providers/keys.js +3 -2
  36. package/dist/providers/registry.js +133 -1
  37. package/dist/repomap.js +93 -0
  38. package/dist/search/chunk.js +158 -0
  39. package/dist/search/embed-store.js +187 -0
  40. package/dist/search/engine.js +203 -0
  41. package/dist/search/fuse.js +35 -0
  42. package/dist/search/index-core.js +187 -0
  43. package/dist/search/indexer.js +241 -0
  44. package/dist/search/store.js +77 -0
  45. package/dist/session.js +42 -8
  46. package/dist/skill-install.js +10 -10
  47. package/dist/skills.js +12 -9
  48. package/dist/summarize.js +31 -0
  49. package/dist/tools/bash.js +21 -2
  50. package/dist/tools/diagnostics.js +41 -0
  51. package/dist/tools/edit.js +29 -7
  52. package/dist/tools/index.js +8 -1
  53. package/dist/tools/list.js +7 -2
  54. package/dist/tools/permission.js +90 -9
  55. package/dist/tools/read.js +23 -4
  56. package/dist/tools/remember.js +1 -1
  57. package/dist/tools/sandbox.js +61 -0
  58. package/dist/tools/search.js +105 -4
  59. package/dist/tools/task.js +195 -29
  60. package/dist/tools/timeout.js +35 -0
  61. package/dist/tools/util.js +10 -0
  62. package/dist/tools/write.js +6 -4
  63. package/dist/trust.js +89 -0
  64. package/dist/ui/app.js +218 -27
  65. package/dist/ui/banner.js +4 -9
  66. package/dist/ui/history.js +30 -0
  67. package/dist/ui/mentions.js +44 -0
  68. package/dist/ui/setup.js +6 -5
  69. package/dist/ui/useEditor.js +83 -0
  70. package/dist/update.js +114 -0
  71. package/dist/worktree.js +173 -0
  72. package/package.json +11 -5
  73. package/scripts/postinstall.mjs +33 -0
  74. package/second-brain/.agents/_Index.md +30 -0
  75. package/second-brain/.agents/skills/_Index.md +30 -0
  76. package/second-brain/.agents/workflows/_Index.md +30 -0
  77. package/second-brain/AGENTS.md +4 -4
  78. package/second-brain/Acceptance/_Index.md +30 -0
  79. package/second-brain/Acceptance/golden-case-template.md +39 -0
  80. package/second-brain/Areas/_Index.md +30 -0
  81. package/second-brain/Bugs/System-OS/_Index.md +30 -0
  82. package/second-brain/Bugs/_Index.md +30 -0
  83. package/second-brain/CLAUDE.md +4 -1
  84. package/second-brain/Checklists/_Index.md +30 -0
  85. package/second-brain/Checklists/preflight-postflight-template.md +29 -0
  86. package/second-brain/Distillations/_Index.md +30 -0
  87. package/second-brain/Entities/_Index.md +30 -0
  88. package/second-brain/Entities/entity-template.md +33 -0
  89. package/second-brain/Evals/_Index.md +30 -0
  90. package/second-brain/Evals/correction-pairs.md +24 -0
  91. package/second-brain/Evals/failure-taxonomy.md +24 -0
  92. package/second-brain/Evals/golden-set.md +25 -0
  93. package/second-brain/Evals/quality-ledger.md +23 -0
  94. package/second-brain/Evals/self-eval-rubric.md +23 -0
  95. package/second-brain/GEMINI.md +4 -4
  96. package/second-brain/Goals/_Index.md +30 -0
  97. package/second-brain/Handoffs/_Index.md +30 -0
  98. package/second-brain/Home.md +7 -0
  99. package/second-brain/Intake/Raw Sources/_Index.md +30 -0
  100. package/second-brain/Intake/_Index.md +30 -0
  101. package/second-brain/Intake/_Quarantine/_Index.md +30 -0
  102. package/second-brain/Learning/_Index.md +30 -0
  103. package/second-brain/Playbooks/_Index.md +30 -0
  104. package/second-brain/Playbooks/playbook-template.md +23 -0
  105. package/second-brain/Projects/_Index.md +30 -0
  106. package/second-brain/Prompts/_Index.md +30 -0
  107. package/second-brain/README.md +2 -1
  108. package/second-brain/Research/_Index.md +30 -0
  109. package/second-brain/Retrospectives/_Index.md +30 -0
  110. package/second-brain/Reviews/_Index.md +30 -0
  111. package/second-brain/Runbooks/_Index.md +30 -0
  112. package/second-brain/Runbooks/eval-loop.md +24 -0
  113. package/second-brain/Sessions/_Index.md +30 -0
  114. package/second-brain/Shared/AI-Context-Index.md +20 -0
  115. package/second-brain/Shared/AI-Threads/_Index.md +30 -0
  116. package/second-brain/Shared/Archive/_Index.md +30 -0
  117. package/second-brain/Shared/Assets/_Index.md +30 -0
  118. package/second-brain/Shared/Context-Packs/_Index.md +30 -0
  119. package/second-brain/Shared/Context7-Docs/_Index.md +30 -0
  120. package/second-brain/Shared/Coordination/NOW.md +28 -0
  121. package/second-brain/Shared/Coordination/_Index.md +30 -0
  122. package/second-brain/Shared/Coordination/agent-registry.md +24 -0
  123. package/second-brain/Shared/Coordination/task-board/_Index.md +30 -0
  124. package/second-brain/Shared/Coordination/task-board/task-template.md +43 -0
  125. package/second-brain/Shared/Coordination/task-board.md +32 -0
  126. package/second-brain/Shared/Core-Facts/_Index.md +30 -0
  127. package/second-brain/Shared/Decision-Memory/_Index.md +30 -0
  128. package/second-brain/Shared/Glossary/_Index.md +30 -0
  129. package/second-brain/Shared/Memory-Inbox/_Index.md +30 -0
  130. package/second-brain/Shared/Operating-State/_Index.md +30 -0
  131. package/second-brain/Shared/Prompting/_Index.md +30 -0
  132. package/second-brain/Shared/Provenance/_Index.md +30 -0
  133. package/second-brain/Shared/Rules/_Index.md +30 -0
  134. package/second-brain/Shared/Rules/contextual-note-rule.md +30 -0
  135. package/second-brain/Shared/Rules/frontmatter-standard.md +10 -0
  136. package/second-brain/Shared/Rules/memory-write-protocol.md +28 -0
  137. package/second-brain/Shared/Rules/procedural-runbook-header.md +40 -0
  138. package/second-brain/Shared/Rules/review-and-staleness-policy.md +22 -0
  139. package/second-brain/Shared/Rules/rules-formatting.md +34 -0
  140. package/second-brain/Shared/Scripts/_Index.md +30 -0
  141. package/second-brain/Shared/Scripts-Archive/_Index.md +30 -0
  142. package/second-brain/Shared/Tech-Standards/_Index.md +30 -0
  143. package/second-brain/Shared/Tech-Standards/verification-standard.md +40 -0
  144. package/second-brain/Shared/User-Memory/_Index.md +30 -0
  145. package/second-brain/Shared/User-Persona/_Index.md +30 -0
  146. package/second-brain/Shared/User-Persona/owner-profile.md +25 -0
  147. package/second-brain/Shared/Working-Memory/_Index.md +30 -0
  148. package/second-brain/Shared/_Index.md +30 -0
  149. package/second-brain/Shared/mcp-servers/_Index.md +30 -0
  150. package/second-brain/Skills/_Index.md +30 -0
  151. package/second-brain/Templates/_Index.md +30 -0
  152. package/second-brain/Templates/bug.md +2 -0
  153. package/second-brain/Templates/handoff.md +2 -0
  154. package/second-brain/Templates/session.md +2 -0
  155. package/second-brain/Tools/_Index.md +30 -0
  156. package/second-brain/Traces/_Index.md +30 -0
  157. package/second-brain/Vault Structure Map.md +33 -1
  158. package/second-brain/copilot/_Index.md +30 -0
  159. package/skills/audit-license-compliance/SKILL.md +117 -0
  160. package/skills/author-codemod/SKILL.md +110 -0
  161. package/skills/build-audit-logging/SKILL.md +112 -0
  162. package/skills/build-cdc-streaming-pipeline/SKILL.md +123 -0
  163. package/skills/build-cli-tool/SKILL.md +108 -0
  164. package/skills/build-data-table/SKILL.md +141 -0
  165. package/skills/build-native-mobile-ui/SKILL.md +154 -0
  166. package/skills/build-offline-first-sync/SKILL.md +118 -0
  167. package/skills/build-realtime-channel/SKILL.md +122 -0
  168. package/skills/build-vector-search/SKILL.md +131 -0
  169. package/skills/compose-local-dev-stack/SKILL.md +149 -0
  170. package/skills/configure-bundler-build/SKILL.md +166 -0
  171. package/skills/configure-dns-tls/SKILL.md +142 -0
  172. package/skills/configure-reverse-proxy-lb/SKILL.md +129 -0
  173. package/skills/configure-security-headers-csp/SKILL.md +122 -0
  174. package/skills/contract-testing/SKILL.md +140 -0
  175. package/skills/datetime-timezone-correctness/SKILL.md +125 -0
  176. package/skills/debug-ci-pipeline-failure/SKILL.md +134 -0
  177. package/skills/debug-flaky-tests/SKILL.md +128 -0
  178. package/skills/defend-llm-prompt-injection/SKILL.md +110 -0
  179. package/skills/deliver-webhooks/SKILL.md +116 -0
  180. package/skills/design-api-pagination/SKILL.md +144 -0
  181. package/skills/design-authorization-model/SKILL.md +119 -0
  182. package/skills/design-backup-dr-recovery/SKILL.md +113 -0
  183. package/skills/design-event-sourcing-cqrs/SKILL.md +143 -0
  184. package/skills/design-multi-tenancy/SKILL.md +100 -0
  185. package/skills/design-protobuf-grpc-service/SKILL.md +146 -0
  186. package/skills/design-relational-schema/SKILL.md +129 -0
  187. package/skills/design-search-index-infra/SKILL.md +151 -0
  188. package/skills/design-state-machine/SKILL.md +108 -0
  189. package/skills/design-token-system/SKILL.md +109 -0
  190. package/skills/distributed-locks-leases/SKILL.md +120 -0
  191. package/skills/encrypt-sensitive-data/SKILL.md +148 -0
  192. package/skills/feature-flags-rollout/SKILL.md +130 -0
  193. package/skills/file-upload-object-storage/SKILL.md +107 -0
  194. package/skills/fuzz-dynamic-security-test/SKILL.md +111 -0
  195. package/skills/harden-llm-app-reliability/SKILL.md +126 -0
  196. package/skills/i18n-localization-setup/SKILL.md +113 -0
  197. package/skills/idempotency-keys/SKILL.md +107 -0
  198. package/skills/implement-push-notifications/SKILL.md +142 -0
  199. package/skills/ingest-webhook-secure/SKILL.md +120 -0
  200. package/skills/integrate-oauth-oidc/SKILL.md +126 -0
  201. package/skills/load-stress-test/SKILL.md +129 -0
  202. package/skills/map-privacy-data-gdpr/SKILL.md +146 -0
  203. package/skills/model-nosql-data/SKILL.md +118 -0
  204. package/skills/money-decimal-arithmetic/SKILL.md +123 -0
  205. package/skills/monitor-ml-drift/SKILL.md +109 -0
  206. package/skills/numeric-precision-units/SKILL.md +144 -0
  207. package/skills/optimize-llm-cost-latency/SKILL.md +103 -0
  208. package/skills/optimize-react-rerenders/SKILL.md +124 -0
  209. package/skills/orchestrate-agent-workflow/SKILL.md +100 -0
  210. package/skills/payments-billing-integration/SKILL.md +114 -0
  211. package/skills/pin-toolchain-versions/SKILL.md +116 -0
  212. package/skills/plan-strangler-migration/SKILL.md +95 -0
  213. package/skills/property-based-testing/SKILL.md +108 -0
  214. package/skills/publish-package-registry/SKILL.md +130 -0
  215. package/skills/recover-git-state/SKILL.md +119 -0
  216. package/skills/remediate-web-vulnerabilities/SKILL.md +125 -0
  217. package/skills/resilience-timeouts-retries/SKILL.md +104 -0
  218. package/skills/resolve-merge-rebase-conflict/SKILL.md +97 -0
  219. package/skills/rewrite-git-history/SKILL.md +109 -0
  220. package/skills/scaffold-cross-platform-app/SKILL.md +137 -0
  221. package/skills/schema-evolution-compatibility/SKILL.md +121 -0
  222. package/skills/send-transactional-email/SKILL.md +126 -0
  223. package/skills/serve-deploy-ml-model/SKILL.md +107 -0
  224. package/skills/setup-cdn-edge-waf/SKILL.md +107 -0
  225. package/skills/setup-devcontainer-env/SKILL.md +131 -0
  226. package/skills/setup-lint-format-precommit/SKILL.md +140 -0
  227. package/skills/setup-monorepo-tooling/SKILL.md +125 -0
  228. package/skills/ship-mobile-app-store-release/SKILL.md +137 -0
  229. package/skills/structured-output-llm/SKILL.md +86 -0
  230. package/skills/supply-chain-sbom-provenance/SKILL.md +120 -0
  231. package/skills/test-data-factories/SKILL.md +158 -0
  232. package/skills/threat-model-stride/SKILL.md +123 -0
  233. package/skills/train-evaluate-ml-model/SKILL.md +109 -0
  234. package/skills/unicode-text-correctness/SKILL.md +109 -0
  235. package/skills/visual-regression-testing/SKILL.md +120 -0
@@ -0,0 +1,109 @@
1
+ ---
2
+ name: design-token-system
3
+ description: Architects a framework-agnostic design-token system with primitive/semantic/component tiers, theming and multi-brand/dark-mode alias contracts, and multi-platform export (CSS vars, Tailwind, JS/TS, iOS/Android) from one W3C-DTCG source via Style Dictionary.
4
+ when_to_use: Setting up or refactoring a token architecture, building a theme/multi-brand/dark-mode system, exporting one token source to web + native, or adopting Style Dictionary / the W3C Design Tokens format. Distinct from style-responsive-tailwind (consuming tokens in markup) and brainstorm-design (choosing the palette/visual direction).
5
+ ---
6
+
7
+ ## When to Use
8
+
9
+ Reach for this skill when the problem is the **token architecture and export pipeline**, not a single component's styling:
10
+
11
+ - "Set up design tokens / a theme system from scratch"
12
+ - "Add dark mode without forking every color"
13
+ - "Support multiple brands / white-label from one codebase"
14
+ - "Export the same tokens to CSS, Tailwind, and our iOS + Android apps"
15
+ - "Adopt Style Dictionary / the W3C Design Tokens (DTCG) format"
16
+ - "We have 300 hardcoded hex/px values — give us a governed token layer"
17
+
18
+ NOT this skill:
19
+ - Writing the markup/utility classes that *consume* tokens → style-responsive-tailwind
20
+ - Picking the actual palette, type pairing, or visual mood → brainstorm-design
21
+ - Translating one Figma frame into a component → implement-from-design
22
+ - Building the React component that renders from tokens → build-react-component
23
+ - Wiring a cross-platform app shell/build → scaffold-cross-platform-app
24
+ - Certifying contrast ratios meet WCAG → audit-accessibility-wcag (this skill *structures* color; it does not verify contrast)
25
+
26
+ ## Steps
27
+
28
+ 1. **Build exactly three tiers — never let a component read a primitive.** This is the whole architecture; get it wrong and theming is impossible.
29
+
30
+ | Tier | Names mean | References | Example | Rule |
31
+ |---|---|---|---|---|
32
+ | **Primitive** (global/core) | nothing — raw scale | literal values only | `blue.500 = #2563EB`, `space.4 = 16px` | No semantics. Never themed. Never imported by components. |
33
+ | **Semantic** (alias) | role/intent | → primitives | `color.bg.surface → gray.50`, `color.intent.danger → red.600` | The *only* layer that swaps per theme/brand. |
34
+ | **Component** (scoped) | one part | → semantics | `button.primary.bg → color.intent.brand` | Optional; add only when a component overrides a semantic. |
35
+
36
+ Default to **2 tiers (primitive + semantic)**; add component tokens only where a component genuinely diverges. Components and Tailwind/CSS consume **semantic tokens only**.
37
+
38
+ 2. **One source of truth in W3C DTCG JSON.** Use the spec's `$value` / `$type` and `{dot.path}` references so any compliant tool (Style Dictionary v4+, Tokens Studio) can read it. No per-platform hand-edited files.
39
+
40
+ ```jsonc
41
+ // tokens/primitive/color.json
42
+ { "color": { "blue": { "500": { "$type": "color", "$value": "#2563EB" } } } }
43
+
44
+ // tokens/semantic/color.json — alias, NOT a literal
45
+ { "color": { "intent": { "brand": { "$type": "color", "$value": "{color.blue.500}" } },
46
+ "bg": { "surface": { "$type": "color", "$value": "{color.gray.50}" } } } }
47
+ ```
48
+ A semantic token whose `$value` is a literal hex is a bug — it must be a `{reference}`.
49
+
50
+ 3. **Theming = swap the semantic layer, never fork the palette.** Light, dark, and each brand are *alternate semantic files* pointing at the *same* primitives. One `primitive/` set; `semantic/light.json`, `semantic/dark.json`, `semantic/brand-acme.json`. Dark mode flips `bg.surface → gray.900` instead of `gray.50` — the primitives don't move. Never create `blue.500.dark`.
51
+
52
+ 4. **Author color in OKLCH so themes shift predictably.** Build scales in OKLCH (fall back to HSL only if tooling can't): equal lightness steps stay perceptually even and a brand hue rotation keeps contrast. Hardcoded hex per shade drifts. Emit hex/rgb as a *build output* for legacy targets, not as the source.
53
+
54
+ 5. **Cover every token type — color is the easy half.** Define and `$type` all of: `color`, `dimension` (spacing/sizing), `fontFamily`/`fontWeight`/`fontSize`/`lineHeight`/`letterSpacing` (typography), `borderRadius`, `shadow` (elevation), `duration`/`cubicBezier` (motion), and z-index. Derive primitives from a **base scale** (4px grid for spacing, a modular ratio for type); semantics name the use (`space.inline.sm`, `text.heading.lg`).
55
+
56
+ 6. **Export everything from one Style Dictionary config.** One source → many platforms, each with the right transform group and output format:
57
+
58
+ ```js
59
+ // style-dictionary.config.js (v4 — ESM)
60
+ export default {
61
+ source: ['tokens/primitive/**/*.json', 'tokens/semantic/light.json'],
62
+ platforms: {
63
+ css: { transformGroup: 'css', buildPath: 'build/css/',
64
+ files: [{ destination: 'vars.css', format: 'css/variables',
65
+ options: { outputReferences: true } }] }, // keeps var(--x) chains
66
+ tailwind: { transformGroup: 'js', buildPath: 'build/tw/',
67
+ files: [{ destination: 'tokens.cjs', format: 'javascript/module-flat' }] },
68
+ ts: { transformGroup: 'js', buildPath: 'build/ts/',
69
+ files: [{ destination: 'tokens.ts', format: 'javascript/es6' }] },
70
+ ios: { transformGroup: 'ios-swift', buildPath: 'build/ios/',
71
+ files: [{ destination: 'Tokens.swift', format: 'ios-swift/class.swift' }] },
72
+ android: { transformGroup: 'android', buildPath: 'build/android/',
73
+ files: [{ destination: 'tokens.xml', format: 'android/resources' }] }
74
+ }
75
+ };
76
+ ```
77
+ Run `style-dictionary build`. For each extra theme, run the same config with `semantic/dark.json` swapped into `source` and scope output under `[data-theme="dark"]` (CSS `options.selector`).
78
+
79
+ 7. **Wire Tailwind to the generated tokens — do not retype them.** `tailwind.config` imports `build/tw/tokens.cjs` into `theme.colors/spacing/...`. CSS vars drive runtime theme switching: Tailwind utilities resolve `var(--color-bg-surface)`, and the `[data-theme]` attribute swaps which value that var resolves to. One toggle, zero recompiled CSS.
80
+
81
+ 8. **Forbid raw values in app code with a linter.** Add `stylelint-declaration-strict-value` (web CSS) or an ESLint/lint rule that bans hex, `rgb(`, and bare `px` outside `tokens/` and `build/`. Raw values must fail CI, not slip through code review.
82
+
83
+ 9. **Govern it as a published API.** Fix a naming grammar `category.role.variant.state` (e.g. `color.bg.surface.hover`); semver the published token package (removed/renamed semantic token = **major**, added = minor, primitive value tweak = patch); keep a CHANGELOG; treat the `semantic` layer as the public API and primitives as private/internal.
84
+
85
+ ## Common Errors
86
+
87
+ - **Components reading primitives** (`button { color: blue.500 }`). Dark mode and rebrand degrade to find-and-replace. Components must reference semantics only.
88
+ - **Forking the palette per theme** (`blue.500.dark`). Palette count explodes and brands drift. Themes swap the *semantic* alias target; primitives are shared and immutable.
89
+ - **Semantic tokens holding literal values** instead of `{references}`. The indirection is the entire point — a literal hex in a semantic token can't be retargeted by a theme.
90
+ - **`outputReferences: false` (the default) flattening CSS vars.** The build bakes `#2563EB` into every rule, killing runtime theme switching. Set `options: { outputReferences: true }` so `var(--color-intent-brand)` chains survive.
91
+ - **Duplicating tokens into `tailwind.config` by hand.** They desync within the first week. Import the Style Dictionary build output; never maintain two sources.
92
+ - **No grid/scale — arbitrary `13px`, `17px` primitives.** Defeats consistency. Primitives come from a 4px (or 8px) grid and a modular type ratio.
93
+ - **Treating contrast as solved because colors are tokenized.** Tokens organize color; they don't guarantee `bg.surface`/`text.primary` meet 4.5:1. Run audit-accessibility-wcag on each theme.
94
+ - **Component tokens for everything**, including parts that never override a semantic. Pure bloat. Add a component token only where it diverges from the semantic.
95
+ - **Per-platform manual edits to `build/` outputs.** They're regenerated; your edit vanishes on the next build. Fix the source and rebuild.
96
+ - **No versioning/changelog on the token package.** A renamed semantic token silently breaks every consumer. Semver it; a rename is a breaking (major) change.
97
+
98
+ ## Verify
99
+
100
+ 1. **Tier discipline:** `grep` app/component source — zero references to primitive names (`blue.500`, `space.4`) and zero raw hex/`rgb(`/bare `px`. Every match is a violation.
101
+ 2. **Aliases resolve:** every semantic `$value` is a `{reference}`, not a literal; `style-dictionary build` reports **0 unresolved references** and exits `0`.
102
+ 3. **One source, many outputs:** a single `style-dictionary build` produces CSS, Tailwind, TS, iOS, and Android artifacts from the same `tokens/` tree (no hand-edited platform file).
103
+ 4. **Theme swap is alias-only:** diff `semantic/light.json` vs `semantic/dark.json` — they differ only in reference *targets*; `primitive/` is byte-identical across themes. Adding a brand touches no primitive.
104
+ 5. **Runtime switch works:** toggling `[data-theme="dark"]` on the built CSS recolors the page with **no CSS recompile** (proves `outputReferences` chains survived).
105
+ 6. **Lint gate is live:** committing a raw `#fff` or `12px` in app code fails CI, not review.
106
+ 7. **Native parity:** the same semantic token (e.g. `color.intent.brand`) yields the same color in `build/css/vars.css`, `build/ios/Tokens.swift`, and `build/android/tokens.xml`.
107
+ 8. **Governance:** naming matches `category.role.variant.state`, the package carries a semver + CHANGELOG, and a token rename ships as a major bump.
108
+
109
+ Done = one W3C-DTCG source builds all platforms with zero unresolved references, components reference semantics only (lint-enforced in CI), themes/brands swap via alias targets over shared immutable primitives, and runtime theme switching recolors with no recompile.
@@ -0,0 +1,120 @@
1
+ ---
2
+ name: distributed-locks-leases
3
+ description: Implements distributed mutual exclusion and leader election correctly across processes/nodes — Redis `SET key token NX PX <ttl>` with a unique random token + Lua compare-and-delete unlock (never bare DEL), etcd/ZooKeeper/Consul leases (lease grant + TTL + keepAlive renewal, ephemeral znode + watch on predecessor for leader election), and Postgres advisory locks (`pg_advisory_lock`/`pg_try_advisory_xact_lock`) for single-DB serialization — while treating every lock as a LEASE that can expire mid-work, so safety rides on monotonic fencing tokens that the protected resource checks-and-rejects-stale (per Kleppmann's Redlock critique), never on the lock alone. Covers TTL sizing vs work duration, renewal/keepalive, the GC-pause/clock-skew expiry hazard, split-brain, and choosing idempotency or partitioning INSTEAD of a lock.
4
+ when_to_use: You need only-one-runner-at-a-time across machines — a leader/singleton (cron that must not double-fire, one active scheduler/consumer), a critical section over a shared external resource (a row, a file, an API quota) spanning multiple nodes, leader election, or you're reaching for Redlock/`SETNX`/etcd leases/ZooKeeper. Distinct from async-concurrency-correctness (in-process mutexes/atomics/channels within ONE process — no network, no lease expiry) and idempotency-keys (the real safety net when the lock fails or expires — make the protected operation safe to repeat instead of/in addition to locking).
5
+ ---
6
+
7
+ ## When to Use
8
+
9
+ Reach for this skill when you need **at most one actor running at a time across separate processes or machines**, coordinated through a shared store — and a second concurrent runner would corrupt state:
10
+
11
+ - "Only one instance should run this cron / scheduler / migration / cleanup at a time"
12
+ - "Elect a leader / single active consumer across N replicas" (active-passive failover)
13
+ - "Two pods both processed the same job / both wrote the same file"
14
+ - "Serialize edits to one row/aggregate/external resource across the cluster"
15
+ - "I'm using Redis `SETNX` / Redlock / etcd lease / ZooKeeper ephemeral node for a lock"
16
+ - "Hold a lock while I do work, renew it, and release it safely"
17
+ - "The lock expired while my job was still running and another node started"
18
+
19
+ NOT this skill:
20
+ - A mutex/semaphore/atomic/channel **inside a single process** (Go `sync.Mutex`, Java `synchronized`/`ReentrantLock`, Python `Lock`, `asyncio` races) — no network, no TTL, no lease expiry → async-concurrency-correctness
21
+ - Making the protected operation **safe to run twice** so a lock failure/expiry is harmless (dedup table, upsert, set-don't-increment) → idempotency-keys (this is the safety net BELOW the lock; prefer it over a lock when you can)
22
+ - Throttling request *rate* (token bucket / sliding window), not exclusivity → rate-limiting
23
+ - Worker pool, job dispatch, DLQ, poison-message handling, exactly-once consumer semantics → message-queue-jobs
24
+ - Optimistic concurrency on a single DB row (`WHERE version = N` / `If-Match`/ETag, no separate lock service) → idempotency-keys (by-design) / db-migration-safety for schema
25
+ - Timeouts, retries, backoff, circuit breakers around the locked call → resilience-timeouts-retries
26
+ - Saga/state-machine coordination of a long multi-step workflow → design-state-machine / orchestrate-agent-workflow
27
+
28
+ ## Steps
29
+
30
+ 1. **First ask: do you actually need a distributed lock? Usually you don't.** A lock is a liveness/correctness liability (a held-but-dead lock stalls everyone; an expired one breaks mutual exclusion). Prefer, in order:
31
+
32
+ | Instead of a lock | Technique | Why it's better |
33
+ |---|---|---|
34
+ | **Idempotency** | make the op safe to repeat (upsert, set-don't-increment, dedup key) → idempotency-keys | concurrent runs are *harmless*, not *prevented* — no expiry hazard at all |
35
+ | **Partitioning** | shard work by key (Kafka partition, consistent-hash, `id % N`) so each key has exactly one owner | structural single-ownership, no shared lock at all |
36
+ | **Single-DB serialization** | `SELECT ... FOR UPDATE` / unique constraint / `INSERT ... ON CONFLICT` / advisory lock (step 6) | the DB transaction *is* the lock, with real ACID guarantees |
37
+ | **A queue / leader-elected scheduler** | one consumer per partition; framework-provided leader election (k8s `Lease`, Raft) | offloads the hard part to a tested system |
38
+
39
+ Use a distributed lock only for **efficiency** (avoid duplicate work, where a rare double-run is *tolerable*) — NOT as your sole correctness guarantee. For correctness you also need step 4 (fencing) or idempotency.
40
+
41
+ 2. **Treat every lock as a LEASE: it auto-expires after a TTL, and it can expire WHILE you still think you hold it.** This is the central hazard. A lock without a TTL deadlocks the whole system if the holder crashes; a lock with a TTL can expire mid-work (GC pause, CPU starvation, slow I/O, network partition, VM freeze) — then the store hands the lock to node B while node A, paused, *believes* it still holds it and resumes writing. Two writers, one lock. Conclusions that follow:
42
+ - Always set a TTL (no infinite locks).
43
+ - TTL alone is never sufficient for correctness — you must also fence (step 4) or be idempotent (step 1).
44
+ - Pick TTL ≥ p99 work duration + safety margin; renew (step 5) for long work rather than setting a huge TTL.
45
+
46
+ 3. **Redis single-node lock — acquire with a unique token, release with compare-and-delete (Lua), never bare `DEL`.** Use one atomic command and a per-acquisition random token so only the owner can unlock:
47
+ ```
48
+ # acquire — NX = only if absent, PX = TTL in ms, token = unique per acquisition (uuid/16 random bytes)
49
+ SET resource_lock <token> NX PX 30000
50
+ ```
51
+ ```lua
52
+ -- release — DELETE ONLY IF the value is still OUR token (compare-and-delete, atomic)
53
+ if redis.call("GET", KEYS[1]) == ARGV[1] then
54
+ return redis.call("DEL", KEYS[1])
55
+ else return 0 end
56
+ ```
57
+ - **Never** `SETNX` + separate `EXPIRE` (non-atomic: crash between them = a lock that never expires). Use `SET ... NX PX` in one call.
58
+ - **Never** a bare `DEL resource_lock` to release: if your lease already expired and B re-acquired, your `DEL` deletes *B's* lock. The token check prevents that.
59
+ - **Redlock (multi-node) is contested — default to single-node + fencing.** Kleppmann's critique ("How to do distributed locking", 2016): Redlock relies on bounded clocks and pauses it can't guarantee, so it provides neither efficiency nor correctness better than a single node *for correctness*. Antirez disputes the framing, but the practical takeaway holds: **do not rely on any timing-based lock (Redlock included) for correctness — fence the resource (step 4).** Use single-node Redis for the cheap mutual-exclusion-for-efficiency case; reach for a consensus store (step 7) when you need real leader election.
60
+
61
+ 4. **Fencing tokens — the only thing that makes a lease-based lock SAFE. The protected resource must reject stale writers.** On every acquisition, get a **monotonically increasing** token (the "fence"). Pass it with every write to the protected resource. The resource stores the highest token it has seen and **rejects any write carrying a token ≤ the last accepted one.** Now a paused node A (token 33) that wakes after B acquired (token 34) gets its write rejected — mutual exclusion is enforced *at the resource*, independent of who "thinks" they hold the lock.
62
+ ```
63
+ client A acquires → fence=33 → write(x, fence=33) accepted, resource now at 33
64
+ A pauses; lease expires; B acquires → fence=34 → write(y, fence=34) accepted, resource at 34
65
+ A resumes, still "holds" lock → write(z, fence=33) REJECTED (33 ≤ 34)
66
+ ```
67
+ - Source of monotonic tokens: ZooKeeper `zxid`/znode version, etcd key `mod_revision` / a `CreateRevision`-based counter, Redis `INCR fence_counter` (single-node only — multi-node Redis can't guarantee monotonicity), or a DB sequence.
68
+ - The resource MUST participate — if your storage/API can't check-and-reject a token (e.g. a dumb blob store), fencing is impossible and you fall back to idempotency (step 1). Many real systems can't fence; that's exactly why idempotency is the more robust default.
69
+
70
+ 5. **Long work: renew (keepalive) instead of guessing a huge TTL — and abort if renewal fails.** For work that may exceed the TTL, run a watchdog that re-extends the lease at ~TTL/3:
71
+ - Redis: a Lua `PEXPIRE` guarded by the same token check (extend only if still ours).
72
+ - etcd: `LeaseKeepAlive` stream; ZooKeeper: session heartbeats keep the ephemeral node alive; Consul: session renew before TTL.
73
+ - **Critical:** if a renewal FAILS or is late, you may have already lost the lease — **stop doing work immediately** (cancel the in-flight operation), don't blindly continue. The renewer and the worker must share a cancellation signal (context/CancellationToken). A renew thread that keeps extending after the worker is wedged is also a bug (it masks a stuck holder).
74
+
75
+ 6. **Postgres advisory locks — the right tool when one Postgres is your coordination point.** No extra infra; the lock lives in the DB you already trust:
76
+ | Function | Scope | Released by | Use for |
77
+ |---|---|---|---|
78
+ | `pg_advisory_lock(key)` | **session** | explicit `pg_advisory_unlock` or session end | held across transactions; must release manually (leaks if connection pooled + forgotten) |
79
+ | `pg_advisory_xact_lock(key)` | **transaction** | automatically at COMMIT/ROLLBACK | **preferred** — no manual release, no leak; held only for the txn |
80
+ | `pg_try_advisory_lock(key)` | session, **non-blocking** | as above | returns `true/false` instantly — "skip if someone else has it" (e.g. cron singleton) |
81
+ - Key is a `bigint` (or two `int4`s) — hash your logical name: `pg_try_advisory_xact_lock(hashtext('nightly-report'))`. Beware `hashtext` collisions; use a deliberate keyspace for unrelated locks.
82
+ - **Advisory locks are NOT enforced by the data** — they're cooperative; only code that *also* takes the lock is excluded. They don't lock rows. For row-level exclusion use `SELECT ... FOR UPDATE` instead.
83
+ - **Pooling gotcha:** with a transaction pooler (PgBouncer `transaction` mode), session-level advisory locks break (different backend per statement). Use `*_xact_lock` or a `session` pool.
84
+
85
+ 7. **etcd / ZooKeeper / Consul — when you need real leader election and consensus.** These are CP (consistent under partition) consensus stores; use them when a *rare* double-leader is unacceptable:
86
+ - **etcd:** `Lease` (grant TTL) + a key written with that lease; election via the `concurrency.Election` API (campaign → leader holds key until lease lapses or it resigns). `mod_revision` gives you a fencing token for free.
87
+ - **ZooKeeper:** create an **ephemeral sequential** znode; the lowest sequence number is leader; each node **watches only its immediate predecessor** (not all nodes — avoids the herd effect). On predecessor delete, re-check if you're now lowest. Ephemeral = auto-removed on session loss → automatic failover. The Curator `LeaderLatch`/`InterProcessMutex` recipes implement this correctly; prefer them over hand-rolling.
88
+ - **Consul:** session + KV `acquire` flag; session TTL + health check ties lock liveness to the holder's health.
89
+ - **Even here, fence.** Consensus guarantees agreement on *who holds the lease*, but a GC-paused leader still doesn't know its lease lapsed — the resource must still reject its stale-token writes (step 4). Consensus narrows the window; it doesn't remove the mid-work-expiry hazard.
90
+
91
+ 8. **Defend against split-brain and clock skew.** Two nodes both believing they're leader = split-brain. Mitigations: a single consensus source of truth (don't run two independent lock services); fencing tokens so even a split-brain second writer is rejected at the resource; **never trust wall-clock time for lease math across nodes** — use the lock service's own expiry, and within a node use a *monotonic* clock (`CLOCK_MONOTONIC`, `time.monotonic()`, `Instant`/`System.nanoTime`) for "have I exceeded my budget?" since NTP steps and VM time-warps corrupt wall-clock deltas. Assume your process can pause arbitrarily long between any two lines (GC, OS scheduler, live-migration).
92
+
93
+ ## Common Errors
94
+
95
+ - **No TTL → permanent deadlock on crash.** A holder dies, the lock is held forever, the system stalls. Fix: always set a TTL; renew for long work (step 5).
96
+ - **TTL but no fencing → silent double-write on mid-work expiry.** The lock expires during a GC pause, B acquires, A resumes and writes. Fix: monotonic fencing token rejected at the resource (step 4), or make the op idempotent (step 1).
97
+ - **`SETNX` then separate `EXPIRE`.** Crash between the two leaves a lock with no expiry = deadlock. Fix: single atomic `SET key token NX PX <ttl>`.
98
+ - **Releasing with bare `DEL` / no owner check.** If your lease already expired and someone re-acquired, you delete *their* lock. Fix: Lua compare-and-delete on your unique token.
99
+ - **Reusing a constant lock value.** Without a per-acquisition random token you can't tell your lock from a successor's — unlock and renew both become unsafe. Fix: fresh uuid/random token each acquire.
100
+ - **Trusting Redlock (or any timing lock) for correctness.** Bounded-clock/bounded-pause assumptions don't hold. Fix: single-node for efficiency-only; fencing/consensus for correctness (steps 3, 4, 7).
101
+ - **Renewal failure ignored.** The watchdog can't renew but the worker keeps writing without the lease. Fix: failed/late renew → cancel the work immediately via a shared cancellation signal.
102
+ - **Session-level `pg_advisory_lock` behind a transaction pooler.** Different backend per statement → lock acquired on one connection, never released / not visible. Fix: `pg_advisory_xact_lock`, or a session-mode pool.
103
+ - **Forgetting to release a session advisory lock.** Leaks until the connection dies; with pooling that connection is reused holding the lock. Fix: prefer `*_xact_lock` (auto-release at txn end).
104
+ - **Using a distributed lock where idempotency/partitioning was the right tool.** You inherit the whole expiry/split-brain failure surface for no reason. Fix: revisit step 1 — can the op be idempotent or key-partitioned instead?
105
+ - **Wall-clock lease math across nodes.** NTP steps / VM time-warps make "is my lease still valid?" wrong. Fix: trust the lock service's expiry; use a monotonic clock for local budget checks.
106
+ - **Watching all nodes in ZooKeeper leader election (herd effect).** Every change wakes every node. Fix: ephemeral-sequential + watch only your immediate predecessor (or use Curator recipes).
107
+
108
+ ## Verify
109
+
110
+ 1. **Mutual exclusion under contention:** spawn N nodes/goroutines racing for the same lock against the *real* shared store; assert exactly one holds it at any instant (e.g. each increments a shared counter inside the section and the section must never overlap — verified with a sentinel that fails if two enter).
111
+ 2. **Crash releases the lock:** kill the holder mid-section; another node acquires within ~TTL (the lease expires), not never (no permanent deadlock) and not instantly (no missing TTL).
112
+ 3. **Fencing rejects the stale writer:** simulate the Kleppmann scenario — A acquires (fence 33), pause A, let the lease expire, B acquires (fence 34) and writes, then resume A's write with fence 33 → the resource **rejects** it. Without fencing, this is the test that exposes the double-write.
113
+ 4. **Atomic acquire:** the acquire path is a single `SET NX PX` (or equivalent) — grep shows no `SETNX`+`EXPIRE` two-step and no infinite/missing TTL.
114
+ 5. **Safe release:** the unlock only deletes when the stored token matches (Lua/compare-and-delete); a test where the lease expired and was re-acquired confirms the old holder's release does NOT remove the new holder's lock.
115
+ 6. **Renewal + abort:** for long work, the lease is extended at ~TTL/3 while the token still matches; inject a renewal failure and assert the worker *cancels* rather than continuing without the lease.
116
+ 7. **Advisory-lock leak/pooling check:** advisory locks are `*_xact_lock` (or explicitly unlocked) and behave correctly under the actual connection-pool mode; `pg_locks` shows no orphaned advisory locks after the txn ends.
117
+ 8. **Leader election failover:** kill the leader; a new leader is elected within the session/lease TTL; assert there is never *zero* leader for long nor *two* leaders simultaneously (split-brain) — and that a deposed leader's writes are fenced out.
118
+ 9. **Default-choice justification:** confirm a distributed lock is genuinely needed — document why idempotency (idempotency-keys) or partitioning couldn't replace it; if the lock is correctness-critical, fencing or idempotency is present, not the lock alone.
119
+
120
+ Done = at most one actor runs at a time under real contention, every lock has a TTL and crash-frees within it, mid-work expiry cannot cause a double effect because the resource rejects stale fencing tokens (or the op is idempotent), acquire/release/renew are atomic and owner-checked, advisory locks are pool-safe and leak-free, leader election survives failover without split-brain, and the choice of a lock over idempotency/partitioning is deliberate — all proven by the contention, crash, and fencing tests in checks 1–8.
@@ -0,0 +1,148 @@
1
+ ---
2
+ name: encrypt-sensitive-data
3
+ description: Encrypts sensitive data at rest, in transit, and per-field using AEAD-only ciphers (AES-256-GCM or ChaCha20-Poly1305 — never ECB, never unauthenticated CBC, never raw RSA) — envelope encryption where a KMS-held KEK wraps a per-record/per-tenant DEK, per-column field encryption for PII with deterministic-vs-randomized chosen per query need, strict unique-nonce/IV discipline (random 96-bit or counter, NEVER reused under one key), AAD binding ciphertext to its context (tenant/row id), versioned keys + rotation that re-wraps DEKs without re-encrypting data, TLS 1.2+/1.3 with mTLS and modern cipher suites, and — critically — passwords are HASHED with argon2id/bcrypt, NOT encrypted. Distinct from secrets-management (stores the app secrets/keys this skill consumes) and map-privacy-data-gdpr (the legal PII/erasure obligations encryption helps satisfy).
4
+ when_to_use: You must protect sensitive data — encrypting PII/PHI/card data at rest, a per-column/field-level encryption scheme, envelope encryption with a KMS (AWS KMS/GCP KMS/Vault Transit), key rotation, choosing a cipher/mode/nonce strategy, enforcing TLS/mTLS, or hashing passwords. Distinct from secrets-management (storing and injecting the KEKs/API keys/credentials — that skill provisions the keys; this one uses them to encrypt data) and map-privacy-data-gdpr (the legal classification/erasure/residency duties that encryption and crypto-shredding help you meet).
5
+ ---
6
+
7
+ ## When to Use
8
+
9
+ Reach for this skill when the task is making sensitive *data* cryptographically protected — at rest, in transit, or field-by-field:
10
+
11
+ - "Encrypt SSNs / card numbers / health records / PII columns in the database"
12
+ - "Set up envelope encryption with AWS KMS / GCP KMS / Vault Transit (DEK + KEK)"
13
+ - "Rotate our encryption keys" / "we need versioned keys without re-encrypting everything"
14
+ - "Which cipher/mode — is AES-CBC okay? do we need a separate MAC? what nonce?"
15
+ - "Enforce TLS 1.3 / mutual TLS between services with modern cipher suites"
16
+ - "Are we storing passwords correctly?" (hash, don't encrypt)
17
+ - "Make a user's data unrecoverable on account deletion" (crypto-shredding)
18
+
19
+ NOT this skill:
20
+ - Storing/injecting the KEKs, API keys, DB creds, and `.env` material this skill *consumes* → secrets-management (it provisions and rotates the secrets; this skill encrypts data *with* them)
21
+ - The legal side — what counts as PII/PHI, lawful basis, right-to-erasure, data residency → map-privacy-data-gdpr (this skill is the *technical control*, e.g. crypto-shredding, that satisfies those duties)
22
+ - TLS *termination/cert issuance* at the edge proxy, ACME, SNI routing → configure-dns-tls and configure-reverse-proxy-lb (this skill covers the cipher-suite/mTLS *policy*, not cert plumbing)
23
+ - Browser security response headers (HSTS, CSP) → configure-security-headers-csp (HSTS *enforces* HTTPS; this skill is the transport crypto itself)
24
+ - Login sessions, JWT signing/verification, token rotation → auth-jwt-session (signatures/JWE are adjacent but that owns session lifecycle)
25
+ - Identifying the threats/attacker model that justify these controls → threat-model-stride
26
+ - A broad security pass over a diff → security-review (this skill is the deep crypto specialist it defers to)
27
+
28
+ ## Steps
29
+
30
+ 1. **Classify data first, then pick the protection tier — encryption is not the answer to everything.** Three distinct goals need three different tools:
31
+
32
+ | Goal | Use | NEVER |
33
+ |---|---|---|
34
+ | Verify a credential later (passwords) | **slow password hash** (argon2id) — one-way | encrypt; never decrypt a password |
35
+ | Protect data you must read back (PII, PHI, PAN, tokens) | **AEAD encryption** + KMS envelope | reversible "encoding", base64, ROT |
36
+ | Integrity/origin without secrecy | HMAC-SHA-256 / signature | "encrypt to authenticate" |
37
+ | Index/search without revealing value | HMAC-based blind index or deterministic enc | plaintext index column |
38
+
39
+ Encrypting a password is a **bug**, not a feature: anything reversible means an attacker (or insider) with the key gets every plaintext password.
40
+
41
+ 2. **Use AEAD ciphers only. Banned modes are non-negotiable.** Authenticated Encryption with Associated Data gives confidentiality *and* tamper-detection in one primitive:
42
+
43
+ | Use this | Why |
44
+ |---|---|
45
+ | **AES-256-GCM** | hardware-accelerated (AES-NI), NIST-approved, ubiquitous KMS support |
46
+ | **ChaCha20-Poly1305** | faster on CPUs without AES-NI (mobile/ARM), constant-time by design |
47
+ | **AES-256-GCM-SIV / XChaCha20-Poly1305** | nonce-misuse-resistant / 192-bit nonce — prefer when you can't guarantee unique 96-bit nonces |
48
+
49
+ | Banned | Why it's broken |
50
+ |---|---|
51
+ | **ECB** | identical plaintext blocks → identical ciphertext (the "ECB penguin"); leaks structure |
52
+ | **CBC/CTR without a MAC** | unauthenticated → padding-oracle (CBC) & bit-flipping attacks; ciphertext is malleable |
53
+ | **Raw RSA / RSA-PKCS#1v1.5 enc** | use RSA-OAEP, or better ECIES/hybrid; never "RSA the whole payload" |
54
+ | DES/3DES/RC4/MD5/SHA-1 | broken/deprecated |
55
+
56
+ Don't hand-roll "AES + separate HMAC" (encrypt-then-MAC) unless you must — get the construction order wrong and you reintroduce the oracle. Use a vetted library: **libsodium** (`crypto_aead_*` / `secretbox`), **Go** `crypto/cipher` GCM or `nacl/secretbox`, **Python** `cryptography` `AESGCM`/`ChaCha20Poly1305` (not the low-level `Cipher` API), **Java** `javax.crypto` GCM or Google **Tink**, **Rust** `aes-gcm`/`chacha20poly1305` RustCrypto crates, **Node** `crypto.createCipheriv('aes-256-gcm', …)` + `getAuthTag()`. **Tink/libsodium are the senior default** — they pick safe modes and manage nonces for you.
57
+
58
+ 3. **Nonce/IV discipline: unique per (key, message), forever. This is the #1 way AEAD fails.** GCM with a **repeated nonce under the same key is catastrophic** — it leaks the XOR of plaintexts *and* the authentication key (forgery). Rules:
59
+ - 96-bit (12-byte) nonce for GCM. Either **random** from a CSPRNG (`os.urandom`/`crypto.randomBytes`/`getrandom`) or a **monotonic counter** — never both, never `0`, never a timestamp, never reuse.
60
+ - Random 96-bit nonces are safe only up to **~2³² messages per key** (birthday bound). High-volume? Rotate the DEK sooner, or use **XChaCha20-Poly1305 (192-bit nonce)** / **AES-GCM-SIV** which tolerate accidental reuse.
61
+ - **Store the nonce alongside the ciphertext** (it's not secret) — typical record = `version ‖ nonce ‖ ciphertext ‖ tag`.
62
+ - Don't derive the nonce from the plaintext or a non-unique field. Don't reuse one nonce across a re-encrypt.
63
+
64
+ 4. **Bind ciphertext to its context with AAD (Associated Data).** AEAD lets you authenticate (not encrypt) extra context — pass the **row id / tenant id / column name / key version** as AAD. This stops an attacker from copying a valid ciphertext from row A into row B (ciphertext substitution): decryption of B fails because the AAD no longer matches. AAD must be reconstructible at decrypt time from the record's own metadata.
65
+
66
+ 5. **Envelope encryption: a KMS-held KEK wraps per-record/per-tenant DEKs. Never encrypt bulk data directly with the KMS key.** The pattern that scales and rotates cleanly:
67
+
68
+ ```
69
+ 1. KMS.GenerateDataKey(KeyId=KEK, KeySpec=AES_256)
70
+ → returns { Plaintext DEK, Encrypted DEK (wrapped by KEK) }
71
+ 2. Encrypt your data locally with the plaintext DEK (AES-256-GCM, fresh nonce)
72
+ 3. Store: encrypted_dek ‖ key_version ‖ nonce ‖ ciphertext ‖ tag
73
+ 4. ZERO the plaintext DEK from memory immediately after use
74
+ 5. Decrypt: KMS.Decrypt(encrypted_dek) → plaintext DEK → local AEAD decrypt
75
+ ```
76
+
77
+ - **KEK** lives in **AWS KMS / GCP KMS / Azure Key Vault / Vault Transit / an HSM** and *never leaves it* — KMS does the wrap/unwrap, your app never sees KEK bytes. **DEK** is short-lived in app memory, zeroed after use.
78
+ - Granularity: **per-tenant or per-record DEK** for crypto-shredding (delete the DEK → that data is gone). Per-row is most flexible; cache the unwrapped DEK briefly (e.g. LRU with TTL) to avoid a KMS call per row.
79
+ - Tools: AWS **KMS** + the **AWS Encryption SDK** (handles the envelope + nonce for you), GCP **KMS**, HashiCorp **Vault Transit** (`vault write transit/encrypt/...` — Vault holds the key, returns ciphertext), or **Tink**'s `KmsEnvelopeAead`. Prefer these over rolling your own envelope.
80
+
81
+ 6. **Per-field/column encryption for PII — choose deterministic vs randomized by query need.** Application-layer (encrypt before the DB sees it) beats trusting only DB-native TDE, because TDE protects the *disk file*, not a SQL-injection or a DBA reading rows.
82
+
83
+ | Mode | Same plaintext → | Lets you | Cost |
84
+ |---|---|---|---|
85
+ | **Randomized** (fresh nonce) | different ciphertext | only decrypt-then-use | leaks nothing; **default for PII** |
86
+ | **Deterministic** (synthetic IV / SIV) | same ciphertext | equality lookup, joins, unique constraint | leaks equality (which rows share a value) |
87
+
88
+ For *searchable* encryption use a **blind index**: store `HMAC-SHA256(key, normalize(value))` in a separate indexed column and query by that, keeping the value column randomized-encrypted. Don't reach for order-preserving/fully-homomorphic encryption (leaky / impractical) unless you truly understand the tradeoff. Postgres `pgcrypto` is fine for small cases but does *application-visible* keys in SQL logs — prefer encrypting in the app. **Don't encrypt a column you need range-query or `LIKE` on** without redesigning the access pattern first.
89
+
90
+ 7. **Passwords: hash with a memory-hard KDF, salted and parameterized — never encrypt, never plain SHA-256.** Use:
91
+
92
+ | Algorithm | Params (2025 baseline) |
93
+ |---|---|
94
+ | **argon2id** (first choice) | m=19–64 MiB, t=2–3, p=1; OWASP min m=19 MiB,t=2,p=1 |
95
+ | **scrypt** | N=2^17, r=8, p=1 (or N=2^15 for lighter) |
96
+ | **bcrypt** (legacy/compat) | cost ≥ 12; **pre-hash with SHA-256 + base64** if password may exceed 72 bytes (bcrypt silently truncates) |
97
+
98
+ - A **per-password random salt** is mandatory (the libraries generate and embed it in the encoded hash — `$argon2id$v=19$m=...`). No global "pepper-as-salt".
99
+ - **Pepper** (optional, defense-in-depth) = a secret key *not* in the DB; either HMAC the password before hashing or keep it in a KMS/HSM. Store the pepper in secrets-management, never beside the hash.
100
+ - **Never** use fast hashes (MD5, SHA-1, SHA-256, SHA-512) bare for passwords — GPUs do billions/sec. **Never** encrypt passwords (reversible = breach of all of them).
101
+ - Verify in **constant time** (the KDF's `verify`/`checkpw` does this); re-hash on login if cost params have since increased.
102
+
103
+ 8. **TLS in transit: 1.2 minimum, 1.3 preferred; modern cipher suites; mTLS for service-to-service.**
104
+ - **Versions:** disable SSLv3/TLS 1.0/1.1 entirely. Allow **TLS 1.2 + 1.3**; prefer 1.3 (1-RTT, AEAD-only, forward-secret by construction).
105
+ - **TLS 1.2 cipher suites** (AEAD + ECDHE forward secrecy only): `ECDHE-ECDSA-AES128-GCM-SHA256`, `ECDHE-RSA-AES256-GCM-SHA384`, `ECDHE-*-CHACHA20-POLY1305`. **No** CBC suites, no static RSA key exchange, no `NULL`/`RC4`/`3DES`/`EXPORT`. TLS 1.3 only offers AEAD suites, so the choice is made for you.
106
+ - Mozilla SSL Config "**Intermediate**" is the safe default; "Modern" = TLS 1.3-only. Verify with **`testssl.sh`** or SSL Labs (target **A/A+**). Enable **HSTS** at the edge (handoff to configure-security-headers-csp).
107
+ - **mTLS** for internal/service-to-service: both sides present certs; pin to your CA, short-lived certs (SPIFFE/SVID, Istio, Linkerd, or a service-mesh issuer). Validate the **full chain + SAN**, not just "a cert was presented."
108
+ - **Never disable cert verification** (`verify=False`, `rejectUnauthorized:false`, `InsecureSkipVerify:true`) outside a throwaway test — it silently turns TLS into plaintext-to-anyone.
109
+
110
+ 9. **Key rotation with versioned keys — rotate the KEK cheaply, re-wrap DEKs, lazy-re-encrypt data.** Store a **`key_version`** with every ciphertext so multiple key generations coexist:
111
+ - **KEK rotation** (cheapest, do on schedule, e.g. annually or per policy): KMS rotates the KEK; you **re-wrap each DEK** (decrypt-unwrap with old, wrap with new). Bulk data is *untouched* — that's the whole point of envelope encryption.
112
+ - **DEK rotation:** generate a new DEK, **re-encrypt the affected records lazily** (on next write, or a background backfill) and bump `key_version`. Keep old key versions readable until backfill completes, then retire.
113
+ - **On compromise:** rotate immediately and force re-encryption; **crypto-shred** by destroying a DEK to make its data permanently unrecoverable (the GDPR-erasure trick — handoff to map-privacy-data-gdpr).
114
+ - Decrypt path must **dispatch on the stored `key_version`**; never assume "current key." Keep a registry of retired versions for audit.
115
+
116
+ 10. **Operational hygiene — the parts that get forgotten.** Generate all keys/nonces/salts from a **CSPRNG** (`os.urandom`, `crypto.randomBytes`, `getrandom(2)`, `SecureRandom`) — never `Math.random`/`rand()`/`mt19937`. **Zero plaintext keys** from memory after use where the language allows (Go `defer` wipe, Rust `zeroize`, libsodium `sodium_memzero`). Don't log plaintext, keys, or full ciphertext. Encrypt **backups and replicas** too (same KMS). Use **constant-time comparison** for MACs/tags/tokens (`hmac.compare_digest`, `crypto.timingSafeEqual`, `subtle.ConstantTimeCompare`) — `==` leaks via timing. Run a **`security-review`** over the crypto diff before shipping.
117
+
118
+ ## Common Errors
119
+
120
+ - **Encrypting passwords instead of hashing.** Reversible = one key compromise dumps every password. Fix: argon2id/bcrypt, one-way (step 7).
121
+ - **Plain/fast hash for passwords** (`SHA256(password)`, unsalted MD5). GPUs crack billions/sec; rainbow tables for unsalted. Fix: memory-hard KDF with per-password salt.
122
+ - **ECB mode / unauthenticated CBC.** ECB leaks structure; CBC-without-MAC → padding oracle, malleable ciphertext. Fix: AEAD (AES-GCM/ChaCha20-Poly1305) only.
123
+ - **Nonce/IV reuse under one key (GCM).** Catastrophic — leaks plaintext XOR *and* the auth key (forgeries). Fix: unique nonce per message; XChaCha20/GCM-SIV if you can't guarantee it (step 3).
124
+ - **Hardcoded / static IV** (`iv = new byte[12]` all zeros). Same as reuse. Fix: fresh CSPRNG nonce per encryption, stored with ciphertext.
125
+ - **Encrypting bulk data directly with the KMS/KEK.** Throughput and cost explode; no clean rotation. Fix: envelope — KEK wraps per-record DEK (step 5).
126
+ - **No AAD binding.** Valid ciphertext copy-pasted between rows/tenants decrypts fine. Fix: pass row/tenant/version as AAD (step 4).
127
+ - **No key version on ciphertext.** Rotation becomes a flag-day re-encrypt-everything. Fix: store `key_version`, dispatch decryption on it (step 9).
128
+ - **Plaintext DEK left in memory / logged.** Heap dump or log leak = game over. Fix: zero after use; never log keys/plaintext/tags.
129
+ - **`Math.random()` / `rand()` for keys, nonces, or salts.** Predictable → forgeable. Fix: CSPRNG only.
130
+ - **Disabling TLS verification** (`verify=False`, `InsecureSkipVerify`, `rejectUnauthorized:false`). Silent MITM. Fix: validate chain + SAN; only bypass in isolated tests.
131
+ - **Weak TLS** (TLS 1.0/1.1, CBC suites, static RSA, RC4/3DES). Fix: TLS 1.2+/1.3, AEAD+ECDHE suites; verify with testssl.sh.
132
+ - **`==` on MACs/tags/tokens.** Timing side-channel. Fix: constant-time comparison.
133
+ - **Roll-your-own crypto / `Cipher` low-level API.** Easy to misorder encrypt-then-MAC, mishandle padding. Fix: libsodium / Tink / AWS Encryption SDK.
134
+ - **Deterministic encryption on high-cardinality PII you didn't mean to.** Leaks equality patterns. Fix: randomized by default; deterministic/blind-index only where a query needs it (step 6).
135
+
136
+ ## Verify
137
+
138
+ 1. **No banned modes/algorithms:** grep the diff for `ECB`, `AES/CBC` without an accompanying MAC, `DES`, `RC4`, `MD5`/`SHA1` on secrets, raw RSA encrypt — zero hits. All symmetric encryption is AES-GCM / ChaCha20-Poly1305 (AEAD).
139
+ 2. **Passwords are hashed, not encrypted:** grep finds argon2id/bcrypt/scrypt on the password path and **no** encrypt/decrypt of passwords; salts are per-password (encoded in the hash); cost params meet the step-7 baseline.
140
+ 3. **Nonce uniqueness:** confirm every encryption draws a fresh CSPRNG nonce (or a guaranteed-unique counter); no static/zero IV; nonce stored with ciphertext. For high volume, DEK rotation or a nonce-misuse-resistant mode is in place.
141
+ 4. **Envelope encryption holds:** bulk data is encrypted with a DEK, the DEK is wrapped by a KMS-held KEK that never leaves KMS, plaintext DEK is zeroed after use, and a `key_version` is stored per record.
142
+ 5. **AAD binds context:** moving a valid ciphertext from one row/tenant to another **fails** decryption (AAD mismatch).
143
+ 6. **Rotation works without re-encrypting everything:** rotating the KEK re-wraps DEKs only; old `key_version` ciphertext still decrypts; a DEK-destroy crypto-shreds its records (they become permanently undecryptable).
144
+ 7. **TLS posture:** `testssl.sh <host>` / SSL Labs returns **A/A+** — TLS 1.2+ only, AEAD+forward-secret suites, no CBC/RC4/3DES; mTLS validates the full chain + SAN; no `verify=False`/`InsecureSkipVerify` in non-test code.
145
+ 8. **Randomness + timing:** all keys/nonces/salts come from a CSPRNG (no `Math.random`/`rand`); MAC/tag/token comparisons are constant-time.
146
+ 9. **Tamper detection:** flipping one ciphertext byte makes decryption **fail** (auth tag rejects it) rather than returning garbage plaintext.
147
+
148
+ Done = sensitive data is encrypted with AEAD under unique nonces, bulk data uses KMS envelope encryption with versioned, rotatable keys and context-binding AAD, passwords are hashed with argon2id/bcrypt (never encrypted), PII fields are randomized-encrypted (deterministic/blind-index only where a query demands it), transport is TLS 1.2+/1.3 with modern suites and mTLS where needed, and all keys/nonces/salts come from a CSPRNG with constant-time tag checks — all proven by checks 1–9, with `security-review` run over the crypto diff.
@@ -0,0 +1,130 @@
1
+ ---
2
+ name: feature-flags-rollout
3
+ description: Implements feature flags and progressive delivery — kill switches, percentage/targeted rollouts, sticky hashed bucketing, fail-safe evaluation, 1→10→50→100 ramps with guardrail-metric rollback, and TTL-enforced stale-flag cleanup — so changes ship decoupled from deploys and reverse in seconds.
4
+ when_to_use: Adding a flag, gating a feature, running a percentage/canary/ring rollout decoupled from deploy, building a kill switch, targeting by user/segment/plan, or paying down flag debt. Covers OpenFeature-compatible managed flag platforms, vendor SDKs, and homegrown flag tables. Distinct from deploy-release (ships the artifact; flags gate behavior inside it) and auth-jwt-session (establishes entitlement; flags must never compute it).
5
+ ---
6
+
7
+ ## When to Use
8
+
9
+ Reach for this skill when the request is about **decoupling a behavior change from the deploy that carries it**:
10
+
11
+ - "Put this behind a flag so we can turn it off without redeploying"
12
+ - "Roll it out to 1% / 10% / a canary ring, then ramp"
13
+ - "Add a kill switch for the new checkout / payments path"
14
+ - "Only enable for plan=enterprise / this segment / our internal allowlist"
15
+ - "Migrate from env-var booleans to a managed flag platform / OpenFeature"
16
+ - "Clean up dead flags / we have 300 flags and nobody knows which are live"
17
+
18
+ NOT this skill:
19
+ - Shipping/promoting the build, blue-green, canary *infra*, rollback of the artifact → deploy-release
20
+ - Deciding *who the user is* or whether they paid → auth-jwt-session (a flag gates rollout; it does not grant entitlement)
21
+ - Computing experiment lift / significance / metric tables from exposure logs → write-analytical-sql
22
+ - Hiding the provider SDK key / signing flag payloads → secrets-management
23
+ - Gating prompt/model changes behind a regression score → llm-eval-harness
24
+
25
+ ## Steps
26
+
27
+ 1. **Classify the flag first — type dictates lifetime, owner, and removal policy.** Do not create a flag without picking one.
28
+
29
+ | Type | Purpose | Lifetime | Owner | Removal |
30
+ |---|---|---|---|---|
31
+ | **Release** | Gate in-progress code, ramp it | days–weeks | feature author | **delete at 100% or revert** — TTL-enforced |
32
+ | **Kill switch (ops)** | Instantly disable a risky path | permanent | on-call/SRE | keep; review yearly |
33
+ | **Ops/config** | Tunables (timeouts, batch size, region) | permanent | platform | keep; document |
34
+ | **Experiment** | A/B exposure split | length of test | data/PM | delete when test concludes |
35
+ | **Permission/entitlement** | Plan/role gating | permanent | product | keep — but source of truth is auth, not the flag |
36
+
37
+ Release flags are 90% of debt. Every one gets an owner + removal date at creation (step 7). Default any new flag to **release** unless it's clearly a permanent switch.
38
+
39
+ 2. **Pick the evaluation locus — server-side by default.** Evaluate where the decision is *trusted and cheap*.
40
+
41
+ | Locus | Use for | Hard rule |
42
+ |---|---|---|
43
+ | **Server** (default) | entitlement-adjacent gating, anything secret, backend behavior | rule logic + flag values never leave the server |
44
+ | **Client** | pure UX (show new button, layout) | only flags in the **public** set; client can lie |
45
+ | **Edge/CDN** | geo/ring routing at the boundary | static rules only |
46
+
47
+ **Never evaluate an entitlement or paywall in the browser** — the user controls the client and can flip any client-side flag with devtools. Gate the *capability* server-side; the client flag only hides the UI. Server SDKs evaluate locally against a streamed ruleset (no per-request network call); client SDKs fetch a bootstrapped, scoped flag set.
48
+
49
+ 3. **Define a deterministic key + a fail-safe default.** The default is what runs when the provider is unreachable — and it *will* be unreachable.
50
+
51
+ ```ts
52
+ // ONE typed helper, the only place flags are read (step 5).
53
+ export function flag<T>(key: FlagKey, ctx: EvalContext, fallback: T): T {
54
+ try {
55
+ return client.variation(key, ctx, fallback); // local eval, no network
56
+ } catch (e) {
57
+ metrics.increment("flag.eval_error", { key });
58
+ return fallback; // FAIL-SAFE: never throw
59
+ }
60
+ }
61
+ // Release flag → fallback = OFF (old code path). Kill switch → fallback = "killed/safe".
62
+ ```
63
+
64
+ Rules: a flag read **must not throw, block, or call out per request**. Fall to **last-known-good** (SDK cache) → then the **hardcoded fallback**. For release flags the fallback is the *old* behavior (fail-off). For kill switches the fallback is the *safe* state (path disabled). Never let SDK init failure crash startup — init async with a timeout and serve fallbacks until ready.
65
+
66
+ 4. **Targeting — percentage by stable hashed bucketing, not RNG.** Bucketing must be **sticky**: the same user sees the same variant across requests, servers, and deploys.
67
+
68
+ ```ts
69
+ // Deterministic bucket 0..9999 — identical on every server, no shared state.
70
+ function bucket(flagKey: string, unitId: string): number {
71
+ const h = sha1(`${flagKey}:${unitId}`); // salt with flagKey so flags are independent
72
+ return parseInt(h.slice(0, 8), 16) % 10000;
73
+ }
74
+ const inRollout = bucket("new-checkout", user.id) < rolloutPct * 100; // 10% → <1000
75
+ ```
76
+
77
+ - **Bucketing unit** = a stable id (userId / accountId / deviceId) — **never** session id, request time, or `Math.random()` (those reshuffle users every request → broken/flickering UX and uninterpretable experiments).
78
+ - Salt the hash with the flag key so two flags at 10% don't hit the *same* 10% of users (correlated rollouts).
79
+ - **Rule order:** allowlist (force-on for QA/internal) → segment/plan rules → percentage → default. First match wins; make precedence explicit.
80
+ - Ramping the percentage must only *add* users, never reshuffle: monotonic threshold on a fixed hash guarantees a user inside 10% stays inside 50%.
81
+
82
+ 5. **Wrap every read behind the one typed helper from step 3.** No raw `client.variation(...)` or `process.env.FEATURE_X` scattered in code. Centralizing gives you: a single fallback policy, one audit point for cleanup, typed keys (no stringly-typed typos), and a place to log exposure for experiments. Key names are namespaced and stable: `team.feature.scope` (e.g. `checkout.new-flow.enabled`).
83
+
84
+ 6. **Ramp on a schedule with a guardrail metric and a one-flip rollback.** Decoupled from deploy means rollback = flip the flag, not redeploy.
85
+
86
+ | Stage | Audience | Hold | Watch (guardrail) |
87
+ |---|---|---|---|
88
+ | 0% + allowlist | internal/QA | until smoke passes | manual QA |
89
+ | 1% | canary cohort | ≥1 peak hour | error rate, p99 latency, the feature's own success metric |
90
+ | 10% | — | ≥1 business day | + downstream load, support tickets |
91
+ | 50% | — | ≥1 day | + cost / DB / queue depth |
92
+ | 100% | everyone | bake 1 week | then **delete the flag** (step 7) |
93
+
94
+ Pick the guardrail **before** ramping (e.g. "5xx rate must stay <0.5%, checkout-success must not drop >1pp"). Wire an automated trip if you can: guardrail breach → set flag to 0% (kill). A flag flip propagates in seconds; a redeploy does not — that gap is the entire point. Never jump 1%→100%.
95
+
96
+ 7. **Lifecycle = owner + removal date + CI enforcement.** A flag with no expiry is permanent debt.
97
+ - At creation, record `{ owner, type, created, removeBy }` (flag description, a registry table, or `// @flag-owner @removeBy=YYYY-MM-DD` next to the helper call).
98
+ - **CI fails the build when a `release` flag passes its `removeBy`** — grep flag metadata, exit nonzero on any overdue release flag. This is the single highest-leverage anti-debt control.
99
+ - Cleanup is a real PR: delete the flag key in the provider **and** the `flag()` call **and** the now-dead branch — keep the winning path, remove the loser. Archive the flag (don't hard-delete history) so old exposure logs stay interpretable.
100
+ - Kill switches and ops flags are exempt from TTL but get an annual review.
101
+
102
+ 8. **Test both branches; flag-off is the safe default.** Every gated change has two live code paths — both must be tested and shippable. Default the flag **off** in test config (proves old path still works), then run the suite again with it **on**. A PR that only works with the flag on is not done.
103
+
104
+ ## Common Errors
105
+
106
+ - **`Math.random()` / time / session id as the bucketing unit.** Users flicker between variants every request — broken UX and uninterpretable experiments. Hash a stable user/account id.
107
+ - **Two flags at 10% hitting the *same* users.** Forgot to salt the hash with the flag key, so rollouts are correlated. Salt = `flagKey:unitId`.
108
+ - **Reshuffling on ramp.** Changing the hash scheme/seed when going 10%→50% moves users *out* of the rollout, regressing them mid-flight. Use a monotonic threshold on one fixed hash.
109
+ - **Flag read that throws or blocks.** Provider hiccup takes down the request path. Wrap in the helper; fail to last-known-good then fallback; never network-call per request (server SDKs eval locally).
110
+ - **SDK init crashes startup.** Synchronous blocking init against an unreachable provider hangs boot. Init async with timeout, serve fallbacks until ready.
111
+ - **Entitlement evaluated client-side.** A client-side flag "unlocks" a paid feature — trivially bypassed in devtools. Gate the capability server-side; the client flag only hides UI (auth-jwt-session owns the grant).
112
+ - **Fail-open release flag.** Provider down → fallback is the *new, unfinished* path. Release fallback must be **off** (old path); only ops defaults bias to "on".
113
+ - **`process.env.FEATURE_X` booleans.** Env flags need a redeploy to flip — that's a config change, not a runtime kill switch, and defeats decoupling. Use the provider/table behind the helper.
114
+ - **Only testing the on-path.** Flag-off regressions ship silently because nobody ran the suite with the flag off. Test both states; off is the default.
115
+ - **Flag never removed.** 100% rolled out months ago, both branches still in code, the loser rotting. CI must fail past `removeBy`; cleanup deletes the dead branch.
116
+ - **Stale flag still referenced after provider deletion.** Deleting the key in the dashboard but leaving the `flag()` call → it silently serves the fallback forever (often the wrong one). Delete provider key and code in the same PR.
117
+
118
+ ## Verify
119
+
120
+ 1. **Determinism / stickiness:** Evaluate the same flag for the same user id 1000× across ≥2 processes → identical variant every time. Restart the service → still identical.
121
+ 2. **Independent rollouts:** Two flags at the same percentage do **not** select the same user set (hash is flag-salted) — compare the bucketed cohorts, overlap ≈ percentage², not 100%.
122
+ 3. **Monotonic ramp:** Take the users inside the 10% rollout; raise to 50% → every one of them is still inside (no user regresses out). Lower back → only the added tail leaves.
123
+ 4. **Fail-safe:** Block/kill the provider (firewall the SDK endpoint), send traffic → every read returns the fallback, nothing throws, requests still succeed, and `flag.eval_error` is emitted (not silent).
124
+ 5. **Kill switch latency:** Flip a kill switch to off → the gated path stops within the SDK's stream/poll interval (seconds), with **no deploy**. Time it.
125
+ 6. **Both branches green:** Full test suite passes with the flag **off** (default) and again with it **on**. CI runs at least the off state.
126
+ 7. **No raw reads:** `grep -rE "\.variation\(|process\.env\.FEATURE" src/` returns only the single helper file — every other read goes through `flag()`.
127
+ 8. **TTL enforcement:** A `release` flag with a past `removeBy` makes CI **exit nonzero**. Verify by backdating one in a throwaway branch.
128
+ 9. **Entitlement is server-trusted:** With the client-side flag forced on in devtools, the server still refuses the gated capability (403/empty), proving the browser can't unlock it.
129
+
130
+ Done = bucketing is deterministic + flag-salted + monotonic under ramp, every read goes through the fail-safe helper (provider-down test serves fallbacks without throwing), the kill switch flips in seconds with no deploy, both flag branches pass tests with off as default, no entitlement is decided client-side, and CI fails on any release flag past its removal date.