@emmanuel-nike/ark-notify-js 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,11 +18,18 @@ For React apps, `react` 18+ is a peer dependency.
18
18
  Wrap your app with the provider, connect a WebSocket client, and subscribe to a channel:
19
19
 
20
20
  ```tsx
21
- import { ArkNotifyProvider, useConnection, useChannel } from 'ark-notify-js/react'
21
+ import {
22
+ configureArkNotify,
23
+ ArkNotifyProvider,
24
+ useConnection,
25
+ useChannel,
26
+ } from 'ark-notify-js/react'
27
+
28
+ configureArkNotify({ baseUrl: 'https://my-instance.ark-notify.com' })
22
29
 
23
30
  function App() {
24
31
  return (
25
- <ArkNotifyProvider baseUrl="http://localhost:3000">
32
+ <ArkNotifyProvider>
26
33
  <Chat />
27
34
  </ArkNotifyProvider>
28
35
  )
@@ -43,9 +50,7 @@ function Chat() {
43
50
  return (
44
51
  <div>
45
52
  <p>Status: {state}</p>
46
- <button onClick={() => publish('message', { text: 'Hello!' })}>
47
- Send
48
- </button>
53
+ <button onClick={() => publish('message', { text: 'Hello!' })}>Send</button>
49
54
  </div>
50
55
  )
51
56
  }
@@ -55,26 +60,31 @@ function Chat() {
55
60
 
56
61
  Ark Notify has two API planes:
57
62
 
58
- | Plane | Purpose | Auth |
59
- |-------|---------|------|
60
- | **Control** | Login, manage applications | JWT (`Authorization: Bearer`) |
61
- | **Data** | WebSocket/SSE clients, server-side publish | `clientId` / connection token (clients) or app key + secret (servers) |
63
+
64
+ | Plane | Purpose | Auth |
65
+ | ----------- | ------------------------------------------ | --------------------------------------------------------------------- |
66
+ | **Control** | Login, manage applications | JWT (`Authorization: Bearer`) |
67
+ | **Data** | WebSocket/SSE clients, server-side publish | `clientId` / connection token (clients) or app key + secret (servers) |
68
+
62
69
 
63
70
  **Never expose your app `secret` in browser code.** Issue connection tokens and private-channel auth from your backend.
64
71
 
65
72
  ## Provider
66
73
 
74
+ `baseUrl` is optional. Omit it to use the built-in default, or set a custom default once with `configureArkNotify()`:
75
+
67
76
  ```tsx
68
- import { ArkNotifyProvider } from 'ark-notify-js/react'
77
+ import { configureArkNotify, ArkNotifyProvider } from 'ark-notify-js/react'
69
78
 
70
- <ArkNotifyProvider
71
- baseUrl="https://notify.example.com"
72
- token={platformJwt} // optional — for admin dashboards
73
- >
79
+ configureArkNotify({ baseUrl: 'https://my-instance.ark-notify.com' })
80
+
81
+ <ArkNotifyProvider token={platformJwt}>
74
82
  {children}
75
83
  </ArkNotifyProvider>
