@uns-kit/core 3.0.47 → 3.0.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/MIGRATIONS.md +27 -0
  2. package/README.md +24 -22
  3. package/dist/runtime/service-access-examples/README.md +21 -0
  4. package/dist/runtime/service-access-examples/background-reader/README.md +29 -0
  5. package/dist/runtime/service-access-examples/background-reader/build.mjs +7 -0
  6. package/dist/runtime/service-access-examples/background-reader/config-example.json +22 -0
  7. package/dist/runtime/service-access-examples/background-reader/package.json +37 -0
  8. package/dist/runtime/service-access-examples/background-reader/src/index.mjs +55 -0
  9. package/dist/runtime/service-access-examples/background-reader/src/reader.mjs +26 -0
  10. package/dist/runtime/service-access-examples/catalog-summary/README.md +30 -0
  11. package/dist/runtime/service-access-examples/catalog-summary/build.mjs +7 -0
  12. package/dist/runtime/service-access-examples/catalog-summary/config-example.json +23 -0
  13. package/dist/runtime/service-access-examples/catalog-summary/package.json +92 -0
  14. package/dist/runtime/service-access-examples/catalog-summary/src/index.mjs +50 -0
  15. package/dist/runtime/service-access-examples/catalog-summary/src/offer.mjs +35 -0
  16. package/dist/runtime/service-access-guide.md +259 -112
  17. package/dist/runtime/service-access-package.d.ts +6 -0
  18. package/dist/runtime/service-access-package.d.ts.map +1 -0
  19. package/dist/runtime/service-access-package.js +45 -0
  20. package/dist/runtime/service-access-package.js.map +1 -0
  21. package/dist/runtime/service-access-package.test.d.ts +2 -0
  22. package/dist/runtime/service-access-package.test.d.ts.map +1 -0
  23. package/dist/runtime/service-access-package.test.js +61 -0
  24. package/dist/runtime/service-access-package.test.js.map +1 -0
  25. package/dist/runtime/service-access.d.ts +26 -0
  26. package/dist/runtime/service-access.d.ts.map +1 -1
  27. package/dist/runtime/service-access.js +23 -0
  28. package/dist/runtime/service-access.js.map +1 -1
  29. package/package.json +1 -1
package/MIGRATIONS.md CHANGED
@@ -9,6 +9,33 @@ Agents must inspect the application's existing ownership and shutdown flow
9
9
  before editing it. The examples below describe the intended behavior, not a
10
10
  mechanical search-and-replace operation.
11
11
 
