@uns-kit/core 3.0.46 → 3.0.48

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 (35) hide show
  1. package/MIGRATIONS.md +29 -0
  2. package/README.md +24 -22
  3. package/dist/runtime/catalog-publication-client.d.ts +21 -0
  4. package/dist/runtime/catalog-publication-client.d.ts.map +1 -0
  5. package/dist/runtime/catalog-publication-client.js +92 -0
  6. package/dist/runtime/catalog-publication-client.js.map +1 -0
  7. package/dist/runtime/catalog-publication.d.ts +97 -0
  8. package/dist/runtime/catalog-publication.d.ts.map +1 -0
  9. package/dist/runtime/catalog-publication.js +185 -0
  10. package/dist/runtime/catalog-publication.js.map +1 -0
  11. package/dist/runtime/service-access-examples/README.md +20 -0
  12. package/dist/runtime/service-access-examples/background-reader/README.md +25 -0
  13. package/dist/runtime/service-access-examples/background-reader/build.mjs +7 -0
  14. package/dist/runtime/service-access-examples/background-reader/config-example.json +22 -0
  15. package/dist/runtime/service-access-examples/background-reader/package.json +28 -0
  16. package/dist/runtime/service-access-examples/background-reader/service-access.json +11 -0
  17. package/dist/runtime/service-access-examples/background-reader/src/index.mjs +55 -0
  18. package/dist/runtime/service-access-examples/background-reader/src/reader.mjs +21 -0
  19. package/dist/runtime/service-access-examples/catalog-summary/README.md +26 -0
  20. package/dist/runtime/service-access-examples/catalog-summary/build.mjs +7 -0
  21. package/dist/runtime/service-access-examples/catalog-summary/config-example.json +23 -0
  22. package/dist/runtime/service-access-examples/catalog-summary/package.json +29 -0
  23. package/dist/runtime/service-access-examples/catalog-summary/service-access.json +65 -0
  24. package/dist/runtime/service-access-examples/catalog-summary/src/index.mjs +50 -0
  25. package/dist/runtime/service-access-examples/catalog-summary/src/offer.mjs +35 -0
  26. package/dist/runtime/service-access-guide.md +246 -0
  27. package/dist/runtime/service-access.d.ts +23 -0
  28. package/dist/runtime/service-access.d.ts.map +1 -0
  29. package/dist/runtime/service-access.js +62 -0
  30. package/dist/runtime/service-access.js.map +1 -0
  31. package/dist/runtime/service-access.test.d.ts +2 -0
  32. package/dist/runtime/service-access.test.d.ts.map +1 -0
  33. package/dist/runtime/service-access.test.js +57 -0
  34. package/dist/runtime/service-access.test.js.map +1 -0
  35. package/package.json +14 -1