76
84
  ```
77
85
 
86
+ You can still pass `baseUrl` on the provider to override the default for that subtree.
87
+
78
88
  ## Platform auth (control plane)
79
89
 
80
90
  For admin dashboards that manage applications:
@@ -87,11 +97,7 @@ function Dashboard() {
87
97
  const { apps, create, remove } = useApplications()
88
98
 
89
99
  if (!isAuthenticated) {
90
- return (
91
- <button onClick={() => login({ email: '...', password: '...' })}>
92
- Log in
93
- </button>
94
- )
100
+ return <button onClick={() => login({ email: '...', password: '...' })}>Log in</button>
95
101
  }
96
102
 
97
103
  return (
@@ -99,7 +105,9 @@ function Dashboard() {
99
105
  <p>Hello, {user?.firstName}</p>
100
106
  <button onClick={() => create({ name: 'My App' })}>New app</button>
101
107
  {apps.map((app) => (
102
- <div key={app.id}>{app.name} — {app.appKey}</div>
108
+ <div key={app.id}>
109
+ {app.name} — {app.appKey}
110
+ </div>
103
111
  ))}
104
112
  </div>
105
113
  )
@@ -115,7 +123,7 @@ import { useConnection } from 'ark-notify-js/react'
115
123
 
116
124
  const {
117
125
  connection,
118
- state, // 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'failed'
126
+ state, // 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'failed'
119
127
  connectionId,
120
128
  clientId,
121
129
  authenticated,
@@ -123,8 +131,8 @@ const {
123
131
  disconnect,
124
132
  } = useConnection({
125
133
  appKey: 'app_abc',
126
- clientId: 'user-42', // when requireClientAuth is false
127
- token: 'app_abc.payload.sig', // recommended — from your backend
134
+ clientId: 'user-42', // when requireClientAuth is false
135
+ token: 'app_abc.payload.sig', // recommended — from your backend
128
136
  autoReconnect: true,
129
137
  onPrivateChannelAuth: async (channel, connectionId) => {
130
138
  const res = await fetch('/api/channel-auth', {
@@ -197,29 +205,38 @@ import {
197
205
  } from 'ark-notify-js'
198
206
 
199
207
  // REST client
200
- const client = new ArkNotifyClient({ baseUrl: 'http://localhost:3000' })
208
+ const client = new ArkNotifyClient()
201
209
  await client.login({ email, password })
202
210
  const { app } = await client.createApplication({ name: 'My App' })
203
211
 
204
212
  // Server-side publish (use app credentials — never in browser)
205
- await client.publishEvent(app.appKey, { appKey, secret }, {
206
- channel: 'room-1',
207
- event: 'order.created',
208
- data: { id: 123 },
209
- })
213
+ await client.publishEvent(
214
+ app.appKey,
215
+ { appKey, secret },
216
+ {
217
+ channel: 'room-1',
218
+ event: 'order.created',
219
+ data: { id: 123 },
220
+ }
221
+ )
210
222
 
211
- // Fetch a connection token (server-side)
223
+ // Fetch a connection token (backend — requires app secret when no serverAuthUrl)
212
224
  const { token } = await fetchConnectionToken({
213
- baseUrl: 'http://localhost:3000',
214
225
  appKey: app.appKey,
215
226
  credentials: { appKey: app.appKey, secret: app.secret! },
216
227
  client_id: 'user-42',
217
228
  user_data: { name: 'Alice' },
218
229
  })
219
230
 
231
+ // Frontend — when the application has a serverAuthUrl configured
232
+ const { token: frontendToken } = await fetchConnectionToken({
233
+ appKey: 'app_abc',
234
+ client_id: 'user-42',
235
+ user_data: { name: 'Alice' },
236
+ })
237
+
220
238
  // WebSocket — pass token directly
221
239
  const conn = new ArkNotifyConnection({
222
- baseUrl: 'http://localhost:3000',
223
240
  appKey: 'app_abc',
224
241
  token,
225
242
  })
@@ -227,7 +244,6 @@ await conn.connect()
227
244
 
228
245
  // WebSocket — auto-fetch token when credentials + clientId are provided (server-side)
229
246
  const autoConn = new ArkNotifyConnection({
230
- baseUrl: 'http://localhost:3000',
231
247
  appKey: app.appKey,
232
248
  clientId: 'user-42',
233
249
  credentials: { appKey: app.appKey, secret: app.secret! },
@@ -240,37 +256,79 @@ await autoConn.subscribe('private-room-1', { auth: 'app_abc:...' })
240
256
  autoConn.publish('room-1', 'message', { text: 'hi' })
241
257
  ```
242
258
 
243
- When `token` is omitted, `ArkNotifyConnection` automatically calls `POST /api/v1/apps/:appKey/connection-token` if both `clientId` and `credentials` are set. On reconnect, a fresh token is fetched.
259
+ When `token` is omitted, `ArkNotifyConnection` automatically calls `POST /api/v1/apps/:appKey/connection-token` when `clientId` is set. Pass `credentials` for backend-only apps, or omit them when the application has a `serverAuthUrl` (frontend-safe). On reconnect, a fresh token is fetched.
244
260
 