12
+ ## 3.0.49 - Inline Service access package declarations
13
+
14
+ Existing file declarations remain compatible; no permission is granted by this
15
+ upgrade. New services may put the declaration object directly at
16
+ `package.json.unsDatahub.serviceAccess`. Use `loadServiceAccessDeclaration` from
17
+ `@uns-kit/core/runtime/service-access-package.js` for runtime reads, or the pure
18
+ resolver/schema from `runtime/service-access.js` for host integrations. Both forms
19
+ normalize to the same semantic digest. Do not declare both `serviceAccess` and
20
+ historical `catalogAccess`.
21
+
22
+ Inline metadata requires a Controller advertising `rttServiceAccessInline`;
23
+ Controller 2.2.96 accepts only the file form. Keep the file form until the target
24
+ Controller supports inline declarations. This patch does not implement the planned
25
+ configuration-save approval UI or automatic installation grants. Administrator
26
+ review, caller authorization, MQTT ACLs and rotating token use remain separate.
27
+ Both complete examples now use inline metadata and require the matching host.
28
+
29
+ ## 3.0.48 - Checked Service access documentation and examples
30
+
31
+ This patch clarifies existing 3.0.47 contracts; no new permission mode or automatic
32
+ application migration is introduced. Follow the
33
+ [source walkthrough](https://github.com/uns-openhub/uns-kit/blob/master/docs/catalog-publication-access.md)
34
+ or bundled `@uns-kit/core/runtime/service-access-guide.md` and complete projects in
35
+ `dist/runtime/service-access-examples/`. Examples use a rotating token provider;
36
+ background API reads, caller-mediated catalogs and MQTT ACLs are separate.
37
+ An application adopting the capability must still follow the 3.0.47 entry below.
38
+
12
39
  ## 3.0.47 - Common Service access and publications
13
40
 
14
41
  When crossing `<3.0.47` to `>=3.0.47`, adoption is explicit; existing endpoint authorization and caller forwarding do
package/README.md CHANGED
@@ -8,10 +8,10 @@ Note: Apps built with uns-kit are intended to be managed by the **UNS OpenHub co
8
8
 
9
9
  | Package | Description |
10
10
  | --- | --- |
11
- | [`@uns-kit/core`](https://github.com/uns-openhub/uns-kit/tree/main/packages/uns-core) | Base runtime (UnsProxyProcess, MQTT helpers, config tooling, gRPC gateway). |
12
- | [`@uns-kit/api`](https://github.com/uns-openhub/uns-kit/tree/main/packages/uns-api) | Express plugin — HTTP endpoints, JWT/JWKS auth, Swagger, UNS metadata. |
13
- | [`@uns-kit/cron`](https://github.com/uns-openhub/uns-kit/tree/main/packages/uns-cron) | Cron-driven scheduler that emits UNS events on a fixed cadence. |
14
- | [`@uns-kit/cli`](https://github.com/uns-openhub/uns-kit/tree/main/packages/uns-cli) | CLI for scaffolding new UNS applications. |
11
+ | [`@uns-kit/core`](https://github.com/uns-openhub/uns-kit/tree/master/packages/uns-core) | Base runtime (UnsProxyProcess, MQTT helpers, config tooling, gRPC gateway). |
12
+ | [`@uns-kit/api`](https://github.com/uns-openhub/uns-kit/tree/master/packages/uns-api) | Express plugin — HTTP endpoints, JWT/JWKS auth, Swagger, UNS metadata. |
13
+ | [`@uns-kit/cron`](https://github.com/uns-openhub/uns-kit/tree/master/packages/uns-cron) | Cron-driven scheduler that emits UNS events on a fixed cadence. |
14
+ | [`@uns-kit/cli`](https://github.com/uns-openhub/uns-kit/tree/master/packages/uns-cli) | CLI for scaffolding new UNS applications. |
15
15
 
16
16
  ## Installation
17
17
 
@@ -95,47 +95,49 @@ const cron = await proc.createCrontabProxy("*/5 * * * *", { event: "tick" });
95
95
 
96
96
  ## Datahub client (last value + history)
97
97
 
98
- `UnsClient` provides a minimal REST client for the UNS OpenHub API, including batch last-value, single-topic catch-all history, and batch range endpoints. Prefer a long-lived service token if available; you can pass it directly and skip username/password auth.
98
+ `UnsClient` provides a minimal REST client for the UNS OpenHub API, including batch last-value, single-topic catch-all history, and batch range endpoints. Use `ServiceTokenProvider` for Controller-managed services so credential rotation is picked up on every request. Declare requirements in `package.json.unsDatahub.serviceAccess` (SDK 3.0.49+ and an inline-capable Controller), or retain the compatible JSON file reference. Review the service's declared scopes and resource paths before Start; never cache a managed token at application startup.
99
99
 
100
- ```ts
101
- import { UnsClient } from "@uns-kit/core";
102
- import { ConfigFile } from "@uns-kit/core";
100
+ Read the [source Service access walkthrough](https://github.com/uns-openhub/uns-kit/blob/master/docs/catalog-publication-access.md)
101
+ or bundled `@uns-kit/core/runtime/service-access-guide.md`. Complete background-reader
102
+ and catalog projects ship in `dist/runtime/service-access-examples/`. A background
103
+ reader uses its service identity only; a catalog operation also carries its caller's
104
+ identity. These API permissions do not create MQTT ACLs or SQL grants.
103
105
 
104
- const config = await ConfigFile.loadConfig();
106
+ ```ts
107
+ import { ServiceTokenProvider, UnsClient } from "@uns-kit/core";
105
108
 
106
- const client = new UnsClient("https://datahub.example.com", {
107
- token: process.env.UNS_SERVICE_TOKEN ?? config.uns.token,
108
- });
109
+ const baseUrl = process.env.UNS_CONTROLLER_PUBLIC_BASE;
110
+ if (!baseUrl) throw new Error("Set the trusted Controller base URL.");
111
+ const client = new UnsClient(baseUrl, { tokenProvider: new ServiceTokenProvider() });
109
112
 
110
113
  const values = await client.lastValue([
111
- "raw/data/line-1/motor/main/temperature",
112
- "raw/data/line-1/motor/main/status",
114
+ "example/plant/line-1/motor/main/temperature",
115
+ "example/plant/line-1/motor/main/status",
113
116
  ]);
114
- console.log(values);
117
+ // Process the values without logging private records.
115
118
 
116
- const data = await client.getAttributeData("sij/acroni/vv/hrm-furnace/equipment/pusher/output-quantity", {
119
+ const data = await client.getAttributeData("example/plant/line-1/motor/main/temperature", {
117
120
  from: "2026-05-07T11:17:01.157Z",
118
121
  to: "2026-05-07T11:22:01.157Z",
119
- table: "uns_sij_hrm_furnace_data",
120
122
  aggregate: "last",
121
123
  dedupe: false,
122
124
  });
123
- console.log(data?.toRecords());
125
+ const records = data?.toRecords();
124
126
 
125
127
  const customData = await client.getData("/projects/project-name/path-to-data/data", {
126
128
  fromDate: "20260325",
127
129
  });
128
- console.log(await customData.json());
130
+ const customResult = await customData.json();
129
131
 
130
132
  const batchHistory = await client.history([
131
- "sij/acroni/vv/hrm-furnace/equipment/zone-1/temperature",
132
- "sij/acroni/vv/hrm-furnace/equipment/zone-2/temperature",
133
+ "example/plant/line-1/motor/main/temperature",
134
+ "example/plant/line-1/motor/main/status",
133
135
  ], {
134
136
  from: "2026-04-09T06:00:00Z",
135
137
  to: "2026-04-09T07:00:00Z",
136
138
  limit: 500,
137
139
  });
138
- console.log(batchHistory?.byTopic);
140
+ // Process the bounded batch result.
139
141
  ```
140
142
 
141
143
  ## Sub-Asset Publishing
@@ -0,0 +1,21 @@
1
+ # Service access examples
2
+
3
+ - `background-reader/`: no API or catalog; a reviewed workload token reads one UNS history source.
4
+ - `catalog-summary/`: a catalog-only caller requests a bounded derived count; the workload has no direct source permission.
5
+
6
+ Each folder is an independent, credential-free Node.js package. Copy one folder
7
+ to a new project, install dependencies and build. The declarations are inline in package.json (SDK 3.0.49+). Deploy/review/start
8
+ with a Controller advertising rttServiceAccessInline; Controller 2.2.96 requires
9
+ the compatible file form instead.
10
+ The [walkthrough](https://github.com/uns-openhub/uns-kit/blob/master/docs/catalog-publication-access.md)
11
+ explains what the developer, administrator, Controller, SDK and caller each own.
12
+
13
+ The examples are also shipped in `@uns-kit/core/dist/runtime/service-access-examples/`.
14
+ No real broker, database, user or production endpoint is supplied. Configure your
15
+ own local test resources. These files create no identities, grants or MQTT ACLs.
16
+
17
+ Maintainers: run `pnpm run ts:build` then `pnpm run test:service-access-examples`.
18
+ The verifier packs the SDK, installs it in a temporary consumer, builds/packs both
19
+ examples, and exercises their real handlers/token provider against a synthetic
20
+ loopback API. This checks imports, shipped files, token rotation, denial and
21
+ closed output, not real Controller policy, MQTT connectivity or cluster ownership.
@@ -0,0 +1,29 @@
1
+ # background-reader
2
+
3
+ A complete Node.js 22+ example using SDK APIs from 3.0.49 onward.
4
+ Read the [Service access walkthrough](https://github.com/uns-openhub/uns-kit/blob/master/docs/catalog-publication-access.md) for the declaration, Controller review and acceptance steps.
5
+
6
+ ```sh
7
+ pnpm install
8
+ pnpm run build
9
+ cp config-example.json config.json
10
+ ```
11
+
12
+ The example configuration targets a local development broker/Controller. Select
13
+ your own endpoints and secret references before deployment. The Controller owns
14
+ instance configuration when deployed. `MQTT_USERNAME`/`MQTT_PASSWORD` concern broker
15
+ ACLs only and grant no OpenHub API rights. Do not commit `config.json` or `.env`.
16
+
17
+ Deploy the package, apply its exact-version Service access policy, and then Start.
18
+ The Controller supplies managed identity/credential paths. Do not manufacture RTT
19
+ launch variables or paste a human token into configuration.
20
+
21
+ This worker has no API or catalog. It polls five minutes of history, at most
22
+ 100 rows, every 30 seconds while active. Provision the exact declared source in
23
+ your own test environment; zero rows is a valid result. Its token can read that
24
+ source, while another source must return 403. SIGINT/SIGTERM aborts the current
25
+ request and stops polling before runtime cleanup.
26
+
27
+ The access declaration is inline in package.json. This representation requires
28
+ Controller capability `rttServiceAccessInline`; it is unsupported by 2.2.96.
29
+ The SDK also accepts a package-relative JSON file reference for older hosts.
@@ -0,0 +1,7 @@
1
+ import { copyFile, mkdir, rm } from "node:fs/promises";
2
+
3
+ await rm("dist", { recursive: true, force: true });
4
+ await mkdir("dist");
5
+ await copyFile("src/index.mjs", "dist/index.js");
6
+ await copyFile("src/reader.mjs", "dist/reader.mjs");
7
+ // service-access.json stays at the package root, as declared in package.json.
@@ -0,0 +1,22 @@
1
+ {
2
+ "infra": {
3
+ "host": "localhost"
4
+ },
5
+ "uns": {
6
+ "processName": "example-background-reader",
7
+ "instanceMode": "wait",
8
+ "handover": false
9
+ },
10
+ "mqtt": {
11
+ "username": {
12
+ "provider": "env",
13
+ "key": "MQTT_USERNAME",
14
+ "default": ""
15
+ },
16
+ "password": {
17
+ "provider": "env",
18
+ "key": "MQTT_PASSWORD",
19
+ "default": ""
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "example-background-reader",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=22"
8
+ },
9
+ "main": "dist/index.js",
10
+ "files": [
11
+ "dist",
12
+ "config-example.json"
13
+ ],
14
+ "scripts": {
15
+ "build": "node build.mjs",
16
+ "start": "node dist/index.js"
17
+ },
18
+ "dependencies": {
19
+ "@uns-kit/core": "^3.0.49"
20
+ },
21
+ "unsDatahub": {
22
+ "schemaVersion": 1,
23
+ "kind": "addon",
24
+ "controllerCompatibility": ">=2 <3",
25
+ "serviceAccess": {
26
+ "protocol": 1,
27
+ "permissions": {
28
+ "scopes": [
29
+ "read:uns"
30
+ ],
31
+ "accessRules": [
32
+ "example/background/line-1/equipment/main/temperature"
33
+ ]
34
+ }
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,55 @@
1
+ import { ConfigFile, UnsProxyProcess } from "@uns-kit/core";
2
+ import { setTimeout as delay } from "node:timers/promises";
3
+
4
+ import { readHistory } from "./reader.mjs";
5
+
6
+ const config = await ConfigFile.loadConfig();
7
+ const controllerBase = process.env.UNS_CONTROLLER_PUBLIC_BASE;
8
+ if (!controllerBase) throw new Error("UNS_CONTROLLER_PUBLIC_BASE must name your Controller.");
9
+ const stop = new AbortController();
10
+ const runtime = new UnsProxyProcess(config.infra.host, {
11
+ processName: config.uns.processName,
12
+ username: config.mqtt?.username,
13
+ password: config.mqtt?.password,
14
+ });
15
+ let currentRead = Promise.resolve();
16
+ let stopping;
17
+ const shutdown = () =>
18
+ (stopping ??= (async () => {
19
+ stop.abort();
20
+ await currentRead.catch(() => {});
21
+ await runtime.shutdown();
22
+ })());
23
+ for (const signal of ["SIGINT", "SIGTERM"])
24
+ process.once(signal, () => {
25
+ void shutdown().then(
26
+ () => process.exit(0),
27
+ () => {
28
+ process.exitCode = 1;
29
+ },
30
+ );
31
+ });
32
+ try {
33
+ await runtime.prepareHandover(config.uns.instanceMode, config.uns.handover);
34
+ while (!stop.signal.aborted) {
35
+ if (runtime.isActive()) {
36
+ const to = new Date(),
37
+ from = new Date(to.getTime() - 5 * 60000);
38
+ try {
39
+ currentRead = readHistory(controllerBase, from.toISOString(), to.toISOString(), { signal: stop.signal });
40
+ const rows = await currentRead;
41
+ // Put application processing here. Never log bearer values or raw records.
42
+ console.info(`Read ${rows.length} samples.`);
43
+ } catch {
44
+ if (!stop.signal.aborted) console.warn("History read failed; check Service access and API availability.");
45
+ }
46
+ }
47
+ try {
48
+ await delay(30000, undefined, { signal: stop.signal });
49
+ } catch (error) {
50
+ if (!stop.signal.aborted) throw error;
51
+ }
52
+ }
53
+ } finally {
54
+ await shutdown();
55
+ }
@@ -0,0 +1,26 @@
1
+ import { ServiceTokenProvider } from "@uns-kit/core/tools/auth/service-token-provider.js";
2
+ import { loadServiceAccessDeclaration } from "@uns-kit/core/runtime/service-access-package.js";
3
+
4
+ const declaration = await loadServiceAccessDeclaration(new URL("../package.json", import.meta.url));
5
+ if (declaration?.permissions.scopes.length !== 1 || declaration.permissions.scopes[0] !== "read:uns" ||
6
+ declaration.permissions.accessRules.length !== 1 || /[+#]/.test(declaration.permissions.accessRules[0]))
7
+ throw new Error("This example requires one exact declared background source.");
8
+ export const sourceTopic = declaration.permissions.accessRules[0];
9
+
10
+ /** A background API read has a workload identity and no human caller token. */
11
+ export async function readHistory(controllerBase, from, to, { tokenProvider = new ServiceTokenProvider(), fetch: request = fetch, signal } = {}) {
12
+ const token = await tokenProvider.getAccessToken();
13
+ if (!token) throw new Error("Service credential unavailable; review Service access.");
14
+ const response = await request(new URL("/api/catchall/batch/range", controllerBase), {
15
+ method: "POST",
16
+ redirect: "error",
17
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
18
+ body: JSON.stringify({ topics: [sourceTopic], from, to, limit: 100 }),
19
+ signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(10000)]) : AbortSignal.timeout(10000),
20
+ });
21
+ if (!response.ok) throw new Error(`UNS history returned HTTP ${response.status}; no credential fallback.`);
22
+ const result = await response.json();
23
+ const rows = result.results?.[0]?.data;
24
+ if (!Array.isArray(rows)) throw new Error("Unexpected history response.");
25
+ return rows;
26
+ }
@@ -0,0 +1,30 @@
1
+ # catalog-summary
2
+
3
+ A complete Node.js 22+ example using SDK APIs from 3.0.49 onward.
4
+ Read the [Service access walkthrough](https://github.com/uns-openhub/uns-kit/blob/master/docs/catalog-publication-access.md) for the declaration, Controller review and acceptance steps.
5
+
6
+ ```sh
7
+ pnpm install
8
+ pnpm run build
9
+ cp config-example.json config.json
10
+ ```
11
+
12
+ The example configuration targets a local development broker/Controller. Select
13
+ your own endpoints and secret references before deployment. The Controller owns
14
+ instance configuration when deployed. `MQTT_USERNAME`/`MQTT_PASSWORD` concern broker
15
+ ACLs only and grant no OpenHub API rights. Do not commit `config.json` or `.env`.
16
+
17
+ Deploy the package, apply its exact-version Service access policy, and then Start.
18
+ The Controller supplies managed identity/credential paths. Do not manufacture RTT
19
+ launch variables or paste a human token into configuration.
20
+
21
+ This service requires a managed Controller launch for signed catalog route
22
+ publication. Its workload has no direct UNS access. Apply catalog:sources:read
23
+ and approve the publication for the intended audience. Give a test reader access
24
+ to the operation path, not the private source path. Execute a timezone-qualified
25
+ window of at most one hour. Expect {count, hasMore}; raw source fields are excluded.
26
+ A direct source request with the reader or service token must return 403.
27
+
28
+ The access declaration is inline in package.json. This representation requires
29
+ Controller capability `rttServiceAccessInline`; it is unsupported by 2.2.96.
30
+ The SDK also accepts a package-relative JSON file reference for older hosts.
@@ -0,0 +1,7 @@
1
+ import { copyFile, mkdir, rm } from "node:fs/promises";
2
+
3
+ await rm("dist", { recursive: true, force: true });
4
+ await mkdir("dist");
5
+ await copyFile("src/index.mjs", "dist/index.js");
6
+ await copyFile("src/offer.mjs", "dist/offer.mjs");
7
+ // The declaration lives in package.json, which npm always retains.
@@ -0,0 +1,23 @@
1
+ {
2
+ "infra": {
3
+ "host": "localhost"
4
+ },
5
+ "uns": {
6
+ "processName": "example-catalog-summary",
7
+ "instanceMode": "wait",
8
+ "handover": false,
9
+ "jwksWellKnownUrl": "http://localhost:3200/api/.well-known/jwks.json"
10
+ },
11
+ "mqtt": {
12
+ "username": {
13
+ "provider": "env",
14
+ "key": "MQTT_USERNAME",
15
+ "default": ""
16
+ },
17
+ "password": {
18
+ "provider": "env",
19
+ "key": "MQTT_PASSWORD",
20
+ "default": ""
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,92 @@
1
+ {
2
+ "name": "example-catalog-summary",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=22"
8
+ },
9
+ "main": "dist/index.js",
10
+ "files": [
11
+ "dist",
12
+ "config-example.json"
13
+ ],
14
+ "scripts": {
15
+ "build": "node build.mjs",
16
+ "start": "node dist/index.js"
17
+ },
18
+ "dependencies": {
19
+ "@uns-kit/core": "^3.0.49",
20
+ "@uns-kit/api": "^3.0.49"
21
+ },
22
+ "unsDatahub": {
23
+ "schemaVersion": 1,
24
+ "kind": "addon",
25
+ "controllerCompatibility": ">=2 <3",
26
+ "serviceAccess": {
27
+ "protocol": 1,
28
+ "permissions": {
29
+ "scopes": [],
30
+ "accessRules": []
31
+ },
32
+ "publications": {
33
+ "protocol": 1,
34
+ "operations": [
35
+ {
36
+ "id": "sample-count",
37
+ "title": "Temperature sample count",
38
+ "method": "GET",
39
+ "path": "/api/example/public/line-1/equipment/dataset/sample-count/json",
40
+ "mode": "approved-service",
41
+ "requestSchema": {
42
+ "type": "object",
43
+ "additionalProperties": false,
44
+ "required": [
45
+ "from",
46
+ "to"
47
+ ],
48
+ "properties": {
49
+ "from": {
50
+ "type": "string",
51
+ "format": "date-time"
52
+ },
53
+ "to": {
54
+ "type": "string",
55
+ "format": "date-time"
56
+ }
57
+ }
58
+ },
59
+ "responseSchema": {
60
+ "type": "object",
61
+ "additionalProperties": false,
62
+ "required": [
63
+ "count",
64
+ "hasMore"
65
+ ],
66
+ "properties": {
67
+ "count": {
68
+ "type": "integer",
69
+ "minimum": 0
70
+ },
71
+ "hasMore": {
72
+ "type": "boolean"
73
+ }
74
+ }
75
+ },
76
+ "sources": [
77
+ {
78
+ "id": "temperature",
79
+ "kind": "history",
80
+ "topic": "example/private/line-1/equipment/main/temperature",
81
+ "maxWindowMs": 3600000,
82
+ "maxRows": 1000
83
+ }
84
+ ],
85
+ "timeoutMs": 10000,
86
+ "maxResponseBytes": 4096
87
+ }
88
+ ]
89
+ }
90
+ }
91
+ }
92
+ }
@@ -0,0 +1,50 @@
1
+ import "@uns-kit/api";
2
+ import { registerApiCatalog } from "@uns-kit/api";
3
+ import { ConfigFile, UnsProxyProcess } from "@uns-kit/core";
4
+ import { CatalogPublicationClient } from "@uns-kit/core/runtime/catalog-publication-client.js";
5
+ import { loadServiceAccessDeclaration } from "@uns-kit/core/runtime/service-access-package.js";
6
+
7
+ import { createSummaryOffer } from "./offer.mjs";
8
+
9
+ const required = (key) => {
10
+ const value = process.env[key];
11
+ if (!value) throw new Error(`Missing managed runtime value: ${key}`);
12
+ return value;
13
+ };
14
+ const declaration = await loadServiceAccessDeclaration(new URL("../package.json", import.meta.url));
15
+ if (!declaration?.publications) throw new Error("Package must declare its catalog operations.");
16
+ const client = new CatalogPublicationClient({
17
+ controllerBase: required("UNS_CONTROLLER_PUBLIC_BASE"),
18
+ rttNode: required("RTT_NODE"),
19
+ instanceId: required("RTT_INSTANCE_ID"),
20
+ version: required("version"),
21
+ manifest: declaration.publications,
22
+ });
23
+ const config = await ConfigFile.loadConfig();
24
+ if (!config.uns.jwksWellKnownUrl) throw new Error("Configure the Controller JWKS URL.");
25
+ const runtime = new UnsProxyProcess(config.infra.host, {
26
+ processName: config.uns.processName,
27
+ username: config.mqtt?.username,
28
+ password: config.mqtt?.password,
29
+ });
30
+ let stopping;
31
+ const shutdown = () => (stopping ??= runtime.shutdown());
32
+ for (const signal of ["SIGINT", "SIGTERM"])
33
+ process.once(signal, () => {
34
+ void shutdown().then(
35
+ () => process.exit(0),
36
+ () => {
37
+ process.exitCode = 1;
38
+ },
39
+ );
40
+ });
41
+ try {
42
+ await runtime.prepareHandover(config.uns.instanceMode, config.uns.handover);
43
+ const api = await runtime.createApiProxy("catalog", { jwks: { wellKnownJwksUrl: config.uns.jwksWellKnownUrl } });
44
+ api.registerCatalogRoute();
45
+ await registerApiCatalog(api, { dataOfferSources: { sampleCount: createSummaryOffer(client) }, context: undefined });
46
+ } catch {
47
+ console.error("Catalog startup failed; check configuration, Service access and runtime logs.");
48
+ await shutdown();
49
+ process.exitCode = 1;
50
+ }
@@ -0,0 +1,35 @@
1
+ import { defineDataCatalogField, defineDataCatalogOfferSource, defineDataCatalogQueryParam, defineDataCatalogSchema } from "@uns-kit/api";
2
+ import { catalogPublicationHandler } from "@uns-kit/api/catalog-publication-handler.js";
3
+
4
+ export function createSummaryOffer(client) {
5
+ return defineDataCatalogOfferSource({
6
+ offerId: "sample-count",
7
+ displayName: "Temperature sample count",
8
+ description: "Count of samples in an approved interval; raw measurements are not returned.",
9
+ topic: "example/public/line-1",
10
+ asset: "equipment",
11
+ objectType: "dataset",
12
+ objectId: "sample-count",
13
+ attribute: "json",
14
+ method: "GET",
15
+ queryParams: ["from", "to"].map((name) =>
16
+ defineDataCatalogQueryParam(name, "ISO 8601 with timezone; at most one hour.", { required: true, format: "date-time" }),
17
+ ),
18
+ response: { statusCode: "200", contentType: "application/json" },
19
+ schema: defineDataCatalogSchema({
20
+ id: "sample-count",
21
+ title: "Sample count",
22
+ contentType: "application/json",
23
+ fields: [
24
+ defineDataCatalogField("count", "integer", "Number of returned samples"),
25
+ defineDataCatalogField("hasMore", "boolean", "More source samples exist than the bounded result contains"),
26
+ ],
27
+ }),
28
+ handler: ({ req, res }) =>
29
+ catalogPublicationHandler(client, "sample-count", async ({ history }) => {
30
+ const rows = await history("temperature");
31
+ if (!Array.isArray(rows.data) || typeof rows.truncated !== "boolean") throw new Error("Unexpected source response.");
32
+ return { count: rows.data.length, hasMore: rows.truncated };
33
+ })(req, res),
34
+ });
35
+ }