@skyporch/daykeeper-react-native 0.1.0 → 0.2.0

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 CHANGED
@@ -1,5 +1,126 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0 — 2026-09-21
4
+
5
+ This version introduces the native transport break described below. The
6
+ pre-release candidate was renumbered from `0.1.1` to `0.2.0`; `0.1.1` was never
7
+ released, so every change below is relative to the published `0.1.0`.
8
+
9
+ ### Security
10
+
11
+ `0.1.0` is no longer supported. Upgrade to `0.2.0`.
12
+
13
+ - `0.1.0` set no redirect policy. On React Native's default XHR-backed Fetch a
14
+ redirected request, including its bearer token, could be followed to another
15
+ endpoint. `0.2.0` sends `redirect: "error"`, `credentials: "omit"` and
16
+ `cache: "no-store"` on every request, and the native export refuses to run
17
+ without an explicitly supplied compliant Fetch transport.
18
+ - `0.1.0` replayed every request once after an HTTP 401, including writes, so
19
+ a message or conversation could be created twice. `0.2.0` never replays a
20
+ write; only reads refresh credentials and retry once.
21
+
22
+ ### Breaking
23
+
24
+ - **Native transport must be configured explicitly.** The native export no
25
+ longer falls back to React Native's XHR-based Fetch. Callers must supply a
26
+ `fetch` implementation that rejects redirects and omits ambient cookies; on
27
+ the validated Expo 57 runtime, `fetch` from `expo/fetch`. A client
28
+ constructed without one now fails before credentials are requested.
29
+ - **Server error codes must look like codes.** `0.1.0` copied any string
30
+ `error` value into `DaykeeperReactNativeApiError.code`. `0.2.0` passes a value
31
+ through unchanged only when it matches `^[a-z][a-z0-9_]{2,63}$`, including
32
+ codes newer than the installed SDK; anything else, such as a free-form English
33
+ sentence, becomes `daykeeper_request_failed`. The vocabulary stays open, so
34
+ handle unknown codes safely. (An unreleased candidate briefly used a 23-entry
35
+ allowlist; it never shipped.)
36
+ - **Writes are never replayed after HTTP 401.** A write rejected with 401 now
37
+ throws `DaykeeperReactNativeApiError` with status 401 and `retryable: false`.
38
+ The token provider receives `forceRefresh: true` only for a read. Refresh the
39
+ session and let the customer send again.
40
+ - **Token provider failures are wrapped.** An error thrown by `getAccessToken`
41
+ now surfaces as `DaykeeperReactNativeTransportError` with code
42
+ `TOKEN_PROVIDER_ERROR` and `retryable: false`, not as the original error.
43
+ Apps that inspected their own provider errors must track that state
44
+ themselves.
45
+ - **Write errors are never retryable, and uncertain outcomes are explicit.**
46
+ Every write error now has `retryable: false` (`0.1.0` marked 408, 429 and 5xx
47
+ retryable). A write that failed after dispatch, or returned 408 or 5xx, also
48
+ has `outcomeUnknown: true`. Reconcile server state before a deliberate new
49
+ write.
50
+
51
+ This package consumes the **customer** contract only, which remains 0.1.0. The
52
+ newly required `Idempotency-Key` header on flow mutations, and the `200`
53
+ returned alongside `201` for a replayed mutation, are **management** contract
54
+ 0.2.0 changes and do not apply here. The breaking changes above are this
55
+ package's own. See [`COMPATIBILITY.md`](COMPATIBILITY.md).
56
+
57
+ ### Changes
58
+
59
+ - Preserve the API-only inbox contract: widget identity and anonymous-thread
60
+ claim return `409 widget_unavailable` before contacting the provider.
61
+ - Preserve every server error code whose shape is a code. Codes matching
62
+ `^[a-z][a-z0-9_]{2,63}$` pass through unchanged; only values that fail that
63
+ shape collapse to `daykeeper_request_failed`. The previous 23-entry
64
+ allowlist collapsed live gateway codes the consuming app switches on, such
65
+ as `support_gateway_request_failed`, `widget_token_required`,
66
+ `invalid_tenant` and `conversation_not_found`. Message projection is
67
+ unchanged: no raw body text enters the message, stack or serialization.
68
+ - Decide on the HTTP status before parsing an error body. An HTML error page
69
+ from a proxy or CDN no longer discards the status as `INVALID_RESPONSE`, and
70
+ a non-JSON 401 still authorizes the single credential refresh on reads.
71
+ - Accept `http://[::1]` and other bracketed IPv6 loopback base URLs for local
72
+ development. The loopback check compared against an unbracketed `::1` and
73
+ never matched.
74
+ - Expose `DaykeeperReactNativeApiError.nextAction`, projected through a closed
75
+ allowlist of `review_usage`, `review_setup` and `refresh_conversation` and
76
+ included in `toJSON` when present. Unrecognized values become `undefined`.
77
+ The contract's `message` field remains unread.
78
+ - Verify the vendored contract's provenance in `check:generated`. The new
79
+ `scripts/check-generated.mjs` recomputes the SHA-256 and Git blob id of
80
+ `openapi/customer.yaml`, requires both plus the upstream commit to appear in
81
+ `openapi/SOURCE.md`, and regenerates types into a temporary directory, so a
82
+ contract edit can no longer pass by regenerating alongside it.
83
+ - Vendor the customer contract from immutable `daykeeper-openapi` tag
84
+ `v1.1.0` (commit `c9a0175`), where `CustomerError` is
85
+ `additionalProperties: true` and the `error` code is documented as extensible.
86
+
87
+ - Set `cache: "no-store"` on every dispatch. Native exports also send
88
+ `Cache-Control: no-cache, no-store` for transports that ignore Fetch cache mode.
89
+ Keep browser cache headers under standard Fetch control to avoid changing CORS.
90
+ - Add seeded-cache account-switch, revoked-credential, freshness and no-new-storage
91
+ probes against real HTTP, with mandatory native positive cache controls.
92
+
93
+ - Set `redirect: "error"` and `credentials: "omit"` on every SDK request,
94
+ including credential refresh. Node/web standard Fetch remains the default.
95
+ - Verify native and Node ESM/CJS exports, declarations and installed-package
96
+ policy behavior independently. Android/release certification remains open.
97
+
98
+ The following changes were developed in the candidate previously numbered
99
+ `0.1.1` and ship as part of `0.2.0`; `0.1.0` remains the published baseline
100
+ until release approval.
101
+
102
+ - Never replay writes after authentication failure; classify uncertain write
103
+ outcomes explicitly and make all write errors non-retryable.
104
+ - Honor first-read HTTP 401 `retryable: false` before credential refresh, within
105
+ the original deadline and response size limit.
106
+ - Keep raw error bodies out of messages, stacks and serialization. Treat
107
+ failed native body reads as transport failures.
108
+ - Respect explicit server retry advice for customer API errors, including
109
+ non-retryable quota ceilings; preserve older status-only read classification.
110
+ - Align vendored OpenAPI license metadata and record the exact source commit
111
+ and checksum; generated types and runtime behavior are unchanged.
112
+ - Classify credential-provider failures as non-retryable `TOKEN_PROVIDER_ERROR`
113
+ without exposing raw provider errors; timeout and cancellation stay distinct.
114
+ - Bound credential acquisition, authentication refresh, transport, and buffered
115
+ or streaming response reads by one cancellable request deadline.
116
+ - Prevent late dispatch, release failed response bodies without waiting for
117
+ cleanup, and sanitize credential-provider failures.
118
+ - Make public SDK and generated contract documentation
119
+ infrastructure-provider neutral.
120
+ - Ship a `NOTICE` file recording the Apache-2.0 contract the generated types
121
+ come from, with the full Apache-2.0 text as `LICENSE-APACHE-2.0`, and list
122
+ supported versions and a private reporting channel in `SECURITY.md`.
123
+
3
124
  ## 0.1.0
