@uns-kit/core 3.0.47 → 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.
- package/MIGRATIONS.md +10 -0
- package/README.md +24 -22
- package/dist/runtime/service-access-examples/README.md +20 -0
- package/dist/runtime/service-access-examples/background-reader/README.md +25 -0
- package/dist/runtime/service-access-examples/background-reader/build.mjs +7 -0
- package/dist/runtime/service-access-examples/background-reader/config-example.json +22 -0
- package/dist/runtime/service-access-examples/background-reader/package.json +28 -0
- package/dist/runtime/service-access-examples/background-reader/service-access.json +11 -0
- package/dist/runtime/service-access-examples/background-reader/src/index.mjs +55 -0
- package/dist/runtime/service-access-examples/background-reader/src/reader.mjs +21 -0
- package/dist/runtime/service-access-examples/catalog-summary/README.md +26 -0
- package/dist/runtime/service-access-examples/catalog-summary/build.mjs +7 -0
- package/dist/runtime/service-access-examples/catalog-summary/config-example.json +23 -0
- package/dist/runtime/service-access-examples/catalog-summary/package.json +29 -0
- package/dist/runtime/service-access-examples/catalog-summary/service-access.json +65 -0
- package/dist/runtime/service-access-examples/catalog-summary/src/index.mjs +50 -0
- package/dist/runtime/service-access-examples/catalog-summary/src/offer.mjs +35 -0
- package/dist/runtime/service-access-guide.md +228 -112
- package/package.json +1 -1
package/MIGRATIONS.md
CHANGED
|
@@ -9,6 +9,16 @@ 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.48 - Checked Service access documentation and examples
|
|
13
|
+
|
|
14
|
+
This patch clarifies existing 3.0.47 contracts; no new permission mode or automatic
|
|
15
|
+
application migration is introduced. Follow the
|
|
16
|
+
[source walkthrough](https://github.com/uns-openhub/uns-kit/blob/master/docs/catalog-publication-access.md)
|
|
17
|
+
or bundled `@uns-kit/core/runtime/service-access-guide.md` and complete projects in
|
|
18
|
+
`dist/runtime/service-access-examples/`. Examples use a rotating token provider;
|
|
19
|
+
background API reads, caller-mediated catalogs and MQTT ACLs are separate.
|
|
20
|
+
An application adopting the capability must still follow the 3.0.47 entry below.
|
|
21
|
+
|
|
12
22
|
## 3.0.47 - Common Service access and publications
|
|
13
23
|
|
|
14
24
|
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/
|
|
12
|
-
| [`@uns-kit/api`](https://github.com/uns-openhub/uns-kit/tree/
|
|
13
|
-
| [`@uns-kit/cron`](https://github.com/uns-openhub/uns-kit/tree/
|
|
14
|
-
| [`@uns-kit/cli`](https://github.com/uns-openhub/uns-kit/tree/
|
|
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.
|
|
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. Review the service's declared scopes and resource paths before Start; never cache a managed token at application startup.
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
106
|
+
```ts
|
|
107
|
+
import { ServiceTokenProvider, UnsClient } from "@uns-kit/core";
|
|
105
108
|
|
|
106
|
-
const
|
|
107
|
-
|
|
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
|
-
"
|
|
112
|
-
"
|
|
114
|
+
"example/plant/line-1/motor/main/temperature",
|
|
115
|
+
"example/plant/line-1/motor/main/status",
|
|
113
116
|
]);
|
|
114
|
-
|
|
117
|
+
// Process the values without logging private records.
|
|
115
118
|
|
|
116
|
-
const data = await client.getAttributeData("
|
|
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
|
-
|
|
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
|
-
|
|
130
|
+
const customResult = await customData.json();
|
|
129
131
|
|
|
130
132
|
const batchHistory = await client.history([
|
|
131
|
-
"
|
|
132
|
-
"
|
|
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
|
-
|
|
140
|
+
// Process the bounded batch result.
|
|
139
141
|
```
|
|
140
142
|
|
|
141
143
|
## Sub-Asset Publishing
|
|
@@ -0,0 +1,20 @@
|
|
|
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. Deploy/review/start with a
|
|
8
|
+
Controller supporting common Service access (verified with Controller 2.2.95).
|
|
9
|
+
The [walkthrough](https://github.com/uns-openhub/uns-kit/blob/master/docs/catalog-publication-access.md)
|
|
10
|
+
explains what the developer, administrator, Controller, SDK and caller each own.
|
|
11
|
+
|
|
12
|
+
The examples are also shipped in `@uns-kit/core/dist/runtime/service-access-examples/`.
|
|
13
|
+
No real broker, database, user or production endpoint is supplied. Configure your
|
|
14
|
+
own local test resources. These files create no identities, grants or MQTT ACLs.
|
|
15
|
+
|
|
16
|
+
Maintainers: run `pnpm run ts:build` then `pnpm run test:service-access-examples`.
|
|
17
|
+
The verifier packs the SDK, installs it in a temporary consumer, builds/packs both
|
|
18
|
+
examples, and exercises their real handlers/token provider against a synthetic
|
|
19
|
+
loopback API. This checks imports, shipped files, token rotation, denial and
|
|
20
|
+
closed output, not real Controller policy, MQTT connectivity or cluster ownership.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# background-reader
|
|
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 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.
|
|
@@ -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,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
|
+
}
|
|
@@ -1,130 +1,246 @@
|
|
|
1
|
-
# Service access
|
|
1
|
+
# Service access: from declaration to a running service
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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.
|
|
10
12
|
|
|
11
|
-
##
|
|
13
|
+
## Choose the access model
|
|
12
14
|
|
|
13
|
-
|
|
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:
|
|
14
45
|
|
|
15
46
|
```json
|
|
16
|
-
{
|
|
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
|
|
17
69
|
```
|
|
18
70
|
|
|
19
|
-
|
|
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:
|
|
20
79
|
|
|
21
80
|
```json
|
|
22
81
|
{
|
|
23
82
|
"protocol": 1,
|
|
24
83
|
"permissions": {
|
|
25
84
|
"scopes": ["read:uns"],
|
|
26
|
-
"accessRules": ["example/background
|
|
85
|
+
"accessRules": ["example/background/line-1/equipment/main/temperature"]
|
|
27
86
|
}
|
|
28
87
|
}
|
|
29
88
|
```
|
|
30
89
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
`
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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.
|