@north-light/cli 0.3.485 → 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.
Files changed (35) hide show
  1. package/dist/api/contract.d.ts +1 -1
  2. package/dist/api/contract.js +1 -1
  3. package/dist/builtin-memory/crouter-concepts/scopes-and-trust.md +2 -1
  4. package/dist/builtin-memory/crouter-sdk/getting-started.md +1 -1
  5. package/dist/builtin-memory/crouter-sdk/release-notes.md +15 -0
  6. package/dist/builtin-memory/northlight-apps/guides/files-and-documents.md +12 -3
  7. package/dist/builtin-memory/northlight-apps/guides/grant-changes.md +16 -0
  8. package/dist/builtin-memory/northlight-apps/guides/many-users.md +1 -1
  9. package/dist/builtin-memory/northlight-apps/guides/test-your-app.md +1 -1
  10. package/dist/clients/attach/viewer.js +180 -180
  11. package/dist/core/grants/cover.d.ts +14 -0
  12. package/dist/core/grants/cover.js +1 -0
  13. package/dist/core/grants/mirror.d.ts +3 -1
  14. package/dist/core/grants/mirror.js +2 -2
  15. package/dist/core/graph/spaces/person.js +3 -3
  16. package/dist/daemon/api/app-listener.js +13 -13
  17. package/dist/daemon/api/principal.d.ts +10 -18
  18. package/dist/daemon/api/principal.js +1 -1
  19. package/dist/daemon/peer/authority.d.ts +27 -0
  20. package/dist/daemon/peer/authority.js +1 -0
  21. package/dist/daemon/peer/ingress.d.ts +17 -15
  22. package/dist/daemon/peer/ingress.js +1 -1
  23. package/dist/daemon/peer/relay-service.js +1 -1
  24. package/docs/apps/guides/files-and-documents.md +12 -3
  25. package/docs/apps/guides/grant-changes.md +16 -0
  26. package/docs/apps/guides/many-users.md +1 -1
  27. package/docs/apps/guides/test-your-app.md +1 -1
  28. package/docs/concepts/scopes-and-trust.md +2 -1
  29. package/docs/sdk/getting-started.md +1 -1
  30. package/docs/sdk/release-notes.md +15 -0
  31. package/package.json +2 -2
  32. package/packages/crouter-identity/dist/index.d.ts +56 -1
  33. package/packages/crouter-identity/dist/index.js +120 -0
  34. package/packages/crouter-identity/package.json +1 -1
  35. package/runtime.lock.json +12 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/cli",
3
- "version": "0.3.485",
3
+ "version": "0.3.487",
4
4
  "description": "crtr — agent runtime with memory, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -137,7 +137,7 @@
137
137
  },
138
138
  "license": "UNLICENSED",
