crewforth 3.1.0-rc.1 → 3.1.0-rc.2
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 +10 -2
- package/kit/eval/smoke-test.sh +29 -0
- package/kit/skills/frontend-flutter/SKILL.md +39 -9
- package/kit/skills/frontend-flutter/references/adaptive-and-accessible.md +29 -3
- package/kit/skills/frontend-flutter/references/platform-async-release.md +41 -0
- package/kit/skills/frontend-flutter/references/sensitive-data.md +54 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -80,8 +80,16 @@ versioning follows [SemVer](https://semver.org/).
|
|
|
80
80
|
rules of its architecture guide, the rebuild cost model, layout by window size and never by device type, isolates,
|
|
81
81
|
platform channels, generated localisations, the mix of unit, widget, integration and golden tests, measuring in
|
|
82
82
|
profile mode on a real device, and a release without a secret in it. Its done-definition is `dart format
|
|
83
|
-
--set-exit-if-changed .`, `flutter analyze`, `flutter test` and the accessibility guideline test.
|
|
84
|
-
|
|
83
|
+
--set-exit-if-changed .`, `flutter analyze`, `flutter test` and the accessibility guideline test. It also holds
|
|
84
|
+
what a real app taught it: panes that split at a foldable's hinge, a list and a detail whose selection and typed
|
|
85
|
+
input survive a change of window class, a widget test at one width of each window class, figures of one width for
|
|
86
|
+
amounts, case changed by the language's rules, no inline text (one central file is enough for one language), a
|
|
87
|
+
domain layer with no `package:flutter` import, goldens made in one pinned container, and a store build that
|
|
88
|
+
refuses the flag of a closed feature. For an app that holds sensitive data: an encrypted database, its key in the
|
|
89
|
+
platform's secure store, both kept out of backup and device transfer, every network call through a transport bound to its own allowed hosts,
|
|
90
|
+
and no ad in a window that shows a sensitive screen. The rules were checked against Flutter's, Dart's and the
|
|
91
|
+
named packages' and platforms' own pages; the sources, and what was not checked, are in the skill's `references/`.
|
|
92
|
+
40 skills now.
|
|
85
93
|
- **In `auto` and `dontAsk`, your own message approves a commit or a push.** Those modes answer a permission prompt
|
|
86
94
|
with software, so the commit gate fails closed there and the only way through was to switch mode for every commit.
|
|
87
95
|
Now a message that is nothing but `approve: commit`, `approve: push` or `approve: commit+push` (`onay:` works too)
|
package/kit/eval/smoke-test.sh
CHANGED
|
@@ -9171,6 +9171,35 @@ if [ -n "$SGR" ] && [ -f "$SGR/.gitattributes" ] && [ -d "$SGR/packaging" ] && [
|
|
|
9171
9171
|
[ "$(printf 'README.md\r\n' | bash "$CDO")" = docs ] || _cv="$_cv crlf"
|
|
9172
9172
|
[ -z "$_cv" ] && pass "ci-docs-only.sh: documentation reads docs; a hook, evals/run.sh, kit/README.md, ci.yml or an empty diff reads code" \
|
|
9173
9173
|
|| fail "ci-docs-only.sh gave the wrong verdict for:$_cv"
|
|
9174
|
+
# The third answer, `text`: a pull request that changes only the text of skills, agents or commands skips the
|
|
9175
|
+
# macOS and Windows jobs as a documentation one does, and still runs the Linux verify job. That list is pinned
|
|
9176
|
+
# the same way, and every pattern must be a .md under one of the six text directories: `kit/*`, a script or
|
|
9177
|
+
# kit/CLAUDE.md in it would let a gate change skip the platforms where it can break.
|
|
9178
|
+
cdo_tpats(){ awk '/# TEXT-PATTERNS-START/{f=1;next} /# TEXT-PATTERNS-END/{f=0} f && /\) *return 0/ {sub(/^[[:space:]]*/,""); sub(/\).*/,""); print}' "$1"; }
|
|
9179
|
+
cdo_tunsafe(){ while IFS= read -r _p; do case "$_p" in kit/skills/\*.md|kit/agents/\*.md|kit/commands/\*.md|plugin/skills/\*.md|plugin/agents/\*.md|plugin/commands/\*.md) ;; *) printf '%s\n' "$_p" ;; esac; done; }
|
|
9180
|
+
_tp="$(cdo_tpats "$CDO" | tr '\n' ' ' | sed 's/ $//')"
|
|
9181
|
+
_twant='kit/skills/*.md kit/agents/*.md kit/commands/*.md plugin/skills/*.md plugin/agents/*.md plugin/commands/*.md'
|
|
9182
|
+
_ttu="$(printf '%s\n' 'kit/*' 'kit/hooks/*.md' 'kit/skills/*' 'kit/skills/*.md' | cdo_tunsafe | grep -c .)"
|
|
9183
|
+
if [ "$_tp" != "$_twant" ]; then fail "the text-only list changed — it reads '$_tp', pinned '$_twant'. Widening it lets a change skip macOS and Windows; update this pin deliberately"
|
|
9184
|
+
elif [ -n "$(cdo_tpats "$CDO" | cdo_tunsafe)" ]; then fail "a text-only pattern is not a .md under skills, agents or commands: $(cdo_tpats "$CDO" | cdo_tunsafe | tr '\n' ' ')"
|
|
9185
|
+
elif [ "$_ttu" != 3 ]; then fail "the text-pattern check caught $_ttu of 3 planted patterns (kit/*, kit/hooks/*.md, kit/skills/*) — it reads nothing"
|
|
9186
|
+
else pass "the text-only list is exactly the .md files of skills, agents and commands in kit/ and plugin/; 3 planted wider patterns are caught"; fi
|
|
9187
|
+
_cv=""
|
|
9188
|
+
[ "$(cdo kit/skills/x/SKILL.md plugin/skills/x/references/y.md)" = text ] || _cv="$_cv text-set"
|
|
9189
|
+
[ "$(cdo kit/agents/a.md CHANGELOG.md README.md)" = text ] || _cv="$_cv text+docs"
|
|
9190
|
+
[ "$(cdo kit/skills/x/SKILL.md kit/hooks/guard-bash.sh)" = code ] || _cv="$_cv text+hook"
|
|
9191
|
+
[ "$(cdo kit/skills/x/scripts/run.sh)" = code ] || _cv="$_cv skill-script"
|
|
9192
|
+
[ "$(cdo kit/skills/x/SKILL.md kit/CLAUDE.md)" = code ] || _cv="$_cv text+rulebook"
|
|
9193
|
+
[ "$(cdo kit/skills/x/SKILL.md kit/hooks/trace-blocklist.txt)" = code ] || _cv="$_cv text+blocklist"
|
|
9194
|
+
[ "$(cdo README.md)" = docs ] || _cv="$_cv docs-stays-docs"
|
|
9195
|
+
[ -z "$_cv" ] && pass "ci-docs-only.sh: skill, agent and command text reads text, with documentation too; with a hook, a skill's script, kit/CLAUDE.md or a blocklist it reads code" \
|
|
9196
|
+
|| fail "ci-docs-only.sh gave the wrong text verdict for:$_cv"
|
|
9197
|
+
# The Linux verify job is gated on nothing: it holds the checks a text change can break.
|
|
9198
|
+
_cyv="$(awk '/^ verify:$/{f=1;next} f && /^ [a-z-]+:$/{f=0} f' "$SGR/.github/workflows/ci.yml" | grep -cE '^ (if|needs):')"
|
|
9199
|
+
_cyn="$(awk '/^ verify:$/{f=1;next} f && /^ [a-z-]+:$/{f=0} f' "$SGR/.github/workflows/ci.yml" | grep -c 'bash packaging/verify.sh')"
|
|
9200
|
+
[ "$_cyv" = 0 ] && [ "$_cyn" -ge 1 ] \
|
|
9201
|
+
&& pass "ci.yml runs the Linux verify job on every pull request, a text-only one included (no if:, no needs:)" \
|
|
9202
|
+
|| fail "ci.yml's verify job is conditional ($_cyv if:/needs: line(s)) or was not found ($_cyn verify.sh call(s)): a text-only pull request would skip the budget, pointer, routing and plugin checks"
|
|
9174
9203
|
# ...and ci.yml really uses it: both cross-platform jobs are gated on its answer, nothing else is.
|
|
9175
9204
|
_cy="$SGR/.github/workflows/ci.yml"
|
|
9176
9205
|
_gated="$(grep -c "if: needs.changes.outputs.scope == 'code'" "$_cy")"
|
|
@@ -13,8 +13,8 @@ Otherwise do not use this skill: `frontend` and the project's own stack apply (s
|
|
|
13
13
|
Trigger phrases: "riverpod", "go_router", "pubspec", "widget test", "analysis_options", "flutter_test", "golden test", "platform channel", "dart isolate", "gen-l10n"
|
|
14
14
|
|
|
15
15
|
For generic frontend discipline, applies the `frontend` skill; this file only covers what Flutter adds. Every rule
|
|
16
|
-
below was checked against Flutter's own documentation (docs.flutter.dev, dart.dev)
|
|
17
|
-
`references
|
|
16
|
+
below was checked against Flutter's own documentation (docs.flutter.dev, dart.dev) or the page of the package or
|
|
17
|
+
platform it names; the depth and the sources are in `references/`, which also say what was not checked. Not used in a project on another stack (delete it if needed).
|
|
18
18
|
|
|
19
19
|
## The project decides, not this file
|
|
20
20
|
- **Read before you write:** `pubspec.yaml` (the packages already chosen), `analysis_options.yaml` (the lint set and
|
|
@@ -31,7 +31,8 @@ below was checked against Flutter's own documentation (docs.flutter.dev, dart.de
|
|
|
31
31
|
an abstract class, so tests can use a fake. **No logic in widgets** — a widget renders state and forwards events.
|
|
32
32
|
- **Unidirectional data flow** and **immutable models**; a model that crosses a layer is not mutated in place.
|
|
33
33
|
- **Dependencies are injected** into view models and repositories, not looked up from inside them. A domain layer
|
|
34
|
-
only when the logic needs one (the guide marks it conditional).
|
|
34
|
+
only when the logic needs one (the guide marks it conditional). **Where there is one it is plain Dart: no file in
|
|
35
|
+
it imports `package:flutter`**, and an architecture test reads its files and fails on such an import.
|
|
35
36
|
- Feature folders (`features/<name>/`) with view, view model and tests together, as `frontend` says.
|
|
36
37
|
|
|
37
38
|
## Widgets and rebuilds — the cost model
|
|
@@ -48,6 +49,11 @@ below was checked against Flutter's own documentation (docs.flutter.dev, dart.de
|
|
|
48
49
|
and desktop). Measure with **`MediaQuery.sizeOf`** (the whole window; cheaper than `MediaQuery.of`) or
|
|
49
50
|
**`LayoutBuilder`** (the space this widget is given). Breakpoints come from the project's design system; the
|
|
50
51
|
Material window classes are compact < 600 · medium 600–839 · expanded 840–1199 · large 1200–1599 · ≥ 1600 dp.
|
|
52
|
+
- **List and detail:** two panes side by side in an expanded window, a push to the detail in a compact one. **What
|
|
53
|
+
the user selected and what they typed survive a change of window class** (a fold, a rotation, a resized window):
|
|
54
|
+
that state lives above the two layouts, not inside either.
|
|
55
|
+
- **A foldable:** when `MediaQuery.displayFeatures` holds a hinge that separates the window, the panes split at the
|
|
56
|
+
hinge and no content is laid out on it.
|
|
51
57
|
- **Do not lock orientation** — it is an accessibility problem. Keep scroll position across a layout change
|
|
52
58
|
(`PageStorageKey`), and support mouse, trackpad and keyboard where the app runs on desktop or a large screen.
|
|
53
59
|
- Accessibility: tap targets 48×48 (Android) / 44×44 (iOS), every tappable labelled, text contrast checked, the UI
|
|
@@ -66,23 +72,45 @@ below was checked against Flutter's own documentation (docs.flutter.dev, dart.de
|
|
|
66
72
|
## Navigation, text, data
|
|
67
73
|
- Navigation: the project's router. Named routes are not recommended by Flutter (no custom deep-link handling, no
|
|
68
74
|
browser forward button); a page-backed route is deep-linkable, a pageless one is not.
|
|
69
|
-
-
|
|
70
|
-
|
|
75
|
+
- **No user-facing text is written inline in a widget, in any app.** An app in more than one language uses
|
|
76
|
+
`gen-l10n` (ARB files, `AppLocalizations.of(context)`), plurals and placeholders in ICU form — never concatenated;
|
|
77
|
+
the `i18n-integrity` skill checks the files. An app in one language may keep every text in one central file instead.
|
|
78
|
+
- **Changing case follows the language.** Dart's `toUpperCase()` / `toLowerCase()` use the language-independent
|
|
79
|
+
mapping (Turkish `i` becomes `I`, not `İ`), so text is cased through one helper that takes the locale.
|
|
71
80
|
- Money is never a `double`; dates carry their time zone or are date-only on purpose (the `frontend` rules apply).
|
|
81
|
+
**Amounts and other numbers that line up are drawn with `FontFeature.tabularFigures()`**, so digits keep one width.
|
|
72
82
|
|
|
73
83
|
## Tests (the `testing` skill owns the strategy)
|
|
74
84
|
- Many **unit** and **widget** tests (`flutter_test`), and enough **integration** tests (`integration_test/`, the
|
|
75
85
|
binding initialised) for the important flows. Tests use fakes of the repositories and services (the guide's
|
|
76
86
|
recommendation), which is why those sit behind abstract classes.
|
|
77
|
-
- **Golden tests** (`matchesGoldenFile`) only where pixels are the contract
|
|
78
|
-
|
|
79
|
-
|
|
87
|
+
- **Golden tests** (`matchesGoldenFile`) only where pixels are the contract, fonts loaded first. **They are generated
|
|
88
|
+
and compared in one place, a pinned container in CI**; `flutter test --update-goldens` is not run on a developer's
|
|
89
|
+
machine, whose rendering differs.
|
|
90
|
+
- **Every new screen reached from the app's navigation gets a widget test at one width of each window class** the
|
|
91
|
+
app supports (for example 320, 360, 700 and 1280 dp), **and a test that crosses a class boundary and finds the
|
|
92
|
+
selection and the typed input still there.** Plus the a11y guideline test.
|
|
93
|
+
|
|
94
|
+
## If the app holds sensitive data (apply only then)
|
|
95
|
+
Financial, health or identity data, or anything the user would not want read off a lost phone or a backup:
|
|
96
|
+
- **The local database is encrypted**, and its **key lives in the platform's secure store** (Keychain, Keystore),
|
|
97
|
+
never in the database's own directory, in preferences or in the code.
|
|
98
|
+
- **The database and the key's files are kept out of the operating system's backup and device transfer**, on both
|
|
99
|
+
platforms.
|
|
100
|
+
- **Every network call goes through a transport bound to its own allowed host or hosts.** A call that leaves that
|
|
101
|
+
list is refused: it throws before anything is sent, and a test makes such a call and expects the throw. One
|
|
102
|
+
transport for the whole app or one per service are both this rule; a call made outside any transport is not.
|
|
103
|
+
- **In an app that shows ads, no ad is drawn in a window where a sensitive screen is visible** — in a two-pane
|
|
104
|
+
layout that is the whole window, not the pane.
|
|
105
|
+
How, and what was checked: **`references/sensitive-data.md`**.
|
|
80
106
|
|
|
81
107
|
## Performance and release
|
|
82
108
|
- **Measure in profile mode on a real device** (`flutter run --profile`); debug mode is not indicative and profile
|
|
83
109
|
mode does not run on an emulator or simulator. Frame budget: about 16 ms at 60 Hz, 8 ms at 120 Hz.
|
|
84
110
|
- Release: `--obfuscate --split-debug-info=<dir>` where the target supports it, the symbols kept for `flutter
|
|
85
111
|
symbolize`. Obfuscation is not encryption: **no secret ships in the app**. An app commits its `pubspec.lock`.
|
|
112
|
+
- **A feature closed by a build flag stays closed in the store build:** the store build fails when it is handed
|
|
113
|
+
that flag, on Android and on iOS. Detail: **`references/platform-async-release.md`**.
|
|
86
114
|
- Logging: `debugPrint` prints in release mode too unless it sits behind a debug check or an assert; log through the
|
|
87
115
|
project's logger (the `observability` skill).
|
|
88
116
|
|
|
@@ -90,5 +118,7 @@ below was checked against Flutter's own documentation (docs.flutter.dev, dart.de
|
|
|
90
118
|
- `dart format --set-exit-if-changed .` exits 0 · `flutter analyze` clean (0 issues, the project's lint set) ·
|
|
91
119
|
`flutter test` green — one run after the last edit, reported with its exit code and counts.
|
|
92
120
|
- No widget holds business logic; no device-type or orientation check decides a layout.
|
|
93
|
-
- The a11y guideline test passes on every new screen; the
|
|
121
|
+
- The a11y guideline test passes on every new screen; each window class the app supports is tested at one width,
|
|
122
|
+
and state survives a change of class.
|
|
123
|
+
- No user-facing text inline; the domain layer (if any) imports no `package:flutter`.
|
|
94
124
|
- Works across the project's target platform matrix (Android / iOS / web / desktop as declared).
|
|
@@ -4,7 +4,8 @@ Checked against https://docs.flutter.dev/ui/adaptive-responsive/general,
|
|
|
4
4
|
https://docs.flutter.dev/ui/adaptive-responsive/best-practices,
|
|
5
5
|
https://docs.flutter.dev/ui/accessibility-and-internationalization/accessibility,
|
|
6
6
|
https://docs.flutter.dev/ui/accessibility/accessibility-testing and the Material window size classes as Android's
|
|
7
|
-
documentation states them (2026-10-02)
|
|
7
|
+
documentation states them (2026-10-02); the foldable part against
|
|
8
|
+
https://api.flutter.dev/flutter/widgets/MediaQueryData/displayFeatures.html (2026-10-07).
|
|
8
9
|
|
|
9
10
|
## The three steps
|
|
10
11
|
1. **Abstract** the widgets that change with the space, and the data they share.
|
|
@@ -32,6 +33,23 @@ documentation states them (2026-10-02).
|
|
|
32
33
|
- A list that keeps its layout across a rotation keeps its scroll position: `PageStorageKey`.
|
|
33
34
|
- Mouse, trackpad and keyboard shortcuts work where the app runs on desktop or a large screen.
|
|
34
35
|
|
|
36
|
+
## List and detail
|
|
37
|
+
- Expanded and wider: the list and the detail are two panes of one screen. Compact: the list is the screen and the
|
|
38
|
+
detail is pushed over it. Medium is the design system's call.
|
|
39
|
+
- **The selection and any typed input are kept across a change of window class.** Hold them above the two layouts
|
|
40
|
+
(the view model, or state owned by the widget that chooses the layout), so that going from two panes to one shows
|
|
41
|
+
the detail that was selected, with the form as it was, and going back does not clear it. A `GlobalKey` or a
|
|
42
|
+
`PageStorageKey` keeps a subtree's state only while its place in the tree allows it; state that must survive is
|
|
43
|
+
not left to that.
|
|
44
|
+
|
|
45
|
+
## A foldable
|
|
46
|
+
- `MediaQuery.displayFeatures` (and `MediaQueryData.displayFeatures`) lists the areas of the display that hardware
|
|
47
|
+
obstructs. The documentation says it is populated on Android only. A **hinge** and a **cutout** obstruct the
|
|
48
|
+
display; a **fold** is a crease and does not. A hinge and a fold carry a state that tells the device's posture.
|
|
49
|
+
- When a hinge separates the window into two areas, **the panes split at the hinge**: one pane on each side, and
|
|
50
|
+
nothing — no text, no control, no divider the user must touch — laid out on the hinge's bounds.
|
|
51
|
+
- A window with no such feature, or with a fold that obstructs nothing, is laid out by its width alone.
|
|
52
|
+
|
|
35
53
|
## Accessibility, tested
|
|
36
54
|
Rules (the `a11y` skill holds them for every stack): tap targets at least 48×48 on Android and 44×44 on iOS; a
|
|
37
55
|
label on every tappable; text contrast at least 4.5:1 (3:1 for large text, 18 pt and above); the UI legible and
|
|
@@ -51,5 +69,13 @@ testWidgets('meets the accessibility guidelines', (tester) async {
|
|
|
51
69
|
});
|
|
52
70
|
```
|
|
53
71
|
|
|
54
|
-
Run it for every new screen
|
|
55
|
-
|
|
72
|
+
Run it for every new screen.
|
|
73
|
+
|
|
74
|
+
## Widths, tested
|
|
75
|
+
- Every new screen reached from the app's navigation has a widget test at **one width of each window class the app
|
|
76
|
+
supports**, for example 320 and 360 dp (compact, a small and a common phone), 700 dp (medium) and 1280 dp (large).
|
|
77
|
+
Set the size with `tester.view.physicalSize` and `devicePixelRatio`, and reset both afterwards.
|
|
78
|
+
- One test **crosses a class boundary**: select an item and type into a form at one width, change the width to
|
|
79
|
+
another class, and expect the same selection and the same input.
|
|
80
|
+
- The narrowest and the widest window alone are not enough: a layout can be right at both ends and wrong in a class
|
|
81
|
+
between them.
|
|
@@ -33,6 +33,15 @@ https://dart.dev/tools/dart-format and https://dart.dev/tools/pub/private-files
|
|
|
33
33
|
the browser's forward button does not work with them.
|
|
34
34
|
|
|
35
35
|
## Text and locales
|
|
36
|
+
- No user-facing text is written inline. An app in more than one language uses `gen-l10n` (below). An app in one
|
|
37
|
+
language may keep every text in one central file of constants instead: one place to read, review and later move
|
|
38
|
+
to ARB files.
|
|
39
|
+
- `String.toUpperCase()` "uses the language independent Unicode mapping and thus only works in some languages"
|
|
40
|
+
(api.dart.dev, 2026-10-07), and `toLowerCase()` likewise: under them Turkish `i` gives `I` and `I` gives `i`,
|
|
41
|
+
where the language has `İ` and `ı`. Text is cased by one helper of the project that takes the locale and handles
|
|
42
|
+
the letters its languages need; nothing calls the two methods on user-facing text directly.
|
|
43
|
+
- `FontFeature.tabularFigures()` (`TextStyle.fontFeatures`) asks the font for figures of one width, so amounts in a
|
|
44
|
+
column line up and a changing number does not shift the text beside it. It does nothing in a font without them.
|
|
36
45
|
- `flutter_localizations` and `intl` in `pubspec.yaml`, `generate: true` under `flutter:`, an `l10n.yaml` with
|
|
37
46
|
`arb-dir`, `template-arb-file`, `output-localization-file`. `flutter gen-l10n` (or `flutter pub get` / `run`)
|
|
38
47
|
generates `AppLocalizations`.
|
|
@@ -47,6 +56,9 @@ https://dart.dev/tools/dart-format and https://dart.dev/tools/pub/private-files
|
|
|
47
56
|
- Golden files: `matchesGoldenFile('goldens/x.png')`, refreshed with `flutter test --update-goldens`. The default
|
|
48
57
|
test font (Ahem) draws boxes; custom fonts render differently across platforms and Flutter versions, so goldens are
|
|
49
58
|
produced and compared on one platform, with fonts loaded first (`flutter_test_config.dart`).
|
|
59
|
+
- One place, in practice: a container image pinned by digest, with the Flutter version pinned, in CI. A golden that
|
|
60
|
+
changed is regenerated there (a CI job that uploads the new files, or the same image run by hand) and reviewed as
|
|
61
|
+
an image diff. `--update-goldens` on a developer's own machine produces files CI then fails.
|
|
50
62
|
|
|
51
63
|
## Release
|
|
52
64
|
- `flutter build <target> --obfuscate --split-debug-info=<dir>` on the targets that support it (Android, iOS, macOS,
|
|
@@ -57,3 +69,32 @@ https://dart.dev/tools/dart-format and https://dart.dev/tools/pub/private-files
|
|
|
57
69
|
- An application commits `pubspec.lock` (a library package does not): transitive upgrades become visible changes.
|
|
58
70
|
- `dart format` rewrites files by default; in CI and in the DoD use `dart format --set-exit-if-changed .` (exit 1
|
|
59
71
|
when anything would change). The line width is the project's `formatter` setting in `analysis_options.yaml`.
|
|
72
|
+
|
|
73
|
+
## A feature closed by a build flag
|
|
74
|
+
A feature that must not reach the store yet (it waits for a review, a consent text, a legal check) is closed in code
|
|
75
|
+
by a compile-time flag: `const bool.fromEnvironment('NAME')`, set with `--dart-define=NAME=true`. The compiler drops
|
|
76
|
+
the closed branch. That keeps it out of an ordinary build; it does not keep it out of a store build that someone
|
|
77
|
+
runs with the flag. **The store build itself refuses the flag**, in the build system of each platform, before
|
|
78
|
+
anything is compiled:
|
|
79
|
+
|
|
80
|
+
- **Android (Gradle):** a task the release bundle depends on reads the project properties the Flutter Gradle plugin
|
|
81
|
+
is handed — `dart-defines` and `extra-front-end-options` — and fails the build when a closed flag is among them.
|
|
82
|
+
- **iOS (Xcode):** a Run Script build phase, placed first in the app target, reads the build settings `DART_DEFINES`
|
|
83
|
+
and `EXTRA_FRONT_END_OPTIONS` and fails a release archive the same way (a line that starts with `error:` shows
|
|
84
|
+
among Xcode's build errors).
|
|
85
|
+
|
|
86
|
+
The ways round a check that only looks for `NAME=true`, each of which the check closes:
|
|
87
|
+
- **The defines are encoded.** They arrive as one value: entries separated by commas, each the base64 of
|
|
88
|
+
`NAME=value`. The check decodes every entry; an entry it cannot decode fails the build.
|
|
89
|
+
- **Any value sets it.** The flag's name fails the build whatever follows the `=`, and so does the name anywhere
|
|
90
|
+
inside another entry.
|
|
91
|
+
- **The front end's own options.** A define can be passed to the Dart front end directly (`-DNAME=…`), and other
|
|
92
|
+
options replace what is compiled. A store build is given no front-end options at all; one that is, fails.
|
|
93
|
+
- **Only the store artifact is checked** — the release app bundle and the release archive. A debug or profile
|
|
94
|
+
build, and a release build for one's own device, may carry the flag.
|
|
95
|
+
- A test in the project pins the check: it runs it with the flag, with the flag encoded, with a front-end option,
|
|
96
|
+
and with none, and expects three failures and one pass.
|
|
97
|
+
|
|
98
|
+
Not from Flutter's documentation: the property and setting names above are what the Flutter tool hands to Gradle
|
|
99
|
+
and Xcode, read from a project that does this. They are not a documented contract, so the project's test also fails
|
|
100
|
+
when a Flutter upgrade renames them, instead of the check passing because it reads nothing.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# An app that holds sensitive data (Flutter)
|
|
2
|
+
|
|
3
|
+
Apply only when the app stores data its user would not want read off a lost phone, a backup or the network:
|
|
4
|
+
financial, health, identity. Checked against https://pub.dev/packages/sqlite3 and its hook documentation,
|
|
5
|
+
https://pub.dev/packages/sqlcipher_flutter_libs and https://developer.android.com/identity/data/autobackup
|
|
6
|
+
(2026-10-07). The iOS part is from Apple's API names and was **not checked against a fetched page**.
|
|
7
|
+
|
|
8
|
+
## The database is encrypted
|
|
9
|
+
- The project's database package decides how; the rule is that the file on disk is ciphertext.
|
|
10
|
+
- With `package:sqlite3` 3.x the SQLite build is chosen by a build hook, in the `pubspec.yaml` at the workspace root:
|
|
11
|
+
|
|
12
|
+
```yaml
|
|
13
|
+
hooks:
|
|
14
|
+
user_defines:
|
|
15
|
+
sqlite3:
|
|
16
|
+
source: sqlcipher # or sqlite3mc; the default is sqlite3, which does not encrypt
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- `sqlcipher_flutter_libs` belongs to version 2.x of `package:sqlite3`; its own page says "Not used anymore, update
|
|
20
|
+
to version 3.x of package:sqlite3 instead". Do not add it to a project on 3.x.
|
|
21
|
+
- A test opens the database file without the key and expects the open to fail: an encryption that was configured
|
|
22
|
+
and silently not applied looks the same from the app.
|
|
23
|
+
|
|
24
|
+
## The key is in the platform's secure store
|
|
25
|
+
- Generated on the device, kept in the Keychain (iOS) or behind the Keystore (Android) through the project's
|
|
26
|
+
secure-storage package. Not in the database's directory, not in shared preferences, not in the code or its assets.
|
|
27
|
+
- A key that is lost makes the data unreadable. Decide and write down what the app does then (an `adr`).
|
|
28
|
+
|
|
29
|
+
## Out of backup and device transfer
|
|
30
|
+
The database and the secure store's own files are excluded on both platforms; a restored copy without its key is
|
|
31
|
+
unreadable at best, and a copy with its key is the data on another device.
|
|
32
|
+
|
|
33
|
+
- **Android.** `android:allowBackup="false"` on `<application>`. For an app that targets Android 12 (API 31) or
|
|
34
|
+
higher that turns off cloud backup but, in the documentation's words, "doesn't disable device-to-device transfers"
|
|
35
|
+
on some manufacturers' devices. So also `android:dataExtractionRules` pointing at an XML file whose
|
|
36
|
+
`<cloud-backup>` and `<device-transfer>` sections each exclude the database and the secure-storage files. Devices
|
|
37
|
+
on Android 11 or lower read `android:fullBackupContent` instead.
|
|
38
|
+
- **iOS.** The files carry the resource value `isExcludedFromBackup` (`NSURLIsExcludedFromBackupKey`), set from
|
|
39
|
+
native code or a plugin after the file exists. Set it again when the file is created anew.
|
|
40
|
+
- A test (or a check in CI) reads the manifest and the rules file and fails when either attribute is missing.
|
|
41
|
+
|
|
42
|
+
## Every network call through a transport bound to its allowed hosts
|
|
43
|
+
- A transport is the one object a service's requests leave through. Each holds the host or hosts **it** may talk
|
|
44
|
+
to and **refuses any other**: it throws before the request is sent. An app with one backend has one transport;
|
|
45
|
+
an app that talks to several services gives each its own, with its own list, so a transport for one service
|
|
46
|
+
cannot reach another's host.
|
|
47
|
+
- A test asks each transport for a host that is not on its list and expects the throw. An architecture test fails
|
|
48
|
+
on a network call made outside a transport (a client constructed or a socket opened anywhere else). In a debug
|
|
49
|
+
run the throw is a crash someone sees; it is never swallowed.
|
|
50
|
+
|
|
51
|
+
## Ads
|
|
52
|
+
In an app that shows ads, **no ad is drawn in a window where a sensitive screen is visible.** In a two-pane layout
|
|
53
|
+
the detail pane may be the sensitive one while the list is not: the rule is about the window, so the list's ad goes
|
|
54
|
+
too. A widget test at an expanded width expects no ad widget while the sensitive detail is shown.
|
package/package.json
CHANGED