@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
@@ -1,130 +1,277 @@
1
- # Service access and publications (3.0.47)
1
+ # Service access: from declaration to a running service
2
2
 
3
- This opt-in protocol separates permission to call a catalog operation from
4
- permission to read its source. It is not a sandbox for arbitrary application
5
- code or a substitute for restricted database and MQTT identities.
3
+ For application developers and coding agents using **SDK 3.0.49 or later**.
4
+ Common Service access also requires a matching Controller; these workflows were
5
+ originally verified with Controller **2.2.95** using file declarations. Inline
6
+ package declarations require a Controller advertising `rttServiceAccessInline`.
7
+ Controller 2.2.96 does not support inline declarations. SDK installation alone
8
+ enables no host rights. The configuration-save approval UI is a separate planned
9
+ Controller change; the approval steps below describe the existing released UI.
6
10
 
7
- Package metadata requests authority; it never grants it. Keep existing JWT/JWKS
8
- endpoint guards. Do not forward caller tokens to privileged sources or retry a
9
- denied caller read with stronger credentials.
11
+ The developer declares a permission ceiling and uses the SDK. An administrator
12
+ reviews the installed release in **Micro services → instance → Service access**.
13
+ The Controller stores that decision, issues the instance credential and supplies
14
+ its file path. The SDK reads the current credential when making a request. The UI
15
+ is a policy editor, not a place to copy a bearer token into application code.
10
16
 
11
- ## Declaration
17
+ ## Choose the access model
12
18
 
13
- Ship a bounded JSON file with the installed package, and retain it in the build:
19
+ | Application activity | Credential used | Administrator approves | Mechanism |
20
+ | --- | --- | --- | --- |
21
+ | Background UNS API read | Service token only | `read:uns` and explicit UNS paths | `ServiceTokenProvider` / API client |
22
+ | 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` |
23
+ | Direct MQTT subscribe/publish | Broker credentials | Broker ACLs for the relevant topics | MQTT connection settings and broker authorization |
24
+ | Direct SQL/external API access | That resource's credential | Database/external-system grants | Application adapter and secret references |
25
+
26
+ **`read:uns` protects OpenHub API reads. It does not install MQTT ACLs.**
27
+ MQTT-style filters in Service access describe API resource paths; their syntax
28
+ does not make the HTTP service token a broker password. `infra.host` and the
29
+ example's `mqtt.username/password` concern runtime MQTT connectivity. Configure
30
+ broker ACLs separately, including the runtime signaling topics your deployment
31
+ requires. A direct database connection is governed by its database identity.
32
+ This capability is not an arbitrary-code sandbox.
33
+
34
+ ## Runnable examples and package contents
35
+
36
+ Both examples are independent Node.js 22+ projects with complete source,
37
+ credential-free configuration, a declaration, build script and package metadata:
38
+
39
+ - [Background reader](https://github.com/uns-openhub/uns-kit/tree/master/examples/service-access/background-reader)
40
+ - [Catalog summary](https://github.com/uns-openhub/uns-kit/tree/master/examples/service-access/catalog-summary)
41
+
42
+ The npm core package also ships this guide at
43
+ `@uns-kit/core/runtime/service-access-guide.md` and both projects under
44
+ `node_modules/@uns-kit/core/dist/runtime/service-access-examples/`. Copy a project
45
+ to your own directory before installing/building it; do not edit `node_modules`.
46
+ Source code, not repeated prose fragments, is the complete runnable example.
47
+
48
+ For new services, put the declaration directly in `package.json`:
14
49
 
15
50
  ```json
16
- {"unsDatahub":{"serviceAccess":"service-access.json"}}
51
+ {
52
+ "files": ["dist", "config-example.json"],
53
+ "unsDatahub": {
54
+ "schemaVersion": 1,
55
+ "kind": "addon",
56
+ "controllerCompatibility": ">=2 <3",
57
+ "serviceAccess": {
58
+ "protocol": 1,
59
+ "permissions": {
60
+ "scopes": ["read:uns"],
61
+ "accessRules": ["example/background/line-1/equipment/main/temperature"]
62
+ }
63
+ }
64
+ }
65
+ }
17
66
  ```
18
67
 
19
- Use `parseServiceAccessDeclaration` from `@uns-kit/core/runtime/service-access.js`:
68
+ This is metadata to merge into the application's existing package.json. Keep the
69
+ real name/version/dependencies/scripts. The SDK is imported as published packages;
70
+ no copied validators, source overlays or application-name Controller branches.
71
+ Do not declare the historical `catalogAccess` marker alongside `serviceAccess`.
72
+
73
+ Existing `"serviceAccess": "service-access.json"` references remain supported.
74
+ Retain that file in `files` when using the file form, including for Controllers
75
+ without inline support. Use one representation; the normalized declaration and
76
+ its digest are identical when the content is identical. Neither form contains
77
+ tokens or an administrator's approval.
78
+
79
+ Runtime code can read the same definition without duplicating it:
80
+
81
+ ```js
82
+ import { loadServiceAccessDeclaration } from '@uns-kit/core/runtime/service-access-package.js';
83
+ const declaration = await loadServiceAccessDeclaration(new URL('../package.json', import.meta.url));
84
+ if (!declaration) throw new Error('Declare the service requirements in package.json.');
85
+ ```
86
+
87
+ This example path is for an entry point directly inside `dist/`. The loader
88
+ reads bounded JSON data; it never imports application code. File references are
89
+ relative to the package, with traversal/symlink escapes rejected. Each JSON file,
90
+ including package.json, is limited to 64 KiB. Host integrations can use the pure
91
+ `resolveServiceAccessDeclaration(packageData, readDeclaration)` with their own
92
+ bounded reader and the exported `serviceAccessPackageReferenceSchema`.
93
+
94
+ In either copied example:
95
+
96
+ ```sh
97
+ pnpm install
98
+ pnpm run build
99
+ cp config-example.json config.json
100
+ ```
101
+
102
+ Select your local MQTT/Controller endpoints and use environment or secret-provider
103
+ references for credentials. Never commit config.json, .env, keys or tokens. Under
104
+ a Controller deployment, instance configuration is managed by that Controller.
105
+ The examples declare ordinary cold operation, not state-preserving hot handover.
106
+
107
+ ## 1. Background service: no Catalog and no HTTP listener
108
+
109
+ The background reader asks for one source through the OpenHub history API:
20
110
 
21
111
  ```json
