@tileflow/react-native 0.1.0-alpha.7 → 0.1.0-alpha.8

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @tileflow/react-native
2
2
 
3
- `@tileflow/react-native` provides a Tileflow `Map` backed by MapLibre Native on iOS and Android. It is pre-release. The first alpha release requires the bootstrap and Trusted Publisher checks in the [publishing guide](https://github.com/tileflow/tileflow-sdk/blob/main/PUBLISHING.md).
3
+ `@tileflow/react-native` provides a Tileflow `Map` backed by MapLibre Native on iOS and Android. It is pre-release. Publication requires the repository’s protected release workflow.
4
4
 
5
5
  The package targets exactly React 19.2.0, React Native 0.83.10 and `@maplibre/maplibre-react-native` 11.5.0, with MapLibre Native Android 13.6.1 and iOS 6.31.0 through the pinned integration below. Expo consumers use Expo 55.0.31 with a native development build; Expo Go does not contain the required native modules.
6
6
 
@@ -99,7 +99,7 @@ Theme and `timeOfDay` changes cross-fade over 450 ms by default: a snapshot of t
99
99
  <Map source={source} themeBlend={{themes: ['night', 'dusk', 'day'], position}} style={{flex: 1}} />
100
100
  ```
101
101
 
102
- The first blend of a set of themes prepares every theme's style and replaces the native style once; moving `position` later changes the map without loading. Removing `themeBlend` returns to `theme`. Both keep the same native Map, context and session, and `ready` events wait for the fade. The device's reduce-motion setting changes at once. See [theme transitions and blends](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/theme-transitions-and-blends.md#react-native) for what follows the position on native maps.
102
+ The first blend of a set of themes prepares every theme's style and replaces the native style once; moving `position` later changes the map without loading. Removing `themeBlend` returns to `theme`. Both keep the same native Map, context and session, and `ready` events wait for the fade. The device's reduce-motion setting changes at once. See [theme transitions and blends](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/README.md) for what follows the position on native maps.
103
103
 
104
104
  ## Boundary choropleths
105
105
 
@@ -132,8 +132,7 @@ export function UnemploymentMap({
132
132
 
133
133
  Values survive theme changes: they are set again once the new style has loaded. `values: null`
134
134
  removes them. Rows keyed by codes the polygons do not carry are joined before they reach the app,
135
- on a server or at build time; see
136
- [boundary areas and choropleths](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/boundary-areas-and-choropleths.md#join-by-codes-the-polygons-do-not-carry).
135
+ on a server or at build time.
137
136
 
138
137
  ## Camera
139
138
 
@@ -178,16 +177,12 @@ Relative URLs are resolved with Core's native URL policy. Protected resources ar
178
177
 
179
178
  Glyph templates use a finite set of exact font-stack expansions. A later theme may extend that same template only through an acknowledged append-only finite union; it cannot change the resource scope, origin or identity.
180
179
 
181
- See [Private native resource admission](./docs/native-admission.md) for the transport and security boundaries.
182
-
183
180
  ## Hosted mobile application configuration
184
181
 
185
182
  Hosted native session delivery uses a scoped, revocable, publishable **mobile application credential** configured once in the native application. It authenticates the distributed application, not its end user. The manifest supplies `mapId`, `apiUrl` and `usageMode`; Tileflow requires the manifest API origin to exactly match the configured canonical HTTPS origin before bootstrap.
186
183
 
187
184
  There is intentionally no credential prop, JS setter, Provider, client object, package-name/bundle-ID authentication or attestation path in this version.
188
185
 
189
- See [Native application configuration](./docs/native-configuration.md) for the exact Android resource and iOS Info.plist keys, credential grammar, origin normalization, rotation/revocation guidance and threat model.
190
-
191
186
  ## Map options and presentation
192
187
 
193
188
  The public `mapOptions` object remains a positive allowlist. It includes the currently supported gesture/control options only. Style inputs, request transformation, headers and upstream lifecycle callbacks remain owned by Tileflow. Runtime validation rejects forged extra keys without reflecting their values.
@@ -208,7 +203,7 @@ The public lifecycle surface remains:
208
203
 
209
204
  `getSourceState()` returns only the last safe projected source snapshot. Errors use existing bounded codes/fields/kinds. No URL, response body, native event, credential, grant, session object, native handle or raw cause enters public callbacks, refs or serializable state.
210
205
 
211
- `onError` reports `source-error`, `renderer-error` and, for Hosted session delivery, `session-error`. A `session-error` means Tileflow refused the session: the mobile credential was rejected (`NATIVE_SESSION_CREDENTIAL_REJECTED`), it does not authorize the Map (`NATIVE_SESSION_MAP_MISMATCH`), the Map was not found (`NATIVE_SESSION_MAP_NOT_FOUND`) or its view or spending limit was reached (`NATIVE_SESSION_QUOTA_EXCEEDED`). It can arrive after the first load, when a renewal is refused. The map keeps what it has drawn but loads nothing new until you call `ref.retry()` or change the source:
206
+ `onError` reports `source-error`, `renderer-error` and, for Hosted session delivery, `session-error`. A `session-error` means Tileflow refused the session: the mobile credential was rejected (`NATIVE_SESSION_CREDENTIAL_REJECTED`), it does not authorize the Map (`NATIVE_SESSION_MAP_MISMATCH`), the Map was not found (`NATIVE_SESSION_MAP_NOT_FOUND`), its view or spending limit was reached (`NATIVE_SESSION_QUOTA_EXCEEDED`), or the service refused the session for another reason that trying again cannot change (`NATIVE_SESSION_REJECTED`), such as a Map that does not use session delivery. For `NATIVE_SESSION_REJECTED`, `event.error.reason` is the service's code when it sent one, for example `MOBILE_SESSION_MODE_REQUIRED`; it is a bounded identifier, never a message or response body. A `session-error` can arrive after the first load, when a renewal is refused. The map keeps what it has drawn but loads nothing new until you call `ref.retry()` or change the source:
212
207
 
213
208
  ```tsx
214
209
  import {Map, type MapRef} from '@tileflow/react-native';
@@ -226,6 +221,11 @@ export function StoreMap() {
226
221
  event.error.code === 'NATIVE_SESSION_QUOTA_EXCEEDED'
227
222
  ) {
228
223
  // Show your own notice; call map.current?.retry() once the limit is raised.
224
+ } else if (
225
+ event.type === 'session-error' &&
226
+ event.error.code === 'NATIVE_SESSION_REJECTED'
227
+ ) {
228
+ // event.error.reason, such as 'MOBILE_SESSION_MODE_REQUIRED', names the cause.
229
229
  }
230
230
  }}
