@north-light/cli 0.3.486 → 0.3.487
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/dist/builtin-memory/crouter-sdk/release-notes.md +6 -0
- package/dist/clients/attach/viewer.js +132 -132
- package/dist/core/grants/cover.d.ts +14 -0
- package/dist/core/grants/cover.js +1 -0
- package/dist/core/grants/mirror.d.ts +3 -1
- package/dist/core/grants/mirror.js +2 -2
- package/dist/daemon/api/app-listener.js +13 -13
- package/dist/daemon/api/principal.d.ts +10 -18
- package/dist/daemon/api/principal.js +1 -1
- package/dist/daemon/peer/authority.d.ts +27 -0
- package/dist/daemon/peer/authority.js +1 -0
- package/dist/daemon/peer/ingress.d.ts +17 -15
- package/dist/daemon/peer/ingress.js +1 -1
- package/dist/daemon/peer/relay-service.js +1 -1
- package/docs/sdk/release-notes.md +6 -0
- package/package.json +2 -2
- package/packages/crouter-identity/dist/index.d.ts +55 -0
- package/packages/crouter-identity/dist/index.js +117 -0
- package/packages/crouter-identity/package.json +1 -1
- package/runtime.lock.json +12 -12
|
@@ -215,6 +215,7 @@ function validatePrincipal(value, kind) {
|
|
|
215
215
|
function keySet(jwks, kid) {
|
|
216
216
|
return jwks instanceof JwksCache ? jwks.forKid(kid) : Promise.resolve(jwks);
|
|
217
217
|
}
|
|
218
|
+
/** @deprecated Use `verifyCallToken`; removed after the peer-token rollout window (plan I2). */
|
|
218
219
|
export async function verifyAccessToken(options) {
|
|
219
220
|
const header = decodeProtectedHeader(options.token);
|
|
220
221
|
if (header.typ !== 'at+jwt' || header.alg !== 'ES256')
|
|
@@ -241,6 +242,7 @@ export async function verifyAccessToken(options) {
|
|
|
241
242
|
* refused, so an app grant token never verifies here; `verifyAccessToken` requires an `app:` grantee, so a machine
|
|
242
243
|
* token never verifies there.
|
|
243
244
|
*/
|
|
245
|
+
/** @deprecated Use `verifyCallToken`; removed after the peer-token rollout window (plan I2). */
|
|
244
246
|
export async function verifyMachineToken(options) {
|
|
245
247
|
const header = decodeProtectedHeader(options.token);
|
|
246
248
|
if (header.typ !== 'at+jwt' || header.alg !== 'ES256')
|
|
@@ -273,6 +275,7 @@ function isActor(value) {
|
|
|
273
275
|
* an `app:` or `user:` principal; the remaining claims are checked for presence and type only; which of them gates a
|
|
274
276
|
* call is the caller's rule (D3-TOK-13 gates on `scope` alone, and nothing gates on `act`).
|
|
275
277
|
*/
|
|
278
|
+
/** @deprecated Use `verifyCallToken`; removed after the peer-token rollout window (plan I2). */
|
|
276
279
|
export async function verifyOutboundToken(options) {
|
|
277
280
|
const header = decodeProtectedHeader(options.token);
|
|
278
281
|
if (header.typ !== 'at+jwt' || (header.alg !== 'ES256' && header.alg !== 'RS256'))
|
|
@@ -291,6 +294,120 @@ export async function verifyOutboundToken(options) {
|
|
|
291
294
|
}
|
|
292
295
|
return payload;
|
|
293
296
|
}
|
|
297
|
+
const MAX_CHAIN_DEPTH = 8;
|
|
298
|
+
const DEVELOPER_CREDENTIAL_LINK = /^developer-credential:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
299
|
+
function isDeveloperCredential(value) {
|
|
300
|
+
return typeof value === 'string' && DEVELOPER_CREDENTIAL_LINK.test(value);
|
|
301
|
+
}
|
|
302
|
+
/** The only envelope a `developer-credential:<uuid>` link may sit in: the token the directory's developer login mints for
|
|
303
|
+
* `cloud:developer`, whose grantee and initiating app are the person themself and which carries no machine, grant or revision. */
|
|
304
|
+
function isDeveloperCredentialEnvelope(p) {
|
|
305
|
+
return p.client_id === 'developer-credential' && p.scope === 'cloud:developer' && p.grantee === p.sub && p.initiating_app === p.sub &&
|
|
306
|
+
p.machine_id === undefined && p.grant === undefined && p.rev === undefined;
|
|
307
|
+
}
|
|
308
|
+
function chainOf(payload) {
|
|
309
|
+
const p = payload;
|
|
310
|
+
const bad = () => { throw new Error('Invalid call token claims'); };
|
|
311
|
+
const appLink = (sub, grant, rev) => {
|
|
312
|
+
if (grant === undefined)
|
|
313
|
+
return { sub };
|
|
314
|
+
if (typeof grant !== 'string' || grant === '')
|
|
315
|
+
return bad();
|
|
316
|
+
if (rev === undefined)
|
|
317
|
+
throw new MissingRevClaimError();
|
|
318
|
+
if (typeof rev !== 'number' || !Number.isInteger(rev) || rev < 1)
|
|
319
|
+
return bad();
|
|
320
|
+
return { sub, grant, rev };
|
|
321
|
+
};
|
|
322
|
+
if (p.act === undefined) {
|
|
323
|
+
if (p.machine_id !== undefined) {
|
|
324
|
+
if (!validatePrincipal(p.machine_id, 'runtime') || p.grantee !== p.sub ||
|
|
325
|
+
p.grant !== undefined || p.rev !== undefined || p.scope !== undefined)
|
|
326
|
+
return bad();
|
|
327
|
+
return [{ sub: p.machine_id }];
|
|
328
|
+
}
|
|
329
|
+
if (validatePrincipal(p.grantee, 'app') && typeof p.grant === 'string')
|
|
330
|
+
return [appLink(p.grantee, p.grant, p.rev)];
|
|
331
|
+
return bad();
|
|
332
|
+
}
|
|
333
|
+
const chain = [];
|
|
334
|
+
let node = p.act;
|
|
335
|
+
while (node !== undefined) {
|
|
336
|
+
if (chain.length >= MAX_CHAIN_DEPTH)
|
|
337
|
+
return bad();
|
|
338
|
+
if (typeof node !== 'object' || node === null || Array.isArray(node))
|
|
339
|
+
return bad();
|
|
340
|
+
const link = node;
|
|
341
|
+
if (isDeveloperCredential(link.sub)) {
|
|
342
|
+
// The one restricted actor beyond runtime/app/user: the directory's developer-CLI token. It verifies and grants nothing else.
|
|
343
|
+
if (chain.length !== 0 || Object.keys(node).length !== 1 || !isDeveloperCredentialEnvelope(p))
|
|
344
|
+
return bad();
|
|
345
|
+
chain.push({ sub: link.sub });
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
if (!(validatePrincipal(link.sub, 'runtime') || validatePrincipal(link.sub, 'app') || validatePrincipal(link.sub, 'user')))
|
|
349
|
+
return bad();
|
|
350
|
+
if (link.sub.startsWith('app:'))
|
|
351
|
+
chain.push(appLink(link.sub, link.grant, link.rev));
|
|
352
|
+
else if (link.grant !== undefined || link.rev !== undefined)
|
|
353
|
+
return bad();
|
|
354
|
+
else
|
|
355
|
+
chain.push({ sub: link.sub });
|
|
356
|
+
node = link.act;
|
|
357
|
+
}
|
|
358
|
+
if (p.machine_id !== undefined && p.machine_id !== chain[0].sub)
|
|
359
|
+
return bad();
|
|
360
|
+
if (p.grant !== undefined && !chain.some((l) => l.sub.startsWith('app:') && l.grant === p.grant))
|
|
361
|
+
return bad();
|
|
362
|
+
// A superset token's other legacy fields must say what its chain says, or the old and new verifiers authorize different
|
|
363
|
+
// identities from one signed token. Provider tokens (`scope`) are exempt: their `grantee` is the person or app the call
|
|
364
|
+
// runs for, not a link of the chain.
|
|
365
|
+
if (p.scope === undefined) {
|
|
366
|
+
const appLinks = chain.filter((l) => l.sub.startsWith('app:'));
|
|
367
|
+
const appLink = appLinks.find((l) => l.grant !== undefined && l.grant === p.grant) ?? appLinks[0];
|
|
368
|
+
if (appLink !== undefined) {
|
|
369
|
+
if (p.grantee !== undefined && p.grantee !== appLink.sub)
|
|
370
|
+
return bad();
|
|
371
|
+
if (p.rev !== undefined && p.rev !== appLink.rev)
|
|
372
|
+
return bad();
|
|
373
|
+
}
|
|
374
|
+
else if (chain[0].sub.startsWith('runtime:')) {
|
|
375
|
+
if (p.grantee !== undefined && p.grantee !== p.sub)
|
|
376
|
+
return bad();
|
|
377
|
+
if (p.rev !== undefined)
|
|
378
|
+
return bad();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return chain;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Verifies a call token of any use (peer, app access, outbound provider, run notice) and returns its actor chain,
|
|
385
|
+
* presenter first. Checks in order: header `typ` `at+jwt`, `alg` ES256 or RS256, non-empty `kid`; signature; `iss`;
|
|
386
|
+
* `aud` a string equal to `audience`; `exp` and `iat` numbers; `sub` = `user:<id>`; `client_id` non-empty; `scope`
|
|
387
|
+
* a string when present; then the chain. Without `act` the legacy machine token (`machine_id`, `grantee` = `sub`)
|
|
388
|
+
* and the legacy app token (`app:` `grantee` + `grant` + `rev`) are normalized to a one-link chain; with `act` any
|
|
389
|
+
* `machine_id` must equal the first link, any top-level `grant` an app link's, and, on a token without `scope`, any `grantee`
|
|
390
|
+
* and `rev` the app link's (or, with no app link, `grantee` = `sub` and no `rev`). It authorizes nothing.
|
|
391
|
+
* Throws `MissingRevClaimError` for an `app:` link carrying `grant` and no `rev`.
|
|
392
|
+
*/
|
|
393
|
+
export async function verifyCallToken(options) {
|
|
394
|
+
const header = decodeProtectedHeader(options.token);
|
|
395
|
+
if (header.typ !== 'at+jwt' || (header.alg !== 'ES256' && header.alg !== 'RS256'))
|
|
396
|
+
throw new Error('Invalid call token header');
|
|
397
|
+
if (!nonEmptyString(header.kid))
|
|
398
|
+
throw new Error('Call token has no key id');
|
|
399
|
+
const keys = await keySet(options.jwks, header.kid);
|
|
400
|
+
const { payload } = await jwtVerify(options.token, createLocalJWKSet(keys), {
|
|
401
|
+
issuer: options.issuer, audience: options.audience, algorithms: ['ES256', 'RS256'], typ: 'at+jwt', requiredClaims: ['exp'],
|
|
402
|
+
});
|
|
403
|
+
if (payload.aud !== options.audience || typeof payload.exp !== 'number' || typeof payload.iat !== 'number' ||
|
|
404
|
+
!validatePrincipal(payload.sub, 'user') || !nonEmptyString(payload.client_id) ||
|
|
405
|
+
(payload.scope !== undefined && typeof payload.scope !== 'string')) {
|
|
406
|
+
throw new Error('Invalid call token claims');
|
|
407
|
+
}
|
|
408
|
+
const chain = chainOf(payload);
|
|
409
|
+
return { sub: payload.sub, aud: payload.aud, chain, ...(payload.scope === undefined ? {} : { scope: payload.scope }), claims: payload };
|
|
410
|
+
}
|
|
294
411
|
/** The directory's grant-sync notice: ES256 with the directory signing key named
|
|
295
412
|
* by `kid`, no `typ`, `aud` = the runtime's registered URL, a lifetime of at most
|
|
296
413
|
* five minutes, and a `jti` the runtime uses to answer replays without a sync. */
|
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.487",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/cli",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.487",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "UNLICENSED",
|
|
12
12
|
"workspaces": [
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"@earendil-works/pi-ai": "1.1.0",
|
|
19
19
|
"@earendil-works/pi-coding-agent": "1.1.0",
|
|
20
20
|
"@earendil-works/pi-tui": "1.1.0",
|
|
21
|
-
"@north-light/identity": "0.3.
|
|
21
|
+
"@north-light/identity": "0.3.487",
|
|
22
22
|
"cron-parser": "^5.6.0",
|
|
23
23
|
"esbuild": "^0.27.7",
|
|
24
24
|
"jose": "^6.2.12",
|
|
@@ -12116,17 +12116,17 @@
|
|
|
12116
12116
|
},
|
|
12117
12117
|
"packages/crouter-api": {
|
|
12118
12118
|
"name": "@north-light/api",
|
|
12119
|
-
"version": "0.3.
|
|
12119
|
+
"version": "0.3.487",
|
|
12120
12120
|
"license": "UNLICENSED"
|
|
12121
12121
|
},
|
|
12122
12122
|
"packages/crouter-env-docker": {
|
|
12123
12123
|
"name": "@north-light/env-docker",
|
|
12124
|
-
"version": "0.3.
|
|
12124
|
+
"version": "0.3.487",
|
|
12125
12125
|
"license": "UNLICENSED"
|
|
12126
12126
|
},
|
|
12127
12127
|
"packages/crouter-identity": {
|
|
12128
12128
|
"name": "@north-light/identity",
|
|
12129
|
-
"version": "0.3.
|
|
12129
|
+
"version": "0.3.487",
|
|
12130
12130
|
"license": "UNLICENSED",
|
|
12131
12131
|
"dependencies": {
|
|
12132
12132
|
"jose": "^6.2.1"
|
|
@@ -12134,20 +12134,20 @@
|
|
|
12134
12134
|
},
|
|
12135
12135
|
"packages/crouter-plugin": {
|
|
12136
12136
|
"name": "@north-light/plugin",
|
|
12137
|
-
"version": "0.3.
|
|
12137
|
+
"version": "0.3.487",
|
|
12138
12138
|
"license": "UNLICENSED",
|
|
12139
12139
|
"dependencies": {
|
|
12140
|
-
"@north-light/api": "0.3.
|
|
12141
|
-
"@north-light/identity": "0.3.
|
|
12140
|
+
"@north-light/api": "0.3.487",
|
|
12141
|
+
"@north-light/identity": "0.3.487"
|
|
12142
12142
|
}
|
|
12143
12143
|
},
|
|
12144
12144
|
"packages/crouter-sdk": {
|
|
12145
12145
|
"name": "@north-light/sdk",
|
|
12146
|
-
"version": "0.3.
|
|
12146
|
+
"version": "0.3.487",
|
|
12147
12147
|
"license": "UNLICENSED",
|
|
12148
12148
|
"dependencies": {
|
|
12149
|
-
"@north-light/api": "0.3.
|
|
12150
|
-
"@north-light/identity": "0.3.
|
|
12149
|
+
"@north-light/api": "0.3.487",
|
|
12150
|
+
"@north-light/identity": "0.3.487",
|
|
12151
12151
|
"jose": "^6.2.1"
|
|
12152
12152
|
},
|
|
12153
12153
|
"bin": {
|