22
112
  {
23
113
  "protocol": 1,
24
114
  "permissions": {
25
115
  "scopes": ["read:uns"],
26
- "accessRules": ["example/background/#"]
116
+ "accessRules": ["example/background/line-1/equipment/main/temperature"]
27
117
  }
28
118
  }
29
119
  ```
30
120
 
31
- This works without a catalog, HTTP endpoint, or human requester. The declaration
32
- requests a ceiling; an administrator must apply the effective policy under
33
- **Microservices → instance → Service access** before the service can start with
34
- a managed identity. Review the exact installed version and declaration digest.
35
- The administrator can select fewer capabilities and narrower MQTT path filters;
36
- unknown or administrative scopes are rejected. The identity, revocation and
37
- same-request reissue mechanisms are the existing managed workload mechanisms.
38
- UNS resource rules bound direct UNS reads; other recognized runtime scopes retain
39
- their existing authorization semantics. This protocol does not create new
40
- SQL, broker, filesystem or arbitrary external-service permissions.
41
-
42
- A combined service adds optional `publications`, containing the manifest parsed
43
- by `parseCatalogPublicationManifest`, to the same envelope. A publication-only
44
- service uses empty `permissions.scopes` and `permissions.accessRules`. Its
45
- managed token then has `catalog:sources:read` and **no raw UNS paths**. Adding
46
- publication sources never adds them to the service's direct `read:uns` rules.
47
- Publication approval is a separate operation/audience grant and never silently
48
- grants other package-requested capabilities. Review both in Service access.
49
- The Catalog contains execution/status and an administrator link to that editor.
50
- There is one writable publication editor.
51
-
52
- The earlier unpublished `unsDatahub.catalogAccess` marker remains an isolated
53
- compatibility adapter for the first local fixtures. New consumers use
54
- `serviceAccess`; declaring both markers is rejected. No released applications
55
- are automatically migrated.
56
-
57
- The optional publication manifest supports GET JSON,
58
- closed JSON schemas (primitive, object and array types; no references, defaults,
59
- or composition), string query parameters, and exact history source topics.
60
- Source path segments follow the existing managed-identity resource vocabulary:
61
- ASCII letters, digits and hyphens. Each source binds required `from`/`to` date-time parameters, a maximum window
62
- (at most 31 days) and row count (at most 27000). Dates require a UTC offset.
63
- Wildcard topics, arbitrary URLs/SQL/source selectors and undeclared input fields
64
- are rejected. The broker caps source responses at 1 MiB; published responses
65
- have a separately declared cap of at most 1 MiB.
66
-
67
- Each operation declares `caller` or `approved-service`. Both require an explicit
68
- administrator publication review and current caller permission on the SDK UNS
69
- resource path (the operation path without `/api/`). In caller mode, source rules
70
- and `read:uns` must also belong to the caller. Service mode never grants these
71
- raw permissions to the caller.
72
-
73
- ## Runtime
74
-
75
- Use `CatalogPublicationClient` with the installed, normalized manifest and the
76
- Controller-provided runtime identity (`RTT_NODE`, `version`, `RTT_INSTANCE_ID`,
77
- `UNS_CONTROLLER_PUBLIC_BASE`). The default `ServiceTokenProvider` reads the
78
- Controller's rotating token file. There is no caller-token fallback.
79
-
80
- Use `catalogPublicationHandler(client, operationId, transform)` in an existing
81
- `registerApiCatalog` handler. Call `api.registerCatalogRoute()` to opt into the
82
- existing signed launch route lease. The transform receives `history(sourceId)`
83
- and the already validated input. It owns the derived result, not authorization.
84
- The HTTP helper sets `Cache-Control: no-store` and rejects undeclared result
85
- fields. Each execution rechecks current authority after asynchronous work,
86
- including cached application results. Custom downloads and other handlers must
87
- use the same mediated path; this candidate adds no persistent result cache.
88
-
89
- A publication-only workload token has `catalog:sources:read` and no direct
90
- `read:uns` authority. A combined service may separately receive `read:uns` on
91
- its reviewed background paths, without receiving raw access to publication
92
- sources. The Controller broker checks current user policy, active machine
93
- identity/token generation, reviewed identity binding, launch, loaded portable
94
- configuration, publication revision, audience and source bounds. Its internal
95
- history token is exact-topic, lasts 15 seconds, and never leaves the broker.
96
- Neither token bytes nor raw records belong in logs, review receipts or audit.
97
-
98
- Two local acceptance consumers (energy intervals and quality sample counts) use
99
- the same helper and Controller path. No application-name branch is needed.
100
-
101
- ## Adoption boundary
102
-
103
- The SDK contract is introduced in 3.0.47. A matching Controller implementation
104
- must also be installed; an SDK upgrade alone does not enable the host capability.
105
- Existing applications do not change mode
106
- implicitly. Install the shared SDK release first, update consumer lockfiles,
107
- then review the matching installed declaration through the Controller.
108
- Controller publication is bound to the installed declaration and portable
109
- configuration; edits require review. A changed loaded configuration requires a
110
- new launch, not token refresh. With the matching Controller candidate, the
111
- signed digest of the same confirmed live launch survives ordinary credential
112
- expiry and Controller restart. Current identity, policy, generation and approval
113
- are still checked; expired bearer tokens remain invalid. Missing, corrupt,
114
- foreign or changed-launch evidence requires explicit reviewed recovery.
115
-
116
-
117
- ## Local common-contract acceptance
118
-
119
- An unrelated background history processor, with no API or Catalog offer, was
120
- reviewed, narrowed live, revoked, reissued with the same identity and cold-moved
121
- to another Controller. It retained the same policy and generation across the
122
- move. A combined service read its separate background source directly, while a
123
- catalog-only user consumed a derived result from a different approved source.
124
- Both the user and workload were denied direct access to that publication source.
125
- A publication-only empty raw policy stayed empty in storage and actual tokens.
126
-
127
- These are local candidate checks, including clean packed-SDK installs. They are
128
- not production, arbitrary-code isolation or external-resource adapter acceptance.
129
- SDK publication and downstream frozen dependency updates
130
- remain separate steps.
121
+ There is no `publications` section and no human caller. The complete
122
+ `background-reader/src/reader.mjs` imports:
123
+
124
+ ```js
125
+ import { ServiceTokenProvider } from '@uns-kit/core/tools/auth/service-token-provider.js';
126
+ const tokens = new ServiceTokenProvider();
127
+ // Inside each request, not once at application startup:
128
+ const token = await tokens.getAccessToken();
129
+ ```
130
+
131
+ It sends the token as `Authorization: Bearer …` to
132
+ `/api/catchall/batch/range` at `UNS_CONTROLLER_PUBLIC_BASE`, with a fixed declared
133
+ source, explicit from/to, a row limit, timeout and no redirects. Missing credentials
134
+ or 401/403 fail the request; no retry with a human or more powerful credential.
135
+ `src/index.mjs` schedules reads only while the runtime is active. Shutdown stops
136
+ admission, aborts the pending read and cleans up the runtime on SIGINT/SIGTERM.
137
+
138
+ Administrator steps after deployment:
139
+
140
+ 1. Open **Service access** for the exact installed example release and instance.
141
+ 2. Use **Restore defaults** to populate the draft, or select `read:uns` and the
142
+ exact declared path manually. Defaults do not save or grant anything.
143
+ 3. Review **Permitted UNS paths**, then **Apply access policy** and confirm.
144
+ Only declared paths or narrower filters are allowed; do not replace them with `#`.
145
+ 4. **Start** the prepared instance. First managed launch requires reviewed access.
146
+ There is no catalog publication to approve.
147
+
148
+ The Controller supplies `UNS_SERVICE_TOKEN_FILE` and runtime endpoint/identity
149
+ metadata. `ServiceTokenProvider` rereads that file for every request, so atomic
150
+ credential rotation is visible without an application restart. When the file is
151
+ configured it is authoritative: missing/empty/unreadable means failure, not fallback.
152
+ Do not cache the returned token for the lifetime of the process.
153
+
154
+ Acceptance on your local stack: create some history for the declared example
155
+ source. Expect 200 for that source and 403 for another source with the same service
156
+ credential. Verify a policy narrowing or revocation takes effect without granting
157
+ additional paths. Zero rows is a valid response if the selected window has no data.
158
+
159
+ ## 2. Catalog: a user requests a derived result
160
+
161
+ The catalog summary has empty direct `permissions.scopes` and `accessRules`. Its
162
+ optional `publications` declaration defines `sample-count`, GET JSON, from/to with
163
+ a timezone, at most one hour/1000 rows from
164
+ `example/private/line-1/equipment/main/temperature`, and a closed result containing
165
+ only `{count, hasMore}`. The response cap is 4096 bytes. See its complete
166
+ `package.json.unsDatahub.serviceAccess`; the source and response schema are reviewed data.
167
+
168
+ `catalog-summary/src/index.mjs` reads and validates that declaration with
169
+ `loadServiceAccessDeclaration`, then constructs `CatalogPublicationClient` using
170
+ the real Controller-injected `RTT_NODE`, `RTT_INSTANCE_ID`, `version` and
171
+ `UNS_CONTROLLER_PUBLIC_BASE`. It configures JWT/JWKS authentication, calls
172
+ `api.registerCatalogRoute()` for the signed installed-launch route lease, and
173
+ registers the offer with `registerApiCatalog`. Do not manufacture these launch
174
+ variables to make a directly started process appear Controller-managed.
175
+
176
+ `src/offer.mjs` wraps its business transform with the actual SDK helper:
177
+
178
+ ```js
179
+ catalogPublicationHandler(client, 'sample-count', async ({ history }) => {
180
+ const rows = await history('temperature');
181
+ return { count: rows.data.length, hasMore: rows.truncated };
182
+ });
183
+ ```
184
+
185
+ `temperature` is the declared source ID, not an arbitrary topic or URL from the
186
+ request. The complete source also validates the source response. The helper takes
187
+ the caller's bearer from the incoming HTTP request. The client separately reads
188
+ the service token through `ServiceTokenProvider`; they are never substituted for
189
+ each other. The business transform owns the count, not the authorization policy.
190
+
191
+ Administrator steps after preparation:
192
+
193
+ 1. Apply only `catalog:sources:read` in **Service access**, with **no direct UNS
194
+ paths**. This capability permits the reviewed catalog workflow, not raw reads.
195
+ 2. Expand **Published operations → Review access**. Check the exact source,
196
+ maximum interval/rows, **Published fields** and intended audience.
197
+ 3. **Review publication**, then **Approve publication**. A selected audience role
198
+ does not itself grant every user access to the catalog operation.
199
+ 4. Give a test reader access to the operation's SDK resource path:
200
+ `example/public/line-1/equipment/dataset/sample-count/json` (without `/api/`).
201
+ Do not grant the private source path just to make Execute work.
202
+ 5. Start the prepared instance and Execute the operation as that reader, for
203
+ example `from=2026-01-01T00:00:00Z` and `to=2026-01-01T01:00:00Z`.
204
+
205
+ When moving an existing service from legacy credentials to this declaration,
206
+ prepare the new version first. Use **Stop → review Service access/publication →
207
+ Start prepared version** on the chosen owner. A still-live older version can
208
+ block inspection of the prepared release; the same-instance Apply guard requires
209
+ this first-adoption sequence. Retain the old configuration for controlled rollback
210
+ until acceptance; do not grant raw access to get past an adoption error.
211
+
212
+ The request path is:
213
+
214
+ 1. Browser sends the user's token to the service API through the Controller.
215
+ 2. The SDK sends its service credential and the caller context to the Controller.
216
+ 3. Controller checks current user operation access, audience, active service
217
+ identity/generation, live launch, loaded configuration, declaration and approval.
218
+ 4. Controller obtains bounded source data using a short-lived exact-source
219
+ credential internally. It never sends that credential to the service/browser.
220
+ 5. The service transforms the data; SDK rechecks authority after the asynchronous
221
+ work and validates the closed output schema before returning JSON.
222
+
223
+ Only SDK-integrated execution gets that output validation. Preserve ordinary
224
+ endpoint JWT/JWKS guards; a custom endpoint or custom download must use the same
225
+ mediated path. Arbitrary app code with independent DB/MQTT credentials is outside
226
+ this enforcement. Revocation cannot erase bytes already delivered.
227
+
228
+ Acceptance: reader Execute 200/{count,hasMore}; caller and service direct source
229
+ reads 403; unapproved/revoked publication 403; invalid window/unknown parameter
230
+ 400; no raw source fields or credentials in result/logs. The caller needs no
231
+ source permission in `approved-service` mode. `caller` mode instead requires the
232
+ caller's own source permissions as well; it never retries with stronger authority.
233
+
234
+ ## Contract limits
235
+
236
+ The publication protocol supports GET JSON, string query parameters, exact history
237
+ topics and closed primitive/object/array schemas. Schema references, defaults and
238
+ composition, wildcard source topics, arbitrary URLs/SQL and undeclared inputs are
239
+ rejected. Source path segments use ASCII letters, digits and hyphens. Each source
240
+ binds the required timezone-qualified `from`/`to`, at most 31 days and 27000 rows.
241
+ The broker caps source responses at 1 MiB; the separately declared output cap is
242
+ also at most 1 MiB. The example intentionally chooses narrower limits.
243
+
244
+ A combined service can declare background `read:uns` paths and `publications` in
245
+ the same envelope. Review both separately. A publication source never becomes a
246
+ directly readable background path just because it appears in the manifest.
247
+ Unknown or administrative service scopes are rejected.
248
+
249
+ Ordinary credential renewal and Controller restart preserve a confirmed live
250
+ launch binding. They do not approve a different identity, declaration, policy,
251
+ configuration or launch. Missing or changed evidence requires reviewed recovery;
252
+ an expired bearer remains invalid.
253
+
254
+ ## Troubleshooting and agent checklist
255
+
256
+ | Symptom | Check |
257
+ | --- | --- |
258
+ | Start requires access review | Exact installed release and `unsDatahub.serviceAccess`; admin must apply the policy |
259
+ | Credential file missing | Real Controller launch/file access; never copy a human token into config |
260
+ | Background read 403 | Active workload identity, `read:uns`, matching permitted path, current generation |
261
+ | Catalog 403 | Caller operation access, audience, service capability and current publication approval |
262
+ | Placement/access changed | Current owner/version/loaded config; stop the legacy owner for first adoption and review the prepared release |
263
+ | API works, MQTT denied | Broker credentials/ACLs; API scopes do not create broker access |
264
+ | Configuration changed | Review the new configuration/contract and restart as required; token refresh cannot replace loaded-launch evidence |
265
+
266
+ For an agent generating a service: choose the model above, use the published SDK
267
+ imports, ship/validate the declaration, preserve local cleanup, and list the admin
268
+ review needed. Do not create identities, modify user rules or broker ACLs, disable
269
+ JWT checks, synthesize launch metadata, or broaden paths as an automatic fix.
270
+ Keep install/build, review, start, release and production deployment separate.
271
+
272
+ Maintainers run `pnpm run ts:build` and `pnpm run test:service-access-examples`.
273
+ The latter verifies packaged imports/assets, rotating token use, failures and the
274
+ real catalog HTTP helper using synthetic loopback endpoints. It does not certify
275
+ real MQTT ACLs, Controller RBAC/cluster ownership, or production resource grants.
276
+ Additional SQL, filesystem, broker and external-resource permission adapters are
277
+ outside this protocol. Follow the version-bounded core MIGRATIONS.md on adoption.
@@ -0,0 +1,6 @@
1
+ import { type ServiceAccessDeclaration } from "./service-access.js";
2
+ /** Read the application's package data without importing application code.
3
+ * Pass new URL('../package.json', import.meta.url) from a dist entry point.
4
+ * Referenced declarations stay inside that package and cannot traverse symlinks. */
5
+ export declare function loadServiceAccessDeclaration(packageFile: string | URL): Promise<ServiceAccessDeclaration | null>;
6
+ //# sourceMappingURL=service-access-package.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-access-package.d.ts","sourceRoot":"","sources":["../../src/runtime/service-access-package.ts"],"names":[],"mappings":"AAKA,OAAO,EAAmC,KAAK,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAuBrG;;oFAEoF;AACpF,wBAAsB,4BAA4B,CAAC,WAAW,EAAE,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC,wBAAwB,GAAG,IAAI,CAAC,CAWtH"}
@@ -0,0 +1,45 @@
1
+ import { constants } from "node:fs";
2
+ import { lstat, open, realpath } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { resolveServiceAccessDeclaration } from "./service-access.js";
6
+ const MAX_BYTES = 65536;
7
+ async function readJson(file) {
8
+ const handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW);
9
+ try {
10
+ const stat = await handle.stat();
11
+ if (!stat.isFile() || stat.size > MAX_BYTES)
12
+ throw Error("service_access_file_invalid");
13
+ const buffer = Buffer.alloc(MAX_BYTES + 1);
14
+ let size = 0;
15
+ while (size < buffer.length) {
16
+ const { bytesRead } = await handle.read(buffer, size, buffer.length - size, size);
17
+ if (!bytesRead)
18
+ break;
19
+ size += bytesRead;
20
+ }
21
+ if (size > MAX_BYTES)
22
+ throw Error("service_access_file_invalid");
23
+ return JSON.parse(buffer.subarray(0, size).toString("utf8"));
24
+ }
25
+ finally {
26
+ await handle.close();
27
+ }
28
+ }
29
+ /** Read the application's package data without importing application code.
30
+ * Pass new URL('../package.json', import.meta.url) from a dist entry point.
31
+ * Referenced declarations stay inside that package and cannot traverse symlinks. */
32
+ export async function loadServiceAccessDeclaration(packageFile) {
33
+ const file = path.resolve(packageFile instanceof URL ? fileURLToPath(packageFile) : packageFile);
34
+ const root = await realpath(path.dirname(file));
35
+ return resolveServiceAccessDeclaration(await readJson(path.join(root, path.basename(file))), async (relative) => {
36
+ let current = root;
37
+ for (const segment of relative.split("/")) {
38
+ current = path.join(current, segment);
39
+ if ((await lstat(current)).isSymbolicLink())
40
+ throw Error("service_access_file_invalid");
41
+ }
42
+ return readJson(current);
43
+ });
44
+ }
45
+ //# sourceMappingURL=service-access-package.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-access-package.js","sourceRoot":"","sources":["../../src/runtime/service-access-package.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,+BAA+B,EAAiC,MAAM,qBAAqB,CAAC;AAErG,MAAM,SAAS,GAAG,KAAK,CAAC;AAExB,KAAK,UAAU,QAAQ,CAAC,IAAY;IAClC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,QAAQ,GAAG,SAAS,CAAC,UAAU,CAAC,CAAC;IAC3E,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACjC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,IAAI,CAAC,IAAI,GAAG,SAAS;YAAE,MAAM,KAAK,CAAC,6BAA6B,CAAC,CAAC;QACxF,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC;QAC3C,IAAI,IAAI,GAAG,CAAC,CAAC;QACb,OAAO,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;YAC5B,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,GAAG,IAAI,EAAE,IAAI,CAAC,CAAC;YAClF,IAAI,CAAC,SAAS;gBAAE,MAAM;YACtB,IAAI,IAAI,SAAS,CAAC;QACpB,CAAC;QACD,IAAI,IAAI,GAAG,SAAS;YAAE,MAAM,KAAK,CAAC,6BAA6B,CAAC,CAAC;QACjE,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/D,CAAC;YAAS,CAAC;QACT,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;AACH,CAAC;AAED;;oFAEoF;AACpF,MAAM,CAAC,KAAK,UAAU,4BAA4B,CAAC,WAAyB;IAC1E,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,YAAY,GAAG,CAAC,CAAC,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;IACjG,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IAChD,OAAO,+BAA+B,CAAC,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE;QAC9G,IAAI,OAAO,GAAG,IAAI,CAAC;QACnB,KAAK,MAAM,OAAO,IAAI,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;YAC1C,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YACtC,IAAI,CAAC,MAAM,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,cAAc,EAAE;gBAAE,MAAM,KAAK,CAAC,6BAA6B,CAAC,CAAC;QAC1F,CAAC;QACD,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC3B,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["import { constants } from \"node:fs\";\nimport { lstat, open, realpath } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { resolveServiceAccessDeclaration, type ServiceAccessDeclaration } from \"./service-access.js\";\n\nconst MAX_BYTES = 65536;\n\nasync function readJson(file: string): Promise<unknown> {\n const handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW);\n try {\n const stat = await handle.stat();\n if (!stat.isFile() || stat.size > MAX_BYTES) throw Error(\"service_access_file_invalid\");\n const buffer = Buffer.alloc(MAX_BYTES + 1);\n let size = 0;\n while (size < buffer.length) {\n const { bytesRead } = await handle.read(buffer, size, buffer.length - size, size);\n if (!bytesRead) break;\n size += bytesRead;\n }\n if (size > MAX_BYTES) throw Error(\"service_access_file_invalid\");\n return JSON.parse(buffer.subarray(0, size).toString(\"utf8\"));\n } finally {\n await handle.close();\n }\n}\n\n/** Read the application's package data without importing application code.\n * Pass new URL('../package.json', import.meta.url) from a dist entry point.\n * Referenced declarations stay inside that package and cannot traverse symlinks. */\nexport async function loadServiceAccessDeclaration(packageFile: string | URL): Promise<ServiceAccessDeclaration | null> {\n const file = path.resolve(packageFile instanceof URL ? fileURLToPath(packageFile) : packageFile);\n const root = await realpath(path.dirname(file));\n return resolveServiceAccessDeclaration(await readJson(path.join(root, path.basename(file))), async (relative) => {\n let current = root;\n for (const segment of relative.split(\"/\")) {\n current = path.join(current, segment);\n if ((await lstat(current)).isSymbolicLink()) throw Error(\"service_access_file_invalid\");\n }\n return readJson(current);\n });\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=service-access-package.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-access-package.test.d.ts","sourceRoot":"","sources":["../../src/runtime/service-access-package.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,61 @@
1
+ import { expect, it, vi } from "vitest";
2
+ import { mkdtemp, mkdir, writeFile, symlink, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+ import { loadServiceAccessDeclaration } from "./service-access-package.js";
7
+ import { resolveServiceAccessDeclaration, serviceAccessDigest } from "./service-access.js";
8
+ const declaration = { protocol: 1, permissions: { scopes: ["read:uns"], accessRules: ["plant/sensor/#"] } };
9
+ const pkg = (serviceAccess) => ({ unsDatahub: { serviceAccess } });
10
+ it("normalizes inline/file representations identically without reading a file for inline data", async () => {
11
+ const read = vi.fn(async () => declaration);
12
+ const inline = await resolveServiceAccessDeclaration(pkg(declaration), read);
13
+ expect(read).not.toHaveBeenCalled();
14
+ const file = await resolveServiceAccessDeclaration(pkg("access.json"), read);
15
+ expect(read).toHaveBeenCalledExactlyOnceWith("access.json");
16
+ expect(inline).toEqual(file);
17
+ expect(serviceAccessDigest(inline)).toBe(serviceAccessDigest(file));
18
+ });
19
+ it.each([null, 42, [], {}, { ...declaration, protocol: 2 }, { ...declaration, approved: true },
20
+ "../access.json", "/access.json", "C:/access.json", "a\\access.json", "a//access.json", "./access.json"])("fails closed on an invalid package reference %j", async (reference) => {
21
+ const read = vi.fn();
22
+ await expect(resolveServiceAccessDeclaration(pkg(reference), read)).rejects.toThrow();
23
+ expect(read).not.toHaveBeenCalled();
24
+ });
25
+ it("keeps absence optional and rejects ambiguous legacy metadata before reading", async () => {
26
+ const read = vi.fn();
27
+ expect(await resolveServiceAccessDeclaration({ name: "ordinary-service" }, read)).toBeNull();
28
+ expect(await resolveServiceAccessDeclaration({ unsDatahub: { catalogAccess: "legacy.json" } }, read)).toBeNull();
29
+ await expect(resolveServiceAccessDeclaration({ unsDatahub: { serviceAccess: declaration, catalogAccess: "old.json" } }, read)).rejects.toThrow("conflict");
30
+ expect(read).not.toHaveBeenCalled();
31
+ });
32
+ it("loads package data and compatible files, refusing symlinks, oversized data and missing declarations", async () => {
33
+ const root = await mkdtemp(path.join(tmpdir(), "service-access-package-"));
34
+ const file = path.join(root, "package.json");
35
+ try {
36
+ const save = (reference) => writeFile(file, JSON.stringify(pkg(reference)));
37
+ await save(declaration);
38
+ expect(await loadServiceAccessDeclaration(pathToFileURL(file))).toEqual(declaration);
39
+ await mkdir(path.join(root, "metadata"));
40
+ await writeFile(path.join(root, "metadata/access.json"), JSON.stringify(declaration));
41
+ await save("metadata/access.json");
42
+ expect(await loadServiceAccessDeclaration(file)).toEqual(declaration);
43
+ await symlink(path.join(root, "metadata"), path.join(root, "linked"));
44
+ await save("linked/access.json");
45
+ await expect(loadServiceAccessDeclaration(file)).rejects.toThrow();
46
+ await save("missing.json");
47
+ await expect(loadServiceAccessDeclaration(file)).rejects.toThrow();
48
+ await writeFile(path.join(root, "huge.json"), " ".repeat(65537));
49
+ await save("huge.json");
50
+ await expect(loadServiceAccessDeclaration(file)).rejects.toThrow("file_invalid");
51
+ await writeFile(file, JSON.stringify({ ...pkg(declaration), description: "x".repeat(65536) }));
52
+ await expect(loadServiceAccessDeclaration(file)).rejects.toThrow("file_invalid");
53
+ await rm(file);
54
+ await symlink(path.join(root, "metadata/access.json"), file);
55
+ await expect(loadServiceAccessDeclaration(file)).rejects.toThrow();
56
+ }
57
+ finally {
58
+ await rm(root, { recursive: true, force: true });
59
+ }
60
+ });
61
+ //# sourceMappingURL=service-access-package.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-access-package.test.js","sourceRoot":"","sources":["../../src/runtime/service-access-package.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { expect, it, vi } from \"vitest\";\nimport { mkdtemp, mkdir, writeFile, symlink, rm } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport path from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\n\nimport { loadServiceAccessDeclaration } from \"./service-access-package.js\";\nimport { resolveServiceAccessDeclaration, serviceAccessDigest } from \"./service-access.js\";\n\nconst declaration = { protocol: 1, permissions: { scopes: [\"read:uns\"], accessRules: [\"plant/sensor/#\"] } };\nconst pkg = (serviceAccess: unknown) => ({ unsDatahub: { serviceAccess } });\n\nit(\"normalizes inline/file representations identically without reading a file for inline data\", async () => {\n const read = vi.fn(async () => declaration);\n const inline = await resolveServiceAccessDeclaration(pkg(declaration), read);\n expect(read).not.toHaveBeenCalled();\n const file = await resolveServiceAccessDeclaration(pkg(\"access.json\"), read);\n expect(read).toHaveBeenCalledExactlyOnceWith(\"access.json\");\n expect(inline).toEqual(file);\n expect(serviceAccessDigest(inline)).toBe(serviceAccessDigest(file));\n});\n\nit.each([null, 42, [], {}, { ...declaration, protocol: 2 }, { ...declaration, approved: true },\n \"../access.json\", \"/access.json\", \"C:/access.json\", \"a\\\\access.json\", \"a//access.json\", \"./access.json\"])\n(\"fails closed on an invalid package reference %j\", async (reference) => {\n const read = vi.fn();\n await expect(resolveServiceAccessDeclaration(pkg(reference), read)).rejects.toThrow();\n expect(read).not.toHaveBeenCalled();\n});\n\nit(\"keeps absence optional and rejects ambiguous legacy metadata before reading\", async () => {\n const read = vi.fn();\n expect(await resolveServiceAccessDeclaration({ name: \"ordinary-service\" }, read)).toBeNull();\n expect(await resolveServiceAccessDeclaration({ unsDatahub: { catalogAccess: \"legacy.json\" } }, read)).toBeNull();\n await expect(resolveServiceAccessDeclaration({ unsDatahub: { serviceAccess: declaration, catalogAccess: \"old.json\" } }, read)).rejects.toThrow(\"conflict\");\n expect(read).not.toHaveBeenCalled();\n});\n\nit(\"loads package data and compatible files, refusing symlinks, oversized data and missing declarations\", async () => {\n const root = await mkdtemp(path.join(tmpdir(), \"service-access-package-\"));\n const file = path.join(root, \"package.json\");\n try {\n const save = (reference: unknown) => writeFile(file, JSON.stringify(pkg(reference)));\n await save(declaration);\n expect(await loadServiceAccessDeclaration(pathToFileURL(file))).toEqual(declaration);\n await mkdir(path.join(root, \"metadata\"));\n await writeFile(path.join(root, \"metadata/access.json\"), JSON.stringify(declaration));\n await save(\"metadata/access.json\");\n expect(await loadServiceAccessDeclaration(file)).toEqual(declaration);\n await symlink(path.join(root, \"metadata\"), path.join(root, \"linked\"));\n await save(\"linked/access.json\");\n await expect(loadServiceAccessDeclaration(file)).rejects.toThrow();\n await save(\"missing.json\");\n await expect(loadServiceAccessDeclaration(file)).rejects.toThrow();\n await writeFile(path.join(root, \"huge.json\"), \" \".repeat(65537));\n await save(\"huge.json\");\n await expect(loadServiceAccessDeclaration(file)).rejects.toThrow(\"file_invalid\");\n await writeFile(file, JSON.stringify({ ...pkg(declaration), description: \"x\".repeat(65536) }));\n await expect(loadServiceAccessDeclaration(file)).rejects.toThrow(\"file_invalid\");\n await rm(file);\n await symlink(path.join(root, \"metadata/access.json\"), file);\n await expect(loadServiceAccessDeclaration(file)).rejects.toThrow();\n } finally {\n await rm(root, { recursive: true, force: true });\n }\n});\n"]}
@@ -1,3 +1,4 @@
1
+ import { z } from "zod";
1
2
  import { type CatalogPublicationManifest } from "./catalog-publication.js";
2
3
  export declare const SERVICE_ACCESS_PROTOCOL = 1;
3
4
  export declare const SERVICE_ACCESS_MANIFEST_FIELD = "serviceAccess";
@@ -11,6 +12,31 @@ export type ServiceAccessDeclaration = {
11
12
  publications?: CatalogPublicationManifest;
12
13
  };
13
14
  export declare function parseServiceAccessDeclaration(value: unknown): ServiceAccessDeclaration;
15
+ /** Package metadata accepts one inline declaration or one safe relative JSON
16
+ * reference. Both normalize through the same parser; neither grants authority. */
17
+ export declare const serviceAccessPackageReferenceSchema: z.ZodUnion<readonly [z.ZodString, z.ZodPipe<z.ZodObject<{
18
+ protocol: z.ZodLiteral<1>;
19
+ permissions: z.ZodObject<{
20
+ scopes: z.ZodArray<z.ZodEnum<{
21
+ "read:uns": "read:uns";
22
+ "identity:asset:publish": "identity:asset:publish";
23
+ "runtime:captures:read": "runtime:captures:read";
24
+ "runtime:captures:write": "runtime:captures:write";
25
+ "runtime:triggers:read": "runtime:triggers:read";
26
+ }>>;
27
+ accessRules: z.ZodArray<z.ZodString>;
28
+ }, z.core.$strict>;
29
+ publications: z.ZodOptional<z.ZodUnknown>;
30
+ }, z.core.$strict>, z.ZodTransform<ServiceAccessDeclaration, {
31
+ protocol: 1;
32
+ permissions: {
33
+ scopes: ("read:uns" | "identity:asset:publish" | "runtime:captures:read" | "runtime:captures:write" | "runtime:triggers:read")[];
34
+ accessRules: string[];
35
+ };
36
+ publications?: unknown;
37
+ }>>]>;
38
+ export type ServiceAccessPackageReference = string | ServiceAccessDeclaration;
39
+ export declare function resolveServiceAccessDeclaration(packageJson: unknown, readDeclaration: (relativePath: string) => Promise<unknown>): Promise<ServiceAccessDeclaration | null>;
14
40
  export declare const serviceAccessDigest: (value: unknown) => string;
15
41
  /** Publication source rules are deliberately not raw UNS permissions. A service
16
42
  * combining a background reader with a catalog must not gain their cross-product. */
@@ -1 +1 @@
1
- {"version":3,"file":"service-access.d.ts","sourceRoot":"","sources":["../../src/runtime/service-access.ts"],"names":[],"mappings":"AAEA,OAAO,EAAwB,KAAK,0BAA0B,EAAmC,MAAM,0BAA0B,CAAC;AAGlI,eAAO,MAAM,uBAAuB,IAAI,CAAC;AACzC,eAAO,MAAM,6BAA6B,kBAAkB,CAAC;AAI7D,eAAO,MAAM,uBAAuB,6HAM1B,CAAC;AAaX,MAAM,MAAM,wBAAwB,GAAG;IACrC,QAAQ,EAAE,CAAC,CAAC;IACZ,WAAW,EAAE;QAAE,MAAM,EAAE,MAAM,EAAE,CAAC;QAAC,WAAW,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IACzD,YAAY,CAAC,EAAE,0BAA0B,CAAC;CAC3C,CAAC;AACF,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,GAAG,wBAAwB,CAQtF;AACD,eAAO,MAAM,mBAAmB,4BAAiB,CAAC;AAClD;qFACqF;AACrF,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,wBAAwB;;;EAKzE;AACD,oFAAoF;AACpF,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAU7E"}
1
+ {"version":3,"file":"service-access.d.ts","sourceRoot":"","sources":["../../src/runtime/service-access.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAwB,KAAK,0BAA0B,EAAmC,MAAM,0BAA0B,CAAC;AAGlI,eAAO,MAAM,uBAAuB,IAAI,CAAC;AACzC,eAAO,MAAM,6BAA6B,kBAAkB,CAAC;AAI7D,eAAO,MAAM,uBAAuB,6HAM1B,CAAC;AAaX,MAAM,MAAM,wBAAwB,GAAG;IACrC,QAAQ,EAAE,CAAC,CAAC;IACZ,WAAW,EAAE;QAAE,MAAM,EAAE,MAAM,EAAE,CAAC;QAAC,WAAW,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IACzD,YAAY,CAAC,EAAE,0BAA0B,CAAC;CAC3C,CAAC;AACF,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,GAAG,wBAAwB,CAQtF;AAED;kFACkF;AAClF,eAAO,MAAM,mCAAmC;;;;;;;;;;;;;;;;;;;;KAQ9C,CAAC;AAEH,MAAM,MAAM,6BAA6B,GAAG,MAAM,GAAG,wBAAwB,CAAC;AAE9E,wBAAsB,+BAA+B,CACnD,WAAW,EAAE,OAAO,EACpB,eAAe,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,GAC1D,OAAO,CAAC,wBAAwB,GAAG,IAAI,CAAC,CAc1C;AACD,eAAO,MAAM,mBAAmB,4BAAiB,CAAC;AAClD;qFACqF;AACrF,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,wBAAwB;;;EAKzE;AACD,oFAAoF;AACpF,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAU7E"}