4
125
 
5
126
  - Add typed customer identity, conversation, message, unread, and seen APIs.
package/COMPATIBILITY.md CHANGED
@@ -1,17 +1,52 @@
1
1
  # React Native compatibility
2
2
 
3
- The initial supported target is Expo 57 with React Native 0.86 and Hermes. The
3
+ ## Version matrix
4
+
5
+ | Package version | Contract consumed | Contract version | Contract tag | Contract commit |
6
+ | ------------------- | ------------------------------------- | ---------------- | ----------------- | ------------------------------------------ |
7
+ | `0.2.0` | customer (`openapi/customer.yaml`) | 0.1.0 | `v1.1.0` | `c9a0175d0053f1a2d57c9329f6d3a36ec6acdb71` |
8
+ | `0.1.0` (published) | customer (`openapi/customer.yaml`) | 0.1.0 | `v1.0.0` | `35f5bd45fe0c6a6901766543bff90dae6838b965` |
9
+ | any | management (`openapi/daykeeper.yaml`) | 0.2.0 | not consumed here | not consumed here |
10
+
11
+ This package consumes only the **customer** contract. The customer contract
12
+ snapshot is pinned to immutable repository tag `v1.1.0`; its OpenAPI `info.version`
13
+ is `0.1.0`. It has no `Idempotency-Key`
14
+ header. The now-required
15
+ `Idempotency-Key` header on flow mutations, and the `200`-alongside-`201`
16
+ replay response, are **management** contract 0.2.0 changes and do **not** apply
17
+ to this package. The management contract is consumed by `daykeeper-node`,
18
+ `daykeeper-mcp` and `daykeeper-cli`, not here. The 0.2.0 break in this package
19
+ is its own: the native transport policy change and error-code passthrough
20
+ described below.
21
+
22
+ Every release records the exact customer contract tag and commit used; see
23
+ [`openapi/SOURCE.md`](openapi/SOURCE.md) and [`RELEASING.md`](RELEASING.md).
24
+
25
+ ## Runtime
26
+
27
+ The initial validation target is Expo 57 with React Native 0.86 and Hermes. The
4
28
  package declares React Native 0.79 or newer because Metro enables package
