@dataworks-technology/data 0.1.3 → 0.1.4

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
@@ -140,7 +140,7 @@ Report an error to the Data Engine for monitoring and alerting.
140
140
  ```typescript
141
141
  await client.reportError({
142
142
  datasetDatasourceId: "ds-456",
143
- eventId: "event-123",
143
+ athleteId: "athlete-123",
144
144
  clientId: 1,
145
145
  errorTitle: "Sensor disconnected",
146
146
  errorDescription: "BLE heart rate sensor lost connection at 00:42:15",
@@ -237,7 +237,7 @@ interface Metric {
237
237
 
238
238
  interface ErrorReport {
239
239
  datasetDatasourceId: string;
240
- eventId: string;
240
+ athleteId: string;
241
241
  clientId: number;
242
242
  errorTitle: string;
243
243
  errorDescription: string;
@@ -278,12 +278,235 @@ Calling `ingest()`, `reportError()`, or `subscribe()` before `login()` throws im
278
278
  - **TypeScript** ≥ 5.0 (optional — works with plain JavaScript too)
279
279
  - **Browser** — compatible with any environment that has `fetch` and `WebSocket`
280
280
 
281
+ ## Derived Metrics — Round-Trip Example
282
+
283
+ A core use case: subscribe to live metrics, apply your own calculations externally, and push **derived metrics** back into the Data Engine. This creates a feedback loop where the platform stores both raw and enriched data.
284
+
285
+ ### Concept
286
+
287
+ ```
288
+ Data Engine → subscribe("heartrate") → Your App → calculate rolling avg → ingest("heartrate_rolling_avg") → Data Engine
289
+ ```
290
+
291
+ Any metric you ingest is treated the same as raw sensor data — it flows through the enrichment pipeline (avg/min/max/zones), gets stored, and is available via subscriptions and the GraphQL API.
292
+
293
+ ### TypeScript Example
294
+
295
+ ```typescript
296
+ import { DataClient } from "@dataworks-technology/data";
297
+
298
+ const client = new DataClient({
299
+ cognitoEndpoint: "https://cognito-idp.eu-west-1.amazonaws.com/",
300
+ clientId: "your-client-id",
301
+ ingestUrl: "https://your-ingest-endpoint.dataworks.live",
302
+ errorUrl: "https://your-error-endpoint.dataworks.live",
303
+ realtimeUrl: "https://your-realtime-endpoint.dataworks.live",
304
+ });
305
+
306
+ await client.login("developer", "password");
307
+
308
+ // 1. Subscribe to raw heart rate data
309
+ const buffer: number[] = [];
310
+
311
+ client.subscribe("dataworks/metrics", (event) => {
312
+ if (event.metric !== "heartrate") return;
313
+
314
+ // 2. Calculate a rolling 3-point average
315
+ buffer.push(event.value);
316
+ if (buffer.length > 3) buffer.shift();
317
+ const avg = buffer.reduce((a, b) => a + b, 0) / buffer.length;
318
+
319
+ // 3. Push the derived metric back into the engine
320
+ client.ingest(
321
+ [
322
+ {
323
+ athleteId: event.athleteId,
324
+ metric: "heartrate_rolling_avg",
325
+ value: Math.round(avg * 10) / 10,
326
+ timestamp: event.timestamp,
327
+ },
328
+ ],
329
+ event.eventId,
330
+ event.datasetDatasourceId,
331
+ );
332
+ });
333
+ ```
334
+
335
+ ### Python Example (Cross-Platform)
336
+
337
+ The same flow works from any language. A full working Python demo is included at `test/demo/round-trip.py` — it proves every SDK capability end-to-end:
338
+
339
+ | Phase | What it does | SDK equivalent |
340
+ |-------|-------------|----------------|
341
+ | 1. Authenticate | Cognito USER_PASSWORD_AUTH | `client.login()` |
342
+ | 2. Subscribe | WebSocket → AppSync Events API | `client.subscribe()` |
343
+ | 3. Receive | Catch events on the subscriber | `onEvent` callback |
344
+ | 4. Calculate | Write to CSV/Google Sheets, compute rolling avg + HR zones | Your business logic |
345
+ | 5. Re-ingest | POST enriched metrics back to the Data Engine | `client.ingest()` |
346
+ | 6. Report Error | Send error through the error pipeline | `client.reportError()` |
347
+
348
+ #### Run the demo
349
+
350
+ ```bash
351
+ # Install Python dependencies
352
+ pip install websocket-client requests
353
+
354
+ # Run from the repo root (reads test.env for credentials)
355
+ python test/demo/round-trip.py
356
+ ```
357
+
358
+ #### Output
359
+
360
+ ```
361
+ PHASE 4: WRITE DATA + CALCULATE
362
+ Enriched data:
363
+ HR=130 → avg=130.0, zone=Zone 2 (Aerobic), delta=—
364
+ HR=140 → avg=135.0, zone=Zone 3 (Tempo), delta=+10
365
+ HR=150 → avg=140.0, zone=Zone 3 (Tempo), delta=+10
366
+ HR=160 → avg=150.0, zone=Zone 4 (Threshold), delta=+10
367
+ HR=170 → avg=160.0, zone=Zone 4 (Threshold), delta=+10
368
+
369
+ PHASE 5: RE-INGEST ENRICHED METRICS
370
+ → heartrate_rolling_avg=130.0
371
+ → heartrate_rolling_avg=135.0
372
+ → heartrate_rolling_avg=140.0
373
+ → heartrate_rolling_avg=150.0
374
+ → heartrate_rolling_avg=160.0
375
+ ✓ Ingested (200)
376
+ ```
377
+
378
+ The raw `heartrate` values (130–170 bpm) are transformed into `heartrate_rolling_avg` derived metrics and pushed back into the platform. These derived metrics are then available to all consumers (Console, Visual, Moments triggers) just like any other metric.
379
+
380
+ #### Google Sheets (optional)
381
+
382
+ The demo can write to Google Sheets instead of CSV — formulas compute rolling averages, HR zone classification, and deltas in the spreadsheet before re-ingesting. See `test/demo/README.md` for setup.
383
+
384
+ ### Ideas for Derived Metrics
385
+
386
+ | Derived metric | Calculation | Use case |
387
+ |---|---|---|
388
+ | `heartrate_rolling_avg` | 3-point moving average | Smooth noisy sensor data |
389
+ | `heartrate_zone` | Zone classification (1–5) | Training load analysis |
390
+ | `speed_efficiency` | Speed / heart rate ratio | Fatigue detection |
391
+ | `power_to_weight` | Power / athlete weight | Normalised performance |
392
+ | `pace_delta` | Current pace − target pace | Real-time coaching alerts |
393
+ | `fatigue_index` | Custom formula over time window | Trigger warnings via Moments engine |
394
+
281
395
  ## Documentation
282
396
 
283
397
  Full documentation with guides, examples, and detailed API reference:
284
398
 
285
399
  **[https://data-docs.dataworks.live](https://data-docs.dataworks.live)**
286
400
 
401
+ ## Development
402
+
403
+ > Internal contributors only — this section covers building, testing, and publishing the package.
404
+
405
+ This package publishes to **public npm** (`registry.npmjs.org`), not to the internal CodeArtifact registry used by `@dataworks/sdk`. The `packages/sdk/.npmrc` and `publishConfig` in `package.json` enforce this.
406
+
407
+ ### Build
408
+
409
+ ```bash
410
+ bun run build # tsup → dist/ (ESM + CJS + DTS)
411
+ bun run test # vitest unit tests (mocked — no infra needed)
412
+ ```
413
+
414
+ ### Publish
415
+
416
+ Login to npm first (one-time):
417
+ ```bash
418
+ npm login --registry https://registry.npmjs.org/
419
+ ```
420
+
421
+ Then from `packages/sdk/`:
422
+ ```bash
423
+ bun run publish:patch # bump patch + publish
424
+ bun run publish:minor # bump minor + publish
425
+ bun run publish:major # bump major + publish
426
+ ```
427
+
428
+ ### E2E Tests
429
+
430
+ The e2e test suite (`test/e2e-public-sdk.test.ts`) validates the full external developer flow against deployed infrastructure. It runs against live AWS services and proves the public SDK works end-to-end across all three stacks (Shared, Data, SDK).
431
+
432
+ #### Prerequisites
433
+
434
+ Deploy all three stacks first — `make deploy` generates `test.env` with the required env vars:
435
+
436
+ | Env var | Source | Purpose |
437
+ |---|---|---|
438
+ | `FRONTEND_CLIENT_ID` | Shared stack → Cognito | App client for USER_PASSWORD_AUTH |
439
+ | `API_URL` | Data stack → API Gateway | Metric ingestion endpoint |
440
+ | `ERROR_URL` | Data stack → API Gateway | Error reporting endpoint |
441
+ | `REALTIME_URL` | Data stack → AppSync Events API | WebSocket subscription endpoint |
442
+ | `COGNITO_BASE_DOMAIN` | Shared stack → Cognito | IDP endpoint for auth |
443
+ | `DEVELOPER_USERNAME` | Shared stack → Cognito | Test user in the `developer` group |
444
+ | `DEVELOPER_PASSWORD` | Shared stack → Cognito | Test user password |
445
+
446
+ #### Run
447
+
448
+ ```bash
449
+ bun x vitest run test/e2e-public-sdk.test.ts
450
+ ```
451
+
452
+ #### What each test proves
453
+
454
+ | # | Area | Test | What it validates | How to verify manually |
455
+ |---|---|---|---|---|
456
+ | 1 | **Login** | `should authenticate testdeveloper` | USER_PASSWORD_AUTH works with frontend client | AWS Console → Cognito → User Pool → Users → `testdeveloper` exists in `developer` group |
457
+ | 2 | **Login** | `should extract tenant from JWT` | `custom:tenant` claim is present in ID token | Decode the JWT at [jwt.io](https://jwt.io) — look for `custom:tenant` |
458
+ | 3 | **Login** | `should include resource server scopes` | Pre-token Lambda injects `dataworks.live/read` and `/write` | AWS Console → Cognito → User Pool → Lambda triggers → Pre token generation V2 is set |
459
+ | 4 | **Login** | `should reject invalid credentials` | Bad password returns error, not a token | Try `client.login("testdeveloper", "wrong")` — should throw |
460
+ | 5 | **Ingest** | `should ingest valid metrics` | JWT passes API Gateway authoriser, Lambda processes payload | AWS Console → CloudWatch → Log group for the ingest Lambda — look for the metric |
461
+ | 6 | **Ingest** | `should filter invalid metrics` | Client-side validation drops bad metrics before sending | The `stderr` output shows `Dropping invalid metric` — local validation, no server call for bad data |
462
+ | 7 | **Ingest** | `should throw when not authenticated` | `ingest()` before `login()` throws immediately | Client-side guard — no network call made |
463
+ | 8 | **Error** | `should reach error endpoint` | Auth + API Gateway routing work (500 = Lambda ran, FK failed) | AWS Console → CloudWatch → Log group for error processor Lambda |
464
+ | 9 | **Error** | `should throw when not authenticated` | `reportError()` before `login()` throws immediately | Client-side guard |
465
+ | 10 | **Subscribe** | `should throw when not authenticated` | `subscribe()` before `login()` throws immediately | Client-side guard |
466
+ | 11 | **Subscribe** | `should create a subscription` | **Skipped in Node.js** (no `WebSocket` global) — see below | Use the Python verification script |
467
+ | 12–14 | **Validation** | `validateMetric` / `filterValidMetrics` | Static validation utilities work correctly | Pure functions — no infra dependency |
468
+
469
+ #### Verifying WebSocket subscribe (test 11)
470
+
471
+ The subscribe test is skipped in the Node.js Vitest environment because `globalThis.WebSocket` is not available. To prove subscribe works end-to-end, use the standalone Python script:
472
+
473
+ ```bash
474
+ pip install websocket-client requests
475
+ python test/verify-subscribe.py
476
+ ```
477
+
478
+ This script:
479
+ 1. Authenticates via Cognito USER_PASSWORD_AUTH (same as the SDK)
480
+ 2. Opens a WebSocket to the AppSync Events API with base64URL-encoded JWT auth subprotocols
481
+ 3. Sends `connection_init` and receives `connection_ack` from AppSync
482
+ 4. Sends a `subscribe` message and receives `subscribe_success`
483
+ 5. Closes cleanly
484
+
485
+ Expected output:
486
+ ```
487
+ Loading config from test.env
488
+
489
+ 1. Authenticating as 'testdeveloper'...
490
+ ✓ Login successful
491
+ Scopes: aws.cognito.signin.user.admin https://dataworks.live/write https://dataworks.live/read
492
+ Tenant: 2x9AOzBvDBgllLB0CXcCrPjK03C
493
+
494
+ 2. Connecting to wss://event-api-chris-dev.dataworks.live/event/realtime...
495
+ Protocol: aws-appsync-event-ws (manual)
496
+ Received: connection_ack
497
+ ✓ WebSocket connection established
498
+
499
+ 3. Subscribing to channel '/dataworks/e2e-verify'...
500
+ Received: subscribe_success
501
+ ✓ Subscribed to /dataworks/e2e-verify
502
+
503
+ 4. WebSocket closed cleanly
504
+
505
+ ============================================================
506
+ ALL CHECKS PASSED — subscribe flow works end-to-end
507
+ ============================================================
508
+ ```
509
+
287
510
  ## License
288
511
 
289
512
  MIT © [Dataworks Technology](https://github.com/Dataworks-Technology)
package/dist/index.cjs CHANGED
@@ -192,7 +192,13 @@ var DataClient = class {
192
192
  "Content-Type": "application/json",
193
193
  Authorization: `Bearer ${this.credentials.accessToken}`
194
194
  },
195
- body: JSON.stringify(error)
195
+ body: JSON.stringify({
196
+ client_id: String(error.clientId),
197
+ dataset_datasource_id: error.datasetDatasourceId,
198
+ athlete_id: error.athleteId,
199
+ error_title: error.errorTitle,
200
+ error_description: error.errorDescription
201
+ })
196
202
  });
197
203
  if (!resp.ok) {
198
204
  const body = await resp.text().catch(() => "");
@@ -217,33 +223,34 @@ var DataClient = class {
217
223
  const url = new URL(this.config.realtimeUrl);
218
224
  url.pathname = "/event/realtime";
219
225
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
226
+ const authPayload = JSON.stringify({
227
+ Authorization: this.credentials.accessToken,
228
+ host: new URL(this.config.realtimeUrl).host
229
+ });
230
+ const authHeader = btoa(authPayload).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
220
231
  const ws = new WebSocket(url.toString(), [
221
232
  "aws-appsync-event-ws",
222
- // Encode auth as a base64 subprotocol header
223
- `header-${btoa(
224
- JSON.stringify({
225
- Authorization: this.credentials.accessToken,
226
- host: new URL(this.config.realtimeUrl).host
227
- })
228
- )}`
233
+ `header-${authHeader}`
229
234
  ]);
230
235
  ws.addEventListener("open", () => {
231
- ws.send(
232
- JSON.stringify({
233
- type: "subscribe",
234
- id: crypto.randomUUID(),
235
- channel: `/${channel}`,
236
- authorization: {
237
- Authorization: this.credentials.accessToken,
238
- host: new URL(this.config.realtimeUrl).host
239
- }
240
- })
241
- );
236
+ ws.send(JSON.stringify({ type: "connection_init" }));
242
237
  });
243
238
  ws.addEventListener("message", (event) => {
244
239
  try {
245
240
  const msg = JSON.parse(String(event.data));
246
- if (msg.type === "data") {
241
+ if (msg.type === "connection_ack") {
242
+ ws.send(
243
+ JSON.stringify({
244
+ type: "subscribe",
245
+ id: crypto.randomUUID(),
246
+ channel: `/${channel}`,
247
+ authorization: {
248
+ Authorization: this.credentials.accessToken,
249
+ host: new URL(this.config.realtimeUrl).host
250
+ }
251
+ })
252
+ );
253
+ } else if (msg.type === "data") {
247
254
  onEvent(JSON.parse(msg.event));
248
255
  }
249
256
  } catch {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../../../node_modules/@dataworks/sdk/dist/client/validation.js","../../../node_modules/@dataworks/sdk/dist/client/auth.js","../src/validation.ts","../src/data-client.ts"],"sourcesContent":["/**\n * @dataworks-technology/data — Dataworks Data Engine SDK\n *\n * Public npm package for external developers to:\n * - Authenticate via Cognito\n * - Ingest live athlete metrics\n * - Subscribe to real-time data streams\n * - Report errors\n *\n * @example\n * ```ts\n * import { DataClient } from \"@dataworks-technology/data\";\n *\n * const client = new DataClient({ ... });\n * await client.login(\"username\", \"password\");\n * await client.ingest(metrics, eventId, dsId);\n * ```\n *\n * @module @dataworks-technology/data\n */\n\nexport { DataClient } from \"./data-client.js\";\n\nexport type {\n DataClientConfig,\n LoginResult,\n Metric,\n MetricItem,\n RawMetricItem,\n MetricsPayload,\n ErrorReport,\n SubscriptionHandler,\n Subscription,\n} from \"./types.js\";\n\n// Re-export validation utilities for standalone use\nexport { validateMetric, filterValidMetrics } from \"./validation.js\";\n","/**\n * Metric validation for Dataworks payloads.\n *\n * Ported from Dataworks-Data packages/adaptors/shared-ts/src/validation.ts\n * to serve as the single source of truth for all Dataworks engine SDKs.\n *\n * @module @dataworks/sdk/client\n */\n/**\n * Returns null if the metric is valid, or a human-readable reason string if invalid.\n */\nexport function validateMetric(m) {\n if (typeof m.metric !== \"string\" || m.metric.trim() === \"\")\n return \"metric must be a non-empty string\";\n if (typeof m.athleteId !== \"string\" || m.athleteId.trim() === \"\")\n return \"athleteId must be a non-empty string\";\n if (!Number.isInteger(m.timestamp) || m.timestamp <= 0)\n return `timestamp must be a positive integer, got ${m.timestamp}`;\n if (m.value === null || m.value === undefined)\n return \"value is null/undefined\";\n if (typeof m.value === \"number\" && !isFinite(m.value))\n return `value is non-finite number (${m.value})`;\n if (typeof m.value !== \"number\" && typeof m.value !== \"string\")\n return `value type ${typeof m.value} not allowed (must be number or string)`;\n if (typeof m.value === \"string\" && m.value === \"\")\n return \"value is empty string\";\n return null;\n}\n/**\n * Filters an array of metrics, returning only valid ones.\n * Logs a warning for each dropped item via the provided warn function.\n */\nexport function filterValidMetrics(metrics, warn) {\n const valid = [];\n for (const m of metrics) {\n const reason = validateMetric(m);\n if (reason) {\n warn(`Dropping invalid metric [${String(m.metric)}=${JSON.stringify(m.value)}]: ${reason}`);\n }\n else {\n valid.push(m);\n }\n }\n return valid;\n}\n","/**\n * Cognito authentication helpers.\n *\n * Provides M2M (client_credentials) and user (USER_PASSWORD_AUTH) flows.\n * Refactored from @dataworks/sdk e2e-utils.ts — uses native fetch instead\n * of axios so the client sub-path has zero heavy dependencies.\n *\n * @module @dataworks/sdk/client\n */\n/**\n * Fetch an M2M access token using the client_credentials grant.\n *\n * @param clientId - Cognito app client ID\n * @param clientSecret - Cognito app client secret\n * @param tokenUrl - Cognito token endpoint (e.g. https://<domain>.auth.<region>.amazoncognito.com/oauth2/token)\n * @returns Access token string\n */\nexport async function getClientToken(clientId, clientSecret, tokenUrl) {\n const resp = await fetch(tokenUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({\n grant_type: \"client_credentials\",\n client_id: clientId,\n client_secret: clientSecret,\n }),\n });\n if (!resp.ok) {\n throw new Error(`getClientToken: Cognito token request failed: ${resp.status} ${resp.statusText}`);\n }\n const data = (await resp.json());\n if (!data.access_token) {\n throw new Error(\"getClientToken: response missing access_token\");\n }\n return data.access_token;\n}\n/**\n * Authenticate a user via Cognito USER_PASSWORD_AUTH flow.\n * Returns access token, ID token, refresh token, and tenant (extracted from JWT).\n *\n * Used by external developer clients — no client secret required when the\n * Cognito app client is configured without one.\n *\n * @param username - Cognito username\n * @param password - Cognito password\n * @param config - Cognito endpoint and client configuration\n * @returns Login result with tokens and tenant\n */\nexport async function loginWithCredentials(username, password, config) {\n const authParams = {\n USERNAME: username,\n PASSWORD: password,\n };\n // If a client secret is configured, compute SECRET_HASH\n if (config.clientSecret) {\n // Dynamic import to keep this module usable in environments without crypto\n const crypto = await import(\"crypto\");\n const hash = crypto\n .createHmac(\"sha256\", config.clientSecret)\n .update(username + config.clientId)\n .digest(\"base64\");\n authParams.SECRET_HASH = hash;\n }\n const resp = await fetch(config.cognitoEndpoint, {\n method: \"POST\",\n headers: {\n \"X-Amz-Target\": \"AWSCognitoIdentityProviderService.InitiateAuth\",\n \"Content-Type\": \"application/x-amz-json-1.1\",\n },\n body: JSON.stringify({\n AuthFlow: \"USER_PASSWORD_AUTH\",\n ClientId: config.clientId,\n AuthParameters: authParams,\n }),\n });\n if (!resp.ok) {\n const body = await resp.text().catch(() => \"\");\n throw new Error(`loginWithCredentials: Cognito auth failed: ${resp.status} — ${body}`);\n }\n const data = (await resp.json());\n const result = data.AuthenticationResult;\n if (!result?.AccessToken || !result?.IdToken) {\n throw new Error(\"loginWithCredentials: response missing AccessToken or IdToken\");\n }\n // Extract tenant from JWT claims (ID token payload)\n const tenant = extractTenantFromJwt(result.IdToken);\n return {\n accessToken: result.AccessToken,\n idToken: result.IdToken,\n refreshToken: result.RefreshToken ?? \"\",\n tenant,\n };\n}\n/**\n * Extracts the tenant claim from a JWT ID token without verifying the signature.\n * The token is validated server-side by AppSync/API Gateway — this is a client-side\n * convenience for reading the tenant value.\n */\nfunction extractTenantFromJwt(idToken) {\n try {\n const payload = idToken.split(\".\")[1];\n const decoded = JSON.parse(Buffer.from(payload, \"base64url\").toString(\"utf-8\"));\n // Cognito custom attributes use \"custom:tenant\" claim\n return (decoded[\"custom:tenant\"] ?? decoded.tenant ?? decoded[\"cognito:groups\"]?.[0] ?? \"\");\n }\n catch {\n return \"\";\n }\n}\n","/**\n * Validation re-exports — thin wrappers around @dataworks/sdk/client validation.\n * Declared locally so the published .d.ts is fully self-contained.\n *\n * @module @dataworks-technology/data\n */\n\nimport {\n validateMetric as _validateMetric,\n filterValidMetrics as _filterValidMetrics,\n} from \"@dataworks/sdk/client\";\nimport type { RawMetricItem, MetricItem } from \"./types.js\";\n\n/**\n * Check whether a metric is valid.\n * Returns null if valid, or a human-readable reason string if invalid.\n */\nexport const validateMetric: (m: RawMetricItem) => string | null =\n _validateMetric;\n\n/**\n * Filter an array of metrics, keeping only valid ones.\n * Logs a warning for each dropped item via the provided warn function.\n */\nexport const filterValidMetrics: (\n metrics: RawMetricItem[],\n warn: (msg: string) => void,\n) => MetricItem[] = _filterValidMetrics;\n","/**\n * DataClient — the public entry point for external developers using the Dataworks Data Engine.\n *\n * Handles authentication, metric ingestion, error reporting, and real-time subscriptions.\n * All SDK foundation code (@dataworks/sdk) is bundled at build time — consumers install\n * only @dataworks/data with zero transitive dependencies.\n *\n * @example\n * ```ts\n * import { DataClient } from \"@dataworks-technology/data\";\n *\n * const client = new DataClient({\n * cognitoEndpoint: \"https://cognito-idp.eu-west-1.amazonaws.com/\",\n * clientId: \"abc123\",\n * ingestUrl: \"https://dev-realtime.dataworks.live\",\n * errorUrl: \"https://dev-realtime-errors.dataworks.live\",\n * realtimeUrl: \"https://dev-event-api.dataworks.live\",\n * });\n *\n * await client.login(\"graeme\", \"password123\");\n * await client.ingest([{ athleteId: \"1\", metric: \"heartrate\", value: 172, timestamp: Date.now() / 1000 }], \"evt1\", \"ds1\");\n * ```\n *\n * @module @dataworks-technology/data\n */\n\nimport { loginWithCredentials } from \"@dataworks/sdk/client\";\nimport { validateMetric, filterValidMetrics } from \"./validation.js\";\nimport type {\n DataClientConfig,\n LoginResult,\n Metric,\n ErrorReport,\n SubscriptionHandler,\n Subscription,\n} from \"./types.js\";\n\nexport class DataClient {\n private readonly config: DataClientConfig;\n private credentials: LoginResult | null = null;\n\n constructor(config: DataClientConfig) {\n this.config = config;\n }\n\n /**\n * Authenticate with the Dataworks platform using Cognito USER_PASSWORD_AUTH.\n * Must be called before ingest(), reportError(), or subscribe().\n *\n * @param username - Cognito username\n * @param password - Cognito password\n * @returns Login result containing tokens and tenant\n */\n async login(username: string, password: string): Promise<LoginResult> {\n this.credentials = await loginWithCredentials(username, password, {\n cognitoEndpoint: this.config.cognitoEndpoint,\n clientId: this.config.clientId,\n });\n return this.credentials;\n }\n\n /**\n * Ingest metric data points into the Dataworks Data Engine.\n * Validates each metric before sending — invalid metrics are silently dropped.\n *\n * @param metrics - Array of metric data points\n * @param eventId - Event identifier\n * @param datasetDatasourceId - Dataset-datasource identifier\n * @throws Error if not authenticated or if the request fails\n */\n async ingest(\n metrics: Metric[],\n eventId: string,\n datasetDatasourceId: string,\n ): Promise<void> {\n this.requireAuth();\n\n const valid = filterValidMetrics(metrics, (msg) =>\n console.warn(`[@dataworks-technology/data] ${msg}`),\n );\n\n if (valid.length === 0) return;\n\n const payload = {\n eventId,\n datasetDatasourceId,\n metrics: valid,\n };\n\n const resp = await fetch(this.config.ingestUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.credentials!.accessToken}`,\n },\n body: JSON.stringify(payload),\n });\n\n if (!resp.ok) {\n const body = await resp.text().catch(() => \"\");\n throw new Error(\n `[@dataworks-technology/data] ingest failed: ${resp.status} — ${body}`,\n );\n }\n }\n\n /**\n * Report an error to the Dataworks Data Engine.\n *\n * @param error - Error report details\n * @throws Error if not authenticated or if the request fails\n */\n async reportError(error: ErrorReport): Promise<void> {\n this.requireAuth();\n\n const resp = await fetch(this.config.errorUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.credentials!.accessToken}`,\n },\n body: JSON.stringify(error),\n });\n\n if (!resp.ok) {\n const body = await resp.text().catch(() => \"\");\n throw new Error(\n `[@dataworks-technology/data] reportError failed: ${resp.status} — ${body}`,\n );\n }\n }\n\n /**\n * Subscribe to a real-time data channel on the AppSync Events API.\n * Uses the Cognito JWT for authentication.\n *\n * The channel name maps to the AppSync Events API namespace (e.g. \"dataworks/metrics\").\n *\n * @param channel - Channel path to subscribe to\n * @param onEvent - Callback invoked for each received event\n * @returns Subscription handle with a close() method\n * @throws Error if not authenticated\n */\n subscribe(channel: string, onEvent: SubscriptionHandler): Subscription {\n this.requireAuth();\n\n const url = new URL(this.config.realtimeUrl);\n // AppSync Events API uses the /event/realtime path for WebSocket connections\n url.pathname = \"/event/realtime\";\n url.protocol = url.protocol === \"https:\" ? \"wss:\" : \"ws:\";\n\n const ws = new WebSocket(url.toString(), [\n \"aws-appsync-event-ws\",\n // Encode auth as a base64 subprotocol header\n `header-${btoa(\n JSON.stringify({\n Authorization: this.credentials!.accessToken,\n host: new URL(this.config.realtimeUrl).host,\n }),\n )}`,\n ]);\n\n ws.addEventListener(\"open\", () => {\n // Subscribe to the channel after connection is established\n ws.send(\n JSON.stringify({\n type: \"subscribe\",\n id: crypto.randomUUID(),\n channel: `/${channel}`,\n authorization: {\n Authorization: this.credentials!.accessToken,\n host: new URL(this.config.realtimeUrl).host,\n },\n }),\n );\n });\n\n ws.addEventListener(\"message\", (event) => {\n try {\n const msg = JSON.parse(String(event.data));\n if (msg.type === \"data\") {\n onEvent(JSON.parse(msg.event));\n }\n } catch {\n // Ignore non-data messages (connection_ack, subscribe_success, etc.)\n }\n });\n\n return {\n close() {\n ws.close();\n },\n };\n }\n\n /**\n * Check whether a metric is valid before ingesting.\n * Returns null if valid, or a human-readable reason string if invalid.\n */\n static validateMetric = validateMetric;\n\n /**\n * Filter an array of metrics, keeping only valid ones.\n * Logs a warning for each dropped item.\n */\n static filterValidMetrics = filterValidMetrics;\n\n /** Returns true if the client has been authenticated via login(). */\n get isAuthenticated(): boolean {\n return this.credentials !== null;\n }\n\n /** Returns the tenant from the last successful login, or null. */\n get tenant(): string | null {\n return this.credentials?.tenant ?? null;\n }\n\n /** @internal Throws if not authenticated. */\n private requireAuth(): void {\n if (!this.credentials) {\n throw new Error(\n \"[@dataworks-technology/data] Not authenticated — call login() first\",\n );\n }\n }\n}\n"],"mappings":"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","names":["filterValidMetrics","validateMetric","crypto","validateMetric","filterValidMetrics","filterValidMetrics","validateMetric"]}
