@remnic/connector-reitti 9.69.64
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/LICENSE +21 -0
- package/README.md +80 -0
- package/dist/index.d.ts +205 -0
- package/dist/index.js +519 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Joshua Warren
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# @remnic/connector-reitti
|
|
2
|
+
|
|
3
|
+
Optional location provider adapter that reads a self-hosted
|
|
4
|
+
[Reitti](https://github.com/dedicatedcode/reitti) instance and feeds the core
|
|
5
|
+
location pipeline (`@remnic/core` ≥ 9.64, issue #2044). Read-only, opt-in,
|
|
6
|
+
current-user endpoints only — no memory writes, no Reitti mutations, no raw
|
|
7
|
+
GPS/track storage, no telemetry.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @remnic/connector-reitti
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`@remnic/core` works without this package: a configured `reitti` source whose
|
|
16
|
+
provider is not registered is skipped with `provider-not-registered`, never an
|
|
17
|
+
error.
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
The adapter is an API client and normalizer. Register it through the core
|
|
22
|
+
location registry — ideally via a computed-specifier dynamic import so
|
|
23
|
+
bundlers cannot statically resolve the optional package:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { getLocationProvider } from "@remnic/core/location";
|
|
27
|
+
|
|
28
|
+
const mod = await import("@remnic/" + "connector-reitti");
|
|
29
|
+
mod.ensureReittiProviderRegistered({
|
|
30
|
+
baseUrl: "https://reitti.example.invalid",
|
|
31
|
+
token: resolvedToken, // from Remnic's secret reference mechanism
|
|
32
|
+
authMode: "x-api-token", // or "bearer" — exactly one header is sent
|
|
33
|
+
timezone: "Europe/Berlin", // from location config; used for day bucketing
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Later, per day window (half-open [startUtc, endUtc)):
|
|
37
|
+
const provider = getLocationProvider("reitti");
|
|
38
|
+
const page = await provider.fetchObservations({ startUtc, endUtc });
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Endpoints used
|
|
42
|
+
|
|
43
|
+
- `GET /api/v1/timeline?date=YYYY-MM-DD&timezone=<IANA>` — primary
|
|
44
|
+
chronological source (VISIT and TRIP entries).
|
|
45
|
+
- `GET /api/v1/visits?date=YYYY-MM-DD&timezone=<IANA>` — optional fallback
|
|
46
|
+
(`visitsFallback: true`) when the timeline day is empty; served as a second
|
|
47
|
+
page through the `visits` cursor.
|
|
48
|
+
|
|
49
|
+
Never `/api/v1/visits/{userId}` or raw location-point endpoints.
|
|
50
|
+
|
|
51
|
+
## Normalization
|
|
52
|
+
|
|
53
|
+
Each timeline interval becomes two observations (start + end instants)
|
|
54
|
+
carrying its place; core's `observationSegments` merges them back into
|
|
55
|
+
half-open `[start, end)` segments. Named places keep their Reitti id
|
|
56
|
+
(`reitti:place:<id>`); trips carry the transport mode and distance in the
|
|
57
|
+
label (`Trip (TRAIN · 12.3 km)`, kind `transit`); unresolved places are
|
|
58
|
+
labeled `Unnamed place` without inventing names or coordinates. Instants are
|
|
59
|
+
clamped into the requested day window. Encoded paths are ignored.
|
|
60
|
+
|
|
61
|
+
## Failure behavior
|
|
62
|
+
|
|
63
|
+
Empty day (`[]`) and provider failure are distinct. Errors are typed
|
|
64
|
+
`ReittiApiError` kinds: `auth` (401/403), `rate-limit` (429), `server` (5xx),
|
|
65
|
+
`network`, `timeout`, `invalid-json`, `response-too-large`, `schema`, `http`.
|
|
66
|
+
Transient GET failures retry with bounded exponential backoff (Retry-After
|
|
67
|
+
honored), always preserving the caller's `AbortSignal`; an aborted caller
|
|
68
|
+
propagates unwrapped and unretried. Response bodies are size-bounded. The
|
|
69
|
+
token never appears in errors, URLs, or logs.
|
|
70
|
+
|
|
71
|
+
## Privacy
|
|
72
|
+
|
|
73
|
+
Coordinates are never emitted by this adapter — coordinate retention stays a
|
|
74
|
+
core config decision (`location.retainCoordinates`, default off). Place
|
|
75
|
+
labels, addresses, and cities are treated as sensitive data by the core
|
|
76
|
+
pipeline. No request beyond the configured instance is ever made.
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { LocationObservation, LocationProvider } from '@remnic/core/location';
|
|
2
|
+
import { ConnectorApiError } from '@remnic/core/http-retry';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Minimal Reitti API client (raw fetch, no SDK).
|
|
6
|
+
*
|
|
7
|
+
* Read-only, current-user endpoints only (issue #2045):
|
|
8
|
+
* GET /api/v1/timeline?date=YYYY-MM-DD&timezone=<IANA>
|
|
9
|
+
* GET /api/v1/visits?date=YYYY-MM-DD&timezone=<IANA>
|
|
10
|
+
* Never /api/v1/visits/{userId} and never raw location-point endpoints.
|
|
11
|
+
*
|
|
12
|
+
* Auth uses exactly ONE configured header form — `X-API-Token` or
|
|
13
|
+
* `Authorization: Bearer` — never both. The token never appears in any
|
|
14
|
+
* error message, URL, or log line produced here.
|
|
15
|
+
*
|
|
16
|
+
* Transport distinguishes empty results from failures (§22): a valid empty
|
|
17
|
+
* day is `[]`, while auth, rate-limit, server, network, timeout, JSON, size,
|
|
18
|
+
* and schema problems are distinct `ReittiApiError` kinds. Retries apply to
|
|
19
|
+
* transient GET failures only, always preserve the caller's abort signal,
|
|
20
|
+
* and never advance any state (the client is stateless).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
type ReittiAuthMode = "x-api-token" | "bearer";
|
|
24
|
+
declare const REITTI_AUTH_MODES: readonly ["x-api-token", "bearer"];
|
|
25
|
+
/** Reitti `TransportMode` values the normalizer preserves in trip labels. */
|
|
26
|
+
declare const REITTI_TRANSPORT_MODES: readonly ["WALKING", "CYCLING", "DRIVING", "TRAIN", "TRANSIT", "MOTORCYCLE", "SCOOTER", "AIRPLANE", "UNKNOWN"];
|
|
27
|
+
/** Reitti `SignificantPlace.PlaceType` values (upstream main, 2026-08). */
|
|
28
|
+
declare const REITTI_PLACE_TYPES: readonly ["RESTAURANT", "PARK", "SHOP", "HOME", "WORK", "HOSPITAL", "SCHOOL", "AIRPORT", "TRAIN_STATION", "GAS_STATION", "HOTEL", "BANK", "PHARMACY", "GYM", "LIBRARY", "CHURCH", "CINEMA", "CAFE", "MUSEUM", "LANDMARK", "TOURIST_ATTRACTION", "HISTORIC_SITE", "MONUMENT", "SHOPPING_MALL", "MARKET", "GALLERY", "THEATER", "GROCERY_STORE", "ATM", "OTHER"];
|
|
29
|
+
type ReittiTransportMode = (typeof REITTI_TRANSPORT_MODES)[number];
|
|
30
|
+
type ReittiPlaceType = (typeof REITTI_PLACE_TYPES)[number];
|
|
31
|
+
/** The place fields this connector consumes; other upstream fields are ignored. */
|
|
32
|
+
interface ReittiSignificantPlace {
|
|
33
|
+
id: number | string | null;
|
|
34
|
+
name: string | null;
|
|
35
|
+
address: string | null;
|
|
36
|
+
city: string | null;
|
|
37
|
+
type: ReittiPlaceType | null;
|
|
38
|
+
}
|
|
39
|
+
/** A validated `/api/v1/timeline` row (VISIT or TRIP). */
|
|
40
|
+
interface ReittiTimelineEntry {
|
|
41
|
+
id: string;
|
|
42
|
+
type: "VISIT" | "TRIP";
|
|
43
|
+
startTime: string;
|
|
44
|
+
endTime: string;
|
|
45
|
+
place: ReittiSignificantPlace | null;
|
|
46
|
+
transportMode: ReittiTransportMode | null;
|
|
47
|
+
distanceMeters: number | null;
|
|
48
|
+
}
|
|
49
|
+
/** A validated visit interval inside a `/api/v1/visits` place summary. */
|
|
50
|
+
interface ReittiVisitDetail {
|
|
51
|
+
startTime: string;
|
|
52
|
+
endTime: string;
|
|
53
|
+
}
|
|
54
|
+
/** A validated `/api/v1/visits` place summary. */
|
|
55
|
+
interface ReittiPlaceVisitSummary {
|
|
56
|
+
place: ReittiSignificantPlace | null;
|
|
57
|
+
visits: ReittiVisitDetail[];
|
|
58
|
+
}
|
|
59
|
+
type ReittiErrorKind = "auth" | "rate-limit" | "server" | "network" | "timeout" | "invalid-json" | "response-too-large" | "schema" | "http";
|
|
60
|
+
declare class ReittiApiError extends ConnectorApiError {
|
|
61
|
+
readonly kind: ReittiErrorKind;
|
|
62
|
+
constructor(message: string, kind: ReittiErrorKind, status?: number);
|
|
63
|
+
get retryable(): boolean;
|
|
64
|
+
}
|
|
65
|
+
interface ReittiClientOptions {
|
|
66
|
+
/** Absolute HTTP(S) base URL of the self-hosted Reitti instance. */
|
|
67
|
+
baseUrl: string;
|
|
68
|
+
/** Pre-resolved API token (from the secret store); never a placeholder. */
|
|
69
|
+
token: string;
|
|
70
|
+
/** Which documented header form to use; exactly one is sent. */
|
|
71
|
+
authMode?: ReittiAuthMode;
|
|
72
|
+
fetchImpl?: typeof fetch;
|
|
73
|
+
timeoutMs?: number;
|
|
74
|
+
maxResponseBytes?: number;
|
|
75
|
+
sleep?: (ms: number) => Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
interface ReittiDayRequest {
|
|
78
|
+
date: string;
|
|
79
|
+
timezone: string;
|
|
80
|
+
signal?: AbortSignal;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Validate an IANA timezone for Reitti requests: non-string/empty input is a
|
|
84
|
+
* TypeError; an unknown zone is a RangeError from core's `assertValidTimezone`.
|
|
85
|
+
*/
|
|
86
|
+
declare function assertReittiTimezone(timezone: string): void;
|
|
87
|
+
/**
|
|
88
|
+
* @deprecated Alias of `assertReittiTimezone`, kept so pre-rename imports from
|
|
89
|
+
* this published package keep resolving. Same TypeError/RangeError behavior.
|
|
90
|
+
*/
|
|
91
|
+
declare function assertValidIanaTimezone(timezone: string): void;
|
|
92
|
+
/**
|
|
93
|
+
* Validate and normalize the base URL: absolute HTTP(S), trailing slashes
|
|
94
|
+
* stripped without touching path semantics (a sub-path install keeps its
|
|
95
|
+
* prefix). Loop instead of a `/\/+$/` regex — CodeQL polynomial-redos.
|
|
96
|
+
*/
|
|
97
|
+
declare function normalizeReittiBaseUrl(raw: string): string;
|
|
98
|
+
declare class ReittiClient {
|
|
99
|
+
private readonly token;
|
|
100
|
+
private readonly baseUrl;
|
|
101
|
+
private readonly authMode;
|
|
102
|
+
private readonly fetchImpl;
|
|
103
|
+
private readonly timeoutMs;
|
|
104
|
+
private readonly maxResponseBytes;
|
|
105
|
+
private readonly sleep;
|
|
106
|
+
constructor(options: ReittiClientOptions);
|
|
107
|
+
/** Every VISIT/TRIP entry overlapping the local day (primary source). */
|
|
108
|
+
fetchTimeline(request: ReittiDayRequest): Promise<ReittiTimelineEntry[]>;
|
|
109
|
+
/** Processed visits grouped by place (fallback/enrichment source). */
|
|
110
|
+
fetchVisits(request: ReittiDayRequest): Promise<ReittiPlaceVisitSummary[]>;
|
|
111
|
+
private requestJson;
|
|
112
|
+
/** Exactly one auth header, per the configured mode — never both. */
|
|
113
|
+
private authHeaders;
|
|
114
|
+
private readBounded;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Reitti payload normalization (issue #2045).
|
|
119
|
+
*
|
|
120
|
+
* Maps validated `/api/v1/timeline` entries (VISIT/TRIP) and `/api/v1/visits`
|
|
121
|
+
* place summaries onto the core `LocationObservation` instant model.
|
|
122
|
+
*
|
|
123
|
+
* Encoding: each interval becomes two observations — one at its start, one at
|
|
124
|
+
* its end, both carrying the interval's place. Core's `observationSegments`
|
|
125
|
+
* merges consecutive same-place observations back into half-open segments,
|
|
126
|
+
* so this is the exact inverse: `A[8,9] T[9,10] B[10,11]` round-trips as
|
|
127
|
+
* place-day segments without gaps.
|
|
128
|
+
*
|
|
129
|
+
* Preservation rules (the core observation type is intentionally narrow):
|
|
130
|
+
* - place ids: `reitti:place:<placeId>` for named places; the entry id is
|
|
131
|
+
* embedded for synthetic ids (`reitti:trip:<id>`, `reitti:visit:<id>`)
|
|
132
|
+
* so source identifiers survive.
|
|
133
|
+
* - nullable places: a TRIP (or a VISIT Reitti could not resolve) never
|
|
134
|
+
* invents a place name. TRIP becomes a `transit`-kind observation whose
|
|
135
|
+
* label carries the transport mode and distance; an unresolved VISIT
|
|
136
|
+
* becomes an `other`-kind observation labeled "Unnamed place".
|
|
137
|
+
* - duration is start→end, reconstructible from the two instants.
|
|
138
|
+
* - encoded paths (`path`) are ignored by design (no track storage).
|
|
139
|
+
* - coordinates are never emitted; retention is a core config decision.
|
|
140
|
+
*
|
|
141
|
+
* Instants are clamped into the requested half-open [startUtc, endUtc)
|
|
142
|
+
* window: Reitti returns day-overlapping entries, while core requires every
|
|
143
|
+
* observation to fall inside the window it asked for.
|
|
144
|
+
*/
|
|
145
|
+
|
|
146
|
+
interface LocationWindow {
|
|
147
|
+
startUtc: string;
|
|
148
|
+
endUtc: string;
|
|
149
|
+
}
|
|
150
|
+
/** Normalize validated timeline entries (primary source) into observations. */
|
|
151
|
+
declare function timelineObservations(entries: readonly ReittiTimelineEntry[], window: LocationWindow): LocationObservation[];
|
|
152
|
+
/** Normalize validated visit summaries (fallback source) into observations. */
|
|
153
|
+
declare function visitSummaryObservations(summaries: readonly ReittiPlaceVisitSummary[], window: LocationWindow): LocationObservation[];
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @remnic/connector-reitti — Reitti location provider (issue #2045).
|
|
157
|
+
*
|
|
158
|
+
* Optional adapter around the core location contract (#2044): an API client
|
|
159
|
+
* and normalizer only. No file I/O, memory writes, config parsing, host SDK
|
|
160
|
+
* imports, or LLM calls. `@remnic/core` never imports this package — hosts
|
|
161
|
+
* register the provider through the core location registry, ideally via a
|
|
162
|
+
* computed-specifier dynamic import so bundlers cannot statically resolve it:
|
|
163
|
+
*
|
|
164
|
+
* const mod = await import("@remnic/" + "connector-reitti");
|
|
165
|
+
* mod.ensureReittiProviderRegistered({ baseUrl, token, timezone });
|
|
166
|
+
*
|
|
167
|
+
* Gates: the master `location.enabled=false` and source `enabled=false`
|
|
168
|
+
* short-circuits live in core config and the core pipeline — the provider is
|
|
169
|
+
* never consulted for a disabled source. Tokens arrive pre-resolved from
|
|
170
|
+
* Remnic's secret reference mechanism and never reach logs or error text.
|
|
171
|
+
*/
|
|
172
|
+
|
|
173
|
+
declare const REITTI_PROVIDER_ID = "reitti";
|
|
174
|
+
declare const REITTI_PROVIDER_DISPLAY_NAME = "Reitti";
|
|
175
|
+
/** Second-page cursor the provider offers when the visits fallback fires. */
|
|
176
|
+
declare const REITTI_VISITS_CURSOR = "visits";
|
|
177
|
+
interface ReittiProviderOptions {
|
|
178
|
+
/** Absolute HTTP(S) base URL of the self-hosted instance. */
|
|
179
|
+
baseUrl: string;
|
|
180
|
+
/** Pre-resolved API token from Remnic's secret reference mechanism. */
|
|
181
|
+
token: string;
|
|
182
|
+
/** Which documented header form to use; exactly one is sent. */
|
|
183
|
+
authMode?: ReittiAuthMode;
|
|
184
|
+
/** IANA zone used to bucket observations into local days (from location config). */
|
|
185
|
+
timezone: string;
|
|
186
|
+
/**
|
|
187
|
+
* When `true`, an empty timeline day falls back to `/api/v1/visits` as the
|
|
188
|
+
* place/visit source (second page, `REITTI_VISITS_CURSOR`). Default `false`.
|
|
189
|
+
*/
|
|
190
|
+
visitsFallback?: boolean;
|
|
191
|
+
fetchImpl?: typeof fetch;
|
|
192
|
+
timeoutMs?: number;
|
|
193
|
+
maxResponseBytes?: number;
|
|
194
|
+
sleep?: (ms: number) => Promise<void>;
|
|
195
|
+
now?: () => Date;
|
|
196
|
+
}
|
|
197
|
+
declare function createReittiProvider(options: ReittiProviderOptions): LocationProvider;
|
|
198
|
+
/**
|
|
199
|
+
* Idempotently register the Reitti provider with the core location registry.
|
|
200
|
+
* Returns `true` when this call performed the registration, `false` when a
|
|
201
|
+
* provider with this id was already registered.
|
|
202
|
+
*/
|
|
203
|
+
declare function ensureReittiProviderRegistered(options: ReittiProviderOptions): boolean;
|
|
204
|
+
|
|
205
|
+
export { type LocationWindow, REITTI_AUTH_MODES, REITTI_PLACE_TYPES, REITTI_PROVIDER_DISPLAY_NAME, REITTI_PROVIDER_ID, REITTI_TRANSPORT_MODES, REITTI_VISITS_CURSOR, ReittiApiError, type ReittiAuthMode, ReittiClient, type ReittiClientOptions, type ReittiDayRequest, type ReittiErrorKind, type ReittiPlaceType, type ReittiPlaceVisitSummary, type ReittiProviderOptions, type ReittiSignificantPlace, type ReittiTimelineEntry, type ReittiTransportMode, type ReittiVisitDetail, assertReittiTimezone, assertValidIanaTimezone, createReittiProvider, ensureReittiProviderRegistered, normalizeReittiBaseUrl, timelineObservations, visitSummaryObservations };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
// openclaw-engram: Local-first memory plugin
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import {
|
|
5
|
+
getLocationProvider,
|
|
6
|
+
isValidLocationDate as isValidLocationDate2,
|
|
7
|
+
registerLocationProvider
|
|
8
|
+
} from "@remnic/core/location";
|
|
9
|
+
|
|
10
|
+
// src/client.ts
|
|
11
|
+
import {
|
|
12
|
+
ConnectorApiError,
|
|
13
|
+
discardResponseBody,
|
|
14
|
+
retryingFetch
|
|
15
|
+
} from "@remnic/core/http-retry";
|
|
16
|
+
import { assertValidTimezone } from "@remnic/core";
|
|
17
|
+
import { isValidLocationDate } from "@remnic/core/location";
|
|
18
|
+
var REITTI_AUTH_MODES = ["x-api-token", "bearer"];
|
|
19
|
+
var REITTI_TRANSPORT_MODES = [
|
|
20
|
+
"WALKING",
|
|
21
|
+
"CYCLING",
|
|
22
|
+
"DRIVING",
|
|
23
|
+
"TRAIN",
|
|
24
|
+
"TRANSIT",
|
|
25
|
+
"MOTORCYCLE",
|
|
26
|
+
"SCOOTER",
|
|
27
|
+
"AIRPLANE",
|
|
28
|
+
"UNKNOWN"
|
|
29
|
+
];
|
|
30
|
+
var REITTI_PLACE_TYPES = [
|
|
31
|
+
"RESTAURANT",
|
|
32
|
+
"PARK",
|
|
33
|
+
"SHOP",
|
|
34
|
+
"HOME",
|
|
35
|
+
"WORK",
|
|
36
|
+
"HOSPITAL",
|
|
37
|
+
"SCHOOL",
|
|
38
|
+
"AIRPORT",
|
|
39
|
+
"TRAIN_STATION",
|
|
40
|
+
"GAS_STATION",
|
|
41
|
+
"HOTEL",
|
|
42
|
+
"BANK",
|
|
43
|
+
"PHARMACY",
|
|
44
|
+
"GYM",
|
|
45
|
+
"LIBRARY",
|
|
46
|
+
"CHURCH",
|
|
47
|
+
"CINEMA",
|
|
48
|
+
"CAFE",
|
|
49
|
+
"MUSEUM",
|
|
50
|
+
"LANDMARK",
|
|
51
|
+
"TOURIST_ATTRACTION",
|
|
52
|
+
"HISTORIC_SITE",
|
|
53
|
+
"MONUMENT",
|
|
54
|
+
"SHOPPING_MALL",
|
|
55
|
+
"MARKET",
|
|
56
|
+
"GALLERY",
|
|
57
|
+
"THEATER",
|
|
58
|
+
"GROCERY_STORE",
|
|
59
|
+
"ATM",
|
|
60
|
+
"OTHER"
|
|
61
|
+
];
|
|
62
|
+
var RETRYABLE_KINDS = ["rate-limit", "server", "network", "timeout"];
|
|
63
|
+
var ReittiApiError = class extends ConnectorApiError {
|
|
64
|
+
kind;
|
|
65
|
+
constructor(message, kind, status) {
|
|
66
|
+
super(message, status);
|
|
67
|
+
this.name = "ReittiApiError";
|
|
68
|
+
this.kind = kind;
|
|
69
|
+
}
|
|
70
|
+
get retryable() {
|
|
71
|
+
return RETRYABLE_KINDS.includes(this.kind);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
75
|
+
var DEFAULT_MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
76
|
+
function isRecord(value) {
|
|
77
|
+
return value !== null && typeof value === "object";
|
|
78
|
+
}
|
|
79
|
+
function assertReittiTimezone(timezone) {
|
|
80
|
+
if (typeof timezone !== "string" || timezone.length === 0) {
|
|
81
|
+
throw new TypeError("Reitti timezone must be a non-empty IANA zone string");
|
|
82
|
+
}
|
|
83
|
+
assertValidTimezone(timezone);
|
|
84
|
+
}
|
|
85
|
+
function assertValidIanaTimezone(timezone) {
|
|
86
|
+
warnLegacyTimezoneValidator();
|
|
87
|
+
assertReittiTimezone(timezone);
|
|
88
|
+
}
|
|
89
|
+
var warnedLegacyTimezoneValidator = false;
|
|
90
|
+
function warnLegacyTimezoneValidator() {
|
|
91
|
+
if (warnedLegacyTimezoneValidator) return;
|
|
92
|
+
warnedLegacyTimezoneValidator = true;
|
|
93
|
+
process.emitWarning(
|
|
94
|
+
"@remnic/connector-reitti: assertValidIanaTimezone is deprecated; use assertReittiTimezone instead.",
|
|
95
|
+
{ type: "DeprecationWarning", code: "REMNIC_DEP_REITTI_ASSERT_VALID_IANA_TIMEZONE" }
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
function normalizeReittiBaseUrl(raw) {
|
|
99
|
+
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
100
|
+
throw new TypeError("Reitti baseUrl must be a non-empty string");
|
|
101
|
+
}
|
|
102
|
+
let parsed;
|
|
103
|
+
try {
|
|
104
|
+
parsed = new URL(raw.trim());
|
|
105
|
+
} catch {
|
|
106
|
+
throw new RangeError(`Reitti baseUrl "${raw}" is not a valid absolute URL`);
|
|
107
|
+
}
|
|
108
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
109
|
+
throw new RangeError(`Reitti baseUrl must be http(s), got "${parsed.protocol}"`);
|
|
110
|
+
}
|
|
111
|
+
let path = parsed.pathname;
|
|
112
|
+
while (path.length > 1 && path.endsWith("/")) {
|
|
113
|
+
path = path.slice(0, -1);
|
|
114
|
+
}
|
|
115
|
+
return `${parsed.protocol}//${parsed.host}${path === "/" ? "" : path}`;
|
|
116
|
+
}
|
|
117
|
+
function assertFiniteInstant(iso, what) {
|
|
118
|
+
if (typeof iso !== "string" || iso.length === 0 || !Number.isFinite(Date.parse(iso))) {
|
|
119
|
+
throw new ReittiApiError(`Reitti response has an invalid ${what}`, "schema");
|
|
120
|
+
}
|
|
121
|
+
return iso;
|
|
122
|
+
}
|
|
123
|
+
function parsePlace(value, context) {
|
|
124
|
+
if (value === null || value === void 0) return null;
|
|
125
|
+
if (!isRecord(value)) {
|
|
126
|
+
throw new ReittiApiError(`Reitti ${context} place must be an object or null`, "schema");
|
|
127
|
+
}
|
|
128
|
+
const id = value.id;
|
|
129
|
+
if (id !== null && id !== void 0 && typeof id !== "number" && typeof id !== "string") {
|
|
130
|
+
throw new ReittiApiError(`Reitti ${context} place.id must be a number, string, or null`, "schema");
|
|
131
|
+
}
|
|
132
|
+
for (const field of ["name", "address", "city"]) {
|
|
133
|
+
const raw = value[field];
|
|
134
|
+
if (raw !== null && raw !== void 0 && typeof raw !== "string") {
|
|
135
|
+
throw new ReittiApiError(`Reitti ${context} place.${field} must be a string or null`, "schema");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const type = value.type;
|
|
139
|
+
if (type !== null && type !== void 0 && !REITTI_PLACE_TYPES.includes(type)) {
|
|
140
|
+
throw new ReittiApiError(`Reitti ${context} place.type "${String(type)}" is not a known Reitti place type`, "schema");
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
id: id ?? null,
|
|
144
|
+
name: value.name ?? null,
|
|
145
|
+
address: value.address ?? null,
|
|
146
|
+
city: value.city ?? null,
|
|
147
|
+
type: type ?? null
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function parseTimelineEntry(value) {
|
|
151
|
+
if (!isRecord(value)) {
|
|
152
|
+
throw new ReittiApiError("Reitti timeline row must be an object", "schema");
|
|
153
|
+
}
|
|
154
|
+
if (value.id === void 0 || typeof value.id !== "string" || value.id.length === 0) {
|
|
155
|
+
throw new ReittiApiError("Reitti timeline row requires a non-empty string id", "schema");
|
|
156
|
+
}
|
|
157
|
+
if (value.type !== "VISIT" && value.type !== "TRIP") {
|
|
158
|
+
throw new ReittiApiError(
|
|
159
|
+
`Reitti timeline row type "${String(value.type)}" must be VISIT or TRIP`,
|
|
160
|
+
"schema"
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
const distance = value.distanceMeters;
|
|
164
|
+
if (distance !== null && distance !== void 0 && (typeof distance !== "number" || !Number.isFinite(distance) || distance < 0)) {
|
|
165
|
+
throw new ReittiApiError("Reitti timeline row distanceMeters must be a non-negative number or null", "schema");
|
|
166
|
+
}
|
|
167
|
+
const mode = value.transportMode;
|
|
168
|
+
if (mode !== null && mode !== void 0 && !REITTI_TRANSPORT_MODES.includes(mode)) {
|
|
169
|
+
throw new ReittiApiError(
|
|
170
|
+
`Reitti timeline row transportMode "${String(mode)}" is not a known Reitti transport mode`,
|
|
171
|
+
"schema"
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
id: value.id,
|
|
176
|
+
type: value.type,
|
|
177
|
+
startTime: assertFiniteInstant(value.startTime, "startTime"),
|
|
178
|
+
endTime: assertFiniteInstant(value.endTime, "endTime"),
|
|
179
|
+
place: parsePlace(value.place, "timeline"),
|
|
180
|
+
transportMode: mode ?? null,
|
|
181
|
+
distanceMeters: distance ?? null
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function parseVisitSummary(value) {
|
|
185
|
+
if (!isRecord(value)) {
|
|
186
|
+
throw new ReittiApiError("Reitti visit summary must be an object", "schema");
|
|
187
|
+
}
|
|
188
|
+
const rawVisits = value.visits;
|
|
189
|
+
if (!Array.isArray(rawVisits)) {
|
|
190
|
+
throw new ReittiApiError("Reitti visit summary requires a visits array", "schema");
|
|
191
|
+
}
|
|
192
|
+
const visits = rawVisits.map((visit) => {
|
|
193
|
+
if (!isRecord(visit)) {
|
|
194
|
+
throw new ReittiApiError("Reitti visit detail must be an object", "schema");
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
startTime: assertFiniteInstant(visit.startTime, "visit startTime"),
|
|
198
|
+
endTime: assertFiniteInstant(visit.endTime, "visit endTime")
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
return { place: parsePlace(value.placeInfo, "visit summary"), visits };
|
|
202
|
+
}
|
|
203
|
+
var ReittiClient = class {
|
|
204
|
+
token;
|
|
205
|
+
baseUrl;
|
|
206
|
+
authMode;
|
|
207
|
+
fetchImpl;
|
|
208
|
+
timeoutMs;
|
|
209
|
+
maxResponseBytes;
|
|
210
|
+
sleep;
|
|
211
|
+
constructor(options) {
|
|
212
|
+
if (typeof options.token !== "string" || options.token.trim().length === 0) {
|
|
213
|
+
throw new TypeError("Reitti token must be a non-empty string (resolve the secret reference first)");
|
|
214
|
+
}
|
|
215
|
+
this.token = options.token.trim();
|
|
216
|
+
this.baseUrl = normalizeReittiBaseUrl(options.baseUrl);
|
|
217
|
+
const authMode = options.authMode ?? "x-api-token";
|
|
218
|
+
if (!REITTI_AUTH_MODES.includes(authMode)) {
|
|
219
|
+
throw new RangeError(`Reitti authMode "${String(authMode)}" must be one of ${REITTI_AUTH_MODES.join(", ")}`);
|
|
220
|
+
}
|
|
221
|
+
this.authMode = authMode;
|
|
222
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
223
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
224
|
+
this.maxResponseBytes = options.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;
|
|
225
|
+
this.sleep = options.sleep;
|
|
226
|
+
}
|
|
227
|
+
/** Every VISIT/TRIP entry overlapping the local day (primary source). */
|
|
228
|
+
async fetchTimeline(request) {
|
|
229
|
+
assertReittiTimezone(request.timezone);
|
|
230
|
+
if (!isValidLocationDate(request.date)) {
|
|
231
|
+
throw new RangeError(`Reitti date "${request.date}" must be a real YYYY-MM-DD day`);
|
|
232
|
+
}
|
|
233
|
+
const payload = await this.requestJson(
|
|
234
|
+
`/api/v1/timeline?date=${encodeURIComponent(request.date)}&timezone=${encodeURIComponent(request.timezone)}`,
|
|
235
|
+
request.signal
|
|
236
|
+
);
|
|
237
|
+
if (!Array.isArray(payload)) {
|
|
238
|
+
throw new ReittiApiError("Reitti /api/v1/timeline returned a non-array body", "schema");
|
|
239
|
+
}
|
|
240
|
+
return payload.map(parseTimelineEntry);
|
|
241
|
+
}
|
|
242
|
+
/** Processed visits grouped by place (fallback/enrichment source). */
|
|
243
|
+
async fetchVisits(request) {
|
|
244
|
+
assertReittiTimezone(request.timezone);
|
|
245
|
+
if (!isValidLocationDate(request.date)) {
|
|
246
|
+
throw new RangeError(`Reitti date "${request.date}" must be a real YYYY-MM-DD day`);
|
|
247
|
+
}
|
|
248
|
+
const payload = await this.requestJson(
|
|
249
|
+
`/api/v1/visits?date=${encodeURIComponent(request.date)}&timezone=${encodeURIComponent(request.timezone)}`,
|
|
250
|
+
request.signal
|
|
251
|
+
);
|
|
252
|
+
if (!isRecord(payload) || !Array.isArray(payload.places)) {
|
|
253
|
+
throw new ReittiApiError("Reitti /api/v1/visits returned an unexpected body (missing places array)", "schema");
|
|
254
|
+
}
|
|
255
|
+
return payload.places.map(parseVisitSummary);
|
|
256
|
+
}
|
|
257
|
+
async requestJson(pathAndQuery, signal) {
|
|
258
|
+
const response = await retryingFetch(`${this.baseUrl}${pathAndQuery}`, {
|
|
259
|
+
init: {
|
|
260
|
+
method: "GET",
|
|
261
|
+
headers: this.authHeaders()
|
|
262
|
+
},
|
|
263
|
+
fetchImpl: this.fetchImpl,
|
|
264
|
+
sleep: this.sleep,
|
|
265
|
+
signal,
|
|
266
|
+
timeoutMs: this.timeoutMs,
|
|
267
|
+
networkError: (err, _attempts, { timedOut }) => timedOut ? new ReittiApiError(`Reitti request timed out after ${this.timeoutMs}ms`, "timeout") : new ReittiApiError(
|
|
268
|
+
`Reitti request failed: ${err instanceof Error ? err.name : String(err)}`,
|
|
269
|
+
"network"
|
|
270
|
+
),
|
|
271
|
+
retryableError: (retryable) => new ReittiApiError(
|
|
272
|
+
`Reitti API responded ${retryable.status}`,
|
|
273
|
+
kindForStatus(retryable.status),
|
|
274
|
+
retryable.status
|
|
275
|
+
)
|
|
276
|
+
});
|
|
277
|
+
if (!response.ok) {
|
|
278
|
+
discardResponseBody(response);
|
|
279
|
+
throw new ReittiApiError(
|
|
280
|
+
`Reitti API responded ${response.status} for ${pathAndQuery.split("?")[0]}`,
|
|
281
|
+
kindForStatus(response.status),
|
|
282
|
+
response.status
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
const text = await this.readBounded(response);
|
|
286
|
+
try {
|
|
287
|
+
return JSON.parse(text);
|
|
288
|
+
} catch {
|
|
289
|
+
throw new ReittiApiError("Reitti API returned a non-JSON body", "invalid-json");
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/** Exactly one auth header, per the configured mode — never both. */
|
|
293
|
+
authHeaders() {
|
|
294
|
+
return this.authMode === "bearer" ? { Authorization: `Bearer ${this.token}`, Accept: "application/json" } : { "X-API-Token": this.token, Accept: "application/json" };
|
|
295
|
+
}
|
|
296
|
+
async readBounded(response) {
|
|
297
|
+
const declared = Number(response.headers.get("content-length"));
|
|
298
|
+
if (Number.isFinite(declared) && declared > this.maxResponseBytes) {
|
|
299
|
+
discardResponseBody(response);
|
|
300
|
+
throw new ReittiApiError(
|
|
301
|
+
`Reitti response exceeds the ${this.maxResponseBytes}-byte bound (${declared} bytes declared)`,
|
|
302
|
+
"response-too-large"
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
const reader = response.body?.getReader();
|
|
306
|
+
if (reader === void 0) {
|
|
307
|
+
const text = await response.text();
|
|
308
|
+
if (Buffer.byteLength(text, "utf8") > this.maxResponseBytes) {
|
|
309
|
+
throw new ReittiApiError(`Reitti response exceeds the ${this.maxResponseBytes}-byte bound`, "response-too-large");
|
|
310
|
+
}
|
|
311
|
+
return text;
|
|
312
|
+
}
|
|
313
|
+
const chunks = [];
|
|
314
|
+
let total = 0;
|
|
315
|
+
for (; ; ) {
|
|
316
|
+
const { done, value } = await reader.read();
|
|
317
|
+
if (done) break;
|
|
318
|
+
total += value?.byteLength ?? 0;
|
|
319
|
+
if (total > this.maxResponseBytes) {
|
|
320
|
+
await reader.cancel().catch(() => {
|
|
321
|
+
});
|
|
322
|
+
throw new ReittiApiError(`Reitti response exceeds the ${this.maxResponseBytes}-byte bound`, "response-too-large");
|
|
323
|
+
}
|
|
324
|
+
chunks.push(value);
|
|
325
|
+
}
|
|
326
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
function kindForStatus(status) {
|
|
330
|
+
if (status === 401 || status === 403) return "auth";
|
|
331
|
+
if (status === 429) return "rate-limit";
|
|
332
|
+
if (status >= 500) return "server";
|
|
333
|
+
return "http";
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// src/normalize.ts
|
|
337
|
+
var PLACE_KIND_BY_REITTI_TYPE = {
|
|
338
|
+
HOME: "home",
|
|
339
|
+
WORK: "work"
|
|
340
|
+
};
|
|
341
|
+
function placeLabel(place) {
|
|
342
|
+
return place.name ?? place.address ?? place.city ?? `Place ${place.id ?? "?"}`;
|
|
343
|
+
}
|
|
344
|
+
function namedPlace(place, fallbackKey) {
|
|
345
|
+
return {
|
|
346
|
+
id: place.id !== null ? `reitti:place:${place.id}` : `reitti:place:unresolved:${fallbackKey}`,
|
|
347
|
+
label: placeLabel(place),
|
|
348
|
+
kind: PLACE_KIND_BY_REITTI_TYPE[place.type ?? ""] ?? "poi"
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function tripPlace(entry) {
|
|
352
|
+
const mode = entry.transportMode ?? "UNKNOWN";
|
|
353
|
+
const km = entry.distanceMeters !== null && entry.distanceMeters > 0 ? ` \xB7 ${(entry.distanceMeters / 1e3).toFixed(1)} km` : "";
|
|
354
|
+
return { id: `reitti:trip:${entry.id}`, label: `Trip (${mode}${km})`, kind: "transit" };
|
|
355
|
+
}
|
|
356
|
+
function unnamedPlace(key) {
|
|
357
|
+
return { id: `reitti:visit:${key}`, label: "Unnamed place", kind: "other" };
|
|
358
|
+
}
|
|
359
|
+
function entryPlace(entry) {
|
|
360
|
+
if (entry.type === "TRIP") return tripPlace(entry);
|
|
361
|
+
return entry.place !== null ? namedPlace(entry.place, entry.id) : unnamedPlace(entry.id);
|
|
362
|
+
}
|
|
363
|
+
function intervalObservations(startMs, endMs, place, window) {
|
|
364
|
+
const clampedStart = Math.max(startMs, window.startMs);
|
|
365
|
+
const clampedEnd = Math.min(endMs, window.endMs - 1);
|
|
366
|
+
if (clampedStart > clampedEnd) return [];
|
|
367
|
+
const instants = clampedStart === clampedEnd ? [clampedStart] : [clampedStart, clampedEnd];
|
|
368
|
+
return instants.map((ms) => ({ observedAtUtc: new Date(ms).toISOString(), place }));
|
|
369
|
+
}
|
|
370
|
+
function windowBounds(window) {
|
|
371
|
+
const startMs = Date.parse(window.startUtc);
|
|
372
|
+
const endMs = Date.parse(window.endUtc);
|
|
373
|
+
if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || startMs >= endMs) {
|
|
374
|
+
throw new RangeError(`Reitti window must satisfy finite startUtc < endUtc, got [${window.startUtc}, ${window.endUtc})`);
|
|
375
|
+
}
|
|
376
|
+
return { startMs, endMs };
|
|
377
|
+
}
|
|
378
|
+
function sortByInstant(observations) {
|
|
379
|
+
return observations.sort(
|
|
380
|
+
(a, b) => a.observedAtUtc === b.observedAtUtc ? 0 : a.observedAtUtc < b.observedAtUtc ? -1 : 1
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
function timelineObservations(entries, window) {
|
|
384
|
+
const bounds = windowBounds(window);
|
|
385
|
+
const observations = [];
|
|
386
|
+
for (const entry of entries) {
|
|
387
|
+
const startMs = Date.parse(entry.startTime);
|
|
388
|
+
const endMs = Date.parse(entry.endTime);
|
|
389
|
+
if (!Number.isFinite(startMs) || !Number.isFinite(endMs)) {
|
|
390
|
+
throw new RangeError(`Reitti timeline entry "${entry.id}" has non-finite instants`);
|
|
391
|
+
}
|
|
392
|
+
observations.push(...intervalObservations(startMs, endMs, entryPlace(entry), bounds));
|
|
393
|
+
}
|
|
394
|
+
return sortByInstant(observations);
|
|
395
|
+
}
|
|
396
|
+
function visitSummaryObservations(summaries, window) {
|
|
397
|
+
const bounds = windowBounds(window);
|
|
398
|
+
const observations = [];
|
|
399
|
+
for (const summary of summaries) {
|
|
400
|
+
const fallbackKey = summary.visits[0]?.startTime ?? "";
|
|
401
|
+
const place = summary.place !== null ? namedPlace(summary.place, fallbackKey) : unnamedPlace(fallbackKey);
|
|
402
|
+
for (const visit of summary.visits) {
|
|
403
|
+
const startMs = Date.parse(visit.startTime);
|
|
404
|
+
const endMs = Date.parse(visit.endTime);
|
|
405
|
+
if (!Number.isFinite(startMs) || !Number.isFinite(endMs)) {
|
|
406
|
+
throw new RangeError("Reitti visit detail has non-finite instants");
|
|
407
|
+
}
|
|
408
|
+
observations.push(...intervalObservations(startMs, endMs, place, bounds));
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return sortByInstant(observations);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// src/index.ts
|
|
415
|
+
var REITTI_PROVIDER_ID = "reitti";
|
|
416
|
+
var REITTI_PROVIDER_DISPLAY_NAME = "Reitti";
|
|
417
|
+
var REITTI_VISITS_CURSOR = "visits";
|
|
418
|
+
function createReittiProvider(options) {
|
|
419
|
+
normalizeReittiBaseUrl(options.baseUrl);
|
|
420
|
+
if (typeof options.token !== "string" || options.token.trim().length === 0) {
|
|
421
|
+
throw new TypeError("Reitti provider requires a non-empty token (resolve the secret reference first)");
|
|
422
|
+
}
|
|
423
|
+
assertReittiTimezone(options.timezone);
|
|
424
|
+
if (options.visitsFallback !== void 0 && typeof options.visitsFallback !== "boolean") {
|
|
425
|
+
throw new TypeError("Reitti visitsFallback must be a boolean");
|
|
426
|
+
}
|
|
427
|
+
const client = new ReittiClient({
|
|
428
|
+
baseUrl: options.baseUrl,
|
|
429
|
+
token: options.token,
|
|
430
|
+
authMode: options.authMode,
|
|
431
|
+
fetchImpl: options.fetchImpl,
|
|
432
|
+
timeoutMs: options.timeoutMs,
|
|
433
|
+
maxResponseBytes: options.maxResponseBytes,
|
|
434
|
+
sleep: options.sleep
|
|
435
|
+
});
|
|
436
|
+
const timezone = options.timezone;
|
|
437
|
+
const visitsFallback = options.visitsFallback ?? false;
|
|
438
|
+
const dayFormatter = new Intl.DateTimeFormat("en-CA", {
|
|
439
|
+
timeZone: timezone,
|
|
440
|
+
year: "numeric",
|
|
441
|
+
month: "2-digit",
|
|
442
|
+
day: "2-digit"
|
|
443
|
+
});
|
|
444
|
+
const localDate = (ms) => dayFormatter.format(new Date(ms));
|
|
445
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
446
|
+
return {
|
|
447
|
+
id: REITTI_PROVIDER_ID,
|
|
448
|
+
displayName: REITTI_PROVIDER_DISPLAY_NAME,
|
|
449
|
+
async verify(signal) {
|
|
450
|
+
try {
|
|
451
|
+
const date = localDate(now().getTime());
|
|
452
|
+
if (!isValidLocationDate2(date)) {
|
|
453
|
+
return { ok: false, detail: `could not derive a valid local date in ${timezone}` };
|
|
454
|
+
}
|
|
455
|
+
await client.fetchTimeline({ date, timezone, signal });
|
|
456
|
+
return { ok: true };
|
|
457
|
+
} catch (error) {
|
|
458
|
+
if (error instanceof Error && error.name === "AbortError") throw error;
|
|
459
|
+
if (error instanceof ReittiApiError && error.kind === "auth") {
|
|
460
|
+
return {
|
|
461
|
+
ok: false,
|
|
462
|
+
detail: "Reitti rejected the token (401/403) \u2014 check the token value and authMode"
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
return { ok: false, detail: error instanceof Error ? error.name : "Reitti probe failed" };
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
async fetchObservations(opts) {
|
|
469
|
+
const startMs = Date.parse(opts.startUtc);
|
|
470
|
+
const endMs = Date.parse(opts.endUtc);
|
|
471
|
+
if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || startMs >= endMs) {
|
|
472
|
+
throw new RangeError(
|
|
473
|
+
`Reitti window must satisfy finite startUtc < endUtc, got [${opts.startUtc}, ${opts.endUtc})`
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
if (opts.cursor !== null && opts.cursor !== void 0 && opts.cursor !== REITTI_VISITS_CURSOR) {
|
|
477
|
+
throw new TypeError(`Reitti provider received an unknown cursor "${opts.cursor}"`);
|
|
478
|
+
}
|
|
479
|
+
const date = localDate(startMs);
|
|
480
|
+
if (!isValidLocationDate2(date)) {
|
|
481
|
+
throw new RangeError(`Reitti provider could not derive a valid local date from "${opts.startUtc}" in ${timezone}`);
|
|
482
|
+
}
|
|
483
|
+
const window = { startUtc: opts.startUtc, endUtc: opts.endUtc };
|
|
484
|
+
if (opts.cursor === REITTI_VISITS_CURSOR) {
|
|
485
|
+
const summaries = await client.fetchVisits({ date, timezone, signal: opts.signal });
|
|
486
|
+
return { observations: visitSummaryObservations(summaries, window), nextCursor: null };
|
|
487
|
+
}
|
|
488
|
+
const entries = await client.fetchTimeline({ date, timezone, signal: opts.signal });
|
|
489
|
+
const observations = timelineObservations(entries, window);
|
|
490
|
+
if (visitsFallback && observations.length === 0) {
|
|
491
|
+
return { observations, nextCursor: REITTI_VISITS_CURSOR };
|
|
492
|
+
}
|
|
493
|
+
return { observations, nextCursor: null };
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
function ensureReittiProviderRegistered(options) {
|
|
498
|
+
if (getLocationProvider(REITTI_PROVIDER_ID) !== void 0) return false;
|
|
499
|
+
registerLocationProvider(createReittiProvider(options));
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
export {
|
|
503
|
+
REITTI_AUTH_MODES,
|
|
504
|
+
REITTI_PLACE_TYPES,
|
|
505
|
+
REITTI_PROVIDER_DISPLAY_NAME,
|
|
506
|
+
REITTI_PROVIDER_ID,
|
|
507
|
+
REITTI_TRANSPORT_MODES,
|
|
508
|
+
REITTI_VISITS_CURSOR,
|
|
509
|
+
ReittiApiError,
|
|
510
|
+
ReittiClient,
|
|
511
|
+
assertReittiTimezone,
|
|
512
|
+
assertValidIanaTimezone,
|
|
513
|
+
createReittiProvider,
|
|
514
|
+
ensureReittiProviderRegistered,
|
|
515
|
+
normalizeReittiBaseUrl,
|
|
516
|
+
timelineObservations,
|
|
517
|
+
visitSummaryObservations
|
|
518
|
+
};
|
|
519
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/client.ts","../src/normalize.ts"],"sourcesContent":["/**\n * @remnic/connector-reitti — Reitti location provider (issue #2045).\n *\n * Optional adapter around the core location contract (#2044): an API client\n * and normalizer only. No file I/O, memory writes, config parsing, host SDK\n * imports, or LLM calls. `@remnic/core` never imports this package — hosts\n * register the provider through the core location registry, ideally via a\n * computed-specifier dynamic import so bundlers cannot statically resolve it:\n *\n * const mod = await import(\"@remnic/\" + \"connector-reitti\");\n * mod.ensureReittiProviderRegistered({ baseUrl, token, timezone });\n *\n * Gates: the master `location.enabled=false` and source `enabled=false`\n * short-circuits live in core config and the core pipeline — the provider is\n * never consulted for a disabled source. Tokens arrive pre-resolved from\n * Remnic's secret reference mechanism and never reach logs or error text.\n */\n\nimport {\n getLocationProvider,\n isValidLocationDate,\n registerLocationProvider,\n type LocationObservationPage,\n type LocationProvider,\n} from \"@remnic/core/location\";\n\nimport {\n ReittiApiError,\n ReittiClient,\n assertReittiTimezone,\n normalizeReittiBaseUrl,\n type ReittiAuthMode,\n} from \"./client.js\";\nimport { timelineObservations, visitSummaryObservations } from \"./normalize.js\";\n\nexport {\n ReittiApiError,\n ReittiClient,\n REITTI_AUTH_MODES,\n REITTI_PLACE_TYPES,\n REITTI_TRANSPORT_MODES,\n assertReittiTimezone,\n assertValidIanaTimezone,\n normalizeReittiBaseUrl,\n} from \"./client.js\";\nexport type {\n ReittiAuthMode,\n ReittiClientOptions,\n ReittiDayRequest,\n ReittiErrorKind,\n ReittiPlaceType,\n ReittiPlaceVisitSummary,\n ReittiSignificantPlace,\n ReittiTimelineEntry,\n ReittiTransportMode,\n ReittiVisitDetail,\n} from \"./client.js\";\nexport { timelineObservations, visitSummaryObservations } from \"./normalize.js\";\nexport type { LocationWindow } from \"./normalize.js\";\n\nexport const REITTI_PROVIDER_ID = \"reitti\";\nexport const REITTI_PROVIDER_DISPLAY_NAME = \"Reitti\";\n\n/** Second-page cursor the provider offers when the visits fallback fires. */\nexport const REITTI_VISITS_CURSOR = \"visits\";\n\nexport interface ReittiProviderOptions {\n /** Absolute HTTP(S) base URL of the self-hosted instance. */\n baseUrl: string;\n /** Pre-resolved API token from Remnic's secret reference mechanism. */\n token: string;\n /** Which documented header form to use; exactly one is sent. */\n authMode?: ReittiAuthMode;\n /** IANA zone used to bucket observations into local days (from location config). */\n timezone: string;\n /**\n * When `true`, an empty timeline day falls back to `/api/v1/visits` as the\n * place/visit source (second page, `REITTI_VISITS_CURSOR`). Default `false`.\n */\n visitsFallback?: boolean;\n fetchImpl?: typeof fetch;\n timeoutMs?: number;\n maxResponseBytes?: number;\n sleep?: (ms: number) => Promise<void>;\n now?: () => Date;\n}\n\nexport function createReittiProvider(options: ReittiProviderOptions): LocationProvider {\n normalizeReittiBaseUrl(options.baseUrl);\n if (typeof options.token !== \"string\" || options.token.trim().length === 0) {\n throw new TypeError(\"Reitti provider requires a non-empty token (resolve the secret reference first)\");\n }\n assertReittiTimezone(options.timezone);\n if (options.visitsFallback !== undefined && typeof options.visitsFallback !== \"boolean\") {\n throw new TypeError(\"Reitti visitsFallback must be a boolean\");\n }\n const client = new ReittiClient({\n baseUrl: options.baseUrl,\n token: options.token,\n authMode: options.authMode,\n fetchImpl: options.fetchImpl,\n timeoutMs: options.timeoutMs,\n maxResponseBytes: options.maxResponseBytes,\n sleep: options.sleep,\n });\n const timezone = options.timezone;\n const visitsFallback = options.visitsFallback ?? false;\n const dayFormatter = new Intl.DateTimeFormat(\"en-CA\", {\n timeZone: timezone,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n });\n const localDate = (ms: number): string => dayFormatter.format(new Date(ms));\n const now = options.now ?? (() => new Date());\n\n return {\n id: REITTI_PROVIDER_ID,\n displayName: REITTI_PROVIDER_DISPLAY_NAME,\n\n async verify(signal) {\n try {\n const date = localDate(now().getTime());\n if (!isValidLocationDate(date)) {\n return { ok: false, detail: `could not derive a valid local date in ${timezone}` };\n }\n await client.fetchTimeline({ date, timezone, signal });\n return { ok: true };\n } catch (error) {\n // A caller-driven abort is cancellation, not provider unavailability.\n if (error instanceof Error && error.name === \"AbortError\") throw error;\n if (error instanceof ReittiApiError && error.kind === \"auth\") {\n return {\n ok: false,\n detail: \"Reitti rejected the token (401/403) — check the token value and authMode\",\n };\n }\n return { ok: false, detail: error instanceof Error ? error.name : \"Reitti probe failed\" };\n }\n },\n\n async fetchObservations(opts): Promise<LocationObservationPage> {\n const startMs = Date.parse(opts.startUtc);\n const endMs = Date.parse(opts.endUtc);\n if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || startMs >= endMs) {\n throw new RangeError(\n `Reitti window must satisfy finite startUtc < endUtc, got [${opts.startUtc}, ${opts.endUtc})`,\n );\n }\n if (opts.cursor !== null && opts.cursor !== undefined && opts.cursor !== REITTI_VISITS_CURSOR) {\n throw new TypeError(`Reitti provider received an unknown cursor \"${opts.cursor}\"`);\n }\n const date = localDate(startMs);\n if (!isValidLocationDate(date)) {\n throw new RangeError(`Reitti provider could not derive a valid local date from \"${opts.startUtc}\" in ${timezone}`);\n }\n const window = { startUtc: opts.startUtc, endUtc: opts.endUtc };\n\n if (opts.cursor === REITTI_VISITS_CURSOR) {\n const summaries = await client.fetchVisits({ date, timezone, signal: opts.signal });\n return { observations: visitSummaryObservations(summaries, window), nextCursor: null };\n }\n const entries = await client.fetchTimeline({ date, timezone, signal: opts.signal });\n const observations = timelineObservations(entries, window);\n // Empty day vs failure (§22): a valid empty timeline is an explicit\n // empty result; the visits fallback, when enabled, is a second page.\n if (visitsFallback && observations.length === 0) {\n return { observations, nextCursor: REITTI_VISITS_CURSOR };\n }\n return { observations, nextCursor: null };\n },\n };\n}\n\n/**\n * Idempotently register the Reitti provider with the core location registry.\n * Returns `true` when this call performed the registration, `false` when a\n * provider with this id was already registered.\n */\nexport function ensureReittiProviderRegistered(options: ReittiProviderOptions): boolean {\n if (getLocationProvider(REITTI_PROVIDER_ID) !== undefined) return false;\n registerLocationProvider(createReittiProvider(options));\n return true;\n}\n","/**\n * Minimal Reitti API client (raw fetch, no SDK).\n *\n * Read-only, current-user endpoints only (issue #2045):\n * GET /api/v1/timeline?date=YYYY-MM-DD&timezone=<IANA>\n * GET /api/v1/visits?date=YYYY-MM-DD&timezone=<IANA>\n * Never /api/v1/visits/{userId} and never raw location-point endpoints.\n *\n * Auth uses exactly ONE configured header form — `X-API-Token` or\n * `Authorization: Bearer` — never both. The token never appears in any\n * error message, URL, or log line produced here.\n *\n * Transport distinguishes empty results from failures (§22): a valid empty\n * day is `[]`, while auth, rate-limit, server, network, timeout, JSON, size,\n * and schema problems are distinct `ReittiApiError` kinds. Retries apply to\n * transient GET failures only, always preserve the caller's abort signal,\n * and never advance any state (the client is stateless).\n */\n\nimport {\n ConnectorApiError,\n discardResponseBody,\n retryingFetch,\n} from \"@remnic/core/http-retry\";\nimport { assertValidTimezone } from \"@remnic/core\";\nimport { isValidLocationDate } from \"@remnic/core/location\";\n\nexport type ReittiAuthMode = \"x-api-token\" | \"bearer\";\n\nexport const REITTI_AUTH_MODES = [\"x-api-token\", \"bearer\"] as const;\n\n/** Reitti `TransportMode` values the normalizer preserves in trip labels. */\nexport const REITTI_TRANSPORT_MODES = [\n \"WALKING\",\n \"CYCLING\",\n \"DRIVING\",\n \"TRAIN\",\n \"TRANSIT\",\n \"MOTORCYCLE\",\n \"SCOOTER\",\n \"AIRPLANE\",\n \"UNKNOWN\",\n] as const;\n\n/** Reitti `SignificantPlace.PlaceType` values (upstream main, 2026-08). */\nexport const REITTI_PLACE_TYPES = [\n \"RESTAURANT\",\n \"PARK\",\n \"SHOP\",\n \"HOME\",\n \"WORK\",\n \"HOSPITAL\",\n \"SCHOOL\",\n \"AIRPORT\",\n \"TRAIN_STATION\",\n \"GAS_STATION\",\n \"HOTEL\",\n \"BANK\",\n \"PHARMACY\",\n \"GYM\",\n \"LIBRARY\",\n \"CHURCH\",\n \"CINEMA\",\n \"CAFE\",\n \"MUSEUM\",\n \"LANDMARK\",\n \"TOURIST_ATTRACTION\",\n \"HISTORIC_SITE\",\n \"MONUMENT\",\n \"SHOPPING_MALL\",\n \"MARKET\",\n \"GALLERY\",\n \"THEATER\",\n \"GROCERY_STORE\",\n \"ATM\",\n \"OTHER\",\n] as const;\n\nexport type ReittiTransportMode = (typeof REITTI_TRANSPORT_MODES)[number];\nexport type ReittiPlaceType = (typeof REITTI_PLACE_TYPES)[number];\n\n/** The place fields this connector consumes; other upstream fields are ignored. */\nexport interface ReittiSignificantPlace {\n id: number | string | null;\n name: string | null;\n address: string | null;\n city: string | null;\n type: ReittiPlaceType | null;\n}\n\n/** A validated `/api/v1/timeline` row (VISIT or TRIP). */\nexport interface ReittiTimelineEntry {\n id: string;\n type: \"VISIT\" | \"TRIP\";\n startTime: string;\n endTime: string;\n place: ReittiSignificantPlace | null;\n transportMode: ReittiTransportMode | null;\n distanceMeters: number | null;\n}\n\n/** A validated visit interval inside a `/api/v1/visits` place summary. */\nexport interface ReittiVisitDetail {\n startTime: string;\n endTime: string;\n}\n\n/** A validated `/api/v1/visits` place summary. */\nexport interface ReittiPlaceVisitSummary {\n place: ReittiSignificantPlace | null;\n visits: ReittiVisitDetail[];\n}\n\nexport type ReittiErrorKind =\n | \"auth\"\n | \"rate-limit\"\n | \"server\"\n | \"network\"\n | \"timeout\"\n | \"invalid-json\"\n | \"response-too-large\"\n | \"schema\"\n | \"http\";\n\nconst RETRYABLE_KINDS: readonly ReittiErrorKind[] = [\"rate-limit\", \"server\", \"network\", \"timeout\"];\n\nexport class ReittiApiError extends ConnectorApiError {\n readonly kind: ReittiErrorKind;\n\n constructor(message: string, kind: ReittiErrorKind, status?: number) {\n super(message, status);\n this.name = \"ReittiApiError\";\n this.kind = kind;\n }\n\n get retryable(): boolean {\n return RETRYABLE_KINDS.includes(this.kind);\n }\n}\n\nconst DEFAULT_TIMEOUT_MS = 30_000;\nconst MAX_RETRIES = 3;\nconst MAX_RETRY_DELAY_MS = 30_000;\nconst DEFAULT_MAX_RESPONSE_BYTES = 5 * 1024 * 1024;\n\nexport interface ReittiClientOptions {\n /** Absolute HTTP(S) base URL of the self-hosted Reitti instance. */\n baseUrl: string;\n /** Pre-resolved API token (from the secret store); never a placeholder. */\n token: string;\n /** Which documented header form to use; exactly one is sent. */\n authMode?: ReittiAuthMode;\n fetchImpl?: typeof fetch;\n timeoutMs?: number;\n maxResponseBytes?: number;\n sleep?: (ms: number) => Promise<void>;\n}\n\nexport interface ReittiDayRequest {\n date: string;\n timezone: string;\n signal?: AbortSignal;\n}\n\n/** Canonical boundary guard for this package: a non-null plain object (arrays are caught by field checks). */\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\";\n}\n\n/**\n * Validate an IANA timezone for Reitti requests: non-string/empty input is a\n * TypeError; an unknown zone is a RangeError from core's `assertValidTimezone`.\n */\nexport function assertReittiTimezone(timezone: string): void {\n if (typeof timezone !== \"string\" || timezone.length === 0) {\n throw new TypeError(\"Reitti timezone must be a non-empty IANA zone string\");\n }\n assertValidTimezone(timezone);\n}\n\n/**\n * @deprecated Alias of `assertReittiTimezone`, kept so pre-rename imports from\n * this published package keep resolving. Same TypeError/RangeError behavior.\n */\nexport function assertValidIanaTimezone(timezone: string): void {\n warnLegacyTimezoneValidator();\n assertReittiTimezone(timezone);\n}\n\nlet warnedLegacyTimezoneValidator = false;\n\nfunction warnLegacyTimezoneValidator(): void {\n if (warnedLegacyTimezoneValidator) return;\n warnedLegacyTimezoneValidator = true;\n process.emitWarning(\n \"@remnic/connector-reitti: assertValidIanaTimezone is deprecated; use assertReittiTimezone instead.\",\n { type: \"DeprecationWarning\", code: \"REMNIC_DEP_REITTI_ASSERT_VALID_IANA_TIMEZONE\" },\n );\n}\n\n/**\n * Validate and normalize the base URL: absolute HTTP(S), trailing slashes\n * stripped without touching path semantics (a sub-path install keeps its\n * prefix). Loop instead of a `/\\/+$/` regex — CodeQL polynomial-redos.\n */\nexport function normalizeReittiBaseUrl(raw: string): string {\n if (typeof raw !== \"string\" || raw.trim().length === 0) {\n throw new TypeError(\"Reitti baseUrl must be a non-empty string\");\n }\n let parsed: URL;\n try {\n parsed = new URL(raw.trim());\n } catch {\n throw new RangeError(`Reitti baseUrl \"${raw}\" is not a valid absolute URL`);\n }\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n throw new RangeError(`Reitti baseUrl must be http(s), got \"${parsed.protocol}\"`);\n }\n let path = parsed.pathname;\n while (path.length > 1 && path.endsWith(\"/\")) {\n path = path.slice(0, -1);\n }\n return `${parsed.protocol}//${parsed.host}${path === \"/\" ? \"\" : path}`;\n}\n\nfunction assertFiniteInstant(iso: unknown, what: string): string {\n if (typeof iso !== \"string\" || iso.length === 0 || !Number.isFinite(Date.parse(iso))) {\n throw new ReittiApiError(`Reitti response has an invalid ${what}`, \"schema\");\n }\n return iso;\n}\n\nfunction parsePlace(value: unknown, context: string): ReittiSignificantPlace | null {\n if (value === null || value === undefined) return null;\n if (!isRecord(value)) {\n throw new ReittiApiError(`Reitti ${context} place must be an object or null`, \"schema\");\n }\n const id = value.id;\n if (id !== null && id !== undefined && typeof id !== \"number\" && typeof id !== \"string\") {\n throw new ReittiApiError(`Reitti ${context} place.id must be a number, string, or null`, \"schema\");\n }\n for (const field of [\"name\", \"address\", \"city\"] as const) {\n const raw = value[field];\n if (raw !== null && raw !== undefined && typeof raw !== \"string\") {\n throw new ReittiApiError(`Reitti ${context} place.${field} must be a string or null`, \"schema\");\n }\n }\n const type = value.type;\n if (type !== null && type !== undefined && !(REITTI_PLACE_TYPES as readonly string[]).includes(type as string)) {\n throw new ReittiApiError(`Reitti ${context} place.type \"${String(type)}\" is not a known Reitti place type`, \"schema\");\n }\n return {\n id: (id ?? null) as ReittiSignificantPlace[\"id\"],\n name: (value.name ?? null) as string | null,\n address: (value.address ?? null) as string | null,\n city: (value.city ?? null) as string | null,\n type: (type ?? null) as ReittiPlaceType | null,\n };\n}\n\nfunction parseTimelineEntry(value: unknown): ReittiTimelineEntry {\n if (!isRecord(value)) {\n throw new ReittiApiError(\"Reitti timeline row must be an object\", \"schema\");\n }\n if (value.id === undefined || typeof value.id !== \"string\" || value.id.length === 0) {\n throw new ReittiApiError(\"Reitti timeline row requires a non-empty string id\", \"schema\");\n }\n if (value.type !== \"VISIT\" && value.type !== \"TRIP\") {\n throw new ReittiApiError(\n `Reitti timeline row type \"${String(value.type)}\" must be VISIT or TRIP`,\n \"schema\",\n );\n }\n const distance = value.distanceMeters;\n if (\n distance !== null &&\n distance !== undefined &&\n (typeof distance !== \"number\" || !Number.isFinite(distance) || distance < 0)\n ) {\n throw new ReittiApiError(\"Reitti timeline row distanceMeters must be a non-negative number or null\", \"schema\");\n }\n const mode = value.transportMode;\n if (\n mode !== null &&\n mode !== undefined &&\n !(REITTI_TRANSPORT_MODES as readonly string[]).includes(mode as string)\n ) {\n throw new ReittiApiError(\n `Reitti timeline row transportMode \"${String(mode)}\" is not a known Reitti transport mode`,\n \"schema\",\n );\n }\n return {\n id: value.id,\n type: value.type,\n startTime: assertFiniteInstant(value.startTime, \"startTime\"),\n endTime: assertFiniteInstant(value.endTime, \"endTime\"),\n place: parsePlace(value.place, \"timeline\"),\n transportMode: (mode ?? null) as ReittiTransportMode | null,\n distanceMeters: (distance ?? null) as number | null,\n };\n}\n\nfunction parseVisitSummary(value: unknown): ReittiPlaceVisitSummary {\n if (!isRecord(value)) {\n throw new ReittiApiError(\"Reitti visit summary must be an object\", \"schema\");\n }\n const rawVisits = value.visits;\n if (!Array.isArray(rawVisits)) {\n throw new ReittiApiError(\"Reitti visit summary requires a visits array\", \"schema\");\n }\n const visits: ReittiVisitDetail[] = rawVisits.map((visit) => {\n if (!isRecord(visit)) {\n throw new ReittiApiError(\"Reitti visit detail must be an object\", \"schema\");\n }\n return {\n startTime: assertFiniteInstant(visit.startTime, \"visit startTime\"),\n endTime: assertFiniteInstant(visit.endTime, \"visit endTime\"),\n };\n });\n return { place: parsePlace(value.placeInfo, \"visit summary\"), visits };\n}\n\nexport class ReittiClient {\n private readonly token: string;\n private readonly baseUrl: string;\n private readonly authMode: ReittiAuthMode;\n private readonly fetchImpl: typeof fetch;\n private readonly timeoutMs: number;\n private readonly maxResponseBytes: number;\n private readonly sleep: ((ms: number) => Promise<void>) | undefined;\n constructor(options: ReittiClientOptions) {\n if (typeof options.token !== \"string\" || options.token.trim().length === 0) {\n throw new TypeError(\"Reitti token must be a non-empty string (resolve the secret reference first)\");\n }\n this.token = options.token.trim();\n this.baseUrl = normalizeReittiBaseUrl(options.baseUrl);\n const authMode = options.authMode ?? \"x-api-token\";\n if (!(REITTI_AUTH_MODES as readonly string[]).includes(authMode)) {\n throw new RangeError(`Reitti authMode \"${String(authMode)}\" must be one of ${REITTI_AUTH_MODES.join(\", \")}`);\n }\n this.authMode = authMode;\n this.fetchImpl = options.fetchImpl ?? fetch;\n this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n this.maxResponseBytes = options.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;\n this.sleep = options.sleep;\n }\n\n /** Every VISIT/TRIP entry overlapping the local day (primary source). */\n async fetchTimeline(request: ReittiDayRequest): Promise<ReittiTimelineEntry[]> {\n assertReittiTimezone(request.timezone);\n if (!isValidLocationDate(request.date)) {\n throw new RangeError(`Reitti date \"${request.date}\" must be a real YYYY-MM-DD day`);\n }\n const payload = await this.requestJson(\n `/api/v1/timeline?date=${encodeURIComponent(request.date)}&timezone=${encodeURIComponent(request.timezone)}`,\n request.signal,\n );\n if (!Array.isArray(payload)) {\n throw new ReittiApiError(\"Reitti /api/v1/timeline returned a non-array body\", \"schema\");\n }\n return payload.map(parseTimelineEntry);\n }\n\n /** Processed visits grouped by place (fallback/enrichment source). */\n async fetchVisits(request: ReittiDayRequest): Promise<ReittiPlaceVisitSummary[]> {\n assertReittiTimezone(request.timezone);\n if (!isValidLocationDate(request.date)) {\n throw new RangeError(`Reitti date \"${request.date}\" must be a real YYYY-MM-DD day`);\n }\n const payload = await this.requestJson(\n `/api/v1/visits?date=${encodeURIComponent(request.date)}&timezone=${encodeURIComponent(request.timezone)}`,\n request.signal,\n );\n if (!isRecord(payload) || !Array.isArray(payload.places)) {\n throw new ReittiApiError(\"Reitti /api/v1/visits returned an unexpected body (missing places array)\", \"schema\");\n }\n return payload.places.map(parseVisitSummary);\n }\n\n private async requestJson(pathAndQuery: string, signal?: AbortSignal): Promise<unknown> {\n const response = await retryingFetch(`${this.baseUrl}${pathAndQuery}`, {\n init: {\n method: \"GET\",\n headers: this.authHeaders(),\n },\n fetchImpl: this.fetchImpl,\n sleep: this.sleep,\n signal,\n timeoutMs: this.timeoutMs,\n networkError: (err, _attempts, { timedOut }) =>\n timedOut\n ? new ReittiApiError(`Reitti request timed out after ${this.timeoutMs}ms`, \"timeout\")\n : new ReittiApiError(\n `Reitti request failed: ${err instanceof Error ? err.name : String(err)}`,\n \"network\",\n ),\n retryableError: (retryable) =>\n new ReittiApiError(\n `Reitti API responded ${retryable.status}`,\n kindForStatus(retryable.status),\n retryable.status,\n ),\n });\n if (!response.ok) {\n discardResponseBody(response);\n throw new ReittiApiError(\n `Reitti API responded ${response.status} for ${pathAndQuery.split(\"?\")[0]}`,\n kindForStatus(response.status),\n response.status,\n );\n }\n const text = await this.readBounded(response);\n try {\n return JSON.parse(text) as unknown;\n } catch {\n throw new ReittiApiError(\"Reitti API returned a non-JSON body\", \"invalid-json\");\n }\n }\n\n /** Exactly one auth header, per the configured mode — never both. */\n private authHeaders(): Record<string, string> {\n return this.authMode === \"bearer\"\n ? { Authorization: `Bearer ${this.token}`, Accept: \"application/json\" }\n : { \"X-API-Token\": this.token, Accept: \"application/json\" };\n }\n\n private async readBounded(response: Response): Promise<string> {\n const declared = Number(response.headers.get(\"content-length\"));\n if (Number.isFinite(declared) && declared > this.maxResponseBytes) {\n discardResponseBody(response);\n throw new ReittiApiError(\n `Reitti response exceeds the ${this.maxResponseBytes}-byte bound (${declared} bytes declared)`,\n \"response-too-large\",\n );\n }\n const reader = response.body?.getReader();\n if (reader === undefined) {\n const text = await response.text();\n if (Buffer.byteLength(text, \"utf8\") > this.maxResponseBytes) {\n throw new ReittiApiError(`Reitti response exceeds the ${this.maxResponseBytes}-byte bound`, \"response-too-large\");\n }\n return text;\n }\n const chunks: Uint8Array[] = [];\n let total = 0;\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n total += value?.byteLength ?? 0;\n if (total > this.maxResponseBytes) {\n await reader.cancel().catch(() => {});\n throw new ReittiApiError(`Reitti response exceeds the ${this.maxResponseBytes}-byte bound`, \"response-too-large\");\n }\n chunks.push(value);\n }\n return Buffer.concat(chunks).toString(\"utf8\");\n }\n}\n\nfunction kindForStatus(status: number): ReittiErrorKind {\n if (status === 401 || status === 403) return \"auth\";\n if (status === 429) return \"rate-limit\";\n if (status >= 500) return \"server\";\n return \"http\";\n}\n","/**\n * Reitti payload normalization (issue #2045).\n *\n * Maps validated `/api/v1/timeline` entries (VISIT/TRIP) and `/api/v1/visits`\n * place summaries onto the core `LocationObservation` instant model.\n *\n * Encoding: each interval becomes two observations — one at its start, one at\n * its end, both carrying the interval's place. Core's `observationSegments`\n * merges consecutive same-place observations back into half-open segments,\n * so this is the exact inverse: `A[8,9] T[9,10] B[10,11]` round-trips as\n * place-day segments without gaps.\n *\n * Preservation rules (the core observation type is intentionally narrow):\n * - place ids: `reitti:place:<placeId>` for named places; the entry id is\n * embedded for synthetic ids (`reitti:trip:<id>`, `reitti:visit:<id>`)\n * so source identifiers survive.\n * - nullable places: a TRIP (or a VISIT Reitti could not resolve) never\n * invents a place name. TRIP becomes a `transit`-kind observation whose\n * label carries the transport mode and distance; an unresolved VISIT\n * becomes an `other`-kind observation labeled \"Unnamed place\".\n * - duration is start→end, reconstructible from the two instants.\n * - encoded paths (`path`) are ignored by design (no track storage).\n * - coordinates are never emitted; retention is a core config decision.\n *\n * Instants are clamped into the requested half-open [startUtc, endUtc)\n * window: Reitti returns day-overlapping entries, while core requires every\n * observation to fall inside the window it asked for.\n */\n\nimport type { LocationObservation, LocationPlace } from \"@remnic/core/location\";\n\nimport type {\n ReittiPlaceVisitSummary,\n ReittiSignificantPlace,\n ReittiTimelineEntry,\n} from \"./client.js\";\n\nexport interface LocationWindow {\n startUtc: string;\n endUtc: string;\n}\n\nconst PLACE_KIND_BY_REITTI_TYPE: Record<string, LocationPlace[\"kind\"]> = {\n HOME: \"home\",\n WORK: \"work\",\n};\n\nfunction placeLabel(place: ReittiSignificantPlace): string {\n return place.name ?? place.address ?? place.city ?? `Place ${place.id ?? \"?\"}`;\n}\n\n/**\n * `fallbackKey` discriminates places Reitti left without an id: a shared\n * `unknown` id would let core's segment merging join two distinct places.\n */\nfunction namedPlace(place: ReittiSignificantPlace, fallbackKey: string): LocationPlace {\n return {\n id: place.id !== null ? `reitti:place:${place.id}` : `reitti:place:unresolved:${fallbackKey}`,\n label: placeLabel(place),\n kind: PLACE_KIND_BY_REITTI_TYPE[place.type ?? \"\"] ?? \"poi\",\n };\n}\n\nfunction tripPlace(entry: ReittiTimelineEntry): LocationPlace {\n const mode = entry.transportMode ?? \"UNKNOWN\";\n const km = entry.distanceMeters !== null && entry.distanceMeters > 0 ? ` · ${(entry.distanceMeters / 1000).toFixed(1)} km` : \"\";\n return { id: `reitti:trip:${entry.id}`, label: `Trip (${mode}${km})`, kind: \"transit\" };\n}\n\nfunction unnamedPlace(key: string): LocationPlace {\n return { id: `reitti:visit:${key}`, label: \"Unnamed place\", kind: \"other\" };\n}\n\nfunction entryPlace(entry: ReittiTimelineEntry): LocationPlace {\n if (entry.type === \"TRIP\") return tripPlace(entry);\n return entry.place !== null ? namedPlace(entry.place, entry.id) : unnamedPlace(entry.id);\n}\n\n/**\n * Observations for one interval, clamped to the window. A whole-window entry\n * (started before the day, ends after it) yields observations at the window\n * edges so its occupancy still lands in the day document.\n */\nfunction intervalObservations(\n startMs: number,\n endMs: number,\n place: LocationPlace,\n window: { startMs: number; endMs: number },\n): LocationObservation[] {\n const clampedStart = Math.max(startMs, window.startMs);\n const clampedEnd = Math.min(endMs, window.endMs - 1);\n if (clampedStart > clampedEnd) return [];\n const instants = clampedStart === clampedEnd ? [clampedStart] : [clampedStart, clampedEnd];\n return instants.map((ms) => ({ observedAtUtc: new Date(ms).toISOString(), place }));\n}\n\nfunction windowBounds(window: LocationWindow): { startMs: number; endMs: number } {\n const startMs = Date.parse(window.startUtc);\n const endMs = Date.parse(window.endUtc);\n if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || startMs >= endMs) {\n throw new RangeError(`Reitti window must satisfy finite startUtc < endUtc, got [${window.startUtc}, ${window.endUtc})`);\n }\n return { startMs, endMs };\n}\n\nfunction sortByInstant(observations: LocationObservation[]): LocationObservation[] {\n return observations.sort((a, b) =>\n a.observedAtUtc === b.observedAtUtc ? 0 : a.observedAtUtc < b.observedAtUtc ? -1 : 1,\n );\n}\n\n/** Normalize validated timeline entries (primary source) into observations. */\nexport function timelineObservations(\n entries: readonly ReittiTimelineEntry[],\n window: LocationWindow,\n): LocationObservation[] {\n const bounds = windowBounds(window);\n const observations: LocationObservation[] = [];\n for (const entry of entries) {\n const startMs = Date.parse(entry.startTime);\n const endMs = Date.parse(entry.endTime);\n if (!Number.isFinite(startMs) || !Number.isFinite(endMs)) {\n throw new RangeError(`Reitti timeline entry \"${entry.id}\" has non-finite instants`);\n }\n observations.push(...intervalObservations(startMs, endMs, entryPlace(entry), bounds));\n }\n return sortByInstant(observations);\n}\n\n/** Normalize validated visit summaries (fallback source) into observations. */\nexport function visitSummaryObservations(\n summaries: readonly ReittiPlaceVisitSummary[],\n window: LocationWindow,\n): LocationObservation[] {\n const bounds = windowBounds(window);\n const observations: LocationObservation[] = [];\n for (const summary of summaries) {\n const fallbackKey = summary.visits[0]?.startTime ?? \"\";\n const place = summary.place !== null ? namedPlace(summary.place, fallbackKey) : unnamedPlace(fallbackKey);\n for (const visit of summary.visits) {\n const startMs = Date.parse(visit.startTime);\n const endMs = Date.parse(visit.endTime);\n if (!Number.isFinite(startMs) || !Number.isFinite(endMs)) {\n throw new RangeError(\"Reitti visit detail has non-finite instants\");\n }\n observations.push(...intervalObservations(startMs, endMs, place, bounds));\n }\n }\n return sortByInstant(observations);\n}\n"],"mappings":"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","names":["isValidLocationDate","isValidLocationDate"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@remnic/connector-reitti",
|
|
3
|
+
"version": "9.69.64",
|
|
4
|
+
"description": "Reitti location connector for Remnic — read a self-hosted Reitti instance as a location provider",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public",
|
|
19
|
+
"provenance": true
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"@remnic/core": "^9.69.64"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"tsup": "^8.0.0",
|
|
26
|
+
"typescript": "^5.7.0",
|
|
27
|
+
"tsx": "^4.0.0",
|
|
28
|
+
"@remnic/core": "9.69.64"
|
|
29
|
+
},
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/joshuaswarren/remnic.git",
|
|
34
|
+
"directory": "packages/connector-reitti"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"remnic",
|
|
38
|
+
"memory",
|
|
39
|
+
"reitti",
|
|
40
|
+
"location",
|
|
41
|
+
"self-hosted"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup src/index.ts --format esm --dts",
|
|
45
|
+
"precheck-types": "node ../../scripts/ensure-bench-build-deps.mjs",
|
|
46
|
+
"check-types": "tsc --noEmit",
|
|
47
|
+
"test": "NODE_OPTIONS=\"${NODE_OPTIONS:+$NODE_OPTIONS }--conditions=remnic-source\" tsx --test src/client.test.ts src/normalize.test.ts src/index.test.ts"
|
|
48
|
+
}
|
|
49
|
+
}
|