139
139
  "dependencies": {
140
- "@north-light/identity": "0.3.485",
140
+ "@north-light/identity": "0.3.487",
141
141
  "@earendil-works/pi-agent-core": "1.1.0",
142
142
  "@earendil-works/pi-ai": "1.1.0",
143
143
  "@earendil-works/pi-coding-agent": "1.1.0",
@@ -22,7 +22,7 @@ export interface IdTokenClaims extends JWTPayload {
22
22
  family_name?: string;
23
23
  picture?: string;
24
24
  }
25
- export type Scope = 'crtr:llm' | 'crtr:act' | 'crtr:schedule' | `crtr:memory:${'read' | 'rw'}:user${'' | `:${string}`}` | 'crtr:memory:manage:user' | `crtr:memory:${'read' | 'rw'}:app:${string}` | `crtr:files:${'read' | 'rw'}:${'user' | `app:${string}`}:${string}` | 'crtr:files:share' | `crtr:${'conversations' | 'objects'}:read:app:${string}` | `crtr:objects:${'message' | 'spawn'}:app:${string}` | `crtr:objects:${'read' | 'watch' | 'link' | 'write'}:${'user' | 'node' | `profile:${string}` | `app:${string}`}` | `crtr:peer:${string}` | `${string}:${string}`;
25
+ export type Scope = 'crtr:llm' | 'crtr:act' | 'crtr:schedule' | `crtr:memory:${'read' | 'rw'}:user${'' | `:${string}`}` | 'crtr:memory:manage:user' | 'crtr:memory:read_private:user' | `crtr:memory:${'read' | 'rw'}:app:${string}` | `crtr:files:${'read' | 'rw'}:${'user' | `app:${string}`}:${string}` | 'crtr:files:share' | `crtr:${'conversations' | 'objects'}:read:app:${string}` | `crtr:objects:${'message' | 'spawn'}:app:${string}` | `crtr:objects:${'read' | 'watch' | 'link' | 'write'}:${'user' | 'node' | `profile:${string}` | `app:${string}`}` | `crtr:peer:${string}` | `${string}:${string}`;
26
26
  /** App-memory form keys omit the app id. */
27
27
  export declare const ENFORCED: ReadonlySet<string>;
28
28
  export declare function parseScope(value: string, grantee?: string): Scope;
@@ -67,6 +67,7 @@ export declare class JwksCache {
67
67
  private fetchKeys;
68
68
  forKid(kid: string | undefined): Promise<JSONWebKeySet>;
69
69
  }
70
+ /** @deprecated Use `verifyCallToken`; removed after the peer-token rollout window (plan I2). */
70
71
  export declare function verifyAccessToken(options: {
71
72
  token: string;
72
73
  jwks: JSONWebKeySet | JwksCache;
@@ -95,6 +96,7 @@ export interface MachineTokenClaims extends JWTPayload {
95
96
  * refused, so an app grant token never verifies here; `verifyAccessToken` requires an `app:` grantee, so a machine
96
97
  * token never verifies there.
97
98
  */
99
+ /** @deprecated Use `verifyCallToken`; removed after the peer-token rollout window (plan I2). */
98
100
  export declare function verifyMachineToken(options: {
99
101
  token: string;
100
102
  jwks: JSONWebKeySet | JwksCache;
@@ -133,12 +135,65 @@ export interface OutboundTokenClaims extends JWTPayload {
133
135
  * an `app:` or `user:` principal; the remaining claims are checked for presence and type only; which of them gates a
134
136
  * call is the caller's rule (D3-TOK-13 gates on `scope` alone, and nothing gates on `act`).
135
137
  */
138
+ /** @deprecated Use `verifyCallToken`; removed after the peer-token rollout window (plan I2). */
136
139
  export declare function verifyOutboundToken(options: {
137
140
  token: string;
138
141
  jwks: JSONWebKeySet | JwksCache;
139
142
  issuer: string;
140
143
  audience: string;
141
144
  }): Promise<OutboundTokenClaims>;
145
+ /** One link of a call token's actor chain. `grant` and `rev` appear only on an `app:` link. */
146
+ export interface CallLink {
147
+ sub: string;
148
+ grant?: string;
149
+ rev?: number;
150
+ }
151
+ /** The verified payload of a call token. Fields beyond `sub`/`aud`/`client_id` are present on some uses only. */
152
+ export interface CallTokenClaims extends JWTPayload {
153
+ iss: string;
154
+ aud: string;
155
+ /** The caller's person, `user:<id>`. */
156
+ sub: string;
157
+ client_id: string;
158
+ exp: number;
159
+ iat: number;
160
+ /** The RFC 8693 actor chain as signed; absent on the legacy shapes. */
161
+ act?: unknown;
162
+ scope?: string;
163
+ /** Legacy same-account machine token (removed at the flag flip). */
164
+ machine_id?: string;
165
+ grantee?: string;
166
+ initiating_app?: string;
167
+ grant?: string;
168
+ rev?: number;
169
+ }
170
+ /** A verified call token. The verifier authorizes nothing: the caller decides authority from `chain`. */
171
+ export interface CallToken {
172
+ /** `user:<id>`. */
173
+ sub: string;
174
+ aud: string;
175
+ /** The actor chain, index 0 = the presenter. Legacy shapes are normalized to the same form. */
176
+ chain: CallLink[];
177
+ /** Present only on provider tokens. */
178
+ scope?: string;
179
+ claims: CallTokenClaims;
180
+ }
181
+ /**
182
+ * Verifies a call token of any use (peer, app access, outbound provider, run notice) and returns its actor chain,
183
+ * presenter first. Checks in order: header `typ` `at+jwt`, `alg` ES256 or RS256, non-empty `kid`; signature; `iss`;
184
+ * `aud` a string equal to `audience`; `exp` and `iat` numbers; `sub` = `user:<id>`; `client_id` non-empty; `scope`
185
+ * a string when present; then the chain. Without `act` the legacy machine token (`machine_id`, `grantee` = `sub`)
186
+ * and the legacy app token (`app:` `grantee` + `grant` + `rev`) are normalized to a one-link chain; with `act` any
187
+ * `machine_id` must equal the first link, any top-level `grant` an app link's, and, on a token without `scope`, any `grantee`
188
+ * and `rev` the app link's (or, with no app link, `grantee` = `sub` and no `rev`). It authorizes nothing.
189
+ * Throws `MissingRevClaimError` for an `app:` link carrying `grant` and no `rev`.
190
+ */
191
+ export declare function verifyCallToken(options: {
192
+ token: string;
193
+ jwks: JSONWebKeySet | JwksCache;
194
+ issuer: string;
195
+ audience: string;
196
+ }): Promise<CallToken>;
142
197
  export interface SyncNoticeClaims extends JWTPayload {
143
198
  purpose: 'grant_sync';
144
199
  iat: number;
@@ -21,6 +21,9 @@ function selector(value) {
21
21
  return { kind: 'share', value };
22
22
  if (value === 'crtr:memory:manage:user')
23
23
  return { kind: 'memory', access: 'manage', holder: 'user' };
24
+ // Read-only access to the person's `public` and `private` memory (never `friends`). It names no document path.
25
+ if (value === 'crtr:memory:read_private:user')
26
+ return { kind: 'memory', access: 'read_private', holder: 'user' };
24
27
  let match = /^crtr:memory:(read|rw):user(?::(.+))?$/.exec(value);
25
28
  if (match && (match[2] === undefined || validPath(match[2])))
26
29
  return { kind: 'memory', access: match[1], holder: 'user', path: match[2] };
@@ -212,6 +215,7 @@ function validatePrincipal(value, kind) {
212
215
  function keySet(jwks, kid) {
213
216
  return jwks instanceof JwksCache ? jwks.forKid(kid) : Promise.resolve(jwks);
214
217
  }
218
+ /** @deprecated Use `verifyCallToken`; removed after the peer-token rollout window (plan I2). */
215
219
  export async function verifyAccessToken(options) {
216
220
  const header = decodeProtectedHeader(options.token);
217
221
  if (header.typ !== 'at+jwt' || header.alg !== 'ES256')
@@ -238,6 +242,7 @@ export async function verifyAccessToken(options) {
238
242
  * refused, so an app grant token never verifies here; `verifyAccessToken` requires an `app:` grantee, so a machine
239
243
  * token never verifies there.
240
244
  */
245
+ /** @deprecated Use `verifyCallToken`; removed after the peer-token rollout window (plan I2). */
241
246
  export async function verifyMachineToken(options) {
242
247
  const header = decodeProtectedHeader(options.token);
243
248
  if (header.typ !== 'at+jwt' || header.alg !== 'ES256')
@@ -270,6 +275,7 @@ function isActor(value) {
270
275
  * an `app:` or `user:` principal; the remaining claims are checked for presence and type only; which of them gates a
271
276
  * call is the caller's rule (D3-TOK-13 gates on `scope` alone, and nothing gates on `act`).
272
277
  */
278
+ /** @deprecated Use `verifyCallToken`; removed after the peer-token rollout window (plan I2). */
273
279
  export async function verifyOutboundToken(options) {
274
280
  const header = decodeProtectedHeader(options.token);
275
281
  if (header.typ !== 'at+jwt' || (header.alg !== 'ES256' && header.alg !== 'RS256'))
@@ -288,6 +294,120 @@ export async function verifyOutboundToken(options) {
288
294
  }
289
295
  return payload;
290
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
+ }
291
411
  /** The directory's grant-sync notice: ES256 with the directory signing key named
292
412
  * by `kid`, no `typ`, `aud` = the runtime's registered URL, a lifetime of at most
293
413
  * five minutes, and a `jti` the runtime uses to answer replays without a sync. */
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/identity",
3
- "version": "0.3.485",
3
+ "version": "0.3.487",
4
4
  "description": "Northlight access-token verification and scope rules",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/runtime.lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@north-light/cli",
3
- "version": "0.3.485",
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.485",
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.485",
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.485",
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.485",
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.485",
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.485",
12137
+ "version": "0.3.487",
12138
12138
  "license": "UNLICENSED",
12139
12139
  "dependencies": {
12140
- "@north-light/api": "0.3.485",
12141
- "@north-light/identity": "0.3.485"
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.485",
12146
+ "version": "0.3.487",
12147
12147
  "license": "UNLICENSED",
12148
12148
  "dependencies": {
12149
- "@north-light/api": "0.3.485",
12150
- "@north-light/identity": "0.3.485",
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": {