245
- ## System admin
261
+ ## Server auth URL webhook
246
262
 
247
- ```tsx
248
- import { useAdminChannels } from 'ark-notify-js/react'
263
+ When your application has a `serverAuthUrl`, Ark Notify POSTs to it before issuing a connection token. Use `@emmanuel-nike/ark-notify-js/server` in your backend handler:
264
+
265
+ ```ts
266
+ import {
267
+ handleServerAuth,
268
+ parseServerAuthRequest,
269
+ } from '@emmanuel-nike/ark-notify-js/server'
270
+
271
+ app.post('/api/ark/connection-auth', async (req, res) => {
272
+ const request = parseServerAuthRequest(req.body)
273
+ if (!request) {
274
+ return res.status(400).json({ allowed: false })
275
+ }
249
276
 
250
- const { data, loading, refresh } = useAdminChannels()
251
- // Requires SYSTEM_ADMIN JWT via ArkNotifyProvider token
277
+ const user = await getUserFromSession(req)
278
+ if (!user) {
279
+ return res.status(401).json({ allowed: false })
280
+ }
281
+
282
+ const response = await handleServerAuth({
283
+ request,
284
+ isAuthorized: () => ({
285
+ clientId: user.id,
286
+ capabilities: { publish: false },
287
+ }),
288
+ })
289
+
290
+ if ('token' in response || response.allowed === true) {
291
+ return res.json(response)
292
+ }
293
+
294
+ return res.status(403).json(response)
295
+ })
252
296
  ```
253
297
 
298
+ To return a pre-signed token instead (option B), pass app credentials:
299
+
300
+ ```ts
301
+ const response = await handleServerAuth({
302
+ request,
303
+ isAuthorized: () => true,
304
+ credentials: { appKey: process.env.ARK_APP_KEY!, secret: process.env.ARK_APP_SECRET! },
305
+ })
306
+ // => { token: "app_abc.<payload>.<signature>" }
307
+ ```
308
+
309
+ Or build a response directly with `createAuthorizedServerAuthResponse()`.
310
+
254
311
  ## API coverage
255
312
 
256
- | Feature | Hook / Class | Method |
257
- |---------|--------------|--------|
258
- | Health | `ArkNotifyClient` | `.health()` |
259
- | Login / me | `usePlatformAuth`, `ArkNotifyClient` | `.login()`, `.me()` |
260
- | Application CRUD | `useApplications`, `ArkNotifyClient` | `.listApplications()`, `.createApplication()`, … |
261
- | Regenerate secret | `useApplications` | `.regenerateSecret()` |
262
- | Admin channels | `useAdminChannels` | `.adminChannels()` |
263
- | Publish (server) | `ArkNotifyClient` | `.publishEvent()` |
264
- | Channel auth (server) | `ArkNotifyClient` | `.authorizeChannel()` |
313
+
314
+ | Feature | Hook / Class | Method |
315
+ | ------------------------- | ----------------------------------------- | --------------------------------------------------- |
316
+ | Health | `ArkNotifyClient` | `.health()` |
317
+ | Login / me | `usePlatformAuth`, `ArkNotifyClient` | `.login()`, `.me()` |
318
+ | Application CRUD | `useApplications`, `ArkNotifyClient` | `.listApplications()`, `.createApplication()`, … |
319
+ | Regenerate secret | `useApplications` | `.regenerateSecret()` |
320
+ | Publish (server) | `ArkNotifyClient` | `.publishEvent()` |
321
+ | Channel auth (server) | `ArkNotifyClient` | `.authorizeChannel()` |
265
322
  | Connection token (server) | `ArkNotifyClient`, `fetchConnectionToken` | `.issueConnectionToken()`, `fetchConnectionToken()` |
266
- | WebSocket connect | `useConnection`, `ArkNotifyConnection` | `.connect()` |
267
- | Subscribe / unsubscribe | `useChannel`, `ArkNotifyConnection` | `.subscribe()`, `.unsubscribe()` |
268
- | Publish (client) | `useChannel`, `ArkNotifyConnection` | `.publish()` |
269
- | Presence | `usePresence`, `ArkNotifyConnection` | `.presenceEnter()`, `.presenceUpdate()`, … |
270
- | SSE stream | `useSSE`, `ArkNotifySSE` | `.connect()` |
271
- | Private channels | `onPrivateChannelAuth` callback | — |
272
- | Auto-reconnect | `useConnection` | `autoReconnect: true` |
273
- | Heartbeat | `ArkNotifyConnection` | Server ping auto-replied |
323
+ | WebSocket connect | `useConnection`, `ArkNotifyConnection` | `.connect()` |
324
+ | Subscribe / unsubscribe | `useChannel`, `ArkNotifyConnection` | `.subscribe()`, `.unsubscribe()` |
325
+ | Publish (client) | `useChannel`, `ArkNotifyConnection` | `.publish()` |
326
+ | Presence | `usePresence`, `ArkNotifyConnection` | `.presenceEnter()`, `.presenceUpdate()`, … |
327
+ | SSE stream | `useSSE`, `ArkNotifySSE` | `.connect()` |
328
+ | Private channels | `onPrivateChannelAuth` callback | — |
329
+ | Auto-reconnect | `useConnection` | `autoReconnect: true` |
330
+ | Heartbeat | `ArkNotifyConnection` | Server ping auto-replied |
331
+
274
332
 
275
333
  ## Error handling
276
334
 
@@ -292,4 +350,4 @@ WebSocket errors are emitted via `connection.on('error', …)` or the `useConnec
292
350
 