1
+ {"version":3,"sources":["../src/index.ts","../../../node_modules/@dataworks/sdk/dist/client/validation.js","../../../node_modules/@dataworks/sdk/dist/client/auth.js","../src/validation.ts","../src/data-client.ts"],"sourcesContent":["/**\n * @dataworks-technology/data — Dataworks Data Engine SDK\n *\n * Public npm package for external developers to:\n * - Authenticate via Cognito\n * - Ingest live athlete metrics\n * - Subscribe to real-time data streams\n * - Report errors\n *\n * @example\n * ```ts\n * import { DataClient } from \"@dataworks-technology/data\";\n *\n * const client = new DataClient({ ... });\n * await client.login(\"username\", \"password\");\n * await client.ingest(metrics, eventId, dsId);\n * ```\n *\n * @module @dataworks-technology/data\n */\n\nexport { DataClient } from \"./data-client.js\";\n\nexport type {\n DataClientConfig,\n LoginResult,\n Metric,\n MetricItem,\n RawMetricItem,\n MetricsPayload,\n ErrorReport,\n SubscriptionHandler,\n Subscription,\n} from \"./types.js\";\n\n// Re-export validation utilities for standalone use\nexport { validateMetric, filterValidMetrics } from \"./validation.js\";\n","/**\n * Metric validation for Dataworks payloads.\n *\n * Ported from Dataworks-Data packages/adaptors/shared-ts/src/validation.ts\n * to serve as the single source of truth for all Dataworks engine SDKs.\n *\n * @module @dataworks/sdk/client\n */\n/**\n * Returns null if the metric is valid, or a human-readable reason string if invalid.\n */\nexport function validateMetric(m) {\n if (typeof m.metric !== \"string\" || m.metric.trim() === \"\")\n return \"metric must be a non-empty string\";\n if (typeof m.athleteId !== \"string\" || m.athleteId.trim() === \"\")\n return \"athleteId must be a non-empty string\";\n if (!Number.isInteger(m.timestamp) || m.timestamp <= 0)\n return `timestamp must be a positive integer, got ${m.timestamp}`;\n if (m.value === null || m.value === undefined)\n return \"value is null/undefined\";\n if (typeof m.value === \"number\" && !isFinite(m.value))\n return `value is non-finite number (${m.value})`;\n if (typeof m.value !== \"number\" && typeof m.value !== \"string\")\n return `value type ${typeof m.value} not allowed (must be number or string)`;\n if (typeof m.value === \"string\" && m.value === \"\")\n return \"value is empty string\";\n return null;\n}\n/**\n * Filters an array of metrics, returning only valid ones.\n * Logs a warning for each dropped item via the provided warn function.\n */\nexport function filterValidMetrics(metrics, warn) {\n const valid = [];\n for (const m of metrics) {\n const reason = validateMetric(m);\n if (reason) {\n warn(`Dropping invalid metric [${String(m.metric)}=${JSON.stringify(m.value)}]: ${reason}`);\n }\n else {\n valid.push(m);\n }\n }\n return valid;\n}\n","/**\n * Cognito authentication helpers.\n *\n * Provides M2M (client_credentials) and user (USER_PASSWORD_AUTH) flows.\n * Refactored from @dataworks/sdk e2e-utils.ts — uses native fetch instead\n * of axios so the client sub-path has zero heavy dependencies.\n *\n * @module @dataworks/sdk/client\n */\n/**\n * Fetch an M2M access token using the client_credentials grant.\n *\n * @param clientId - Cognito app client ID\n * @param clientSecret - Cognito app client secret\n * @param tokenUrl - Cognito token endpoint (e.g. https://<domain>.auth.<region>.amazoncognito.com/oauth2/token)\n * @returns Access token string\n */\nexport async function getClientToken(clientId, clientSecret, tokenUrl) {\n const resp = await fetch(tokenUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({\n grant_type: \"client_credentials\",\n client_id: clientId,\n client_secret: clientSecret,\n }),\n });\n if (!resp.ok) {\n throw new Error(`getClientToken: Cognito token request failed: ${resp.status} ${resp.statusText}`);\n }\n const data = (await resp.json());\n if (!data.access_token) {\n throw new Error(\"getClientToken: response missing access_token\");\n }\n return data.access_token;\n}\n/**\n * Authenticate a user via Cognito USER_PASSWORD_AUTH flow.\n * Returns access token, ID token, refresh token, and tenant (extracted from JWT).\n *\n * Used by external developer clients — no client secret required when the\n * Cognito app client is configured without one.\n *\n * @param username - Cognito username\n * @param password - Cognito password\n * @param config - Cognito endpoint and client configuration\n * @returns Login result with tokens and tenant\n */\nexport async function loginWithCredentials(username, password, config) {\n const authParams = {\n USERNAME: username,\n PASSWORD: password,\n };\n // If a client secret is configured, compute SECRET_HASH\n if (config.clientSecret) {\n // Dynamic import to keep this module usable in environments without crypto\n const crypto = await import(\"crypto\");\n const hash = crypto\n .createHmac(\"sha256\", config.clientSecret)\n .update(username + config.clientId)\n .digest(\"base64\");\n authParams.SECRET_HASH = hash;\n }\n const resp = await fetch(config.cognitoEndpoint, {\n method: \"POST\",\n headers: {\n \"X-Amz-Target\": \"AWSCognitoIdentityProviderService.InitiateAuth\",\n \"Content-Type\": \"application/x-amz-json-1.1\",\n },\n body: JSON.stringify({\n AuthFlow: \"USER_PASSWORD_AUTH\",\n ClientId: config.clientId,\n AuthParameters: authParams,\n }),\n });\n if (!resp.ok) {\n const body = await resp.text().catch(() => \"\");\n throw new Error(`loginWithCredentials: Cognito auth failed: ${resp.status} — ${body}`);\n }\n const data = (await resp.json());\n const result = data.AuthenticationResult;\n if (!result?.AccessToken || !result?.IdToken) {\n throw new Error(\"loginWithCredentials: response missing AccessToken or IdToken\");\n }\n // Extract tenant from JWT claims (ID token payload)\n const tenant = extractTenantFromJwt(result.IdToken);\n return {\n accessToken: result.AccessToken,\n idToken: result.IdToken,\n refreshToken: result.RefreshToken ?? \"\",\n tenant,\n };\n}\n/**\n * Extracts the tenant claim from a JWT ID token without verifying the signature.\n * The token is validated server-side by AppSync/API Gateway — this is a client-side\n * convenience for reading the tenant value.\n */\nfunction extractTenantFromJwt(idToken) {\n try {\n const payload = idToken.split(\".\")[1];\n const decoded = JSON.parse(Buffer.from(payload, \"base64url\").toString(\"utf-8\"));\n // Cognito custom attributes use \"custom:tenant\" claim\n return (decoded[\"custom:tenant\"] ?? decoded.tenant ?? decoded[\"cognito:groups\"]?.[0] ?? \"\");\n }\n catch {\n return \"\";\n }\n}\n","/**\n * Validation re-exports — thin wrappers around @dataworks/sdk/client validation.\n * Declared locally so the published .d.ts is fully self-contained.\n *\n * @module @dataworks-technology/data\n */\n\nimport {\n validateMetric as _validateMetric,\n filterValidMetrics as _filterValidMetrics,\n} from \"@dataworks/sdk/client\";\nimport type { RawMetricItem, MetricItem } from \"./types.js\";\n\n/**\n * Check whether a metric is valid.\n * Returns null if valid, or a human-readable reason string if invalid.\n */\nexport const validateMetric: (m: RawMetricItem) => string | null =\n _validateMetric;\n\n/**\n * Filter an array of metrics, keeping only valid ones.\n * Logs a warning for each dropped item via the provided warn function.\n */\nexport const filterValidMetrics: (\n metrics: RawMetricItem[],\n warn: (msg: string) => void,\n) => MetricItem[] = _filterValidMetrics;\n","/**\n * DataClient — the public entry point for external developers using the Dataworks Data Engine.\n *\n * Handles authentication, metric ingestion, error reporting, and real-time subscriptions.\n * All SDK foundation code (@dataworks/sdk) is bundled at build time — consumers install\n * only @dataworks/data with zero transitive dependencies.\n *\n * @example\n * ```ts\n * import { DataClient } from \"@dataworks-technology/data\";\n *\n * const client = new DataClient({\n * cognitoEndpoint: \"https://cognito-idp.eu-west-1.amazonaws.com/\",\n * clientId: \"abc123\",\n * ingestUrl: \"https://dev-realtime.dataworks.live\",\n * errorUrl: \"https://dev-realtime-errors.dataworks.live\",\n * realtimeUrl: \"https://dev-event-api.dataworks.live\",\n * });\n *\n * await client.login(\"graeme\", \"password123\");\n * await client.ingest([{ athleteId: \"1\", metric: \"heartrate\", value: 172, timestamp: Date.now() / 1000 }], \"evt1\", \"ds1\");\n * ```\n *\n * @module @dataworks-technology/data\n */\n\nimport { loginWithCredentials } from \"@dataworks/sdk/client\";\nimport { validateMetric, filterValidMetrics } from \"./validation.js\";\nimport type {\n DataClientConfig,\n LoginResult,\n Metric,\n ErrorReport,\n SubscriptionHandler,\n Subscription,\n} from \"./types.js\";\n\nexport class DataClient {\n private readonly config: DataClientConfig;\n private credentials: LoginResult | null = null;\n\n constructor(config: DataClientConfig) {\n this.config = config;\n }\n\n /**\n * Authenticate with the Dataworks platform using Cognito USER_PASSWORD_AUTH.\n * Must be called before ingest(), reportError(), or subscribe().\n *\n * @param username - Cognito username\n * @param password - Cognito password\n * @returns Login result containing tokens and tenant\n */\n async login(username: string, password: string): Promise<LoginResult> {\n this.credentials = await loginWithCredentials(username, password, {\n cognitoEndpoint: this.config.cognitoEndpoint,\n clientId: this.config.clientId,\n });\n return this.credentials;\n }\n\n /**\n * Ingest metric data points into the Dataworks Data Engine.\n * Validates each metric before sending — invalid metrics are silently dropped.\n *\n * @param metrics - Array of metric data points\n * @param eventId - Event identifier\n * @param datasetDatasourceId - Dataset-datasource identifier\n * @throws Error if not authenticated or if the request fails\n */\n async ingest(\n metrics: Metric[],\n eventId: string,\n datasetDatasourceId: string,\n ): Promise<void> {\n this.requireAuth();\n\n const valid = filterValidMetrics(metrics, (msg) =>\n console.warn(`[@dataworks-technology/data] ${msg}`),\n );\n\n if (valid.length === 0) return;\n\n const payload = {\n eventId,\n datasetDatasourceId,\n metrics: valid,\n };\n\n const resp = await fetch(this.config.ingestUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.credentials!.accessToken}`,\n },\n body: JSON.stringify(payload),\n });\n\n if (!resp.ok) {\n const body = await resp.text().catch(() => \"\");\n throw new Error(\n `[@dataworks-technology/data] ingest failed: ${resp.status} — ${body}`,\n );\n }\n }\n\n /**\n * Report an error to the Dataworks Data Engine.\n *\n * @param error - Error report details\n * @throws Error if not authenticated or if the request fails\n */\n async reportError(error: ErrorReport): Promise<void> {\n this.requireAuth();\n\n const resp = await fetch(this.config.errorUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.credentials!.accessToken}`,\n },\n body: JSON.stringify({\n client_id: String(error.clientId),\n dataset_datasource_id: error.datasetDatasourceId,\n athlete_id: error.athleteId,\n error_title: error.errorTitle,\n error_description: error.errorDescription,\n }),\n });\n\n if (!resp.ok) {\n const body = await resp.text().catch(() => \"\");\n throw new Error(\n `[@dataworks-technology/data] reportError failed: ${resp.status} — ${body}`,\n );\n }\n }\n\n /**\n * Subscribe to a real-time data channel on the AppSync Events API.\n * Uses the Cognito JWT for authentication.\n *\n * The channel name maps to the AppSync Events API namespace (e.g. \"dataworks/metrics\").\n *\n * @param channel - Channel path to subscribe to\n * @param onEvent - Callback invoked for each received event\n * @returns Subscription handle with a close() method\n * @throws Error if not authenticated\n */\n subscribe(channel: string, onEvent: SubscriptionHandler): Subscription {\n this.requireAuth();\n\n const url = new URL(this.config.realtimeUrl);\n // AppSync Events API uses the /event/realtime path for WebSocket connections\n url.pathname = \"/event/realtime\";\n url.protocol = url.protocol === \"https:\" ? \"wss:\" : \"ws:\";\n\n // AWS AppSync Events API requires base64URL-encoded auth in the subprotocol.\n // Standard base64 (btoa) contains +, /, = which violate RFC 6455 token rules\n // and are rejected by non-browser WebSocket implementations (Bun, Node).\n const authPayload = JSON.stringify({\n Authorization: this.credentials!.accessToken,\n host: new URL(this.config.realtimeUrl).host,\n });\n const authHeader = btoa(authPayload)\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/, \"\");\n\n const ws = new WebSocket(url.toString(), [\n \"aws-appsync-event-ws\",\n `header-${authHeader}`,\n ]);\n\n ws.addEventListener(\"open\", () => {\n // Send connection_init — required for AppSync to send connection_ack\n ws.send(JSON.stringify({ type: \"connection_init\" }));\n });\n\n ws.addEventListener(\"message\", (event) => {\n try {\n const msg = JSON.parse(String(event.data));\n\n if (msg.type === \"connection_ack\") {\n // Connection established — now subscribe to the channel\n ws.send(\n JSON.stringify({\n type: \"subscribe\",\n id: crypto.randomUUID(),\n channel: `/${channel}`,\n authorization: {\n Authorization: this.credentials!.accessToken,\n host: new URL(this.config.realtimeUrl).host,\n },\n }),\n );\n } else if (msg.type === \"data\") {\n onEvent(JSON.parse(msg.event));\n }\n } catch {\n // Ignore malformed or non-data messages (ka, subscribe_success, etc.)\n }\n });\n\n return {\n close() {\n ws.close();\n },\n };\n }\n\n /**\n * Check whether a metric is valid before ingesting.\n * Returns null if valid, or a human-readable reason string if invalid.\n */\n static validateMetric = validateMetric;\n\n /**\n * Filter an array of metrics, keeping only valid ones.\n * Logs a warning for each dropped item.\n */\n static filterValidMetrics = filterValidMetrics;\n\n /** Returns true if the client has been authenticated via login(). */\n get isAuthenticated(): boolean {\n return this.credentials !== null;\n }\n\n /** Returns the tenant from the last successful login, or null. */\n get tenant(): string | null {\n return this.credentials?.tenant ?? null;\n }\n\n /** @internal Throws if not authenticated. */\n private requireAuth(): void {\n if (!this.credentials) {\n throw new Error(\n \"[@dataworks-technology/data] Not authenticated — call login() first\",\n );\n }\n }\n}\n"],"mappings":"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","names":["filterValidMetrics","validateMetric","crypto","validateMetric","filterValidMetrics","filterValidMetrics","validateMetric"]}
package/dist/index.d.cts CHANGED
@@ -59,8 +59,8 @@ interface MetricsPayload {
59
59
  interface ErrorReport {
60
60
  /** Dataset-datasource identifier */
61
61
  datasetDatasourceId: string;
62
- /** Event identifier */
63
- eventId: string;
62
+ /** Athlete identifier */
63
+ athleteId: string;
64
64
  /** Client identifier */
65
65
  clientId: number;
66
66
  /** Short error title */
package/dist/index.d.ts CHANGED
@@ -59,8 +59,8 @@ interface MetricsPayload {
59
59
  interface ErrorReport {
60
60
  /** Dataset-datasource identifier */
61
61
  datasetDatasourceId: string;
62
- /** Event identifier */
63
- eventId: string;
62
+ /** Athlete identifier */
63
+ athleteId: string;
64
64
  /** Client identifier */
65
65
  clientId: number;
66
66
  /** Short error title */
package/dist/index.js CHANGED
@@ -154,7 +154,13 @@ var DataClient = class {
154
154
  "Content-Type": "application/json",
155
155
  Authorization: `Bearer ${this.credentials.accessToken}`
156
156
  },
157
- body: JSON.stringify(error)
157
+ body: JSON.stringify({
158
+ client_id: String(error.clientId),
159
+ dataset_datasource_id: error.datasetDatasourceId,
160
+ athlete_id: error.athleteId,
161
+ error_title: error.errorTitle,
162
+ error_description: error.errorDescription
163
+ })
158
164
  });
159
165
  if (!resp.ok) {
160
166
  const body = await resp.text().catch(() => "");
@@ -179,33 +185,34 @@ var DataClient = class {
179
185
  const url = new URL(this.config.realtimeUrl);
180
186
  url.pathname = "/event/realtime";
181
187
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
188
+ const authPayload = JSON.stringify({
189
+ Authorization: this.credentials.accessToken,
190
+ host: new URL(this.config.realtimeUrl).host
191
+ });
192
+ const authHeader = btoa(authPayload).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
182
193
  const ws = new WebSocket(url.toString(), [
183
194
  "aws-appsync-event-ws",
184
- // Encode auth as a base64 subprotocol header
185
- `header-${btoa(
186
- JSON.stringify({
187
- Authorization: this.credentials.accessToken,
188
- host: new URL(this.config.realtimeUrl).host
189
- })
190
- )}`
195
+ `header-${authHeader}`
191
196
  ]);
192
197
  ws.addEventListener("open", () => {
193
- ws.send(
194
- JSON.stringify({
195
- type: "subscribe",
196
- id: crypto.randomUUID(),
197
- channel: `/${channel}`,
198
- authorization: {
199
- Authorization: this.credentials.accessToken,
200
- host: new URL(this.config.realtimeUrl).host
201
- }
202
- })
203
- );
198
+ ws.send(JSON.stringify({ type: "connection_init" }));
204
199
  });
205
200
  ws.addEventListener("message", (event) => {
206
201
  try {
207
202
  const msg = JSON.parse(String(event.data));
208
- if (msg.type === "data") {
203
+ if (msg.type === "connection_ack") {
204
+ ws.send(
205
+ JSON.stringify({
206
+ type: "subscribe",
207
+ id: crypto.randomUUID(),
208
+ channel: `/${channel}`,
209
+ authorization: {
210
+ Authorization: this.credentials.accessToken,
211
+ host: new URL(this.config.realtimeUrl).host
212
+ }
213
+ })
214
+ );
215
+ } else if (msg.type === "data") {
209
216
  onEvent(JSON.parse(msg.event));
210
217
  }
211
218
  } catch {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../node_modules/@dataworks/sdk/dist/client/validation.js","../../../node_modules/@dataworks/sdk/dist/client/auth.js","../src/validation.ts","../src/data-client.ts"],"sourcesContent":["/**\n * Metric validation for Dataworks payloads.\n *\n * Ported from Dataworks-Data packages/adaptors/shared-ts/src/validation.ts\n * to serve as the single source of truth for all Dataworks engine SDKs.\n *\n * @module @dataworks/sdk/client\n */\n/**\n * Returns null if the metric is valid, or a human-readable reason string if invalid.\n */\nexport function validateMetric(m) {\n if (typeof m.metric !== \"string\" || m.metric.trim() === \"\")\n return \"metric must be a non-empty string\";\n if (typeof m.athleteId !== \"string\" || m.athleteId.trim() === \"\")\n return \"athleteId must be a non-empty string\";\n if (!Number.isInteger(m.timestamp) || m.timestamp <= 0)\n return `timestamp must be a positive integer, got ${m.timestamp}`;\n if (m.value === null || m.value === undefined)\n return \"value is null/undefined\";\n if (typeof m.value === \"number\" && !isFinite(m.value))\n return `value is non-finite number (${m.value})`;\n if (typeof m.value !== \"number\" && typeof m.value !== \"string\")\n return `value type ${typeof m.value} not allowed (must be number or string)`;\n if (typeof m.value === \"string\" && m.value === \"\")\n return \"value is empty string\";\n return null;\n}\n/**\n * Filters an array of metrics, returning only valid ones.\n * Logs a warning for each dropped item via the provided warn function.\n */\nexport function filterValidMetrics(metrics, warn) {\n const valid = [];\n for (const m of metrics) {\n const reason = validateMetric(m);\n if (reason) {\n warn(`Dropping invalid metric [${String(m.metric)}=${JSON.stringify(m.value)}]: ${reason}`);\n }\n else {\n valid.push(m);\n }\n }\n return valid;\n}\n","/**\n * Cognito authentication helpers.\n *\n * Provides M2M (client_credentials) and user (USER_PASSWORD_AUTH) flows.\n * Refactored from @dataworks/sdk e2e-utils.ts — uses native fetch instead\n * of axios so the client sub-path has zero heavy dependencies.\n *\n * @module @dataworks/sdk/client\n */\n/**\n * Fetch an M2M access token using the client_credentials grant.\n *\n * @param clientId - Cognito app client ID\n * @param clientSecret - Cognito app client secret\n * @param tokenUrl - Cognito token endpoint (e.g. https://<domain>.auth.<region>.amazoncognito.com/oauth2/token)\n * @returns Access token string\n */\nexport async function getClientToken(clientId, clientSecret, tokenUrl) {\n const resp = await fetch(tokenUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({\n grant_type: \"client_credentials\",\n client_id: clientId,\n client_secret: clientSecret,\n }),\n });\n if (!resp.ok) {\n throw new Error(`getClientToken: Cognito token request failed: ${resp.status} ${resp.statusText}`);\n }\n const data = (await resp.json());\n if (!data.access_token) {\n throw new Error(\"getClientToken: response missing access_token\");\n }\n return data.access_token;\n}\n/**\n * Authenticate a user via Cognito USER_PASSWORD_AUTH flow.\n * Returns access token, ID token, refresh token, and tenant (extracted from JWT).\n *\n * Used by external developer clients — no client secret required when the\n * Cognito app client is configured without one.\n *\n * @param username - Cognito username\n * @param password - Cognito password\n * @param config - Cognito endpoint and client configuration\n * @returns Login result with tokens and tenant\n */\nexport async function loginWithCredentials(username, password, config) {\n const authParams = {\n USERNAME: username,\n PASSWORD: password,\n };\n // If a client secret is configured, compute SECRET_HASH\n if (config.clientSecret) {\n // Dynamic import to keep this module usable in environments without crypto\n const crypto = await import(\"crypto\");\n const hash = crypto\n .createHmac(\"sha256\", config.clientSecret)\n .update(username + config.clientId)\n .digest(\"base64\");\n authParams.SECRET_HASH = hash;\n }\n const resp = await fetch(config.cognitoEndpoint, {\n method: \"POST\",\n headers: {\n \"X-Amz-Target\": \"AWSCognitoIdentityProviderService.InitiateAuth\",\n \"Content-Type\": \"application/x-amz-json-1.1\",\n },\n body: JSON.stringify({\n AuthFlow: \"USER_PASSWORD_AUTH\",\n ClientId: config.clientId,\n AuthParameters: authParams,\n }),\n });\n if (!resp.ok) {\n const body = await resp.text().catch(() => \"\");\n throw new Error(`loginWithCredentials: Cognito auth failed: ${resp.status} — ${body}`);\n }\n const data = (await resp.json());\n const result = data.AuthenticationResult;\n if (!result?.AccessToken || !result?.IdToken) {\n throw new Error(\"loginWithCredentials: response missing AccessToken or IdToken\");\n }\n // Extract tenant from JWT claims (ID token payload)\n const tenant = extractTenantFromJwt(result.IdToken);\n return {\n accessToken: result.AccessToken,\n idToken: result.IdToken,\n refreshToken: result.RefreshToken ?? \"\",\n tenant,\n };\n}\n/**\n * Extracts the tenant claim from a JWT ID token without verifying the signature.\n * The token is validated server-side by AppSync/API Gateway — this is a client-side\n * convenience for reading the tenant value.\n */\nfunction extractTenantFromJwt(idToken) {\n try {\n const payload = idToken.split(\".\")[1];\n const decoded = JSON.parse(Buffer.from(payload, \"base64url\").toString(\"utf-8\"));\n // Cognito custom attributes use \"custom:tenant\" claim\n return (decoded[\"custom:tenant\"] ?? decoded.tenant ?? decoded[\"cognito:groups\"]?.[0] ?? \"\");\n }\n catch {\n return \"\";\n }\n}\n","/**\n * Validation re-exports — thin wrappers around @dataworks/sdk/client validation.\n * Declared locally so the published .d.ts is fully self-contained.\n *\n * @module @dataworks-technology/data\n */\n\nimport {\n validateMetric as _validateMetric,\n filterValidMetrics as _filterValidMetrics,\n} from \"@dataworks/sdk/client\";\nimport type { RawMetricItem, MetricItem } from \"./types.js\";\n\n/**\n * Check whether a metric is valid.\n * Returns null if valid, or a human-readable reason string if invalid.\n */\nexport const validateMetric: (m: RawMetricItem) => string | null =\n _validateMetric;\n\n/**\n * Filter an array of metrics, keeping only valid ones.\n * Logs a warning for each dropped item via the provided warn function.\n */\nexport const filterValidMetrics: (\n metrics: RawMetricItem[],\n warn: (msg: string) => void,\n) => MetricItem[] = _filterValidMetrics;\n","/**\n * DataClient — the public entry point for external developers using the Dataworks Data Engine.\n *\n * Handles authentication, metric ingestion, error reporting, and real-time subscriptions.\n * All SDK foundation code (@dataworks/sdk) is bundled at build time — consumers install\n * only @dataworks/data with zero transitive dependencies.\n *\n * @example\n * ```ts\n * import { DataClient } from \"@dataworks-technology/data\";\n *\n * const client = new DataClient({\n * cognitoEndpoint: \"https://cognito-idp.eu-west-1.amazonaws.com/\",\n * clientId: \"abc123\",\n * ingestUrl: \"https://dev-realtime.dataworks.live\",\n * errorUrl: \"https://dev-realtime-errors.dataworks.live\",\n * realtimeUrl: \"https://dev-event-api.dataworks.live\",\n * });\n *\n * await client.login(\"graeme\", \"password123\");\n * await client.ingest([{ athleteId: \"1\", metric: \"heartrate\", value: 172, timestamp: Date.now() / 1000 }], \"evt1\", \"ds1\");\n * ```\n *\n * @module @dataworks-technology/data\n */\n\nimport { loginWithCredentials } from \"@dataworks/sdk/client\";\nimport { validateMetric, filterValidMetrics } from \"./validation.js\";\nimport type {\n DataClientConfig,\n LoginResult,\n Metric,\n ErrorReport,\n SubscriptionHandler,\n Subscription,\n} from \"./types.js\";\n\nexport class DataClient {\n private readonly config: DataClientConfig;\n private credentials: LoginResult | null = null;\n\n constructor(config: DataClientConfig) {\n this.config = config;\n }\n\n /**\n * Authenticate with the Dataworks platform using Cognito USER_PASSWORD_AUTH.\n * Must be called before ingest(), reportError(), or subscribe().\n *\n * @param username - Cognito username\n * @param password - Cognito password\n * @returns Login result containing tokens and tenant\n */\n async login(username: string, password: string): Promise<LoginResult> {\n this.credentials = await loginWithCredentials(username, password, {\n cognitoEndpoint: this.config.cognitoEndpoint,\n clientId: this.config.clientId,\n });\n return this.credentials;\n }\n\n /**\n * Ingest metric data points into the Dataworks Data Engine.\n * Validates each metric before sending — invalid metrics are silently dropped.\n *\n * @param metrics - Array of metric data points\n * @param eventId - Event identifier\n * @param datasetDatasourceId - Dataset-datasource identifier\n * @throws Error if not authenticated or if the request fails\n */\n async ingest(\n metrics: Metric[],\n eventId: string,\n datasetDatasourceId: string,\n ): Promise<void> {\n this.requireAuth();\n\n const valid = filterValidMetrics(metrics, (msg) =>\n console.warn(`[@dataworks-technology/data] ${msg}`),\n );\n\n if (valid.length === 0) return;\n\n const payload = {\n eventId,\n datasetDatasourceId,\n metrics: valid,\n };\n\n const resp = await fetch(this.config.ingestUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.credentials!.accessToken}`,\n },\n body: JSON.stringify(payload),\n });\n\n if (!resp.ok) {\n const body = await resp.text().catch(() => \"\");\n throw new Error(\n `[@dataworks-technology/data] ingest failed: ${resp.status} — ${body}`,\n );\n }\n }\n\n /**\n * Report an error to the Dataworks Data Engine.\n *\n * @param error - Error report details\n * @throws Error if not authenticated or if the request fails\n */\n async reportError(error: ErrorReport): Promise<void> {\n this.requireAuth();\n\n const resp = await fetch(this.config.errorUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.credentials!.accessToken}`,\n },\n body: JSON.stringify(error),\n });\n\n if (!resp.ok) {\n const body = await resp.text().catch(() => \"\");\n throw new Error(\n `[@dataworks-technology/data] reportError failed: ${resp.status} — ${body}`,\n );\n }\n }\n\n /**\n * Subscribe to a real-time data channel on the AppSync Events API.\n * Uses the Cognito JWT for authentication.\n *\n * The channel name maps to the AppSync Events API namespace (e.g. \"dataworks/metrics\").\n *\n * @param channel - Channel path to subscribe to\n * @param onEvent - Callback invoked for each received event\n * @returns Subscription handle with a close() method\n * @throws Error if not authenticated\n */\n subscribe(channel: string, onEvent: SubscriptionHandler): Subscription {\n this.requireAuth();\n\n const url = new URL(this.config.realtimeUrl);\n // AppSync Events API uses the /event/realtime path for WebSocket connections\n url.pathname = \"/event/realtime\";\n url.protocol = url.protocol === \"https:\" ? \"wss:\" : \"ws:\";\n\n const ws = new WebSocket(url.toString(), [\n \"aws-appsync-event-ws\",\n // Encode auth as a base64 subprotocol header\n `header-${btoa(\n JSON.stringify({\n Authorization: this.credentials!.accessToken,\n host: new URL(this.config.realtimeUrl).host,\n }),\n )}`,\n ]);\n\n ws.addEventListener(\"open\", () => {\n // Subscribe to the channel after connection is established\n ws.send(\n JSON.stringify({\n type: \"subscribe\",\n id: crypto.randomUUID(),\n channel: `/${channel}`,\n authorization: {\n Authorization: this.credentials!.accessToken,\n host: new URL(this.config.realtimeUrl).host,\n },\n }),\n );\n });\n\n ws.addEventListener(\"message\", (event) => {\n try {\n const msg = JSON.parse(String(event.data));\n if (msg.type === \"data\") {\n onEvent(JSON.parse(msg.event));\n }\n } catch {\n // Ignore non-data messages (connection_ack, subscribe_success, etc.)\n }\n });\n\n return {\n close() {\n ws.close();\n },\n };\n }\n\n /**\n * Check whether a metric is valid before ingesting.\n * Returns null if valid, or a human-readable reason string if invalid.\n */\n static validateMetric = validateMetric;\n\n /**\n * Filter an array of metrics, keeping only valid ones.\n * Logs a warning for each dropped item.\n */\n static filterValidMetrics = filterValidMetrics;\n\n /** Returns true if the client has been authenticated via login(). */\n get isAuthenticated(): boolean {\n return this.credentials !== null;\n }\n\n /** Returns the tenant from the last successful login, or null. */\n get tenant(): string | null {\n return this.credentials?.tenant ?? null;\n }\n\n /** @internal Throws if not authenticated. */\n private requireAuth(): void {\n if (!this.credentials) {\n throw new Error(\n \"[@dataworks-technology/data] Not authenticated — call login() first\",\n );\n }\n }\n}\n"],"mappings":";AAWO,SAAS,eAAe,GAAG;AAC9B,MAAI,OAAO,EAAE,WAAW,YAAY,EAAE,OAAO,KAAK,MAAM;AACpD,WAAO;AACX,MAAI,OAAO,EAAE,cAAc,YAAY,EAAE,UAAU,KAAK,MAAM;AAC1D,WAAO;AACX,MAAI,CAAC,OAAO,UAAU,EAAE,SAAS,KAAK,EAAE,aAAa;AACjD,WAAO,6CAA6C,EAAE,SAAS;AACnE,MAAI,EAAE,UAAU,QAAQ,EAAE,UAAU;AAChC,WAAO;AACX,MAAI,OAAO,EAAE,UAAU,YAAY,CAAC,SAAS,EAAE,KAAK;AAChD,WAAO,+BAA+B,EAAE,KAAK;AACjD,MAAI,OAAO,EAAE,UAAU,YAAY,OAAO,EAAE,UAAU;AAClD,WAAO,cAAc,OAAO,EAAE,KAAK;AACvC,MAAI,OAAO,EAAE,UAAU,YAAY,EAAE,UAAU;AAC3C,WAAO;AACX,SAAO;AACX;AAKO,SAAS,mBAAmB,SAAS,MAAM;AAC9C,QAAM,QAAQ,CAAC;AACf,aAAW,KAAK,SAAS;AACrB,UAAM,SAAS,eAAe,CAAC;AAC/B,QAAI,QAAQ;AACR,WAAK,4BAA4B,OAAO,EAAE,MAAM,CAAC,IAAI,KAAK,UAAU,EAAE,KAAK,CAAC,MAAM,MAAM,EAAE;AAAA,IAC9F,OACK;AACD,YAAM,KAAK,CAAC;AAAA,IAChB;AAAA,EACJ;AACA,SAAO;AACX;;;ACIA,eAAsB,qBAAqB,UAAU,UAAU,QAAQ;AACnE,QAAM,aAAa;AAAA,IACf,UAAU;AAAA,IACV,UAAU;AAAA,EACd;AAEA,MAAI,OAAO,cAAc;AAErB,UAAMA,UAAS,MAAM,OAAO,QAAQ;AACpC,UAAM,OAAOA,QACR,WAAW,UAAU,OAAO,YAAY,EACxC,OAAO,WAAW,OAAO,QAAQ,EACjC,OAAO,QAAQ;AACpB,eAAW,cAAc;AAAA,EAC7B;AACA,QAAM,OAAO,MAAM,MAAM,OAAO,iBAAiB;AAAA,IAC7C,QAAQ;AAAA,IACR,SAAS;AAAA,MACL,gBAAgB;AAAA,MAChB,gBAAgB;AAAA,IACpB;AAAA,IACA,MAAM,KAAK,UAAU;AAAA,MACjB,UAAU;AAAA,MACV,UAAU,OAAO;AAAA,MACjB,gBAAgB;AAAA,IACpB,CAAC;AAAA,EACL,CAAC;AACD,MAAI,CAAC,KAAK,IAAI;AACV,UAAM,OAAO,MAAM,KAAK,KAAK,EAAE,MAAM,MAAM,EAAE;AAC7C,UAAM,IAAI,MAAM,8CAA8C,KAAK,MAAM,WAAM,IAAI,EAAE;AAAA,EACzF;AACA,QAAM,OAAQ,MAAM,KAAK,KAAK;AAC9B,QAAM,SAAS,KAAK;AACpB,MAAI,CAAC,QAAQ,eAAe,CAAC,QAAQ,SAAS;AAC1C,UAAM,IAAI,MAAM,+DAA+D;AAAA,EACnF;AAEA,QAAM,SAAS,qBAAqB,OAAO,OAAO;AAClD,SAAO;AAAA,IACH,aAAa,OAAO;AAAA,IACpB,SAAS,OAAO;AAAA,IAChB,cAAc,OAAO,gBAAgB;AAAA,IACrC;AAAA,EACJ;AACJ;AAMA,SAAS,qBAAqB,SAAS;AACnC,MAAI;AACA,UAAM,UAAU,QAAQ,MAAM,GAAG,EAAE,CAAC;AACpC,UAAM,UAAU,KAAK,MAAM,OAAO,KAAK,SAAS,WAAW,EAAE,SAAS,OAAO,CAAC;AAE9E,WAAQ,QAAQ,eAAe,KAAK,QAAQ,UAAU,QAAQ,gBAAgB,IAAI,CAAC,KAAK;AAAA,EAC5F,QACM;AACF,WAAO;AAAA,EACX;AACJ;;;AC3FO,IAAMC,kBACX;AAMK,IAAMC,sBAGO;;;ACUb,IAAM,aAAN,MAAiB;AAAA,EAItB,YAAY,QAA0B;AAFtC,SAAQ,cAAkC;AAGxC,SAAK,SAAS;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,MAAM,UAAkB,UAAwC;AACpE,SAAK,cAAc,MAAM,qBAAqB,UAAU,UAAU;AAAA,MAChE,iBAAiB,KAAK,OAAO;AAAA,MAC7B,UAAU,KAAK,OAAO;AAAA,IACxB,CAAC;AACD,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,OACJ,SACA,SACA,qBACe;AACf,SAAK,YAAY;AAEjB,UAAM,QAAQC;AAAA,MAAmB;AAAA,MAAS,CAAC,QACzC,QAAQ,KAAK,gCAAgC,GAAG,EAAE;AAAA,IACpD;AAEA,QAAI,MAAM,WAAW,EAAG;AAExB,UAAM,UAAU;AAAA,MACd;AAAA,MACA;AAAA,MACA,SAAS;AAAA,IACX;AAEA,UAAM,OAAO,MAAM,MAAM,KAAK,OAAO,WAAW;AAAA,MAC9C,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,eAAe,UAAU,KAAK,YAAa,WAAW;AAAA,MACxD;AAAA,MACA,MAAM,KAAK,UAAU,OAAO;AAAA,IAC9B,CAAC;AAED,QAAI,CAAC,KAAK,IAAI;AACZ,YAAM,OAAO,MAAM,KAAK,KAAK,EAAE,MAAM,MAAM,EAAE;AAC7C,YAAM,IAAI;AAAA,QACR,+CAA+C,KAAK,MAAM,WAAM,IAAI;AAAA,MACtE;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,YAAY,OAAmC;AACnD,SAAK,YAAY;AAEjB,UAAM,OAAO,MAAM,MAAM,KAAK,OAAO,UAAU;AAAA,MAC7C,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,eAAe,UAAU,KAAK,YAAa,WAAW;AAAA,MACxD;AAAA,MACA,MAAM,KAAK,UAAU,KAAK;AAAA,IAC5B,CAAC;AAED,QAAI,CAAC,KAAK,IAAI;AACZ,YAAM,OAAO,MAAM,KAAK,KAAK,EAAE,MAAM,MAAM,EAAE;AAC7C,YAAM,IAAI;AAAA,QACR,oDAAoD,KAAK,MAAM,WAAM,IAAI;AAAA,MAC3E;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,UAAU,SAAiB,SAA4C;AACrE,SAAK,YAAY;AAEjB,UAAM,MAAM,IAAI,IAAI,KAAK,OAAO,WAAW;AAE3C,QAAI,WAAW;AACf,QAAI,WAAW,IAAI,aAAa,WAAW,SAAS;AAEpD,UAAM,KAAK,IAAI,UAAU,IAAI,SAAS,GAAG;AAAA,MACvC;AAAA;AAAA,MAEA,UAAU;AAAA,QACR,KAAK,UAAU;AAAA,UACb,eAAe,KAAK,YAAa;AAAA,UACjC,MAAM,IAAI,IAAI,KAAK,OAAO,WAAW,EAAE;AAAA,QACzC,CAAC;AAAA,MACH,CAAC;AAAA,IACH,CAAC;AAED,OAAG,iBAAiB,QAAQ,MAAM;AAEhC,SAAG;AAAA,QACD,KAAK,UAAU;AAAA,UACb,MAAM;AAAA,UACN,IAAI,OAAO,WAAW;AAAA,UACtB,SAAS,IAAI,OAAO;AAAA,UACpB,eAAe;AAAA,YACb,eAAe,KAAK,YAAa;AAAA,YACjC,MAAM,IAAI,IAAI,KAAK,OAAO,WAAW,EAAE;AAAA,UACzC;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF,CAAC;AAED,OAAG,iBAAiB,WAAW,CAAC,UAAU;AACxC,UAAI;AACF,cAAM,MAAM,KAAK,MAAM,OAAO,MAAM,IAAI,CAAC;AACzC,YAAI,IAAI,SAAS,QAAQ;AACvB,kBAAQ,KAAK,MAAM,IAAI,KAAK,CAAC;AAAA,QAC/B;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF,CAAC;AAED,WAAO;AAAA,MACL,QAAQ;AACN,WAAG,MAAM;AAAA,MACX;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAeA,IAAI,kBAA2B;AAC7B,WAAO,KAAK,gBAAgB;AAAA,EAC9B;AAAA;AAAA,EAGA,IAAI,SAAwB;AAC1B,WAAO,KAAK,aAAa,UAAU;AAAA,EACrC;AAAA;AAAA,EAGQ,cAAoB;AAC1B,QAAI,CAAC,KAAK,aAAa;AACrB,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAAA;AAAA;AAAA;AAAA;AA5La,WAkKJ,iBAAiBC;AAAA;AAAA;AAAA;AAAA;AAlKb,WAwKJ,qBAAqBD;","names":["crypto","validateMetric","filterValidMetrics","filterValidMetrics","validateMetric"]}
1
+ {"version":3,"sources":["../../../node_modules/@dataworks/sdk/dist/client/validation.js","../../../node_modules/@dataworks/sdk/dist/client/auth.js","../src/validation.ts","../src/data-client.ts"],"sourcesContent":["/**\n * Metric validation for Dataworks payloads.\n *\n * Ported from Dataworks-Data packages/adaptors/shared-ts/src/validation.ts\n * to serve as the single source of truth for all Dataworks engine SDKs.\n *\n * @module @dataworks/sdk/client\n */\n/**\n * Returns null if the metric is valid, or a human-readable reason string if invalid.\n */\nexport function validateMetric(m) {\n if (typeof m.metric !== \"string\" || m.metric.trim() === \"\")\n return \"metric must be a non-empty string\";\n if (typeof m.athleteId !== \"string\" || m.athleteId.trim() === \"\")\n return \"athleteId must be a non-empty string\";\n if (!Number.isInteger(m.timestamp) || m.timestamp <= 0)\n return `timestamp must be a positive integer, got ${m.timestamp}`;\n if (m.value === null || m.value === undefined)\n return \"value is null/undefined\";\n if (typeof m.value === \"number\" && !isFinite(m.value))\n return `value is non-finite number (${m.value})`;\n if (typeof m.value !== \"number\" && typeof m.value !== \"string\")\n return `value type ${typeof m.value} not allowed (must be number or string)`;\n if (typeof m.value === \"string\" && m.value === \"\")\n return \"value is empty string\";\n return null;\n}\n/**\n * Filters an array of metrics, returning only valid ones.\n * Logs a warning for each dropped item via the provided warn function.\n */\nexport function filterValidMetrics(metrics, warn) {\n const valid = [];\n for (const m of metrics) {\n const reason = validateMetric(m);\n if (reason) {\n warn(`Dropping invalid metric [${String(m.metric)}=${JSON.stringify(m.value)}]: ${reason}`);\n }\n else {\n valid.push(m);\n }\n }\n return valid;\n}\n","/**\n * Cognito authentication helpers.\n *\n * Provides M2M (client_credentials) and user (USER_PASSWORD_AUTH) flows.\n * Refactored from @dataworks/sdk e2e-utils.ts — uses native fetch instead\n * of axios so the client sub-path has zero heavy dependencies.\n *\n * @module @dataworks/sdk/client\n */\n/**\n * Fetch an M2M access token using the client_credentials grant.\n *\n * @param clientId - Cognito app client ID\n * @param clientSecret - Cognito app client secret\n * @param tokenUrl - Cognito token endpoint (e.g. https://<domain>.auth.<region>.amazoncognito.com/oauth2/token)\n * @returns Access token string\n */\nexport async function getClientToken(clientId, clientSecret, tokenUrl) {\n const resp = await fetch(tokenUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({\n grant_type: \"client_credentials\",\n client_id: clientId,\n client_secret: clientSecret,\n }),\n });\n if (!resp.ok) {\n throw new Error(`getClientToken: Cognito token request failed: ${resp.status} ${resp.statusText}`);\n }\n const data = (await resp.json());\n if (!data.access_token) {\n throw new Error(\"getClientToken: response missing access_token\");\n }\n return data.access_token;\n}\n/**\n * Authenticate a user via Cognito USER_PASSWORD_AUTH flow.\n * Returns access token, ID token, refresh token, and tenant (extracted from JWT).\n *\n * Used by external developer clients — no client secret required when the\n * Cognito app client is configured without one.\n *\n * @param username - Cognito username\n * @param password - Cognito password\n * @param config - Cognito endpoint and client configuration\n * @returns Login result with tokens and tenant\n */\nexport async function loginWithCredentials(username, password, config) {\n const authParams = {\n USERNAME: username,\n PASSWORD: password,\n };\n // If a client secret is configured, compute SECRET_HASH\n if (config.clientSecret) {\n // Dynamic import to keep this module usable in environments without crypto\n const crypto = await import(\"crypto\");\n const hash = crypto\n .createHmac(\"sha256\", config.clientSecret)\n .update(username + config.clientId)\n .digest(\"base64\");\n authParams.SECRET_HASH = hash;\n }\n const resp = await fetch(config.cognitoEndpoint, {\n method: \"POST\",\n headers: {\n \"X-Amz-Target\": \"AWSCognitoIdentityProviderService.InitiateAuth\",\n \"Content-Type\": \"application/x-amz-json-1.1\",\n },\n body: JSON.stringify({\n AuthFlow: \"USER_PASSWORD_AUTH\",\n ClientId: config.clientId,\n AuthParameters: authParams,\n }),\n });\n if (!resp.ok) {\n const body = await resp.text().catch(() => \"\");\n throw new Error(`loginWithCredentials: Cognito auth failed: ${resp.status} — ${body}`);\n }\n const data = (await resp.json());\n const result = data.AuthenticationResult;\n if (!result?.AccessToken || !result?.IdToken) {\n throw new Error(\"loginWithCredentials: response missing AccessToken or IdToken\");\n }\n // Extract tenant from JWT claims (ID token payload)\n const tenant = extractTenantFromJwt(result.IdToken);\n return {\n accessToken: result.AccessToken,\n idToken: result.IdToken,\n refreshToken: result.RefreshToken ?? \"\",\n tenant,\n };\n}\n/**\n * Extracts the tenant claim from a JWT ID token without verifying the signature.\n * The token is validated server-side by AppSync/API Gateway — this is a client-side\n * convenience for reading the tenant value.\n */\nfunction extractTenantFromJwt(idToken) {\n try {\n const payload = idToken.split(\".\")[1];\n const decoded = JSON.parse(Buffer.from(payload, \"base64url\").toString(\"utf-8\"));\n // Cognito custom attributes use \"custom:tenant\" claim\n return (decoded[\"custom:tenant\"] ?? decoded.tenant ?? decoded[\"cognito:groups\"]?.[0] ?? \"\");\n }\n catch {\n return \"\";\n }\n}\n","/**\n * Validation re-exports — thin wrappers around @dataworks/sdk/client validation.\n * Declared locally so the published .d.ts is fully self-contained.\n *\n * @module @dataworks-technology/data\n */\n\nimport {\n validateMetric as _validateMetric,\n filterValidMetrics as _filterValidMetrics,\n} from \"@dataworks/sdk/client\";\nimport type { RawMetricItem, MetricItem } from \"./types.js\";\n\n/**\n * Check whether a metric is valid.\n * Returns null if valid, or a human-readable reason string if invalid.\n */\nexport const validateMetric: (m: RawMetricItem) => string | null =\n _validateMetric;\n\n/**\n * Filter an array of metrics, keeping only valid ones.\n * Logs a warning for each dropped item via the provided warn function.\n */\nexport const filterValidMetrics: (\n metrics: RawMetricItem[],\n warn: (msg: string) => void,\n) => MetricItem[] = _filterValidMetrics;\n","/**\n * DataClient — the public entry point for external developers using the Dataworks Data Engine.\n *\n * Handles authentication, metric ingestion, error reporting, and real-time subscriptions.\n * All SDK foundation code (@dataworks/sdk) is bundled at build time — consumers install\n * only @dataworks/data with zero transitive dependencies.\n *\n * @example\n * ```ts\n * import { DataClient } from \"@dataworks-technology/data\";\n *\n * const client = new DataClient({\n * cognitoEndpoint: \"https://cognito-idp.eu-west-1.amazonaws.com/\",\n * clientId: \"abc123\",\n * ingestUrl: \"https://dev-realtime.dataworks.live\",\n * errorUrl: \"https://dev-realtime-errors.dataworks.live\",\n * realtimeUrl: \"https://dev-event-api.dataworks.live\",\n * });\n *\n * await client.login(\"graeme\", \"password123\");\n * await client.ingest([{ athleteId: \"1\", metric: \"heartrate\", value: 172, timestamp: Date.now() / 1000 }], \"evt1\", \"ds1\");\n * ```\n *\n * @module @dataworks-technology/data\n */\n\nimport { loginWithCredentials } from \"@dataworks/sdk/client\";\nimport { validateMetric, filterValidMetrics } from \"./validation.js\";\nimport type {\n DataClientConfig,\n LoginResult,\n Metric,\n ErrorReport,\n SubscriptionHandler,\n Subscription,\n} from \"./types.js\";\n\nexport class DataClient {\n private readonly config: DataClientConfig;\n private credentials: LoginResult | null = null;\n\n constructor(config: DataClientConfig) {\n this.config = config;\n }\n\n /**\n * Authenticate with the Dataworks platform using Cognito USER_PASSWORD_AUTH.\n * Must be called before ingest(), reportError(), or subscribe().\n *\n * @param username - Cognito username\n * @param password - Cognito password\n * @returns Login result containing tokens and tenant\n */\n async login(username: string, password: string): Promise<LoginResult> {\n this.credentials = await loginWithCredentials(username, password, {\n cognitoEndpoint: this.config.cognitoEndpoint,\n clientId: this.config.clientId,\n });\n return this.credentials;\n }\n\n /**\n * Ingest metric data points into the Dataworks Data Engine.\n * Validates each metric before sending — invalid metrics are silently dropped.\n *\n * @param metrics - Array of metric data points\n * @param eventId - Event identifier\n * @param datasetDatasourceId - Dataset-datasource identifier\n * @throws Error if not authenticated or if the request fails\n */\n async ingest(\n metrics: Metric[],\n eventId: string,\n datasetDatasourceId: string,\n ): Promise<void> {\n this.requireAuth();\n\n const valid = filterValidMetrics(metrics, (msg) =>\n console.warn(`[@dataworks-technology/data] ${msg}`),\n );\n\n if (valid.length === 0) return;\n\n const payload = {\n eventId,\n datasetDatasourceId,\n metrics: valid,\n };\n\n const resp = await fetch(this.config.ingestUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.credentials!.accessToken}`,\n },\n body: JSON.stringify(payload),\n });\n\n if (!resp.ok) {\n const body = await resp.text().catch(() => \"\");\n throw new Error(\n `[@dataworks-technology/data] ingest failed: ${resp.status} — ${body}`,\n );\n }\n }\n\n /**\n * Report an error to the Dataworks Data Engine.\n *\n * @param error - Error report details\n * @throws Error if not authenticated or if the request fails\n */\n async reportError(error: ErrorReport): Promise<void> {\n this.requireAuth();\n\n const resp = await fetch(this.config.errorUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.credentials!.accessToken}`,\n },\n body: JSON.stringify({\n client_id: String(error.clientId),\n dataset_datasource_id: error.datasetDatasourceId,\n athlete_id: error.athleteId,\n error_title: error.errorTitle,\n error_description: error.errorDescription,\n }),\n });\n\n if (!resp.ok) {\n const body = await resp.text().catch(() => \"\");\n throw new Error(\n `[@dataworks-technology/data] reportError failed: ${resp.status} — ${body}`,\n );\n }\n }\n\n /**\n * Subscribe to a real-time data channel on the AppSync Events API.\n * Uses the Cognito JWT for authentication.\n *\n * The channel name maps to the AppSync Events API namespace (e.g. \"dataworks/metrics\").\n *\n * @param channel - Channel path to subscribe to\n * @param onEvent - Callback invoked for each received event\n * @returns Subscription handle with a close() method\n * @throws Error if not authenticated\n */\n subscribe(channel: string, onEvent: SubscriptionHandler): Subscription {\n this.requireAuth();\n\n const url = new URL(this.config.realtimeUrl);\n // AppSync Events API uses the /event/realtime path for WebSocket connections\n url.pathname = \"/event/realtime\";\n url.protocol = url.protocol === \"https:\" ? \"wss:\" : \"ws:\";\n\n // AWS AppSync Events API requires base64URL-encoded auth in the subprotocol.\n // Standard base64 (btoa) contains +, /, = which violate RFC 6455 token rules\n // and are rejected by non-browser WebSocket implementations (Bun, Node).\n const authPayload = JSON.stringify({\n Authorization: this.credentials!.accessToken,\n host: new URL(this.config.realtimeUrl).host,\n });\n const authHeader = btoa(authPayload)\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/, \"\");\n\n const ws = new WebSocket(url.toString(), [\n \"aws-appsync-event-ws\",\n `header-${authHeader}`,\n ]);\n\n ws.addEventListener(\"open\", () => {\n // Send connection_init — required for AppSync to send connection_ack\n ws.send(JSON.stringify({ type: \"connection_init\" }));\n });\n\n ws.addEventListener(\"message\", (event) => {\n try {\n const msg = JSON.parse(String(event.data));\n\n if (msg.type === \"connection_ack\") {\n // Connection established — now subscribe to the channel\n ws.send(\n JSON.stringify({\n type: \"subscribe\",\n id: crypto.randomUUID(),\n channel: `/${channel}`,\n authorization: {\n Authorization: this.credentials!.accessToken,\n host: new URL(this.config.realtimeUrl).host,\n },\n }),\n );\n } else if (msg.type === \"data\") {\n onEvent(JSON.parse(msg.event));\n }\n } catch {\n // Ignore malformed or non-data messages (ka, subscribe_success, etc.)\n }\n });\n\n return {\n close() {\n ws.close();\n },\n };\n }\n\n /**\n * Check whether a metric is valid before ingesting.\n * Returns null if valid, or a human-readable reason string if invalid.\n */\n static validateMetric = validateMetric;\n\n /**\n * Filter an array of metrics, keeping only valid ones.\n * Logs a warning for each dropped item.\n */\n static filterValidMetrics = filterValidMetrics;\n\n /** Returns true if the client has been authenticated via login(). */\n get isAuthenticated(): boolean {\n return this.credentials !== null;\n }\n\n /** Returns the tenant from the last successful login, or null. */\n get tenant(): string | null {\n return this.credentials?.tenant ?? null;\n }\n\n /** @internal Throws if not authenticated. */\n private requireAuth(): void {\n if (!this.credentials) {\n throw new Error(\n \"[@dataworks-technology/data] Not authenticated — call login() first\",\n );\n }\n }\n}\n"],"mappings":"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","names":["crypto","validateMetric","filterValidMetrics","filterValidMetrics","validateMetric"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dataworks-technology/data",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Dataworks Data Engine SDK — authenticate, ingest metrics, and subscribe to live data",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",