@@ -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,28 @@
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
+ "service-access.json",
13
+ "config-example.json"
14
+ ],
15
+ "scripts": {
16
+ "build": "node build.mjs",
17
+ "start": "node dist/index.js"
18
+ },
19
+ "dependencies": {
20
+ "@uns-kit/core": "^3.0.47"
21
+ },
22
+ "unsDatahub": {
23
+ "schemaVersion": 1,
24
+ "kind": "addon",
25
+ "controllerCompatibility": ">=2 <3",
26
+ "serviceAccess": "service-access.json"
27
+ }
28
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "protocol": 1,
3
+ "permissions": {
4
+ "scopes": [
5
+ "read:uns"
6
+ ],
7
+ "accessRules": [
8
+ "example/background/line-1/equipment/main/temperature"
9
+ ]
10
+ }
11
+ }
@@ -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,21 @@
1
+ import { ServiceTokenProvider } from "@uns-kit/core/tools/auth/service-token-provider.js";
2
+
3
+ export const sourceTopic = "example/background/line-1/equipment/main/temperature";
4
+
5
+ /** A background API read has a workload identity and no human caller token. */
6
+ export async function readHistory(controllerBase, from, to, { tokenProvider = new ServiceTokenProvider(), fetch: request = fetch, signal } = {}) {
7
+ const token = await tokenProvider.getAccessToken();
8
+ if (!token) throw new Error("Service credential unavailable; review Service access.");
9
+ const response = await request(new URL("/api/catchall/batch/range", controllerBase), {
10
+ method: "POST",
11
+ redirect: "error",
12
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
13
+ body: JSON.stringify({ topics: [sourceTopic], from, to, limit: 100 }),
14
+ signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(10000)]) : AbortSignal.timeout(10000),
15
+ });
16
+ if (!response.ok) throw new Error(`UNS history returned HTTP ${response.status}; no credential fallback.`);
17
+ const result = await response.json();
18
+ const rows = result.results?.[0]?.data;
19
+ if (!Array.isArray(rows)) throw new Error("Unexpected history response.");
20
+ return rows;
21
+ }
@@ -0,0 +1,26 @@
1
+ # catalog-summary
2
+
3
+ A complete Node.js 22+ example using published SDK APIs from 3.0.47 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.
@@ -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
+ // service-access.json stays at the package root, as declared in package.json.
@@ -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,29 @@
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
+ "service-access.json",
13
+ "config-example.json"
14
+ ],
15
+ "scripts": {
16
+ "build": "node build.mjs",
17
+ "start": "node dist/index.js"
18
+ },
19
+ "dependencies": {
20
+ "@uns-kit/core": "^3.0.47",
21
+ "@uns-kit/api": "^3.0.47"
22
+ },
23
+ "unsDatahub": {
24
+ "schemaVersion": 1,
25
+ "kind": "addon",
26
+ "controllerCompatibility": ">=2 <3",
27
+ "serviceAccess": "service-access.json"
28
+ }
29
+ }
@@ -0,0 +1,65 @@
1
+ {
2
+ "protocol": 1,
3
+ "permissions": {
4
+ "scopes": [],
5
+ "accessRules": []
6
+ },
7
+ "publications": {
8
+ "protocol": 1,
9
+ "operations": [
10
+ {
11
+ "id": "sample-count",
12
+ "title": "Temperature sample count",
13
+ "method": "GET",
14
+ "path": "/api/example/public/line-1/equipment/dataset/sample-count/json",
15
+ "mode": "approved-service",
16
+ "requestSchema": {
17
+ "type": "object",
18
+ "additionalProperties": false,
19
+ "required": [
20
+ "from",
21
+ "to"
22
+ ],
23
+ "properties": {
24
+ "from": {
25
+ "type": "string",
26
+ "format": "date-time"
27
+ },
28
+ "to": {
29
+ "type": "string",
30
+ "format": "date-time"
31
+ }
32
+ }
33
+ },
34
+ "responseSchema": {
35
+ "type": "object",
36
+ "additionalProperties": false,
37
+ "required": [
38
+ "count",
39
+ "hasMore"
40
+ ],
41
+ "properties": {
42
+ "count": {
43
+ "type": "integer",
44
+ "minimum": 0
45
+ },
46
+ "hasMore": {
47
+ "type": "boolean"
48
+ }
49
+ }
50
+ },
51
+ "sources": [
52
+ {
53
+ "id": "temperature",
54
+ "kind": "history",
55
+ "topic": "example/private/line-1/equipment/main/temperature",
56
+ "maxWindowMs": 3600000,
57
+ "maxRows": 1000
58
+ }
59
+ ],
60
+ "timeoutMs": 10000,
61
+ "maxResponseBytes": 4096
62
+ }
63
+ ]
64
+ }
65
+ }
@@ -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 { parseServiceAccessDeclaration } from "@uns-kit/core/runtime/service-access.js";
6
+ import { readFile } from "node:fs/promises";
7
+
8
+ import { createSummaryOffer } from "./offer.mjs";
9
+
10
+ const required = (key) => {
11
+ const value = process.env[key];
12
+ if (!value) throw new Error(`Missing managed runtime value: ${key}`);
13
+ return value;
14
+ };
15
+ const declaration = parseServiceAccessDeclaration(JSON.parse(await readFile(new URL("../service-access.json", import.meta.url), "utf8")));
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
+ }
@@ -0,0 +1,246 @@
1
+ # Service access: from declaration to a running service
2
+
3
+ For application developers and coding agents using **SDK 3.0.47 or later**.
4
+ Common Service access also requires a matching Controller; these workflows were
5
+ verified with Controller **2.2.95**. SDK installation alone enables no host rights.
6
+
7
+ The developer declares a permission ceiling and uses the SDK. An administrator
8
+ reviews the installed release in **Micro services → instance → Service access**.
9
+ The Controller stores that decision, issues the instance credential and supplies
10
+ its file path. The SDK reads the current credential when making a request. The UI
11
+ is a policy editor, not a place to copy a bearer token into application code.
12
+
13
+ ## Choose the access model
14
+
15
+ | Application activity | Credential used | Administrator approves | Mechanism |
16
+ | --- | --- | --- | --- |
17
+ | Background UNS API read | Service token only | `read:uns` and explicit UNS paths | `ServiceTokenProvider` / API client |
18
+ | Approved derived catalog operation | Service token plus the incoming user's token | Service capability, exact publication sources/output and audience; user separately needs operation access | `CatalogPublicationClient` + `catalogPublicationHandler` |
19
+ | Direct MQTT subscribe/publish | Broker credentials | Broker ACLs for the relevant topics | MQTT connection settings and broker authorization |
20
+ | Direct SQL/external API access | That resource's credential | Database/external-system grants | Application adapter and secret references |
21
+
22
+ **`read:uns` protects OpenHub API reads. It does not install MQTT ACLs.**
23
+ MQTT-style filters in Service access describe API resource paths; their syntax
24
+ does not make the HTTP service token a broker password. `infra.host` and the
25
+ example's `mqtt.username/password` concern runtime MQTT connectivity. Configure
26
+ broker ACLs separately, including the runtime signaling topics your deployment
27
+ requires. A direct database connection is governed by its database identity.
28
+ This capability is not an arbitrary-code sandbox.
29
+
30
+ ## Runnable examples and package contents
31
+
32
+ Both examples are independent Node.js 22+ projects with complete source,
33
+ credential-free configuration, a declaration, build script and package metadata:
34
+
35
+ - [Background reader](https://github.com/uns-openhub/uns-kit/tree/master/examples/service-access/background-reader)
36
+ - [Catalog summary](https://github.com/uns-openhub/uns-kit/tree/master/examples/service-access/catalog-summary)
37
+
38
+ The npm core package also ships this guide at
39
+ `@uns-kit/core/runtime/service-access-guide.md` and both projects under
40
+ `node_modules/@uns-kit/core/dist/runtime/service-access-examples/`. Copy a project
41
+ to your own directory before installing/building it; do not edit `node_modules`.
42
+ Source code, not repeated prose fragments, is the complete runnable example.
43
+
44
+ Each `package.json` points at the root declaration and retains it in the artifact:
45
+
46
+ ```json
47
+ {
48
+ "files": ["dist", "service-access.json", "config-example.json"],
49
+ "unsDatahub": {
50
+ "schemaVersion": 1,
51
+ "kind": "addon",
52
+ "controllerCompatibility": ">=2 <3",
53
+ "serviceAccess": "service-access.json"
54
+ }
55
+ }
56
+ ```
57
+
58
+ This is metadata to merge into the application's existing package.json. Keep the
59
+ real name/version/dependencies/scripts. The SDK is imported as published packages;
60
+ no copied validators, source overlays or application-name Controller branches.
61
+ Do not declare the historical `catalogAccess` marker alongside `serviceAccess`.
62
+
63
+ In either copied example:
64
+
65
+ ```sh
66
+ pnpm install
67
+ pnpm run build
68
+ cp config-example.json config.json
69
+ ```
70
+
71
+ Select your local MQTT/Controller endpoints and use environment or secret-provider
72
+ references for credentials. Never commit config.json, .env, keys or tokens. Under
73
+ a Controller deployment, instance configuration is managed by that Controller.
74
+ The examples declare ordinary cold operation, not state-preserving hot handover.
75
+
76
+ ## 1. Background service: no Catalog and no HTTP listener
77
+
78
+ The background reader asks for one source through the OpenHub history API:
79
+
80
+ ```json
81
+ {
82
+ "protocol": 1,
83
+ "permissions": {
84
+ "scopes": ["read:uns"],
85
+ "accessRules": ["example/background/line-1/equipment/main/temperature"]
86
+ }
87
+ }
88
+ ```
89
+
90
+ There is no `publications` section and no human caller. The complete
91
+ `background-reader/src/reader.mjs` imports:
92
+
93
+ ```js
94
+ import { ServiceTokenProvider } from '@uns-kit/core/tools/auth/service-token-provider.js';
95
+ const tokens = new ServiceTokenProvider();
96
+ // Inside each request, not once at application startup:
97
+ const token = await tokens.getAccessToken();
98
+ ```
99
+
100
+ It sends the token as `Authorization: Bearer …` to
101
+ `/api/catchall/batch/range` at `UNS_CONTROLLER_PUBLIC_BASE`, with a fixed declared
102
+ source, explicit from/to, a row limit, timeout and no redirects. Missing credentials
103
+ or 401/403 fail the request; no retry with a human or more powerful credential.
104
+ `src/index.mjs` schedules reads only while the runtime is active. Shutdown stops
105
+ admission, aborts the pending read and cleans up the runtime on SIGINT/SIGTERM.
106
+
107
+ Administrator steps after deployment:
108
+
109
+ 1. Open **Service access** for the exact installed example release and instance.
110
+ 2. Use **Restore defaults** to populate the draft, or select `read:uns` and the
111
+ exact declared path manually. Defaults do not save or grant anything.
112
+ 3. Review **Permitted UNS paths**, then **Apply access policy** and confirm.
113
+ Only declared paths or narrower filters are allowed; do not replace them with `#`.
114
+ 4. **Start** the prepared instance. First managed launch requires reviewed access.
115
+ There is no catalog publication to approve.
116
+
117
+ The Controller supplies `UNS_SERVICE_TOKEN_FILE` and runtime endpoint/identity
118
+ metadata. `ServiceTokenProvider` rereads that file for every request, so atomic
119
+ credential rotation is visible without an application restart. When the file is
120
+ configured it is authoritative: missing/empty/unreadable means failure, not fallback.
121
+ Do not cache the returned token for the lifetime of the process.
122
+
123
+ Acceptance on your local stack: create some history for the declared example
124
+ source. Expect 200 for that source and 403 for another source with the same service
125
+ credential. Verify a policy narrowing or revocation takes effect without granting
126
+ additional paths. Zero rows is a valid response if the selected window has no data.
127
+
128
+ ## 2. Catalog: a user requests a derived result
129
+
130
+ The catalog summary has empty direct `permissions.scopes` and `accessRules`. Its
131
+ optional `publications` declaration defines `sample-count`, GET JSON, from/to with
132
+ a timezone, at most one hour/1000 rows from
133
+ `example/private/line-1/equipment/main/temperature`, and a closed result containing
134
+ only `{count, hasMore}`. The response cap is 4096 bytes. See its complete
135
+ `service-access.json`; the source and response schema are reviewed data.
136
+
137
+ `catalog-summary/src/index.mjs` reads and validates that declaration with
138
+ `parseServiceAccessDeclaration`, then constructs `CatalogPublicationClient` using
139
+ the real Controller-injected `RTT_NODE`, `RTT_INSTANCE_ID`, `version` and
140
+ `UNS_CONTROLLER_PUBLIC_BASE`. It configures JWT/JWKS authentication, calls
141
+ `api.registerCatalogRoute()` for the signed installed-launch route lease, and
142
+ registers the offer with `registerApiCatalog`. Do not manufacture these launch
143
+ variables to make a directly started process appear Controller-managed.
144
+
145
+ `src/offer.mjs` wraps its business transform with the actual SDK helper:
146
+
147
+ ```js
148
+ catalogPublicationHandler(client, 'sample-count', async ({ history }) => {
149
+ const rows = await history('temperature');
150
+ return { count: rows.data.length, hasMore: rows.truncated };
151
+ });
152
+ ```
153
+
154
+ `temperature` is the declared source ID, not an arbitrary topic or URL from the
155
+ request. The complete source also validates the source response. The helper takes
156
+ the caller's bearer from the incoming HTTP request. The client separately reads
157
+ the service token through `ServiceTokenProvider`; they are never substituted for
158
+ each other. The business transform owns the count, not the authorization policy.
159
+
160
+ Administrator steps after preparation:
161
+
162
+ 1. Apply only `catalog:sources:read` in **Service access**, with **no direct UNS
163
+ paths**. This capability permits the reviewed catalog workflow, not raw reads.
164
+ 2. Expand **Published operations → Review access**. Check the exact source,
165
+ maximum interval/rows, **Published fields** and intended audience.
166
+ 3. **Review publication**, then **Approve publication**. A selected audience role
167
+ does not itself grant every user access to the catalog operation.
168
+ 4. Give a test reader access to the operation's SDK resource path:
169
+ `example/public/line-1/equipment/dataset/sample-count/json` (without `/api/`).
170
+ Do not grant the private source path just to make Execute work.
171
+ 5. Start the prepared instance and Execute the operation as that reader, for
172
+ example `from=2026-01-01T00:00:00Z` and `to=2026-01-01T01:00:00Z`.
173
+
174
+ When moving an existing service from legacy credentials to this declaration,
175
+ prepare the new version first. Use **Stop → review Service access/publication →
176
+ Start prepared version** on the chosen owner. A still-live older version can
177
+ block inspection of the prepared release; the same-instance Apply guard requires
178
+ this first-adoption sequence. Retain the old configuration for controlled rollback
179
+ until acceptance; do not grant raw access to get past an adoption error.
180
+
181
+ The request path is:
182
+
183
+ 1. Browser sends the user's token to the service API through the Controller.
184
+ 2. The SDK sends its service credential and the caller context to the Controller.
185
+ 3. Controller checks current user operation access, audience, active service
186
+ identity/generation, live launch, loaded configuration, declaration and approval.
187
+ 4. Controller obtains bounded source data using a short-lived exact-source
188
+ credential internally. It never sends that credential to the service/browser.
189
+ 5. The service transforms the data; SDK rechecks authority after the asynchronous
190
+ work and validates the closed output schema before returning JSON.
191
+
192
+ Only SDK-integrated execution gets that output validation. Preserve ordinary
193
+ endpoint JWT/JWKS guards; a custom endpoint or custom download must use the same
194
+ mediated path. Arbitrary app code with independent DB/MQTT credentials is outside
195
+ this enforcement. Revocation cannot erase bytes already delivered.
196
+
197
+ Acceptance: reader Execute 200/{count,hasMore}; caller and service direct source
198
+ reads 403; unapproved/revoked publication 403; invalid window/unknown parameter
199
+ 400; no raw source fields or credentials in result/logs. The caller needs no
200
+ source permission in `approved-service` mode. `caller` mode instead requires the
201
+ caller's own source permissions as well; it never retries with stronger authority.
202
+
203
+ ## Contract limits
204
+
205
+ The publication protocol supports GET JSON, string query parameters, exact history
206
+ topics and closed primitive/object/array schemas. Schema references, defaults and
207
+ composition, wildcard source topics, arbitrary URLs/SQL and undeclared inputs are
208
+ rejected. Source path segments use ASCII letters, digits and hyphens. Each source
209
+ binds the required timezone-qualified `from`/`to`, at most 31 days and 27000 rows.
210
+ The broker caps source responses at 1 MiB; the separately declared output cap is
211
+ also at most 1 MiB. The example intentionally chooses narrower limits.
212
+
213
+ A combined service can declare background `read:uns` paths and `publications` in
214
+ the same envelope. Review both separately. A publication source never becomes a
215
+ directly readable background path just because it appears in the manifest.
216
+ Unknown or administrative service scopes are rejected.
217
+
218
+ Ordinary credential renewal and Controller restart preserve a confirmed live
219
+ launch binding. They do not approve a different identity, declaration, policy,
220
+ configuration or launch. Missing or changed evidence requires reviewed recovery;
221
+ an expired bearer remains invalid.
222
+
223
+ ## Troubleshooting and agent checklist
224
+
225
+ | Symptom | Check |
226
+ | --- | --- |
227
+ | Start requires access review | Exact installed release and `unsDatahub.serviceAccess`; admin must apply the policy |
228
+ | Credential file missing | Real Controller launch/file access; never copy a human token into config |
229
+ | Background read 403 | Active workload identity, `read:uns`, matching permitted path, current generation |
230
+ | Catalog 403 | Caller operation access, audience, service capability and current publication approval |
231
+ | Placement/access changed | Current owner/version/loaded config; stop the legacy owner for first adoption and review the prepared release |
232
+ | API works, MQTT denied | Broker credentials/ACLs; API scopes do not create broker access |
233
+ | Configuration changed | Review the new configuration/contract and restart as required; token refresh cannot replace loaded-launch evidence |
234
+
235
+ For an agent generating a service: choose the model above, use the published SDK
236
+ imports, ship/validate the declaration, preserve local cleanup, and list the admin
237
+ review needed. Do not create identities, modify user rules or broker ACLs, disable
238
+ JWT checks, synthesize launch metadata, or broaden paths as an automatic fix.
239
+ Keep install/build, review, start, release and production deployment separate.
240
+
241
+ Maintainers run `pnpm run ts:build` and `pnpm run test:service-access-examples`.
242
+ The latter verifies packaged imports/assets, rotating token use, failures and the
243
+ real catalog HTTP helper using synthetic loopback endpoints. It does not certify
244
+ real MQTT ACLs, Controller RBAC/cluster ownership, or production resource grants.
245
+ Additional SQL, filesystem, broker and external-resource permission adapters are
246
+ outside this protocol. Follow the version-bounded core MIGRATIONS.md on adoption.
@@ -0,0 +1,23 @@
1
+ import { type CatalogPublicationManifest } from "./catalog-publication.js";
2
+ export declare const SERVICE_ACCESS_PROTOCOL = 1;
3
+ export declare const SERVICE_ACCESS_MANIFEST_FIELD = "serviceAccess";
4
+ export declare const SERVICE_RESOURCE_SCOPES: readonly ["read:uns", "identity:asset:publish", "runtime:captures:read", "runtime:captures:write", "runtime:triggers:read"];
5
+ export type ServiceAccessDeclaration = {
6
+ protocol: 1;
7
+ permissions: {
8
+ scopes: string[];
9
+ accessRules: string[];
10
+ };
11
+ publications?: CatalogPublicationManifest;
12
+ };
13
+ export declare function parseServiceAccessDeclaration(value: unknown): ServiceAccessDeclaration;
14
+ export declare const serviceAccessDigest: (value: unknown) => string;
15
+ /** Publication source rules are deliberately not raw UNS permissions. A service
16
+ * combining a background reader with a catalog must not gain their cross-product. */
17
+ export declare function serviceAccessCeiling(declaration: ServiceAccessDeclaration): {
18
+ scopes: string[];
19
+ accessRules: string[];
20
+ };
21
+ /** Conservative MQTT filter inclusion, not matching a filter as if it were data. */
22
+ export declare function serviceRuleWithin(requested: string, allowed: string): boolean;
23
+ //# sourceMappingURL=service-access.d.ts.map