293
351
  ## License
294
352
 
295
- MIT
353
+ MIT
package/dist/index.cjs CHANGED
@@ -35,6 +35,85 @@ function isPrivateChannel(channel) {
35
35
  return channel.startsWith("private-");
36
36
  }
37
37
 
38
+ // src/connection-token.ts
39
+ function requiresAppSecret(options) {
40
+ if ("serverAuthUrl" in options) {
41
+ const value = options.serverAuthUrl;
42
+ return value === null || value === "";
43
+ }
44
+ if ("server_auth_url" in options) {
45
+ const value = options.server_auth_url;
46
+ return value === null || value === "";
47
+ }
48
+ return false;
49
+ }
50
+ function buildConnectionTokenRequest(options) {
51
+ const {
52
+ baseUrl,
53
+ appKey,
54
+ credentials,
55
+ client_id,
56
+ clientId,
57
+ user_data,
58
+ userData,
59
+ ttl,
60
+ capabilities,
61
+ serverAuthUrl,
62
+ server_auth_url
63
+ } = options;
64
+ const resolvedClientId = client_id ?? clientId;
65
+ if (!resolvedClientId) {
66
+ throw new Error("client_id is required to fetch a connection token");
67
+ }
68
+ if (requiresAppSecret(options) && !credentials) {
69
+ throw new Error(
70
+ "credentials are required when serverAuthUrl is explicitly set to null"
71
+ );
72
+ }
73
+ const body = {
74
+ client_id: resolvedClientId,
75
+ user_data: user_data ?? userData,
76
+ ttl,
77
+ capabilities
78
+ };
79
+ if ("serverAuthUrl" in options) {
80
+ body.serverAuthUrl = serverAuthUrl;
81
+ } else if ("server_auth_url" in options) {
82
+ body.server_auth_url = server_auth_url;
83
+ }
84
+ const headers = {
85
+ "Content-Type": "application/json"
86
+ };
87
+ if (credentials) {
88
+ headers["X-App-Key"] = credentials.appKey;
89
+ headers["X-App-Secret"] = credentials.secret;
90
+ }
91
+ return {
92
+ url: `${resolveBaseUrl(baseUrl)}/api/v1/apps/${appKey}/connection-token`,
93
+ headers,
94
+ body
95
+ };
96
+ }
97
+ async function fetchConnectionToken(options) {
98
+ const { fetch: fetchFn = globalThis.fetch.bind(globalThis) } = options;
99
+ const { url, headers, body } = buildConnectionTokenRequest(options);
100
+ const response = await fetchFn(url, {
101
+ method: "POST",
102
+ headers,
103
+ body: JSON.stringify(body)
104
+ });
105
+ if (!response.ok) {
106
+ let errorBody;
107
+ try {
108
+ errorBody = await response.json();
109
+ } catch {
110
+ errorBody = { error: "request_failed", message: response.statusText };
111
+ }
112
+ throw new ArkNotifyError(response.status, errorBody);
113
+ }
114
+ return response.json();
115
+ }
116
+
38
117
  // src/client.ts
