@north-light/sdk 0.3.490 → 0.3.492

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/client.d.ts CHANGED
@@ -67,8 +67,12 @@ export interface CrouterOptions {
67
67
  /** The namespaces of `Crouter` that only the owner's own daemon serves. A person's app listener answers them `404 unknown operation`. */
68
68
  export type OwnerOnly = 'nodes' | 'bash' | 'canvas' | 'crons' | 'structuredRequests' | 'customObjects' | 'human' | 'models' | 'system' | 'auth';
69
69
  /** What an app reaches on a person's runtime (the app listener): `Crouter` without the owner-daemon namespaces, `files.download`
70
- * only, and `profiles.list` only. `Crouter` is assignable to it, so a function that takes `AppClient` takes either. */
70
+ * only, `profiles.list` only, `customObjects` register/publish/unregister and `canvas` watch/unwatch. `Crouter` is assignable to it, so a function that takes `AppClient` takes either. */
71
71
  export type AppClient = Omit<Crouter, OwnerOnly | 'files' | 'profiles'> & {
72
+ /** Register, publish to and unregister the custom objects the app owns (`receive` and `ack` are owner-daemon only). */
73
+ readonly customObjects: Pick<CustomObjects, 'register' | 'publish' | 'unregister'>;
74
+ /** Link nodes to the app's objects: `watch` and `unwatch` with `watcher` (and `watcher_machine` for a node of another machine), under `crtr:objects:link:node`. */
75
+ readonly canvas: Pick<Canvas, 'watch' | 'unwatch'>;
72
76
  /** Download a file the person's runtime holds (`files.download`); the other `files` calls are owner-daemon only. */
73
77
  readonly files: Pick<Files, 'download'>;
74
78
  /** The profiles the app may start runs with (`profiles.list`); `ensure` and `retrieve` are owner-daemon only. */
@@ -2,6 +2,6 @@ export { errorCodes } from '@north-light/api';
2
2
  export type { ErrorCode, ErrorEnvelope, ErrorOrigin, ErrorType } from '@north-light/api';
3
3
  export { mapError } from './errors.js';
4
4
  /** Codes the SDK raises itself rather than reading from a daemon or directory response. */
5
- export declare const sdkErrorCodes: readonly ["crouter_error", "id_token_invalid", "oauth_nonce_missing", "oauth_state_mismatch", "oauth_exchange_mismatch", "not_connected", "refusal_handler_reentry", "run_idle", "waiting_on_user", "request_aborted", "request_timeout", "deadline_exceeded", "connection_error", "transport_error", "daemon_unavailable", "daemon_request_interrupted", "daemon_health_unavailable", "runtime_version_unsupported", "invalid_response", "stream_error", "stream_ended"];
5
+ export declare const sdkErrorCodes: readonly ["crouter_error", "id_token_invalid", "oauth_nonce_missing", "oauth_state_mismatch", "oauth_exchange_mismatch", "not_connected", "refusal_handler_reentry", "run_idle", "waiting_on_user", "request_aborted", "request_timeout", "deadline_exceeded", "connection_error", "transport_error", "daemon_unavailable", "daemon_request_interrupted", "daemon_health_unavailable", "runtime_version_unsupported", "invalid_response", "stream_error", "stream_ended", "no_hosted_runtime", "not_granted"];
6
6
  /** A code the SDK raises itself. An `APIError`'s `code` is one of these or an `ErrorCode` from the contract. */
7
7
  export type SdkErrorCode = typeof sdkErrorCodes[number];
@@ -40,4 +40,8 @@ export const sdkErrorCodes = [
40
40
  /** An event stream failed, or ended before the reply or node it carried did. */
41
41
  'stream_error',
42
42
  'stream_ended',
43
+ /** `oauth.appClient(userId)`: the person has no hosted runtime (`NoHostedRuntimeError`). */
44
+ 'no_hosted_runtime',
45
+ /** `oauth.appClient(userId)`: the person has no active grant for the app (`NotGrantedError`). */
46
+ 'not_granted',
43
47
  ];
package/dist/errors.d.ts CHANGED
@@ -26,6 +26,12 @@ export interface RuntimeVersionDetails {
26
26
  required_contract?: string;
27
27
  runtime_contract?: string | null;
28
28
  }
29
+ /** `oauth.appClient(userId)`: the person has no hosted runtime for the app to act on (directory `invalid_grant` "user has no runtime"). `code` is `no_hosted_runtime`, `status` 409; not retryable. */
30
+ export declare class NoHostedRuntimeError extends APIError {
31
+ }
32
+ /** `oauth.appClient(userId)`: the person has no active grant for this app (directory `invalid_grant` "no active grant"), so they have not signed in to it. `code` is `not_granted`, `status` 409; not retryable. */
33
+ export declare class NotGrantedError extends APIError {
34
+ }
29
35
  /** The daemon rejected the request: HTTP 400. */
30
36
  export declare class BadRequestError extends APIError {
31
37
  }
package/dist/errors.js CHANGED
@@ -20,6 +20,12 @@ export class RuntimeVersionError extends APIError {
20
20
  /** The version the runtime named in `Crouter-Runtime-Version`, or `null` when it sent none (a runtime from before the header existed). */
21
21
  get runtimeVersion() { return this.details.runtime_version; }
22
22
  }
23
+ /** `oauth.appClient(userId)`: the person has no hosted runtime for the app to act on (directory `invalid_grant` "user has no runtime"). `code` is `no_hosted_runtime`, `status` 409; not retryable. */
24
+ export class NoHostedRuntimeError extends APIError {
25
+ }
26
+ /** `oauth.appClient(userId)`: the person has no active grant for this app (directory `invalid_grant` "no active grant"), so they have not signed in to it. `code` is `not_granted`, `status` 409; not retryable. */
27
+ export class NotGrantedError extends APIError {
28
+ }
23
29
  /** The daemon rejected the request: HTTP 400. */
24
30
  export class BadRequestError extends APIError {
25
31
  }
package/dist/index.d.ts CHANGED
@@ -33,7 +33,7 @@ export type { RunNotice, VerifyRunNoticeOptions } from './resources/run-notice.j
33
33
  export type { NodeStreamEvent, NodeStreamEventListener, NodeStreamEventType } from './resources/node-stream.js';
34
34
  export { describeToolDefault, followActivity } from './resources/activity.js';
35
35
  export type { ActivityStep, DescribeTool } from './resources/activity.js';
36
- export { APIError, APIConnectionError, APIConnectionTimeoutError, APIUserAbortError, AuthenticationError, BadRequestError, ConflictError, CrouterError, DeadlineExceededError, InternalServerError, NotFoundError, PermissionDeniedError, RateLimitError, RuntimeVersionError, UnprocessableEntityError, mapError, } from './errors.js';
36
+ export { APIError, APIConnectionError, APIConnectionTimeoutError, APIUserAbortError, AuthenticationError, BadRequestError, ConflictError, CrouterError, DeadlineExceededError, InternalServerError, NoHostedRuntimeError, NotGrantedError, NotFoundError, PermissionDeniedError, RateLimitError, RuntimeVersionError, UnprocessableEntityError, mapError, } from './errors.js';
37
37
  export type { DeadlineDetails, RuntimeVersionDetails } from './errors.js';
38
38
  export { SDK_VERSION } from './version.js';
39
39
  export { setupLink } from './setup-link.js';
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ export { parsePrivateJwk, privateKeyFromFile } from './oauth/keygen.js';
12
12
  export { cursorFromRequest, forwardRunEvents } from './resources/forward.js';
13
13
  export { RUN_NOTICE_SCOPE, RunNoticeError, verifyRunNotice } from './resources/run-notice.js';
14
14
  export { describeToolDefault, followActivity } from './resources/activity.js';
15
- export { APIError, APIConnectionError, APIConnectionTimeoutError, APIUserAbortError, AuthenticationError, BadRequestError, ConflictError, CrouterError, DeadlineExceededError, InternalServerError, NotFoundError, PermissionDeniedError, RateLimitError, RuntimeVersionError, UnprocessableEntityError, mapError, } from './errors.js';
15
+ export { APIError, APIConnectionError, APIConnectionTimeoutError, APIUserAbortError, AuthenticationError, BadRequestError, ConflictError, CrouterError, DeadlineExceededError, InternalServerError, NoHostedRuntimeError, NotGrantedError, NotFoundError, PermissionDeniedError, RateLimitError, RuntimeVersionError, UnprocessableEntityError, mapError, } from './errors.js';
16
16
  export { SDK_VERSION } from './version.js';
17
17
  export { setupLink } from './setup-link.js';
18
18
  export { sdkErrorCodes } from './error-codes.js';
@@ -0,0 +1,25 @@
1
+ /** What the directory answers an app's `client_credentials` request for `sub=user:<id>` with. */
2
+ export interface AppTokenResponse {
3
+ access_token?: unknown;
4
+ runtimeUrl?: unknown;
5
+ expires_in?: unknown;
6
+ }
7
+ /** An app token for one person's runtime. */
8
+ export interface AppToken {
9
+ accessToken: string;
10
+ runtimeUrl: string;
11
+ expiresAt: number;
12
+ }
13
+ /** Reads the directory's answer into an `AppToken`; `expiresAt` is the token's own `exp`, else `expires_in` from now. */
14
+ export declare function appTokenFrom(response: AppTokenResponse, now?: number): AppToken;
15
+ /** A token stays usable until this long before its expiry. */
16
+ export declare const APP_TOKEN_SKEW_MS = 60000;
17
+ /** One minted app token per person, shared by every client of that person and by concurrent callers (one mint in flight per person). */
18
+ export declare class AppTokenCache {
19
+ private readonly mint;
20
+ private readonly tokens;
21
+ private readonly minting;
22
+ constructor(mint: (userId: string) => Promise<AppToken>);
23
+ /** The cached token while it has more than a minute left, else a fresh mint. `rejected` is the token a runtime just answered 401 to: it is never served again. */
24
+ get(userId: string, rejected?: string): Promise<AppToken>;
25
+ }
@@ -0,0 +1,41 @@
1
+ import { decodeJwt } from 'jose';
2
+ import { CrouterError } from '../errors.js';
3
+ /** Reads the directory's answer into an `AppToken`; `expiresAt` is the token's own `exp`, else `expires_in` from now. */
4
+ export function appTokenFrom(response, now = Date.now()) {
5
+ if (typeof response.access_token !== 'string' || !response.access_token || typeof response.runtimeUrl !== 'string' || !response.runtimeUrl) {
6
+ throw new CrouterError('Directory returned an incomplete app token (access_token and runtimeUrl are required)');
7
+ }
8
+ const exp = decodeJwt(response.access_token).exp;
9
+ const expiresAt = typeof exp === 'number' ? exp * 1_000
10
+ : typeof response.expires_in === 'number' ? now + response.expires_in * 1_000
11
+ : undefined;
12
+ if (expiresAt === undefined)
13
+ throw new CrouterError('Directory returned an app token with no expiry');
14
+ return { accessToken: response.access_token, runtimeUrl: response.runtimeUrl, expiresAt };
15
+ }
16
+ /** A token stays usable until this long before its expiry. */
17
+ export const APP_TOKEN_SKEW_MS = 60_000;
18
+ /** One minted app token per person, shared by every client of that person and by concurrent callers (one mint in flight per person). */
19
+ export class AppTokenCache {
20
+ mint;
21
+ tokens = new Map();
22
+ minting = new Map();
23
+ constructor(mint) {
24
+ this.mint = mint;
25
+ }
26
+ /** The cached token while it has more than a minute left, else a fresh mint. `rejected` is the token a runtime just answered 401 to: it is never served again. */
27
+ get(userId, rejected) {
28
+ const cached = this.tokens.get(userId);
29
+ if (cached && cached.accessToken !== rejected && cached.expiresAt - Date.now() > APP_TOKEN_SKEW_MS)
30
+ return Promise.resolve(cached);
31
+ const pending = this.minting.get(userId);
32
+ if (pending)
33
+ return pending;
34
+ const minted = this.mint(userId).then((token) => { this.tokens.set(userId, token); return token; });
35
+ this.minting.set(userId, minted);
36
+ const done = () => { if (this.minting.get(userId) === minted)
37
+ this.minting.delete(userId); };
38
+ minted.then(done, done);
39
+ return minted;
40
+ }
41
+ }
@@ -108,8 +108,9 @@ export interface OAuth2Options {
108
108
  declare const AppCrouter: new (options: CrouterOptions) => AppClient;
109
109
  /** A runtime client on one person's stored connection (`oauth.client(userId)`): an `AppClient` that refreshes its
110
110
  * token through the `OAuth2Client`'s store, plus whose connection it is and the runtime it calls. It offers only what
111
- * the person's app listener serves. The owner-daemon namespaces (`nodes`, `crons`, `canvas`, …) are absent from its
112
- * type and throw `CrouterError` if reached anyway; `files` is `download` only and `profiles` is `list` only.
111
+ * the person's app listener serves. The owner-daemon namespaces (`nodes`, `crons`, …) are absent from its
112
+ * type and throw `CrouterError` if reached anyway; `files` is `download` only, `profiles` is `list` only, `customObjects` is
113
+ * `register`, `publish` and `unregister`, and `canvas` is `watch` and `unwatch`.
113
114
  * `request` still sends any route. */
114
115
  export declare class ConnectedCrouter extends AppCrouter {
115
116
  /** The person whose connection this client uses (`user:<id>`). */
@@ -126,6 +127,7 @@ export declare class OAuth2Client {
126
127
  private readonly fetcher;
127
128
  private readonly timeoutMs;
128
129
  private readonly store;
130
+ private readonly appTokens;
129
131
  private jwks?;
130
132
  private discoveryPromise?;
131
133
  /** One refresh per (user, refresh token) in this process; a client holding a newer token never joins an older refusal. */
@@ -228,6 +230,18 @@ export declare class OAuth2Client {
228
230
  * `refusal_handler_reentry`.
229
231
  */
230
232
  client(userId: string): Promise<ConnectedCrouter>;
233
+ /**
234
+ * A runtime client for the app itself acting on `userId`'s hosted runtime, with no sign-in of theirs and no stored connection:
235
+ * the app signs a `client_credentials` request for `sub=user:<id>` and the directory answers a token for the person's runtime
236
+ * (`runtimeUrl` on the client). It needs the app registered with the `app_token_for_user` operation and an active grant from
237
+ * the person. Tokens are cached per person until a minute before they expire; the client mints again on expiry and once on a 401.
238
+ *
239
+ * Throws `NoHostedRuntimeError` (`no_hosted_runtime`) when the person has no hosted runtime, `NotGrantedError` (`not_granted`)
240
+ * when they have no active grant for the app; a runtime still provisioning (503), a directory 5xx or a 429 throws an error with
241
+ * `retryable` true; `invalid_client` is a 401 `unauthorized` naming the key.
242
+ */
243
+ appClient(userId: string): Promise<ConnectedCrouter>;
244
+ private mintAppToken;
231
245
  /**
232
246
  * Follow the app's background runs until each comes to rest, and hand each ending to `onEnded` (see
233
247
  * `RunWatchOptions`). Runs are opened with `client(userId)`, so refusals reach `onRefusal`. Call `start()`.
@@ -1,11 +1,12 @@
1
1
  import { calculateJwkThumbprint, decodeJwt, decodeProtectedHeader, exportJWK, importJWK, importPKCS8, SignJWT } from 'jose';
2
2
  import { boundedFetch, FetchDeadlineError, JwksCache, parseScope, verifyIdToken } from '@north-light/identity';
3
3
  import { deadlineExceeded, sleepWithin } from '@north-light/api';
4
- import { APIError, CrouterError, mapError } from '../errors.js';
4
+ import { APIError, CrouterError, NoHostedRuntimeError, NotGrantedError, mapError } from '../errors.js';
5
5
  import { Crouter } from '../client.js';
6
6
  import { withinDeadline } from '../resources/request.js';
7
7
  import { RunWatcher } from '../resources/run-watcher.js';
8
8
  import { GrantWatcher } from '../resources/grant-watcher.js';
9
+ import { AppTokenCache, appTokenFrom } from './app-token.js';
9
10
  import { attachRefusal, isRefusalCode, refusalKindOf } from './refusal.js';
10
11
  export { authorizationRefusalOf, refusalOf } from './refusal.js';
11
12
  /** A connection store for one process (local development and tests): its lock does not reach other processes. */
@@ -62,13 +63,15 @@ function httpsUrl(value) {
62
63
  return undefined;
63
64
  }
64
65
  }
65
- const OWNER_ONLY = ['nodes', 'bash', 'canvas', 'crons', 'structuredRequests', 'customObjects', 'human', 'models', 'system', 'auth'];
66
+ /** The owner-daemon namespaces a person's runtime does not serve to an app. `canvas` and `customObjects` are narrowed below, not absent. */
67
+ const OWNER_ONLY = ['nodes', 'bash', 'crons', 'structuredRequests', 'human', 'models', 'system', 'auth'];
66
68
  /** The class `ConnectedCrouter` extends: `Crouter`'s implementation under the type of what a person's app listener serves. */
67
69
  const AppCrouter = Crouter;
68
70
  /** A runtime client on one person's stored connection (`oauth.client(userId)`): an `AppClient` that refreshes its
69
71
  * token through the `OAuth2Client`'s store, plus whose connection it is and the runtime it calls. It offers only what
70
- * the person's app listener serves. The owner-daemon namespaces (`nodes`, `crons`, `canvas`, …) are absent from its
71
- * type and throw `CrouterError` if reached anyway; `files` is `download` only and `profiles` is `list` only.
72
+ * the person's app listener serves. The owner-daemon namespaces (`nodes`, `crons`, …) are absent from its
73
+ * type and throw `CrouterError` if reached anyway; `files` is `download` only, `profiles` is `list` only, `customObjects` is
74
+ * `register`, `publish` and `unregister`, and `canvas` is `watch` and `unwatch`.
72
75
  * `request` still sends any route. */
73
76
  export class ConnectedCrouter extends AppCrouter {
74
77
  /** The person whose connection this client uses (`user:<id>`). */
@@ -87,6 +90,9 @@ export class ConnectedCrouter extends AppCrouter {
87
90
  const { files, profiles } = this;
88
91
  Object.defineProperty(this, 'files', { value: { download: files.download.bind(files) } });
89
92
  Object.defineProperty(this, 'profiles', { value: { list: profiles.list } });
93
+ const { customObjects, canvas } = this;
94
+ Object.defineProperty(this, 'customObjects', { value: { register: customObjects.register.bind(customObjects), publish: customObjects.publish.bind(customObjects), unregister: customObjects.unregister.bind(customObjects) } });
95
+ Object.defineProperty(this, 'canvas', { value: { watch: canvas.watch.bind(canvas), unwatch: canvas.unwatch.bind(canvas) } });
90
96
  }
91
97
  }
92
98
  /** The default deadline of one directory operation (`OAuth2Options.timeoutMs`): a discovery read is forgotten when it passes. */
@@ -106,6 +112,7 @@ export class OAuth2Client {
106
112
  fetcher;
107
113
  timeoutMs;
108
114
  store;
115
+ appTokens = new AppTokenCache((userId) => this.mintAppToken(userId));
109
116
  jwks;
110
117
  discoveryPromise;
111
118
  /** One refresh per (user, refresh token) in this process; a client holding a newer token never joins an older refusal. */
@@ -422,6 +429,30 @@ export class OAuth2Client {
422
429
  ...(this.options.fetch ? { fetch: this.options.fetch } : {}),
423
430
  });
424
431
  }
432
+ /**
433
+ * A runtime client for the app itself acting on `userId`'s hosted runtime, with no sign-in of theirs and no stored connection:
434
+ * the app signs a `client_credentials` request for `sub=user:<id>` and the directory answers a token for the person's runtime
435
+ * (`runtimeUrl` on the client). It needs the app registered with the `app_token_for_user` operation and an active grant from
436
+ * the person. Tokens are cached per person until a minute before they expire; the client mints again on expiry and once on a 401.
437
+ *
438
+ * Throws `NoHostedRuntimeError` (`no_hosted_runtime`) when the person has no hosted runtime, `NotGrantedError` (`not_granted`)
439
+ * when they have no active grant for the app; a runtime still provisioning (503), a directory 5xx or a 429 throws an error with
440
+ * `retryable` true; `invalid_client` is a 401 `unauthorized` naming the key.
441
+ */
442
+ async appClient(userId) {
443
+ if (!/^user:[^:]+$/.test(userId))
444
+ throw new CrouterError(`appClient needs a Northlight user id (user:<id>), got ${JSON.stringify(userId)}`);
445
+ const { runtimeUrl } = await this.appTokens.get(userId);
446
+ let used;
447
+ return new ConnectedCrouter({ userId, runtimeUrl }, {
448
+ tokenSource: async () => { used = (await this.appTokens.get(userId)).accessToken; return used; },
449
+ onUnauthorized: async () => { await this.appTokens.get(userId, used); },
450
+ ...(this.options.fetch ? { fetch: this.options.fetch } : {}),
451
+ });
452
+ }
453
+ async mintAppToken(userId) {
454
+ return appTokenFrom(await this.tokenResponse({ grant_type: 'client_credentials', sub: userId }));
455
+ }
425
456
  /**
426
457
  * Follow the app's background runs until each comes to rest, and hand each ending to `onEnded` (see
427
458
  * `RunWatchOptions`). Runs are opened with `client(userId)`, so refusals reach `onRefusal`. Call `start()`.
@@ -550,7 +581,8 @@ export class OAuth2Client {
550
581
  // invalid_grant carries the directory's reason as its error_description; a reason it does not name is grant_revoked.
551
582
  const description = refusal.error_description ?? '';
552
583
  const code = refusal.error === 'invalid_grant'
553
- ? description === 'runtime_provisioning' || isRefusalCode(description) ? description : 'grant_revoked'
584
+ ? description === 'runtime_provisioning' || isRefusalCode(description) ? description
585
+ : description === 'user has no runtime' ? 'no_hosted_runtime' : description === 'no active grant' ? 'not_granted' : 'grant_revoked'
554
586
  : refusal.error === 'invalid_client' ? 'unauthorized' : 'invalid_request';
555
587
  const reconnect = isRefusalCode(code);
556
588
  if (code === 'runtime_provisioning' && attempt < 2) {
@@ -565,11 +597,14 @@ export class OAuth2Client {
565
597
  continue;
566
598
  }
567
599
  const message = refusal.error_description ?? refusal.error ?? 'Directory token request failed';
600
+ if (code === 'no_hosted_runtime' || code === 'not_granted') {
601
+ throw new (code === 'no_hosted_runtime' ? NoHostedRuntimeError : NotGrantedError)(409, code, message, undefined, response.headers, { origin: 'directory', type: 'invalid_request_error', retryable: false });
602
+ }
568
603
  const keyHint = code === 'unauthorized' ? await describeSigningKey(key, kid) : undefined;
569
604
  // mapError gives the refusal its status class (401 → AuthenticationError) as errors.md documents.
570
605
  throw mapError(new APIError(code === 'runtime_provisioning' ? 503 : reconnect || code === 'unauthorized' ? 401 : response.status, code, keyHint ? `${message}: ${keyHint.hint}` : message, code === 'invalid_request' ? { oauth_error: refusal.error }
571
606
  : keyHint ? { oauth_error: 'invalid_client', kid: keyHint.kid, ...(keyHint.thumbprint ? { key_thumbprint: keyHint.thumbprint } : {}) } : undefined, response.headers, { origin: 'directory', type: code === 'runtime_provisioning' ? 'server_error' : code === 'invalid_request' ? 'invalid_request_error' : 'authentication_error',
572
- retryable: code === 'runtime_provisioning', user_action: reconnect ? 'reconnect' : undefined }));
607
+ retryable: code === 'runtime_provisioning' || response.status >= 500 || response.status === 429, user_action: reconnect ? 'reconnect' : undefined }));
573
608
  }
574
609
  }
575
610
  }
@@ -2,7 +2,7 @@
2
2
  name: northlight
3
3
  description: "Use when building, debugging or shipping an app that signs people in with Northlight, starts runs on their Northlight runtime, or calls Northlight providers (sms, search, integrations, context, device) with @north-light/sdk."
4
4
  metadata:
5
- sdk-version: "0.3.490"
5
+ sdk-version: "0.3.492"
6
6
  ---
7
7
 
8
8
  # Building a Northlight app
@@ -11,7 +11,7 @@ An app on Northlight signs people in with their Northlight account, then starts
11
11
 
12
12
  ## First: check this skill matches the SDK
13
13
 
14
- This skill is for SDK version `0.3.490`. If the project has `node_modules/@north-light/sdk/package.json`, compare its `version` with that number. If they differ, run `npx @north-light/sdk skill` in the project, then read the rewritten `.agents/skills/northlight/SKILL.md` (or `.claude/skills/northlight/SKILL.md`) instead of this copy. That command needs no account and no crtr. A project that has no SDK yet starts with `npx @north-light/sdk@latest init <dir>`.
14
+ This skill is for SDK version `0.3.492`. If the project has `node_modules/@north-light/sdk/package.json`, compare its `version` with that number. If they differ, run `npx @north-light/sdk skill` in the project, then read the rewritten `.agents/skills/northlight/SKILL.md` (or `.claude/skills/northlight/SKILL.md`) instead of this copy. That command needs no account and no crtr. A project that has no SDK yet starts with `npx @north-light/sdk@latest init <dir>`.
15
15
 
16
16
  ## Where things run
17
17
 
package/dist/version.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** This SDK's release version. Informational only: compatibility is the contract level, not this number. */
2
- export declare const SDK_VERSION = "0.3.490";
2
+ export declare const SDK_VERSION = "0.3.492";
3
3
  /** The release sent in `Crouter-Minimum-Runtime-Version` on every request. It is frozen, so a runtime from before the
4
4
  * contract level existed (which only knows this header) refuses this SDK, while every later runtime accepts it. */
5
5
  export declare const LEGACY_FLOOR = "0.3.423";
package/dist/version.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // SDK_VERSION is written by scripts/release-version.mjs on every release; do not edit by hand.
2
2
  /** This SDK's release version. Informational only: compatibility is the contract level, not this number. */
3
- export const SDK_VERSION = '0.3.490';
3
+ export const SDK_VERSION = '0.3.492';
4
4
  // LEGACY_FLOOR is written once by `scripts/contract.mjs release`, at the first release that carries the contract level
5
5
  // (the `0.0.0` placeholder is replaced there and never again); do not edit by hand.
6
6
  /** The release sent in `Crouter-Minimum-Runtime-Version` on every request. It is frozen, so a runtime from before the
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@north-light/sdk",
3
- "version": "0.3.490",
4
- "crouterContract": "1.12",
3
+ "version": "0.3.492",
4
+ "crouterContract": "1.13",
5
5
  "description": "Typed Node and browser client for running crouter agents through the crtrd /v1 API.",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",
@@ -44,8 +44,8 @@
44
44
  "prepublishOnly": "node -e \"if (process.env.npm_config_tag !== 'next') { console.error('@north-light/sdk publishes only with --tag next; scripts/promote-sdk.mjs is the only mover of latest.'); process.exit(1); }\""
45
45
  },
46
46
  "dependencies": {
47
- "@north-light/api": "0.3.490",
48
- "@north-light/identity": "0.3.490",
47
+ "@north-light/api": "0.3.492",
48
+ "@north-light/identity": "0.3.492",
49
49
  "jose": "^6.2.1"
50
50
  },
51
51
  "peerDependencies": {