@wedetech/sdk 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +152 -0
- package/dist/client.d.ts +92 -0
- package/dist/client.js +271 -0
- package/dist/crypto.d.ts +6 -0
- package/dist/crypto.js +114 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +24 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/types.d.ts +374 -0
- package/dist/types.js +4 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 wede tech
|
|
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,152 @@
|
|
|
1
|
+
# @wedetech/sdk
|
|
2
|
+
|
|
3
|
+
Official JavaScript and TypeScript client for the wede API.
|
|
4
|
+
|
|
5
|
+
wede keeps critical operations running whatever the state of the network. Operations captured without connectivity are synchronised when it returns, and each one is stored exactly once.
|
|
6
|
+
|
|
7
|
+
Full documentation: [docs.wede.pt](https://docs.wede.pt)
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
Node.js 20 or later, browsers, Cloudflare Workers and Pages Functions, Deno, Bun and React Native.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @wedetech/sdk
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Credentials
|
|
20
|
+
|
|
21
|
+
There are two ways to authenticate. Use the one that matches where the code runs.
|
|
22
|
+
|
|
23
|
+
**On a server**, use the tenant API key (`wede_live_...` or `wede_test_...`). Keep it as a secret and never ship it in a browser or mobile bundle.
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { WedeClient } from '@wedetech/sdk'
|
|
27
|
+
|
|
28
|
+
const wede = new WedeClient({ apiKey: process.env.WEDE_API_KEY! })
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**On a device or in a browser**, sign the user in and use the session token. What the token can do depends on the user's role in your tenant.
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
const { token } = await WedeClient.login(email, password)
|
|
35
|
+
const wede = new WedeClient({ accessToken: token })
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The token expires after 8 hours. Sign in again and call `wede.setAccessToken(newToken)`. Five failed sign-in attempts lock the account for 15 minutes.
|
|
39
|
+
|
|
40
|
+
| Option | Default | Description |
|
|
41
|
+
| --- | --- | --- |
|
|
42
|
+
| `apiKey` | | Tenant API key, server side only |
|
|
43
|
+
| `accessToken` | | User session token |
|
|
44
|
+
| `baseUrl` | `https://api.wede.pt` | API base URL |
|
|
45
|
+
| `timeout` | `10000` | Request timeout in milliseconds |
|
|
46
|
+
| `retries` | `3` | Attempts on network failure |
|
|
47
|
+
|
|
48
|
+
## Send an event
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
const res = await wede.sendEvent({
|
|
52
|
+
type: 'cardiac_arrest',
|
|
53
|
+
vertical: 'healthcare',
|
|
54
|
+
priority: 'high',
|
|
55
|
+
payload: { patient_ref: 'P-1042' },
|
|
56
|
+
metadata: { zone_id: 'LIS-01' },
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
res.event_id
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
| Field | Required | Values |
|
|
63
|
+
| --- | --- | --- |
|
|
64
|
+
| `type` | yes | An event type of your catalog for this vertical, or an event category (see below) |
|
|
65
|
+
| `vertical` | yes | A vertical registered for your tenant |
|
|
66
|
+
| `priority` | yes | `low`, `normal`, `high`, `critical` |
|
|
67
|
+
| `idempotency_key` | no | Unique per operation. Generated when omitted |
|
|
68
|
+
| `payload` | yes | Your operation data |
|
|
69
|
+
| `metadata` | no | `zone_id`, `operator_id`, `sdk_version`, `offline_generated_at`, `connectivity_state_at_generation` |
|
|
70
|
+
| `destination` | no | One-off recipient for this event: `phone`, `email` or `device_id`. Never stored |
|
|
71
|
+
|
|
72
|
+
Sending the same `idempotency_key` twice returns `409 duplicate_event`, so retries are always safe.
|
|
73
|
+
|
|
74
|
+
## Event types and categories
|
|
75
|
+
|
|
76
|
+
Event types are yours. You create them in your catalog, per vertical, and link each one to a category:
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
await wede.createCatalogAction({
|
|
80
|
+
vertical: 'healthcare',
|
|
81
|
+
code: 'cardiac_arrest',
|
|
82
|
+
name: 'Cardiac arrest',
|
|
83
|
+
category: 'EMERGENCY',
|
|
84
|
+
})
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Categories are a shared list kept by wede (`listEventCategories()`), for example `EMERGENCY`, `PAYMENT`, `BOOKING`, `STATUS_UPDATE`, `IDENTITY_CONFIRM`, `DISPATCH` and `OTHER`. The list stays open and grows over time. Using them is optional: an event can use a category code directly as its `type`.
|
|
88
|
+
|
|
89
|
+
An event's `type` is accepted when it is an active type of your catalog for the event's vertical, or an active category. Anything else returns `422 unknown_event_type`. Every event stores its category at the moment it is received; changing a type's category later only affects new events.
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
## Operations captured without connectivity
|
|
93
|
+
|
|
94
|
+
Capture each operation at the moment it happens and keep the returned object in your own storage. When connectivity returns, send them in one batch.
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { captureEvent } from '@wedetech/sdk'
|
|
98
|
+
|
|
99
|
+
const captured = await captureEvent({
|
|
100
|
+
type: 'delivery_confirmed',
|
|
101
|
+
vertical: 'logistics',
|
|
102
|
+
priority: 'normal',
|
|
103
|
+
payload: { parcel: 'PX-2231' },
|
|
104
|
+
})
|
|
105
|
+
// store `captured` locally, unchanged
|
|
106
|
+
|
|
107
|
+
const result = await wede.syncBatch(queuedEvents)
|
|
108
|
+
result.summary // { total, accepted, duplicates, rejected }
|
|
109
|
+
result.results // one entry per event
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
- Up to 500 events per call.
|
|
113
|
+
- `captureEvent` adds the capture time and an integrity checksum. An event whose content changes after capture is rejected.
|
|
114
|
+
- `duplicates` is your proof against double counting: an operation that already reached wede, online or in an earlier batch, is stored only once.
|
|
115
|
+
- Each event is checked on its own: an event with an unknown type or vertical, or a checksum that does not match, is rejected and the others are still accepted.
|
|
116
|
+
|
|
117
|
+
The same captured object can be sent online first with `sendEvent`. If the connection drops before you get the answer, keep it in the queue and send it in the batch.
|
|
118
|
+
|
|
119
|
+
## React Native
|
|
120
|
+
|
|
121
|
+
The package works in React Native as is. When the runtime has no Web Crypto, the SDK uses its own SHA-256 implementation, so no polyfill is needed. Use `accessToken`, never an API key, inside the app.
|
|
122
|
+
|
|
123
|
+
## Errors
|
|
124
|
+
|
|
125
|
+
| Class | When |
|
|
126
|
+
| --- | --- |
|
|
127
|
+
| `WedeAuthError` | Missing or invalid credentials (`401`) |
|
|
128
|
+
| `WedeError` | The API answered with an error. `status`, `code` and `details` carry the reason |
|
|
129
|
+
| `WedeNetworkError` | No response after all retries |
|
|
130
|
+
|
|
131
|
+
Common codes: `duplicate_event` (409), `unknown_event_type` (422), `unknown_vertical` (422), `invalid_payload` (422), `forbidden` (403).
|
|
132
|
+
|
|
133
|
+
## Method reference
|
|
134
|
+
|
|
135
|
+
| Area | Methods |
|
|
136
|
+
| --- | --- |
|
|
137
|
+
| Sign in | `WedeClient.login`, `setAccessToken` |
|
|
138
|
+
| Events | `sendEvent`, `listEvents`, `getEvent` |
|
|
139
|
+
| Event types | `listEventCategories`, `listCatalogActions`, `createCatalogAction`, `updateCatalogAction`, `deleteCatalogAction` |
|
|
140
|
+
| Offline sync | `captureEvent`, `syncBatch`, `getSyncStatus`, `registerDevice`, `syncDeviceQueue` |
|
|
141
|
+
| Connectivity | `getConnectivityStatus`, `reportConnectivity`, `listZones`, `getZone` |
|
|
142
|
+
| Tenant | `getTenantInfo`, `getUsage`, `getBilling`, `updateDispatchSettings` |
|
|
143
|
+
| Teams and dispatch | `listTeams`, `getTeam`, `updateMemberLocation`, `scoreTeams`, `dispatch`, `dispatchAction` |
|
|
144
|
+
| Missions | `listMissions`, `listMyMissions`, `getMission`, `updateMissionStatus` |
|
|
145
|
+
| Parsers | `listParsers`, `getParser`, `getActiveParser` |
|
|
146
|
+
| Webhooks | `listWebhooks`, `createWebhook`, `deleteWebhook` |
|
|
147
|
+
|
|
148
|
+
What each credential can call depends on its role in your tenant. A call outside it returns `403`.
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { WedeClientOptions, EventInput, SendEventResult, EventList, EventDetail, ListEventsParams, CapturedEvent, SyncBatchResult, SyncStatus, ConnectivityState, RegisterDeviceParams, SyncDeviceQueueParams, SyncDeviceQueueResult, ConnectivityStatus, ConnectivityReport, Zone, ZoneList, TenantInfo, Usage, Billing, DispatchSettings, Team, TeamList, ScoreTeamsParams, ScoreTeamsResult, DispatchParams, DispatchActionParams, Mission, MissionList, ListMissionsParams, MissionStatus, CatalogAction, CreateCatalogAction, UpdateCatalogAction, EventCategory, LoginResult, Parser, ParserList, Webhook, CreateWebhook } from './types.js';
|
|
2
|
+
export declare const SDK_VERSION = "2.0.0";
|
|
3
|
+
/**
|
|
4
|
+
* Integrity checksum expected by POST /v1/sync/batch:
|
|
5
|
+
* SHA-256 (hex) of JSON.stringify(payload) + idempotency_key + offline_generated_at.
|
|
6
|
+
*/
|
|
7
|
+
export declare function integrityHash(payload: Record<string, unknown>, idempotencyKey: string, offlineGeneratedAt: string): Promise<string>;
|
|
8
|
+
/**
|
|
9
|
+
* Captures an event at the moment it happens, without connectivity.
|
|
10
|
+
* Store the returned object as is; do not change the payload afterwards.
|
|
11
|
+
*/
|
|
12
|
+
export declare function captureEvent(event: EventInput, connectivityState?: ConnectivityState): Promise<CapturedEvent>;
|
|
13
|
+
export declare class WedeClient {
|
|
14
|
+
private readonly apiKey?;
|
|
15
|
+
private accessToken?;
|
|
16
|
+
private readonly baseUrl;
|
|
17
|
+
private readonly timeout;
|
|
18
|
+
private readonly retries;
|
|
19
|
+
constructor(options: WedeClientOptions);
|
|
20
|
+
/**
|
|
21
|
+
* Signs a user in and returns a session token for WedeClient({ accessToken }).
|
|
22
|
+
* Five failed attempts lock the account for 15 minutes.
|
|
23
|
+
*/
|
|
24
|
+
static login(email: string, password: string, options?: {
|
|
25
|
+
baseUrl?: string;
|
|
26
|
+
timeout?: number;
|
|
27
|
+
}): Promise<LoginResult>;
|
|
28
|
+
/** Replaces the session token, for example after signing in again. */
|
|
29
|
+
setAccessToken(token: string): void;
|
|
30
|
+
private authHeader;
|
|
31
|
+
private request;
|
|
32
|
+
private qs;
|
|
33
|
+
/** Submits an event. Same idempotency_key twice returns 409 duplicate_event. */
|
|
34
|
+
sendEvent(event: EventInput): Promise<SendEventResult>;
|
|
35
|
+
listEvents(params?: ListEventsParams): Promise<EventList>;
|
|
36
|
+
getEvent(eventId: string): Promise<EventDetail>;
|
|
37
|
+
/** Sends events captured with captureEvent(). Up to 500 per call. */
|
|
38
|
+
syncBatch(events: CapturedEvent[]): Promise<SyncBatchResult>;
|
|
39
|
+
getSyncStatus(): Promise<SyncStatus>;
|
|
40
|
+
registerDevice(params: RegisterDeviceParams): Promise<{
|
|
41
|
+
device_id: string;
|
|
42
|
+
registered: boolean;
|
|
43
|
+
}>;
|
|
44
|
+
syncDeviceQueue(params: SyncDeviceQueueParams): Promise<SyncDeviceQueueResult>;
|
|
45
|
+
getConnectivityStatus(zoneId?: string): Promise<ConnectivityStatus>;
|
|
46
|
+
reportConnectivity(report: ConnectivityReport): Promise<void>;
|
|
47
|
+
listZones(): Promise<ZoneList>;
|
|
48
|
+
getZone(zoneCode: string): Promise<Zone>;
|
|
49
|
+
getTenantInfo(): Promise<TenantInfo>;
|
|
50
|
+
/** from and to are ISO 8601 dates. */
|
|
51
|
+
getUsage(from: string, to: string): Promise<Usage>;
|
|
52
|
+
getBilling(): Promise<Billing>;
|
|
53
|
+
updateDispatchSettings(settings: Partial<DispatchSettings>): Promise<DispatchSettings>;
|
|
54
|
+
listTeams(): Promise<TeamList>;
|
|
55
|
+
getTeam(teamId: string): Promise<Team>;
|
|
56
|
+
updateMemberLocation(teamId: string, memberId: string, lat: number, lng: number): Promise<{
|
|
57
|
+
id: string;
|
|
58
|
+
lat: number;
|
|
59
|
+
lng: number;
|
|
60
|
+
last_seen: string;
|
|
61
|
+
}>;
|
|
62
|
+
/** Free teams ranked by best conditions (score 0 to 1, higher is better). */
|
|
63
|
+
scoreTeams(params: ScoreTeamsParams): Promise<ScoreTeamsResult>;
|
|
64
|
+
/** Dispatches a specific team to an event. */
|
|
65
|
+
dispatch(params: DispatchParams): Promise<Record<string, unknown>>;
|
|
66
|
+
/** Scores and, if automatic dispatch is on, dispatches for a catalog action. */
|
|
67
|
+
dispatchAction(params: DispatchActionParams): Promise<Record<string, unknown>>;
|
|
68
|
+
listMissions(params?: ListMissionsParams): Promise<MissionList>;
|
|
69
|
+
/** Missions assigned to the authenticated field user. */
|
|
70
|
+
listMyMissions(): Promise<MissionList>;
|
|
71
|
+
getMission(missionId: string): Promise<Mission>;
|
|
72
|
+
updateMissionStatus(missionId: string, status: MissionStatus, feedback?: Record<string, unknown>): Promise<Mission>;
|
|
73
|
+
/** The open list of event categories kept by wede. */
|
|
74
|
+
listEventCategories(): Promise<{
|
|
75
|
+
data: EventCategory[];
|
|
76
|
+
count: number;
|
|
77
|
+
}>;
|
|
78
|
+
listCatalogActions(vertical?: string): Promise<{
|
|
79
|
+
data: CatalogAction[];
|
|
80
|
+
}>;
|
|
81
|
+
createCatalogAction(action: CreateCatalogAction): Promise<CatalogAction>;
|
|
82
|
+
updateCatalogAction(actionId: string, changes: UpdateCatalogAction): Promise<CatalogAction>;
|
|
83
|
+
deleteCatalogAction(actionId: string): Promise<void>;
|
|
84
|
+
listParsers(): Promise<ParserList>;
|
|
85
|
+
getParser(parserId: string): Promise<Parser>;
|
|
86
|
+
getActiveParser(vertical: string): Promise<Parser>;
|
|
87
|
+
listWebhooks(): Promise<{
|
|
88
|
+
data: Webhook[];
|
|
89
|
+
}>;
|
|
90
|
+
createWebhook(webhook: CreateWebhook): Promise<Webhook>;
|
|
91
|
+
deleteWebhook(webhookId: string): Promise<void>;
|
|
92
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { WedeError, WedeAuthError, WedeNetworkError } from './errors.js';
|
|
2
|
+
import { sha256Hex, uuid } from './crypto.js';
|
|
3
|
+
const DEFAULT_BASE_URL = 'https://api.wede.pt';
|
|
4
|
+
const DEFAULT_TIMEOUT = 10_000;
|
|
5
|
+
const DEFAULT_RETRIES = 3;
|
|
6
|
+
export const SDK_VERSION = '2.0.0';
|
|
7
|
+
/**
|
|
8
|
+
* Integrity checksum expected by POST /v1/sync/batch:
|
|
9
|
+
* SHA-256 (hex) of JSON.stringify(payload) + idempotency_key + offline_generated_at.
|
|
10
|
+
*/
|
|
11
|
+
export async function integrityHash(payload, idempotencyKey, offlineGeneratedAt) {
|
|
12
|
+
return sha256Hex(JSON.stringify(payload) + idempotencyKey + offlineGeneratedAt);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Captures an event at the moment it happens, without connectivity.
|
|
16
|
+
* Store the returned object as is; do not change the payload afterwards.
|
|
17
|
+
*/
|
|
18
|
+
export async function captureEvent(event, connectivityState) {
|
|
19
|
+
const idempotency_key = event.idempotency_key ?? uuid();
|
|
20
|
+
const offline_generated_at = new Date().toISOString();
|
|
21
|
+
const integrity_hash = await integrityHash(event.payload, idempotency_key, offline_generated_at);
|
|
22
|
+
const captured = {
|
|
23
|
+
type: event.type,
|
|
24
|
+
vertical: event.vertical,
|
|
25
|
+
priority: event.priority,
|
|
26
|
+
idempotency_key,
|
|
27
|
+
payload: event.payload,
|
|
28
|
+
offline_generated_at,
|
|
29
|
+
integrity_hash,
|
|
30
|
+
};
|
|
31
|
+
if (connectivityState)
|
|
32
|
+
captured.connectivity_state_at_generation = connectivityState;
|
|
33
|
+
return captured;
|
|
34
|
+
}
|
|
35
|
+
export class WedeClient {
|
|
36
|
+
apiKey;
|
|
37
|
+
accessToken;
|
|
38
|
+
baseUrl;
|
|
39
|
+
timeout;
|
|
40
|
+
retries;
|
|
41
|
+
constructor(options) {
|
|
42
|
+
if (!options?.apiKey && !options?.accessToken)
|
|
43
|
+
throw new WedeAuthError('apiKey or accessToken is required');
|
|
44
|
+
if (options.apiKey && options.accessToken)
|
|
45
|
+
throw new WedeAuthError('Use apiKey or accessToken, not both');
|
|
46
|
+
this.apiKey = options.apiKey;
|
|
47
|
+
this.accessToken = options.accessToken;
|
|
48
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
49
|
+
this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
50
|
+
this.retries = options.retries ?? DEFAULT_RETRIES;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Signs a user in and returns a session token for WedeClient({ accessToken }).
|
|
54
|
+
* Five failed attempts lock the account for 15 minutes.
|
|
55
|
+
*/
|
|
56
|
+
static async login(email, password, options) {
|
|
57
|
+
const baseUrl = (options?.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
58
|
+
const controller = new AbortController();
|
|
59
|
+
const timer = setTimeout(() => controller.abort(), options?.timeout ?? DEFAULT_TIMEOUT);
|
|
60
|
+
let res;
|
|
61
|
+
try {
|
|
62
|
+
res = await fetch(baseUrl + '/v1/auth/login', {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: { 'Content-Type': 'application/json' },
|
|
65
|
+
body: JSON.stringify({ email, password }),
|
|
66
|
+
signal: controller.signal,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
throw new WedeNetworkError(err instanceof Error ? err.message : undefined);
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
}
|
|
75
|
+
const data = await res.json().catch(() => ({}));
|
|
76
|
+
if (res.status === 401)
|
|
77
|
+
throw new WedeAuthError(data?.message ?? 'Invalid email or password');
|
|
78
|
+
if (!res.ok)
|
|
79
|
+
throw new WedeError(data?.message ?? `Login failed with status ${res.status}`, data?.error ?? 'api_error', res.status);
|
|
80
|
+
return data;
|
|
81
|
+
}
|
|
82
|
+
/** Replaces the session token, for example after signing in again. */
|
|
83
|
+
setAccessToken(token) {
|
|
84
|
+
if (!this.accessToken)
|
|
85
|
+
throw new WedeAuthError('This client uses an API key');
|
|
86
|
+
this.accessToken = token;
|
|
87
|
+
}
|
|
88
|
+
authHeader() {
|
|
89
|
+
return this.apiKey ? { 'X-Wede-API-Key': this.apiKey } : { Authorization: `Bearer ${this.accessToken}` };
|
|
90
|
+
}
|
|
91
|
+
async request(method, path, body) {
|
|
92
|
+
let lastError;
|
|
93
|
+
for (let attempt = 1; attempt <= this.retries; attempt++) {
|
|
94
|
+
const controller = new AbortController();
|
|
95
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
96
|
+
let res;
|
|
97
|
+
try {
|
|
98
|
+
res = await fetch(this.baseUrl + path, {
|
|
99
|
+
method,
|
|
100
|
+
headers: { ...this.authHeader(), 'Content-Type': 'application/json' },
|
|
101
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
102
|
+
signal: controller.signal,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
clearTimeout(timer);
|
|
107
|
+
lastError = err;
|
|
108
|
+
if (attempt < this.retries)
|
|
109
|
+
await new Promise(r => setTimeout(r, 300 * attempt));
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
if (res.status === 204)
|
|
114
|
+
return undefined;
|
|
115
|
+
const text = await res.text();
|
|
116
|
+
let data = undefined;
|
|
117
|
+
if (text) {
|
|
118
|
+
try {
|
|
119
|
+
data = JSON.parse(text);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
data = { message: text };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (res.status === 401)
|
|
126
|
+
throw new WedeAuthError(data?.message);
|
|
127
|
+
if (!res.ok) {
|
|
128
|
+
throw new WedeError(data?.message ?? `Request failed with status ${res.status}`, data?.error ?? 'api_error', res.status, data?.details);
|
|
129
|
+
}
|
|
130
|
+
return data;
|
|
131
|
+
}
|
|
132
|
+
throw new WedeNetworkError(lastError instanceof Error ? lastError.message : undefined);
|
|
133
|
+
}
|
|
134
|
+
qs(params) {
|
|
135
|
+
if (!params)
|
|
136
|
+
return '';
|
|
137
|
+
const entries = Object.entries(params).filter(([, v]) => v !== undefined && v !== null);
|
|
138
|
+
if (entries.length === 0)
|
|
139
|
+
return '';
|
|
140
|
+
return '?' + new URLSearchParams(entries.map(([k, v]) => [k, String(v)])).toString();
|
|
141
|
+
}
|
|
142
|
+
// ── Events ─────────────────────────────────────────────────────────────
|
|
143
|
+
/** Submits an event. Same idempotency_key twice returns 409 duplicate_event. */
|
|
144
|
+
sendEvent(event) {
|
|
145
|
+
return this.request('POST', '/v1/events', { ...event, idempotency_key: event.idempotency_key ?? uuid() });
|
|
146
|
+
}
|
|
147
|
+
listEvents(params) {
|
|
148
|
+
return this.request('GET', '/v1/events' + this.qs(params));
|
|
149
|
+
}
|
|
150
|
+
getEvent(eventId) {
|
|
151
|
+
return this.request('GET', '/v1/events/' + encodeURIComponent(eventId));
|
|
152
|
+
}
|
|
153
|
+
// ── Offline capture and batch sync ─────────────────────────────────────
|
|
154
|
+
/** Sends events captured with captureEvent(). Up to 500 per call. */
|
|
155
|
+
syncBatch(events) {
|
|
156
|
+
return this.request('POST', '/v1/sync/batch', { events });
|
|
157
|
+
}
|
|
158
|
+
getSyncStatus() {
|
|
159
|
+
return this.request('GET', '/v1/sync/status');
|
|
160
|
+
}
|
|
161
|
+
// ── Device queue ───────────────────────────────────────────────────────
|
|
162
|
+
registerDevice(params) {
|
|
163
|
+
return this.request('POST', '/v1/devices/register', params);
|
|
164
|
+
}
|
|
165
|
+
syncDeviceQueue(params) {
|
|
166
|
+
return this.request('POST', '/v1/devices/sync', params);
|
|
167
|
+
}
|
|
168
|
+
// ── Connectivity and zones ─────────────────────────────────────────────
|
|
169
|
+
getConnectivityStatus(zoneId) {
|
|
170
|
+
return this.request('GET', '/v1/connectivity/status' + this.qs({ zone_id: zoneId }));
|
|
171
|
+
}
|
|
172
|
+
reportConnectivity(report) {
|
|
173
|
+
return this.request('POST', '/v1/connectivity/report', {
|
|
174
|
+
...report,
|
|
175
|
+
detected_at: report.detected_at ?? new Date().toISOString(),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
listZones() {
|
|
179
|
+
return this.request('GET', '/v1/tenant/zones');
|
|
180
|
+
}
|
|
181
|
+
getZone(zoneCode) {
|
|
182
|
+
return this.request('GET', '/v1/tenant/zones/' + encodeURIComponent(zoneCode));
|
|
183
|
+
}
|
|
184
|
+
// ── Tenant ─────────────────────────────────────────────────────────────
|
|
185
|
+
getTenantInfo() {
|
|
186
|
+
return this.request('GET', '/v1/tenant/me');
|
|
187
|
+
}
|
|
188
|
+
/** from and to are ISO 8601 dates. */
|
|
189
|
+
getUsage(from, to) {
|
|
190
|
+
return this.request('GET', '/v1/tenant/usage' + this.qs({ from, to }));
|
|
191
|
+
}
|
|
192
|
+
getBilling() {
|
|
193
|
+
return this.request('GET', '/v1/tenant/billing');
|
|
194
|
+
}
|
|
195
|
+
updateDispatchSettings(settings) {
|
|
196
|
+
return this.request('PATCH', '/v1/tenant/dispatch-settings', settings);
|
|
197
|
+
}
|
|
198
|
+
// ── Teams and dispatch ─────────────────────────────────────────────────
|
|
199
|
+
listTeams() {
|
|
200
|
+
return this.request('GET', '/v1/teams');
|
|
201
|
+
}
|
|
202
|
+
getTeam(teamId) {
|
|
203
|
+
return this.request('GET', '/v1/teams/' + encodeURIComponent(teamId));
|
|
204
|
+
}
|
|
205
|
+
updateMemberLocation(teamId, memberId, lat, lng) {
|
|
206
|
+
return this.request('PATCH', `/v1/teams/${encodeURIComponent(teamId)}/members/${encodeURIComponent(memberId)}/location`, { lat, lng });
|
|
207
|
+
}
|
|
208
|
+
/** Free teams ranked by best conditions (score 0 to 1, higher is better). */
|
|
209
|
+
scoreTeams(params) {
|
|
210
|
+
return this.request('POST', '/v1/teams/dispatch/score', params);
|
|
211
|
+
}
|
|
212
|
+
/** Dispatches a specific team to an event. */
|
|
213
|
+
dispatch(params) {
|
|
214
|
+
return this.request('POST', '/v1/teams/dispatch', params);
|
|
215
|
+
}
|
|
216
|
+
/** Scores and, if automatic dispatch is on, dispatches for a catalog action. */
|
|
217
|
+
dispatchAction(params) {
|
|
218
|
+
return this.request('POST', '/v1/teams/dispatch/action', params);
|
|
219
|
+
}
|
|
220
|
+
// ── Missions ───────────────────────────────────────────────────────────
|
|
221
|
+
listMissions(params) {
|
|
222
|
+
return this.request('GET', '/v1/missions' + this.qs(params));
|
|
223
|
+
}
|
|
224
|
+
/** Missions assigned to the authenticated field user. */
|
|
225
|
+
listMyMissions() {
|
|
226
|
+
return this.request('GET', '/v1/missions/my');
|
|
227
|
+
}
|
|
228
|
+
getMission(missionId) {
|
|
229
|
+
return this.request('GET', '/v1/missions/' + encodeURIComponent(missionId));
|
|
230
|
+
}
|
|
231
|
+
updateMissionStatus(missionId, status, feedback) {
|
|
232
|
+
return this.request('PATCH', `/v1/missions/${encodeURIComponent(missionId)}/status`, { status, feedback });
|
|
233
|
+
}
|
|
234
|
+
// ── Event categories ───────────────────────────────────────────────────
|
|
235
|
+
/** The open list of event categories kept by wede. */
|
|
236
|
+
listEventCategories() {
|
|
237
|
+
return this.request('GET', '/v1/event-categories');
|
|
238
|
+
}
|
|
239
|
+
// ── Catalog (your event types) and parsers ─────────────────────────────
|
|
240
|
+
listCatalogActions(vertical) {
|
|
241
|
+
return this.request('GET', '/v1/catalog/actions' + this.qs({ vertical }));
|
|
242
|
+
}
|
|
243
|
+
createCatalogAction(action) {
|
|
244
|
+
return this.request('POST', '/v1/catalog/actions', action);
|
|
245
|
+
}
|
|
246
|
+
updateCatalogAction(actionId, changes) {
|
|
247
|
+
return this.request('PATCH', '/v1/catalog/actions/' + encodeURIComponent(actionId), changes);
|
|
248
|
+
}
|
|
249
|
+
deleteCatalogAction(actionId) {
|
|
250
|
+
return this.request('DELETE', '/v1/catalog/actions/' + encodeURIComponent(actionId));
|
|
251
|
+
}
|
|
252
|
+
listParsers() {
|
|
253
|
+
return this.request('GET', '/v1/parsers');
|
|
254
|
+
}
|
|
255
|
+
getParser(parserId) {
|
|
256
|
+
return this.request('GET', '/v1/parsers/' + encodeURIComponent(parserId));
|
|
257
|
+
}
|
|
258
|
+
getActiveParser(vertical) {
|
|
259
|
+
return this.request('GET', `/v1/parsers/vertical/${encodeURIComponent(vertical)}/active`);
|
|
260
|
+
}
|
|
261
|
+
// ── Webhooks ───────────────────────────────────────────────────────────
|
|
262
|
+
listWebhooks() {
|
|
263
|
+
return this.request('GET', '/v1/webhooks');
|
|
264
|
+
}
|
|
265
|
+
createWebhook(webhook) {
|
|
266
|
+
return this.request('POST', '/v1/webhooks', webhook);
|
|
267
|
+
}
|
|
268
|
+
deleteWebhook(webhookId) {
|
|
269
|
+
return this.request('DELETE', '/v1/webhooks/' + encodeURIComponent(webhookId));
|
|
270
|
+
}
|
|
271
|
+
}
|
package/dist/crypto.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Plain JavaScript SHA-256, used only when Web Crypto is not available. */
|
|
2
|
+
export declare function sha256Fallback(data: Uint8Array): Uint8Array;
|
|
3
|
+
/** SHA-256 of a UTF-8 string, as lowercase hex. */
|
|
4
|
+
export declare function sha256Hex(text: string): Promise<string>;
|
|
5
|
+
/** Random UUID v4. */
|
|
6
|
+
export declare function uuid(): string;
|
package/dist/crypto.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Runtime-independent helpers. Use Web Crypto when the runtime has it (Node 20+,
|
|
2
|
+
// browsers, Workers, Deno, Bun) and fall back to plain JavaScript otherwise
|
|
3
|
+
// (React Native without a crypto polyfill).
|
|
4
|
+
function utf8(text) {
|
|
5
|
+
if (typeof TextEncoder !== 'undefined')
|
|
6
|
+
return new TextEncoder().encode(text);
|
|
7
|
+
const out = [];
|
|
8
|
+
for (const ch of text) {
|
|
9
|
+
let cp = ch.codePointAt(0);
|
|
10
|
+
if (cp >= 0xd800 && cp <= 0xdfff)
|
|
11
|
+
cp = 0xfffd; // lone surrogate, same as TextEncoder
|
|
12
|
+
if (cp < 0x80)
|
|
13
|
+
out.push(cp);
|
|
14
|
+
else if (cp < 0x800)
|
|
15
|
+
out.push(0xc0 | (cp >> 6), 0x80 | (cp & 63));
|
|
16
|
+
else if (cp < 0x10000)
|
|
17
|
+
out.push(0xe0 | (cp >> 12), 0x80 | ((cp >> 6) & 63), 0x80 | (cp & 63));
|
|
18
|
+
else
|
|
19
|
+
out.push(0xf0 | (cp >> 18), 0x80 | ((cp >> 12) & 63), 0x80 | ((cp >> 6) & 63), 0x80 | (cp & 63));
|
|
20
|
+
}
|
|
21
|
+
return new Uint8Array(out);
|
|
22
|
+
}
|
|
23
|
+
function hex(bytes) {
|
|
24
|
+
let s = '';
|
|
25
|
+
for (let i = 0; i < bytes.length; i++)
|
|
26
|
+
s += bytes[i].toString(16).padStart(2, '0');
|
|
27
|
+
return s;
|
|
28
|
+
}
|
|
29
|
+
const K = new Uint32Array([
|
|
30
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
31
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
32
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
33
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
34
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
35
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
36
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
37
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
38
|
+
]);
|
|
39
|
+
/** Plain JavaScript SHA-256, used only when Web Crypto is not available. */
|
|
40
|
+
export function sha256Fallback(data) {
|
|
41
|
+
const bitLen = data.length * 8;
|
|
42
|
+
const padded = new Uint8Array(((data.length + 9 + 63) >> 6) << 6);
|
|
43
|
+
padded.set(data);
|
|
44
|
+
padded[data.length] = 0x80;
|
|
45
|
+
const view = new DataView(padded.buffer);
|
|
46
|
+
view.setUint32(padded.length - 8, Math.floor(bitLen / 0x100000000));
|
|
47
|
+
view.setUint32(padded.length - 4, bitLen >>> 0);
|
|
48
|
+
const h = new Uint32Array([0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]);
|
|
49
|
+
const w = new Uint32Array(64);
|
|
50
|
+
const rotr = (x, n) => (x >>> n) | (x << (32 - n));
|
|
51
|
+
for (let off = 0; off < padded.length; off += 64) {
|
|
52
|
+
for (let i = 0; i < 16; i++)
|
|
53
|
+
w[i] = view.getUint32(off + i * 4);
|
|
54
|
+
for (let i = 16; i < 64; i++) {
|
|
55
|
+
const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >>> 3);
|
|
56
|
+
const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >>> 10);
|
|
57
|
+
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) >>> 0;
|
|
58
|
+
}
|
|
59
|
+
let [a, b, c, d, e, f, g, hh] = [h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7]];
|
|
60
|
+
for (let i = 0; i < 64; i++) {
|
|
61
|
+
const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
62
|
+
const ch = (e & f) ^ (~e & g);
|
|
63
|
+
const t1 = (hh + S1 + ch + K[i] + w[i]) >>> 0;
|
|
64
|
+
const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
65
|
+
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
66
|
+
const t2 = (S0 + maj) >>> 0;
|
|
67
|
+
hh = g;
|
|
68
|
+
g = f;
|
|
69
|
+
f = e;
|
|
70
|
+
e = (d + t1) >>> 0;
|
|
71
|
+
d = c;
|
|
72
|
+
c = b;
|
|
73
|
+
b = a;
|
|
74
|
+
a = (t1 + t2) >>> 0;
|
|
75
|
+
}
|
|
76
|
+
h[0] = (h[0] + a) >>> 0;
|
|
77
|
+
h[1] = (h[1] + b) >>> 0;
|
|
78
|
+
h[2] = (h[2] + c) >>> 0;
|
|
79
|
+
h[3] = (h[3] + d) >>> 0;
|
|
80
|
+
h[4] = (h[4] + e) >>> 0;
|
|
81
|
+
h[5] = (h[5] + f) >>> 0;
|
|
82
|
+
h[6] = (h[6] + g) >>> 0;
|
|
83
|
+
h[7] = (h[7] + hh) >>> 0;
|
|
84
|
+
}
|
|
85
|
+
const out = new Uint8Array(32);
|
|
86
|
+
const ov = new DataView(out.buffer);
|
|
87
|
+
for (let i = 0; i < 8; i++)
|
|
88
|
+
ov.setUint32(i * 4, h[i]);
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
/** SHA-256 of a UTF-8 string, as lowercase hex. */
|
|
92
|
+
export async function sha256Hex(text) {
|
|
93
|
+
const bytes = utf8(text);
|
|
94
|
+
const subtle = globalThis.crypto?.subtle;
|
|
95
|
+
if (subtle)
|
|
96
|
+
return hex(new Uint8Array(await subtle.digest('SHA-256', bytes)));
|
|
97
|
+
return hex(sha256Fallback(bytes));
|
|
98
|
+
}
|
|
99
|
+
/** Random UUID v4. */
|
|
100
|
+
export function uuid() {
|
|
101
|
+
const c = globalThis.crypto;
|
|
102
|
+
if (c?.randomUUID)
|
|
103
|
+
return c.randomUUID();
|
|
104
|
+
const b = new Uint8Array(16);
|
|
105
|
+
if (c?.getRandomValues)
|
|
106
|
+
c.getRandomValues(b);
|
|
107
|
+
else
|
|
108
|
+
for (let i = 0; i < 16; i++)
|
|
109
|
+
b[i] = Math.floor(Math.random() * 256);
|
|
110
|
+
b[6] = (b[6] & 0x0f) | 0x40;
|
|
111
|
+
b[8] = (b[8] & 0x3f) | 0x80;
|
|
112
|
+
const h = hex(b);
|
|
113
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
114
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare class WedeError extends Error {
|
|
2
|
+
readonly code: string;
|
|
3
|
+
readonly status?: number | undefined;
|
|
4
|
+
readonly details?: unknown | undefined;
|
|
5
|
+
constructor(message: string, code: string, status?: number | undefined, details?: unknown | undefined);
|
|
6
|
+
}
|
|
7
|
+
export declare class WedeAuthError extends WedeError {
|
|
8
|
+
constructor(message?: string);
|
|
9
|
+
}
|
|
10
|
+
export declare class WedeNetworkError extends WedeError {
|
|
11
|
+
constructor(message?: string);
|
|
12
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export class WedeError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
status;
|
|
4
|
+
details;
|
|
5
|
+
constructor(message, code, status, details) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.details = details;
|
|
10
|
+
this.name = 'WedeError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class WedeAuthError extends WedeError {
|
|
14
|
+
constructor(message = 'Invalid or missing credentials') {
|
|
15
|
+
super(message, 'unauthorized', 401);
|
|
16
|
+
this.name = 'WedeAuthError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export class WedeNetworkError extends WedeError {
|
|
20
|
+
constructor(message = 'Network request failed') {
|
|
21
|
+
super(message, 'network_error');
|
|
22
|
+
this.name = 'WedeNetworkError';
|
|
23
|
+
}
|
|
24
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event type. Either the code of an action in your catalog for the event's
|
|
3
|
+
* vertical (for example "cardiac_arrest"), or the code of an active event
|
|
4
|
+
* category used directly (for example "STATUS_UPDATE"). Letters, digits and _.
|
|
5
|
+
*/
|
|
6
|
+
export type EventType = string;
|
|
7
|
+
/** An event category. wede keeps this list open; categories are never required. */
|
|
8
|
+
export interface EventCategory {
|
|
9
|
+
code: string;
|
|
10
|
+
label: string;
|
|
11
|
+
description: string | null;
|
|
12
|
+
is_active: boolean;
|
|
13
|
+
}
|
|
14
|
+
export type Priority = 'low' | 'normal' | 'high' | 'critical';
|
|
15
|
+
export type ConnectivityState = 'online' | 'degraded' | 'sms_only' | 'voice_only' | 'offline';
|
|
16
|
+
export type MissionStatus = 'CREATED' | 'SENT' | 'ACK' | 'ON_ROUTE' | 'ON_SITE' | 'COMPLETED' | 'FAILED';
|
|
17
|
+
export type WebhookEventName = 'event.created' | 'event.status_changed' | 'event.delivered' | 'event.failed' | 'sync.completed' | 'connectivity.degraded' | 'connectivity.restored' | 'assignment.created' | 'team.created' | 'team.updated' | 'team.deleted' | 'team.dispatched' | 'team.equipment.added' | 'team.equipment.updated' | 'team.equipment.removed' | 'mission.created' | 'mission.status_updated';
|
|
18
|
+
export interface WedeClientOptions {
|
|
19
|
+
/** Tenant API key (wede_live_... or wede_test_...). Server side only. */
|
|
20
|
+
apiKey?: string;
|
|
21
|
+
/** Session token of a user (see WedeClient.login). Use this on devices. */
|
|
22
|
+
accessToken?: string;
|
|
23
|
+
/** Defaults to https://api.wede.pt */
|
|
24
|
+
baseUrl?: string;
|
|
25
|
+
/** Request timeout in milliseconds. Defaults to 10000. */
|
|
26
|
+
timeout?: number;
|
|
27
|
+
/** Attempts on network failure. Defaults to 3. */
|
|
28
|
+
retries?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface EventMetadata {
|
|
31
|
+
zone_id?: string;
|
|
32
|
+
operator_id?: string;
|
|
33
|
+
sdk_version?: string;
|
|
34
|
+
offline_generated_at?: string;
|
|
35
|
+
connectivity_state_at_generation?: ConnectivityState;
|
|
36
|
+
}
|
|
37
|
+
/** One-off recipient for this event only. Never stored by wede. */
|
|
38
|
+
export interface EventDestination {
|
|
39
|
+
phone?: string;
|
|
40
|
+
email?: string;
|
|
41
|
+
device_id?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface EventInput {
|
|
44
|
+
type: EventType;
|
|
45
|
+
/** A vertical registered for your tenant, for example "healthcare". */
|
|
46
|
+
vertical: string;
|
|
47
|
+
priority: Priority;
|
|
48
|
+
/** Unique per operation. Generated by the SDK when omitted. */
|
|
49
|
+
idempotency_key?: string;
|
|
50
|
+
payload: Record<string, unknown>;
|
|
51
|
+
metadata?: EventMetadata;
|
|
52
|
+
destination?: EventDestination;
|
|
53
|
+
}
|
|
54
|
+
export interface SendEventResult {
|
|
55
|
+
event_id: string;
|
|
56
|
+
idempotency_key: string;
|
|
57
|
+
status: string;
|
|
58
|
+
channel_selected: string;
|
|
59
|
+
estimated_delivery_ms: number;
|
|
60
|
+
request_id: string;
|
|
61
|
+
}
|
|
62
|
+
export interface EventSummary {
|
|
63
|
+
event_id: string;
|
|
64
|
+
/** Your event type. Absent for wede platform roles, which only see the category. */
|
|
65
|
+
type?: EventType;
|
|
66
|
+
category: string;
|
|
67
|
+
vertical: string;
|
|
68
|
+
priority: Priority;
|
|
69
|
+
status: string;
|
|
70
|
+
channel_used: string | null;
|
|
71
|
+
created_at: string;
|
|
72
|
+
updated_at: string;
|
|
73
|
+
zone_id: string | null;
|
|
74
|
+
}
|
|
75
|
+
export interface EventList {
|
|
76
|
+
data: EventSummary[];
|
|
77
|
+
next_cursor: string | null;
|
|
78
|
+
total_count: number;
|
|
79
|
+
}
|
|
80
|
+
export interface EventLifecycleEntry {
|
|
81
|
+
status: string;
|
|
82
|
+
channel: string | null;
|
|
83
|
+
note: string | null;
|
|
84
|
+
timestamp: string;
|
|
85
|
+
}
|
|
86
|
+
export interface EventDetail {
|
|
87
|
+
id: string;
|
|
88
|
+
/** Your event type. Absent for wede platform roles, which only see the category. */
|
|
89
|
+
type?: EventType;
|
|
90
|
+
category: string;
|
|
91
|
+
vertical: string;
|
|
92
|
+
priority: Priority;
|
|
93
|
+
status: string;
|
|
94
|
+
lifecycle: EventLifecycleEntry[];
|
|
95
|
+
[key: string]: unknown;
|
|
96
|
+
}
|
|
97
|
+
export interface ListEventsParams {
|
|
98
|
+
status?: string;
|
|
99
|
+
type?: EventType;
|
|
100
|
+
category?: string;
|
|
101
|
+
vertical?: string;
|
|
102
|
+
zone_id?: string;
|
|
103
|
+
/** Up to 200. Defaults to 50. */
|
|
104
|
+
limit?: number;
|
|
105
|
+
}
|
|
106
|
+
/** An event captured without connectivity, ready for syncBatch(). */
|
|
107
|
+
export interface CapturedEvent {
|
|
108
|
+
type: EventType;
|
|
109
|
+
vertical: string;
|
|
110
|
+
priority: Priority;
|
|
111
|
+
idempotency_key: string;
|
|
112
|
+
payload: Record<string, unknown>;
|
|
113
|
+
/** ISO 8601 moment of capture. */
|
|
114
|
+
offline_generated_at: string;
|
|
115
|
+
connectivity_state_at_generation?: ConnectivityState;
|
|
116
|
+
/** SHA-256 hex, computed by captureEvent(). */
|
|
117
|
+
integrity_hash: string;
|
|
118
|
+
}
|
|
119
|
+
export type SyncItemResult = 'accepted' | 'duplicate' | 'rejected';
|
|
120
|
+
export interface SyncBatchResult {
|
|
121
|
+
summary: {
|
|
122
|
+
total: number;
|
|
123
|
+
accepted: number;
|
|
124
|
+
duplicates: number;
|
|
125
|
+
rejected: number;
|
|
126
|
+
};
|
|
127
|
+
results: Array<{
|
|
128
|
+
idempotency_key: string;
|
|
129
|
+
event_id: string | null;
|
|
130
|
+
result: SyncItemResult;
|
|
131
|
+
error_code: string | null;
|
|
132
|
+
error_message: string | null;
|
|
133
|
+
}>;
|
|
134
|
+
}
|
|
135
|
+
export interface SyncStatus {
|
|
136
|
+
tenant_id: string;
|
|
137
|
+
synced_total: number;
|
|
138
|
+
pending: number;
|
|
139
|
+
completed: number;
|
|
140
|
+
failed: number;
|
|
141
|
+
last_sync_at: string | null;
|
|
142
|
+
}
|
|
143
|
+
export interface RegisterDeviceParams {
|
|
144
|
+
device_id: string;
|
|
145
|
+
platform: 'ios' | 'android' | 'web' | 'other';
|
|
146
|
+
app_version?: string;
|
|
147
|
+
}
|
|
148
|
+
export interface DeviceQueueItem {
|
|
149
|
+
/** Starts at 1, increases by one per operation, never reused or skipped. */
|
|
150
|
+
sequence_number: number;
|
|
151
|
+
action_id?: string;
|
|
152
|
+
event_lat?: number;
|
|
153
|
+
event_lng?: number;
|
|
154
|
+
vertical?: string;
|
|
155
|
+
priority?: string;
|
|
156
|
+
payload?: Record<string, unknown>;
|
|
157
|
+
created_offline_at: string;
|
|
158
|
+
}
|
|
159
|
+
export interface SyncDeviceQueueParams {
|
|
160
|
+
device_id: string;
|
|
161
|
+
last_received_seq: number;
|
|
162
|
+
/** Up to 500 per call. */
|
|
163
|
+
dispatches: DeviceQueueItem[];
|
|
164
|
+
}
|
|
165
|
+
export interface SyncDeviceQueueResult {
|
|
166
|
+
accepted: number[];
|
|
167
|
+
duplicates: number[];
|
|
168
|
+
failed: number[];
|
|
169
|
+
server_seq: number;
|
|
170
|
+
device_last_received_seq: number;
|
|
171
|
+
synced_at: string;
|
|
172
|
+
integrity_gap_detected: boolean;
|
|
173
|
+
integrity_incident_id: string | null;
|
|
174
|
+
}
|
|
175
|
+
export interface ZoneConnectivity {
|
|
176
|
+
zone_id: string;
|
|
177
|
+
zone_name: string;
|
|
178
|
+
country: string;
|
|
179
|
+
state: ConnectivityState;
|
|
180
|
+
incident_active: boolean;
|
|
181
|
+
last_updated: string;
|
|
182
|
+
fallback_channel: string;
|
|
183
|
+
}
|
|
184
|
+
export interface ConnectivityStatus {
|
|
185
|
+
zones: ZoneConnectivity[];
|
|
186
|
+
as_of: string;
|
|
187
|
+
}
|
|
188
|
+
export interface ConnectivityReport {
|
|
189
|
+
zone_id: string;
|
|
190
|
+
state: ConnectivityState;
|
|
191
|
+
/** ISO 8601. Defaults to now. */
|
|
192
|
+
detected_at?: string;
|
|
193
|
+
signal_strength_dbm?: number | null;
|
|
194
|
+
network_type?: 'wifi' | 'lte' | 'umts' | 'edge' | 'gprs' | 'gsm' | 'none' | null;
|
|
195
|
+
device_id?: string;
|
|
196
|
+
}
|
|
197
|
+
export interface Zone {
|
|
198
|
+
zone_code: string;
|
|
199
|
+
name: string;
|
|
200
|
+
[key: string]: unknown;
|
|
201
|
+
}
|
|
202
|
+
export interface ZoneList {
|
|
203
|
+
zones: Zone[];
|
|
204
|
+
total: number;
|
|
205
|
+
}
|
|
206
|
+
export interface TenantInfo {
|
|
207
|
+
id: string;
|
|
208
|
+
name: string;
|
|
209
|
+
zones: unknown[];
|
|
210
|
+
[key: string]: unknown;
|
|
211
|
+
}
|
|
212
|
+
export interface Usage {
|
|
213
|
+
tenant_id: string;
|
|
214
|
+
period: {
|
|
215
|
+
from: string;
|
|
216
|
+
to: string;
|
|
217
|
+
};
|
|
218
|
+
totals: {
|
|
219
|
+
events: number;
|
|
220
|
+
offline_events: number;
|
|
221
|
+
sms_messages: number;
|
|
222
|
+
voice_calls: number;
|
|
223
|
+
};
|
|
224
|
+
limits: Record<string, number>;
|
|
225
|
+
by_vertical: Record<string, number>;
|
|
226
|
+
by_channel: Record<string, number>;
|
|
227
|
+
}
|
|
228
|
+
export interface Billing {
|
|
229
|
+
tenant_id: string;
|
|
230
|
+
country: string;
|
|
231
|
+
current_plan: Record<string, unknown> | null;
|
|
232
|
+
available_plans: Record<string, unknown>[];
|
|
233
|
+
channel_costs: Record<string, unknown>[];
|
|
234
|
+
usage: Record<string, number>;
|
|
235
|
+
}
|
|
236
|
+
export interface DispatchSettings {
|
|
237
|
+
dispatch_mode: boolean;
|
|
238
|
+
dispatch_threshold: number;
|
|
239
|
+
reinforcement_timeout_min: number;
|
|
240
|
+
}
|
|
241
|
+
export interface Team {
|
|
242
|
+
id: string;
|
|
243
|
+
name: string;
|
|
244
|
+
vertical: string;
|
|
245
|
+
status: string;
|
|
246
|
+
members: Record<string, unknown>[];
|
|
247
|
+
[key: string]: unknown;
|
|
248
|
+
}
|
|
249
|
+
export interface TeamList {
|
|
250
|
+
data: Team[];
|
|
251
|
+
count: number;
|
|
252
|
+
}
|
|
253
|
+
export interface ScoreTeamsParams {
|
|
254
|
+
lat: number;
|
|
255
|
+
lng: number;
|
|
256
|
+
vertical?: string;
|
|
257
|
+
priority?: string;
|
|
258
|
+
required_equipment?: string[];
|
|
259
|
+
}
|
|
260
|
+
export interface ScoredTeam {
|
|
261
|
+
team_id: string;
|
|
262
|
+
team_name: string;
|
|
263
|
+
status: string;
|
|
264
|
+
distance_km: number;
|
|
265
|
+
eta_min: number;
|
|
266
|
+
/** 0 to 1, higher means better conditions. */
|
|
267
|
+
score: number;
|
|
268
|
+
recommended: boolean;
|
|
269
|
+
channel: string;
|
|
270
|
+
[key: string]: unknown;
|
|
271
|
+
}
|
|
272
|
+
export interface ScoreTeamsResult {
|
|
273
|
+
scored: ScoredTeam[];
|
|
274
|
+
count: number;
|
|
275
|
+
}
|
|
276
|
+
export interface DispatchParams {
|
|
277
|
+
event_id: string;
|
|
278
|
+
team_id: string;
|
|
279
|
+
notes?: string;
|
|
280
|
+
event_lat?: number;
|
|
281
|
+
event_lng?: number;
|
|
282
|
+
}
|
|
283
|
+
export interface DispatchActionParams {
|
|
284
|
+
/** Id of an action in your catalog. */
|
|
285
|
+
action_id: string;
|
|
286
|
+
lat: number;
|
|
287
|
+
lng: number;
|
|
288
|
+
priority?: string;
|
|
289
|
+
/** Your event id. Generated by wede when omitted. */
|
|
290
|
+
event_id?: string;
|
|
291
|
+
}
|
|
292
|
+
export interface Mission {
|
|
293
|
+
id: string;
|
|
294
|
+
event_id: string;
|
|
295
|
+
team_id: string;
|
|
296
|
+
status: MissionStatus;
|
|
297
|
+
[key: string]: unknown;
|
|
298
|
+
}
|
|
299
|
+
export interface MissionList {
|
|
300
|
+
data: Mission[];
|
|
301
|
+
count: number;
|
|
302
|
+
}
|
|
303
|
+
export interface ListMissionsParams {
|
|
304
|
+
team_id?: string;
|
|
305
|
+
status?: MissionStatus;
|
|
306
|
+
limit?: number;
|
|
307
|
+
}
|
|
308
|
+
export interface CatalogAction {
|
|
309
|
+
id: string;
|
|
310
|
+
vertical: string;
|
|
311
|
+
code: string;
|
|
312
|
+
name: string;
|
|
313
|
+
description?: string | null;
|
|
314
|
+
/** Event category of this type. Defaults to OTHER. */
|
|
315
|
+
category: string;
|
|
316
|
+
is_active: boolean;
|
|
317
|
+
[key: string]: unknown;
|
|
318
|
+
}
|
|
319
|
+
export interface CreateCatalogAction {
|
|
320
|
+
vertical: string;
|
|
321
|
+
/** Lowercase letters, digits and underscores. Cannot be the code of a category. */
|
|
322
|
+
code: string;
|
|
323
|
+
name: string;
|
|
324
|
+
description?: string;
|
|
325
|
+
/** Code of an active event category. Defaults to OTHER. */
|
|
326
|
+
category?: string;
|
|
327
|
+
}
|
|
328
|
+
export interface UpdateCatalogAction {
|
|
329
|
+
name?: string;
|
|
330
|
+
description?: string;
|
|
331
|
+
is_active?: boolean;
|
|
332
|
+
/** Applies to new events only; events already stored keep their category. */
|
|
333
|
+
category?: string;
|
|
334
|
+
}
|
|
335
|
+
export interface LoginResult {
|
|
336
|
+
token: string;
|
|
337
|
+
/** Seconds. */
|
|
338
|
+
expires_in: number;
|
|
339
|
+
user: {
|
|
340
|
+
id: string;
|
|
341
|
+
email: string;
|
|
342
|
+
name: string | null;
|
|
343
|
+
rbac_level: string;
|
|
344
|
+
[key: string]: unknown;
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
export interface Parser {
|
|
348
|
+
id: string;
|
|
349
|
+
vertical: string;
|
|
350
|
+
name: string;
|
|
351
|
+
[key: string]: unknown;
|
|
352
|
+
}
|
|
353
|
+
export interface ParserList {
|
|
354
|
+
data: Parser[];
|
|
355
|
+
count: number;
|
|
356
|
+
}
|
|
357
|
+
export interface Webhook {
|
|
358
|
+
webhook_id: string;
|
|
359
|
+
url: string;
|
|
360
|
+
events: WebhookEventName[];
|
|
361
|
+
active: boolean;
|
|
362
|
+
description: string | null;
|
|
363
|
+
created_at: string;
|
|
364
|
+
failure_count: number;
|
|
365
|
+
[key: string]: unknown;
|
|
366
|
+
}
|
|
367
|
+
export interface CreateWebhook {
|
|
368
|
+
url: string;
|
|
369
|
+
events: WebhookEventName[];
|
|
370
|
+
/** At least 16 characters. Used to sign deliveries. */
|
|
371
|
+
secret: string;
|
|
372
|
+
active?: boolean;
|
|
373
|
+
description?: string;
|
|
374
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wedetech/sdk",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Official JavaScript and TypeScript client for the wede API (Node.js, browsers, edge runtimes and React Native)",
|
|
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
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc",
|
|
23
|
+
"test": "tsc && node --test test/unit.test.mjs",
|
|
24
|
+
"test:integration": "tsc && node --test test/integration.test.mjs",
|
|
25
|
+
"prepublishOnly": "npm test"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"wede",
|
|
29
|
+
"operational-continuity",
|
|
30
|
+
"offline-first",
|
|
31
|
+
"sdk",
|
|
32
|
+
"react-native"
|
|
33
|
+
],
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"typescript": "^5.6.0"
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/Wedeadmin/wedetech-sdk-js.git"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://docs.wede.pt",
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
}
|
|
46
|
+
}
|