Skip to content

Add com.codename1.security: biometric auth + secure storage#4987

Open
shai-almog wants to merge 11 commits into
masterfrom
feat/biometrics-core
Open

Add com.codename1.security: biometric auth + secure storage#4987
shai-almog wants to merge 11 commits into
masterfrom
feat/biometrics-core

Conversation

@shai-almog
Copy link
Copy Markdown
Collaborator

Summary

  • Promotes biometric authentication from the FingerprintScanner cn1lib into core under com.codename1.security, so Touch ID / Face ID / Android BiometricPrompt are first-class APIs alongside Display.capturePhoto, LocationManager, etc.
  • Public surface designed to match Flutter's local_auth: typed BiometricType list (FINGERPRINT, FACE, IRIS, STRONG, WEAK), typed BiometricError codes (NOT_AVAILABLE, NOT_ENROLLED, LOCKED_OUT, PERMANENTLY_LOCKED_OUT, USER_CANCELED, KEY_REVOKED, ...), fluent AuthenticationOptions builder, and a working stopAuthentication() cancel.
  • SecureStorage sibling API gives biometric-gated keychain storage (iOS Security framework, Android AES/AndroidKeyStore with the cn1lib's hard-won workarounds for Samsung 8.0.0 and the API 33 carve-out preserved).
  • Build plugin auto-injects LocalAuthentication.framework on iOS and USE_BIOMETRIC / USE_FINGERPRINT permissions on Android, so apps don't need build-hint surgery.
  • The existing FingerprintScanner cn1lib stays untouched; projects that depend on it keep building unchanged.

API

Biometrics b = Biometrics.getInstance();
if (b.canAuthenticate()) {
    b.authenticate("Unlock your wallet").onResult((ok, err) -> {
        if (err != null) {
            BiometricError code = ((BiometricException) err).getError();
            // branch on the typed code
        } else {
            // authenticated
        }
    });
}

// Secure storage round-trip
SecureStorage.getInstance().set("Save token", "user@example.com", token);
SecureStorage.getInstance().get("Unlock token", "user@example.com")
    .onResult((value, err) -> { /* ... */ });

Port implementations

  • iOS (IOSBiometrics + IOSSecureStorage): wraps LAContext.evaluatePolicy(...) and SecItemAdd/SecItemCopyMatching/SecItemDelete. 9 new natives added to IOSNative.java / IOSNative.m. Non-ARC-safe (matches the iOS port's CLANG_ENABLE_OBJC_ARC=NO config). stopAuthentication() calls [LAContext invalidate] and resolves the in-flight AsyncResource with USER_CANCELED — the cn1lib previously logged "not implemented on iOS" here.
  • Android (AndroidBiometrics + AndroidSecureStorage): keeps the cn1lib's dual path — FingerprintManager on API 23-28, BiometricPrompt on API 29+. The API 29+ paths go through a small reflection adapter (BiometricsApi29) because the cn1-binaries android.jar predates API 28; runtime resolution works fine on devices that support it. Preserves the cn1lib's non-obvious workarounds (Samsung 8.0.0 cipher-init quirk, setUserAuthenticationRequired API 33 carve-out per FingerprintScanner Simulator - native j2me theme is not taken from project #8).
  • JavaSE simulator (JavaSEBiometrics + JavaSESecureStorage): adds a Simulate -> Biometric Simulation submenu next to the existing Location Simulation and Push Simulation. Lets the developer toggle hardware availability, per-modality enrollment (Face / Touch / Iris), and the outcome of the next authenticate() call. State persists across simulator restarts via java.util.prefs. Secure-storage round-trip works against Preferences so apps can be exercised end-to-end without a device.

Test plan

  • Core module builds clean under -source 1.5 -target 1.5 (mvn -pl core install -Plocal-dev-javase -DskipTests)
  • JavaSE port builds and packages JavaSEBiometrics / JavaSESecureStorage
  • Android port builds (reflection adapter compiles against the cn1-binaries android.jar)
  • codenameone-maven-plugin builds with the auto-injection edits to IPhoneBuilder and AndroidGradleBuilder
  • Static smoke test exercising BiometricType, BiometricError, AuthenticationOptions, BiometricException against the built core jar
  • Run a sample in the JavaSE simulator: toggle Simulate -> Biometric Simulation -> Hardware Available + Face ID Enrolled, verify Biometrics.getAvailableBiometrics() returns [FACE] and that cycling the "Next outcome" radio produces the matching success/error in the test app
  • Build an Android sample app for an API 23 fingerprint device (legacy path) and an API 29+ device (BiometricPrompt path); verify BiometricError.LOCKED_OUT after repeated failures and that stopAuthentication() dismisses the prompt
  • Build an iOS sample app for a Face ID iPhone and a Touch ID iPad; verify [LAContext invalidate] cancellation path and the SecureStorage round-trip
  • SecureStorage invalidation: write a value, enroll a new biometric, verify the next get(...) fails with BiometricError.KEY_REVOKED on both Android and iOS
  • Build a sample app that does not declare ios.add_libs or any biometric build hint; confirm the generated Xcode project links LocalAuthentication.framework and the generated AndroidManifest.xml contains <uses-permission android:name="android.permission.USE_BIOMETRIC" />
  • Coexistence: confirm an app depending on the original FingerprintScanner cn1lib can also use the new Biometrics API in the same module without symbol conflicts

🤖 Generated with Claude Code

Promotes biometric authentication (Touch ID, Face ID, Android
BiometricPrompt) from the FingerprintScanner cn1lib into core so it is
available alongside Location, Capture, and the other first-class device
APIs. Public surface mirrors Flutter's local_auth: typed BiometricType
list, typed BiometricError codes, AuthenticationOptions builder, and a
stopAuthentication() cancel.

iOS port wraps LocalAuthentication.framework + Security.framework
(SecItemAdd / SecItemCopyMatching / SecItemDelete). Android port keeps
the cn1lib's dual path -- FingerprintManager on API 23-28 and
BiometricPrompt on API 29+; the BiometricPrompt + BiometricManager calls
go through a reflection adapter (BiometricsApi29) because the
cn1-binaries android.jar predates API 28. JavaSE port adds a
Simulate -> Biometric Simulation submenu (Available toggle, per-modality
enrollment, configurable next-call outcome) so apps can be exercised in
the simulator. The Maven plugin always links LocalAuthentication.framework
on iOS and injects USE_BIOMETRIC / USE_FINGERPRINT permissions on Android
so apps don't need build hint surgery.

The existing FingerprintScanner cn1lib continues to work unchanged for
projects that depend on it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread Ports/Android/src/com/codename1/impl/android/AndroidBiometrics.java Fixed
@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 19, 2026

Compared 19 screenshots: 19 matched.
✅ JavaScript-port screenshot tests passed.

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 19, 2026

Compared 11 screenshots: 11 matched.
✅ JavaSE simulator integration screenshots matched stored baselines.

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 19, 2026

Compared 110 screenshots: 110 matched.

Native Android coverage

  • 📊 Line coverage: 11.67% (6556/56180 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 9.44% (32915/348817), branch 4.07% (1348/33132), complexity 5.14% (1631/31754), method 8.96% (1330/14836), class 14.90% (301/2020)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6327 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.ClassReader – 0.00% (0/1519 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1148 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.MethodWriter – 0.00% (0/923 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/730 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/623 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.Frame – 0.00% (0/564 lines covered)
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysJvmKt – 0.00% (0/495 lines covered)
      • kotlinx.coroutines.kotlinx.coroutines.JobSupport – 0.00% (0/423 lines covered)

✅ Native Android screenshot tests passed.

Native Android coverage

  • 📊 Line coverage: 11.67% (6556/56180 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 9.44% (32915/348817), branch 4.07% (1348/33132), complexity 5.14% (1631/31754), method 8.96% (1330/14836), class 14.90% (301/2020)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6327 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.ClassReader – 0.00% (0/1519 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1148 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.MethodWriter – 0.00% (0/923 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/730 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/623 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.Frame – 0.00% (0/564 lines covered)
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysJvmKt – 0.00% (0/495 lines covered)
      • kotlinx.coroutines.kotlinx.coroutines.JobSupport – 0.00% (0/423 lines covered)

Benchmark Results

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 1238.000 ms
Base64 CN1 encode 188.000 ms
Base64 encode ratio (CN1/native) 0.152x (84.8% faster)
Base64 native decode 795.000 ms
Base64 CN1 decode 141.000 ms
Base64 decode ratio (CN1/native) 0.177x (82.3% faster)
Image encode benchmark status skipped (SIMD unsupported)

shai-almog and others added 3 commits May 20, 2026 03:42
CI surfaced two issues against the initial commit:

1. CodenameOne/ and Ports/CLDC11/ run a validator that rejects classic /**
   Javadoc -- the codebase has standardised on /// markdown comments. Convert
   all 8 files in com.codename1.security to that style and translate
   {@link X} / {@code Y} to [X] / `Y` markdown.

2. IOSNative.m calls com_codename1_impl_ios_IOSBiometrics_* and
   com_codename1_impl_ios_IOSSecureStorage_* callbacks but did not #include
   the ParparVM-generated headers, so clang complained about implicit
   function declarations and the iOS native build, build-ios, build-ios-metal
   and packaging jobs all failed identically. Add the two #includes alongside
   the existing com_codename1_impl_ios_IOSImplementation.h include.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
JDK 17 (and JDK 8) javac on CI defaults file.encoding to US-ASCII;
file.encoding=UTF-8 is only the JDK 18+ default per JEP 400. That made
the em-dash in AndroidBiometrics.java:64 ('-- values are stable per
AOSP') fatal with three "unmappable character" errors and broke the
Ant compile step. Replace with ASCII double-hyphen to match the
ASCII-only invariant documented in CLAUDE memory.

Also remove the unreachable UnrecoverableEntryException catch in
AndroidSecureStorage.getSecretKey -- keyStore.getKey() only declares
UnrecoverableKeyException, not its parent, so the second clause is
dead code (javac warning) and the import is unused.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
build-test (8) flagged LI_LAZY_INIT_STATIC on the lazy `fallback`
field in both Biometrics.getInstance() and SecureStorage.getInstance()
-- the if-null-then-assign-then-return pattern isn't thread-safe and
the workflow treats this rule as forbidden. The fallback stubs are
cheap immutable objects, so promote them to `private static final`
eager fields and return them with a ternary.

Also drop the explicit no-arg constructors on AuthenticationOptions,
StubBiometrics and StubSecureStorage (PMD UnnecessaryConstructor) --
the compiler-generated default has the correct visibility in each
case (public, package-private, package-private).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 20, 2026

Compared 108 screenshots: 108 matched.
✅ Native iOS Metal screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 223 seconds

Build and Run Timing

Metric Duration
Simulator Boot 70000 ms
Simulator Boot (Run) 1000 ms
App Install 23000 ms
App Launch 13000 ms
Test Execution 310000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 1014.000 ms
Base64 CN1 encode 2079.000 ms
Base64 encode ratio (CN1/native) 2.050x (105.0% slower)
Base64 native decode 467.000 ms
Base64 CN1 decode 1162.000 ms
Base64 decode ratio (CN1/native) 2.488x (148.8% slower)
Base64 SIMD encode 613.000 ms
Base64 encode ratio (SIMD/native) 0.605x (39.5% faster)
Base64 encode ratio (SIMD/CN1) 0.295x (70.5% faster)
Base64 SIMD decode 934.000 ms
Base64 decode ratio (SIMD/native) 2.000x (100.0% slower)
Base64 decode ratio (SIMD/CN1) 0.804x (19.6% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 71.000 ms
Image createMask (SIMD on) 15.000 ms
Image createMask ratio (SIMD on/off) 0.211x (78.9% faster)
Image applyMask (SIMD off) 191.000 ms
Image applyMask (SIMD on) 72.000 ms
Image applyMask ratio (SIMD on/off) 0.377x (62.3% faster)
Image modifyAlpha (SIMD off) 448.000 ms
Image modifyAlpha (SIMD on) 215.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.480x (52.0% faster)
Image modifyAlpha removeColor (SIMD off) 284.000 ms
Image modifyAlpha removeColor (SIMD on) 178.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.627x (37.3% faster)
Image PNG encode (SIMD off) 1422.000 ms
Image PNG encode (SIMD on) 1120.000 ms
Image PNG encode ratio (SIMD on/off) 0.788x (21.2% faster)
Image JPEG encode 589.000 ms

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 20, 2026

Compared 110 screenshots: 110 matched.
✅ Native iOS screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 224 seconds

Build and Run Timing

Metric Duration
Simulator Boot 63000 ms
Simulator Boot (Run) 1000 ms
App Install 12000 ms
App Launch 34000 ms
Test Execution 306000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 628.000 ms
Base64 CN1 encode 1845.000 ms
Base64 encode ratio (CN1/native) 2.938x (193.8% slower)
Base64 native decode 284.000 ms
Base64 CN1 decode 1264.000 ms
Base64 decode ratio (CN1/native) 4.451x (345.1% slower)
Base64 SIMD encode 539.000 ms
Base64 encode ratio (SIMD/native) 0.858x (14.2% faster)
Base64 encode ratio (SIMD/CN1) 0.292x (70.8% faster)
Base64 SIMD decode 584.000 ms
Base64 decode ratio (SIMD/native) 2.056x (105.6% slower)
Base64 decode ratio (SIMD/CN1) 0.462x (53.8% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 66.000 ms
Image createMask (SIMD on) 9.000 ms
Image createMask ratio (SIMD on/off) 0.136x (86.4% faster)
Image applyMask (SIMD off) 126.000 ms
Image applyMask (SIMD on) 62.000 ms
Image applyMask ratio (SIMD on/off) 0.492x (50.8% faster)
Image modifyAlpha (SIMD off) 138.000 ms
Image modifyAlpha (SIMD on) 56.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.406x (59.4% faster)
Image modifyAlpha removeColor (SIMD off) 326.000 ms
Image modifyAlpha removeColor (SIMD on) 129.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.396x (60.4% faster)
Image PNG encode (SIMD off) 1144.000 ms
Image PNG encode (SIMD on) 794.000 ms
Image PNG encode ratio (SIMD on/off) 0.694x (30.6% faster)
Image JPEG encode 1711.000 ms

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 20, 2026

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs [Report archive]
    • ByteCodeTranslator: 0 findings (no issues)
    • android: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 0 findings (no issues)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

shai-almog and others added 3 commits May 20, 2026 19:01
…ject

CodeQL alert "Insecure local authentication" (java/android/insecure-
local-authentication, alert #108) correctly flagged that
AndroidBiometrics.authenticate() granted access on the bare
onAuthenticationSucceeded callback. Without a CryptoObject the success
path can be reached via runtime hooking tools (Frida) without the user
ever actually authenticating.

Add a single-use AES probe key to the AndroidKeyStore with
setUserAuthenticationRequired(true) and (API 24+)
setInvalidatedByBiometricEnrollment(true), initialise a Cipher under
it, and pass that Cipher to BiometricPrompt / FingerprintManager as
the CryptoObject. The success callbacks then run
authedCipher.doFinal(PROBE_PLAINTEXT); a real biometric unlocks the
cipher and the doFinal returns, a spoofed callback fails because the
Keystore refuses the operation.

Probe key is recreated on KeyPermanentlyInvalidatedException (which
happens when the user enrols a new biometric -- the security property
CodeQL is asking us to enforce). The key is independent of the
AndroidSecureStorage per-account keys, so SecureStorage entries are
not affected when the probe is rotated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tighten the legacy FingerprintManager success path so CodeQL's
dataflow analyser can see unambiguously that the cipher used for
doFinal() comes from the AuthenticationResult.getCryptoObject() that
the OS returned, not the local probe Cipher variable. If the result
is missing a CryptoObject (shouldn't happen, but a spoofed callback
might supply null) we now fail with AUTHENTICATION_FAILED rather than
falling back to the local cipher reference.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round of fixes from the PR review:

1. **Non-abstract base classes.** Biometrics and SecureStorage are now
   concrete with no-op default implementations -- StubBiometrics and
   StubSecureStorage are deleted. The default class is returned as the
   fallback for unsupported ports, so app code never needs a null check
   or a platform if. Reduces class count and eliminates two
   public-package types that were really impl details.

2. **Six SIC_INNER_SHOULD_BE_STATIC_ANON warnings fixed** in
   AndroidBiometrics + AndroidSecureStorage by converting the
   self-contained Runnables / CipherWork lambdas to actual Java 8
   lambdas (the Android port already compiles -source 1.8).

3. **SpotBugs forbidden_rules now applied across every project**, not
   only core-unittests. A regression in android/ios spotbugs now fails
   CI exactly the way a regression in core does.

4. **Conditional injection.** IPhoneBuilder and AndroidGradleBuilder
   only inject LocalAuthentication.framework / USE_BIOMETRIC /
   USE_FINGERPRINT when the bytecode scanner observes any
   com.codename1.security class. Apps that never touch the API pay
   nothing. Mirrored in BuildDaemon (legacy build server).

5. **JavaSEBiometrics installBuildHints** -- first time the API is
   touched in the simulator, set ios.NSFaceIDUsageDescription on the
   project (placeholder text the developer should overwrite). Mirrors
   the historical FingerprintScanner cn1lib pattern.

6. **/// javadoc on every public getter / setter / enum constant**
   under com.codename1.security; STRONG/WEAK/IRIS now explained;
   iOS/Android/JavaSE/fallback behaviour spelled out throughout.

7. **Developer guide chapter** docs/developer-guide/Biometric-
   Authentication.asciidoc covering quick start, platform support
   matrix, build hints, prompt configuration, typed errors, simulator
   workflow, and the Keystore-bound success-verification security note.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 20, 2026

Developer Guide build artifacts are available for download from this workflow run:

Developer Guide quality checks:

  • AsciiDoc linter: No issues found (report)
  • Vale: No alerts found (report)
  • Image references: No unused images detected (report)

@github-actions
Copy link
Copy Markdown
Contributor

Cloudflare Preview

shai-almog and others added 4 commits May 20, 2026 22:58
build-test (8) runs the Android port Ant build with -source 1.6 (the
Maven build uses 1.8, so my local checks missed this). Revert the
Java-8 lambdas added in the previous commit to named private static
inner classes -- same SpotBugs SIC_INNER_SHOULD_BE_STATIC_ANON
outcome, Java 1.6 compatible.

Also clean up the new docs/developer-guide/Biometric-Authentication
asciidoc against the project's Vale style ruleset: 8 Microsoft.
Contractions findings (is not / will not / cannot / does not / was not
must be contractions), one Microsoft.Auto (don't hyphenate
"auto-sets"), one Microsoft.Adverbs ("silently" removed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three editorial cleanups requested in PR review:

1. Drop the comparison to Flutter local_auth in Biometrics.java javadoc
   and in the developer guide intro. The API stands on its own.

2. Avoid asciidoc em-dashes (--) throughout the new chapter. Replace
   with colons in headings and definition lists, semicolons or full
   sentences in prose. The four-dash code-block delimiters (----) are
   intentional asciidoc syntax and remain.

3. Drop the "legacy build daemon" wording. The build daemon is the
   build server and is not legacy. Reword to "the Codename One Maven
   plugin and the build daemon both automatically inject ...".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous round of feedback flagged that the build-hint injection
wasn't visible in JavaSEPort -- it was buried in JavaSEBiometrics'
public methods. Move the logic to JavaSEPort.installBiometricsBuildHints
IfNeeded() and call it from getBiometrics() and getSecureStorage().

The semantics are unchanged: the first time the application touches
either API in the simulator we detect whether ios.NSFaceIDUsageDescription
is set on the project; if not, write a placeholder so the next iOS
device build doesn't crash. The developer should overwrite the
placeholder text with their app-specific localised reason before
shipping (Apple rejects builds with the placeholder default).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vale's Microsoft.HeadingColons rule flags lowercase first words after
a heading colon. Capitalise "Authenticate" and "Secure storage" in the
two Quick start subsection titles. Fallout from the em-dash removal in
the previous commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants