@uns-kit/core 3.0.48 → 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.
- package/MIGRATIONS.md +17 -0
- package/README.md +1 -1
- package/dist/runtime/service-access-examples/README.md +3 -2
- package/dist/runtime/service-access-examples/background-reader/README.md +5 -1
- package/dist/runtime/service-access-examples/background-reader/package.json +12 -3
- package/dist/runtime/service-access-examples/background-reader/src/reader.mjs +6 -1
- package/dist/runtime/service-access-examples/catalog-summary/README.md +5 -1
- package/dist/runtime/service-access-examples/catalog-summary/build.mjs +1 -1
- package/dist/runtime/service-access-examples/catalog-summary/package.json +67 -4
- package/dist/runtime/service-access-examples/catalog-summary/src/index.mjs +3 -3
- package/dist/runtime/service-access-guide.md +38 -7
- package/dist/runtime/service-access-package.d.ts +6 -0
- package/dist/runtime/service-access-package.d.ts.map +1 -0
- package/dist/runtime/service-access-package.js +45 -0
- package/dist/runtime/service-access-package.js.map +1 -0
- package/dist/runtime/service-access-package.test.d.ts +2 -0
- package/dist/runtime/service-access-package.test.d.ts.map +1 -0
- package/dist/runtime/service-access-package.test.js +61 -0
- package/dist/runtime/service-access-package.test.js.map +1 -0
- package/dist/runtime/service-access.d.ts +26 -0
- package/dist/runtime/service-access.d.ts.map +1 -1
- package/dist/runtime/service-access.js +23 -0
- package/dist/runtime/service-access.js.map +1 -1
- package/package.json +1 -1
- package/dist/runtime/service-access-examples/background-reader/service-access.json +0 -11
- package/dist/runtime/service-access-examples/catalog-summary/service-access.json +0 -65
package/MIGRATIONS.md
CHANGED
|
@@ -9,6 +9,23 @@ Agents must inspect the application's existing ownership and shutdown flow
|
|
|
9
9
|
before editing it. The examples below describe the intended behavior, not a
|
|
10
10
|
mechanical search-and-replace operation.
|
|
11
11
|
|
|
12
|
+
## 3.0.49 - Inline Service access package declarations
|
|
13
|
+
|
|
14
|
+
Existing file declarations remain compatible; no permission is granted by this
|
|
15
|
+
upgrade. New services may put the declaration object directly at
|
|
16
|
+
`package.json.unsDatahub.serviceAccess`. Use `loadServiceAccessDeclaration` from
|
|
17
|
+
`@uns-kit/core/runtime/service-access-package.js` for runtime reads, or the pure
|
|
18
|
+
resolver/schema from `runtime/service-access.js` for host integrations. Both forms
|
|
19
|
+
normalize to the same semantic digest. Do not declare both `serviceAccess` and
|
|
20
|
+
historical `catalogAccess`.
|
|
21
|
+
|
|
22
|
+
Inline metadata requires a Controller advertising `rttServiceAccessInline`;
|
|
23
|
+
Controller 2.2.96 accepts only the file form. Keep the file form until the target
|
|
24
|
+
Controller supports inline declarations. This patch does not implement the planned
|
|
25
|
+
configuration-save approval UI or automatic installation grants. Administrator
|
|
26
|
+
review, caller authorization, MQTT ACLs and rotating token use remain separate.
|
|
27
|
+
Both complete examples now use inline metadata and require the matching host.
|
|
28
|
+
|
|
12
29
|
## 3.0.48 - Checked Service access documentation and examples
|
|
13
30
|
|
|
14
31
|
This patch clarifies existing 3.0.47 contracts; no new permission mode or automatic
|
package/README.md
CHANGED
|
@@ -95,7 +95,7 @@ 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. 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.
|
|
98
|
+
`UnsClient` provides a minimal REST client for the UNS OpenHub API, including batch last-value, single-topic catch-all history, and batch range endpoints. Use `ServiceTokenProvider` for Controller-managed services so credential rotation is picked up on every request. Declare requirements in `package.json.unsDatahub.serviceAccess` (SDK 3.0.49+ and an inline-capable Controller), or retain the compatible JSON file reference. Review the service's declared scopes and resource paths before Start; never cache a managed token at application startup.
|
|
99
99
|
|
|
100
100
|
Read the [source Service access walkthrough](https://github.com/uns-openhub/uns-kit/blob/master/docs/catalog-publication-access.md)
|
|
101
101
|
or bundled `@uns-kit/core/runtime/service-access-guide.md`. Complete background-reader
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
- `catalog-summary/`: a catalog-only caller requests a bounded derived count; the workload has no direct source permission.
|
|
5
5
|
|
|
6
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
|
|
8
|
-
|
|
7
|
+
to a new project, install dependencies and build. The declarations are inline in package.json (SDK 3.0.49+). Deploy/review/start
|
|
8
|
+
with a Controller advertising rttServiceAccessInline; Controller 2.2.96 requires
|
|
9
|
+
the compatible file form instead.
|
|
9
10
|
The [walkthrough](https://github.com/uns-openhub/uns-kit/blob/master/docs/catalog-publication-access.md)
|
|
10
11
|
explains what the developer, administrator, Controller, SDK and caller each own.
|
|
11
12
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# background-reader
|
|
2
2
|
|
|
3
|
-
A complete Node.js 22+ example using
|
|
3
|
+
A complete Node.js 22+ example using SDK APIs from 3.0.49 onward.
|
|
4
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
5
|
|
|
6
6
|
```sh
|
|
@@ -23,3 +23,7 @@ This worker has no API or catalog. It polls five minutes of history, at most
|
|
|
23
23
|
your own test environment; zero rows is a valid result. Its token can read that
|
|
24
24
|
source, while another source must return 403. SIGINT/SIGTERM aborts the current
|
|
25
25
|
request and stops polling before runtime cleanup.
|
|
26
|
+
|
|
27
|
+
The access declaration is inline in package.json. This representation requires
|
|
28
|
+
Controller capability `rttServiceAccessInline`; it is unsupported by 2.2.96.
|
|
29
|
+
The SDK also accepts a package-relative JSON file reference for older hosts.
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
"main": "dist/index.js",
|
|
10
10
|
"files": [
|
|
11
11
|
"dist",
|
|
12
|
-
"service-access.json",
|
|
13
12
|
"config-example.json"
|
|
14
13
|
],
|
|
15
14
|
"scripts": {
|
|
@@ -17,12 +16,22 @@
|
|
|
17
16
|
"start": "node dist/index.js"
|
|
18
17
|
},
|
|
19
18
|
"dependencies": {
|
|
20
|
-
"@uns-kit/core": "^3.0.
|
|
19
|
+
"@uns-kit/core": "^3.0.49"
|
|
21
20
|
},
|
|
22
21
|
"unsDatahub": {
|
|
23
22
|
"schemaVersion": 1,
|
|
24
23
|
"kind": "addon",
|
|
25
24
|
"controllerCompatibility": ">=2 <3",
|
|
26
|
-
"serviceAccess":
|
|
25
|
+
"serviceAccess": {
|
|
26
|
+
"protocol": 1,
|
|
27
|
+
"permissions": {
|
|
28
|
+
"scopes": [
|
|
29
|
+
"read:uns"
|
|
30
|
+
],
|
|
31
|
+
"accessRules": [
|
|
32
|
+
"example/background/line-1/equipment/main/temperature"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
27
36
|
}
|
|
28
37
|
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { ServiceTokenProvider } from "@uns-kit/core/tools/auth/service-token-provider.js";
|
|
2
|
+
import { loadServiceAccessDeclaration } from "@uns-kit/core/runtime/service-access-package.js";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
const declaration = await loadServiceAccessDeclaration(new URL("../package.json", import.meta.url));
|
|
5
|
+
if (declaration?.permissions.scopes.length !== 1 || declaration.permissions.scopes[0] !== "read:uns" ||
|
|
6
|
+
declaration.permissions.accessRules.length !== 1 || /[+#]/.test(declaration.permissions.accessRules[0]))
|
|
7
|
+
throw new Error("This example requires one exact declared background source.");
|
|
8
|
+
export const sourceTopic = declaration.permissions.accessRules[0];
|
|
4
9
|
|
|
5
10
|
/** A background API read has a workload identity and no human caller token. */
|
|
6
11
|
export async function readHistory(controllerBase, from, to, { tokenProvider = new ServiceTokenProvider(), fetch: request = fetch, signal } = {}) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# catalog-summary
|
|
2
2
|
|
|
3
|
-
A complete Node.js 22+ example using
|
|
3
|
+
A complete Node.js 22+ example using SDK APIs from 3.0.49 onward.
|
|
4
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
5
|
|
|
6
6
|
```sh
|
|
@@ -24,3 +24,7 @@ and approve the publication for the intended audience. Give a test reader access
|
|
|
24
24
|
to the operation path, not the private source path. Execute a timezone-qualified
|
|
25
25
|
window of at most one hour. Expect {count, hasMore}; raw source fields are excluded.
|
|
26
26
|
A direct source request with the reader or service token must return 403.
|
|
27
|
+
|
|
28
|
+
The access declaration is inline in package.json. This representation requires
|
|
29
|
+
Controller capability `rttServiceAccessInline`; it is unsupported by 2.2.96.
|
|
30
|
+
The SDK also accepts a package-relative JSON file reference for older hosts.
|
|
@@ -4,4 +4,4 @@ await rm("dist", { recursive: true, force: true });
|
|
|
4
4
|
await mkdir("dist");
|
|
5
5
|
await copyFile("src/index.mjs", "dist/index.js");
|
|
6
6
|
await copyFile("src/offer.mjs", "dist/offer.mjs");
|
|
7
|
-
//
|
|
7
|
+
// The declaration lives in package.json, which npm always retains.
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
"main": "dist/index.js",
|
|
10
10
|
"files": [
|
|
11
11
|
"dist",
|
|
12
|
-
"service-access.json",
|
|
13
12
|
"config-example.json"
|
|
14
13
|
],
|
|
15
14
|
"scripts": {
|
|
@@ -17,13 +16,77 @@
|
|
|
17
16
|
"start": "node dist/index.js"
|
|
18
17
|
},
|
|
19
18
|
"dependencies": {
|
|
20
|
-
"@uns-kit/core": "^3.0.
|
|
21
|
-
"@uns-kit/api": "^3.0.
|
|
19
|
+
"@uns-kit/core": "^3.0.49",
|
|
20
|
+
"@uns-kit/api": "^3.0.49"
|
|
22
21
|
},
|
|
23
22
|
"unsDatahub": {
|
|
24
23
|
"schemaVersion": 1,
|
|
25
24
|
"kind": "addon",
|
|
26
25
|
"controllerCompatibility": ">=2 <3",
|
|
27
|
-
"serviceAccess":
|
|
26
|
+
"serviceAccess": {
|
|
27
|
+
"protocol": 1,
|
|
28
|
+
"permissions": {
|
|
29
|
+
"scopes": [],
|
|
30
|
+
"accessRules": []
|
|
31
|
+
},
|
|
32
|
+
"publications": {
|
|
33
|
+
"protocol": 1,
|
|
34
|
+
"operations": [
|
|
35
|
+
{
|
|
36
|
+
"id": "sample-count",
|
|
37
|
+
"title": "Temperature sample count",
|
|
38
|
+
"method": "GET",
|
|
39
|
+
"path": "/api/example/public/line-1/equipment/dataset/sample-count/json",
|
|
40
|
+
"mode": "approved-service",
|
|
41
|
+
"requestSchema": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"additionalProperties": false,
|
|
44
|
+
"required": [
|
|
45
|
+
"from",
|
|
46
|
+
"to"
|
|
47
|
+
],
|
|
48
|
+
"properties": {
|
|
49
|
+
"from": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"format": "date-time"
|
|
52
|
+
},
|
|
53
|
+
"to": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"format": "date-time"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"responseSchema": {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"additionalProperties": false,
|
|
62
|
+
"required": [
|
|
63
|
+
"count",
|
|
64
|
+
"hasMore"
|
|
65
|
+
],
|
|
66
|
+
"properties": {
|
|
67
|
+
"count": {
|
|
68
|
+
"type": "integer",
|
|
69
|
+
"minimum": 0
|
|
70
|
+
},
|
|
71
|
+
"hasMore": {
|
|
72
|
+
"type": "boolean"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"sources": [
|
|
77
|
+
{
|
|
78
|
+
"id": "temperature",
|
|
79
|
+
"kind": "history",
|
|
80
|
+
"topic": "example/private/line-1/equipment/main/temperature",
|
|
81
|
+
"maxWindowMs": 3600000,
|
|
82
|
+
"maxRows": 1000
|
|
83
|
+
}
|
|
84
|
+
],
|
|
85
|
+
"timeoutMs": 10000,
|
|
86
|
+
"maxResponseBytes": 4096
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
}
|
|
28
91
|
}
|
|
29
92
|
}
|
|
@@ -2,8 +2,7 @@ import "@uns-kit/api";
|
|
|
2
2
|
import { registerApiCatalog } from "@uns-kit/api";
|
|
3
3
|
import { ConfigFile, UnsProxyProcess } from "@uns-kit/core";
|
|
4
4
|
import { CatalogPublicationClient } from "@uns-kit/core/runtime/catalog-publication-client.js";
|
|
5
|
-
import {
|
|
6
|
-
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { loadServiceAccessDeclaration } from "@uns-kit/core/runtime/service-access-package.js";
|
|
7
6
|
|
|
8
7
|
import { createSummaryOffer } from "./offer.mjs";
|
|
9
8
|
|
|
@@ -12,7 +11,8 @@ const required = (key) => {
|
|
|
12
11
|
if (!value) throw new Error(`Missing managed runtime value: ${key}`);
|
|
13
12
|
return value;
|
|
14
13
|
};
|
|
15
|
-
const declaration =
|
|
14
|
+
const declaration = await loadServiceAccessDeclaration(new URL("../package.json", import.meta.url));
|
|
15
|
+
if (!declaration?.publications) throw new Error("Package must declare its catalog operations.");
|
|
16
16
|
const client = new CatalogPublicationClient({
|
|
17
17
|
controllerBase: required("UNS_CONTROLLER_PUBLIC_BASE"),
|
|
18
18
|
rttNode: required("RTT_NODE"),
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# Service access: from declaration to a running service
|
|
2
2
|
|
|
3
|
-
For application developers and coding agents using **SDK 3.0.
|
|
3
|
+
For application developers and coding agents using **SDK 3.0.49 or later**.
|
|
4
4
|
Common Service access also requires a matching Controller; these workflows were
|
|
5
|
-
verified with Controller **2.2.95
|
|
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
11
|
The developer declares a permission ceiling and uses the SDK. An administrator
|
|
8
12
|
reviews the installed release in **Micro services → instance → Service access**.
|
|
@@ -41,16 +45,22 @@ The npm core package also ships this guide at
|
|
|
41
45
|
to your own directory before installing/building it; do not edit `node_modules`.
|
|
42
46
|
Source code, not repeated prose fragments, is the complete runnable example.
|
|
43
47
|
|
|
44
|
-
|
|
48
|
+
For new services, put the declaration directly in `package.json`:
|
|
45
49
|
|
|
46
50
|
```json
|
|
47
51
|
{
|
|
48
|
-
"files": ["dist", "
|
|
52
|
+
"files": ["dist", "config-example.json"],
|
|
49
53
|
"unsDatahub": {
|
|
50
54
|
"schemaVersion": 1,
|
|
51
55
|
"kind": "addon",
|
|
52
56
|
"controllerCompatibility": ">=2 <3",
|
|
53
|
-
"serviceAccess":
|
|
57
|
+
"serviceAccess": {
|
|
58
|
+
"protocol": 1,
|
|
59
|
+
"permissions": {
|
|
60
|
+
"scopes": ["read:uns"],
|
|
61
|
+
"accessRules": ["example/background/line-1/equipment/main/temperature"]
|
|
62
|
+
}
|
|
63
|
+
}
|
|
54
64
|
}
|
|
55
65
|
}
|
|
56
66
|
```
|
|
@@ -60,6 +70,27 @@ real name/version/dependencies/scripts. The SDK is imported as published package
|
|
|
60
70
|
no copied validators, source overlays or application-name Controller branches.
|
|
61
71
|
Do not declare the historical `catalogAccess` marker alongside `serviceAccess`.
|
|
62
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
|
+
|
|
63
94
|
In either copied example:
|
|
64
95
|
|
|
65
96
|
```sh
|
|
@@ -132,10 +163,10 @@ optional `publications` declaration defines `sample-count`, GET JSON, from/to wi
|
|
|
132
163
|
a timezone, at most one hour/1000 rows from
|
|
133
164
|
`example/private/line-1/equipment/main/temperature`, and a closed result containing
|
|
134
165
|
only `{count, hasMore}`. The response cap is 4096 bytes. See its complete
|
|
135
|
-
`
|
|
166
|
+
`package.json.unsDatahub.serviceAccess`; the source and response schema are reviewed data.
|
|
136
167
|
|
|
137
168
|
`catalog-summary/src/index.mjs` reads and validates that declaration with
|
|
138
|
-
`
|
|
169
|
+
`loadServiceAccessDeclaration`, then constructs `CatalogPublicationClient` using
|
|
139
170
|
the real Controller-injected `RTT_NODE`, `RTT_INSTANCE_ID`, `version` and
|
|
140
171
|
`UNS_CONTROLLER_PUBLIC_BASE`. It configures JWT/JWKS authentication, calls
|
|
141
172
|
`api.registerCatalogRoute()` for the signed installed-launch route lease, and
|
|
@@ -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 @@
|
|
|
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":"
|
|
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"}
|
|
@@ -36,6 +36,29 @@ export function parseServiceAccessDeclaration(value) {
|
|
|
36
36
|
throw Error("service_access_capability_required");
|
|
37
37
|
return { protocol: 1, permissions, ...(publications ? { publications } : {}) };
|
|
38
38
|
}
|
|
39
|
+
/** Package metadata accepts one inline declaration or one safe relative JSON
|
|
40
|
+
* reference. Both normalize through the same parser; neither grants authority. */
|
|
41
|
+
export const serviceAccessPackageReferenceSchema = z.union([
|
|
42
|
+
z.string().min(1).max(512).refine((value) => !value.includes("\\") && !value.startsWith("/") &&
|
|
43
|
+
!/^[A-Za-z]:/.test(value) &&
|
|
44
|
+
value.split("/").every((part) => !!part && part !== "." && part !== ".."), "Expected a package-relative declaration path"),
|
|
45
|
+
envelope.transform(parseServiceAccessDeclaration),
|
|
46
|
+
]);
|
|
47
|
+
export async function resolveServiceAccessDeclaration(packageJson, readDeclaration) {
|
|
48
|
+
const pkg = z.object({
|
|
49
|
+
unsDatahub: z.object({
|
|
50
|
+
serviceAccess: z.unknown().optional(),
|
|
51
|
+
catalogAccess: z.unknown().optional(),
|
|
52
|
+
}).passthrough().optional(),
|
|
53
|
+
}).passthrough().parse(packageJson);
|
|
54
|
+
const metadata = pkg.unsDatahub;
|
|
55
|
+
if (metadata?.serviceAccess === undefined)
|
|
56
|
+
return null;
|
|
57
|
+
if (metadata.catalogAccess !== undefined)
|
|
58
|
+
throw Error("service_access_declaration_conflict");
|
|
59
|
+
const reference = serviceAccessPackageReferenceSchema.parse(metadata.serviceAccess);
|
|
60
|
+
return parseServiceAccessDeclaration(typeof reference === "string" ? await readDeclaration(reference) : reference);
|
|
61
|
+
}
|
|
39
62
|
export const serviceAccessDigest = operatorDigest;
|
|
40
63
|
/** Publication source rules are deliberately not raw UNS permissions. A service
|
|
41
64
|
* combining a background reader with a catalog must not gain their cross-product. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service-access.js","sourceRoot":"","sources":["../../src/runtime/service-access.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from \"zod\";\n\nimport { CATALOG_SOURCE_SCOPE, type CatalogPublicationManifest, parseCatalogPublicationManifest } from \"./catalog-publication.js\";\nimport { operatorDigest, operatorJson } from \"./operator-api.js\";\n\nexport const SERVICE_ACCESS_PROTOCOL = 1;\nexport const SERVICE_ACCESS_MANIFEST_FIELD = \"serviceAccess\";\n// Only existing platform data/runtime capabilities. Administrative authority is\n// never a package-requestable scope. UNS rules bound direct UNS reads; other\n// runtime capabilities retain their existing platform authorization semantics.\nexport const SERVICE_RESOURCE_SCOPES = [\n \"read:uns\",\n \"identity:asset:publish\",\n \"runtime:captures:read\",\n \"runtime:captures:write\",\n \"runtime:triggers:read\",\n] as const;\nconst rule = z\n .string()\n .min(1)\n .max(512)\n .refine((s) => s.split(\"/\").every((p, i, a) => p === \"+\" || (p === \"#\" && i === a.length - 1) || /^[A-Za-z0-9-]+$/.test(p)));\nconst envelope = z\n .object({\n protocol: z.literal(1),\n permissions: z.object({ scopes: z.array(z.enum(SERVICE_RESOURCE_SCOPES)).max(5), accessRules: z.array(rule).max(64) }).strict(),\n publications: z.unknown().optional(),\n })\n .strict();\nexport type ServiceAccessDeclaration = {\n protocol: 1;\n permissions: { scopes: string[]; accessRules: string[] };\n publications?: CatalogPublicationManifest;\n};\nexport function parseServiceAccessDeclaration(value: unknown): ServiceAccessDeclaration {\n operatorJson(value);\n const parsed = envelope.parse(value);\n const permissions = { scopes: [...new Set(parsed.permissions.scopes)].sort(), accessRules: [...new Set(parsed.permissions.accessRules)].sort() };\n if (!!permissions.scopes.length !== !!permissions.accessRules.length) throw Error(\"service_access_resources_required\");\n const publications = parsed.publications === undefined ? undefined : parseCatalogPublicationManifest(parsed.publications);\n if (!permissions.scopes.length && !publications) throw Error(\"service_access_capability_required\");\n return { protocol: 1, permissions, ...(publications ? { publications } : {}) };\n}\nexport const serviceAccessDigest = operatorDigest;\n/** Publication source rules are deliberately not raw UNS permissions. A service\n * combining a background reader with a catalog must not gain their cross-product. */\nexport function serviceAccessCeiling(declaration: ServiceAccessDeclaration) {\n return {\n scopes: [...declaration.permissions.scopes, ...(declaration.publications ? [CATALOG_SOURCE_SCOPE] : [])].sort(),\n accessRules: [...declaration.permissions.accessRules],\n };\n}\n/** Conservative MQTT filter inclusion, not matching a filter as if it were data. */\nexport function serviceRuleWithin(requested: string, allowed: string): boolean {\n const a = allowed.split(\"/\"),\n r = requested.split(\"/\");\n let i = 0;\n for (; i < a.length; i++) {\n if (a[i] === \"#\") return true;\n if (r[i] === undefined || r[i] === \"#\") return false;\n if (a[i] !== r[i] && a[i] !== \"+\") return false;\n }\n return i === r.length;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"service-access.js","sourceRoot":"","sources":["../../src/runtime/service-access.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from \"zod\";\n\nimport { CATALOG_SOURCE_SCOPE, type CatalogPublicationManifest, parseCatalogPublicationManifest } from \"./catalog-publication.js\";\nimport { operatorDigest, operatorJson } from \"./operator-api.js\";\n\nexport const SERVICE_ACCESS_PROTOCOL = 1;\nexport const SERVICE_ACCESS_MANIFEST_FIELD = \"serviceAccess\";\n// Only existing platform data/runtime capabilities. Administrative authority is\n// never a package-requestable scope. UNS rules bound direct UNS reads; other\n// runtime capabilities retain their existing platform authorization semantics.\nexport const SERVICE_RESOURCE_SCOPES = [\n \"read:uns\",\n \"identity:asset:publish\",\n \"runtime:captures:read\",\n \"runtime:captures:write\",\n \"runtime:triggers:read\",\n] as const;\nconst rule = z\n .string()\n .min(1)\n .max(512)\n .refine((s) => s.split(\"/\").every((p, i, a) => p === \"+\" || (p === \"#\" && i === a.length - 1) || /^[A-Za-z0-9-]+$/.test(p)));\nconst envelope = z\n .object({\n protocol: z.literal(1),\n permissions: z.object({ scopes: z.array(z.enum(SERVICE_RESOURCE_SCOPES)).max(5), accessRules: z.array(rule).max(64) }).strict(),\n publications: z.unknown().optional(),\n })\n .strict();\nexport type ServiceAccessDeclaration = {\n protocol: 1;\n permissions: { scopes: string[]; accessRules: string[] };\n publications?: CatalogPublicationManifest;\n};\nexport function parseServiceAccessDeclaration(value: unknown): ServiceAccessDeclaration {\n operatorJson(value);\n const parsed = envelope.parse(value);\n const permissions = { scopes: [...new Set(parsed.permissions.scopes)].sort(), accessRules: [...new Set(parsed.permissions.accessRules)].sort() };\n if (!!permissions.scopes.length !== !!permissions.accessRules.length) throw Error(\"service_access_resources_required\");\n const publications = parsed.publications === undefined ? undefined : parseCatalogPublicationManifest(parsed.publications);\n if (!permissions.scopes.length && !publications) throw Error(\"service_access_capability_required\");\n return { protocol: 1, permissions, ...(publications ? { publications } : {}) };\n}\n\n/** Package metadata accepts one inline declaration or one safe relative JSON\n * reference. Both normalize through the same parser; neither grants authority. */\nexport const serviceAccessPackageReferenceSchema = z.union([\n z.string().min(1).max(512).refine(\n (value) => !value.includes(\"\\\\\") && !value.startsWith(\"/\") &&\n !/^[A-Za-z]:/.test(value) &&\n value.split(\"/\").every((part) => !!part && part !== \".\" && part !== \"..\"),\n \"Expected a package-relative declaration path\",\n ),\n envelope.transform(parseServiceAccessDeclaration),\n]);\n\nexport type ServiceAccessPackageReference = string | ServiceAccessDeclaration;\n\nexport async function resolveServiceAccessDeclaration(\n packageJson: unknown,\n readDeclaration: (relativePath: string) => Promise<unknown>,\n): Promise<ServiceAccessDeclaration | null> {\n const pkg = z.object({\n unsDatahub: z.object({\n serviceAccess: z.unknown().optional(),\n catalogAccess: z.unknown().optional(),\n }).passthrough().optional(),\n }).passthrough().parse(packageJson);\n const metadata = pkg.unsDatahub;\n if (metadata?.serviceAccess === undefined) return null;\n if (metadata.catalogAccess !== undefined) throw Error(\"service_access_declaration_conflict\");\n const reference = serviceAccessPackageReferenceSchema.parse(metadata.serviceAccess);\n return parseServiceAccessDeclaration(\n typeof reference === \"string\" ? await readDeclaration(reference) : reference,\n );\n}\nexport const serviceAccessDigest = operatorDigest;\n/** Publication source rules are deliberately not raw UNS permissions. A service\n * combining a background reader with a catalog must not gain their cross-product. */\nexport function serviceAccessCeiling(declaration: ServiceAccessDeclaration) {\n return {\n scopes: [...declaration.permissions.scopes, ...(declaration.publications ? [CATALOG_SOURCE_SCOPE] : [])].sort(),\n accessRules: [...declaration.permissions.accessRules],\n };\n}\n/** Conservative MQTT filter inclusion, not matching a filter as if it were data. */\nexport function serviceRuleWithin(requested: string, allowed: string): boolean {\n const a = allowed.split(\"/\"),\n r = requested.split(\"/\");\n let i = 0;\n for (; i < a.length; i++) {\n if (a[i] === \"#\") return true;\n if (r[i] === undefined || r[i] === \"#\") return false;\n if (a[i] !== r[i] && a[i] !== \"+\") return false;\n }\n return i === r.length;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,65 +0,0 @@
|
|
|
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
|
-
}
|