@sharkpush/ts 0.1.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.
Files changed (3) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +350 -0
  3. package/package.json +90 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 SharkPush
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,350 @@
1
+ # @sharkpush/sdk
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@sharkpush/sdk.svg)](https://www.npmjs.com/package/@sharkpush/sdk)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
5
+
6
+ Pure TypeScript SDK for the [SharkPush](https://sharkpush.dev) REST API —
7
+ identities, devices, events, the objective-based `communicate()` API, and
8
+ real-time SSE/WebSocket subscriptions. Works in **Node.js 18+** and modern
9
+ browsers.
10
+
11
+ This SDK mirrors the iOS / Flutter / React Native / Android SDKs' API
12
+ surface in idiomatic, strict TypeScript (ESM):
13
+
14
+ | Other SDK | TypeScript SDK |
15
+ | ---------------------------------- | ----------------------------------------------------------- |
16
+ | `SharkpushClient` | `SharkpushClient` |
17
+ | `Identity`, `Device`, `DevicePlatform` | `Identity`, `Device`, `DevicePlatform` (interfaces) |
18
+ | `CommunicationIntent`, `CommunicationRequest` | `CommunicationIntent`, `CommunicationRequest` |
19
+ | `Event`, `Message`, `MessageAttempt` | `Event`, `Message`, `MessageAttempt` (interfaces) |
20
+ | `SharkpushError` | `SharkpushError` (abstract) + subclasses |
21
+ | `RetryConfig` | `RetryConfig` |
22
+ | (new) | `subscribeLiveUpdates()` / `subscribeEvents()` (SSE / WS) |
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ npm install @sharkpush/sdk
28
+ # or:
29
+ pnpm add @sharkpush/sdk
30
+ # or:
31
+ yarn add @sharkpush/sdk
32
+ ```
33
+
34
+ > The package ships as ESM (`"type": "module"`). Node 18+ and modern
35
+ > bundlers (webpack 5+, Vite, esbuild, Rollup) support ESM natively. For
36
+ > older consumers, transpile through your bundler.
37
+
38
+ ## Quickstart
39
+
40
+ ```ts
41
+ import { SharkpushClient } from '@sharkpush/sdk';
42
+
43
+ const client = new SharkpushClient({ apiKey: 'sk_REPLACE_ME' });
44
+
45
+ // 1. Create an identity.
46
+ const identity = await client.createIdentity({
47
+ externalUserId: 'u1',
48
+ email: 'u1@example.com',
49
+ });
50
+
51
+ // 2. Register a web push token (or 'ios' / 'android' / 'huawei').
52
+ await client.registerDevice({
53
+ identityId: identity.id,
54
+ platform: 'web',
55
+ token: 'REPLACE_ME',
56
+ });
57
+
58
+ // 3. Publish an event.
59
+ await client.sendEvent({
60
+ name: 'user.signup',
61
+ payload: { plan: 'pro', identity_id: identity.id },
62
+ });
63
+
64
+ // 4. Trigger a communication.
65
+ const req = await client.communicate({
66
+ userId: identity.id,
67
+ objective: 'otp',
68
+ priority: 'normal',
69
+ data: { code: '4242' },
70
+ });
71
+ console.log('req', req.id, req.status);
72
+
73
+ // 5. Subscribe to live updates (SSE by default).
74
+ const unsubscribe = client.subscribeLiveUpdates((snapshot) => {
75
+ console.log('snapshot', snapshot.kind, snapshot.timestamp);
76
+ });
77
+ // … later:
78
+ unsubscribe();
79
+
80
+ // 6. Close the client when done.
81
+ await client.close();
82
+ ```
83
+
84
+ ## API reference
85
+
86
+ ### `SharkpushClient`
87
+
88
+ ```ts
89
+ const client = new SharkpushClient({
90
+ apiKey: 'sk_REPLACE_ME',
91
+ endpoint: 'https://api.sharkpush.dev', // optional
92
+ apiPrefix: '/v1', // optional
93
+ tenantId: 'tenant_abc', // optional
94
+ retryConfig: DEFAULT_RETRY_CONFIG, // optional
95
+ fetchImpl: fetch, // optional (testing)
96
+ });
97
+ ```
98
+
99
+ The API key **must** start with `sk_`. It is never logged in plaintext;
100
+ `client.toString()` returns a redacted form like
101
+ `SharkpushClient(endpoint: …, apiKey: sk_••••…1234, tenantId: …)`.
102
+
103
+ #### `createIdentity`
104
+
105
+ ```ts
106
+ await client.createIdentity({
107
+ externalUserId?: 'u1',
108
+ anonymous?: false,
109
+ email?: 't@e.com',
110
+ phone?: '+2547…',
111
+ locale?: 'en_KE',
112
+ timezone?: 'Africa/Nairobi',
113
+ idempotencyKey?: 'my-key',
114
+ });
115
+ ```
116
+
117
+ Creates a new `Identity`. Pass `anonymous: true` for guest identities.
118
+ For non-anonymous identities, `externalUserId` is required.
119
+
120
+ #### `getIdentity`
121
+
122
+ ```ts
123
+ await client.getIdentity('id_42');
124
+ ```
125
+
126
+ Retrieves an `Identity` by ID.
127
+
128
+ #### `registerDevice`
129
+
130
+ ```ts
131
+ await client.registerDevice({
132
+ identityId: 'id_1',
133
+ platform: 'web', // 'ios' | 'android' | 'web' | 'huawei'
134
+ token: 'REPLACE_ME',
135
+ locale?: 'en_KE',
136
+ timezone?: 'Africa/Nairobi',
137
+ idempotencyKey?: 'my-key',
138
+ });
139
+ ```
140
+
141
+ Registers / refreshes a push token for an `Identity`.
142
+
143
+ #### `sendEvent`
144
+
145
+ ```ts
146
+ await client.sendEvent({
147
+ name: 'user.signup',
148
+ payload: { plan: 'pro' },
149
+ correlationId?: 'corr-1',
150
+ aggregateId?: 'agg-1',
151
+ idempotencyKey?: 'my-key',
152
+ });
153
+ ```
154
+
155
+ Publishes an event to the SharkPush event bus.
156
+
157
+ #### `communicate`
158
+
159
+ ```ts
160
+ const req = await client.communicate({
161
+ userId: 'u1',
162
+ objective: 'otp',
163
+ priority: 'normal', // 'low' | 'normal' | 'high' | 'critical'
164
+ constraints?: { … },
165
+ data?: { code: '4242' },
166
+ metadata?: { … },
167
+ channel?: 'push',
168
+ idempotencyKey?: 'my-key',
169
+ });
170
+ ```
171
+
172
+ Triggers a communication with the given objective. Returns the 202
173
+ Accepted response (`CommunicationRequest`).
174
+
175
+ #### `getMessage` / `listMessages`
176
+
177
+ ```ts
178
+ await client.getMessage('m_42');
179
+
180
+ await client.listMessages({
181
+ limit: 20,
182
+ offset: 0,
183
+ status?: 'DELIVERED',
184
+ channel?: 'push',
185
+ });
186
+ ```
187
+
188
+ #### Real-time — `subscribeLiveUpdates`
189
+
190
+ ```ts
191
+ const unsubscribe = client.subscribeLiveUpdates(
192
+ (snapshot) => {
193
+ console.log(snapshot.kind, snapshot.timestamp);
194
+ // snapshot.kind ∈ 'message' | 'event' | 'status' | 'heartbeat'
195
+ // snapshot.message? — Message
196
+ // snapshot.event? — Event
197
+ // snapshot.status? — string
198
+ // snapshot.source — 'sse' | 'websocket'
199
+ },
200
+ {
201
+ streamUrl?: 'https://api.sharkpush.dev/v1/stream', // optional
202
+ transport?: 'sse', // or 'websocket'
203
+ },
204
+ );
205
+
206
+ // … later:
207
+ unsubscribe(); // closes the underlying connection when last handler leaves
208
+ ```
209
+
210
+ #### Real-time — `subscribeEvents`
211
+
212
+ ```ts
213
+ const unsubscribe = client.subscribeEvents(
214
+ { name: 'user.signup' }, // filter (omit `name` for all events)
215
+ (event) => console.log(event.name, event.payload),
216
+ { transport?: 'sse' },
217
+ );
218
+
219
+ // … later:
220
+ unsubscribe();
221
+ ```
222
+
223
+ #### `close`
224
+
225
+ ```ts
226
+ await client.close();
227
+ ```
228
+
229
+ Closes the HTTP layer and any active stream subscriptions. After
230
+ `close()` is called, the client must not be reused.
231
+
232
+ ## Idempotency
233
+
234
+ Every mutating method accepts an optional `idempotencyKey`. If omitted,
235
+ a random UUID v4 is generated. The same key can be safely retried — the
236
+ server deduplicates based on `X-Idempotency-Key`.
237
+
238
+ ## Retries
239
+
240
+ HTTP requests retry with exponential backoff (base **500 ms**, max **30 s**,
241
+ max **3 attempts**) on HTTP `5xx` and network errors. `4xx` errors (except
242
+ `429`) are not retried.
243
+
244
+ Customise via `retryConfig`:
245
+
246
+ ```ts
247
+ import { DEFAULT_RETRY_CONFIG, SharkpushClient } from '@sharkpush/sdk';
248
+
249
+ const client = new SharkpushClient({
250
+ apiKey: 'sk_REPLACE_ME',
251
+ retryConfig: {
252
+ baseDelayMs: 1000,
253
+ maxDelayMs: 60_000,
254
+ maxAttempts: 5,
255
+ },
256
+ });
257
+ ```
258
+
259
+ ## Error handling
260
+
261
+ All errors are typed subclasses of `SharkpushError`:
262
+
263
+ | Class | HTTP status |
264
+ | ---------------------- | ----------------- |
265
+ | `AuthenticationError` | 401, 403 |
266
+ | `ValidationError` | 400, 422 |
267
+ | `NotFoundError` | 404 |
268
+ | `RateLimitError` | 429 |
269
+ | `ServerError` | 5xx |
270
+ | `NetworkError` | (no HTTP) |
271
+ | `DecodeError` | (no HTTP) |
272
+ | `UnknownError` | anything else |
273
+
274
+ ```ts
275
+ import {
276
+ RateLimitError,
277
+ AuthenticationError,
278
+ SharkpushError,
279
+ } from '@sharkpush/sdk';
280
+
281
+ try {
282
+ await client.communicate({ userId: 'u1', objective: 'otp' });
283
+ } catch (e) {
284
+ if (e instanceof RateLimitError) {
285
+ console.warn(`rate limited, retry after ${e.retryAfterSeconds}s`);
286
+ } else if (e instanceof AuthenticationError) {
287
+ console.warn(`auth failed: ${e.message}`);
288
+ } else if (e instanceof SharkpushError) {
289
+ console.warn(`sharkpush error: ${e.code} ${e.message}`);
290
+ }
291
+ }
292
+ ```
293
+
294
+ ## Real-time transports
295
+
296
+ The SDK supports two real-time transports:
297
+
298
+ 1. **SSE** (`EventSource`) — preferred. Browser-native; Node 18+ ships
299
+ `EventSource` globally. Auto-reconnects with exponential backoff
300
+ (base 1 s, max 30 s) when the server closes the connection.
301
+ 2. **WebSocket** — fallback for environments where SSE is buffered
302
+ (some proxies) or when bidirectional messaging is required. Sends
303
+ the API key as `?api_key=…` and the tenant ID as `?tenant_id=…`
304
+ query parameters.
305
+
306
+ The transport is selected via the `transport` option on
307
+ `subscribeLiveUpdates` / `subscribeEvents`. The default is `sse`.
308
+
309
+ ## Browser usage
310
+
311
+ The SDK uses the global `fetch`, `EventSource`, `WebSocket`,
312
+ `AbortController`, `TextDecoder`, `crypto.randomUUID`, and `URL`. All
313
+ modern browsers (Chrome 64+, Firefox 62+, Safari 12.1+, Edge 79+)
314
+ support these natively.
315
+
316
+ ```html
317
+ <script type="module">
318
+ import { SharkpushClient } from 'https://esm.sh/@sharkpush/sdk@0.1.0';
319
+
320
+ const client = new SharkpushClient({ apiKey: 'sk_REPLACE_ME' });
321
+ const id = await client.createIdentity({ anonymous: true });
322
+ console.log('identity', id.id);
323
+ </script>
324
+ ```
325
+
326
+ See [`examples/browser/`](./examples/browser) for a complete example.
327
+
328
+ ## Node.js usage
329
+
330
+ ```ts
331
+ import { SharkpushClient } from '@sharkpush/sdk';
332
+
333
+ const client = new SharkpushClient({ apiKey: process.env.SHARKPUSH_API_KEY! });
334
+ // …
335
+ await client.close();
336
+ ```
337
+
338
+ See [`examples/node-app/`](./examples/node-app) for a complete example.
339
+
340
+ ## Security
341
+
342
+ - The API key is treated as a secret. It is **never** logged in
343
+ plaintext and is redacted in `toString()` output.
344
+ - Use `REPLACE_ME` placeholders in examples and never commit real keys.
345
+ - WebSocket subscriptions send the API key as a query parameter — only
346
+ use `wss://` (TLS) endpoints.
347
+
348
+ ## License
349
+
350
+ MIT — see [LICENSE](./LICENSE).
package/package.json ADDED
@@ -0,0 +1,90 @@
1
+ {
2
+ "name": "@sharkpush/ts",
3
+ "version": "0.1.0",
4
+ "description": "Official TypeScript SDK for the SharkPush REST API. Supports identities, devices, events, the objective-based communicate() API, and real-time SSE/WebSocket subscriptions in Node.js and modern browsers.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc",
23
+ "clean": "rimraf dist",
24
+ "dev": "tsc --watch",
25
+ "typecheck": "tsc --noEmit",
26
+ "test": "jest",
27
+ "test:coverage": "jest --coverage",
28
+ "lint": "eslint . --ext .ts"
29
+ },
30
+ "keywords": [
31
+ "sharkpush",
32
+ "sdk",
33
+ "typescript",
34
+ "javascript",
35
+ "push-notifications",
36
+ "notifications",
37
+ "messaging",
38
+ "realtime",
39
+ "sse",
40
+ "websocket",
41
+ "node",
42
+ "browser"
43
+ ],
44
+ "author": "SharkPush",
45
+ "license": "MIT",
46
+ "homepage": "https://docs.sharkpush.dev",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "https://github.com/Roy-Wanyoike/sharkpush.git",
50
+ "directory": "sdks/ts"
51
+ },
52
+ "bugs": {
53
+ "url": "https://github.com/Roy-Wanyoike/sharkpush/issues"
54
+ },
55
+ "engines": {
56
+ "node": ">=18"
57
+ },
58
+ "publishConfig": {
59
+ "access": "public"
60
+ },
61
+ "devDependencies": {
62
+ "@babel/core": "^7.24.0",
63
+ "@babel/preset-env": "^7.24.0",
64
+ "@babel/preset-typescript": "^7.24.0",
65
+ "@types/jest": "^29.5.0",
66
+ "@types/node": "^20.0.0",
67
+ "@typescript-eslint/eslint-plugin": "^7.0.0",
68
+ "@typescript-eslint/parser": "^7.0.0",
69
+ "babel-jest": "^29.7.0",
70
+ "eslint": "^8.50.0",
71
+ "jest": "^29.7.0",
72
+ "rimraf": "^6.0.1",
73
+ "typescript": "^5.4.0"
74
+ },
75
+ "jest": {
76
+ "testEnvironment": "node",
77
+ "transform": {
78
+ "^.+\\.tsx?$": "babel-jest"
79
+ },
80
+ "testMatch": [
81
+ "**/__tests__/**/*.test.ts"
82
+ ],
83
+ "collectCoverageFrom": [
84
+ "src/**/*.ts",
85
+ "!src/types.ts",
86
+ "!src/index.ts"
87
+ ],
88
+ "forceExit": true
89
+ }
90
+ }