sello 0.1.0 → 0.1.2
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/README.md +2 -0
- package/dist/cbor.js +337 -0
- package/dist/cli/bench.js +389 -0
- package/dist/cli/demo.js +113 -0
- package/dist/cli/sello.js +515 -0
- package/dist/cose/protected-header.js +210 -0
- package/dist/cose/sign1.js +124 -0
- package/dist/crypto/ed25519.js +117 -0
- package/dist/crypto/identifiers.js +64 -0
- package/dist/hpke/base.js +349 -0
- package/dist/hpke/receipt.js +79 -0
- package/dist/index.js +15 -0
- package/dist/log/canonical-url.js +168 -0
- package/dist/log/mock-log.js +147 -0
- package/dist/log/rekor.js +120 -0
- package/dist/log/types.js +0 -0
- package/dist/mcp/middleware.js +162 -0
- package/dist/owner/verify.js +271 -0
- package/dist/receipt/body.js +210 -0
- package/dist/registry/json-registry.js +233 -0
- package/dist/sdk/index.js +22 -0
- package/dist/sdk/keys.js +191 -0
- package/dist/sdk/logs.js +196 -0
- package/dist/sdk/publisher.js +106 -0
- package/dist/sdk/service.js +561 -0
- package/dist/service/create-receipt.js +174 -0
- package/dist/token/jws-profile.js +174 -0
- package/docs/decisions.md +2 -2
- package/docs/release-checklist.md +4 -3
- package/docs/sdk-quickstart.md +2 -0
- package/package.json +10 -6
- package/src/cli/sello.ts +5 -3
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { encodeProtectedHeader } from "../cose/protected-header.js";
|
|
2
|
+
import { signReceiptEnvelope } from "../cose/sign1.js";
|
|
3
|
+
import { deriveTokenIdentifiers, sha256 } from "../crypto/identifiers.js";
|
|
4
|
+
import { sealReceiptBody } from "../hpke/receipt.js";
|
|
5
|
+
import {
|
|
6
|
+
|
|
7
|
+
assertCanonicalLogUrl,
|
|
8
|
+
logUrlsEqual,
|
|
9
|
+
} from "../log/canonical-url.js";
|
|
10
|
+
import {
|
|
11
|
+
ZERO_SHA256_DIGEST,
|
|
12
|
+
encodeReceiptBody,
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
} from "../receipt/body.js";
|
|
16
|
+
import { verifySelloJwsToken } from "../token/jws-profile.js";
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
export function buildReceipt(input ) {
|
|
57
|
+
assertBytes(input.authorizationTokenBytes, "authorizationTokenBytes");
|
|
58
|
+
assertByteLength(input.ownerHpkePublicKey, 32, "ownerHpkePublicKey");
|
|
59
|
+
assertBytes(input.serviceKid, "serviceKid");
|
|
60
|
+
assertBytes(input.servicePrivateKey, "servicePrivateKey");
|
|
61
|
+
assertBytes(input.actionInputBytes, "actionInputBytes");
|
|
62
|
+
|
|
63
|
+
if (typeof input.serviceIdentifier !== "string" || input.serviceIdentifier.length === 0) {
|
|
64
|
+
throw new TypeError("serviceIdentifier must be a non-empty string");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (typeof input.actionType !== "string" || input.actionType.length === 0) {
|
|
68
|
+
throw new TypeError("actionType must be a non-empty string");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const selectedLogUrl = selectOwnerTrustedLog(input.selloLogs, input.logUrl);
|
|
72
|
+
const identifiers = deriveTokenIdentifiers(input.authorizationTokenBytes);
|
|
73
|
+
const receiptBody = {
|
|
74
|
+
"agent-identifier": identifiers.agent_identifier,
|
|
75
|
+
"action-type": input.actionType,
|
|
76
|
+
"action-input-hash": sha256(input.actionInputBytes),
|
|
77
|
+
"action-output-hash":
|
|
78
|
+
input.resultStatus === "denied"
|
|
79
|
+
? ZERO_SHA256_DIGEST
|
|
80
|
+
: sha256(input.actionOutputBytes ?? new Uint8Array()),
|
|
81
|
+
"result-status": input.resultStatus,
|
|
82
|
+
timestamp: input.timestamp,
|
|
83
|
+
};
|
|
84
|
+
const protectedHeaderBytes = encodeProtectedHeader({
|
|
85
|
+
kid: input.serviceKid,
|
|
86
|
+
sello_token_ref: identifiers.sello_token_ref,
|
|
87
|
+
sello_log_url: selectedLogUrl,
|
|
88
|
+
});
|
|
89
|
+
const payload = sealReceiptBody({
|
|
90
|
+
plaintext: encodeReceiptBody(receiptBody),
|
|
91
|
+
ownerPublicKey: input.ownerHpkePublicKey,
|
|
92
|
+
protectedHeaderBytes,
|
|
93
|
+
serviceIdentifier: input.serviceIdentifier,
|
|
94
|
+
selloTokenRef: identifiers.sello_token_ref,
|
|
95
|
+
});
|
|
96
|
+
const envelope = signReceiptEnvelope({
|
|
97
|
+
protectedHeaderBytes,
|
|
98
|
+
payload,
|
|
99
|
+
servicePrivateKey: input.servicePrivateKey,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
receiptBody,
|
|
104
|
+
protectedHeaderBytes,
|
|
105
|
+
envelope,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function createReceipt(input ) {
|
|
110
|
+
const built = buildReceipt({
|
|
111
|
+
...input,
|
|
112
|
+
logUrl: input.log.logUrl,
|
|
113
|
+
});
|
|
114
|
+
const logEntry = input.log.append(built.envelope, input.timestamp);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
...built,
|
|
118
|
+
logEntry,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function createReceiptFromJwsToken(
|
|
123
|
+
input ,
|
|
124
|
+
) {
|
|
125
|
+
const verifiedToken = verifySelloJwsToken({
|
|
126
|
+
authorizationToken: input.authorizationToken,
|
|
127
|
+
issuerPublicKey: input.tokenIssuerPublicKey,
|
|
128
|
+
});
|
|
129
|
+
const selloLogs = verifiedToken.selloLogs ?? input.fallbackSelloLogs;
|
|
130
|
+
|
|
131
|
+
return createReceipt({
|
|
132
|
+
...input,
|
|
133
|
+
authorizationTokenBytes: verifiedToken.authorizationTokenBytes,
|
|
134
|
+
ownerHpkePublicKey: verifiedToken.ownerHpkePublicKey,
|
|
135
|
+
selloLogs: selloLogs ?? [],
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function selectOwnerTrustedLog(
|
|
140
|
+
selloLogs ,
|
|
141
|
+
candidateLogUrl ,
|
|
142
|
+
) {
|
|
143
|
+
if (!Array.isArray(selloLogs) || selloLogs.length === 0) {
|
|
144
|
+
throw new TypeError("selloLogs must contain at least one owner-trusted log");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const canonicalLogs = selloLogs.map((logUrl) => {
|
|
148
|
+
assertCanonicalLogUrl(logUrl, "selloLogs entry");
|
|
149
|
+
return logUrl;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const match = canonicalLogs.find((logUrl) => logUrlsEqual(logUrl, candidateLogUrl));
|
|
153
|
+
if (!match) {
|
|
154
|
+
throw new TypeError("service log must be listed in selloLogs");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return match;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function assertByteLength(
|
|
161
|
+
value ,
|
|
162
|
+
length ,
|
|
163
|
+
name ,
|
|
164
|
+
) {
|
|
165
|
+
if (!(value instanceof Uint8Array) || value.byteLength !== length) {
|
|
166
|
+
throw new TypeError(`${name} must be a ${length}-byte Uint8Array`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function assertBytes(value , name ) {
|
|
171
|
+
if (!(value instanceof Uint8Array)) {
|
|
172
|
+
throw new TypeError(`${name} must be a Uint8Array`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { signEd25519, verifyEd25519Signature } from "../crypto/ed25519.js";
|
|
2
|
+
import { assertCanonicalLogUrl } from "../log/canonical-url.js";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
const textEncoder = new TextEncoder();
|
|
24
|
+
const textDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
25
|
+
const BASE64URL_32_BYTE_LENGTH = 43;
|
|
26
|
+
|
|
27
|
+
export function verifySelloJwsToken(
|
|
28
|
+
input ,
|
|
29
|
+
) {
|
|
30
|
+
const authorizationTokenBytes = normalizeTokenBytes(input.authorizationToken);
|
|
31
|
+
const authorizationToken = textDecoder.decode(authorizationTokenBytes);
|
|
32
|
+
const parts = authorizationToken.split(".");
|
|
33
|
+
|
|
34
|
+
if (parts.length !== 3 || parts.some((part) => part.length === 0)) {
|
|
35
|
+
throw new TypeError("authorization token must be compact JWS");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const [encodedProtected, encodedPayload, encodedSignature] = parts;
|
|
39
|
+
const protectedHeader = parseJsonObject(
|
|
40
|
+
base64urlDecode(encodedProtected, "JWS protected header"),
|
|
41
|
+
"JWS protected header",
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
if (protectedHeader.alg !== "EdDSA") {
|
|
45
|
+
throw new TypeError("JWS alg must be EdDSA");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if ("crit" in protectedHeader) {
|
|
49
|
+
throw new TypeError("JWS crit is not supported by the v0.1 token profile");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const signingInput = textEncoder.encode(`${encodedProtected}.${encodedPayload}`);
|
|
53
|
+
const signature = base64urlDecode(encodedSignature, "JWS signature");
|
|
54
|
+
if (!verifyEd25519Signature(signingInput, signature, input.issuerPublicKey)) {
|
|
55
|
+
throw new TypeError("JWS signature verification failed");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const payload = parseJsonObject(
|
|
59
|
+
base64urlDecode(encodedPayload, "JWS payload"),
|
|
60
|
+
"JWS payload",
|
|
61
|
+
);
|
|
62
|
+
const ownerHpkePublicKey = readOwnerHpkePublicKey(payload);
|
|
63
|
+
const selloLogs = readSelloLogs(payload);
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
authorizationTokenBytes,
|
|
67
|
+
ownerHpkePublicKey,
|
|
68
|
+
...(selloLogs === undefined ? {} : { selloLogs }),
|
|
69
|
+
protectedHeader,
|
|
70
|
+
payload,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function signSelloJwsToken(input ) {
|
|
75
|
+
const protectedHeader = {
|
|
76
|
+
alg: "EdDSA",
|
|
77
|
+
typ: "JWT",
|
|
78
|
+
...input.protectedHeader,
|
|
79
|
+
};
|
|
80
|
+
const encodedProtected = base64urlEncode(
|
|
81
|
+
textEncoder.encode(JSON.stringify(protectedHeader)),
|
|
82
|
+
);
|
|
83
|
+
const encodedPayload = base64urlEncode(
|
|
84
|
+
textEncoder.encode(JSON.stringify(input.payload)),
|
|
85
|
+
);
|
|
86
|
+
const signingInput = textEncoder.encode(`${encodedProtected}.${encodedPayload}`);
|
|
87
|
+
const signature = signEd25519(signingInput, input.issuerPrivateKey);
|
|
88
|
+
|
|
89
|
+
return `${encodedProtected}.${encodedPayload}.${base64urlEncode(signature)}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function base64urlEncode(bytes ) {
|
|
93
|
+
return Buffer.from(bytes).toString("base64url");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function readOwnerHpkePublicKey(payload ) {
|
|
97
|
+
const encoded = payload.owner_hpke_pk;
|
|
98
|
+
|
|
99
|
+
if (typeof encoded !== "string") {
|
|
100
|
+
throw new TypeError("owner_hpke_pk must be a string");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (encoded.length !== BASE64URL_32_BYTE_LENGTH) {
|
|
104
|
+
throw new TypeError("owner_hpke_pk must encode a raw 32-byte X25519 public key");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const publicKey = base64urlDecode(encoded, "owner_hpke_pk");
|
|
108
|
+
if (publicKey.byteLength !== 32) {
|
|
109
|
+
throw new TypeError("owner_hpke_pk must encode a raw 32-byte X25519 public key");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return publicKey;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function readSelloLogs(payload ) {
|
|
116
|
+
const value = payload.sello_logs;
|
|
117
|
+
|
|
118
|
+
if (value === undefined) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!Array.isArray(value)) {
|
|
123
|
+
throw new TypeError("sello_logs must be an array");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return value.map((entry) => {
|
|
127
|
+
if (typeof entry !== "string") {
|
|
128
|
+
throw new TypeError("sello_logs entries must be strings");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
assertCanonicalLogUrl(entry, "sello_logs entry");
|
|
132
|
+
return entry;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function normalizeTokenBytes(token ) {
|
|
137
|
+
if (typeof token === "string") {
|
|
138
|
+
if (!/^[\x21-\x7e]+$/.test(token)) {
|
|
139
|
+
throw new TypeError("authorization token must be visible ASCII");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return textEncoder.encode(token);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (token instanceof Uint8Array) {
|
|
146
|
+
return new Uint8Array(token);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
throw new TypeError("authorizationToken must be a string or Uint8Array");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function parseJsonObject(bytes , name ) {
|
|
153
|
+
let parsed ;
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
parsed = JSON.parse(textDecoder.decode(bytes));
|
|
157
|
+
} catch {
|
|
158
|
+
throw new TypeError(`${name} must be UTF-8 JSON`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
162
|
+
throw new TypeError(`${name} must be a JSON object`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return parsed ;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function base64urlDecode(value , name ) {
|
|
169
|
+
if (!/^[A-Za-z0-9_-]*$/.test(value) || value.length % 4 === 1) {
|
|
170
|
+
throw new TypeError(`${name} must be unpadded base64url`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return Uint8Array.from(Buffer.from(value, "base64url"));
|
|
174
|
+
}
|
package/docs/decisions.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Initial Scaffold
|
|
4
4
|
|
|
5
|
-
- Use plain TypeScript modules with Node
|
|
5
|
+
- Use plain TypeScript modules with Node 22.7+'s native type stripping for the first implementation slice.
|
|
6
6
|
- Use Node's built-in test runner to avoid dependency installation before the crypto-library spike.
|
|
7
7
|
- Keep `package.json` npm-compatible, but run tests with `node --run test` or the raw `node --test --experimental-strip-types` command while `npm` is unavailable in this workspace.
|
|
8
8
|
- Start with token-derived identifiers because they require no third-party dependencies and exercise the spec's exact-byte handling rule.
|
|
@@ -75,5 +75,5 @@
|
|
|
75
75
|
|
|
76
76
|
## Demo Command
|
|
77
77
|
|
|
78
|
-
- Ship a small `sello-demo` binary that runs through Node
|
|
78
|
+
- Ship a small `sello-demo` binary that runs through Node 22.7+'s native TypeScript type stripping.
|
|
79
79
|
- The demo prints success, error, and denied receipts as verified JSON, and `--tamper` appends a deliberately bad entry to show structured rejection output.
|
|
@@ -5,8 +5,8 @@ Use this checklist before publishing a Sello npm release.
|
|
|
5
5
|
## Preflight
|
|
6
6
|
|
|
7
7
|
- Confirm `git status --short` is clean.
|
|
8
|
-
- Confirm `node -v` is `
|
|
9
|
-
- If multiple Node versions are installed, confirm `PATH` resolves `node` to Node
|
|
8
|
+
- Confirm `node -v` is `v22.7.0` or newer.
|
|
9
|
+
- If multiple Node versions are installed, confirm `PATH` resolves `node` to Node 22.7 or newer before running package scripts.
|
|
10
10
|
- Confirm `package.json` has the intended version.
|
|
11
11
|
- Confirm `README.md` and `docs/sdk-quickstart.md` match the current CLI and examples.
|
|
12
12
|
- Confirm the paper link and local PDF are current, if the paper changed.
|
|
@@ -24,8 +24,9 @@ Fresh clone smoke test:
|
|
|
24
24
|
tmpdir=$(mktemp -d)
|
|
25
25
|
git clone https://github.com/juanfiguera/sello.git "$tmpdir/sello"
|
|
26
26
|
cd "$tmpdir/sello"
|
|
27
|
-
node -v # must be
|
|
27
|
+
node -v # must be v22.7.0 or newer
|
|
28
28
|
node --run test
|
|
29
|
+
node --run package:test
|
|
29
30
|
npm pack --dry-run
|
|
30
31
|
node --experimental-strip-types src/cli/sello.ts --help
|
|
31
32
|
node --experimental-strip-types src/cli/sello.ts dev --dry-run
|
package/docs/sdk-quickstart.md
CHANGED
package/package.json
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sello",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Reference implementation of the Sello protocol for service-signed AI agent receipts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"sideEffects": false,
|
|
8
8
|
"exports": {
|
|
9
|
-
".": "./
|
|
9
|
+
".": "./dist/index.js",
|
|
10
10
|
"./fixtures/vectors/sello-v0.1.json": "./fixtures/vectors/sello-v0.1.json",
|
|
11
11
|
"./spec": "./SPEC.md",
|
|
12
12
|
"./package.json": "./package.json"
|
|
13
13
|
},
|
|
14
14
|
"bin": {
|
|
15
|
-
"sello": "
|
|
16
|
-
"sello-bench": "
|
|
17
|
-
"sello-demo": "
|
|
15
|
+
"sello": "dist/cli/sello.js",
|
|
16
|
+
"sello-bench": "dist/cli/bench.js",
|
|
17
|
+
"sello-demo": "dist/cli/demo.js"
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
|
+
"dist",
|
|
20
21
|
"examples",
|
|
21
22
|
"src",
|
|
22
23
|
"fixtures/vectors",
|
|
@@ -32,11 +33,14 @@
|
|
|
32
33
|
"dev": "node --experimental-strip-types src/cli/sello.ts dev",
|
|
33
34
|
"example:mcp": "node --experimental-strip-types examples/mcp-tool-server.ts",
|
|
34
35
|
"example:tool": "node --experimental-strip-types examples/quickstart-tool.ts",
|
|
36
|
+
"build": "node --disable-warning=ExperimentalWarning scripts/build-dist.mjs",
|
|
37
|
+
"package:test": "node scripts/package-smoke.mjs",
|
|
35
38
|
"pack:check": "npm pack --dry-run",
|
|
39
|
+
"prepack": "node --disable-warning=ExperimentalWarning scripts/build-dist.mjs",
|
|
36
40
|
"test": "node --test --experimental-strip-types"
|
|
37
41
|
},
|
|
38
42
|
"engines": {
|
|
39
|
-
"node": ">=
|
|
43
|
+
"node": ">=22.7.0"
|
|
40
44
|
},
|
|
41
45
|
"keywords": [
|
|
42
46
|
"ai-agents",
|
package/src/cli/sello.ts
CHANGED
|
@@ -505,10 +505,12 @@ function printDevConfig(port: number, state: DevState): void {
|
|
|
505
505
|
}
|
|
506
506
|
|
|
507
507
|
function enforceNodeVersion(): void {
|
|
508
|
-
const major =
|
|
509
|
-
|
|
508
|
+
const [major = 0, minor = 0] = process.versions.node
|
|
509
|
+
.split(".")
|
|
510
|
+
.map((part) => Number(part));
|
|
511
|
+
if (major < 22 || (major === 22 && minor < 7)) {
|
|
510
512
|
throw new TypeError(
|
|
511
|
-
`Sello requires Node >=
|
|
513
|
+
`Sello requires Node >=22.7.0; current Node is ${process.versions.node}`,
|
|
512
514
|
);
|
|
513
515
|
}
|
|
514
516
|
}
|