ReviewOS

also looking at this

craft-native/craft

fix(sdk): the event map typed one event that never fires and missed six that do

#133
Closed glennmichael123 wants to merge fix/sdk-event-map into docs/ios-gate-claims-expired
18 files +562 -163

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

packages/zig/src/bridge_mobile_bgtasks.zigmodified+7-4
Changes to packages/zig/src/bridge_mobile_bgtasks.zig
@@ -103,10 +103,13 @@
103103//! posting); a field that is **present with the wrong type** is refused, never
104104//! coerced.
105105//!
106//! **The `config.enableBackgroundTasks` gate has no Zig mirror.** It appears
107//! nowhere in `packages/zig/src`, so the three actions here are served
108//! unconditionally — the securestore and notifcancel modules document the same
109//! choice. In an app that left the gate at its default `false` this changes a
106//! **The `config.enableBackgroundTasks` gate is read, not skipped.**
107//! `ios_config.gateFor` maps all three actions to `.background_tasks`, so
108//! `ios_dispatch.offerToModules` answers `CAPABILITY_DISABLED` before this
109//! module is asked. This paragraph used to say the flag "appears nowhere in
110//! `packages/zig/src`" and that the actions were served unconditionally — true
111//! when written, false since `ios_config.zig`, and repeated in eight other
112//! module headers that were equally true and are equally false. In an app that left the gate at its default `false` this changes a
110113//! hang into an answer: `schedule` now reaches the scheduler and reports
111114//! whatever it says (a rejection, until the plist gains
112115//! `BGTaskSchedulerPermittedIdentifiers`), and the cancels report the no-op
packages/zig/src/bridge_mobile_calendar.zigmodified+11-4
Changes to packages/zig/src/bridge_mobile_calendar.zig
@@ -139,10 +139,17 @@
139139//! `deleteCalendarEvent` — which requests nothing (3350) — the key is only
140140//! evidence of `config.enableCalendar`, and it is checked for that reason.
141141//!
142//! **`config.enableCalendar` has no Zig mirror**, the same gap
143//! `bridge_mobile_contactpicker.zig` records for `enableContacts`. The plist key
144//! `packages/ios/src/index.ts:190` writes iff that flag is the stand-in, and the
145//! mapping is exact — nothing else writes or shares it.
142//! **`config.enableCalendar` is read directly now.** `ios_config.gateFor` maps
143//! all three actions to `.calendar`, so `ios_dispatch.offerToModules` answers
144//! `CAPABILITY_DISABLED` before this module is asked. This used to say the flag
145//! had no Zig mirror, which was true until `ios_config.zig` read
146//! `craft.config.json`.
147//!
148//! The plist key `packages/ios/src/index.ts:190` writes iff that flag was the
149//! stand-in, and on the one path where it is *only* evidence of the flag rather
150//! than a precondition of the API it is now redundant with the real gate.
151//! Tracked in issue #131; the paths where the key is a genuine precondition are
152//! separated below and stay.
146153//!
147154//! **Behaviour changes on every disabled or malformed path, in Zig's favour.**
148155//! Swift's three cases have no `else` (717-731): an app with
packages/zig/src/bridge_mobile_contactpicker.zigmodified+14-6
Changes to packages/zig/src/bridge_mobile_contactpicker.zig
@@ -83,12 +83,20 @@
8383//! Until then, "the native call failed" is what a cancel says, and saying so
8484//! here is the point.
8585//!
86//! **`config.enableContacts` has no Zig mirror.** `ios.zig`'s `AppConfig` has
87//! no such field — the same gap `bridge_mobile_system.zig` records for
88//! `enableShare`. The Info.plist is read instead, and for this flag the mapping
89//! is exact: `packages/ios/src/index.ts:189` writes `NSContactsUsageDescription`
90//! if and only if `config.enableContacts`, with none of the sharing that made
91//! the location keys ambiguous.
86//! **`config.enableContacts` is read directly now, and the plist proxy below
87//! is what is left over.** `ios_config.gateFor` maps this action to
88//! `.contacts`, so `ios_dispatch.offerToModules` answers `CAPABILITY_DISABLED`
89//! before this module is asked. The paragraph here used to say the flag had no
90//! Zig mirror, which was true until `ios_config.zig` read
91//! `craft.config.json`.
92//!
93//! The `NSContactsUsageDescription` check further down was the stand-in for
94//! that flag — `packages/ios/src/index.ts:189` writes the key if and only if
95//! `config.enableContacts`, with none of the sharing that made the location
96//! keys ambiguous. It is now a second, weaker gate behind the real one, and it
97//! can only differ in the direction that refuses an action the config allows:
98//! an app whose Info.plist was not written by the SDK. Tracked in issue #131
99//! along with three more of the same shape.
92100//!
93101//! Two things about that gate have to be said plainly. First, the key is **not
94102//! a precondition of the API**: `CNContactPickerViewController` runs out of
packages/zig/src/bridge_mobile_filepicker.zigmodified+6-5
Changes to packages/zig/src/bridge_mobile_filepicker.zig
@@ -74,11 +74,12 @@
7474//! Info.plist key to read instead: `renderUsageDescriptions`
7575//! (`packages/ios/src/index.ts:183-199`) writes nothing from either flag.
7676//!
77//! Both are therefore served unconditionally, and an app that set
78//! `enableFilePicker: false` or `enableFileDownload: false` now gets the
79//! behaviour anyway. Restoring the gate means adding the fields to
80//! `AppConfig`, not adding a guess here — the same trade
81//! `bridge_mobile_system.zig` records word for word for `share`.
77//! Both flags are read directly now: `ios_config.gateFor` maps `pickFile` to
78//! `.file_picker` and `saveFile` to `.file_download`, so
79//! `ios_dispatch.offerToModules` answers `CAPABILITY_DISABLED` before this
80//! module is asked. This used to say both were served unconditionally and that
81//! restoring the gate meant adding fields to `AppConfig` — true when written,
82//! and answered by `ios_config.zig` reading `craft.config.json` instead.
8283//!
8384//! Falling through to the shim is not the safe option it was for
8485//! `takeScreenshot`, because the shim does not answer these correctly today:
packages/zig/src/bridge_mobile_imagepicker.zigmodified+16-9
Changes to packages/zig/src/bridge_mobile_imagepicker.zig
@@ -78,23 +78,30 @@
7878//! **false** (`CraftApp.swift:192`, `packages/ios/src/index.ts:120`). When it
7979//! is false the `case` falls off the end and replies *nothing at all*, while
8080//! `CraftSwiftShim.handleAction` still returns `true` (`:5247`) so Zig
81//! believes the hand-off succeeded. The injected promise is the hand-built
82//! kind with no timeout. **In a default-config app, `openCamera` and
83//! `pickImage` hang the page forever today.** The "working Swift shim" is
84//! only working for an app that set `enableCamera: true`.
81//! believes the hand-off succeeded, and the injected promise is the hand-built
82//! kind with no timeout — so on the shim a default-config app hangs the page
83//! forever. `ios_config.gateFor` maps both actions to `.camera` now, so
84//! `ios_dispatch.offerToModules` answers `CAPABILITY_DISABLED` first and that
85//! hang is unreachable while Zig is linked. This paragraph asserted the hang
86//! in the present tense until then, and the next one said `enableCamera` had
87//! no Zig-reachable channel at all.
8588//!
86//! `enableCamera` has no Zig-reachable channel, so this module gates on the
87//! Info.plist keys `packages/ios/src/index.ts:185-186` writes:
89//! The Info.plist keys `packages/ios/src/index.ts:185-186` writes are still
90//! read, for two different reasons:
8891//!
8992//! - **`NSCameraUsageDescription` is an extra gate on `openCamera` only, and
9093//! it is a crash guard rather than a config proxy.** Presenting a `.camera` picker in a
9194//! process without that key is a TCC termination, not an error anything
9295//! could be told about. It is written for
9396//! `enableCamera || enableVideoRecording || enableQRScanner || enableAR`.
94//! - **`NSPhotoLibraryUsageDescription` stands in for `config.enableCamera`,
97//! - **`NSPhotoLibraryUsageDescription` stood in for `config.enableCamera`,
9598//! and it is over-broad by `enableVideoRecording`** — it is written for
96//! `enableCamera || enableVideoRecording`, which is the closest proxy
97//! available. It is emphatically **not** a photo-library permission gate:
99//! `enableCamera || enableVideoRecording`, which was the closest proxy
100//! available while the flag could not be read. It is now a second, weaker
101//! gate behind the real one, and the only one of these proxies inexact even
102//! in an SDK-generated app. Tracked in issue #131 with three more.
103//!
104//! It is emphatically **not** a photo-library permission gate:
98105//! `UIImagePickerController` with `.photoLibrary` has run out-of-process
99106//! since iOS 11 and neither prompts nor requires the key. Refusing on it is
100107//! a statement about how the app was configured, never about what the
packages/zig/src/bridge_mobile_location.zigmodified+13-9
Changes to packages/zig/src/bridge_mobile_location.zig
@@ -1236,16 +1236,20 @@ const key_always = "NSLocationAlwaysAndWhenInUseUsageDescription";
12361236/// Decide, from evidence in the running process, whether geolocation was
12371237/// configured and which prompt to ask for.
12381238///
1239/// Neither `config.enableGeolocation` nor `config.enableBackgroundLocation` has
1240/// a Zig mirror — they appear nowhere in `packages/zig/src` outside a comment —
1241/// which is the same gap `bridge_mobile_watch.zig` documents for
1242/// `enableWatchApp`. Rather than guess, this reads the Info.plist keys the
1243/// generator writes from exactly those flags. Apple requires the usage
1244/// description before authorization may be requested at all, so its absence is
1245/// both the honest signal that the app was never built for location and the
1246/// condition under which requesting would be illegitimate; the refusal happens
1239/// Both `config.enableGeolocation` and `config.enableBackgroundLocation` are
1240/// read directly now — `ios_config.gateFor` maps `getCurrentPosition`,
1241/// `watchPosition` and `startLocationRecording` to `.geolocation`, and
1242/// `ios_dispatch.offerToModules` answers `CAPABILITY_DISABLED` before this
1243/// module is asked. This used to say neither had a Zig mirror and that they
1244/// appeared nowhere in `packages/zig/src`; `ios_config.zig`'s own header names
1245/// this file as one of the workarounds it replaced.
1246///
1247/// The plist read below stays, and not as a config proxy. Apple requires the
1248/// usage description before authorization may be requested at all, so its
1249/// absence is the condition under which requesting would be illegitimate — a
1250/// precondition of the API rather than evidence of a flag. The refusal happens
12471251/// before any request is made, so what the system would otherwise do never
1248/// arises.
1252/// arises, and which of the two keys is present still selects the prompt.
12491253///
12501254/// Two divergences, both real and neither hideable:
12511255///
packages/zig/src/bridge_mobile_motion.zigmodified+12-5
Changes to packages/zig/src/bridge_mobile_motion.zig
@@ -79,15 +79,21 @@
7979//! `else`**, so on the Swift path with the flag off `startMotionUpdates`
8080//! replies nothing at all while `CraftSwiftShim.handleAction` still returns
8181//! true: the page's promise never settles. The flag defaults to `false`
82//! (`packages/ios/src/index.ts:139`) and has no mirror anywhere under
83//! `packages/zig/src`.
82//! (`packages/ios/src/index.ts:139`). It is read directly now:
83//! `ios_config.gateFor` maps `startMotionUpdates` to `.motion_sensors`, so
84//! `ios_dispatch.offerToModules` answers `CAPABILITY_DISABLED` before this
85//! module is asked. Until `ios_config.zig` read `craft.config.json` the flag
86//! had no mirror anywhere under `packages/zig/src`, and the paragraph below is
87//! what this module did about that.
8488//!
8589//! `NSMotionUsageDescription` is an *exact* proxy for it —
8690//! `packages/ios/src/index.ts:192` writes that Info.plist key from
8791//! `config.enableMotionSensors` and from nothing else, with no `||` (unlike the
8892//! location keys, where `bridge_mobile_location.zig` had to document a residual
8993//! gap). So `requireMotionConfigured` reads the key and refuses when it is
90//! absent. Two things that buys, and one it costs:
94//! absent — now a second, weaker gate behind the real one, which can only
95//! differ by refusing an action the config allows. Tracked in issue #131 with
96//! three more of the same shape. Two things it bought, and one it cost:
9197//!
9298//! - a hang becomes a nameable refusal, which is strictly better than the
9399//! shim's silence;
@@ -249,8 +255,9 @@ pub const default_interval_ms: f64 = 100;
249255const reply_true = "true";
250256
251257/// Emitted into Info.plist by `packages/ios/src/index.ts:192` iff
252/// `config.enableMotionSensors` — the flag with no Zig mirror. See the module
253/// comment for what this proxy claims and what it does not.
258/// `config.enableMotionSensors`, which `ios_config.gateFor` now reads directly.
259/// See the module comment for what this proxy claims, what it does not, and why
260/// it is redundant with the real gate.
254261const key_motion_usage = "NSMotionUsageDescription";
255262
256263/// Which handler an action selects, split out from `handleMessage` so the
packages/zig/src/bridge_mobile_notifcancel.zigmodified+5-5
Changes to packages/zig/src/bridge_mobile_notifcancel.zig
@@ -43,11 +43,11 @@
4343//! with no `else`: a missing `id`, a non-string `id`, or the gate at its
4444//! default `false` replies nothing at all, and the untimed promise never
4545//! settles. Malformed input errors here instead (`MissingData` /
46//! `InvalidParameter`). The `enableLocalNotifications` gate has no Zig mirror
47//! (it appears nowhere in `packages/zig/src`), so the actions are served
48//! unconditionally — the securestore module documents the same choice, and
49//! cancelling notifications grants a page strictly less than the wipe that
50//! precedent already serves ungated.
46//! `InvalidParameter`), and the gate itself is read rather than skipped:
47//! `ios_config.gateFor` maps all three actions to `.local_notifications`, so
48//! `ios_dispatch.offerToModules` answers `CAPABILITY_DISABLED` before this
49//! module is asked. A settled rejection rather than a hang is the divergence,
50//! and it is the whole of it.
5151//!
5252//! **An `id` with an embedded NUL.** `\u0000` is a legal JSON escape and a
5353//! page can send it; the route to the UN center is `stringWithUTF8String:`,
packages/zig/src/bridge_mobile_notifications.zigmodified+4-5
Changes to packages/zig/src/bridge_mobile_notifications.zig
@@ -40,11 +40,10 @@
4040//! **Swift's silent hang.** The dispatcher arm is
4141//! `if config.enableLocalNotifications { … }` with no `else`, and the gate
4242//! defaults to `false` — so with notifications disabled the untimed promise
43//! never settles. `enableLocalNotifications` has no mirror anywhere in
44//! `packages/zig/src`, so this action is served unconditionally, the same call
45//! `bridge_mobile_securestore.zig` and `bridge_mobile_notifcancel.zig` made.
46//! Listing an app's *own* pending notifications grants a page strictly less
47//! than the cancel-everything that precedent already serves ungated.
43//! never settles. Here the flag is read: `ios_config.gateFor` maps this action
44//! to `.local_notifications` and `ios_dispatch.offerToModules` answers
45//! `CAPABILITY_DISABLED` before this module is asked. A settled rejection
46//! rather than a hang is the divergence, and it is the whole of it.
4847//!
4948//! **A truncated string reported as the whole one.** `-[NSString UTF8String]`
5049//! is NUL-terminated, so a stored title carrying an embedded U+0000 — which
packages/zig/src/bridge_mobile_securestore.zigmodified+21-12
Changes to packages/zig/src/bridge_mobile_securestore.zig
@@ -40,18 +40,27 @@
4040//! **Swift's silent hang.** The set/get/remove dispatcher arms are `if let`s
4141//! with no `else`: a missing `key`, a non-string `value`, or a disabled
4242//! `enableSecureStorage` config replies nothing at all, and the legacy promise
43//! never settles. Malformed input errors here instead. The capability gate has
44//! no Zig mirror (`enableSecureStorage` appears nowhere in `packages/zig/src`),
45//! so the actions are served unconditionally. For set/get/remove that grants a
46//! page nothing new: the *ungated* sharedItems actions next door already
47//! read and write the same items (one namespace — see above). The two gated
48//! modules that came before chose differently — clipboard kept its gate
49//! plumbable (`craft_ios_set_clipboard_enabled`, rejecting `PermissionDenied`
50//! when off) and haptics declared its gated action `.unavailable` — and
51//! `secureClear` is where the difference is real: under Swift's default-false
52//! gate a page got CAPABILITY_DISABLED instead of a wipe, and here the wipe is
53//! always live. If that gate is wanted back, clipboard's hook is the pattern;
54//! what must not come back is Swift's silent hang on the other three.
43//! never settles. Malformed input errors here instead, and the gate is read
44//! rather than skipped: `ios_config.gateFor` maps all four actions to
45//! `.secure_storage`, so `ios_dispatch.offerToModules` answers
46//! `CAPABILITY_DISABLED` before this module is asked. A settled rejection
47//! rather than a hang is the divergence, and it is the whole of it.
48//!
49//! This paragraph argued the opposite case at length — that
50//! `enableSecureStorage` "appears nowhere in `packages/zig/src`", that the
51//! actions were therefore served unconditionally, and that `secureClear` was
52//! where it mattered because a page got a wipe where Swift's default-false
53//! gate would have refused. Every sentence of it was true when written and
54//! none of it survived `ios_config.zig`, which read the flags out of
55//! `craft.config.json` and closed the gap for thirty-odd actions at once.
56//! Worth knowing while reading any other module's account of a missing gate:
57//! the same paragraph was written in eight other files.
58//!
59//! What has not changed is that `secureClear` is the one action where the gate
60//! is load-bearing. The *ungated* sharedItems actions next door read and write
61//! the same items (one namespace — see above), so set/get/remove being gated
62//! grants a page nothing it could not reach anyway; a wipe has no such
63//! equivalent.
5564//!
5665//! **`get`'s collapse of every failure into `null`.** Swift returns `nil` for
5766//! *any* non-success status, so a page cannot tell "nothing stored" from "the
packages/zig/src/bridge_mobile_speech.zigmodified+14-3
Changes to packages/zig/src/bridge_mobile_speech.zig
@@ -56,13 +56,24 @@
5656//! **The plist guard replaces the nil-recognizer guard.** Swift builds its
5757//! `SFSpeechRecognizer` only when `config.enableSpeechRecognition` is on
5858//! (`:439-440`), and `beginRecording`'s `guard let speechRecognizer` then
59//! emits `"Speech recognizer unavailable"` when it is off. Zig cannot see the
60//! ivar, but `packages/ios/src/index.ts:183` writes
59//! emits `"Speech recognizer unavailable"` when it is off. Zig cannot see that
60//! ivar — still true — but `packages/ios/src/index.ts:183` writes
6161//! `NSSpeechRecognitionUsageDescription` from that flag **and nothing else,
62//! with no `||`** — the exact-proxy shape `bridge_mobile_motion.zig` already
62//! with no `||`** — the exact-proxy shape `bridge_mobile_motion.zig` also
6363//! reads. So a missing key means the flag was off, and the message emitted is
6464//! the one Swift would have emitted for the same configuration.
6565//!
66//! The flag itself is no longer out of reach: `ios_config.gateFor` maps
67//! `startListening` to `.speech_recognition`, so `ios_dispatch` answers
68//! `CAPABILITY_DISABLED` before this module is asked. That does **not** make
69//! the plist read redundant, and it is worth being precise about why, because
70//! the four other modules reading a key as a flag proxy *are* now redundant
71//! (#131) and this one looks identical at a glance. Here the key is also a
72//! precondition: `requestAuthorization:` without it does not fail, iOS
73//! terminates the process — so the check has to happen whatever the config
74//! says. The gate takes over the reason for refusing a disabled app; the key
75//! still owns not being killed, and the message.
76//!
6677//! The check has to run *before* `requestAuthorization:`, where Swift's runs
6778//! after, because asking for speech authorization without the key does not
6879//! fail — iOS terminates the process. Order diverges; the page-visible
packages/zig/src/bridge_mobile_system.zigmodified+7-5
Changes to packages/zig/src/bridge_mobile_system.zig
@@ -36,11 +36,13 @@ const is_darwin = builtin.target.os.tag.isDarwin();
3636/// custom app schemes are its main use. Filtering here would silently break
3737/// them.
3838///
39/// **`share` is served unconditionally.** Swift gates it on
40/// `config.enableShare` and replies `CAPABILITY_DISABLED`; `ios.zig`'s
41/// `AppConfig` has no such field, so Zig cannot reproduce the gate. An app that
42/// set `enableShare: false` will now get a share sheet. Restoring the gate
43/// means adding the field to `AppConfig`, not adding a guess here.
39/// **`share` is gated, and not here.** Swift replies `CAPABILITY_DISABLED`
40/// when `config.enableShare` is off, and `ios_dispatch.offerToModules` sends
41/// the same code before this module is asked — `ios_config.gateFor("share")`
42/// maps it. This paragraph used to say the opposite, that `AppConfig` had no
43/// such field and an app setting `enableShare: false` would get a share sheet
44/// anyway; that was true until `ios_config.zig` read the flags out of
45/// `craft.config.json`, and false from then on with nothing to notice.
4446pub const A = struct {
4547 pub const open_url = "openURL";
4648 pub const share = "share";
packages/zig/src/bridge_mobile_watch.zigmodified+7-4
Changes to packages/zig/src/bridge_mobile_watch.zig
@@ -76,10 +76,13 @@
7676//! `localizedDescription` are all logged before the enum is chosen.
7777//!
7878//! **The `enableWatchApp` config gate.** Swift only ever calls
79//! `setupWatchConnectivity` under `if config.enableWatchApp`, and that flag has
80//! no Zig mirror — it appears nowhere in `packages/zig/src`. Both actions are
81//! therefore served unconditionally, exactly as `securestore` and `notifcancel`
82//! document for their own gates. This grants a page nothing it should not have:
79//! `setupWatchConnectivity` under `if config.enableWatchApp` — a setup-time
80//! gate rather than a per-action one, so no `case` arm here carries an
81//! `if config.` and `ios_config.gateFor` correctly maps none of these actions.
82//! Both are therefore served unconditionally, and this is the one module whose
83//! version of that sentence is still true: `securestore` and `notifcancel` said
84//! the same until `ios_config.zig` read the flags their arms *do* carry.
85//! Serving these ungated grants a page nothing it should not have:
8386//! with the gate off Swift never activated a session, so `defaultSession`'s
8487//! `activationState` is `NotActivated` and both actions answer honestly —
8588//! `{"reachable":false}` and a refusal to update.