Push Notifications & Subscriptions epic (#379)#381
Conversation
Add curated CloudKitService support for the five CloudKit Web Services push-notification endpoints, plus MistDemo CLI, integration-phase, and web-server wiring. MistKit core (#49/#50/#51/#52/#53): - listSubscriptions / lookupSubscriptions / modifySubscriptions (+ create/ delete convenience) and createAPNsToken / registerAPNsToken - New domain types under Models/Subscriptions and Models/Tokens; errors route through CloudKitServiceError; CloudKitResponseType conformances per op - openapi.yaml: promote the records/query inline query shape into a shared named `Query` schema, referenced from both records/query and Subscription.query, so query subscriptions reuse the same query model; regenerated MistKitOpenAPI MistDemo: - Real list/lookup/create-token/register-token commands + new modify-subscriptions command; SubscriptionRoundtripPhase + TokenRoundtripPhase wired into the private pipeline - Web server: WebBackend methods + routes for subscriptions/* and tokens/*, replacing the 501 pending stubs; tokens.js feeds the minted token into register Native app left informational (per #52/#53). Adds unit + service + web-route tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…p ci]
Surfaced by running the live MistDemo integration suite against a real
container.
- modifySubscriptions: CloudKit echoes a deleted subscription as a bare
{ subscriptionID } with no subscriptionType. Filter those deletion
acknowledgements instead of fatal-erroring in SubscriptionInfo(from:).
- tokens/create + tokens/register: the endpoints are container-scoped
(/database/{version}/{container}/{environment}/tokens/...) with no
{database} segment. Corrected openapi.yaml + regenerated; added
ContainerOperationInputPath for the database-less path init. (The live
endpoints still return 405 server-side — tracked in #379.)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Cross-checked against Apple's authoritative reference:
- developer.apple.com/library/archive/.../CreateTokens.html
- developer.apple.com/library/archive/.../RegisterTokens.html
Two real spec bugs surfaced, independent of the ongoing 405 mystery:
1. tokens/register was missing apnsEnvironment in the request body.
Apple lists both apnsEnvironment + apnsToken as Required. Updated
openapi.yaml; registerAPNsToken now takes (token, environment, db).
2. TokenResponse shape was wrong. Apple returns { apnsEnvironment,
apnsToken, webcourierURL } (long-poll URL for browser/SW callers).
Ours had a speculative `webcAuthToken` and no webcourierURL.
Reshaped TokenResponse + APNsTokenResult; webcourierURL is now a
URL on the domain type, environment is echoed back.
Regenerated MistKitOpenAPI; updated CLI (--apns-environment flag on
register-token), MistDemo integration phase, web routes + JS panel,
and tests. 498 MistKit + 941 MistDemo unit tests pass; lint clean.
Live test-private still fails at Phase 16 (TokenRoundtripPhase) —
createAPNsToken returns 405 before we ever reach the corrected
registerAPNsToken. Skip CI: integration scope didn't change, and
the live live-suite failure is by design pending the next diagnostic
pass (see #379 comment).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CloudKit JS calls setApiModuleName("device") on token requests, producing
POST /device/1/{container}/{environment}/tokens/{create,register}. The
archived REST reference documents these under /database/... but the live
service only routes OPTIONS there, hence the persistent 405 with
Allow: OPTIONS. Switching to /device/ unblocks server-side calls.
Also surfaces clientId on both request bodies (CloudKit JS always sends
one, defaulting to a UUID) and exposes it as an optional caller-supplied
parameter so callers can pin both halves of the round-trip to one
logical client.
Live test-private phase 16 (createToken+registerToken) now returns 200
with the expected {apnsToken, apnsEnvironment, webcourierURL} body —
previously it failed at the first call with HTTP 405.
Adds MistDemoLoggingBootstrap so --verbose on the integration commands
turns on swift-log at .debug for com.brightdigit.MistKit.middleware,
letting the existing LoggingMiddleware actually emit the wire trace
that made this diagnosis possible.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Code Review: Push Notifications & Subscriptions epic (#379)Overall this is a solid, well-structured implementation that follows the project's established patterns faithfully. The architecture mirrors Moderate1. Web server silently forwards empty strings for missing subscription fields subscriptionID: input.subscriptionID ?? "",
recordType: input.query?.recordType ?? "",
zoneID: ZoneID(zoneName: input.zoneID?.zoneName ?? "")If the browser omits 2. self.apnsToken =
try container.decodeIfPresent(String.self, forKey: .apnsToken) ?? ""A missing 3. await #expect(throws: CloudKitError.self) {This catches any Minor / Informational4. APNsEnvironment has two near-identical 5. 6. Public memberwise init allows structurally invalid 7. 8. Nit
Overall the PR is in good shape. The two web-server input-validation issues (#1 and #2) are the most worth fixing before merge since they result in confusing errors for the browser demo; the rest are polish items. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## v1.0.0-beta.2 #381 +/- ##
================================================
Coverage ? 70.23%
================================================
Files ? 151
Lines ? 3383
Branches ? 0
================================================
Hits ? 2376
Misses ? 1007
Partials ? 0
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
…ip ci]
Close the loop on subscription notifications with a headless, receive-side
test path and the matching web-app reception, using the verified CloudKit
web-courier wire format.
MistDemoKit (Swift):
- WebCourierPoller: long-polls a webcourierURL on a dedicated URLSession;
exposes pollOnce/waitForFrame plus a notifications() AsyncThrowingStream
(the addNotificationListener mirror). No de-dup: the courier is
consume-on-delivery and nid is shared across subscriptions for one change.
- CourierFrame / CourierNotification: typed model mapping the {aps, ck}
payload to the documented CloudKit.QueryNotification fields.
- NotificationRoundtripPhase: create subscription (create/update/delete on
Note) -> mint+register courier token -> trigger a record change -> await
the push filtered by subscriptionID -> self-clean. Soft on non-arrival
(delivery is eventual); registered in the private-DB pipeline.
Web app (tokens.js): MistKit mode now long-polls the courier URL returned by
/api/tokens and renders incoming notifications, mirroring CloudKit JS mode's
registerForNotifications()/addNotificationListener.
Docs: WEB_COURIER_SPIKE.md records the resolved wire format (request shape,
{aps, ck} framing, consume-on-delivery / no-cursor). CLAUDE.md corrects the
stale --config-file reference to the real Swift Configuration precedence.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…skip ci] Pin the three real web-courier payload shapes captured against a live container — an alerting push, a bare ck-only frame, and a silent content-available push — plus the fo→reason mapping, as a decoding regression for CourierNotification. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… [skip ci] modifySubscriptions silently dropped CloudKit's inline per-subscription errors (e.g. INTERNAL_ERROR "could not find subscription we just created"), returning an empty array where CloudKit JS shows a visible failure. Apply the records' RecordResult success-or-failure pattern to subscriptions so failures are surfaced consistently across the API. - openapi.yaml: add SubscriptionOperationFailure; make SubscriptionsModifyResponse items oneOf:[SubscriptionOperationFailure, Subscription] (mirrors ModifyResponse). Regenerated MistKitOpenAPI. - Hoist the server-error-code enum to a shared CloudKitServerErrorCode (RecordOperationFailure.ServerErrorCode kept as a typealias) so record and subscription failures share one code type. - Add SubscriptionOperationFailure + SubscriptionResult (.success/.failure/.get()) and CloudKitError.subscriptionOperationFailed. - modifySubscriptions now returns [SubscriptionResult] (surfaces failures, still skips deletion acks); createSubscription throws on failure; deleteSubscription surfaces a failed delete. Web demo maps .get() so the panel shows the error. Note: this surfaces the failure; it does not make the create succeed — that CloudKit-side cause (likely a non-queryable record type) is unchanged. Breaking: modifySubscriptions return type [SubscriptionInfo] -> [SubscriptionResult]. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Delivers the full server-side push-notification workflow for CloudKit Web Services — subscriptions (the triggers) plus APNs tokens (the delivery path) — closing epic #379 and sub-issues #49, #50, #51, #52, #53.
subscriptions/list,/lookup,/modify): newCloudKitServicemethods, CLI commands (list-subscriptions,lookup-subscription,modify-subscriptions),SubscriptionRoundtripPhasein the live runner. Working live since the first commit on this branch.tokens/create,tokens/register): newCloudKitServicemethods, CLI commands (create-token,register-token),TokenRoundtripPhase. The endpoint route turned out to be/device/1/…(CloudKit JS usessetApiModuleName("device")), not/database/1/…as Apple's archived REST reference documents — that detail was the missing piece that resolved the persistent HTTP 405 on every server-side call. TheclientIdfield CloudKit JS always sends is now part of both bodies, exposed as an optional caller-supplied parameter (defaults to a fresh UUID per call).--verboseontest-private/test-publicnow bootstrapsLoggingSystemso MistKit's existingLoggingMiddlewareactually emits the wire trace — necessary for diagnosing the 405 and useful for anyone reverse-engineering CloudKit JS in the future.Live verification
swift run mistdemo test-private --verboseagainstiCloud.com.brightdigit.MistDemo— all 17 phases green, including:SubscriptionRoundtripPhase(create / list / lookup / delete)TokenRoundtripPhase(POST /device/1/.../tokens/create→ 200, thentokens/register→ 200)test-public --verboseblocked on an unrelated pre-existing config bug (S2S key path has escaped backslashes —com\\~apple\\~CloudDocsinstead ofcom~apple~CloudDocs). Filing as a separate follow-up; not in scope here.Test plan
swift test— 498/498 MistKit unit testscd Examples/MistDemo && swift test— 941/941 MistDemo unit tests./Scripts/lint.sh— clean (only 4 pre-existing warnings unrelated to this branch)swift run mistdemo test-private --verbose— 17/17 phases green livetest-private --verbosefrom a clean checkout if you want to see the wire traceKnown follow-ups (out of scope for this PR)
tokens/registerresponse spec — live server actually returns the same{apnsToken, apnsEnvironment, webcourierURL}body astokens/create, not the empty 200 our spec assumes. Behaviorally harmless (we ignore the body) but accurate.CloudKitService+TokenOperations.swiftstill cites Apple's archivedRegisterTokens.htmlas the source of truth; CloudKit JS is the actual authority now.MockTransportdoesn't capture the request body, so we can't assertclientIdlands in the JSON from unit tests. A capturing transport variant would let us regression-pin the wire shape.Resources/js/tokens.jsdoesn't yet surface aclientIdinput. Backend accepts requests without it (auto-UUID), so the panel works; adding a field is pure UX polish.test-publicconfig loading.Closes #49, #50, #51, #52, #53, #379.