@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.
@@ -47,7 +47,9 @@ type HostedNativeSessionErrorKind = 'terminal' | 'recoverable' | 'cancelled';
47
47
  declare class HostedNativeSessionError extends Error {
48
48
  readonly code: HostedNativeSessionErrorCode;
49
49
  readonly kind: HostedNativeSessionErrorKind;
50
- constructor(code: HostedNativeSessionErrorCode);
50
+ /** For `NATIVE_SESSION_REJECTED`, the service's code when its answer carried one. */
51
+ readonly reason?: string;
52
+ constructor(code: HostedNativeSessionErrorCode, reason?: string);
51
53
  }
52
54
  type HostedNativeSessionAuthority = Readonly<{
53
55
  grant: string;
@@ -71,6 +73,7 @@ type HostedNativeResourcePolicy = Readonly<Pick<HostedNativeSessionAuthority, 'm
71
73
  type SafeError = Readonly<{
72
74
  code: HostedNativeSessionErrorCode;
73
75
  kind: HostedNativeSessionErrorKind;
76
+ reason?: string;
74
77
  }>;
75
78
  type Lifecycle = 'foreground' | 'background';
76
79
  type HostedNativeSessionState = Readonly<{
@@ -24,6 +24,8 @@ var RETRY_LIMIT_MS = 6e4;
24
24
  var RETRY_AFTER_LIMIT_MS = 15 * 60 * 1e3;
25
25
  var RETRY_AFTER_SECONDS = /^\d{1,10}$/u;
26
26
  var QUOTA_CODES = /* @__PURE__ */ new Set(["COMMERCIAL_MAP_LIMIT_REACHED", "COMMERCIAL_SPENDING_LIMIT_REACHED"]);
27
+ var SERVICE_CODE = /^[A-Z][A-Z0-9_]{0,63}$/u;
28
+ var SERVICE_CODE_BODY_LIMIT = 4096;
27
29
  var MOBILE_CREDENTIAL = /^tf_public_[0-9a-f]{48}$/u;
28
30
  var MAP_ID = /^map_[A-Za-z0-9_-]{16}$/u;
29
31
  var CREDENTIAL_ID = /^key_[A-Za-z0-9_-]{8,80}$/u;
@@ -67,7 +69,8 @@ var TERMINAL_CODES = /* @__PURE__ */ new Set([
67
69
  "NATIVE_SESSION_CREDENTIAL_REJECTED",
68
70
  "NATIVE_SESSION_MAP_MISMATCH",
69
71
  "NATIVE_SESSION_MAP_NOT_FOUND",
70
- "NATIVE_SESSION_QUOTA_EXCEEDED"
72
+ "NATIVE_SESSION_QUOTA_EXCEEDED",
73
+ "NATIVE_SESSION_REJECTED"
71
74
  ]);
72
75
  var ERROR_MESSAGES = {
73
76
  NATIVE_SESSION_INPUT_INVALID: "Native session configuration is invalid.",
@@ -86,13 +89,16 @@ var ERROR_MESSAGES = {
86
89
  NATIVE_SESSION_DISPOSED: "Native session controller is disposed."
87
90
  };
88
91
  var HostedNativeSessionError = class extends Error {
89
- constructor(code) {
92
+ constructor(code, reason) {
90
93
  super(ERROR_MESSAGES[code]);
91
94
  __publicField(this, "code");
92
95
  __publicField(this, "kind");
93
96
  this.name = "HostedNativeSessionError";
94
97
  this.code = code;
95
98
  this.kind = errorKind(code);
99
+ if (code === "NATIVE_SESSION_REJECTED" && typeof reason === "string" && SERVICE_CODE.test(reason)) {
100
+ this.reason = reason;
101
+ }
96
102
  }
97
103
  };
98
104
  function createHostedNativeSessionController(input) {
@@ -221,10 +227,10 @@ function createHostedNativeSessionController(input) {
221
227
  if (retryAt === null || at >= retryAt) failures = Math.min(failures + 1, 16);
222
228
  const backoff = Math.min(RETRY_INITIAL_MS * 2 ** (failures - 1), RETRY_LIMIT_MS);
223
229
  retryAt = Math.max(retryAt ?? at, at + Math.max(backoff, retryAfterDelays.get(error) ?? 0));
224
- retryError = Object.freeze({ code: error.code, kind: error.kind });
230
+ retryError = safeError(error);
225
231
  }
226
232
  function rotateIfRequired(at) {
227
- if (terminal) throw new HostedNativeSessionError(terminal.code);
233
+ if (terminal) throw errorFrom(terminal);
228
234
  if (!activeSession || binding.kind !== "hosted") return;
229
235
  if (at - activeSession.startedAt < ROTATION_AGE_MS && activeSession.requestCount < ROTATION_REQUEST_LIMIT) {
230
236
  return;
@@ -258,7 +264,7 @@ function createHostedNativeSessionController(input) {
258
264
  return current;
259
265
  }
260
266
  async function ensureAuthority(session, at) {
261
- if (terminal) throw new HostedNativeSessionError(terminal.code);
267
+ if (terminal) throw errorFrom(terminal);
262
268
  if (session.authority && session.authority.clockEpoch === clock.epoch && session.authority.validUntil - at > REFRESH_MARGIN_MS) {
263
269
  return { authority: session.authority, session };
264
270
  }
@@ -270,7 +276,7 @@ function createHostedNativeSessionController(input) {
270
276
  if (session.authority && session.authority.clockEpoch === clock.epoch && session.authority.validUntil > at) {
271
277
  return { authority: session.authority, session };
272
278
  }
273
- throw new HostedNativeSessionError(retryError.code);
279
+ throw errorFrom(retryError);
274
280
  }
275
281
  const operationBinding = binding;
276
282
  const abort = new SessionAbortController();
@@ -297,14 +303,16 @@ function createHostedNativeSessionController(input) {
297
303
  const normalized = abort.signal.aborted ? abort.error() : normalizeError(error);
298
304
  const target = latestSession(session);
299
305
  if (normalized.kind === "terminal" && binding === operationBinding && !terminal) {
300
- const failure = Object.freeze({ code: normalized.code, kind: normalized.kind });
306
+ const failure = safeError(normalized);
301
307
  terminal = failure;
302
308
  const rejectedSession = activeSession;
303
309
  if (rejectedSession) {
304
310
  rejectedSession.authority = null;
305
311
  rejectedSession.error = failure;
306
312
  }
307
- for (const pending of [...operations]) pending.abort(failure.code);
313
+ for (const pending of [...operations]) {
314
+ pending.abort(failure.code, failure.reason);
315
+ }
308
316
  if (binding === operationBinding && rejectedSession) {
309
317
  publishSession(rejectedSession, "error", failure);
310
318
  }
@@ -313,7 +321,7 @@ function createHostedNativeSessionController(input) {
313
321
  delayRetry(normalized);
314
322
  }
315
323
  if (normalized.code !== "NATIVE_SESSION_REPLACED" && normalized.code !== "NATIVE_SESSION_DISPOSED") {
316
- target.error = Object.freeze({ code: normalized.code, kind: normalized.kind });
324
+ target.error = safeError(normalized);
317
325
  publishSession(target, "error", target.error);
318
326
  }
319
327
  throw normalized;
@@ -339,7 +347,7 @@ function createHostedNativeSessionController(input) {
339
347
  if (abort.signal.aborted) throw abort.error();
340
348
  const expectedSurfaceId = session.authority?.public.surfaceId ?? session.requestedSurfaceId;
341
349
  const response = await fetchResponse(hostedBinding, session, expectedSurfaceId, abort);
342
- const source = await readBoundedResponse(response, abort);
350
+ const { source, size } = await readBoundedResponse(response, abort);
343
351
  let body;
344
352
  try {
345
353
  body = source.length ? JSON.parse(source) : null;
@@ -349,14 +357,15 @@ function createHostedNativeSessionController(input) {
349
357
  }
350
358
  body = null;
351
359
  }
352
- if (response.status === 409 && allowRestart && isExactRestart(body, session.sessionId)) {
360
+ if (response.status === 409 && isExactRestart(body, session.sessionId)) {
361
+ if (!allowRestart) throw new HostedNativeSessionError("NATIVE_SESSION_UNAVAILABLE");
353
362
  assertCurrentSession(session, hostedBinding);
354
363
  const replacement = redirectSession(session, readClock());
355
364
  if (abort.signal.aborted) throw abort.error();
356
365
  return bootstrap(replacement, hostedBinding, abort, false, false);
357
366
  }
358
367
  if (response.status !== 201) {
359
- const failure = statusError(response.status, body);
368
+ const failure = statusError(response.status, body, size);
360
369
  const delay = retryAfterMs(response.headers, now);
361
370
  if (delay !== null) retryAfterDelays.set(failure, delay);
362
371
  throw failure;
@@ -494,7 +503,7 @@ function createHostedNativeSessionController(input) {
494
503
  if (bindingGeneration !== expectedGeneration) {
495
504
  throw new HostedNativeSessionError("NATIVE_SESSION_REPLACED");
496
505
  }
497
- if (terminal) throw new HostedNativeSessionError(terminal.code);
506
+ if (terminal) throw errorFrom(terminal);
498
507
  checkedAt = readClock();
499
508
  if (resolved.authority.clockEpoch === clock.epoch && resolved.authority.validUntil > checkedAt) {
500
509
  admittedAuthorities.set(resolved.authority.public, resolved.authority);
@@ -569,7 +578,7 @@ function createHostedNativeSessionController(input) {
569
578
  if (disposed) throw new HostedNativeSessionError("NATIVE_SESSION_DISPOSED");
570
579
  lifecycle = "foreground";
571
580
  if (terminal) {
572
- const error = new HostedNativeSessionError(terminal.code);
581
+ const error = errorFrom(terminal);
573
582
  publish(currentSnapshot());
574
583
  throw error;
575
584
  }
@@ -842,7 +851,7 @@ async function readBoundedResponse(response, abort) {
842
851
  bytes.set(chunk, offset);
843
852
  offset += chunk.byteLength;
844
853
  }
845
- return decodeUtf8(bytes);
854
+ return { source: decodeUtf8(bytes), size };
846
855
  }
847
856
  function decodeUtf8(bytes) {
848
857
  let result = "";
@@ -890,7 +899,7 @@ function decodeUtf8(bytes) {
890
899
  function hasNoStore(value) {
891
900
  return typeof value === "string" && value.split(",").some((part) => part.trim().toLowerCase() === "no-store");
892
901
  }
893
- function statusError(status, body) {
902
+ function statusError(status, body, size) {
894
903
  if (status === 401) return new HostedNativeSessionError("NATIVE_SESSION_CREDENTIAL_REJECTED");
895
904
  if (status === 403) return new HostedNativeSessionError("NATIVE_SESSION_MAP_MISMATCH");
896
905
  if (status === 404) return new HostedNativeSessionError("NATIVE_SESSION_MAP_NOT_FOUND");
@@ -902,7 +911,21 @@ function statusError(status, body) {
902
911
  if (status === 408 || status >= 500 && status <= 599) {
903
912
  return new HostedNativeSessionError("NATIVE_SESSION_UNAVAILABLE");
904
913
  }
905
- return new HostedNativeSessionError("NATIVE_SESSION_REJECTED");
914
+ if (status >= 400 && status <= 499) {
915
+ return new HostedNativeSessionError(
916
+ "NATIVE_SESSION_REJECTED",
917
+ size <= SERVICE_CODE_BODY_LIMIT && isRecord(body) && typeof body.code === "string" ? body.code : void 0
918
+ );
919
+ }
920
+ return new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_INVALID");
921
+ }
922
+ function safeError(error) {
923
+ return Object.freeze(
924
+ error.reason === void 0 ? { code: error.code, kind: error.kind } : { code: error.code, kind: error.kind, reason: error.reason }
925
+ );
926
+ }
927
+ function errorFrom(error) {
928
+ return new HostedNativeSessionError(error.code, error.reason);
906
929
  }
907
930
  var retryAfterDelays = /* @__PURE__ */ new WeakMap();
908
931
  function retryAfterMs(headers, now) {
@@ -952,12 +975,13 @@ async function cancelResponse(response) {
952
975
  } catch {
953
976
  }
954
977
  }
955
- var _aborted, _code, _listeners;
978
+ var _aborted, _code, _reason, _listeners;
956
979
  var SessionAbortController = class {
957
980
  constructor() {
958
981
  __publicField(this, "signal");
959
982
  __privateAdd(this, _aborted, false);
960
983
  __privateAdd(this, _code, "NATIVE_SESSION_REPLACED");
984
+ __privateAdd(this, _reason);
961
985
  __privateAdd(this, _listeners, /* @__PURE__ */ new Set());
962
986
  const isAborted = () => __privateGet(this, _aborted);
963
987
  this.signal = Object.freeze({
@@ -972,9 +996,10 @@ var SessionAbortController = class {
972
996
  }
973
997
  });
974
998
  }
975
- abort(code) {
999
+ abort(code, reason) {
976
1000
  if (__privateGet(this, _aborted)) return;
977
1001
  __privateSet(this, _code, code);
1002
+ __privateSet(this, _reason, reason);
978
1003
  __privateSet(this, _aborted, true);
979
1004
  for (const listener of [...__privateGet(this, _listeners)]) {
980
1005
  try {
@@ -985,11 +1010,12 @@ var SessionAbortController = class {
985
1010
  __privateGet(this, _listeners).clear();
986
1011
  }
987
1012
  error() {
988
- return new HostedNativeSessionError(__privateGet(this, _code));
1013
+ return new HostedNativeSessionError(__privateGet(this, _code), __privateGet(this, _reason));
989
1014
  }
990
1015
  };
991
1016
  _aborted = new WeakMap();
992
1017
  _code = new WeakMap();
1018
+ _reason = new WeakMap();
993
1019
  _listeners = new WeakMap();
994
1020
  function raceWithAbort(promise, controller) {
995
1021
  if (controller.signal.aborted) return Promise.reject(controller.error());
@@ -1,5 +1,5 @@
1
1
  import { TileflowNativeSourceState } from '@tileflow/core/native';
2
- import { x as MapSourceState } from '../contract-BaK24QZD.js';
2
+ import { x as MapSourceState } from '../contract-BWT1jxqg.js';
3
3
  import '@maplibre/maplibre-react-native';
4
4
  import 'react';
5
5
  import 'react-native';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tileflow/react-native",
3
- "version": "0.1.0-alpha.7",
3
+ "version": "0.1.0-alpha.8",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Tileflow maps, camera control and semantic interactions for React Native.",
6
6
  "type": "module",
@@ -18,7 +18,6 @@
18
18
  "types": "./dist/index.d.ts",
19
19
  "files": [
20
20
  "dist",
21
- "docs",
22
21
  "android/build.gradle",
23
22
  "android/src/main",
24
23
  "ios/*.h",
@@ -36,8 +35,8 @@
36
35
  }
37
36
  },
38
37
  "dependencies": {
39
- "@tileflow/core": ">=0.1.0-alpha.42 <0.1.0-beta.0",
40
- "@tileflow/interactions": ">=0.1.0-alpha.6 <0.1.0-beta.0"
38
+ "@tileflow/core": ">=0.1.0-alpha.43 <0.1.0-beta.0",
39
+ "@tileflow/interactions": ">=0.1.0-alpha.7 <0.1.0-beta.0"
41
40
  },
42
41
  "peerDependencies": {
43
42
  "@maplibre/maplibre-react-native": "11.5.0",
@@ -1,124 +0,0 @@
1
- # Private native resource admission
2
-
3
- This guide describes the private native networking boundary used by the mounted Tileflow `Map` in `@tileflow/react-native`. The transport, session controller, resource catalog, configuration reader and native bridge are **not a public API**. The package root exports the Tileflow `Map` and public Map types only; it does not export grants, credentials, session objects, admission handles, native renderer handles or request interception controls.
4
-
5
- The mounted owner prepares a **bounded Style/TileJSON resource closure** before native rendering. It covers the current owned style document, one TileJSON edge per tile source, tile templates, sprite leaves, glyph templates, native-v1 managed font faces and supported base assets represented by the current contracts. It does not recursively trust arbitrary remote graphs. Ambiguous, unsafe, oversized or incomplete protected graphs fail closed, while foreign/unowned resources remain unchanged.
6
-
7
- ## Exact native dependencies
8
-
9
- The source targets React 19.2.0, React Native 0.83.10, MapLibre React Native 11.5.0, MapLibre Native Android 13.6.1 and MapLibre Native iOS 6.31.0. Android's HTTP dependency is OkHttp 4.12.0. These pins and the existence of source tests are **not evidence** of a successful build, device run, Hosted service or app-store acceptance; qualify the exact revision in the intended host.
10
-
11
- Android's library build uses the host's Android Gradle/Kotlin plugins and SDK settings. `react-native.config.cjs` autolinks `TileflowNativeAdmissionPackage`, which registers the private admission, document, configuration and surface modules. The library depends on the autolinked `:maplibre_maplibre-react-native` project and rejects conflicting MapLibre Native versions rather than selecting another version silently.
12
-
13
- On iOS, the `TileflowNativeAdmission` pod depends on the exact React and MapLibre React Native pods. MapLibre Native itself is the existing pinned Swift Package Manager product, not a second MapLibre CocoaPod. Retain the host's React Native post-install work and MapLibre setup, then invoke:
14
-
15
- ```ruby
16
- $MLRN.post_install(installer)
17
- tileflow_native_admission_post_install(installer)
18
- ```
19
-
20
- The helper requires the MapLibre distribution reference with `exactVersion` 6.31.0 and reuses that product for the Tileflow pod/test targets. It does not add another SDK or change the pin.
21
-
22
- ## Installation ownership
23
-
24
- Tileflow owns the MapLibre networking seam in explicit installation order. Android captures the previous `ModuleProvider`, wraps `createHttpRequest()` and delegates unowned traffic to the previous provider. iOS adds `TFNativeAdmissionURLProtocol` only to MapLibre's session configuration and uses an independent internal URLSession; it never registers an application-global protocol.
25
-
26
- Installation/removal are acknowledged operations. Partial installation rolls back only state still owned by that attempt. A later incompatible provider/configuration replacement makes protected work fail closed; removal never overwrites a later owner. This is not a guarantee for arbitrary third-party mutation chains.
27
-
28
- The process-level installation may be shared by mounted Maps, but that is the only shared ownership. Each real Map owns a distinct source generation, Hosted binding/session/controller, native context, catalog, queue, tickets and callbacks. One Map's retirement or cleanup failure cannot spend, remove or reuse another Map's authority.
29
-
30
- ## Mounted Map lifecycle
31
-
32
- A native Map is created only after source resolution and context acknowledgement. If the resolved manifest uses Hosted session delivery, application configuration must first yield a binding whose canonical API origin exactly matches the manifest. Non-session delivery does not read the mobile credential or bootstrap Hosted authority.
33
-
34
- Theme transactions keep the same native Map, context, session and camera. Resource additions are append-only and become usable only after native `extendContext` acknowledgement. Glyph templates may extend their finite font-stack allowlist across themes by an acknowledged union; the template URL, origin, class and scope cannot change. A logical source/Map replacement retires the previous context before opening the next one.
35
-
36
- Retirement is idempotent and cancels queue, JavaScript wait, admitted work, document acquisition and late native completions. Removal acknowledgements are retryable. Strict Mode unmount/remount therefore cannot revive the old generation or share its context with the replacement.
37
-
38
- ## Resource identity and projection
39
-
40
- Every protected resource is canonical HTTPS and belongs to one explicit class: `style`, `tilejson`, `tile`, `sprite`, `glyph` or `font`. Tile and TileJSON entries also preserve exact tileset identity. Tile templates use only the bounded native placeholder grammar. Glyph templates require both `{fontstack}` and `{range}` and a finite exact font-stack set.
41
-
42
- Style preparation resolves relative URLs through Core's native URL policy. A style may discover at most one bounded TileJSON document for a declared tile source; TileJSON cannot recursively point to another TileJSON. Sprite base URLs are admitted through their concrete JSON/PNG 1x/2x leaves before the style receives its transient context discriminator. Native-v1 font-face arrays preserve family/style/weight attributes while only their admitted URL is rewritten.
43
-
44
- Owned URLs must match the exact Hosted origin/path rules and the session policy's scope/tileset set. A resource on a foreign origin remains unmodified. An URL on an owned origin but outside a known resource path fails closed instead of inheriting authority merely because its origin matches.
45
-
46
- The non-secret `__tf_native_context` discriminator selects one Map context. It is stripped before HTTP and never conveys authority. Unknown, malformed, stale, duplicate or retired reserved context values are rejected rather than delegated as third-party traffic. Other URL identity is preserved.
47
-
48
- ## Admission accounting
49
-
50
- Every eligible protected request produces one logical controller `acquire()` result. A bounded bridge batch may carry several tickets but never shares one acquire, preallocates grants, counts chunks or maintains a native commercial counter. Direct/non-session contexts delegate without commercial acquisition.
51
-
52
- The controller preserves the 10,000-request and six-hour rotation semantics: the 10,000th acquisition remains in its session and the next rotates. Bootstrap/refresh are single-flight where allowed. Native cancellation does not refund a logical acquisition already made. HTTP response observation does not confirm or undo server-owned commercial completion.
53
-
54
- `X-Tileflow-Native-Grant` is the only protected-resource grant carrier. It is attached only after resource/context/session policy checks and never enters query strings, URLs, cache keys, public state, events, errors or logs.
55
-
56
- ## Independent bootstrap and expiry
57
-
58
- Bootstrap uses an independent OkHttp client or ephemeral URLSession channel. It sends the bounded session-start POST to the configured canonical API origin and carries the publishable mobile credential only in `X-Tileflow-Mobile-Client`. Cookies, ambient credentials and automatic redirects are disabled.
59
-
60
- Each context binds its validated bootstrap endpoint/credential. Retiring that context cancels its bootstrap without cancelling another Map. TypeScript alone parses session authority and makes admission decisions.
61
-
62
- A bootstrap ends after 10 seconds, well inside the 30-second protected transport deadline. A stalled renewal therefore fails its bridge batch, and the tickets waiting on it, instead of outliving them. A ticket whose own deadline passes while JavaScript is still answering its batch fails alone; only a batch that JavaScript does not answer within 30 seconds retires the context. When admission retires the context of a mounted Map, the Map reports `renderer-error` and opens a new context for the same source, up to three times without a successful load in between.
63
-
64
- ## Session failures
65
-
66
- The controller classifies each bootstrap or renewal failure:
67
-
68
- | Response | Result |
69
- | ------------------------------------------------------------------------------ | ------------------------------------------------------- |
70
- | 401, 403, 404 | Stops the session until `ref.retry()` or another source |
71
- | 429 with `COMMERCIAL_MAP_LIMIT_REACHED` or `COMMERCIAL_SPENDING_LIMIT_REACHED` | Stops the session until `ref.retry()` or another source |
72
- | Any other 429 | Rate limited: retries after a delay |
73
- | 408, 5xx, network failure or timeout | Unavailable: retries after a delay |
74
- | Another status, or a response that breaks the session contract | Retries after a delay |
75
-
76
- A stopped session reports `session-error` through `onError`, including when a renewal is refused after the first load. A delay starts at one second and doubles up to one minute; a `Retry-After` in seconds or as an HTTP date lengthens it, up to 15 minutes. During the delay no bootstrap leaves the device, and a grant that is still valid keeps admitting protected requests. A successful bootstrap or `ref.retry()` clears the delay.
77
-
78
- The controller's private transport budget remains anchored to its existing local `validUntil`. Native never reconstructs a fresh lifetime from server timestamps. The budget is bounded by the earlier ticket-enqueue time, the native wait deadline and a safety margin, and is rechecked immediately before network start and redirects. Backgrounding, clock rollback, source replacement, disposal and expiry invalidate stale authority.
79
-
80
- No synchronous network hook, main thread or OkHttp interceptor waits for JavaScript. Suspended JavaScript therefore cannot authorize stale protected work.
81
-
82
- ## Redirects and observation
83
-
84
- Protected redirects are handled by the adapter, not by automatic credential forwarding. Same-origin targets must still satisfy catalog, path/class, tileset, context and ticket validity. Cross-origin, different-port, uncataloged or conflicting-context targets receive no forwarded grant. A redirect does not consume another admission for the same ticket.
85
-
86
- iOS does not rely on MapLibre forwarding `didReceiveResponse` to a configuration delegate. The adapter observes its own response. Native observation events contain only bounded identifiers/status and are private; they are not renderer readiness or commercial completion events.
87
-
88
- ## Renderer readiness
89
-
90
- Admission success is not Map readiness. The private surface adapter requires current style acceptance, committed positive native layout and a fully rendered native map/frame after that layout. Background, layout loss, camera gesture/command epochs and style replacement invalidate the evidence. Resume re-arms native evidence; JavaScript animation frames and sleeps are not readiness proof.
91
-
92
- Camera commands carry private monotonically correlated tokens. A successful public controller command is acknowledged only after the native target is applied, not merely queued by the upstream React Native bridge. User gestures are classified from native gesture reasons and remain separate from programmatic commands.
93
-
94
- ## Fixed transport bounds
95
-
96
- | Boundary | Limit |
97
- | -------------------------------------- | --------------------------------------------: |
98
- | Live Map contexts per installation | 16 |
99
- | Catalog resources per context | 128 |
100
- | Native engine ingress per installation | 2,048 |
101
- | Protected work per context | 128 |
102
- | Tickets per bridge batch | 8 |
103
- | Resource URL | 2,048 characters including discriminator room |
104
- | Admission bridge payload | 524,288 bytes |
105
- | Grant | 24,576 characters |
106
- | Protected transport deadline | 30 seconds from enqueue |
107
- | Validity safety margin | 1 second |
108
- | Protected redirects | 3 |
109
- | Protected response body | 8,388,608 bytes |
110
- | Pending bootstrap bridge work | 32 |
111
- | Bootstrap request / response | 2,048 / 65,536 bytes |
112
- | Bootstrap deadline | 10 seconds |
113
-
114
- Cancellation retains transport capacity until the corresponding native cleanup boundary. Capacity bounds are transport safety limits, not product quotas or billing counters.
115
-
116
- ## Application configuration
117
-
118
- The publishable mobile application credential and trusted API origin are configured through native application configuration only. They are not component props or JavaScript globals. See [Native application configuration](./native-configuration.md) for exact iOS/Android keys, credential grammar, origin normalization, rotation/revocation and the end-user threat-model boundary.
119
-
120
- ## Local qualification
121
-
122
- The source-checkout [mounted Map harness](../harness/README.md) uses the real public Tileflow component in an existing development host and is excluded from package exports/packed files. It creates no service, runner or deployment.
123
-
124
- Before accepting a revision, qualify package build/types/tests, the exact Android and iOS native test suites, CocoaPods/SPM integration, two concurrent Maps, Strict Mode replay, source replacement during each asynchronous phase, theme success/rollback, controlled camera gestures/commands, background/resume, readiness ordering, redirects, ownership mutation and teardown retries. Inspect only secret-free fixture assertions and public events. Authored source and tests are not evidence that these checks passed.
@@ -1,210 +0,0 @@
1
- # Native application configuration
2
-
3
- This guide describes the private application-configuration reader used by the mounted Tileflow
4
- `Map` in `@tileflow/react-native`. Its root exports the mounted `Map` and public types; the
5
- configuration reader remains private. Mobile credentials remain application
6
- configuration: there is no credential prop, JavaScript setter, provider, public configuration
7
- object or public configuration subpath.
8
-
9
- Importing `@tileflow/react-native` does not read configuration, install networking, start a session,
10
- acquire admission or create a native Map. Configuration is read lazily only when a mounted source
11
- resolves to Hosted session delivery. Follow the [package setup](../README.md) and the
12
- [native admission guide](native-admission.md) for the exact native dependencies and installation
13
- order. No Expo config plugin is supplied.
14
-
15
- ## Prerequisites and identity
16
-
17
- Hosted native production requires a claimed Tileflow account/project, a Hosted Map and its scoped,
18
- revocable, publishable mobile application credential. The credential authenticates the distributed
19
- application, not an end user. There is no Tileflow end-user login, account provider or authentication
20
- hook in this integration. Web deployment with allowed-domain authorization is a different mechanism.
21
- A compatible SDK/API pair is required; this document is not a publication or availability claim.
22
-
23
- Use only the publishable mobile credential, whose exact grammar is `tf_public_` followed by
24
- 48 lowercase hexadecimal characters, for a total of 58 ASCII characters. A CLI, deployment,
25
- administration or other privileged credential must never enter application configuration.
26
-
27
- The two logical fields are `apiOrigin` and `credential`. `apiOrigin` is the API origin approved by
28
- the application operator, not a value copied automatically from a remote manifest. The native
29
- session contract has no implicit origin. The examples below use `api.example.test` and an
30
- intentionally invalid credential placeholder; replace both in the application build.
31
-
32
- ## Configure iOS
33
-
34
- In the **application target's Info.plist**, add one `TileflowMobileConfiguration` key of type Array.
35
- Its two String elements are `apiOrigin=...` and `credential=...`:
36
-
37
- ```xml
38
- <key>TileflowMobileConfiguration</key>
39
- <array>
40
- <string>apiOrigin=https://api.example.test</string>
41
- <string>credential=YOUR_PUBLISHABLE_MOBILE_CREDENTIAL</string>
42
- </array>
43
- ```
44
-
45
- Place this in the final application plist, not in a pod, framework, MapLibre configuration or
46
- `InfoPlist.strings`. When Xcode generates the plist, ensure the resulting application target still
47
- contains one literal array. Application-owned build substitution may supply each string, but
48
- unresolved placeholders fail validation.
49
-
50
- The autolinked `TileflowNativeConfiguration` module reads only `NSBundle.mainBundle` when its
51
- private asynchronous reader is requested. It exports no constants or events. Constructing the
52
- module, importing the package and mounting a non-session Map do not perform that read.
53
-
54
- ## Configure Android
55
-
56
- In the application's `android/app/src/main/res/values/tileflow.xml`, add one literal String Array:
57
-
58
- ```xml
59
- <?xml version="1.0" encoding="utf-8"?>
60
- <resources>
61
- <string-array name="tileflow_mobile_configuration" translatable="false">
62
- <item>apiOrigin=https://api.example.test</item>
63
- <item>credential=YOUR_PUBLISHABLE_MOBILE_CREDENTIAL</item>
64
- </string-array>
65
- </resources>
66
- ```
67
-
68
- The array belongs to the application package. Do not use AndroidManifest metadata, BuildConfig,
69
- a system property, an alias or a second runtime input. Both entries must be literal strings;
70
- references such as `@string/...`, `@array/...`, theme attributes and styled strings are rejected.
71
- Build flavors may deliberately provide their own single final application configuration, but each
72
- resulting application must be checked independently.
73
-
74
- `TileflowNativeAdmissionPackage` autolinks the configuration reader alongside the private admission
75
- and surface modules. Constructing those modules does not read configuration or install networking.
76
- The library includes a resource-shrinker keep declaration for the dynamically located array; the
77
- application's optimized release build must retain it. The library ships no default configuration
78
- array or embedded credential.
79
-
80
- ## Array and origin contract
81
-
82
- Both platforms use the same bounded list of exactly two `key=value` strings. Order is irrelevant;
83
- keys are case-sensitive. Split only at the first equals sign. Missing, empty, additional,
84
- wrong-typed or duplicate entries fail closed. Each complete entry is limited to 2,080 UTF-16 code
85
- units.
86
-
87
- The application-owned `apiOrigin` is at most 2,048 UTF-16 code units. Normalization is deliberately
88
- limited to the HTTPS scheme and ASCII hostname, omission of port 443 and removal of one optional
89
- root slash. For example, application configuration `HTTPS://API.Example.test:443/` becomes
90
- `https://api.example.test`. Port 8443 remains distinct. The grammar permits ASCII DNS labels or
91
- canonical dotted-decimal IPv4; IPv6 literals are outside this application-configuration grammar.
92
-
93
- Ports are decimal 1–65535 without leading zeroes. HTTP, user-info, non-root paths, query strings,
94
- fragments, whitespace, backslashes, percent escapes, wildcards, trailing host dots, Unicode hostname
95
- conversion and shortened/octal/hexadecimal/integer IPv4 forms are rejected.
96
-
97
- The **remote manifest is not normalized into trust**. Its declared `apiUrl` must already be the exact
98
- canonical string stored in the application snapshot. A trailing slash, explicit default port or
99
- case variation in the remote declaration is rejected, even if normalizing it would produce the same
100
- origin. Equality includes scheme, host and non-default port; there is no suffix or subdomain match.
101
- If both root and map-level API declarations are present, they must agree exactly.
102
-
103
- ## Source-to-session boundary
104
-
105
- Use the public Native manifest URL returned by an explicit compatible
106
- [`deploy --with-native`](../../cli/docs/native-artifacts.md#publish-web-and-native-together):
107
-
108
- ```text
109
- https://api.example.test/maps/map_0123456789abcdef/native/manifest.json
110
- ```
111
-
112
- The `Map` source remains `{map, manifestUrl}`. `map` is the authored map name, not the managed Map ID.
113
- The initial document is bounded, one-map, version-1 public metadata. It contains theme names,
114
- identity, revisions and references, not Style JSON or authority. It receives no credential or grant.
115
-
116
- The private binding resolver consumes the current resolved Core source, not an executable config or
117
- a renderer style. Only `map.usageMode === 'session'` can request application configuration. It
118
- requires the declared Hosted `mapId`, exact `apiUrl`, agreement between the resolved logical map
119
- name and selected source, and both the original requested URL and final response URL equal to the
120
- canonical Hosted route for that managed Map. A redirect to another origin, Map, CDN route or query
121
- alias is rejected. URL identity is a selector checked against the document, not authority.
122
-
123
- A missing `usageMode` produces the existing direct binding without evaluating configuration only for
124
- a non-Hosted source. A document obtained from the reserved Hosted Native route cannot omit that mode
125
- to downgrade into direct delivery. Local, self-hosted and other non-session sources otherwise keep
126
- their existing account-free behavior. Unknown modes, malformed session metadata, origin mismatches
127
- or conflicting Map identity fail before a Hosted binding is delivered.
128
-
129
- The publishable credential is sent **only** to the application's configured API origin at
130
- `POST /v1/sessions/start`. The existing bootstrap transport does not follow redirects. Neither a
131
- manifest URL nor a manifest-declared `apiUrl` can select a different credential destination. Styles,
132
- TileJSON, tiles, sprites, glyphs and fonts use the existing resource grant, never that credential.
133
-
134
- The resolver itself has no renderer or resource-admission authority. The mounted Map passes the
135
- acknowledged binding into its private admission/session owner only after source resolution. Creating
136
- a resolver does not call `acquire()`, and importing the package does not construct a resolver.
137
-
138
- ## Deployment revisions and stale discovery
139
-
140
- Hosted themes carry mandatory revision hashes and versioned protected style URLs of the form
141
- `/maps/<mapId>/native/v<deploymentVersion>/<theme>.json`. Every theme in one manifest belongs to the
142
- same deployment version. The server checks current deployment ownership and verifies stored style
143
- bytes against the revision. The mounted owner checks the returned style URL and its
144
- Map/theme/deployment/revision metadata before projecting resources or creating a renderer.
145
-
146
- If the root protected style cannot be read or fails this correspondence, the owner may discard its
147
- cached discovery document and reload the canonical manifest **once per explicit source/theme
148
- selection**. This recovery retains the same session and admission context. A refresh cannot switch
149
- Map, origin or delivery mode, or move backwards to an older deployment. Child-resource failures do
150
- not replenish that retry budget. A second failure uses the existing safe error or theme-rollback
151
- path. It never composes a new active style with stale public metadata silently.
152
-
153
- A public manifest GET, session start or Map render is not itself a commercial completion event.
154
- Server completion remains conditional on an eligible protected GET returning 200/206 under the
155
- existing admission contract. Disabled mobile delivery fails closed even for valid public references.
156
-
157
- ## Mounted ownership, caching and cancellation
158
-
159
- The native reader caches one immutable application snapshot, or one safe failure, for the
160
- application process. It does not retain a React context. JavaScript coalesces native reads and copies
161
- the result into a frozen private value. Each mounted Map still owns an independent pending binding,
162
- session controller, admission context, callbacks and renderer lifetime. Shared immutable application
163
- configuration never implies shared grants, counters or source ownership.
164
-
165
- Source replacement and Map disposal invalidate that owner's in-flight binding immediately. A late
166
- configuration success or failure cannot deliver authority to a retired owner. Retiring one Map does
167
- not cancel another Map's access to the process-level immutable configuration snapshot. Configuration
168
- is not hot-reloaded; restart the application process after changing native values.
169
-
170
- The configuration reader installs no timeout, timer, background poller or service. One unresolved
171
- native read can remain pending, but every Map resolver remains independently disposable. Camera,
172
- resource projection and rendered-readiness logic are owned elsewhere in the mounted Map runtime;
173
- they are not configuration-reader responsibilities.
174
-
175
- ## Public credential threat model and rotation
176
-
177
- A credential distributed inside a mobile application is extractable. Native configuration is not a
178
- secret vault, proof of installation authenticity or end-user authentication. This version does not
179
- authenticate with bundle ID, package name, signing identity or attestation. Origin binding prevents
180
- the SDK from forwarding its configured credential to an origin selected by an untrusted manifest;
181
- it does not stop a party that has copied the publishable credential from making its own requests.
182
- Scope, revocation and server-side authorization remain necessary.
183
-
184
- The credential crosses only the private reader/binding/bootstrap path. It is not a native grant and
185
- is not a resource URL, cache key, public ref field, source state, renderer event or diagnostic.
186
- Errors use fixed internal codes/messages rather than configured values or native causes. Do not log
187
- native module responses, credentials, bootstrap bodies or grant material.
188
-
189
- For rotation, issue a new credential with the intended Map scope, update the same native entries,
190
- rebuild and distribute the application, then retire the old credential according to the rollout
191
- policy. Revoking an old credential may stop installed versions that still contain it. Do not repair
192
- that failure with a JavaScript override or a second credential source.
193
-
194
- A future Expo plugin, if introduced, would write these same native values rather than create another
195
- runtime configuration channel. No plugin or end-user login is part of this integration. Selection
196
- presentation and foreground-location policy remain application-owned; this contract adds neither UI
197
- nor a location API.
198
-
199
- ## Validation boundary
200
-
201
- The authored source tests cover grammar, exact origin matching, source/final URL identity, lazy reads,
202
- safe failures, stale results, bounded discovery recovery, independent resolvers and the absence of a
203
- public credential surface. Native tests cover parser boundaries, versioned style ownership,
204
- duplicate/wrong-typed configuration, resource literals, caching and reentrancy. Package tests protect
205
- autolinking, exact peers, private configuration declarations and archive contents.
206
-
207
- Those sources are not execution evidence. Local qualification must additionally exercise the final
208
- iOS Info.plist and Android resource extraction, resource shrinking, bridge registration, cold starts,
209
- concurrent mounted Maps, direct versus session sources, replacement/disposal, stale public metadata
210
- and corrected/revoked credentials in the pinned native host.