@dataworks-technology/data 0.1.3

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 ADDED
@@ -0,0 +1,289 @@
1
+ # @dataworks-technology/data
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@dataworks-technology/data)](https://www.npmjs.com/package/@dataworks-technology/data)
4
+ [![license](https://img.shields.io/npm/l/@dataworks-technology/data)](./LICENSE)
5
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/@dataworks-technology/data)](https://bundlephobia.com/package/@dataworks-technology/data)
6
+
7
+ Official SDK for the Dataworks Data Engine — authenticate, ingest live athlete metrics, subscribe to real-time data streams, and report errors.
8
+
9
+ ## Features
10
+
11
+ - **Zero dependencies** — fully self-contained, no transitive installs
12
+ - **Dual format** — ESM and CommonJS bundles included
13
+ - **TypeScript-first** — complete type declarations shipped with the package
14
+ - **Built-in validation** — metrics are validated before ingestion
15
+ - **Real-time subscriptions** — WebSocket-based live data streaming
16
+ - **Cognito authentication** — secure login with automatic token management
17
+
18
+ ## Prerequisites
19
+
20
+ You need a Dataworks developer account. Contact your Dataworks administrator to receive:
21
+
22
+ | Credential | Description |
23
+ |---|---|
24
+ | `cognitoEndpoint` | Cognito User Pool endpoint URL |
25
+ | `clientId` | Cognito app client ID |
26
+ | `ingestUrl` | API Gateway endpoint for metric ingestion |
27
+ | `errorUrl` | API Gateway endpoint for error reporting |
28
+ | `realtimeUrl` | AppSync Events API endpoint for subscriptions |
29
+ | Username + password | Your developer login credentials |
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ npm install @dataworks-technology/data
35
+ ```
36
+
37
+ ```bash
38
+ yarn add @dataworks-technology/data
39
+ ```
40
+
41
+ ```bash
42
+ pnpm add @dataworks-technology/data
43
+ ```
44
+
45
+ ```bash
46
+ bun add @dataworks-technology/data
47
+ ```
48
+
49
+ ## Quick Start
50
+
51
+ ```typescript
52
+ import { DataClient } from "@dataworks-technology/data";
53
+
54
+ const client = new DataClient({
55
+ cognitoEndpoint: "https://cognito-idp.eu-west-1.amazonaws.com/",
56
+ clientId: "your-client-id",
57
+ ingestUrl: "https://your-ingest-endpoint.dataworks.live",
58
+ errorUrl: "https://your-error-endpoint.dataworks.live",
59
+ realtimeUrl: "https://your-realtime-endpoint.dataworks.live",
60
+ });
61
+
62
+ // Authenticate
63
+ await client.login("username", "password");
64
+
65
+ // Ingest metrics
66
+ await client.ingest(
67
+ [
68
+ {
69
+ athleteId: "athlete-1",
70
+ metric: "heartrate",
71
+ value: 172,
72
+ timestamp: Math.floor(Date.now() / 1000),
73
+ },
74
+ ],
75
+ "event-id",
76
+ "dataset-datasource-id",
77
+ );
78
+ ```
79
+
80
+ ## API
81
+
82
+ ### `new DataClient(config)`
83
+
84
+ Create a new client instance.
85
+
86
+ ```typescript
87
+ const client = new DataClient({
88
+ cognitoEndpoint: "https://cognito-idp.eu-west-1.amazonaws.com/",
89
+ clientId: "abc123",
90
+ ingestUrl: "https://ingest.dataworks.live",
91
+ errorUrl: "https://errors.dataworks.live",
92
+ realtimeUrl: "https://realtime.dataworks.live",
93
+ });
94
+ ```
95
+
96
+ ### `client.login(username, password)`
97
+
98
+ Authenticate with Cognito. Must be called before any other operation.
99
+
100
+ ```typescript
101
+ const result = await client.login("username", "password");
102
+ // result: { accessToken, idToken, refreshToken, tenant }
103
+ ```
104
+
105
+ ### `client.ingest(metrics, eventId, datasetDatasourceId)`
106
+
107
+ Send metric data points to the Data Engine. Invalid metrics are automatically filtered out.
108
+
109
+ ```typescript
110
+ await client.ingest(
111
+ [
112
+ { athleteId: "1", metric: "heartrate", value: 172, timestamp: 1700000000 },
113
+ { athleteId: "1", metric: "speed", value: 4.2, timestamp: 1700000000 },
114
+ ],
115
+ "event-123",
116
+ "ds-456",
117
+ );
118
+ ```
119
+
120
+ ### `client.subscribe(channel, onEvent)`
121
+
122
+ Subscribe to real-time data events via WebSocket.
123
+
124
+ ```typescript
125
+ const subscription = client.subscribe(
126
+ "/default/events/event-123",
127
+ (event) => {
128
+ console.log("Received:", event);
129
+ },
130
+ );
131
+
132
+ // Later: close the subscription
133
+ subscription.close();
134
+ ```
135
+
136
+ ### `client.reportError(error)`
137
+
138
+ Report an error to the Data Engine for monitoring and alerting.
139
+
140
+ ```typescript
141
+ await client.reportError({
142
+ datasetDatasourceId: "ds-456",
143
+ eventId: "event-123",
144
+ clientId: 1,
145
+ errorTitle: "Sensor disconnected",
146
+ errorDescription: "BLE heart rate sensor lost connection at 00:42:15",
147
+ });
148
+ ```
149
+
150
+ ### `client.isAuthenticated`
151
+
152
+ Check if the client has valid credentials.
153
+
154
+ ```typescript
155
+ if (client.isAuthenticated) {
156
+ await client.ingest(metrics, eventId, dsId);
157
+ }
158
+ ```
159
+
160
+ ### `client.tenant`
161
+
162
+ Get the tenant from the authenticated session (or `null`).
163
+
164
+ ```typescript
165
+ console.log(`Logged in as tenant: ${client.tenant}`);
166
+ ```
167
+
168
+ ## Validation Utilities
169
+
170
+ Standalone validation functions are exported for pre-checking metrics before ingestion:
171
+
172
+ ### `DataClient.validateMetric(metric)`
173
+
174
+ Returns `null` if valid, or a string describing the validation failure.
175
+
176
+ ```typescript
177
+ import { validateMetric } from "@dataworks-technology/data";
178
+
179
+ const error = validateMetric({
180
+ metric: "heartrate",
181
+ athleteId: "1",
182
+ value: 172,
183
+ timestamp: 1700000000,
184
+ });
185
+
186
+ if (error) {
187
+ console.warn("Invalid metric:", error);
188
+ }
189
+ ```
190
+
191
+ ### `DataClient.filterValidMetrics(metrics, warnFn)`
192
+
193
+ Filters an array, keeping only valid metrics. Calls `warnFn` for each invalid item.
194
+
195
+ ```typescript
196
+ import { filterValidMetrics } from "@dataworks-technology/data";
197
+
198
+ const valid = filterValidMetrics(rawMetrics, (msg) => console.warn(msg));
199
+ ```
200
+
201
+ ### Validation Rules
202
+
203
+ | Field | Rule |
204
+ |---|---|
205
+ | `metric` | Non-empty string |
206
+ | `athleteId` | Non-empty string |
207
+ | `value` | Number (finite, not NaN) or non-empty string |
208
+ | `timestamp` | Positive integer (Unix seconds) |
209
+
210
+ ## Types
211
+
212
+ All types are exported for use in your application:
213
+
214
+ ```typescript
215
+ import type {
216
+ DataClientConfig,
217
+ LoginResult,
218
+ Metric,
219
+ MetricItem,
220
+ RawMetricItem,
221
+ MetricsPayload,
222
+ ErrorReport,
223
+ SubscriptionHandler,
224
+ Subscription,
225
+ } from "@dataworks-technology/data";
226
+ ```
227
+
228
+ ### Key Interfaces
229
+
230
+ ```typescript
231
+ interface Metric {
232
+ athleteId: string;
233
+ metric: string;
234
+ value: number | string;
235
+ timestamp: number; // Unix seconds
236
+ }
237
+
238
+ interface ErrorReport {
239
+ datasetDatasourceId: string;
240
+ eventId: string;
241
+ clientId: number;
242
+ errorTitle: string;
243
+ errorDescription: string;
244
+ }
245
+
246
+ interface LoginResult {
247
+ accessToken: string;
248
+ idToken: string;
249
+ refreshToken: string;
250
+ tenant: string;
251
+ }
252
+ ```
253
+
254
+ ## Error Handling
255
+
256
+ All async methods throw on failure. Wrap calls in try/catch:
257
+
258
+ ```typescript
259
+ try {
260
+ await client.login("user", "pass");
261
+ } catch (err) {
262
+ // Authentication failed (invalid credentials, network error, etc.)
263
+ }
264
+
265
+ try {
266
+ await client.ingest(metrics, eventId, dsId);
267
+ } catch (err) {
268
+ // Ingestion failed (401 expired token, 5xx server error, etc.)
269
+ }
270
+ ```
271
+
272
+ Calling `ingest()`, `reportError()`, or `subscribe()` before `login()` throws immediately.
273
+
274
+ ## Requirements
275
+
276
+ - **Node.js** ≥ 18 (uses native `fetch`)
277
+ - **ESM or CommonJS** — both module formats are included
278
+ - **TypeScript** ≥ 5.0 (optional — works with plain JavaScript too)
279
+ - **Browser** — compatible with any environment that has `fetch` and `WebSocket`
280
+
281
+ ## Documentation
282
+
283
+ Full documentation with guides, examples, and detailed API reference:
284
+
285
+ **[https://data-docs.dataworks.live](https://data-docs.dataworks.live)**
286
+
287
+ ## License
288
+
289
+ MIT © [Dataworks Technology](https://github.com/Dataworks-Technology)
package/dist/index.cjs ADDED
@@ -0,0 +1,291 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ DataClient: () => DataClient,
34
+ filterValidMetrics: () => filterValidMetrics2,
35
+ validateMetric: () => validateMetric2
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+
39
+ // ../../node_modules/@dataworks/sdk/dist/client/validation.js
40
+ function validateMetric(m) {
41
+ if (typeof m.metric !== "string" || m.metric.trim() === "")
42
+ return "metric must be a non-empty string";
43
+ if (typeof m.athleteId !== "string" || m.athleteId.trim() === "")
44
+ return "athleteId must be a non-empty string";
45
+ if (!Number.isInteger(m.timestamp) || m.timestamp <= 0)
46
+ return `timestamp must be a positive integer, got ${m.timestamp}`;
47
+ if (m.value === null || m.value === void 0)
48
+ return "value is null/undefined";
49
+ if (typeof m.value === "number" && !isFinite(m.value))
50
+ return `value is non-finite number (${m.value})`;
51
+ if (typeof m.value !== "number" && typeof m.value !== "string")
52
+ return `value type ${typeof m.value} not allowed (must be number or string)`;
53
+ if (typeof m.value === "string" && m.value === "")
54
+ return "value is empty string";
55
+ return null;
56
+ }
57
+ function filterValidMetrics(metrics, warn) {
58
+ const valid = [];
59
+ for (const m of metrics) {
60
+ const reason = validateMetric(m);
61
+ if (reason) {
62
+ warn(`Dropping invalid metric [${String(m.metric)}=${JSON.stringify(m.value)}]: ${reason}`);
63
+ } else {
64
+ valid.push(m);
65
+ }
66
+ }
67
+ return valid;
68
+ }
69
+
70
+ // ../../node_modules/@dataworks/sdk/dist/client/auth.js
71
+ async function loginWithCredentials(username, password, config) {
72
+ const authParams = {
73
+ USERNAME: username,
74
+ PASSWORD: password
75
+ };
76
+ if (config.clientSecret) {
77
+ const crypto2 = await import("crypto");
78
+ const hash = crypto2.createHmac("sha256", config.clientSecret).update(username + config.clientId).digest("base64");
79
+ authParams.SECRET_HASH = hash;
80
+ }
81
+ const resp = await fetch(config.cognitoEndpoint, {
82
+ method: "POST",
83
+ headers: {
84
+ "X-Amz-Target": "AWSCognitoIdentityProviderService.InitiateAuth",
85
+ "Content-Type": "application/x-amz-json-1.1"
86
+ },
87
+ body: JSON.stringify({
88
+ AuthFlow: "USER_PASSWORD_AUTH",
89
+ ClientId: config.clientId,
90
+ AuthParameters: authParams
91
+ })
92
+ });
93
+ if (!resp.ok) {
94
+ const body = await resp.text().catch(() => "");
95
+ throw new Error(`loginWithCredentials: Cognito auth failed: ${resp.status} \u2014 ${body}`);
96
+ }
97
+ const data = await resp.json();
98
+ const result = data.AuthenticationResult;
99
+ if (!result?.AccessToken || !result?.IdToken) {
100
+ throw new Error("loginWithCredentials: response missing AccessToken or IdToken");
101
+ }
102
+ const tenant = extractTenantFromJwt(result.IdToken);
103
+ return {
104
+ accessToken: result.AccessToken,
105
+ idToken: result.IdToken,
106
+ refreshToken: result.RefreshToken ?? "",
107
+ tenant
108
+ };
109
+ }
110
+ function extractTenantFromJwt(idToken) {
111
+ try {
112
+ const payload = idToken.split(".")[1];
113
+ const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf-8"));
114
+ return decoded["custom:tenant"] ?? decoded.tenant ?? decoded["cognito:groups"]?.[0] ?? "";
115
+ } catch {
116
+ return "";
117
+ }
118
+ }
119
+
120
+ // src/validation.ts
121
+ var validateMetric2 = validateMetric;
122
+ var filterValidMetrics2 = filterValidMetrics;
123
+
124
+ // src/data-client.ts
125
+ var DataClient = class {
126
+ constructor(config) {
127
+ this.credentials = null;
128
+ this.config = config;
129
+ }
130
+ /**
131
+ * Authenticate with the Dataworks platform using Cognito USER_PASSWORD_AUTH.
132
+ * Must be called before ingest(), reportError(), or subscribe().
133
+ *
134
+ * @param username - Cognito username
135
+ * @param password - Cognito password
136
+ * @returns Login result containing tokens and tenant
137
+ */
138
+ async login(username, password) {
139
+ this.credentials = await loginWithCredentials(username, password, {
140
+ cognitoEndpoint: this.config.cognitoEndpoint,
141
+ clientId: this.config.clientId
142
+ });
143
+ return this.credentials;
144
+ }
145
+ /**
146
+ * Ingest metric data points into the Dataworks Data Engine.
147
+ * Validates each metric before sending — invalid metrics are silently dropped.
148
+ *
149
+ * @param metrics - Array of metric data points
150
+ * @param eventId - Event identifier
151
+ * @param datasetDatasourceId - Dataset-datasource identifier
152
+ * @throws Error if not authenticated or if the request fails
153
+ */
154
+ async ingest(metrics, eventId, datasetDatasourceId) {
155
+ this.requireAuth();
156
+ const valid = filterValidMetrics2(
157
+ metrics,
158
+ (msg) => console.warn(`[@dataworks-technology/data] ${msg}`)
159
+ );
160
+ if (valid.length === 0) return;
161
+ const payload = {
162
+ eventId,
163
+ datasetDatasourceId,
164
+ metrics: valid
165
+ };
166
+ const resp = await fetch(this.config.ingestUrl, {
167
+ method: "POST",
168
+ headers: {
169
+ "Content-Type": "application/json",
170
+ Authorization: `Bearer ${this.credentials.accessToken}`
171
+ },
172
+ body: JSON.stringify(payload)
173
+ });
174
+ if (!resp.ok) {
175
+ const body = await resp.text().catch(() => "");
176
+ throw new Error(
177
+ `[@dataworks-technology/data] ingest failed: ${resp.status} \u2014 ${body}`
178
+ );
179
+ }
180
+ }
181
+ /**
182
+ * Report an error to the Dataworks Data Engine.
183
+ *
184
+ * @param error - Error report details
185
+ * @throws Error if not authenticated or if the request fails
186
+ */
187
+ async reportError(error) {
188
+ this.requireAuth();
189
+ const resp = await fetch(this.config.errorUrl, {
190
+ method: "POST",
191
+ headers: {
192
+ "Content-Type": "application/json",
193
+ Authorization: `Bearer ${this.credentials.accessToken}`
194
+ },
195
+ body: JSON.stringify(error)
196
+ });
197
+ if (!resp.ok) {
198
+ const body = await resp.text().catch(() => "");
199
+ throw new Error(
200
+ `[@dataworks-technology/data] reportError failed: ${resp.status} \u2014 ${body}`
201
+ );
202
+ }
203
+ }
204
+ /**
205
+ * Subscribe to a real-time data channel on the AppSync Events API.
206
+ * Uses the Cognito JWT for authentication.
207
+ *
208
+ * The channel name maps to the AppSync Events API namespace (e.g. "dataworks/metrics").
209
+ *
210
+ * @param channel - Channel path to subscribe to
211
+ * @param onEvent - Callback invoked for each received event
212
+ * @returns Subscription handle with a close() method
213
+ * @throws Error if not authenticated
214
+ */
215
+ subscribe(channel, onEvent) {
216
+ this.requireAuth();
217
+ const url = new URL(this.config.realtimeUrl);
218
+ url.pathname = "/event/realtime";
219
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
220
+ const ws = new WebSocket(url.toString(), [
221
+ "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
+ )}`
229
+ ]);
230
+ 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
+ );
242
+ });
243
+ ws.addEventListener("message", (event) => {
244
+ try {
245
+ const msg = JSON.parse(String(event.data));
246
+ if (msg.type === "data") {
247
+ onEvent(JSON.parse(msg.event));
248
+ }
249
+ } catch {
250
+ }
251
+ });
252
+ return {
253
+ close() {
254
+ ws.close();
255
+ }
256
+ };
257
+ }
258
+ /** Returns true if the client has been authenticated via login(). */
259
+ get isAuthenticated() {
260
+ return this.credentials !== null;
261
+ }
262
+ /** Returns the tenant from the last successful login, or null. */
263
+ get tenant() {
264
+ return this.credentials?.tenant ?? null;
265
+ }
266
+ /** @internal Throws if not authenticated. */
267
+ requireAuth() {
268
+ if (!this.credentials) {
269
+ throw new Error(
270
+ "[@dataworks-technology/data] Not authenticated \u2014 call login() first"
271
+ );
272
+ }
273
+ }
274
+ };
275
+ /**
276
+ * Check whether a metric is valid before ingesting.
277
+ * Returns null if valid, or a human-readable reason string if invalid.
278
+ */
279
+ DataClient.validateMetric = validateMetric2;
280
+ /**
281
+ * Filter an array of metrics, keeping only valid ones.
282
+ * Logs a warning for each dropped item.
283
+ */
284
+ DataClient.filterValidMetrics = filterValidMetrics2;
285
+ // Annotate the CommonJS export names for ESM import in node:
286
+ 0 && (module.exports = {
287
+ DataClient,
288
+ filterValidMetrics,
289
+ validateMetric
290
+ });
291
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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"]}
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Public types for @dataworks/data.
3
+ *
4
+ * @module @dataworks-technology/data
5
+ */
6
+ /** Configuration for connecting to the Dataworks Data Engine. */
7
+ interface DataClientConfig {
8
+ /** Cognito User Pool endpoint (e.g. "https://cognito-idp.eu-west-1.amazonaws.com/") */
9
+ cognitoEndpoint: string;
10
+ /** Cognito app client ID */
11
+ clientId: string;
12
+ /** Data Engine ingest URL (API Gateway endpoint) */
13
+ ingestUrl: string;
14
+ /** Data Engine error reporting URL (API Gateway endpoint) */
15
+ errorUrl: string;
16
+ /** AppSync Events API endpoint for real-time subscriptions */
17
+ realtimeUrl: string;
18
+ }
19
+ /** Credentials returned from a successful login. */
20
+ interface LoginResult {
21
+ accessToken: string;
22
+ idToken: string;
23
+ refreshToken: string;
24
+ /** Tenant extracted from the JWT claims. */
25
+ tenant: string;
26
+ }
27
+ /** A single metric data point to ingest. */
28
+ interface Metric {
29
+ /** Unique athlete identifier */
30
+ athleteId: string;
31
+ /** Metric name (e.g. "heartrate", "speed", "power") */
32
+ metric: string;
33
+ /** Metric value — number or string */
34
+ value: number | string;
35
+ /** Unix timestamp in seconds */
36
+ timestamp: number;
37
+ }
38
+ /** Validated metric item (all fields confirmed present and correct). */
39
+ interface MetricItem {
40
+ athleteId: string;
41
+ metric: string;
42
+ value: unknown;
43
+ timestamp: number;
44
+ }
45
+ /** Loosely-typed input for validation — all fields are unknown. */
46
+ interface RawMetricItem {
47
+ metric: unknown;
48
+ athleteId: unknown;
49
+ value: unknown;
50
+ timestamp: unknown;
51
+ }
52
+ /** Payload shape for metric ingestion. */
53
+ interface MetricsPayload {
54
+ eventId: string;
55
+ datasetDatasourceId: string;
56
+ metrics: MetricItem[];
57
+ }
58
+ /** Error report to send to the Data Engine. */
59
+ interface ErrorReport {
60
+ /** Dataset-datasource identifier */
61
+ datasetDatasourceId: string;
62
+ /** Event identifier */
63
+ eventId: string;
64
+ /** Client identifier */
65
+ clientId: number;
66
+ /** Short error title */
67
+ errorTitle: string;
68
+ /** Detailed error description */
69
+ errorDescription: string;
70
+ }
71
+ /** Callback for subscription events. */
72
+ type SubscriptionHandler = (event: unknown) => void;
73
+ /** Active subscription that can be closed. */
74
+ interface Subscription {
75
+ /** Close the subscription and disconnect. */
76
+ close(): void;
77
+ }
78
+
79
+ declare class DataClient {
80
+ private readonly config;
81
+ private credentials;
82
+ constructor(config: DataClientConfig);
83
+ /**
84
+ * Authenticate with the Dataworks platform using Cognito USER_PASSWORD_AUTH.
85
+ * Must be called before ingest(), reportError(), or subscribe().
86
+ *
87
+ * @param username - Cognito username
88
+ * @param password - Cognito password
89
+ * @returns Login result containing tokens and tenant
90
+ */
91
+ login(username: string, password: string): Promise<LoginResult>;
92
+ /**
93
+ * Ingest metric data points into the Dataworks Data Engine.
94
+ * Validates each metric before sending — invalid metrics are silently dropped.
95
+ *
96
+ * @param metrics - Array of metric data points
97
+ * @param eventId - Event identifier
98
+ * @param datasetDatasourceId - Dataset-datasource identifier
99
+ * @throws Error if not authenticated or if the request fails
100
+ */
101
+ ingest(metrics: Metric[], eventId: string, datasetDatasourceId: string): Promise<void>;
102
+ /**
103
+ * Report an error to the Dataworks Data Engine.
104
+ *
105
+ * @param error - Error report details
106
+ * @throws Error if not authenticated or if the request fails
107
+ */
108
+ reportError(error: ErrorReport): Promise<void>;
109
+ /**
110
+ * Subscribe to a real-time data channel on the AppSync Events API.
111
+ * Uses the Cognito JWT for authentication.
112
+ *
113
+ * The channel name maps to the AppSync Events API namespace (e.g. "dataworks/metrics").
114
+ *
115
+ * @param channel - Channel path to subscribe to
116
+ * @param onEvent - Callback invoked for each received event
117
+ * @returns Subscription handle with a close() method
118
+ * @throws Error if not authenticated
119
+ */
120
+ subscribe(channel: string, onEvent: SubscriptionHandler): Subscription;
121
+ /**
122
+ * Check whether a metric is valid before ingesting.
123
+ * Returns null if valid, or a human-readable reason string if invalid.
124
+ */
125
+ static validateMetric: (m: RawMetricItem) => string | null;
126
+ /**
127
+ * Filter an array of metrics, keeping only valid ones.
128
+ * Logs a warning for each dropped item.
129
+ */
130
+ static filterValidMetrics: (metrics: RawMetricItem[], warn: (msg: string) => void) => MetricItem[];
131
+ /** Returns true if the client has been authenticated via login(). */
132
+ get isAuthenticated(): boolean;
133
+ /** Returns the tenant from the last successful login, or null. */
134
+ get tenant(): string | null;
135
+ /** @internal Throws if not authenticated. */
136
+ private requireAuth;
137
+ }
138
+
139
+ /**
140
+ * Validation re-exports — thin wrappers around @dataworks/sdk/client validation.
141
+ * Declared locally so the published .d.ts is fully self-contained.
142
+ *
143
+ * @module @dataworks-technology/data
144
+ */
145
+
146
+ /**
147
+ * Check whether a metric is valid.
148
+ * Returns null if valid, or a human-readable reason string if invalid.
149
+ */
150
+ declare const validateMetric: (m: RawMetricItem) => string | null;
151
+ /**
152
+ * Filter an array of metrics, keeping only valid ones.
153
+ * Logs a warning for each dropped item via the provided warn function.
154
+ */
155
+ declare const filterValidMetrics: (metrics: RawMetricItem[], warn: (msg: string) => void) => MetricItem[];
156
+
157
+ export { DataClient, type DataClientConfig, type ErrorReport, type LoginResult, type Metric, type MetricItem, type MetricsPayload, type RawMetricItem, type Subscription, type SubscriptionHandler, filterValidMetrics, validateMetric };
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Public types for @dataworks/data.
3
+ *
4
+ * @module @dataworks-technology/data
5
+ */
6
+ /** Configuration for connecting to the Dataworks Data Engine. */
7
+ interface DataClientConfig {
8
+ /** Cognito User Pool endpoint (e.g. "https://cognito-idp.eu-west-1.amazonaws.com/") */
9
+ cognitoEndpoint: string;
10
+ /** Cognito app client ID */
11
+ clientId: string;
12
+ /** Data Engine ingest URL (API Gateway endpoint) */
13
+ ingestUrl: string;
14
+ /** Data Engine error reporting URL (API Gateway endpoint) */
15
+ errorUrl: string;
16
+ /** AppSync Events API endpoint for real-time subscriptions */
17
+ realtimeUrl: string;
18
+ }
19
+ /** Credentials returned from a successful login. */
20
+ interface LoginResult {
21
+ accessToken: string;
22
+ idToken: string;
23
+ refreshToken: string;
24
+ /** Tenant extracted from the JWT claims. */
25
+ tenant: string;
26
+ }
27
+ /** A single metric data point to ingest. */
28
+ interface Metric {
29
+ /** Unique athlete identifier */
30
+ athleteId: string;
31
+ /** Metric name (e.g. "heartrate", "speed", "power") */
32
+ metric: string;
33
+ /** Metric value — number or string */
34
+ value: number | string;
35
+ /** Unix timestamp in seconds */
36
+ timestamp: number;
37
+ }
38
+ /** Validated metric item (all fields confirmed present and correct). */
39
+ interface MetricItem {
40
+ athleteId: string;
41
+ metric: string;
42
+ value: unknown;
43
+ timestamp: number;
44
+ }
45
+ /** Loosely-typed input for validation — all fields are unknown. */
46
+ interface RawMetricItem {
47
+ metric: unknown;
48
+ athleteId: unknown;
49
+ value: unknown;
50
+ timestamp: unknown;
51
+ }
52
+ /** Payload shape for metric ingestion. */
53
+ interface MetricsPayload {
54
+ eventId: string;
55
+ datasetDatasourceId: string;
56
+ metrics: MetricItem[];
57
+ }
58
+ /** Error report to send to the Data Engine. */
59
+ interface ErrorReport {
60
+ /** Dataset-datasource identifier */
61
+ datasetDatasourceId: string;
62
+ /** Event identifier */
63
+ eventId: string;
64
+ /** Client identifier */
65
+ clientId: number;
66
+ /** Short error title */
67
+ errorTitle: string;
68
+ /** Detailed error description */
69
+ errorDescription: string;
70
+ }
71
+ /** Callback for subscription events. */
72
+ type SubscriptionHandler = (event: unknown) => void;
73
+ /** Active subscription that can be closed. */
74
+ interface Subscription {
75
+ /** Close the subscription and disconnect. */
76
+ close(): void;
77
+ }
78
+
79
+ declare class DataClient {
80
+ private readonly config;
81
+ private credentials;
82
+ constructor(config: DataClientConfig);
83
+ /**
84
+ * Authenticate with the Dataworks platform using Cognito USER_PASSWORD_AUTH.
85
+ * Must be called before ingest(), reportError(), or subscribe().
86
+ *
87
+ * @param username - Cognito username
88
+ * @param password - Cognito password
89
+ * @returns Login result containing tokens and tenant
90
+ */
91
+ login(username: string, password: string): Promise<LoginResult>;
92
+ /**
93
+ * Ingest metric data points into the Dataworks Data Engine.
94
+ * Validates each metric before sending — invalid metrics are silently dropped.
95
+ *
96
+ * @param metrics - Array of metric data points
97
+ * @param eventId - Event identifier
98
+ * @param datasetDatasourceId - Dataset-datasource identifier
99
+ * @throws Error if not authenticated or if the request fails
100
+ */
101
+ ingest(metrics: Metric[], eventId: string, datasetDatasourceId: string): Promise<void>;
102
+ /**
103
+ * Report an error to the Dataworks Data Engine.
104
+ *
105
+ * @param error - Error report details
106
+ * @throws Error if not authenticated or if the request fails
107
+ */
108
+ reportError(error: ErrorReport): Promise<void>;
109
+ /**
110
+ * Subscribe to a real-time data channel on the AppSync Events API.
111
+ * Uses the Cognito JWT for authentication.
112
+ *
113
+ * The channel name maps to the AppSync Events API namespace (e.g. "dataworks/metrics").
114
+ *
115
+ * @param channel - Channel path to subscribe to
116
+ * @param onEvent - Callback invoked for each received event
117
+ * @returns Subscription handle with a close() method
118
+ * @throws Error if not authenticated
119
+ */
120
+ subscribe(channel: string, onEvent: SubscriptionHandler): Subscription;
121
+ /**
122
+ * Check whether a metric is valid before ingesting.
123
+ * Returns null if valid, or a human-readable reason string if invalid.
124
+ */
125
+ static validateMetric: (m: RawMetricItem) => string | null;
126
+ /**
127
+ * Filter an array of metrics, keeping only valid ones.
128
+ * Logs a warning for each dropped item.
129
+ */
130
+ static filterValidMetrics: (metrics: RawMetricItem[], warn: (msg: string) => void) => MetricItem[];
131
+ /** Returns true if the client has been authenticated via login(). */
132
+ get isAuthenticated(): boolean;
133
+ /** Returns the tenant from the last successful login, or null. */
134
+ get tenant(): string | null;
135
+ /** @internal Throws if not authenticated. */
136
+ private requireAuth;
137
+ }
138
+
139
+ /**
140
+ * Validation re-exports — thin wrappers around @dataworks/sdk/client validation.
141
+ * Declared locally so the published .d.ts is fully self-contained.
142
+ *
143
+ * @module @dataworks-technology/data
144
+ */
145
+
146
+ /**
147
+ * Check whether a metric is valid.
148
+ * Returns null if valid, or a human-readable reason string if invalid.
149
+ */
150
+ declare const validateMetric: (m: RawMetricItem) => string | null;
151
+ /**
152
+ * Filter an array of metrics, keeping only valid ones.
153
+ * Logs a warning for each dropped item via the provided warn function.
154
+ */
155
+ declare const filterValidMetrics: (metrics: RawMetricItem[], warn: (msg: string) => void) => MetricItem[];
156
+
157
+ export { DataClient, type DataClientConfig, type ErrorReport, type LoginResult, type Metric, type MetricItem, type MetricsPayload, type RawMetricItem, type Subscription, type SubscriptionHandler, filterValidMetrics, validateMetric };
package/dist/index.js ADDED
@@ -0,0 +1,252 @@
1
+ // ../../node_modules/@dataworks/sdk/dist/client/validation.js
2
+ function validateMetric(m) {
3
+ if (typeof m.metric !== "string" || m.metric.trim() === "")
4
+ return "metric must be a non-empty string";
5
+ if (typeof m.athleteId !== "string" || m.athleteId.trim() === "")
6
+ return "athleteId must be a non-empty string";
7
+ if (!Number.isInteger(m.timestamp) || m.timestamp <= 0)
8
+ return `timestamp must be a positive integer, got ${m.timestamp}`;
9
+ if (m.value === null || m.value === void 0)
10
+ return "value is null/undefined";
11
+ if (typeof m.value === "number" && !isFinite(m.value))
12
+ return `value is non-finite number (${m.value})`;
13
+ if (typeof m.value !== "number" && typeof m.value !== "string")
14
+ return `value type ${typeof m.value} not allowed (must be number or string)`;
15
+ if (typeof m.value === "string" && m.value === "")
16
+ return "value is empty string";
17
+ return null;
18
+ }
19
+ function filterValidMetrics(metrics, warn) {
20
+ const valid = [];
21
+ for (const m of metrics) {
22
+ const reason = validateMetric(m);
23
+ if (reason) {
24
+ warn(`Dropping invalid metric [${String(m.metric)}=${JSON.stringify(m.value)}]: ${reason}`);
25
+ } else {
26
+ valid.push(m);
27
+ }
28
+ }
29
+ return valid;
30
+ }
31
+
32
+ // ../../node_modules/@dataworks/sdk/dist/client/auth.js
33
+ async function loginWithCredentials(username, password, config) {
34
+ const authParams = {
35
+ USERNAME: username,
36
+ PASSWORD: password
37
+ };
38
+ if (config.clientSecret) {
39
+ const crypto2 = await import("crypto");
40
+ const hash = crypto2.createHmac("sha256", config.clientSecret).update(username + config.clientId).digest("base64");
41
+ authParams.SECRET_HASH = hash;
42
+ }
43
+ const resp = await fetch(config.cognitoEndpoint, {
44
+ method: "POST",
45
+ headers: {
46
+ "X-Amz-Target": "AWSCognitoIdentityProviderService.InitiateAuth",
47
+ "Content-Type": "application/x-amz-json-1.1"
48
+ },
49
+ body: JSON.stringify({
50
+ AuthFlow: "USER_PASSWORD_AUTH",
51
+ ClientId: config.clientId,
52
+ AuthParameters: authParams
53
+ })
54
+ });
55
+ if (!resp.ok) {
56
+ const body = await resp.text().catch(() => "");
57
+ throw new Error(`loginWithCredentials: Cognito auth failed: ${resp.status} \u2014 ${body}`);
58
+ }
59
+ const data = await resp.json();
60
+ const result = data.AuthenticationResult;
61
+ if (!result?.AccessToken || !result?.IdToken) {
62
+ throw new Error("loginWithCredentials: response missing AccessToken or IdToken");
63
+ }
64
+ const tenant = extractTenantFromJwt(result.IdToken);
65
+ return {
66
+ accessToken: result.AccessToken,
67
+ idToken: result.IdToken,
68
+ refreshToken: result.RefreshToken ?? "",
69
+ tenant
70
+ };
71
+ }
72
+ function extractTenantFromJwt(idToken) {
73
+ try {
74
+ const payload = idToken.split(".")[1];
75
+ const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf-8"));
76
+ return decoded["custom:tenant"] ?? decoded.tenant ?? decoded["cognito:groups"]?.[0] ?? "";
77
+ } catch {
78
+ return "";
79
+ }
80
+ }
81
+
82
+ // src/validation.ts
83
+ var validateMetric2 = validateMetric;
84
+ var filterValidMetrics2 = filterValidMetrics;
85
+
86
+ // src/data-client.ts
87
+ var DataClient = class {
88
+ constructor(config) {
89
+ this.credentials = null;
90
+ this.config = config;
91
+ }
92
+ /**
93
+ * Authenticate with the Dataworks platform using Cognito USER_PASSWORD_AUTH.
94
+ * Must be called before ingest(), reportError(), or subscribe().
95
+ *
96
+ * @param username - Cognito username
97
+ * @param password - Cognito password
98
+ * @returns Login result containing tokens and tenant
99
+ */
100
+ async login(username, password) {
101
+ this.credentials = await loginWithCredentials(username, password, {
102
+ cognitoEndpoint: this.config.cognitoEndpoint,
103
+ clientId: this.config.clientId
104
+ });
105
+ return this.credentials;
106
+ }
107
+ /**
108
+ * Ingest metric data points into the Dataworks Data Engine.
109
+ * Validates each metric before sending — invalid metrics are silently dropped.
110
+ *
111
+ * @param metrics - Array of metric data points
112
+ * @param eventId - Event identifier
113
+ * @param datasetDatasourceId - Dataset-datasource identifier
114
+ * @throws Error if not authenticated or if the request fails
115
+ */
116
+ async ingest(metrics, eventId, datasetDatasourceId) {
117
+ this.requireAuth();
118
+ const valid = filterValidMetrics2(
119
+ metrics,
120
+ (msg) => console.warn(`[@dataworks-technology/data] ${msg}`)
121
+ );
122
+ if (valid.length === 0) return;
123
+ const payload = {
124
+ eventId,
125
+ datasetDatasourceId,
126
+ metrics: valid
127
+ };
128
+ const resp = await fetch(this.config.ingestUrl, {
129
+ method: "POST",
130
+ headers: {
131
+ "Content-Type": "application/json",
132
+ Authorization: `Bearer ${this.credentials.accessToken}`
133
+ },
134
+ body: JSON.stringify(payload)
135
+ });
136
+ if (!resp.ok) {
137
+ const body = await resp.text().catch(() => "");
138
+ throw new Error(
139
+ `[@dataworks-technology/data] ingest failed: ${resp.status} \u2014 ${body}`
140
+ );
141
+ }
142
+ }
143
+ /**
144
+ * Report an error to the Dataworks Data Engine.
145
+ *
146
+ * @param error - Error report details
147
+ * @throws Error if not authenticated or if the request fails
148
+ */
149
+ async reportError(error) {
150
+ this.requireAuth();
151
+ const resp = await fetch(this.config.errorUrl, {
152
+ method: "POST",
153
+ headers: {
154
+ "Content-Type": "application/json",
155
+ Authorization: `Bearer ${this.credentials.accessToken}`
156
+ },
157
+ body: JSON.stringify(error)
158
+ });
159
+ if (!resp.ok) {
160
+ const body = await resp.text().catch(() => "");
161
+ throw new Error(
162
+ `[@dataworks-technology/data] reportError failed: ${resp.status} \u2014 ${body}`
163
+ );
164
+ }
165
+ }
166
+ /**
167
+ * Subscribe to a real-time data channel on the AppSync Events API.
168
+ * Uses the Cognito JWT for authentication.
169
+ *
170
+ * The channel name maps to the AppSync Events API namespace (e.g. "dataworks/metrics").
171
+ *
172
+ * @param channel - Channel path to subscribe to
173
+ * @param onEvent - Callback invoked for each received event
174
+ * @returns Subscription handle with a close() method
175
+ * @throws Error if not authenticated
176
+ */
177
+ subscribe(channel, onEvent) {
178
+ this.requireAuth();
179
+ const url = new URL(this.config.realtimeUrl);
180
+ url.pathname = "/event/realtime";
181
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
182
+ const ws = new WebSocket(url.toString(), [
183
+ "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
+ )}`
191
+ ]);
192
+ 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
+ );
204
+ });
205
+ ws.addEventListener("message", (event) => {
206
+ try {
207
+ const msg = JSON.parse(String(event.data));
208
+ if (msg.type === "data") {
209
+ onEvent(JSON.parse(msg.event));
210
+ }
211
+ } catch {
212
+ }
213
+ });
214
+ return {
215
+ close() {
216
+ ws.close();
217
+ }
218
+ };
219
+ }
220
+ /** Returns true if the client has been authenticated via login(). */
221
+ get isAuthenticated() {
222
+ return this.credentials !== null;
223
+ }
224
+ /** Returns the tenant from the last successful login, or null. */
225
+ get tenant() {
226
+ return this.credentials?.tenant ?? null;
227
+ }
228
+ /** @internal Throws if not authenticated. */
229
+ requireAuth() {
230
+ if (!this.credentials) {
231
+ throw new Error(
232
+ "[@dataworks-technology/data] Not authenticated \u2014 call login() first"
233
+ );
234
+ }
235
+ }
236
+ };
237
+ /**
238
+ * Check whether a metric is valid before ingesting.
239
+ * Returns null if valid, or a human-readable reason string if invalid.
240
+ */
241
+ DataClient.validateMetric = validateMetric2;
242
+ /**
243
+ * Filter an array of metrics, keeping only valid ones.
244
+ * Logs a warning for each dropped item.
245
+ */
246
+ DataClient.filterValidMetrics = filterValidMetrics2;
247
+ export {
248
+ DataClient,
249
+ filterValidMetrics2 as filterValidMetrics,
250
+ validateMetric2 as validateMetric
251
+ };
252
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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":"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","names":["crypto","validateMetric","filterValidMetrics","filterValidMetrics","validateMetric"]}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@dataworks-technology/data",
3
+ "version": "0.1.3",
4
+ "description": "Dataworks Data Engine SDK — authenticate, ingest metrics, and subscribe to live data",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsup",
22
+ "test": "vitest run",
23
+ "prepublishOnly": "bun run build",
24
+ "publish:patch": "npm version patch && npm publish --@dataworks-technology:registry=https://registry.npmjs.org/",
25
+ "publish:minor": "npm version minor && npm publish --@dataworks-technology:registry=https://registry.npmjs.org/",
26
+ "publish:major": "npm version major && npm publish --@dataworks-technology:registry=https://registry.npmjs.org/"
27
+ },
28
+ "publishConfig": {
29
+ "registry": "https://registry.npmjs.org/",
30
+ "access": "public"
31
+ },
32
+ "keywords": [
33
+ "dataworks",
34
+ "sports",
35
+ "data",
36
+ "metrics",
37
+ "live",
38
+ "sdk"
39
+ ],
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/Dataworks-Technology/Dataworks-Data"
44
+ },
45
+ "devDependencies": {
46
+ "@dataworks/sdk": "^1.6.0",
47
+ "tsup": "^8.4.0",
48
+ "typescript": "^5.8.3",
49
+ "vitest": "^3.2.3"
50
+ },
51
+ "dependencies": {}
52
+ }