231
231
  style={{flex: 1}}
@@ -247,7 +247,7 @@ $MLRN.post_install(installer)
247
247
  tileflow_native_admission_post_install(installer)
248
248
  ```
249
249
 
250
- Do not add a second MapLibre Native CocoaPod. See [Private native resource admission](./docs/native-admission.md) for installation-order and teardown details.
250
+ Do not add a second MapLibre Native CocoaPod.
251
251
 
252
252
  ## Source-checkout harness
253
253
 
@@ -276,8 +276,7 @@ remain in native vector or GeoJSON layers and are selected through semantic rend
276
276
  queries rather than thousands of React marker views.
277
277
 
278
278
  The application owns persistent selection and its presentation. The SDK emits normalized
279
- annotation or POI targets but no popup, callout, tooltip, sheet, panel or modal renderer. See
280
- [Native interaction contract](./docs/native-interactions.md).
279
+ annotation or POI targets but no popup, callout, tooltip, sheet, panel or modal renderer.
281
280
 
282
281
  ## Current boundary
283
282
 
@@ -297,7 +296,3 @@ Source and tests describe contracts; they do not establish that a native build,
297
296
  fix arrives. Applications can compose a validated foreground fix through the existing accessible
298
297
  annotation and controlled-camera contracts while keeping permission, provider lifetime, privacy
299
298
  policy, status UI and recenter controls outside Tileflow.
300
-
301
- See [Application-owned foreground location](./docs/native-location.md) for the package-owned
302
- source-checkout recipe. The recipe is excluded from package exports and packed runtime files and
303
- imports no concrete location provider.
@@ -110,13 +110,19 @@ type MapSourceState = Readonly<{
110
110
  error: MapSourceError;
111
111
  }>;
112
112
  /**
113
- * The Hosted session stopped: its credential, Map or quota was rejected. The map keeps what it has
114
- * drawn and loads nothing new until `ref.retry()` or another source. Service outages and rate
115
- * limits retry on their own and are not reported.
113
+ * The Hosted session stopped: its credential, Map or quota was rejected, or Tileflow refused the
114
+ * session for another reason that trying again will not change (`NATIVE_SESSION_REJECTED`). The map
115
+ * keeps what it has drawn and loads nothing new until `ref.retry()` or another source. Service
116
+ * outages and rate limits retry on their own and are not reported.
116
117
  */
117
118
  type MapSessionError = Readonly<{
118
- code: 'NATIVE_SESSION_CREDENTIAL_REJECTED' | 'NATIVE_SESSION_MAP_MISMATCH' | 'NATIVE_SESSION_MAP_NOT_FOUND' | 'NATIVE_SESSION_QUOTA_EXCEEDED' | 'NATIVE_SESSION_INPUT_INVALID';
119
+ code: 'NATIVE_SESSION_CREDENTIAL_REJECTED' | 'NATIVE_SESSION_MAP_MISMATCH' | 'NATIVE_SESSION_MAP_NOT_FOUND' | 'NATIVE_SESSION_QUOTA_EXCEEDED' | 'NATIVE_SESSION_REJECTED' | 'NATIVE_SESSION_INPUT_INVALID';
119
120
  kind: 'terminal';
121
+ /**
122
+ * For `NATIVE_SESSION_REJECTED`, the service's code when its answer carried one, such as
123
+ * `MOBILE_SESSION_MODE_REQUIRED`. A bounded identifier, never a message or response body.
124
+ */
125
+ reason?: string;
120
126
  }>;
121
127
  type MapRef = Readonly<{
122
128
  /** A synchronous read of the last source snapshot. Does not trigger loading or native commands. */
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { ReactElement } from 'react';
2
2
  import { TileflowAnnotation } from '@tileflow/interactions';
3
3
  export { TileflowAnnotation, TileflowInteractionBinding, TileflowInteractionCoordinate, TileflowInteractionDiagnostic, TileflowPoiCategory, TileflowResolvedAnnotationTarget, TileflowResolvedPoiFeatureTarget } from '@tileflow/interactions';
4
- import { d as MapProps } from './contract-BaK24QZD.js';
5
- export { e as MapBaseProps, f as MapBoundaryFeatureState, c as MapCameraProps, g as MapColorScheme, h as MapErrorEvent, i as MapEventHandlers, b as MapInitialViewInputs, j as MapInteractionEvent, k as MapInteractionProps, l as MapLoadEvent, m as MapMarkerRenderContext, n as MapMarkerRenderer, o as MapOptions, p as MapPresentationProps, q as MapReadinessChangeEvent, r as MapRef, s as MapSelection, t as MapSessionError, u as MapSource, v as MapSourceError, w as MapSourceProps, x as MapSourceState, y as MapTheme, z as MapThemeBlend, A as MapThemeChangeEvent, B as MapThemeSelection, C as MapThemeTransition, M as MapView, a as MapViewChangeEvent } from './contract-BaK24QZD.js';
4
+ import { d as MapProps } from './contract-BWT1jxqg.js';
5
+ export { e as MapBaseProps, f as MapBoundaryFeatureState, c as MapCameraProps, g as MapColorScheme, h as MapErrorEvent, i as MapEventHandlers, b as MapInitialViewInputs, j as MapInteractionEvent, k as MapInteractionProps, l as MapLoadEvent, m as MapMarkerRenderContext, n as MapMarkerRenderer, o as MapOptions, p as MapPresentationProps, q as MapReadinessChangeEvent, r as MapRef, s as MapSelection, t as MapSessionError, u as MapSource, v as MapSourceError, w as MapSourceProps, x as MapSourceState, y as MapTheme, z as MapThemeBlend, A as MapThemeChangeEvent, B as MapThemeSelection, C as MapThemeTransition, M as MapView, a as MapViewChangeEvent } from './contract-BWT1jxqg.js';
6
6
  import '@maplibre/maplibre-react-native';
7
7
  import 'react-native';
8
8
  import '@tileflow/core/native';
package/dist/index.js CHANGED
@@ -1965,6 +1965,8 @@ var RETRY_LIMIT_MS = 6e4;
1965
1965
  var RETRY_AFTER_LIMIT_MS = 15 * 60 * 1e3;
1966
1966
  var RETRY_AFTER_SECONDS = /^\d{1,10}$/u;
1967
1967
  var QUOTA_CODES = /* @__PURE__ */ new Set(["COMMERCIAL_MAP_LIMIT_REACHED", "COMMERCIAL_SPENDING_LIMIT_REACHED"]);
1968
+ var SERVICE_CODE = /^[A-Z][A-Z0-9_]{0,63}$/u;
1969
+ var SERVICE_CODE_BODY_LIMIT = 4096;
1968
1970
  var MOBILE_CREDENTIAL = /^tf_public_[0-9a-f]{48}$/u;
1969
1971
  var MAP_ID = /^map_[A-Za-z0-9_-]{16}$/u;
1970
1972
  var CREDENTIAL_ID = /^key_[A-Za-z0-9_-]{8,80}$/u;
@@ -2008,7 +2010,8 @@ var TERMINAL_CODES = /* @__PURE__ */ new Set([
2008
2010
  "NATIVE_SESSION_CREDENTIAL_REJECTED",
2009
2011
  "NATIVE_SESSION_MAP_MISMATCH",
2010
2012
  "NATIVE_SESSION_MAP_NOT_FOUND",
2011
- "NATIVE_SESSION_QUOTA_EXCEEDED"
2013
+ "NATIVE_SESSION_QUOTA_EXCEEDED",
2014
+ "NATIVE_SESSION_REJECTED"
2012
2015
  ]);
2013
2016
  var ERROR_MESSAGES = {
2014
2017
  NATIVE_SESSION_INPUT_INVALID: "Native session configuration is invalid.",
@@ -2027,13 +2030,16 @@ var ERROR_MESSAGES = {
2027
2030
  NATIVE_SESSION_DISPOSED: "Native session controller is disposed."
2028
2031
  };
2029
2032
  var HostedNativeSessionError = class extends Error {
2030
- constructor(code) {
2033
+ constructor(code, reason) {
2031
2034
  super(ERROR_MESSAGES[code]);
2032
2035
  __publicField(this, "code");
2033
2036
  __publicField(this, "kind");
2034
2037
  this.name = "HostedNativeSessionError";
2035
2038
  this.code = code;
2036
2039
  this.kind = errorKind(code);
2040
+ if (code === "NATIVE_SESSION_REJECTED" && typeof reason === "string" && SERVICE_CODE.test(reason)) {
2041
+ this.reason = reason;
2042
+ }
2037
2043
  }
2038
2044
  };
2039
2045
  function createHostedNativeSessionController(input) {
@@ -2162,10 +2168,10 @@ function createHostedNativeSessionController(input) {
2162
2168
  if (retryAt === null || at >= retryAt) failures = Math.min(failures + 1, 16);
2163
2169
  const backoff = Math.min(RETRY_INITIAL_MS * 2 ** (failures - 1), RETRY_LIMIT_MS);
2164
2170
  retryAt = Math.max(retryAt ?? at, at + Math.max(backoff, retryAfterDelays.get(error) ?? 0));
2165
- retryError = Object.freeze({ code: error.code, kind: error.kind });
2171
+ retryError = safeError(error);
2166
2172
  }
2167
2173
  function rotateIfRequired(at) {
2168
- if (terminal) throw new HostedNativeSessionError(terminal.code);
2174
+ if (terminal) throw errorFrom(terminal);
2169
2175
  if (!activeSession || binding.kind !== "hosted") return;
2170
2176
  if (at - activeSession.startedAt < ROTATION_AGE_MS && activeSession.requestCount < ROTATION_REQUEST_LIMIT) {
2171
2177
  return;
@@ -2199,7 +2205,7 @@ function createHostedNativeSessionController(input) {
2199
2205
  return current;
2200
2206
  }
2201
2207
  async function ensureAuthority(session, at) {
2202
- if (terminal) throw new HostedNativeSessionError(terminal.code);
2208
+ if (terminal) throw errorFrom(terminal);
2203
2209
  if (session.authority && session.authority.clockEpoch === clock.epoch && session.authority.validUntil - at > REFRESH_MARGIN_MS) {
2204
2210
  return { authority: session.authority, session };
2205
2211
  }
@@ -2211,7 +2217,7 @@ function createHostedNativeSessionController(input) {
2211
2217
  if (session.authority && session.authority.clockEpoch === clock.epoch && session.authority.validUntil > at) {
2212
2218
  return { authority: session.authority, session };
2213
2219
  }
2214
- throw new HostedNativeSessionError(retryError.code);
2220
+ throw errorFrom(retryError);
2215
2221
  }
2216
2222
  const operationBinding = binding;
2217
2223
  const abort = new SessionAbortController();
@@ -2238,14 +2244,16 @@ function createHostedNativeSessionController(input) {
2238
2244
  const normalized = abort.signal.aborted ? abort.error() : normalizeError(error);
2239
2245
  const target = latestSession(session);
2240
2246
  if (normalized.kind === "terminal" && binding === operationBinding && !terminal) {
2241
- const failure3 = Object.freeze({ code: normalized.code, kind: normalized.kind });
2247
+ const failure3 = safeError(normalized);
2242
2248
  terminal = failure3;
2243
2249
  const rejectedSession = activeSession;
2244
2250
  if (rejectedSession) {
2245
2251
  rejectedSession.authority = null;
2246
2252
  rejectedSession.error = failure3;
2247
2253
  }
2248
- for (const pending of [...operations]) pending.abort(failure3.code);
2254
+ for (const pending of [...operations]) {
2255
+ pending.abort(failure3.code, failure3.reason);
2256
+ }
2249
2257
  if (binding === operationBinding && rejectedSession) {
2250
2258
  publishSession(rejectedSession, "error", failure3);
2251
2259
  }
@@ -2254,7 +2262,7 @@ function createHostedNativeSessionController(input) {
2254
2262
  delayRetry(normalized);
2255
2263
  }
2256
2264
  if (normalized.code !== "NATIVE_SESSION_REPLACED" && normalized.code !== "NATIVE_SESSION_DISPOSED") {
2257
- target.error = Object.freeze({ code: normalized.code, kind: normalized.kind });
2265
+ target.error = safeError(normalized);
2258
2266
  publishSession(target, "error", target.error);
2259
2267
  }
2260
2268
  throw normalized;
@@ -2280,7 +2288,7 @@ function createHostedNativeSessionController(input) {
2280
2288
  if (abort.signal.aborted) throw abort.error();
2281
2289
  const expectedSurfaceId = session.authority?.public.surfaceId ?? session.requestedSurfaceId;
2282
2290
  const response = await fetchResponse(hostedBinding, session, expectedSurfaceId, abort);
2283
- const source = await readBoundedResponse(response, abort);
2291
+ const { source, size } = await readBoundedResponse(response, abort);
2284
2292
  let body;
2285
2293
  try {
2286
2294
  body = source.length ? JSON.parse(source) : null;
@@ -2290,14 +2298,15 @@ function createHostedNativeSessionController(input) {
2290
2298
  }
2291
2299
  body = null;
2292
2300
  }
2293
- if (response.status === 409 && allowRestart && isExactRestart(body, session.sessionId)) {
2301
+ if (response.status === 409 && isExactRestart(body, session.sessionId)) {
2302
+ if (!allowRestart) throw new HostedNativeSessionError("NATIVE_SESSION_UNAVAILABLE");
2294
2303
  assertCurrentSession(session, hostedBinding);
2295
2304
  const replacement = redirectSession(session, readClock());
2296
2305
  if (abort.signal.aborted) throw abort.error();
2297
2306
  return bootstrap(replacement, hostedBinding, abort, false, false);
2298
2307
  }
2299
2308
  if (response.status !== 201) {
2300
- const failure3 = statusError(response.status, body);
2309
+ const failure3 = statusError(response.status, body, size);
2301
2310
  const delay = retryAfterMs(response.headers, now);
2302
2311
  if (delay !== null) retryAfterDelays.set(failure3, delay);
2303
2312
  throw failure3;
@@ -2435,7 +2444,7 @@ function createHostedNativeSessionController(input) {
2435
2444
  if (bindingGeneration !== expectedGeneration) {
2436
2445
  throw new HostedNativeSessionError("NATIVE_SESSION_REPLACED");
2437
2446
  }
2438
- if (terminal) throw new HostedNativeSessionError(terminal.code);
2447
+ if (terminal) throw errorFrom(terminal);
2439
2448
  checkedAt = readClock();
2440
2449
  if (resolved.authority.clockEpoch === clock.epoch && resolved.authority.validUntil > checkedAt) {
2441
2450
  admittedAuthorities.set(resolved.authority.public, resolved.authority);
@@ -2510,7 +2519,7 @@ function createHostedNativeSessionController(input) {
2510
2519
  if (disposed) throw new HostedNativeSessionError("NATIVE_SESSION_DISPOSED");
2511
2520
  lifecycle = "foreground";
2512
2521
  if (terminal) {
2513
- const error = new HostedNativeSessionError(terminal.code);
2522
+ const error = errorFrom(terminal);
2514
2523
  publish(currentSnapshot());
2515
2524
  throw error;
2516
2525
  }
@@ -2783,7 +2792,7 @@ async function readBoundedResponse(response, abort) {
2783
2792
  bytes.set(chunk, offset);
2784
2793
  offset += chunk.byteLength;
2785
2794
  }
2786
- return decodeUtf8(bytes);
2795
+ return { source: decodeUtf8(bytes), size };
2787
2796
  }
2788
2797
  function decodeUtf8(bytes) {
2789
2798
  let result = "";
@@ -2831,7 +2840,7 @@ function decodeUtf8(bytes) {
2831
2840
  function hasNoStore(value) {
2832
2841
  return typeof value === "string" && value.split(",").some((part) => part.trim().toLowerCase() === "no-store");
2833
2842
  }
2834
- function statusError(status, body) {
2843
+ function statusError(status, body, size) {
2835
2844
  if (status === 401) return new HostedNativeSessionError("NATIVE_SESSION_CREDENTIAL_REJECTED");
2836
2845
  if (status === 403) return new HostedNativeSessionError("NATIVE_SESSION_MAP_MISMATCH");
2837
2846
  if (status === 404) return new HostedNativeSessionError("NATIVE_SESSION_MAP_NOT_FOUND");
@@ -2843,7 +2852,21 @@ function statusError(status, body) {
2843
2852
  if (status === 408 || status >= 500 && status <= 599) {
2844
2853
  return new HostedNativeSessionError("NATIVE_SESSION_UNAVAILABLE");
2845
2854
  }
2846
- return new HostedNativeSessionError("NATIVE_SESSION_REJECTED");
2855
+ if (status >= 400 && status <= 499) {
2856
+ return new HostedNativeSessionError(
2857
+ "NATIVE_SESSION_REJECTED",
2858
+ size <= SERVICE_CODE_BODY_LIMIT && isRecord(body) && typeof body.code === "string" ? body.code : void 0
2859
+ );
2860
+ }
2861
+ return new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_INVALID");
2862
+ }
2863
+ function safeError(error) {
2864
+ return Object.freeze(
2865
+ error.reason === void 0 ? { code: error.code, kind: error.kind } : { code: error.code, kind: error.kind, reason: error.reason }
2866
+ );
2867
+ }
2868
+ function errorFrom(error) {
2869
+ return new HostedNativeSessionError(error.code, error.reason);
2847
2870
  }
2848
2871
  var retryAfterDelays = /* @__PURE__ */ new WeakMap();
2849
2872
  function retryAfterMs(headers, now) {
@@ -2893,12 +2916,13 @@ async function cancelResponse(response) {
2893
2916
  } catch {
2894
2917
  }
2895
2918
  }
2896
- var _aborted, _code, _listeners;
2919
+ var _aborted, _code, _reason, _listeners;
2897
2920
  var SessionAbortController = class {
2898
2921
  constructor() {
2899
2922
  __publicField(this, "signal");
2900
2923
  __privateAdd(this, _aborted, false);
2901
2924
  __privateAdd(this, _code, "NATIVE_SESSION_REPLACED");
2925
+ __privateAdd(this, _reason);
2902
2926
  __privateAdd(this, _listeners, /* @__PURE__ */ new Set());
2903
2927
  const isAborted = () => __privateGet(this, _aborted);
2904
2928
  this.signal = Object.freeze({
@@ -2913,9 +2937,10 @@ var SessionAbortController = class {
2913
2937
  }
2914
2938
  });
2915
2939
  }
2916
- abort(code) {
2940
+ abort(code, reason) {
2917
2941
  if (__privateGet(this, _aborted)) return;
2918
2942
  __privateSet(this, _code, code);
2943
+ __privateSet(this, _reason, reason);
2919
2944
  __privateSet(this, _aborted, true);
2920
2945
  for (const listener of [...__privateGet(this, _listeners)]) {
2921
2946
  try {
@@ -2926,11 +2951,12 @@ var SessionAbortController = class {
2926
2951
  __privateGet(this, _listeners).clear();
2927
2952
  }
2928
2953
  error() {
2929
- return new HostedNativeSessionError(__privateGet(this, _code));
2954
+ return new HostedNativeSessionError(__privateGet(this, _code), __privateGet(this, _reason));
2930
2955
  }
2931
2956
  };
2932
2957
  _aborted = new WeakMap();
2933
2958
  _code = new WeakMap();
2959
+ _reason = new WeakMap();
2934
2960
  _listeners = new WeakMap();
2935
2961
  function raceWithAbort(promise, controller) {
2936
2962
  if (controller.signal.aborted) return Promise.reject(controller.error());
@@ -3238,7 +3264,7 @@ function createNativeAdmissionOwner(options) {
3238
3264
  reported = code;
3239
3265
  if (state.status !== "error") return;
3240
3266
  try {
3241
- input.onSessionError?.(Object.freeze({ code: state.error.code, kind: state.error.kind }));
3267
+ input.onSessionError?.(Object.freeze({ ...state.error }));
3242
3268
  } catch {
3243
3269
  }
3244
3270
  });
@@ -5824,8 +5850,10 @@ var sessionErrorCodes = /* @__PURE__ */ new Set([
5824
5850
  "NATIVE_SESSION_MAP_MISMATCH",
5825
5851
  "NATIVE_SESSION_MAP_NOT_FOUND",
5826
5852
  "NATIVE_SESSION_QUOTA_EXCEEDED",
5853
+ "NATIVE_SESSION_REJECTED",
5827
5854
  "NATIVE_SESSION_INPUT_INVALID"
5828
5855
  ]);
5856
+ var SERVICE_CODE2 = /^[A-Z][A-Z0-9_]{0,63}$/u;
5829
5857
  function themeSource(source, name3) {
5830
5858
  const theme = Object.hasOwn(source.map.themes, name3) ? source.map.themes[name3] : void 0;
5831
5859
  if (!theme) throw new NativePreparationError();
@@ -6085,9 +6113,14 @@ function createMountedMapOwner(ports2) {
6085
6113
  reopenings += 1;
6086
6114
  restart(epoch);
6087
6115
  }
6088
- function sessionError(epoch, code) {
6116
+ function sessionError(epoch, code, reason) {
6089
6117
  if (disposed || current !== epoch || !epoch.live || !sessionErrorCodes.has(code)) return;
6090
- const error = Object.freeze({ code, kind: "terminal" });
6118
+ const detail = code === "NATIVE_SESSION_REJECTED" && typeof reason === "string" && SERVICE_CODE2.test(reason) ? { reason } : {};
6119
+ const error = Object.freeze({
6120
+ code,
6121
+ kind: "terminal",
6122
+ ...detail
6123
+ });
6091
6124
  emit({ type: "session-error", generation: generation(), error });
6092
6125
  }
6093
6126
  function createEpoch(developmentOrigin) {
@@ -6125,7 +6158,7 @@ function createMountedMapOwner(ports2) {
6125
6158
  onRetired: () => void Promise.resolve().then(() => reopen(epoch)),
6126
6159
  onSessionError: (error) => {
6127
6160
  if (error.kind !== "terminal") return;
6128
- void Promise.resolve().then(() => sessionError(epoch, error.code));
6161
+ void Promise.resolve().then(() => sessionError(epoch, error.code, error.reason));
6129
6162
  }
6130
6163
  });
6131
6164
  const map = await epoch.lease.ready;
@@ -1,4 +1,4 @@
1
- import { M as MapView, a as MapViewChangeEvent, b as MapInitialViewInputs, c as MapCameraProps } from '../contract-BaK24QZD.js';
1
+ import { M as MapView, a as MapViewChangeEvent, b as MapInitialViewInputs, c as MapCameraProps } from '../contract-BWT1jxqg.js';
2
2
  import '@maplibre/maplibre-react-native';
3
3
  import 'react';
4
4
  import 'react-native';
@@ -1,4 +1,4 @@
1
- import { b as MapInitialViewInputs, M as MapView } from '../contract-BaK24QZD.js';
1
+ import { b as MapInitialViewInputs, M as MapView } from '../contract-BWT1jxqg.js';
2
2
  import '@maplibre/maplibre-react-native';
3
3
  import 'react';
4
4
  import 'react-native';
@@ -11,6 +11,7 @@ type NativeMapAdmissionInput = Omit<SessionInput, 'fetch' | 'sessionIdFactory'>
11
11
  onSessionError?: (error: Readonly<{
12
12
  code: HostedNativeSessionErrorCode;
13
13
  kind: HostedNativeSessionErrorKind;
14
+ reason?: string;
14
15
  }>) => void;
15
16
  }>;
16
17
  type NativeMapAdmission = Readonly<{
@@ -343,6 +343,8 @@ var RETRY_LIMIT_MS = 6e4;
343
343
  var RETRY_AFTER_LIMIT_MS = 15 * 60 * 1e3;
344
344
  var RETRY_AFTER_SECONDS = /^\d{1,10}$/u;
345
345
  var QUOTA_CODES = /* @__PURE__ */ new Set(["COMMERCIAL_MAP_LIMIT_REACHED", "COMMERCIAL_SPENDING_LIMIT_REACHED"]);
346
+ var SERVICE_CODE = /^[A-Z][A-Z0-9_]{0,63}$/u;
347
+ var SERVICE_CODE_BODY_LIMIT = 4096;
346
348
  var MOBILE_CREDENTIAL = /^tf_public_[0-9a-f]{48}$/u;
347
349
  var MAP_ID = /^map_[A-Za-z0-9_-]{16}$/u;
348
350
  var CREDENTIAL_ID = /^key_[A-Za-z0-9_-]{8,80}$/u;
@@ -386,7 +388,8 @@ var TERMINAL_CODES = /* @__PURE__ */ new Set([
386
388
  "NATIVE_SESSION_CREDENTIAL_REJECTED",
387
389
  "NATIVE_SESSION_MAP_MISMATCH",
388
390
  "NATIVE_SESSION_MAP_NOT_FOUND",
389
- "NATIVE_SESSION_QUOTA_EXCEEDED"
391
+ "NATIVE_SESSION_QUOTA_EXCEEDED",
392
+ "NATIVE_SESSION_REJECTED"
390
393
  ]);
391
394
  var ERROR_MESSAGES = {
392
395
  NATIVE_SESSION_INPUT_INVALID: "Native session configuration is invalid.",
@@ -405,13 +408,16 @@ var ERROR_MESSAGES = {
405
408
  NATIVE_SESSION_DISPOSED: "Native session controller is disposed."
406
409
  };
407
410
  var HostedNativeSessionError = class extends Error {
408
- constructor(code) {
411
+ constructor(code, reason) {
409
412
  super(ERROR_MESSAGES[code]);
410
413
  __publicField(this, "code");
411
414
  __publicField(this, "kind");
412
415
  this.name = "HostedNativeSessionError";
413
416
  this.code = code;
414
417
  this.kind = errorKind(code);
418
+ if (code === "NATIVE_SESSION_REJECTED" && typeof reason === "string" && SERVICE_CODE.test(reason)) {
419
+ this.reason = reason;
420
+ }
415
421
  }
416
422
  };
417
423
  function createHostedNativeSessionController(input) {
@@ -540,10 +546,10 @@ function createHostedNativeSessionController(input) {
540
546
  if (retryAt === null || at >= retryAt) failures = Math.min(failures + 1, 16);
541
547
  const backoff = Math.min(RETRY_INITIAL_MS * 2 ** (failures - 1), RETRY_LIMIT_MS);
542
548
  retryAt = Math.max(retryAt ?? at, at + Math.max(backoff, retryAfterDelays.get(error) ?? 0));
543
- retryError = Object.freeze({ code: error.code, kind: error.kind });
549
+ retryError = safeError(error);
544
550
  }
545
551
  function rotateIfRequired(at) {
546
- if (terminal) throw new HostedNativeSessionError(terminal.code);
552
+ if (terminal) throw errorFrom(terminal);
547
553
  if (!activeSession || binding.kind !== "hosted") return;
548
554
  if (at - activeSession.startedAt < ROTATION_AGE_MS && activeSession.requestCount < ROTATION_REQUEST_LIMIT) {
549
555
  return;
@@ -577,7 +583,7 @@ function createHostedNativeSessionController(input) {
577
583
  return current;
578
584
  }
579
585
  async function ensureAuthority(session, at) {
580
- if (terminal) throw new HostedNativeSessionError(terminal.code);
586
+ if (terminal) throw errorFrom(terminal);
581
587
  if (session.authority && session.authority.clockEpoch === clock.epoch && session.authority.validUntil - at > REFRESH_MARGIN_MS) {
582
588
  return { authority: session.authority, session };
583
589
  }
@@ -589,7 +595,7 @@ function createHostedNativeSessionController(input) {
589
595
  if (session.authority && session.authority.clockEpoch === clock.epoch && session.authority.validUntil > at) {
590
596
  return { authority: session.authority, session };
591
597
  }
592
- throw new HostedNativeSessionError(retryError.code);
598
+ throw errorFrom(retryError);
593
599
  }
594
600
  const operationBinding = binding;
595
601
  const abort = new SessionAbortController();
@@ -616,14 +622,16 @@ function createHostedNativeSessionController(input) {
616
622
  const normalized = abort.signal.aborted ? abort.error() : normalizeError(error);
617
623
  const target = latestSession(session);
618
624
  if (normalized.kind === "terminal" && binding === operationBinding && !terminal) {
619
- const failure = Object.freeze({ code: normalized.code, kind: normalized.kind });
625
+ const failure = safeError(normalized);
620
626
  terminal = failure;
621
627
  const rejectedSession = activeSession;
622
628
  if (rejectedSession) {
623
629
  rejectedSession.authority = null;
624
630
  rejectedSession.error = failure;
625
631
  }
626
- for (const pending of [...operations]) pending.abort(failure.code);
632
+ for (const pending of [...operations]) {
633
+ pending.abort(failure.code, failure.reason);
634
+ }
627
635
  if (binding === operationBinding && rejectedSession) {
628
636
  publishSession(rejectedSession, "error", failure);
629
637
  }
@@ -632,7 +640,7 @@ function createHostedNativeSessionController(input) {
632
640
  delayRetry(normalized);
633
641
  }
634
642
  if (normalized.code !== "NATIVE_SESSION_REPLACED" && normalized.code !== "NATIVE_SESSION_DISPOSED") {
635
- target.error = Object.freeze({ code: normalized.code, kind: normalized.kind });
643
+ target.error = safeError(normalized);
636
644
  publishSession(target, "error", target.error);
637
645
  }
638
646
  throw normalized;
@@ -658,7 +666,7 @@ function createHostedNativeSessionController(input) {
658
666
  if (abort.signal.aborted) throw abort.error();
659
667
  const expectedSurfaceId = session.authority?.public.surfaceId ?? session.requestedSurfaceId;
660
668
  const response = await fetchResponse(hostedBinding, session, expectedSurfaceId, abort);
661
- const source = await readBoundedResponse(response, abort);
669
+ const { source, size } = await readBoundedResponse(response, abort);
662
670
  let body;
663
671
  try {
664
672
  body = source.length ? JSON.parse(source) : null;
@@ -668,14 +676,15 @@ function createHostedNativeSessionController(input) {
668
676
  }
669
677
  body = null;
670
678
  }
671
- if (response.status === 409 && allowRestart && isExactRestart(body, session.sessionId)) {
679
+ if (response.status === 409 && isExactRestart(body, session.sessionId)) {
680
+ if (!allowRestart) throw new HostedNativeSessionError("NATIVE_SESSION_UNAVAILABLE");
672
681
  assertCurrentSession(session, hostedBinding);
673
682
  const replacement = redirectSession(session, readClock());
674
683
  if (abort.signal.aborted) throw abort.error();
675
684
  return bootstrap(replacement, hostedBinding, abort, false, false);
676
685
  }
677
686
  if (response.status !== 201) {
678
- const failure = statusError(response.status, body);
687
+ const failure = statusError(response.status, body, size);
679
688
  const delay = retryAfterMs(response.headers, now);
680
689
  if (delay !== null) retryAfterDelays.set(failure, delay);
681
690
  throw failure;
@@ -813,7 +822,7 @@ function createHostedNativeSessionController(input) {
813
822
  if (bindingGeneration !== expectedGeneration) {
814
823
  throw new HostedNativeSessionError("NATIVE_SESSION_REPLACED");
815
824
  }
816
- if (terminal) throw new HostedNativeSessionError(terminal.code);
825
+ if (terminal) throw errorFrom(terminal);
817
826
  checkedAt = readClock();
818
827
  if (resolved.authority.clockEpoch === clock.epoch && resolved.authority.validUntil > checkedAt) {
819
828
  admittedAuthorities.set(resolved.authority.public, resolved.authority);
@@ -888,7 +897,7 @@ function createHostedNativeSessionController(input) {
888
897
  if (disposed) throw new HostedNativeSessionError("NATIVE_SESSION_DISPOSED");
889
898
  lifecycle = "foreground";
890
899
  if (terminal) {
891
- const error = new HostedNativeSessionError(terminal.code);
900
+ const error = errorFrom(terminal);
892
901
  publish(currentSnapshot());
893
902
  throw error;
894
903
  }
@@ -1161,7 +1170,7 @@ async function readBoundedResponse(response, abort) {
1161
1170
  bytes.set(chunk, offset);
1162
1171
  offset += chunk.byteLength;
1163
1172
  }
1164
- return decodeUtf8(bytes);
1173
+ return { source: decodeUtf8(bytes), size };
1165
1174
  }
1166
1175
  function decodeUtf8(bytes) {
1167
1176
  let result = "";
@@ -1209,7 +1218,7 @@ function decodeUtf8(bytes) {
1209
1218
  function hasNoStore(value) {
1210
1219
  return typeof value === "string" && value.split(",").some((part) => part.trim().toLowerCase() === "no-store");
1211
1220
  }
1212
- function statusError(status, body) {
1221
+ function statusError(status, body, size) {
1213
1222
  if (status === 401) return new HostedNativeSessionError("NATIVE_SESSION_CREDENTIAL_REJECTED");
1214
1223
  if (status === 403) return new HostedNativeSessionError("NATIVE_SESSION_MAP_MISMATCH");
1215
1224
  if (status === 404) return new HostedNativeSessionError("NATIVE_SESSION_MAP_NOT_FOUND");
@@ -1221,7 +1230,21 @@ function statusError(status, body) {
1221
1230
  if (status === 408 || status >= 500 && status <= 599) {
1222
1231
  return new HostedNativeSessionError("NATIVE_SESSION_UNAVAILABLE");
1223
1232
  }
1224
- return new HostedNativeSessionError("NATIVE_SESSION_REJECTED");
1233
+ if (status >= 400 && status <= 499) {
1234
+ return new HostedNativeSessionError(
1235
+ "NATIVE_SESSION_REJECTED",
1236
+ size <= SERVICE_CODE_BODY_LIMIT && isRecord(body) && typeof body.code === "string" ? body.code : void 0
1237
+ );
1238
+ }
1239
+ return new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_INVALID");
1240
+ }
1241
+ function safeError(error) {
1242
+ return Object.freeze(
1243
+ error.reason === void 0 ? { code: error.code, kind: error.kind } : { code: error.code, kind: error.kind, reason: error.reason }
1244
+ );
1245
+ }
1246
+ function errorFrom(error) {
1247
+ return new HostedNativeSessionError(error.code, error.reason);
1225
1248
  }
1226
1249
  var retryAfterDelays = /* @__PURE__ */ new WeakMap();
1227
1250
  function retryAfterMs(headers, now) {
@@ -1271,12 +1294,13 @@ async function cancelResponse(response) {
1271
1294
  } catch {
1272
1295
  }
1273
1296
  }
1274
- var _aborted, _code, _listeners;
1297
+ var _aborted, _code, _reason, _listeners;
1275
1298
  var SessionAbortController = class {
1276
1299
  constructor() {
1277
1300
  __publicField(this, "signal");
1278
1301
  __privateAdd(this, _aborted, false);
1279
1302
  __privateAdd(this, _code, "NATIVE_SESSION_REPLACED");
1303
+ __privateAdd(this, _reason);
1280
1304
  __privateAdd(this, _listeners, /* @__PURE__ */ new Set());
1281
1305
  const isAborted = () => __privateGet(this, _aborted);
1282
1306
  this.signal = Object.freeze({
@@ -1291,9 +1315,10 @@ var SessionAbortController = class {
1291
1315
  }
1292
1316
  });
1293
1317
  }
1294
- abort(code) {
1318
+ abort(code, reason) {
1295
1319
  if (__privateGet(this, _aborted)) return;
1296
1320
  __privateSet(this, _code, code);
1321
+ __privateSet(this, _reason, reason);
1297
1322
  __privateSet(this, _aborted, true);
1298
1323
  for (const listener of [...__privateGet(this, _listeners)]) {
1299
1324
  try {
@@ -1304,11 +1329,12 @@ var SessionAbortController = class {
1304
1329
  __privateGet(this, _listeners).clear();
1305
1330
  }
1306
1331
  error() {
1307
- return new HostedNativeSessionError(__privateGet(this, _code));
1332
+ return new HostedNativeSessionError(__privateGet(this, _code), __privateGet(this, _reason));
1308
1333
  }
1309
1334
  };
1310
1335
  _aborted = new WeakMap();
1311
1336
  _code = new WeakMap();
1337
+ _reason = new WeakMap();
1312
1338
  _listeners = new WeakMap();
1313
1339
  function raceWithAbort(promise, controller) {
1314
1340
  if (controller.signal.aborted) return Promise.reject(controller.error());
@@ -1616,7 +1642,7 @@ function createNativeAdmissionOwner(options) {
1616
1642
  reported = code;
1617
1643
  if (state.status !== "error") return;
1618
1644
  try {
1619
- input.onSessionError?.(Object.freeze({ code: state.error.code, kind: state.error.kind }));
1645
+ input.onSessionError?.(Object.freeze({ ...state.error }));
1620
1646
  } catch {
1621
1647
  }
1622
1648
  });
@@ -1,4 +1,4 @@
1
- import { B as MapThemeSelection, g as MapColorScheme } from '../contract-BaK24QZD.js';
1
+ import { B as MapThemeSelection, g as MapColorScheme } from '../contract-BWT1jxqg.js';
2
2
  import '@maplibre/maplibre-react-native';
3
3
  import 'react';
4
4
  import 'react-native';