5
29
  exports by default from that line onward. Releases must be smoke-tested through
6
30
  Metro on both iOS and Android before this matrix is expanded.
7
31
 
8
- The client depends only on the React Native web-compatible globals `fetch`,
9
- `Headers`, `URL`, and `AbortController`. It handles both standards-based
32
+ The client uses an explicitly configured native Fetch transport plus the
33
+ web-compatible globals `Headers`, `URL`, and `AbortController`. It handles both standards-based
10
34
  streaming responses and React Native's buffered `Response.text()` response. A
11
35
  custom `fetch` implementation must preserve those request and response
12
- semantics.
36
+ semantics and enforce `redirect: "error"` plus `credentials: "omit"` before
37
+ forwarding anything. It must also honor `Cache-Control: no-cache, no-store`;
38
+ the native export adds this because Expo 57 ignores Fetch's `cache` option.
39
+ All exports set `cache: "no-store"`. Native entry types require `fetch`; runtime validation
40
+ rejects an omitted transport before credentials are requested. Standard
41
+ Node/web Fetch remains the non-native default. Expo is not an SDK dependency.
42
+
43
+ Writes never automatically replay, including after 401. Read credential refresh
44
+ honors explicit non-retryable advice before a second dispatch. An uncertain
45
+ write result is marked `outcomeUnknown: true` and `retryable: false`; reconcile
46
+ server state before a deliberate retry. This is not server-side idempotency.
47
+ See the recovery contract in the README.
13
48
 
14
- Version 0.1 is headless and ships no native module. It does not yet provide a
49
+ The SDK is headless and ships no native module. It does not yet provide a
15
50
  messenger UI, push-notification registration, attachment selection/upload,
16
51
  deep-link routing, persistent offline queues, or background delivery. Consumers
17
52
  own those surfaces until they are added here behind reviewed native interfaces.
@@ -19,3 +54,54 @@ own those surfaces until they are added here behind reviewed native interfaces.
19
54
  Tokens must be short-lived and customer-scoped. Keep them in memory where
20
55
  possible; if persistence is unavoidable, use an OS-backed secret store and
21
56
  never AsyncStorage.
