Stability: v1 development phase. The public API and persisted formats may change aggressively without backward compatibility. Canonical outcome and error wire types are consumed directly from
arkret-models-integrationandarkret-wire.
Arkret push notification gateway types, preferences, and device registration helpers.
chime provides shared data types and request-building logic for Arkret clients that register push-capable devices with their own Station. A Gateway URL is registration data; the client never calls the Gateway directly.
chime aligns push routes with the current Realm / Space vocabulary:
- Realm: security boundary — membership, capability, E2EE, and federation are governed at this level. Push permission and per-realm mute apply here.
- Space: navigation container — board, list, section, or calendar bucket. Lives inside a Realm and inherits its security context.
Two client-SDK shape changes. See
CHANGELOG.md [Unreleased] for the chime-facing
wire-breaking list; the normative source remains ../arkret-spec/spec/v1/.
mention_redirect_target_route_tokensrecognition — typed push-payload structs expose the field; consumers decide whether the active device route token is an authorised redirect target.- Floria-passthrough diagnostic reason code —
historical_onlyis part of the typed reason-code surface and suppresses user-visible notification.
Ephemeral kinds (ak.presence, ak.typing, ak.receipt.read,
ak.call.signal) are confirmed not consumed by chime. See
CHANGELOG.md [Unreleased]; the normative source
remains ../arkret-spec/spec/v1/.
简体中文接入文档见 zh/README.md,包括
zh/integration-guide.md、
zh/error-codes.md 和
zh/production-checklist.md。
| Gateway | Description |
|---|---|
| Standard | Arkret Push Gateway using platform adapters such as FCM, APNs, Web Push, or desktop notification bridges. |
| Bugle | Arkret Push Gateway with domestic Chinese Android OEM provider support through JPush, Huawei, Xiaomi, OPPO, vivo, HONOR, and HarmonyOS routes. |
| Custom | User-provided gateway URL for self-hosted or third-party gateways. |
Bugle host integrations still own provider SDK setup and token
acquisition. For app-id resolution, GatewayBinding::resolve_app_id
and PushGatewayType::Bugle prefer domestic_app_id for Android.
| Feature | Description |
|---|---|
| (default) | Core types, JSON serialization helpers, and Arkret register/unregister request builders. No network dependencies. |
http |
Adds ArkretPushClient, a small reqwest helper for calling the signed-in account's Station at POST /_arkret/edge/push/register-device and POST /_arkret/edge/push/unregister-device. |
Server-side
ak.edge.push.command.notify.v1wire types are not part of this crate. chime is a client SDK: it builds register/unregister requests and evaluates client-side push rules. Gateway implementations that need to validate outbound blind-wakeup payloads use the SDK'sarkret_push_policy::blind_payload_sanitizerdirectly.
AKP-0007 Circle primitive
introduces an encrypted sub-boundary inside a Realm. The security scope
of an Event lives in its producer-signed scope_ref; object read
projections may materialize an equal effective_scope. Neither name
reaches the push wire:
- Receiving a Circle-scoped wakeup. The push wakeup wire carries no
Circle identifier and no scope field. Raw Realm ids, Circle ids and
effective_scopeMUST NOT enter/_arkret/edge/push/notify(push-notifications.md§5.1); the wakeup only carries opaqueroute_tokens.scope_route_token/devices[].target_route_token. The client resolves the security boundary from the durable Event it fetches after the wakeup, never from the wakeup itself. The closed SDK wire type rejectscircle_name/circle_display_nameand thecircle_members/circle_member_dids/circle_member_actor_idsmembership surfaces during deserialization. - Muting a Circle on this device. Per-scope mute state lives in the host application, matched host-side against the scope the client resolved for the fetched Event — the wakeup itself cannot be matched, because it carries no scope. A muted scope MUST NOT raise a UI surface; wire-level delivery is unchanged so floria can still account for the receipt.
chime is not published to a registry; consumers depend on a sibling checkout
by path. Add to your Cargo.toml:
[dependencies]
chime = { path = "../chime" }
# or with the Arkret HTTP registration client:
# chime = { path = "../chime", features = ["http"] }use chime::{GatewayBinding, PushGatewayType, PushPreferences};
let prefs = PushPreferences {
enabled: true,
gateways: vec![
GatewayBinding::new(
PushGatewayType::Bugle,
"https://push.palpo.im/_arkret/edge/push/notify",
)
.with_app_id("app.example.android.cn"),
],
..Default::default()
};
let binding = &prefs.gateways[0];
let app_id = binding.resolve_app_id(
Some(chime::PushPlatform::Android),
Some("app.example.android"),
Some("app.example.android.cn"),
);
assert_eq!(app_id, Some("app.example.android.cn"));
// `PushPreferences` is plain `serde`; persist it with whatever encoder
// the host already uses.
let json = serde_json::to_string_pretty(&prefs).unwrap();
let loaded: PushPreferences = serde_json::from_str(&json).unwrap();
assert_eq!(loaded.gateways[0].gateway_type, PushGatewayType::Bugle);use chime::{
GatewayBinding, PushDeviceConfig, PushGatewayType, PushPreferences,
build_register_device_request,
};
let config = PushDeviceConfig {
account_id: Some(arkret_wire::AccountId::new(
arkret_wire::DidCoreId::new("ak:did_core:web:alice.example").unwrap(),
arkret_wire::DidCoreId::new("ak:did_core:web:station.example").unwrap(),
)),
device_id: "ak:device:01904100-0000-7000-8000-000000000004",
push_key: Some("fcm-or-apns-device-token"),
platform: Some("android"),
app_id: Some("app.example.android"),
domestic_app_id: Some("app.example.android.cn"),
registration_id: None,
display_name: Some("Alice Phone"),
idempotency_key: Some("device-phone-register-v1"),
request_id: Some("ak:request:01904100-0000-7000-8000-000000000001"),
proof: None,
visible_notification_opt_in: false,
};
let binding = GatewayBinding::new(
PushGatewayType::Standard,
"https://push.example.com/_arkret/edge/push/notify",
);
let prefs = PushPreferences {
enabled: true,
..Default::default()
};
let request = build_register_device_request(&config, &binding, &prefs).unwrap();
assert_eq!(
request.device_id,
"ak:device:01904100-0000-7000-8000-000000000004"
);# #[cfg(feature = "http")]
# async fn doc_run() -> Result<(), chime::PushRegistrationError> {
# use chime::{GatewayBinding, PushDeviceConfig, PushGatewayType, PushPreferences};
# let config = PushDeviceConfig {
# account_id: None,
# device_id: "ak:device:01904100-0000-7000-8000-000000000004",
# push_key: Some("fcm-or-apns-device-token"),
# platform: None,
# app_id: None,
# domestic_app_id: None,
# registration_id: None,
# display_name: None,
# idempotency_key: None,
# request_id: None,
# proof: None,
# visible_notification_opt_in: false,
# };
# let binding = GatewayBinding::new(
# PushGatewayType::Standard,
# "https://push.example.com/_arkret/edge/push/notify",
# );
# let prefs = PushPreferences {
# enabled: true,
# ..Default::default()
# };
use chime::{ArkretPushClient, build_register_device_request};
// This is the signed-in account's Station, never `binding.push_gateway`.
let client = ArkretPushClient::new("https://station.example")
.with_bearer_token("session-token")
.with_timeout(std::time::Duration::from_secs(10))
.with_session_grant("externally-issued-coauth-or-coland-grant")?;
let request = build_register_device_request(&config, &binding, &prefs)?;
let response = client
.register_device_with_request(&request, None, None)
.await?;
let _push_target_id = response.body.push_target_id;
# Ok(())
# }Production notes:
- Authentication material is sent only to the account's Station through headers. It is never forwarded to
binding.push_gateway; do not put secrets in that URL either. - Each binding's
push_gatewaymust be HTTPS unlessallow_insecure_loopback_push_gatewayis enabled for localhost development. - WebPush registrations can set
webpush_endpoint_allowlistto restrict accepted endpoint origins or host patterns before a browser endpoint is sent to the service. push_keyis redacted in debug output and logs; callers should still store it as a secret.ArkretPushClientretries only safe idempotent requests: register calls need an idempotency key, while unregister is treated as idempotent.Retry-Afteris honored before exponential backoff plus bounded jitter.
PushRegistrationState stores non-secret local state: principal ID, device id, platform/app id, gateway, token hash/preview, registration id, expiry, and last success timestamp. Build it from a successful register response with build_registration_state().
Persistence is host-owned: store the state through an OS keychain, encrypted app storage, or another locally protected store. The state includes schema_version so applications can migrate persisted records without exposing raw platform tokens.
chime is a library, not a service — it has no daemon, no config file
of its own, and no environment variables it reads. Operational concerns
live entirely in the host application that wires ArkretPushClient
into the signed-in account's Arkret Station.
| Knob | Value | Note |
|---|---|---|
| MSRV | 1.98 |
Enforced by CI; bump only with a minor release. |
| TLS backend | rustls-tls (via reqwest) |
No system OpenSSL dependency. |
| Tokio runtime | optional via http feature |
Multi-thread or current-thread, host's choice. |
| Logging | tracing events on the chime target |
No allocator on the hot path; subscribe at info-level for register/unregister breadcrumbs. |
- HTTPS only.
validate_push_gateway_urlrejects non-HTTPS gateway URLs unless the host opts in by settingPushPreferences::allow_insecure_loopback_push_gateway = truefor a loopback dev URL. - WebPush origins. Set
PushPreferences::webpush_endpoint_allowlistto the list of FCM / Apple / Mozilla origins or host patterns you accept;*.push.services.mozilla.comandhttps://fcm.googleapis.comare typical entries. - Auth in headers, not URLs. Session tokens, bearer tokens, API
keys, and HTTP message signatures must travel in headers. Putting
them in a binding's
push_gatewayquery string is rejected. - Session grant fail-closed.
ArkretPushClient::new(...)defaults towith_required_session_grant(true). Always attachX-Arkret-Session-Grantviawith_session_grant("...")(orwith_header(...)) before calling register/unregister. Dev sandboxes that need to skip this can flip the flag. - Idempotency. If you do not provide an idempotency key, the client generates one and reuses it across retries. Persist your own key for server-side dedup if you care about cross-process deduplication.
- Retries. The client does not retry (
PushRetryPolicy::disabled()is the fixed policy); hosts that need retries wrap calls in their own retry loop built onarkret_retry::RetryPolicy, honoringRetry-AfterviaPushRegistrationError::retry_after_duration(). - Token storage. Persist
PushRegistrationStatein an OS keychain, encrypted app storage, or another locally protected store. Never log rawpush_key; the crate redacts it inDebugimpls and tracing fields, and you should mirror that in your own code. - Privacy gate. Gateway-side payload validation is not chime's
surface. Validate every outbound wakeup through the SDK's
arkret_push_policy::blind_payload_sanitizerbefore it leaves the gateway; treating every notification as a blind wakeup is the safest default.
chime emits tracing events with the target prefix chime:
INFO chime::push::client Arkret push device registered (prebuilt body) platform=android operation_id=ak.edge.push.command.register_device.v1 request_id=ak:request:01904100-0000-7000-8000-000000000001
Subscribe with any tracing-subscriber layer; Sentry / OpenTelemetry
exporters work without further glue.
| Symptom | Likely cause |
|---|---|
MissingField("session_grant") returned before any HTTP request |
with_required_session_grant(true) is on but no X-Arkret-Session-Grant header is attached. |
InvalidPushGatewayUrl { reason: "scheme must be https" } |
HTTP push gateway URL in production. Allow loopback only if you're testing locally. |
WebPushEndpointNotAllowed |
The browser-issued endpoint origin is not in webpush_endpoint_allowlist. Add the origin or host pattern. |
Gone (HTTP 410) |
The push token was retired. Drop the local registration and let the device generate a new token before re-registering. |
Unauthenticated after token refresh |
The bearer token rotated mid-request. Refresh with_bearer_token(...) and retry. |
The crate maps to the Arkret v1 push HTTP binding:
| Operation ID | HTTP Binding | Request builder |
|---|---|---|
ak.edge.push.command.register_device.v1 |
POST /_arkret/edge/push/register-device |
build_register_device_request() |
ak.edge.push.command.unregister_device.v1 |
POST /_arkret/edge/push/unregister-device |
build_unregister_device_request() |
ak.edge.push.command.notify.v1 |
POST /_arkret/edge/push/notify |
Gateway/server-side operation, not a client registration call. |
Register/unregister builders keep the complete optional AccountId {principal_id, station_id} as local ownership metadata. It is never added to Station request bodies or headers. Device id, app id, gateway URL, and push key are projected into the canonical request; operation id, idempotency key, request id, and proof follow their transport bindings. Register responses carry push_target_id, optional registration_id, and optional expires_at; response headers carry transport metadata such as request id.
Push notifications are derived wakeups, not canonical truth. For E2EE Realms, push gateways should receive only minimized metadata such as push_target_id, wakeup_kind, counts, priority/TTL/collapse hints, and optional body-free push_hint.
The SDK-owned notify wire model enforces that boundary on the gateway side. Entity facets and view-renderer output are routing or context hints only when the application has explicitly authorized that use; do not serialize facet-derived title, body, display name, or space name into provider payloads. The closed SDK model rejects unknown aliases and provider wrappers during deserialization.
Arkret Client
|
v
+---------+ PushPreferences { gateways: Vec<GatewayBinding> }
| chime | --> GatewayBinding { gateway_type, push_gateway, push_key, app_id }
+----+----+ build_register_device_request(config, binding, prefs)
| ArkretPushClient (optional)
v
Account Station
|
v
Arkret Push Gateway
|
+-- FCM / APNs / Web Push / Desktop
+-- Bugle domestic providers:
+-- JPush
+-- Huawei / HarmonyOS
+-- Xiaomi
+-- OPPO / OnePlus
+-- vivo
+-- HONOR
Licensed under Apache 2.0. See LICENSE.