@north-light/sdk 0.3.521 → 0.3.523
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/oauth/app-token.d.ts +15 -5
- package/dist/oauth/app-token.js +16 -8
- package/dist/oauth/index.d.ts +13 -0
- package/dist/oauth/index.js +18 -1
- package/dist/skill/northlight/SKILL.md +2 -2
- package/dist/testing/cloud.d.ts +1 -1
- package/dist/testing/cloud.js +5 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
|
@@ -10,16 +10,26 @@ export interface AppToken {
|
|
|
10
10
|
runtimeUrl: string;
|
|
11
11
|
expiresAt: number;
|
|
12
12
|
}
|
|
13
|
+
/** The app's own directory API token: what its `client_credentials` request with no `sub` answers. */
|
|
14
|
+
export interface DirectoryApiToken {
|
|
15
|
+
accessToken: string;
|
|
16
|
+
expiresAt: number;
|
|
17
|
+
}
|
|
13
18
|
/** Reads the directory's answer into an `AppToken`; `expiresAt` is the token's own `exp`, else `expires_in` from now. */
|
|
14
19
|
export declare function appTokenFrom(response: AppTokenResponse, now?: number): AppToken;
|
|
20
|
+
/** Reads the directory's answer to a `client_credentials` request with no `sub` into a `DirectoryApiToken`. */
|
|
21
|
+
export declare function directoryApiTokenFrom(response: AppTokenResponse, now?: number): DirectoryApiToken;
|
|
15
22
|
/** A token stays usable until this long before its expiry. */
|
|
16
23
|
export declare const APP_TOKEN_SKEW_MS = 60000;
|
|
17
|
-
/** One minted
|
|
18
|
-
export declare class AppTokenCache {
|
|
24
|
+
/** One minted token per key (a person's user id; the directory API token has one key), shared by every caller of that key (one mint in flight per key). */
|
|
25
|
+
export declare class AppTokenCache<T extends {
|
|
26
|
+
accessToken: string;
|
|
27
|
+
expiresAt: number;
|
|
28
|
+
} = AppToken> {
|
|
19
29
|
private readonly mint;
|
|
20
30
|
private readonly tokens;
|
|
21
31
|
private readonly minting;
|
|
22
|
-
constructor(mint: (userId: string) => Promise<
|
|
23
|
-
/** The cached token while it has more than a minute left, else a fresh mint. `rejected` is the token a
|
|
24
|
-
get(userId: string, rejected?: string): Promise<
|
|
32
|
+
constructor(mint: (userId: string) => Promise<T>);
|
|
33
|
+
/** The cached token while it has more than a minute left, else a fresh mint. `rejected` is the token a server just answered 401 to: it is never served again. */
|
|
34
|
+
get(userId: string, rejected?: string): Promise<T>;
|
|
25
35
|
}
|
package/dist/oauth/app-token.js
CHANGED
|
@@ -5,17 +5,25 @@ export function appTokenFrom(response, now = Date.now()) {
|
|
|
5
5
|
if (typeof response.access_token !== 'string' || !response.access_token || typeof response.runtimeUrl !== 'string' || !response.runtimeUrl) {
|
|
6
6
|
throw new CrouterError('Directory returned an incomplete app token (access_token and runtimeUrl are required)');
|
|
7
7
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
return { accessToken: response.access_token, runtimeUrl: response.runtimeUrl, expiresAt: expiryOf(response.access_token, response.expires_in, now) };
|
|
9
|
+
}
|
|
10
|
+
/** Reads the directory's answer to a `client_credentials` request with no `sub` into a `DirectoryApiToken`. */
|
|
11
|
+
export function directoryApiTokenFrom(response, now = Date.now()) {
|
|
12
|
+
if (typeof response.access_token !== 'string' || !response.access_token) {
|
|
13
|
+
throw new CrouterError('Directory returned an incomplete directory API token (access_token is required)');
|
|
14
|
+
}
|
|
15
|
+
return { accessToken: response.access_token, expiresAt: expiryOf(response.access_token, response.expires_in, now) };
|
|
16
|
+
}
|
|
17
|
+
function expiryOf(accessToken, expiresIn, now) {
|
|
18
|
+
const exp = decodeJwt(accessToken).exp;
|
|
19
|
+
const expiresAt = typeof exp === 'number' ? exp * 1_000 : typeof expiresIn === 'number' ? now + expiresIn * 1_000 : undefined;
|
|
12
20
|
if (expiresAt === undefined)
|
|
13
|
-
throw new CrouterError('Directory returned
|
|
14
|
-
return
|
|
21
|
+
throw new CrouterError('Directory returned a token with no expiry');
|
|
22
|
+
return expiresAt;
|
|
15
23
|
}
|
|
16
24
|
/** A token stays usable until this long before its expiry. */
|
|
17
25
|
export const APP_TOKEN_SKEW_MS = 60_000;
|
|
18
|
-
/** One minted
|
|
26
|
+
/** One minted token per key (a person's user id; the directory API token has one key), shared by every caller of that key (one mint in flight per key). */
|
|
19
27
|
export class AppTokenCache {
|
|
20
28
|
mint;
|
|
21
29
|
tokens = new Map();
|
|
@@ -23,7 +31,7 @@ export class AppTokenCache {
|
|
|
23
31
|
constructor(mint) {
|
|
24
32
|
this.mint = mint;
|
|
25
33
|
}
|
|
26
|
-
/** The cached token while it has more than a minute left, else a fresh mint. `rejected` is the token a
|
|
34
|
+
/** The cached token while it has more than a minute left, else a fresh mint. `rejected` is the token a server just answered 401 to: it is never served again. */
|
|
27
35
|
get(userId, rejected) {
|
|
28
36
|
const cached = this.tokens.get(userId);
|
|
29
37
|
if (cached && cached.accessToken !== rejected && cached.expiresAt - Date.now() > APP_TOKEN_SKEW_MS)
|
package/dist/oauth/index.d.ts
CHANGED
|
@@ -128,6 +128,7 @@ export declare class OAuth2Client {
|
|
|
128
128
|
private readonly timeoutMs;
|
|
129
129
|
private readonly store;
|
|
130
130
|
private readonly appTokens;
|
|
131
|
+
private readonly directoryApiTokens;
|
|
131
132
|
private jwks?;
|
|
132
133
|
private discoveryPromise?;
|
|
133
134
|
/** One refresh per (user, refresh token) in this process; a client holding a newer token never joins an older refusal. */
|
|
@@ -242,6 +243,18 @@ export declare class OAuth2Client {
|
|
|
242
243
|
*/
|
|
243
244
|
appClient(userId: string): Promise<ConnectedCrouter>;
|
|
244
245
|
private mintAppToken;
|
|
246
|
+
private mintDirectoryApiToken;
|
|
247
|
+
/**
|
|
248
|
+
* The app's own directory API token (`sub=app:<clientId>`, audience `<issuer>/v1`): the bearer for the directory's `/v1` routes
|
|
249
|
+
* called as the app itself, not for a person. The SDK signs a `client_credentials` request with the `OAuth2Client`'s key
|
|
250
|
+
* (`private_key_jwt`) and no `sub` (a `sub` would mint a person's runtime token, see `appClient`). The token is cached until a
|
|
251
|
+
* minute before it expires, and concurrent callers share one mint.
|
|
252
|
+
*
|
|
253
|
+
* Pass `rejected`, the token the directory just answered 401 to, to get a different one: the cached copy is dropped and the
|
|
254
|
+
* SDK mints again, so `const t = await oauth.directoryToken(); … 401 … oauth.directoryToken(t)` re-mints once. Errors are the
|
|
255
|
+
* token request's: `invalid_client` is a 401 `unauthorized` naming the key, a directory 5xx or a 429 has `retryable` true.
|
|
256
|
+
*/
|
|
257
|
+
directoryToken(rejected?: string): Promise<string>;
|
|
245
258
|
/**
|
|
246
259
|
* Follow the app's background runs until each comes to rest, and hand each ending to `onEnded` (see
|
|
247
260
|
* `RunWatchOptions`). Runs are opened with `client(userId)`, so refusals reach `onRefusal`. Call `start()`.
|
package/dist/oauth/index.js
CHANGED
|
@@ -6,7 +6,7 @@ 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
|
+
import { AppTokenCache, appTokenFrom, directoryApiTokenFrom } from './app-token.js';
|
|
10
10
|
import { attachRefusal, isRefusalCode, refusalKindOf } from './refusal.js';
|
|
11
11
|
export { authorizationRefusalOf, refusalOf } from './refusal.js';
|
|
12
12
|
/** A connection store for one process (local development and tests): its lock does not reach other processes. */
|
|
@@ -113,6 +113,7 @@ export class OAuth2Client {
|
|
|
113
113
|
timeoutMs;
|
|
114
114
|
store;
|
|
115
115
|
appTokens = new AppTokenCache((userId) => this.mintAppToken(userId));
|
|
116
|
+
directoryApiTokens = new AppTokenCache(() => this.mintDirectoryApiToken());
|
|
116
117
|
jwks;
|
|
117
118
|
discoveryPromise;
|
|
118
119
|
/** One refresh per (user, refresh token) in this process; a client holding a newer token never joins an older refusal. */
|
|
@@ -453,6 +454,22 @@ export class OAuth2Client {
|
|
|
453
454
|
async mintAppToken(userId) {
|
|
454
455
|
return appTokenFrom(await this.tokenResponse({ grant_type: 'client_credentials', sub: userId }));
|
|
455
456
|
}
|
|
457
|
+
async mintDirectoryApiToken() {
|
|
458
|
+
return directoryApiTokenFrom(await this.tokenResponse({ grant_type: 'client_credentials' }));
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* The app's own directory API token (`sub=app:<clientId>`, audience `<issuer>/v1`): the bearer for the directory's `/v1` routes
|
|
462
|
+
* called as the app itself, not for a person. The SDK signs a `client_credentials` request with the `OAuth2Client`'s key
|
|
463
|
+
* (`private_key_jwt`) and no `sub` (a `sub` would mint a person's runtime token, see `appClient`). The token is cached until a
|
|
464
|
+
* minute before it expires, and concurrent callers share one mint.
|
|
465
|
+
*
|
|
466
|
+
* Pass `rejected`, the token the directory just answered 401 to, to get a different one: the cached copy is dropped and the
|
|
467
|
+
* SDK mints again, so `const t = await oauth.directoryToken(); … 401 … oauth.directoryToken(t)` re-mints once. Errors are the
|
|
468
|
+
* token request's: `invalid_client` is a 401 `unauthorized` naming the key, a directory 5xx or a 429 has `retryable` true.
|
|
469
|
+
*/
|
|
470
|
+
directoryToken(rejected) {
|
|
471
|
+
return this.directoryApiTokens.get('directory', rejected).then((token) => token.accessToken);
|
|
472
|
+
}
|
|
456
473
|
/**
|
|
457
474
|
* Follow the app's background runs until each comes to rest, and hand each ending to `onEnded` (see
|
|
458
475
|
* `RunWatchOptions`). Runs are opened with `client(userId)`, so refusals reach `onRefusal`. Call `start()`.
|
|
@@ -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.
|
|
5
|
+
sdk-version: "0.3.523"
|
|
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.
|
|
14
|
+
This skill is for SDK version `0.3.523`. 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/testing/cloud.d.ts
CHANGED
|
@@ -279,7 +279,7 @@ export declare class FakeCloud {
|
|
|
279
279
|
private route;
|
|
280
280
|
private runIntercepts;
|
|
281
281
|
private directory;
|
|
282
|
-
/** `grant_type=client_credentials
|
|
282
|
+
/** `grant_type=client_credentials`: with `sub`, the directory's per-user runtime-token mint; without, the app's own directory API token. The client assertion is not verified. */
|
|
283
283
|
private mint;
|
|
284
284
|
private refresh;
|
|
285
285
|
/** `grant_type=authorization_code`: the code `authorize` issued, once. The client assertion and PKCE verifier are not verified. */
|
package/dist/testing/cloud.js
CHANGED
|
@@ -520,12 +520,14 @@ export class FakeCloud {
|
|
|
520
520
|
return this.exchange(params, call);
|
|
521
521
|
return credentials ? this.mint(params, call) : this.refresh(params, presented, call);
|
|
522
522
|
}
|
|
523
|
-
/** `grant_type=client_credentials
|
|
523
|
+
/** `grant_type=client_credentials`: with `sub`, the directory's per-user runtime-token mint; without, the app's own directory API token. The client assertion is not verified. */
|
|
524
524
|
mint(params, call) {
|
|
525
525
|
const sub = params.get('sub');
|
|
526
526
|
if (!sub) {
|
|
527
|
-
|
|
528
|
-
|
|
527
|
+
const now = Math.floor(this.clock.now() / 1000);
|
|
528
|
+
const clientId = params.get('client_id') ?? '';
|
|
529
|
+
const access_token = unsignedJwt({ sub: `app:${clientId}`, client_id: clientId, aud: `${this.issuer}/v1`, exp: now + 600, iat: now, serial: ++this.tokenSerial });
|
|
530
|
+
return Response.json({ access_token, token_type: 'Bearer', expires_in: 600 });
|
|
529
531
|
}
|
|
530
532
|
const user = this.users.get(sub);
|
|
531
533
|
const refused = (description, answer) => { call.answer = answer; return Response.json({ error: 'invalid_grant', error_description: description }, { status: 400 }); };
|
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.
|
|
2
|
+
export declare const SDK_VERSION = "0.3.523";
|
|
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.
|
|
3
|
+
export const SDK_VERSION = '0.3.523';
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/sdk",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.523",
|
|
4
4
|
"crouterContract": "1.25",
|
|
5
5
|
"description": "Typed Node and browser client for running crouter agents through the crtrd /v1 API.",
|
|
6
6
|
"type": "module",
|
|
@@ -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.
|
|
48
|
-
"@north-light/identity": "0.3.
|
|
47
|
+
"@north-light/api": "0.3.523",
|
|
48
|
+
"@north-light/identity": "0.3.523",
|
|
49
49
|
"jose": "^6.2.1"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|