57
+
58
+ ## Error codes: open vocabulary
59
+
60
+ The gateway's error vocabulary is open and grows without an SDK release, so
61
+ the SDK does not keep a list of known codes. Any `error` value shaped like a
62
+ code -- ASCII lowercase snake_case, 3 to 64 characters, matching
63
+ `^[a-z][a-z0-9_]{2,63}$` -- reaches the caller as
64
+ `DaykeeperReactNativeApiError.code` unchanged, including codes newer than the
65
+ installed SDK. Anything else, including the free-form English messages the
66
+ gateway returns for some 4xx failures, collapses to
67
+ `daykeeper_request_failed`. The error message is always the code alone: the
68
+ contract's `message` and `nextAction` fields are never projected into the
69
+ message, stack or serialization. Consuming apps must still handle an unknown
70
+ code safely, and must not treat `daykeeper_request_failed` as a specific
71
+ condition.
72
+
73
+ `nextAction` is the exception and stays a closed vocabulary of exactly
74
+ `review_usage`, `review_setup` and `refresh_conversation`, typed as
75
+ `DaykeeperReactNativeNextAction`. A code is only ever compared, so an unknown
76
+ one is inert; a next action is an instruction the app acts on, so an
77
+ unrecognized value is dropped to `undefined` and omitted from serialization
78
+ rather than surfaced. The contract's `message` field is still never read.
79
+
80
+ ## Native network boundary: release gate
81
+
82
+ Candidate 0.2.0 sets strict redirect/cookie policy on every dispatch and refuses
83
+ implicit native transport selection. Supply explicit Expo57 `expo/fetch` on the
84
+ validated host or a custom compliant native implementation. A caller-provided
85
+ function remains trusted to implement those semantics; the SDK cannot repair
86
+ an XHR wrapper that ignores them. Checking `response.url` afterward cannot
87
+ prevent an already-forwarded request.
88
+
89
+ The synthetic two-listener fixture in [`smoke/boundary`](smoke/boundary/README.md)
90
+ measures arrivals, partial bodies, cookies and authorization without retaining
91
+ credential or message values. Direct positive controls must pass before a
92
+ rejected redirect is credited as a boundary. All four writes and one read are
93
+ tested against 301, 302, 303, 307 and 308, within and across origins.
94
+
95
+ On iOS 26.5 / React Native 0.86.2 / Hermes, the XHR-backed Fetch implementation
96
+ followed redirects despite `redirect: "error"`. Explicit `credentials: "omit"`
97
+ did omit a seeded cookie in this runtime. Explicit Expo 57 `expo/fetch` with
98
+ both options rejected all 50 redirects before receiver arrival and omitted the
99
+ cookie; its five direct API controls succeeded. These are observations for that
100
+ specific runtime, not a supported transport migration or Android certification.
101
+
102
+ Before release, verify each consumer's actual transport, rerun packed-package
103
+ tests on iOS and Android plus release builds, and certify standalone bare-native
104
+ transport coverage. Do not silently require Expo or claim that passing Fetch
105
+ flags fixes XHR redirect behavior. Exact gateway
106
+ URLs must not rely on canonicalization redirects. Do not broaden native HTTP
107
+ exceptions for this fixture or use it on signed-in devices.
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 SkyPorch
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/NOTICE ADDED
@@ -0,0 +1,11 @@
1
+ Daykeeper React Native SDK
2
+ Copyright (c) 2026 SkyPorch
3
+
4
+ Licensed under the MIT License. See the LICENSE file.
5
+
6
+ The TypeScript types in this package are generated from the Daykeeper customer
7
+ OpenAPI description, copied from SkyPorch/daykeeper-openapi and licensed under
8
+ the Apache License, Version 2.0. A copy of that license, taken verbatim from
9
+ SkyPorch/daykeeper-openapi, ships in this package as `LICENSE-APACHE-2.0`.
10
+ The source repository keeps that description under `openapi/customer.yaml` and
11
+ records the exact tag and commit in `openapi/SOURCE.md`.
package/README.md CHANGED
@@ -1,28 +1,45 @@
1
1
  # `@skyporch/daykeeper-react-native`
2
2
 
3
3
  The official headless React Native client for customer-facing Daykeeper support
4
- experiences. It is generated from the Daykeeper customer API contract and is
4
+ experiences. Its types come from the Daykeeper customer API contract; its transport is
5
5
  designed for Expo and bare React Native applications on iOS and Android.
6
6
 