39
118
  var ArkNotifyClient = class {
40
119
  constructor(config) {
@@ -113,10 +192,6 @@ var ArkNotifyClient = class {
113
192
  method: "POST"
114
193
  });
115
194
  }
116
- // ── System admin ────────────────────────────────────────────────────────
117
- adminChannels() {
118
- return this.request("/api/v1/admin/channels");
119
- }
120
195
  // ── Data plane ──────────────────────────────────────────────────────────
121
196
  publishEvent(appKey, credentials, input) {
122
197
  return this.request(`/api/v1/apps/${appKey}/events`, {
@@ -132,63 +207,17 @@ var ArkNotifyClient = class {
132
207
  credentials
133
208
  });
134
209
  }
135
- issueConnectionToken(appKey, credentials, input) {
136
- return this.request(`/api/v1/apps/${appKey}/connection-token`, {
137
- method: "POST",
138
- body: input,
139
- credentials
210
+ issueConnectionToken(appKey, input, credentials) {
211
+ return fetchConnectionToken({
212
+ baseUrl: this.baseUrl,
213
+ appKey,
214
+ credentials,
215
+ ...input,
216
+ fetch: this.fetchFn
140
217
  });
141
218
  }
142
219
  };
143
220
 
144
- // src/connection-token.ts
145
- async function fetchConnectionToken(options) {
146
- const {
147
- baseUrl,
148
- appKey,
149
- credentials,
150
- fetch: fetchFn = globalThis.fetch.bind(globalThis),
151
- client_id,
152
- clientId,
153
- user_data,
154
- userData,
155
- ttl,
156
- capabilities,
157
- serverAuthUrl
158
- } = options;
159
- const resolvedClientId = client_id ?? clientId;
160
- if (!resolvedClientId) {
161
- throw new Error("client_id is required to fetch a connection token");
162
- }
163
- const body = {
164
- client_id: resolvedClientId,
165
- user_data: user_data ?? userData,
166
- ttl,
167
- capabilities,
168
- serverAuthUrl
169
- };
170
- const url = `${resolveBaseUrl(baseUrl)}/api/v1/apps/${appKey}/connection-token`;
171
- const response = await fetchFn(url, {
172
- method: "POST",
173
- headers: {
174
- "Content-Type": "application/json",
175
- "X-App-Key": credentials.appKey,
176
- "X-App-Secret": credentials.secret
177
- },
178
- body: JSON.stringify(body)
179
- });
180
- if (!response.ok) {
181
- let errorBody;
182
- try {
183
- errorBody = await response.json();
184
- } catch {
185
- errorBody = { error: "request_failed", message: response.statusText };
186
- }
187
- throw new ArkNotifyError(response.status, errorBody);
188
- }
189
- return response.json();
190
- }
191
-
192
221
  // src/connection.ts
193
222
  var ArkNotifyConnection = class {
194
223
  constructor(config) {
@@ -269,13 +298,14 @@ var ArkNotifyConnection = class {
269
298
  toWebSocketUrl(this.config.baseUrl, `/app/${this.config.appKey}`)
270
299
  );
271
300
  let token = resolveValue(this.config.token);
272
- if (!token && this.config.clientId && this.config.credentials) {
301
+ if (!token && this.config.clientId) {
273
302
  const result = await fetchConnectionToken({
274
303
  baseUrl: this.config.baseUrl,
275
304
  appKey: this.config.appKey,
276
305
  credentials: this.config.credentials,
277
306
  client_id: this.config.clientId,
278
307
  user_data: this.config.user_data,
308
+ serverAuthUrl: this.config.serverAuthUrl,
279
309
  fetch: this.config.fetch
280
310
  });
281
311
  token = result.token;