7
7
  This package contains no native module and no embedded support UI. It provides
8
8
  typed identity, conversation, message, unread, seen, and anonymous-claim APIs.
9
9
  Push notifications, attachments, deep links, and native UI can be added here
10
10
  without coupling consuming applications to Daykeeper's private platform or to
11
- Chatwoot.
11
+ any infrastructure provider.
12
+
13
+ API-only inbox gateways support customer conversations, messages, unread state,
14
+ and seen markers. They do not provide widget identity or anonymous-conversation
15
+ claim operations: those calls return `409` with `widget_unavailable` before the
16
+ conversation provider is contacted. Use a widget-enabled gateway for those
17
+ operations.
18
+
19
+ This README describes the unreleased `0.2.0` candidate. Production certification
20
+ is still open; a successful Metro build or Node test does not establish native
21
+ parity. See [`COMPATIBILITY.md`](COMPATIBILITY.md) before a release or upgrade.
12
22
 
13
23
  ## Install
14
24
 
25
+ After the candidate is approved and published:
26
+
15
27
  ```sh
16
- npm install @skyporch/daykeeper-react-native
28
+ npm install @skyporch/daykeeper-react-native@0.2.0
17
29
  ```
18
30
 
31
+ Before publication, test with the reviewed packed tarball. An unversioned npm
32
+ install may select the older published SDK, which does not enforce this policy.
33
+
19
34
  ## Use
20
35
 
21
36
  ```ts
22
37
  import { createDaykeeperReactNativeClient } from "@skyporch/daykeeper-react-native";
38
+ import { fetch as expoFetch } from "expo/fetch";
23
39
 
24
40
  const daykeeper = createDaykeeperReactNativeClient({
25
41
  baseUrl: "https://support.example.com/support-api",
42
+ fetch: expoFetch,
26
43
  getAccessToken: async ({ forceRefresh }) => {
27
44
  // Exchange the signed-in app session for a short-lived, customer-scoped
28
45
  // Daykeeper token using your own backend. Bypass any local token cache when
@@ -34,9 +51,29 @@ const daykeeper = createDaykeeperReactNativeClient({
34
51
  const { conversations } = await daykeeper.listConversations();
35
52
  ```
36
53
 
54
+ The native export requires an explicit Fetch implementation. This example uses
55
+ the host's existing Expo 57 installation; the SDK does not install Expo. Bare
56
+ React Native applications may supply their own native Fetch implementation,
57
+ but it must reject redirects before following them, omit ambient cookies,
58
+ honor request Cache-Control directives, and support cancellation. The XHR-backed React Native global is not compliant;
59
+ wrapping it or setting Fetch flags does not repair its redirect behavior. Do not
60
+ pass it as the native transport. Omitting `fetch` fails before token acquisition.
61
+
62
+ Every request sets `redirect: "error"`, `credentials: "omit"`, and `cache: "no-store"`.
63
+ The native export also sends `Cache-Control: no-cache, no-store`, because the
64
+ validated Expo runtime ignores the standard Fetch cache option. This revalidates
65
+ existing cache entries and prevents new storage, including across account
66
+ changes. Node/web uses standard Fetch cache semantics without adding a custom
67
+ browser request header. Keep the API's response `Cache-Control: no-store` too.
68
+ A custom Fetch
69
+ function is a caller-owned trust boundary: the SDK cannot prevent an injected
70
+ implementation from ignoring those options. Use an exact HTTPS gateway URL,
71
+ not one that redirects. Node/web exports can use standard global Fetch.
72
+
37
73
  The token provider runs for every request so the consuming app can rotate
38
- short-lived credentials. If the gateway returns HTTP 401, the SDK asks the
39
- provider for one forced refresh and retries exactly once. Keep customer tokens
74
+ short-lived credentials. For a GET returning HTTP 401 without an explicit
75
+ `retryable: false`, the SDK asks the provider for one forced refresh and retries
76
+ the read once. Writes never refresh and replay automatically. Keep customer tokens
40
77
  in memory where possible. Never place them in URLs, analytics, crash reports,
41
78
  or application logs.
42
79
 
@@ -51,8 +88,69 @@ or application logs.
51
88
  See [`COMPATIBILITY.md`](COMPATIBILITY.md) for the supported runtime contract
52
89
  and release certification matrix.
53
90
 
91
+ ## Deadlines and cancellation
92
+
93
+ `timeoutMs` defaults to 30 seconds (allowed range: 1–60 seconds). One budget
94
+ covers token acquisition, both authentication attempts, and streaming or
95
+ buffered response reads. Token providers receive an optional `signal` alongside
96
+ `forceRefresh`; pass it to your backend exchange to cancel that work too.
97
+
98
+ Every customer API method accepts `{ signal }` request options. Use a caller
99
+ `AbortController` when a screen unmounts or the signed-in identity changes.
100
+ Pre-aborted calls do not invoke credentials or send a request. A late token
101
+ cannot start a request after cancellation or timeout.
102
+
103
+ Errors preserve `REQUEST_ABORTED` or `REQUEST_TIMEOUT` even if a credential
104
+ provider or native response stalls. Cancellation does not undo a message or
105
+ conversation the server already accepted. Reconcile history before retrying a
106
+ write with an uncertain outcome; network failures and timeouts are not
107
+ automatically replayed.
108
+
109
+ Credential-provider failures use the non-retryable `TOKEN_PROVIDER_ERROR`
110
+ code, distinct from network failures. Raw provider errors are not exposed.
111
+ Handle sign-in recovery in the token provider or the application; deadline and
112
+ caller cancellation still use their dedicated error codes.
113
+
114
+ ## Safe recovery
115
+
116
+ All four writes (`createConversation`, `sendMessage`, `markConversationSeen`, and
117
+ `claimAnonymousConversation`) dispatch at most once per call, including after
118
+ HTTP 401. Their errors always have `retryable: false`, even when a server hint
119
+ says otherwise. Refresh credentials in your app before a deliberate new write;
120
+ do not wrap writes in a generic retry loop.
121
+
122
+ Both exported error classes include `outcomeUnknown` in their properties and
123
+ `toJSON()` output. It is true when a write may have reached the server but the
124
+ SDK cannot confirm its result: a timeout, cancellation, transport/body failure
125
+ after dispatch, or HTTP 408/5xx. Preserve the draft and read conversation history
126
+ before offering another send. Cancellation cannot roll back an accepted write.
127
+ `outcomeUnknown: false` is not an exactly-once guarantee or proof of no side
128
+ effects. The customer API has no SDK-supported idempotency-key contract yet.
129
+ The SDK's dispatch bound is not a wire-level guarantee: a native HTTP stack may
130
+ retry internally (observed for a dropped GET on iOS). Never treat this SDK as
131
+ an exactly-once delivery mechanism.
132
+
133
+ For reads, `DaykeeperReactNativeApiError.retryable` respects an explicit server
134
+ boolean; older responses fall back to status-based classification (408, 429,
135
+ and 5xx). No automatic backoff or rate-limit retry is performed. Only a first
136
+ read 401 without an explicit false hint permits one credential refresh. A
137
+ stalled, oversized, cancelled, or failed 401 body does not permit that refresh;
138
+ a completed legacy empty/non-JSON 401 does.
139
+
140
+ A usage ceiling can return HTTP 429 with `retryable: false`. Show suitable local
141
+ copy for its stable code and let a workspace administrator review usage.
142
+ Server error codes matching `^[a-z][a-z0-9_]{2,63}$` are preserved in SDK
143
+ errors, including codes introduced after this release. Handle unfamiliar codes
144
+ with a generic recovery message. Malformed codes become
145
+ `daykeeper_request_failed`. Raw server messages, next-action URLs, and
146
+ credential/transport errors are not copied.
147
+
148
+ On logout or account change, abort outstanding calls, clear customer UI/history
149
+ and in-memory token caches, and ignore results belonging to the old session.
150
+ The headless SDK does not own the app's login state or persistent storage.
151
+
54
152
  ## Release status
55
153
 
56
- Version `0.1.0` is generated from the customer contract recorded in
154
+ Candidate `0.2.0` is generated from the customer contract recorded in
57
155
  [`openapi/SOURCE.md`](openapi/SOURCE.md). Releases use the protected,
58
156
  provenance-producing process in [`RELEASING.md`](RELEASING.md).